changes
This commit is contained in:
@@ -84,6 +84,7 @@
|
||||
'calendar',
|
||||
'ai',
|
||||
'fusion_ringcentral',
|
||||
'fusion_tasks',
|
||||
],
|
||||
'external_dependencies': {
|
||||
'python': ['pdf2image', 'PIL'],
|
||||
@@ -128,6 +129,7 @@
|
||||
'wizard/odsp_pre_approved_wizard_views.xml',
|
||||
'wizard/odsp_ready_delivery_wizard_views.xml',
|
||||
'wizard/ltc_repair_create_so_wizard_views.xml',
|
||||
'wizard/send_page11_wizard_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/pdf_template_inherit_views.xml',
|
||||
'views/dashboard_views.xml',
|
||||
@@ -140,9 +142,8 @@
|
||||
'views/adp_claims_views.xml',
|
||||
'views/submission_history_views.xml',
|
||||
'views/fusion_loaner_views.xml',
|
||||
'views/page11_sign_request_views.xml',
|
||||
'views/technician_task_views.xml',
|
||||
'views/task_sync_views.xml',
|
||||
'views/technician_location_views.xml',
|
||||
'report/report_actions.xml',
|
||||
'report/report_templates.xml',
|
||||
'report/sale_report_portrait.xml',
|
||||
@@ -168,7 +169,6 @@
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'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/document_preview.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/google_address_autocomplete.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/debug_required_fields.js',
|
||||
'fusion_claims/static/src/xml/document_preview.xml',
|
||||
'fusion_claims/static/src/xml/fusion_task_map_view.xml',
|
||||
],
|
||||
},
|
||||
'images': ['static/description/icon.png'],
|
||||
|
||||
BIN
fusion_claims/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
fusion_claims/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
fusion_claims/__pycache__/__manifest__.cpython-312.pyc
Normal file
BIN
fusion_claims/__pycache__/__manifest__.cpython-312.pyc
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Server Action: Sync ADP Fields to Invoices -->
|
||||
<!-- 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>
|
||||
<data/>
|
||||
</odoo>
|
||||
|
||||
@@ -45,26 +45,6 @@
|
||||
<field name="value">True</field>
|
||||
</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) -->
|
||||
<record id="config_field_sale_type" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.field_sale_type</field>
|
||||
@@ -147,12 +127,6 @@
|
||||
<field name="value">1-888-222-5099</field>
|
||||
</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 -->
|
||||
<record id="config_ltc_form_password" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.ltc_form_password</field>
|
||||
|
||||
@@ -6,16 +6,6 @@
|
||||
-->
|
||||
<odoo>
|
||||
<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 -->
|
||||
<record id="ir_cron_renew_delivery_reminders" model="ir.cron">
|
||||
<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)"/>
|
||||
</record>
|
||||
|
||||
<!-- Cron Job: Calculate Travel Times for Technician Tasks -->
|
||||
<record id="ir_cron_technician_travel_times" model="ir.cron">
|
||||
<field name="name">Fusion Claims: Calculate Technician Travel Times</field>
|
||||
<field name="model_id" ref="model_fusion_technician_task"/>
|
||||
<!-- Cron Job: Expire Old Page 11 Signing Requests -->
|
||||
<record id="ir_cron_expire_page11_requests" model="ir.cron">
|
||||
<field name="name">Fusion Claims: Expire Page 11 Signing Requests</field>
|
||||
<field name="model_id" ref="model_fusion_page11_sign_request"/>
|
||||
<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_type">days</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>
|
||||
|
||||
<!-- 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>
|
||||
</odoo>
|
||||
|
||||
@@ -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="partner_to">{{ object.partner_id.id }}</field>
|
||||
<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="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;">
|
||||
<t t-out="object.company_id.name"/>
|
||||
</p>
|
||||
<h2 style="color:#1a202c;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;">
|
||||
Please find attached your quotation <strong style="color:#2d3748;"><t t-out="object.name"/></strong>.
|
||||
<h2 style="font-size:22px;font-weight:700;margin:0 0 6px 0;line-height:1.3;">ADP Quotation</h2>
|
||||
<p style="opacity:0.65;font-size:15px;line-height:1.5;margin:0 0 24px 0;">
|
||||
Please find attached your quotation <strong><t t-out="object.name"/></strong>.
|
||||
</p>
|
||||
<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 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;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 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;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;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">
|
||||
<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-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;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);">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;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>
|
||||
<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>
|
||||
<div style="padding:10px 14px;border:1px dashed #e2e8f0;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>
|
||||
<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;opacity:0.65;"><strong style="opacity:1;">Attached:</strong> ADP Quotation (PDF)</p>
|
||||
</div>
|
||||
<div style="border-left:3px solid #2B6CB0;padding:12px 16px;margin:0 0 24px 0;background:#f7fafc;">
|
||||
<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>
|
||||
<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;">Please review the attached quotation. If you have any questions or need assistance, do not hesitate to contact us.</p>
|
||||
</div>
|
||||
<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>
|
||||
@@ -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="partner_to">{{ object.partner_id.id }}</field>
|
||||
<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="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;">
|
||||
<t t-out="object.company_id.name"/>
|
||||
</p>
|
||||
<h2 style="color:#1a202c;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;">
|
||||
Your ADP sales order <strong style="color:#2d3748;"><t t-out="object.name"/></strong> has been confirmed.
|
||||
<h2 style="font-size:22px;font-weight:700;margin:0 0 6px 0;line-height:1.3;">Order Confirmed</h2>
|
||||
<p style="opacity:0.65;font-size:15px;line-height:1.5;margin:0 0 24px 0;">
|
||||
Your ADP sales order <strong><t t-out="object.name"/></strong> has been confirmed.
|
||||
</p>
|
||||
<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 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;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 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;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;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">
|
||||
<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-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;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);">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;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>
|
||||
<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>
|
||||
<div style="padding:10px 14px;border:1px dashed #e2e8f0;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>
|
||||
<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;opacity:0.65;"><strong style="opacity:1;">Attached:</strong> Sales Order Confirmation (PDF)</p>
|
||||
</div>
|
||||
<div style="border-left:3px solid #38a169;padding:12px 16px;margin:0 0 24px 0;background:#f7fafc;">
|
||||
<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>
|
||||
<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;">Your order is being processed. We will keep you updated on the delivery status and any updates from the Assistive Devices Program.</p>
|
||||
</div>
|
||||
<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>
|
||||
@@ -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="partner_to">{{ object.partner_id.id }}</field>
|
||||
<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="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;">
|
||||
<t t-out="object.company_id.name"/>
|
||||
</p>
|
||||
<h2 style="color:#1a202c;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;">
|
||||
Please find attached your invoice <strong style="color:#2d3748;"><t t-out="object.name or 'Draft'"/></strong>.
|
||||
<h2 style="font-size:22px;font-weight:700;margin:0 0 6px 0;line-height:1.3;">Invoice</h2>
|
||||
<p style="opacity:0.65;font-size:15px;line-height:1.5;margin:0 0 24px 0;">
|
||||
Please find attached your invoice <strong><t t-out="object.name or 'Draft'"/></strong>.
|
||||
</p>
|
||||
<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 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;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 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;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;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">
|
||||
<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-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 == 'adp'">ADP Portion</t>
|
||||
</td></tr>
|
||||
</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>
|
||||
<div style="padding:10px 14px;border:1px dashed #e2e8f0;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>
|
||||
<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;opacity:0.65;"><strong style="opacity:1;">Attached:</strong> Invoice (PDF)</p>
|
||||
</div>
|
||||
<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;">
|
||||
<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>
|
||||
<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;">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>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div style="border-left:3px solid #2B6CB0;padding:12px 16px;margin:0 0 24px 0;background:#f7fafc;">
|
||||
<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>
|
||||
<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;">Please review the attached invoice and process payment at your earliest convenience. Contact us if you have any questions.</p>
|
||||
</div>
|
||||
</t>
|
||||
<t t-set="sig" t-value="object.invoice_user_id.signature or object.user_id.signature"/>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
from . import email_builder_mixin
|
||||
from . import adp_posting_schedule
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
@@ -27,12 +26,9 @@ from . import client_chat
|
||||
from . import ai_agent_ext
|
||||
from . import dashboard
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
from . import technician_task
|
||||
from . import task_sync
|
||||
from . import technician_location
|
||||
from . import push_subscription
|
||||
from . import ltc_facility
|
||||
from . import ltc_repair
|
||||
from . import ltc_cleanup
|
||||
from . import ltc_form_submission
|
||||
from . import ltc_form_submission
|
||||
from . import page11_sign_request
|
||||
BIN
fusion_claims/models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
fusion_claims/models/__pycache__/account_move.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/account_move.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
fusion_claims/models/__pycache__/account_payment.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/account_payment.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
fusion_claims/models/__pycache__/ai_agent_ext.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/ai_agent_ext.cpython-312.pyc
Normal file
Binary file not shown.
BIN
fusion_claims/models/__pycache__/client_chat.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/client_chat.cpython-312.pyc
Normal file
Binary file not shown.
BIN
fusion_claims/models/__pycache__/client_profile.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/client_profile.cpython-312.pyc
Normal file
Binary file not shown.
BIN
fusion_claims/models/__pycache__/dashboard.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/dashboard.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
fusion_claims/models/__pycache__/ltc_cleanup.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/ltc_cleanup.cpython-312.pyc
Normal file
Binary file not shown.
BIN
fusion_claims/models/__pycache__/ltc_facility.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/ltc_facility.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
fusion_claims/models/__pycache__/ltc_repair.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/ltc_repair.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
fusion_claims/models/__pycache__/product_product.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/product_product.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
fusion_claims/models/__pycache__/res_company.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/res_company.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
fusion_claims/models/__pycache__/res_partner.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/res_partner.cpython-312.pyc
Normal file
Binary file not shown.
BIN
fusion_claims/models/__pycache__/sale_order.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/sale_order.cpython-312.pyc
Normal file
Binary file not shown.
BIN
fusion_claims/models/__pycache__/sale_order_line.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/sale_order_line.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
fusion_claims/models/__pycache__/task_sync.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/task_sync.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
fusion_claims/models/__pycache__/technician_task.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/technician_task.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
fusion_claims/models/__pycache__/xml_parser.cpython-312.pyc
Normal file
BIN
fusion_claims/models/__pycache__/xml_parser.cpython-312.pyc
Normal file
Binary file not shown.
@@ -1,242 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Fusion Claims - Professional Email Builder Mixin
|
||||
# Provides consistent, dark/light mode safe email templates across all modules.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class FusionEmailBuilderMixin(models.AbstractModel):
|
||||
_name = 'fusion.email.builder.mixin'
|
||||
_description = 'Fusion Email Builder Mixin'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Color constants
|
||||
# ------------------------------------------------------------------
|
||||
_EMAIL_COLORS = {
|
||||
'info': '#2B6CB0',
|
||||
'success': '#38a169',
|
||||
'attention': '#d69e2e',
|
||||
'urgent': '#c53030',
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _email_build(
|
||||
self,
|
||||
title,
|
||||
summary,
|
||||
sections=None,
|
||||
note=None,
|
||||
note_color=None,
|
||||
email_type='info',
|
||||
attachments_note=None,
|
||||
button_url=None,
|
||||
button_text='View Case Details',
|
||||
sender_name=None,
|
||||
extra_html='',
|
||||
):
|
||||
"""Build a complete professional email HTML string.
|
||||
|
||||
Args:
|
||||
title: Email heading (e.g. "Application Approved")
|
||||
summary: One-sentence summary HTML (may contain <strong> tags)
|
||||
sections: list of (heading, rows) where rows is list of (label, value)
|
||||
e.g. [('Case Details', [('Client', 'John'), ('Case', 'S30073')])]
|
||||
note: Optional note/next-steps text (plain or HTML)
|
||||
note_color: Override left-border color for note (default uses email_type)
|
||||
email_type: 'info' | 'success' | 'attention' | 'urgent'
|
||||
attachments_note: Optional string listing attached files
|
||||
button_url: Optional CTA button URL
|
||||
button_text: CTA button label
|
||||
sender_name: Name for sign-off (defaults to current user)
|
||||
extra_html: Any additional HTML to insert before sign-off
|
||||
"""
|
||||
accent = self._EMAIL_COLORS.get(email_type, self._EMAIL_COLORS['info'])
|
||||
company = self._get_company_info()
|
||||
|
||||
parts = []
|
||||
# -- Wrapper open + accent bar
|
||||
parts.append(
|
||||
f'<div style="font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,Arial,sans-serif;'
|
||||
f'max-width:600px;margin:0 auto;color:#2d3748;">'
|
||||
f'<div style="height:4px;background-color:{accent};"></div>'
|
||||
f'<div style="background:#ffffff;padding:32px 28px;border:1px solid #e2e8f0;border-top:none;">'
|
||||
)
|
||||
|
||||
# -- Company name
|
||||
parts.append(
|
||||
f'<p style="color:{accent};font-size:13px;font-weight:600;letter-spacing:0.5px;'
|
||||
f'text-transform:uppercase;margin:0 0 24px 0;">{company["name"]}</p>'
|
||||
)
|
||||
|
||||
# -- Title
|
||||
parts.append(
|
||||
f'<h2 style="color:#1a202c;font-size:22px;font-weight:700;'
|
||||
f'margin:0 0 6px 0;line-height:1.3;">{title}</h2>'
|
||||
)
|
||||
|
||||
# -- Summary
|
||||
parts.append(
|
||||
f'<p style="color:#718096;font-size:15px;line-height:1.5;'
|
||||
f'margin:0 0 24px 0;">{summary}</p>'
|
||||
)
|
||||
|
||||
# -- Sections (details tables)
|
||||
if sections:
|
||||
for heading, rows in sections:
|
||||
parts.append(self._email_section(heading, rows))
|
||||
|
||||
# -- Note / Next Steps
|
||||
if note:
|
||||
nc = note_color or accent
|
||||
parts.append(self._email_note(note, nc))
|
||||
|
||||
# -- Extra HTML
|
||||
if extra_html:
|
||||
parts.append(extra_html)
|
||||
|
||||
# -- Attachment note
|
||||
if attachments_note:
|
||||
parts.append(self._email_attachment_note(attachments_note))
|
||||
|
||||
# -- CTA Button
|
||||
if button_url:
|
||||
parts.append(self._email_button(button_url, button_text, accent))
|
||||
|
||||
# -- Sign-off
|
||||
signer = sender_name or (self.env.user.name if self.env.user else '')
|
||||
parts.append(
|
||||
f'<p style="color:#2d3748;font-size:14px;line-height:1.6;margin:24px 0 0 0;">'
|
||||
f'Best regards,<br/>'
|
||||
f'<strong>{signer}</strong><br/>'
|
||||
f'<span style="color:#718096;">{company["name"]}</span></p>'
|
||||
)
|
||||
|
||||
# -- Close content card
|
||||
parts.append('</div>')
|
||||
|
||||
# -- Footer
|
||||
footer_parts = [company['name']]
|
||||
if company['phone']:
|
||||
footer_parts.append(company['phone'])
|
||||
if company['email']:
|
||||
footer_parts.append(company['email'])
|
||||
footer_text = ' · '.join(footer_parts)
|
||||
|
||||
parts.append(
|
||||
f'<div style="padding:16px 28px;text-align:center;">'
|
||||
f'<p style="color:#a0aec0;font-size:11px;line-height:1.5;margin:0;">'
|
||||
f'{footer_text}<br/>'
|
||||
f'This is an automated notification from the ADP Claims Management System.</p>'
|
||||
f'</div>'
|
||||
)
|
||||
|
||||
# -- Close wrapper
|
||||
parts.append('</div>')
|
||||
|
||||
return ''.join(parts)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Building blocks
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _email_section(self, heading, rows):
|
||||
"""Build a labeled details table section.
|
||||
|
||||
Args:
|
||||
heading: Section title (e.g. "Case Details")
|
||||
rows: list of (label, value) tuples. Value can be plain text or HTML.
|
||||
"""
|
||||
if not rows:
|
||||
return ''
|
||||
|
||||
html = (
|
||||
'<table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;">'
|
||||
f'<tr><td colspan="2" style="padding:10px 14px;font-size:12px;font-weight:600;'
|
||||
f'color:#718096;text-transform:uppercase;letter-spacing:0.5px;'
|
||||
f'border-bottom:2px solid #e2e8f0;">{heading}</td></tr>'
|
||||
)
|
||||
|
||||
for label, value in rows:
|
||||
if value is None or value == '' or value is False:
|
||||
continue
|
||||
html += (
|
||||
f'<tr>'
|
||||
f'<td style="padding:10px 14px;color:#718096;font-size:14px;'
|
||||
f'border-bottom:1px solid #f0f0f0;width:35%;">{label}</td>'
|
||||
f'<td style="padding:10px 14px;color:#2d3748;font-size:14px;'
|
||||
f'border-bottom:1px solid #f0f0f0;">{value}</td>'
|
||||
f'</tr>'
|
||||
)
|
||||
|
||||
html += '</table>'
|
||||
return html
|
||||
|
||||
def _email_note(self, text, color='#2B6CB0'):
|
||||
"""Build a left-border accent note block."""
|
||||
return (
|
||||
f'<div style="border-left:3px solid {color};padding:12px 16px;'
|
||||
f'margin:0 0 24px 0;background:#f7fafc;">'
|
||||
f'<p style="margin:0;font-size:14px;line-height:1.5;color:#2d3748;">{text}</p>'
|
||||
f'</div>'
|
||||
)
|
||||
|
||||
def _email_button(self, url, text='View Case Details', color='#2B6CB0'):
|
||||
"""Build a centered CTA button."""
|
||||
return (
|
||||
f'<p style="text-align:center;margin:28px 0;">'
|
||||
f'<a href="{url}" style="display:inline-block;background:{color};color:#ffffff;'
|
||||
f'padding:12px 28px;text-decoration:none;border-radius:6px;'
|
||||
f'font-size:14px;font-weight:600;">{text}</a></p>'
|
||||
)
|
||||
|
||||
def _email_attachment_note(self, description):
|
||||
"""Build a dashed-border attachment callout.
|
||||
|
||||
Args:
|
||||
description: e.g. "ADP Application (PDF), XML Data File"
|
||||
"""
|
||||
return (
|
||||
f'<div style="padding:10px 14px;border:1px dashed #e2e8f0;border-radius:6px;'
|
||||
f'margin:0 0 24px 0;">'
|
||||
f'<p style="margin:0;font-size:13px;color:#718096;">'
|
||||
f'<strong style="color:#2d3748;">Attached:</strong> {description}</p>'
|
||||
f'</div>'
|
||||
)
|
||||
|
||||
def _email_status_badge(self, label, color='#2B6CB0'):
|
||||
"""Return an inline status badge/pill HTML snippet."""
|
||||
# Pick a light background tint for the badge
|
||||
bg_map = {
|
||||
'#38a169': '#f0fff4',
|
||||
'#2B6CB0': '#ebf4ff',
|
||||
'#d69e2e': '#fefcbf',
|
||||
'#c53030': '#fff5f5',
|
||||
}
|
||||
bg = bg_map.get(color, '#ebf4ff')
|
||||
return (
|
||||
f'<span style="display:inline-block;background:{bg};color:{color};'
|
||||
f'padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600;">'
|
||||
f'{label}</span>'
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_company_info(self):
|
||||
"""Return company name, phone, email for email templates."""
|
||||
company = getattr(self, 'company_id', None) or self.env.company
|
||||
return {
|
||||
'name': company.name or 'Our Company',
|
||||
'phone': company.phone or '',
|
||||
'email': company.email or '',
|
||||
}
|
||||
|
||||
def _email_is_enabled(self):
|
||||
"""Check if email notifications are enabled in settings."""
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
val = ICP.get_param('fusion_claims.enable_email_notifications', 'True')
|
||||
return val.lower() in ('true', '1', 'yes')
|
||||
@@ -57,6 +57,12 @@ class FusionADPDeviceCode(models.Model):
|
||||
index=True,
|
||||
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(
|
||||
string='Device Description',
|
||||
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', ''))
|
||||
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', ''))
|
||||
|
||||
# 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
|
||||
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(),
|
||||
'active': True,
|
||||
}
|
||||
if build_type:
|
||||
vals['build_type'] = build_type
|
||||
|
||||
if existing:
|
||||
existing.write(vals)
|
||||
|
||||
389
fusion_claims/models/page11_sign_request.py
Normal file
389
fusion_claims/models/page11_sign_request.py
Normal 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))
|
||||
@@ -1,73 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
"""
|
||||
Web Push Subscription model for storing browser push notification subscriptions.
|
||||
"""
|
||||
|
||||
from odoo import models, fields, api
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionPushSubscription(models.Model):
|
||||
_name = 'fusion.push.subscription'
|
||||
_description = 'Web Push Subscription'
|
||||
_order = 'create_date desc'
|
||||
|
||||
user_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='User',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
index=True,
|
||||
)
|
||||
endpoint = fields.Text(
|
||||
string='Endpoint URL',
|
||||
required=True,
|
||||
)
|
||||
p256dh_key = fields.Text(
|
||||
string='P256DH Key',
|
||||
required=True,
|
||||
)
|
||||
auth_key = fields.Text(
|
||||
string='Auth Key',
|
||||
required=True,
|
||||
)
|
||||
browser_info = fields.Char(
|
||||
string='Browser Info',
|
||||
help='User agent or browser identification',
|
||||
)
|
||||
active = fields.Boolean(
|
||||
default=True,
|
||||
)
|
||||
|
||||
_constraints = [
|
||||
models.Constraint(
|
||||
'unique(endpoint)',
|
||||
'This push subscription endpoint already exists.',
|
||||
),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def register_subscription(self, user_id, endpoint, p256dh_key, auth_key, browser_info=None):
|
||||
"""Register or update a push subscription."""
|
||||
existing = self.sudo().search([('endpoint', '=', endpoint)], limit=1)
|
||||
if existing:
|
||||
existing.write({
|
||||
'user_id': user_id,
|
||||
'p256dh_key': p256dh_key,
|
||||
'auth_key': auth_key,
|
||||
'browser_info': browser_info or existing.browser_info,
|
||||
'active': True,
|
||||
})
|
||||
return existing
|
||||
return self.sudo().create({
|
||||
'user_id': user_id,
|
||||
'endpoint': endpoint,
|
||||
'p256dh_key': p256dh_key,
|
||||
'auth_key': auth_key,
|
||||
'browser_info': browser_info,
|
||||
})
|
||||
@@ -317,16 +317,6 @@ class ResConfigSettings(models.TransientModel):
|
||||
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
|
||||
# ------------------------------------------------------------------
|
||||
@@ -349,62 +339,6 @@ class ResConfigSettings(models.TransientModel):
|
||||
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
|
||||
# ------------------------------------------------------------------
|
||||
@@ -609,15 +543,11 @@ class ResConfigSettings(models.TransientModel):
|
||||
# an existing non-empty value (e.g. API keys, user-customized settings).
|
||||
_protected_keys = [
|
||||
'fusion_claims.ai_api_key',
|
||||
'fusion_claims.google_maps_api_key',
|
||||
'fusion_claims.vendor_code',
|
||||
'fusion_claims.ai_model',
|
||||
'fusion_claims.adp_posting_base_date',
|
||||
'fusion_claims.application_reminder_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
|
||||
_existing = {}
|
||||
|
||||
@@ -2,82 +2,12 @@
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import logging
|
||||
import requests
|
||||
from odoo import models, fields, api
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_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
|
||||
# ==========================================================================
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
x_fc_is_field_staff = fields.Boolean(
|
||||
string='Field Staff',
|
||||
default=False,
|
||||
help='Check this to show the user in the Technician/Field Staff dropdown when scheduling tasks.',
|
||||
)
|
||||
x_fc_start_address = fields.Char(
|
||||
related='partner_id.x_fc_start_address',
|
||||
readonly=False,
|
||||
string='Start Location',
|
||||
)
|
||||
x_fc_tech_sync_id = fields.Char(
|
||||
string='Tech Sync ID',
|
||||
help='Shared identifier for this technician across Odoo instances. '
|
||||
'Must be the same value on all instances for the same person.',
|
||||
copy=False,
|
||||
)
|
||||
@@ -1862,6 +1862,10 @@ class SaleOrder(models.Model):
|
||||
string='Previous Status Before Hold',
|
||||
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(
|
||||
string='Status Before Delivery',
|
||||
@@ -2327,6 +2331,20 @@ class SaleOrder(models.Model):
|
||||
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 must be signed by: Authorizer (OT) and Vendor (our company)
|
||||
@@ -3120,11 +3138,49 @@ class SaleOrder(models.Model):
|
||||
self.ensure_one()
|
||||
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):
|
||||
"""Open the Page 11 & 12 PDF."""
|
||||
self.ensure_one()
|
||||
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):
|
||||
"""Open the Final Submitted Application PDF."""
|
||||
self.ensure_one()
|
||||
@@ -3686,6 +3742,41 @@ class SaleOrder(models.Model):
|
||||
|
||||
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):
|
||||
"""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:
|
||||
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)
|
||||
if 'x_fc_adp_portion' in self.env['account.move.line']._fields:
|
||||
line_vals['x_fc_adp_portion'] = adp_portion
|
||||
@@ -5170,13 +5267,13 @@ class SaleOrder(models.Model):
|
||||
f'border-bottom:2px solid #4a5568;{font}"'
|
||||
)
|
||||
cell_style = (
|
||||
'style="padding:7px 10px;font-size:12px;color:#2d3748;'
|
||||
'border-bottom:1px solid #e2e8f0;"'
|
||||
'style="padding:7px 10px;font-size:12px;'
|
||||
'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 = (
|
||||
'style="padding:7px 10px;font-size:12px;color:#2d3748;'
|
||||
'border-bottom:1px solid #e2e8f0;text-align:right;"'
|
||||
'style="padding:7px 10px;font-size:12px;'
|
||||
'border-bottom:1px solid rgba(128,128,128,0.15);text-align:right;"'
|
||||
)
|
||||
hdr_r = hdr_style.replace('text-align:left', 'text-align:right')
|
||||
|
||||
@@ -5187,9 +5284,9 @@ class SaleOrder(models.Model):
|
||||
|
||||
html = (
|
||||
'<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>'
|
||||
'<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>'
|
||||
f'<th {hdr_style}>S/N</th>'
|
||||
f'<th {hdr_style}>ADP Code</th>'
|
||||
@@ -5241,13 +5338,13 @@ class SaleOrder(models.Model):
|
||||
colspan = 5
|
||||
total_style = (
|
||||
'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 = (
|
||||
f'style="padding:8px 10px;font-size:12px;font-weight:700;'
|
||||
f'color:#1a202c;border-top:2px solid #2d3748;text-align:right;"'
|
||||
'style="padding:8px 10px;font-size:12px;font-weight:700;'
|
||||
'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 {total_style}>${total_adp:,.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}")
|
||||
return False
|
||||
|
||||
def _send_withdrawal_email(self, reason=None):
|
||||
"""Send notification when application is withdrawn."""
|
||||
def _send_withdrawal_email(self, reason=None, intent=None):
|
||||
"""Send notification when application is withdrawn.
|
||||
|
||||
Args:
|
||||
reason: Free-text reason for withdrawal.
|
||||
intent: 'cancel' or 'resubmit' — determines email wording.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self._is_email_notifications_enabled():
|
||||
return False
|
||||
@@ -5542,17 +5644,34 @@ class SaleOrder(models.Model):
|
||||
client_name = (recipients.get('client') or self.partner_id).name or 'Client'
|
||||
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:
|
||||
note_text += f'<br/><strong>Reason:</strong> {reason}'
|
||||
|
||||
body_html = self._email_build(
|
||||
title='Application Withdrawn',
|
||||
title=title,
|
||||
summary=f'The ADP application for <strong>{client_name}</strong> has been withdrawn.',
|
||||
email_type='attention',
|
||||
sections=[('Case Details', self._build_case_detail_rows())],
|
||||
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',
|
||||
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:])
|
||||
try:
|
||||
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,
|
||||
'email_to': email_to, 'email_cc': email_cc,
|
||||
'model': 'sale.order', 'res_id': self.id,
|
||||
}).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
|
||||
except Exception as 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_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
|
||||
# This ensures document history is maintained for audit purposes
|
||||
@@ -5885,7 +6007,7 @@ class SaleOrder(models.Model):
|
||||
|
||||
for order in self:
|
||||
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)
|
||||
new_data = vals.get(field_name)
|
||||
label = document_labels.get(field_name, field_name)
|
||||
@@ -6584,96 +6706,6 @@ class SaleOrder(models.Model):
|
||||
except Exception as 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)
|
||||
# ==========================================================================
|
||||
|
||||
@@ -1,660 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
"""
|
||||
Cross-instance technician task sync.
|
||||
|
||||
Enables two Odoo instances (e.g. Westin and Mobility) that share the same
|
||||
field technicians to see each other's delivery tasks, preventing double-booking.
|
||||
|
||||
Remote tasks appear as read-only "shadow" records in the local calendar.
|
||||
The existing _find_next_available_slot() automatically sees shadow tasks,
|
||||
so collision detection works without changes to the scheduling algorithm.
|
||||
|
||||
Technicians are matched across instances using the x_fc_tech_sync_id field
|
||||
on res.users. Set the same value (e.g. "gordy") on both instances for the
|
||||
same person -- no mapping table needed.
|
||||
"""
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
import requests
|
||||
from datetime import timedelta
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
SYNC_TASK_FIELDS = [
|
||||
'x_fc_sync_uuid', 'name', 'technician_id', 'additional_technician_ids',
|
||||
'task_type', 'status',
|
||||
'scheduled_date', 'time_start', 'time_end', 'duration_hours',
|
||||
'address_street', 'address_street2', 'address_city', 'address_zip',
|
||||
'address_state_id', 'address_buzz_code',
|
||||
'address_lat', 'address_lng', 'priority', 'partner_id', 'partner_phone',
|
||||
'pod_required', 'description',
|
||||
]
|
||||
|
||||
TERMINAL_STATUSES = ('completed', 'cancelled')
|
||||
|
||||
|
||||
class FusionTaskSyncConfig(models.Model):
|
||||
_name = 'fusion.task.sync.config'
|
||||
_description = 'Task Sync Remote Instance'
|
||||
|
||||
name = fields.Char('Instance Name', required=True,
|
||||
help='e.g. Westin Healthcare, Mobility Specialties')
|
||||
instance_id = fields.Char('Instance ID', required=True,
|
||||
help='Short identifier, e.g. westin or mobility')
|
||||
url = fields.Char('Odoo URL', required=True,
|
||||
help='e.g. http://192.168.1.40:8069')
|
||||
database = fields.Char('Database', required=True)
|
||||
username = fields.Char('API Username', required=True)
|
||||
api_key = fields.Char('API Key', required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
last_sync = fields.Datetime('Last Successful Sync', readonly=True)
|
||||
last_sync_error = fields.Text('Last Error', readonly=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JSON-RPC helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _jsonrpc(self, service, method, args):
|
||||
"""Execute a JSON-RPC call against the remote Odoo instance."""
|
||||
self.ensure_one()
|
||||
url = f"{self.url.rstrip('/')}/jsonrpc"
|
||||
payload = {
|
||||
'jsonrpc': '2.0',
|
||||
'method': 'call',
|
||||
'id': 1,
|
||||
'params': {
|
||||
'service': service,
|
||||
'method': method,
|
||||
'args': args,
|
||||
},
|
||||
}
|
||||
try:
|
||||
resp = requests.post(url, json=payload, timeout=15)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
if result.get('error'):
|
||||
err = result['error'].get('data', {}).get('message', str(result['error']))
|
||||
raise UserError(f"Remote error: {err}")
|
||||
return result.get('result')
|
||||
except requests.exceptions.ConnectionError:
|
||||
_logger.warning("Task sync: cannot connect to %s", self.url)
|
||||
return None
|
||||
except requests.exceptions.Timeout:
|
||||
_logger.warning("Task sync: timeout connecting to %s", self.url)
|
||||
return None
|
||||
|
||||
def _authenticate(self):
|
||||
"""Authenticate with the remote instance and return the uid."""
|
||||
self.ensure_one()
|
||||
uid = self._jsonrpc('common', 'authenticate',
|
||||
[self.database, self.username, self.api_key, {}])
|
||||
if not uid:
|
||||
_logger.error("Task sync: authentication failed for %s", self.name)
|
||||
return uid
|
||||
|
||||
def _rpc(self, model, method, args, kwargs=None):
|
||||
"""Execute a method on the remote instance via execute_kw.
|
||||
execute_kw(db, uid, password, model, method, [args], {kwargs})
|
||||
"""
|
||||
self.ensure_one()
|
||||
uid = self._authenticate()
|
||||
if not uid:
|
||||
return None
|
||||
call_args = [self.database, uid, self.api_key, model, method, args]
|
||||
if kwargs:
|
||||
call_args.append(kwargs)
|
||||
return self._jsonrpc('object', 'execute_kw', call_args)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tech sync ID helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_local_tech_map(self):
|
||||
"""Build {local_user_id: x_fc_tech_sync_id} for all local field staff."""
|
||||
techs = self.env['res.users'].sudo().search([
|
||||
('x_fc_is_field_staff', '=', True),
|
||||
('x_fc_tech_sync_id', '!=', False),
|
||||
('active', '=', True),
|
||||
])
|
||||
return {u.id: u.x_fc_tech_sync_id for u in techs}
|
||||
|
||||
def _get_remote_tech_map(self):
|
||||
"""Build {x_fc_tech_sync_id: remote_user_id} from the remote instance."""
|
||||
self.ensure_one()
|
||||
remote_users = self._rpc('res.users', 'search_read', [
|
||||
[('x_fc_is_field_staff', '=', True),
|
||||
('x_fc_tech_sync_id', '!=', False),
|
||||
('active', '=', True)],
|
||||
], {'fields': ['id', 'x_fc_tech_sync_id']})
|
||||
if not remote_users:
|
||||
return {}
|
||||
return {
|
||||
ru['x_fc_tech_sync_id']: ru['id']
|
||||
for ru in remote_users
|
||||
if ru.get('x_fc_tech_sync_id')
|
||||
}
|
||||
|
||||
def _get_local_syncid_to_uid(self):
|
||||
"""Build {x_fc_tech_sync_id: local_user_id} for local field staff."""
|
||||
techs = self.env['res.users'].sudo().search([
|
||||
('x_fc_is_field_staff', '=', True),
|
||||
('x_fc_tech_sync_id', '!=', False),
|
||||
('active', '=', True),
|
||||
])
|
||||
return {u.x_fc_tech_sync_id: u.id for u in techs}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Connection test
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def action_test_connection(self):
|
||||
"""Test the connection to the remote instance."""
|
||||
self.ensure_one()
|
||||
uid = self._authenticate()
|
||||
if uid:
|
||||
remote_map = self._get_remote_tech_map()
|
||||
local_map = self._get_local_tech_map()
|
||||
matched = set(local_map.values()) & set(remote_map.keys())
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Connection Successful',
|
||||
'message': f'Connected to {self.name}. '
|
||||
f'{len(matched)} technician(s) matched by sync ID.',
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
raise UserError(f"Cannot connect to {self.name}. Check URL, database, and API key.")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PUSH: send local task changes to remote instance
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_local_instance_id(self):
|
||||
"""Return this instance's own ID from config parameters."""
|
||||
return self.env['ir.config_parameter'].sudo().get_param(
|
||||
'fusion_claims.sync_instance_id', '')
|
||||
|
||||
@api.model
|
||||
def _push_tasks(self, tasks, operation='create'):
|
||||
"""Push local task changes to all active remote instances.
|
||||
Called from technician_task create/write overrides.
|
||||
Non-blocking: errors are logged, not raised.
|
||||
"""
|
||||
configs = self.sudo().search([('active', '=', True)])
|
||||
if not configs:
|
||||
return
|
||||
local_id = configs[0]._get_local_instance_id()
|
||||
if not local_id:
|
||||
return
|
||||
for config in configs:
|
||||
try:
|
||||
config._push_tasks_to_remote(tasks, operation, local_id)
|
||||
except Exception:
|
||||
_logger.exception("Task sync push to %s failed", config.name)
|
||||
|
||||
def _push_tasks_to_remote(self, tasks, operation, local_instance_id):
|
||||
"""Push task data to a single remote instance.
|
||||
|
||||
Maps additional_technician_ids via sync IDs so the remote instance
|
||||
also blocks those technicians' schedules.
|
||||
"""
|
||||
self.ensure_one()
|
||||
local_map = self._get_local_tech_map()
|
||||
remote_map = self._get_remote_tech_map()
|
||||
if not local_map or not remote_map:
|
||||
return
|
||||
|
||||
ctx = {'context': {'skip_task_sync': True, 'skip_travel_recalc': True}}
|
||||
|
||||
for task in tasks:
|
||||
sync_id = local_map.get(task.technician_id.id)
|
||||
if not sync_id:
|
||||
continue
|
||||
remote_tech_uid = remote_map.get(sync_id)
|
||||
if not remote_tech_uid:
|
||||
continue
|
||||
|
||||
# Map additional technicians to remote user IDs
|
||||
remote_additional_ids = []
|
||||
for tech in task.additional_technician_ids:
|
||||
add_sync_id = local_map.get(tech.id)
|
||||
if add_sync_id:
|
||||
remote_add_uid = remote_map.get(add_sync_id)
|
||||
if remote_add_uid:
|
||||
remote_additional_ids.append(remote_add_uid)
|
||||
|
||||
task_data = {
|
||||
'x_fc_sync_uuid': task.x_fc_sync_uuid,
|
||||
'x_fc_sync_source': local_instance_id,
|
||||
'x_fc_sync_remote_id': task.id,
|
||||
'name': f"[{local_instance_id.upper()}] {task.name}",
|
||||
'technician_id': remote_tech_uid,
|
||||
'additional_technician_ids': [(6, 0, remote_additional_ids)],
|
||||
'task_type': task.task_type,
|
||||
'status': task.status,
|
||||
'scheduled_date': str(task.scheduled_date) if task.scheduled_date else False,
|
||||
'time_start': task.time_start,
|
||||
'time_end': task.time_end,
|
||||
'duration_hours': task.duration_hours,
|
||||
'address_street': task.address_street or '',
|
||||
'address_street2': task.address_street2 or '',
|
||||
'address_city': task.address_city or '',
|
||||
'address_zip': task.address_zip or '',
|
||||
'address_lat': float(task.address_lat or 0),
|
||||
'address_lng': float(task.address_lng or 0),
|
||||
'priority': task.priority or 'normal',
|
||||
'x_fc_sync_client_name': task.partner_id.name if task.partner_id else '',
|
||||
}
|
||||
|
||||
existing = self._rpc(
|
||||
'fusion.technician.task', 'search',
|
||||
[[('x_fc_sync_uuid', '=', task.x_fc_sync_uuid)]],
|
||||
{'limit': 1})
|
||||
|
||||
if operation in ('create', 'write'):
|
||||
if existing:
|
||||
self._rpc('fusion.technician.task', 'write',
|
||||
[existing, task_data], ctx)
|
||||
elif operation == 'create':
|
||||
task_data['sale_order_id'] = False
|
||||
self._rpc('fusion.technician.task', 'create',
|
||||
[[task_data]], ctx)
|
||||
|
||||
elif operation == 'unlink' and existing:
|
||||
self._rpc('fusion.technician.task', 'write',
|
||||
[existing, {'status': 'cancelled', 'active': False}], ctx)
|
||||
|
||||
@api.model
|
||||
def _push_shadow_status(self, shadow_tasks):
|
||||
"""Push local status changes on shadow tasks back to their source instance.
|
||||
|
||||
When a tech completes (or cancels) a shadow task locally, update the
|
||||
original task on the remote instance so both sides stay in sync.
|
||||
"""
|
||||
configs = self.sudo().search([('active', '=', True)])
|
||||
config_by_instance = {c.instance_id: c for c in configs}
|
||||
ctx = {'context': {'skip_task_sync': True, 'skip_travel_recalc': True}}
|
||||
|
||||
for task in shadow_tasks:
|
||||
config = config_by_instance.get(task.x_fc_sync_source)
|
||||
if not config or not task.x_fc_sync_remote_id:
|
||||
continue
|
||||
try:
|
||||
update_vals = {'status': task.status}
|
||||
if task.status == 'completed' and task.completion_datetime:
|
||||
update_vals['completion_datetime'] = str(task.completion_datetime)
|
||||
config._rpc(
|
||||
'fusion.technician.task', 'write',
|
||||
[[task.x_fc_sync_remote_id], update_vals], ctx)
|
||||
_logger.info(
|
||||
"Pushed status '%s' for shadow task %s back to %s (remote id %d)",
|
||||
task.status, task.name, config.name, task.x_fc_sync_remote_id)
|
||||
if task.status == 'completed':
|
||||
try:
|
||||
config._rpc(
|
||||
'fusion.technician.task',
|
||||
'_notify_scheduler_on_completion',
|
||||
[[task.x_fc_sync_remote_id]])
|
||||
except Exception:
|
||||
_logger.warning(
|
||||
"Could not trigger completion notification on remote for %s",
|
||||
task.name)
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
"Failed to push status for shadow task %s to %s",
|
||||
task.name, config.name)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PULL: cron-based full reconciliation
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _cron_pull_remote_tasks(self):
|
||||
"""Cron job: pull tasks and technician locations from all active remote instances."""
|
||||
configs = self.sudo().search([('active', '=', True)])
|
||||
for config in configs:
|
||||
try:
|
||||
config._pull_tasks_from_remote()
|
||||
config._pull_technician_locations()
|
||||
config.sudo().write({
|
||||
'last_sync': fields.Datetime.now(),
|
||||
'last_sync_error': False,
|
||||
})
|
||||
except Exception as e:
|
||||
_logger.exception("Task sync pull from %s failed", config.name)
|
||||
config.sudo().write({'last_sync_error': str(e)})
|
||||
|
||||
def _pull_tasks_from_remote(self):
|
||||
"""Pull all active tasks for matched technicians from the remote instance.
|
||||
|
||||
After syncing, recalculates travel chains for all affected tech+date
|
||||
combos so route planning accounts for both local and shadow tasks.
|
||||
"""
|
||||
self.ensure_one()
|
||||
local_syncid_to_uid = self._get_local_syncid_to_uid()
|
||||
if not local_syncid_to_uid:
|
||||
return
|
||||
|
||||
remote_map = self._get_remote_tech_map()
|
||||
if not remote_map:
|
||||
return
|
||||
|
||||
matched_sync_ids = set(local_syncid_to_uid.keys()) & set(remote_map.keys())
|
||||
if not matched_sync_ids:
|
||||
_logger.info("Task sync: no matched technicians between local and %s", self.name)
|
||||
return
|
||||
|
||||
remote_tech_ids = [remote_map[sid] for sid in matched_sync_ids]
|
||||
remote_syncid_by_uid = {v: k for k, v in remote_map.items()}
|
||||
|
||||
cutoff = fields.Date.today() - timedelta(days=7)
|
||||
remote_tasks = self._rpc(
|
||||
'fusion.technician.task', 'search_read',
|
||||
[[
|
||||
'|',
|
||||
('technician_id', 'in', remote_tech_ids),
|
||||
('additional_technician_ids', 'in', remote_tech_ids),
|
||||
('scheduled_date', '>=', str(cutoff)),
|
||||
('x_fc_sync_source', '=', False),
|
||||
]],
|
||||
{'fields': SYNC_TASK_FIELDS + ['id']})
|
||||
|
||||
if remote_tasks is None:
|
||||
return
|
||||
|
||||
Task = self.env['fusion.technician.task'].sudo().with_context(
|
||||
skip_task_sync=True, skip_travel_recalc=True)
|
||||
|
||||
remote_uuids = set()
|
||||
affected_combos = set()
|
||||
|
||||
for rt in remote_tasks:
|
||||
sync_uuid = rt.get('x_fc_sync_uuid')
|
||||
if not sync_uuid:
|
||||
continue
|
||||
remote_uuids.add(sync_uuid)
|
||||
|
||||
remote_tech_raw = rt['technician_id']
|
||||
remote_uid = remote_tech_raw[0] if isinstance(remote_tech_raw, (list, tuple)) else remote_tech_raw
|
||||
tech_sync_id = remote_syncid_by_uid.get(remote_uid)
|
||||
local_uid = local_syncid_to_uid.get(tech_sync_id) if tech_sync_id else None
|
||||
if not local_uid:
|
||||
continue
|
||||
|
||||
partner_raw = rt.get('partner_id')
|
||||
client_name = partner_raw[1] if isinstance(partner_raw, (list, tuple)) and len(partner_raw) > 1 else ''
|
||||
client_phone = rt.get('partner_phone', '') or ''
|
||||
|
||||
state_raw = rt.get('address_state_id')
|
||||
state_name = ''
|
||||
if isinstance(state_raw, (list, tuple)) and len(state_raw) > 1:
|
||||
state_name = state_raw[1]
|
||||
|
||||
# Map additional technicians from remote to local
|
||||
local_additional_ids = []
|
||||
remote_add_raw = rt.get('additional_technician_ids', [])
|
||||
if remote_add_raw and isinstance(remote_add_raw, list):
|
||||
for add_uid in remote_add_raw:
|
||||
add_sync_id = remote_syncid_by_uid.get(add_uid)
|
||||
if add_sync_id:
|
||||
local_add_uid = local_syncid_to_uid.get(add_sync_id)
|
||||
if local_add_uid:
|
||||
local_additional_ids.append(local_add_uid)
|
||||
|
||||
sched_date = rt.get('scheduled_date')
|
||||
|
||||
vals = {
|
||||
'x_fc_sync_uuid': sync_uuid,
|
||||
'x_fc_sync_source': self.instance_id,
|
||||
'x_fc_sync_remote_id': rt['id'],
|
||||
'name': f"[{self.instance_id.upper()}] {rt.get('name', '')}",
|
||||
'technician_id': local_uid,
|
||||
'additional_technician_ids': [(6, 0, local_additional_ids)],
|
||||
'task_type': rt.get('task_type', 'delivery'),
|
||||
'status': rt.get('status', 'scheduled'),
|
||||
'scheduled_date': sched_date,
|
||||
'time_start': rt.get('time_start', 9.0),
|
||||
'time_end': rt.get('time_end', 10.0),
|
||||
'duration_hours': rt.get('duration_hours', 1.0),
|
||||
'address_street': rt.get('address_street', ''),
|
||||
'address_street2': rt.get('address_street2', ''),
|
||||
'address_city': rt.get('address_city', ''),
|
||||
'address_zip': rt.get('address_zip', ''),
|
||||
'address_buzz_code': rt.get('address_buzz_code', ''),
|
||||
'address_lat': rt.get('address_lat', 0),
|
||||
'address_lng': rt.get('address_lng', 0),
|
||||
'priority': rt.get('priority', 'normal'),
|
||||
'pod_required': rt.get('pod_required', False),
|
||||
'description': rt.get('description', ''),
|
||||
'x_fc_sync_client_name': client_name,
|
||||
'x_fc_sync_client_phone': client_phone,
|
||||
}
|
||||
|
||||
if state_name:
|
||||
state_rec = self.env['res.country.state'].sudo().search(
|
||||
[('name', '=', state_name)], limit=1)
|
||||
if state_rec:
|
||||
vals['address_state_id'] = state_rec.id
|
||||
|
||||
existing = Task.search([('x_fc_sync_uuid', '=', sync_uuid)], limit=1)
|
||||
if existing:
|
||||
if existing.status in TERMINAL_STATUSES:
|
||||
vals.pop('status', None)
|
||||
existing.write(vals)
|
||||
else:
|
||||
vals['sale_order_id'] = False
|
||||
Task.create([vals])
|
||||
|
||||
if sched_date:
|
||||
affected_combos.add((local_uid, sched_date))
|
||||
for add_uid in local_additional_ids:
|
||||
affected_combos.add((add_uid, sched_date))
|
||||
|
||||
stale_shadows = Task.search([
|
||||
('x_fc_sync_source', '=', self.instance_id),
|
||||
('x_fc_sync_uuid', 'not in', list(remote_uuids)),
|
||||
('scheduled_date', '>=', str(cutoff)),
|
||||
('active', '=', True),
|
||||
])
|
||||
if stale_shadows:
|
||||
for st in stale_shadows:
|
||||
if st.scheduled_date and st.technician_id:
|
||||
affected_combos.add((st.technician_id.id, st.scheduled_date))
|
||||
for tech in st.additional_technician_ids:
|
||||
if st.scheduled_date:
|
||||
affected_combos.add((tech.id, st.scheduled_date))
|
||||
stale_shadows.write({'active': False, 'status': 'cancelled'})
|
||||
_logger.info("Deactivated %d stale shadow tasks from %s",
|
||||
len(stale_shadows), self.instance_id)
|
||||
|
||||
if affected_combos:
|
||||
today = fields.Date.today()
|
||||
today_str = str(today)
|
||||
future_combos = set()
|
||||
for tid, d in affected_combos:
|
||||
if not d:
|
||||
continue
|
||||
d_str = str(d) if not isinstance(d, str) else d
|
||||
if d_str >= today_str:
|
||||
future_combos.add((tid, d_str))
|
||||
if future_combos:
|
||||
TaskModel = self.env['fusion.technician.task'].sudo()
|
||||
try:
|
||||
ungeocode = TaskModel.search([
|
||||
('x_fc_sync_source', '=', self.instance_id),
|
||||
('active', '=', True),
|
||||
('scheduled_date', '>=', today_str),
|
||||
('status', 'not in', ['cancelled']),
|
||||
'|',
|
||||
('address_lat', '=', 0), ('address_lat', '=', False),
|
||||
])
|
||||
geocoded = 0
|
||||
for shadow in ungeocode:
|
||||
if shadow.address_display:
|
||||
if shadow.with_context(skip_travel_recalc=True)._geocode_address():
|
||||
geocoded += 1
|
||||
if geocoded:
|
||||
_logger.info("Geocoded %d shadow tasks from %s",
|
||||
geocoded, self.name)
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
"Shadow task geocoding after sync from %s failed", self.name)
|
||||
|
||||
try:
|
||||
TaskModel._recalculate_combos_travel(future_combos)
|
||||
_logger.info(
|
||||
"Recalculated travel for %d tech+date combos after sync from %s",
|
||||
len(future_combos), self.name)
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
"Travel recalculation after sync from %s failed", self.name)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PULL: technician locations from remote instance
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _pull_technician_locations(self):
|
||||
"""Pull latest GPS locations for matched technicians from the remote instance.
|
||||
|
||||
Creates local location records with source='sync' so the map view
|
||||
shows technician positions from both instances. Only keeps the single
|
||||
most recent synced location per technician (replaces older synced
|
||||
records to avoid clutter).
|
||||
"""
|
||||
self.ensure_one()
|
||||
local_syncid_to_uid = self._get_local_syncid_to_uid()
|
||||
if not local_syncid_to_uid:
|
||||
return
|
||||
|
||||
remote_map = self._get_remote_tech_map()
|
||||
if not remote_map:
|
||||
return
|
||||
|
||||
matched_sync_ids = set(local_syncid_to_uid.keys()) & set(remote_map.keys())
|
||||
if not matched_sync_ids:
|
||||
return
|
||||
|
||||
remote_tech_ids = [remote_map[sid] for sid in matched_sync_ids]
|
||||
remote_syncid_by_uid = {v: k for k, v in remote_map.items()}
|
||||
|
||||
remote_locations = self._rpc(
|
||||
'fusion.technician.location', 'search_read',
|
||||
[[
|
||||
('user_id', 'in', remote_tech_ids),
|
||||
('logged_at', '>', str(fields.Datetime.subtract(
|
||||
fields.Datetime.now(), hours=24))),
|
||||
('source', '!=', 'sync'),
|
||||
]],
|
||||
{
|
||||
'fields': ['user_id', 'latitude', 'longitude',
|
||||
'accuracy', 'logged_at'],
|
||||
'order': 'logged_at desc',
|
||||
})
|
||||
|
||||
if not remote_locations:
|
||||
return
|
||||
|
||||
Location = self.env['fusion.technician.location'].sudo()
|
||||
|
||||
seen_techs = set()
|
||||
synced_count = 0
|
||||
for rloc in remote_locations:
|
||||
remote_uid_raw = rloc['user_id']
|
||||
remote_uid = (remote_uid_raw[0]
|
||||
if isinstance(remote_uid_raw, (list, tuple))
|
||||
else remote_uid_raw)
|
||||
if remote_uid in seen_techs:
|
||||
continue
|
||||
seen_techs.add(remote_uid)
|
||||
|
||||
sync_id = remote_syncid_by_uid.get(remote_uid)
|
||||
local_uid = local_syncid_to_uid.get(sync_id) if sync_id else None
|
||||
if not local_uid:
|
||||
continue
|
||||
|
||||
lat = rloc.get('latitude', 0)
|
||||
lng = rloc.get('longitude', 0)
|
||||
if not lat or not lng:
|
||||
continue
|
||||
|
||||
old_synced = Location.search([
|
||||
('user_id', '=', local_uid),
|
||||
('source', '=', 'sync'),
|
||||
('sync_instance', '=', self.instance_id),
|
||||
])
|
||||
if old_synced:
|
||||
old_synced.unlink()
|
||||
|
||||
Location.create({
|
||||
'user_id': local_uid,
|
||||
'latitude': lat,
|
||||
'longitude': lng,
|
||||
'accuracy': rloc.get('accuracy', 0),
|
||||
'logged_at': rloc.get('logged_at', fields.Datetime.now()),
|
||||
'source': 'sync',
|
||||
'sync_instance': self.instance_id,
|
||||
})
|
||||
synced_count += 1
|
||||
|
||||
if synced_count:
|
||||
_logger.info("Synced %d technician location(s) from %s",
|
||||
synced_count, self.name)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# CLEANUP
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _cron_cleanup_old_shadows(self):
|
||||
"""Remove shadow tasks older than 30 days (completed/cancelled)."""
|
||||
cutoff = fields.Date.today() - timedelta(days=30)
|
||||
old_shadows = self.env['fusion.technician.task'].sudo().search([
|
||||
('x_fc_sync_source', '!=', False),
|
||||
('scheduled_date', '<', str(cutoff)),
|
||||
('status', 'in', ['completed', 'cancelled']),
|
||||
])
|
||||
if old_shadows:
|
||||
count = len(old_shadows)
|
||||
old_shadows.unlink()
|
||||
_logger.info("Cleaned up %d old shadow tasks", count)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Manual trigger
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def action_sync_now(self):
|
||||
"""Manually trigger a full sync for this config."""
|
||||
self.ensure_one()
|
||||
self._pull_tasks_from_remote()
|
||||
self._pull_technician_locations()
|
||||
self.sudo().write({
|
||||
'last_sync': fields.Datetime.now(),
|
||||
'last_sync_error': False,
|
||||
})
|
||||
shadow_count = self.env['fusion.technician.task'].sudo().search_count([
|
||||
('x_fc_sync_source', '=', self.instance_id),
|
||||
])
|
||||
loc_count = self.env['fusion.technician.location'].sudo().search_count([
|
||||
('source', '=', 'sync'),
|
||||
('sync_instance', '=', self.instance_id),
|
||||
])
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Sync Complete',
|
||||
'message': (f'Synced from {self.name}. '
|
||||
f'{shadow_count} shadow task(s), '
|
||||
f'{loc_count} technician location(s) visible.'),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
"""
|
||||
Fusion Technician Location
|
||||
GPS location logging for field technicians.
|
||||
"""
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionTechnicianLocation(models.Model):
|
||||
_name = 'fusion.technician.location'
|
||||
_description = 'Technician Location Log'
|
||||
_order = 'logged_at desc'
|
||||
|
||||
user_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Technician',
|
||||
required=True,
|
||||
index=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
latitude = fields.Float(
|
||||
string='Latitude',
|
||||
digits=(10, 7),
|
||||
required=True,
|
||||
)
|
||||
longitude = fields.Float(
|
||||
string='Longitude',
|
||||
digits=(10, 7),
|
||||
required=True,
|
||||
)
|
||||
accuracy = fields.Float(
|
||||
string='Accuracy (m)',
|
||||
help='GPS accuracy in meters',
|
||||
)
|
||||
logged_at = fields.Datetime(
|
||||
string='Logged At',
|
||||
default=fields.Datetime.now,
|
||||
required=True,
|
||||
index=True,
|
||||
)
|
||||
source = fields.Selection([
|
||||
('portal', 'Portal'),
|
||||
('app', 'Mobile App'),
|
||||
('sync', 'Synced'),
|
||||
], string='Source', default='portal')
|
||||
sync_instance = fields.Char(
|
||||
'Sync Instance', index=True,
|
||||
help='Source instance ID if synced (e.g. westin, mobility)',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def log_location(self, latitude, longitude, accuracy=None):
|
||||
"""Log the current user's location. Called from portal JS."""
|
||||
return self.sudo().create({
|
||||
'user_id': self.env.user.id,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'accuracy': accuracy or 0,
|
||||
'source': 'portal',
|
||||
})
|
||||
|
||||
@api.model
|
||||
def get_latest_locations(self):
|
||||
"""Get the most recent location for each technician (for map view).
|
||||
|
||||
Includes both local GPS pings and synced locations from remote
|
||||
instances, so the map shows all shared technicians regardless of
|
||||
which Odoo instance they are clocked into.
|
||||
"""
|
||||
self.env.cr.execute("""
|
||||
SELECT DISTINCT ON (user_id)
|
||||
user_id, latitude, longitude, accuracy, logged_at,
|
||||
COALESCE(sync_instance, '') AS sync_instance
|
||||
FROM fusion_technician_location
|
||||
WHERE logged_at > NOW() - INTERVAL '24 hours'
|
||||
ORDER BY user_id, logged_at DESC
|
||||
""")
|
||||
rows = self.env.cr.dictfetchall()
|
||||
local_id = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'fusion_claims.sync_instance_id', '')
|
||||
result = []
|
||||
for row in rows:
|
||||
user = self.env['res.users'].sudo().browse(row['user_id'])
|
||||
src = row.get('sync_instance') or local_id
|
||||
result.append({
|
||||
'user_id': row['user_id'],
|
||||
'name': user.name,
|
||||
'latitude': row['latitude'],
|
||||
'longitude': row['longitude'],
|
||||
'accuracy': row['accuracy'],
|
||||
'logged_at': str(row['logged_at']),
|
||||
'sync_instance': src,
|
||||
})
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def _cron_cleanup_old_locations(self):
|
||||
"""Remove location logs based on configurable retention setting.
|
||||
|
||||
Setting (fusion_claims.location_retention_days):
|
||||
- Empty / not set => keep 30 days (default)
|
||||
- "0" => delete at end of day (keep today only)
|
||||
- "1" .. "N" => keep for N days
|
||||
"""
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
raw = (ICP.get_param('fusion_claims.location_retention_days') or '').strip()
|
||||
|
||||
if raw == '':
|
||||
retention_days = 30 # default: 1 month
|
||||
else:
|
||||
try:
|
||||
retention_days = max(int(raw), 0)
|
||||
except (ValueError, TypeError):
|
||||
retention_days = 30
|
||||
|
||||
cutoff = fields.Datetime.subtract(fields.Datetime.now(), days=retention_days)
|
||||
old_records = self.search([('logged_at', '<', cutoff)])
|
||||
count = len(old_records)
|
||||
if count:
|
||||
old_records.unlink()
|
||||
_logger.info(
|
||||
"Cleaned up %d technician location records (retention=%d days)",
|
||||
count, retention_days,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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_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_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_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
|
||||
@@ -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_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_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_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
|
||||
@@ -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_manager,fusion.ltc.form.submission.manager,model_fusion_ltc_form_submission,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_ltc_repair_create_so_wizard_user,fusion.ltc.repair.create.so.wizard.user,model_fusion_ltc_repair_create_so_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_ltc_repair_create_so_wizard_manager,fusion.ltc.repair.create.so.wizard.manager,model_fusion_ltc_repair_create_so_wizard,sales_team.group_sale_manager,1,1,1,1
|
||||
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
|
||||
|
@@ -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>
|
||||
</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>
|
||||
|
||||
@@ -138,6 +138,75 @@ $transition-speed: .25s;
|
||||
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)
|
||||
.fc_sidebar_toggle_btn {
|
||||
position: absolute;
|
||||
|
||||
@@ -180,9 +180,22 @@ const SOURCE_COLORS = {
|
||||
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 } } */
|
||||
function groupTasks(tasksData, localInstanceId) {
|
||||
// Sort by date ASC, time ASC
|
||||
function groupTasks(tasksData, localInstanceId, visibleTechIds) {
|
||||
const sorted = [...tasksData].sort((a, b) => {
|
||||
const da = a.scheduled_date || "";
|
||||
const db = b.scheduled_date || "";
|
||||
@@ -190,6 +203,8 @@ function groupTasks(tasksData, localInstanceId) {
|
||||
return (a.time_start || 0) - (b.time_start || 0);
|
||||
});
|
||||
|
||||
const hasTechFilter = visibleTechIds && Object.keys(visibleTechIds).length > 0;
|
||||
|
||||
const groups = {};
|
||||
const order = [GROUP_PENDING, GROUP_YESTERDAY, GROUP_TODAY, GROUP_TOMORROW, GROUP_THIS_WEEK, GROUP_LATER];
|
||||
for (const key of order) {
|
||||
@@ -205,12 +220,15 @@ function groupTasks(tasksData, localInstanceId) {
|
||||
|
||||
const dayCounters = {};
|
||||
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 dayKey = task.scheduled_date || "none";
|
||||
dayCounters[dayKey] = (dayCounters[dayKey] || 0) + 1;
|
||||
task._scheduleNum = dayCounters[dayKey];
|
||||
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._statusLabel = STATUS_LABELS[task.status] || task.status || "";
|
||||
task._statusIcon = STATUS_ICONS[task.status] || "fa-circle";
|
||||
@@ -228,7 +246,6 @@ function groupTasks(tasksData, localInstanceId) {
|
||||
groups[g].count++;
|
||||
}
|
||||
|
||||
// Return only non-empty groups in order
|
||||
return order.map((k) => groups[k]).filter((g) => g.count > 0);
|
||||
}
|
||||
|
||||
@@ -259,12 +276,10 @@ export class FusionTaskMapController extends Component {
|
||||
showRoute: true,
|
||||
taskCount: 0,
|
||||
techCount: 0,
|
||||
// Sidebar
|
||||
sidebarOpen: true,
|
||||
groups: [], // [{key, label, tasks[], count}]
|
||||
collapsedGroups: {}, // {groupKey: true}
|
||||
activeTaskId: null, // Highlighted task
|
||||
// Day filters for map pins (which groups show on map)
|
||||
groups: [],
|
||||
collapsedGroups: {},
|
||||
activeTaskId: null,
|
||||
visibleGroups: {
|
||||
[GROUP_YESTERDAY]: false,
|
||||
[GROUP_TODAY]: true,
|
||||
@@ -272,6 +287,8 @@ export class FusionTaskMapController extends Component {
|
||||
[GROUP_THIS_WEEK]: false,
|
||||
[GROUP_LATER]: false,
|
||||
},
|
||||
allTechnicians: [],
|
||||
visibleTechIds: {},
|
||||
});
|
||||
|
||||
// Yesterday collapsed by default in sidebar list
|
||||
@@ -339,9 +356,17 @@ export class FusionTaskMapController extends Component {
|
||||
this.tasksData = result.tasks || [];
|
||||
this.locationsData = result.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.groups = groupTasks(this.tasksData, this.localInstanceId);
|
||||
}
|
||||
|
||||
async _loadAndRender() {
|
||||
@@ -1008,6 +1033,28 @@ export class FusionTaskMapController extends Component {
|
||||
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 ─────────────────────────────────────────────
|
||||
toggleTraffic() {
|
||||
this.state.showTraffic = !this.state.showTraffic;
|
||||
|
||||
@@ -52,6 +52,22 @@
|
||||
<button class="fc_day_chip fc_day_chip--all" t-on-click="showAllDays"
|
||||
title="Show all">All</button>
|
||||
</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>
|
||||
|
||||
<!-- Sidebar body: grouped task list -->
|
||||
|
||||
@@ -341,6 +341,27 @@
|
||||
</field>
|
||||
</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 -->
|
||||
<!-- ===================================================================== -->
|
||||
@@ -350,24 +371,59 @@
|
||||
<field name="inherit_id" ref="account.view_account_invoice_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<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/>
|
||||
<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'])]"/>
|
||||
<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"
|
||||
domain="[('adp_exported', '=', True)]"/>
|
||||
<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'])]"/>
|
||||
<separator/>
|
||||
<!-- Client Type Filters -->
|
||||
<filter string="REG Clients" name="reg_clients"
|
||||
domain="[('x_fc_client_type', '=', 'REG')]"/>
|
||||
<filter string="ODS/OWP/ACS" name="full_funding"
|
||||
domain="[('x_fc_client_type', 'in', ['ODS', 'OWP', 'ACS'])]"/>
|
||||
<separator/>
|
||||
<!-- Invoice Portion Filters -->
|
||||
<filter string="Client Invoices (25%)" name="client_invoices"
|
||||
domain="[('x_fc_adp_invoice_portion', '=', 'client')]"/>
|
||||
<filter string="ADP Invoices (75%)" name="adp_portion_invoices"
|
||||
domain="[('x_fc_adp_invoice_portion', '=', 'adp')]"/>
|
||||
<separator/>
|
||||
<!-- ADP Billing Status Filters -->
|
||||
<filter string="Billing: Waiting" name="billing_waiting"
|
||||
domain="[('x_fc_adp_billing_status', '=', 'waiting')]"/>
|
||||
<filter string="Billing: Submitted" name="billing_submitted"
|
||||
@@ -376,6 +432,16 @@
|
||||
domain="[('x_fc_adp_billing_status', '=', 'need_correction')]"/>
|
||||
<filter string="Billing: Payment Issued" name="billing_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>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<field name="device_code"/>
|
||||
<field name="device_type"/>
|
||||
<field name="manufacturer" optional="show"/>
|
||||
<field name="build_type" optional="show"/>
|
||||
<field name="device_description" optional="hide"/>
|
||||
<field name="adp_price"/>
|
||||
<field name="max_quantity"/>
|
||||
@@ -44,6 +45,7 @@
|
||||
<group string="Device Information">
|
||||
<field name="device_type"/>
|
||||
<field name="manufacturer"/>
|
||||
<field name="build_type"/>
|
||||
<field name="device_description"/>
|
||||
</group>
|
||||
<group string="Pricing">
|
||||
@@ -77,9 +79,12 @@
|
||||
<field name="device_description"/>
|
||||
<separator/>
|
||||
<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/>
|
||||
<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="Build Type" name="group_build_type" context="{'group_by': 'build_type'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
@@ -778,6 +783,348 @@
|
||||
<field name="help" type="html"><p class="o_view_nocontent_smiling_face">No Ontario Works cases yet</p></field>
|
||||
</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 -->
|
||||
<!-- ===================================================================== -->
|
||||
@@ -947,10 +1294,10 @@
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- MARCH OF DIMES: ACTION -->
|
||||
<!-- MARCH OF DIMES: ACTIONS -->
|
||||
<!-- ===================================================================== -->
|
||||
<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="view_mode">list,kanban,form</field>
|
||||
<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="domain">[('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 id="action_fc_muscular_dystrophy_orders" model="ir.actions.act_window">
|
||||
@@ -1070,6 +1598,90 @@
|
||||
<field name="context">{'default_move_type': 'out_invoice'}</field>
|
||||
</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 -->
|
||||
<record id="action_fc_open_ai_chat" model="ir.actions.server">
|
||||
<field name="name">Ask Fusion Claims AI</field>
|
||||
@@ -1102,11 +1714,15 @@ else:
|
||||
name="Fusion Claims"
|
||||
web_icon="fusion_claims,static/description/icon.png"
|
||||
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 ===== -->
|
||||
<menuitem id="menu_fc_ltc"
|
||||
name="LTC Management"
|
||||
name="LTC"
|
||||
parent="menu_adp_claims_root"
|
||||
sequence="5"/>
|
||||
<menuitem id="menu_ltc_overview"
|
||||
@@ -1163,6 +1779,8 @@ else:
|
||||
action="action_adp_orders_all" sequence="1"/>
|
||||
<menuitem id="menu_adp_invoices" name="ADP Invoices" parent="menu_fc_adp"
|
||||
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"
|
||||
name="Quotation Stage"
|
||||
@@ -1270,14 +1888,139 @@ else:
|
||||
sequence="25"/>
|
||||
<menuitem id="menu_fc_odsp_all" name="All ODSP Cases" parent="menu_fc_odsp"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
action="action_fc_odsp_ontario_works_orders" sequence="30"/>
|
||||
<menuitem id="menu_fc_march_of_dimes" name="March of Dimes" parent="menu_adp_claims_root"
|
||||
action="action_fc_march_of_dimes_orders" sequence="30"/>
|
||||
sequence="30"/>
|
||||
<menuitem id="menu_odsp_ow_all" name="All OW Cases" parent="menu_fc_odsp_ontario_works"
|
||||
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 ===== -->
|
||||
<menuitem id="menu_fc_other_fundings" name="Other Fundings" parent="menu_adp_claims_root"
|
||||
sequence="35"/>
|
||||
@@ -1292,6 +2035,24 @@ else:
|
||||
<menuitem id="menu_fc_wsib" name="WSIB" parent="menu_fc_other_fundings"
|
||||
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 ===== -->
|
||||
<menuitem id="menu_fc_client_intelligence"
|
||||
name="Client Intelligence"
|
||||
|
||||
@@ -409,7 +409,7 @@
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<menuitem id="menu_loaner_root"
|
||||
name="Loaner Management"
|
||||
name="Loaners"
|
||||
parent="menu_adp_claims_root"
|
||||
sequence="58"/>
|
||||
|
||||
|
||||
89
fusion_claims/views/page11_sign_request_views.xml
Normal file
89
fusion_claims/views/page11_sign_request_views.xml
Normal 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>
|
||||
@@ -194,26 +194,6 @@
|
||||
</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>
|
||||
|
||||
<div class="row mt-4 o_settings_container">
|
||||
@@ -256,117 +236,6 @@
|
||||
</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>
|
||||
|
||||
<div class="row mt-4 o_settings_container">
|
||||
|
||||
@@ -1088,6 +1088,13 @@
|
||||
invisible="x_fc_technician_task_count == 0">
|
||||
<field name="x_fc_technician_task_count" widget="statinfo" string="Tasks"/>
|
||||
</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>
|
||||
</field>
|
||||
</record>
|
||||
@@ -1201,6 +1208,18 @@
|
||||
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)"/>
|
||||
|
||||
<!-- 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 -->
|
||||
<button name="action_application_received" type="object"
|
||||
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')"
|
||||
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"
|
||||
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')"
|
||||
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) -->
|
||||
<!-- ============================================================ -->
|
||||
@@ -1493,10 +1519,16 @@
|
||||
icon="fa-pause"
|
||||
invisible="x_fc_adp_application_status in ('on_hold', 'denied', 'withdrawn', 'cancelled', 'case_closed')"/>
|
||||
<button name="action_resume_from_hold" type="object"
|
||||
string="Resume"
|
||||
string="Resume"
|
||||
class="btn-success btn-sm me-1"
|
||||
icon="fa-play"
|
||||
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"
|
||||
type="action" string="Withdraw"
|
||||
class="btn-secondary btn-sm me-1"
|
||||
@@ -1815,8 +1847,14 @@
|
||||
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')"
|
||||
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>
|
||||
<field name="page11_sign_status" invisible="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2105,22 +2143,6 @@
|
||||
</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 -->
|
||||
<field name="x_fc_has_deductions" invisible="1"/>
|
||||
<field name="x_fc_total_deduction_amount" invisible="1"/>
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- SYNC CONFIG - FORM VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_task_sync_config_form" model="ir.ui.view">
|
||||
<field name="name">fusion.task.sync.config.form</field>
|
||||
<field name="model">fusion.task.sync.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Task Sync Configuration">
|
||||
<header>
|
||||
<button name="action_test_connection" type="object"
|
||||
string="Test Connection" class="btn-secondary" icon="fa-plug"/>
|
||||
<button name="action_sync_now" type="object"
|
||||
string="Sync Now" class="btn-success" icon="fa-sync"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1><field name="name" placeholder="e.g. Westin Healthcare"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Connection">
|
||||
<field name="instance_id" placeholder="e.g. westin"/>
|
||||
<field name="url" placeholder="http://192.168.1.40:8069"/>
|
||||
<field name="database" placeholder="e.g. westin-v19"/>
|
||||
<field name="username" placeholder="e.g. admin"/>
|
||||
<field name="api_key" password="True"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<group string="Status">
|
||||
<field name="last_sync"/>
|
||||
<field name="last_sync_error" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<div class="alert alert-info mt-3">
|
||||
<i class="fa fa-info-circle"/>
|
||||
Technicians are matched across instances by their
|
||||
<strong>Tech Sync ID</strong> field (Settings > Users).
|
||||
Set the same ID (e.g. "gordy") on both instances for each shared technician.
|
||||
</div>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- SYNC CONFIG - LIST VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_task_sync_config_list" model="ir.ui.view">
|
||||
<field name="name">fusion.task.sync.config.list</field>
|
||||
<field name="model">fusion.task.sync.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="instance_id"/>
|
||||
<field name="url"/>
|
||||
<field name="database"/>
|
||||
<field name="active"/>
|
||||
<field name="last_sync"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- SYNC CONFIG - ACTION + MENU -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="action_task_sync_config" model="ir.actions.act_window">
|
||||
<field name="name">Task Sync Instances</field>
|
||||
<field name="res_model">fusion.task.sync.config</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_task_sync_config"
|
||||
name="Task Sync"
|
||||
parent="fusion_claims.menu_technician_schedule"
|
||||
action="action_task_sync_config"
|
||||
sequence="99"/>
|
||||
|
||||
</odoo>
|
||||
@@ -1,113 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- LIST VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_technician_location_list" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.location.list</field>
|
||||
<field name="model">fusion.technician.location</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Technician Locations" create="0" edit="0"
|
||||
default_order="logged_at desc">
|
||||
<field name="user_id" widget="many2one_avatar_user"/>
|
||||
<field name="logged_at" string="Time"/>
|
||||
<field name="latitude" optional="hide"/>
|
||||
<field name="longitude" optional="hide"/>
|
||||
<field name="accuracy" string="Accuracy (m)" optional="hide"/>
|
||||
<field name="source"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- FORM VIEW (read-only) -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_technician_location_form" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.location.form</field>
|
||||
<field name="model">fusion.technician.location</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Location Log" create="0" edit="0">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="user_id"/>
|
||||
<field name="logged_at"/>
|
||||
<field name="source"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="latitude"/>
|
||||
<field name="longitude"/>
|
||||
<field name="accuracy"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- SEARCH VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_technician_location_search" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.location.search</field>
|
||||
<field name="model">fusion.technician.location</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Location Logs">
|
||||
<field name="user_id" string="Technician"/>
|
||||
<separator/>
|
||||
<filter string="Today" name="filter_today"
|
||||
domain="[('logged_at', '>=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Last 7 Days" name="filter_7d"
|
||||
domain="[('logged_at', '>=', (context_today() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Last 30 Days" name="filter_30d"
|
||||
domain="[('logged_at', '>=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="Technician" name="group_user" context="{'group_by': 'user_id'}"/>
|
||||
<filter string="Date" name="group_date" context="{'group_by': 'logged_at:day'}"/>
|
||||
<filter string="Source" name="group_source" context="{'group_by': 'source'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- ACTION -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="action_technician_locations" model="ir.actions.act_window">
|
||||
<field name="name">Location History</field>
|
||||
<field name="res_model">fusion.technician.location</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_technician_location_search"/>
|
||||
<field name="context">{
|
||||
'search_default_filter_today': 1,
|
||||
'search_default_group_user': 1,
|
||||
}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No location data logged yet.
|
||||
</p>
|
||||
<p>Technician locations are automatically logged when they use the portal.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- MENU ITEMS (under Technician Management) -->
|
||||
<!-- ================================================================== -->
|
||||
<menuitem id="menu_technician_locations"
|
||||
name="Location History"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_locations"
|
||||
sequence="50"/>
|
||||
|
||||
<!-- CRON: Cleanup old location records (runs daily) -->
|
||||
<record id="ir_cron_cleanup_technician_locations" model="ir.cron">
|
||||
<field name="name">Cleanup Old Technician Locations</field>
|
||||
<field name="model_id" ref="model_fusion_technician_location"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_cleanup_old_locations()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,541 +1,156 @@
|
||||
<?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>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- SEQUENCE -->
|
||||
<!-- SEARCH VIEW EXTENSION -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="seq_technician_task" model="ir.sequence">
|
||||
<field name="name">Technician Task</field>
|
||||
<field name="code">fusion.technician.task</field>
|
||||
<field name="prefix">TASK-</field>
|
||||
<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"/>
|
||||
<record id="view_technician_task_search_claims" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.task.search.claims</field>
|
||||
<field name="model">fusion.technician.task</field>
|
||||
<field name="inherit_id" ref="fusion_tasks.view_technician_task_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='share']" position="after">
|
||||
<field name="x_fc_is_field_staff"/>
|
||||
<field name="x_fc_start_address"
|
||||
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 expr="//filter[@name='filter_pod']" position="after">
|
||||
<filter string="Has Purchase Order" name="has_po"
|
||||
domain="[('purchase_order_id', '!=', False)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- SEARCH VIEW -->
|
||||
<!-- FORM VIEW EXTENSION -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_technician_task_search" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.task.search</field>
|
||||
<record id="view_technician_task_form_claims" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.task.form.claims</field>
|
||||
<field name="model">fusion.technician.task</field>
|
||||
<field name="inherit_id" ref="fusion_tasks.view_technician_task_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Tasks">
|
||||
<!-- Quick Filters -->
|
||||
<filter string="Today" name="filter_today"
|
||||
domain="[('scheduled_date', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Tomorrow" name="filter_tomorrow"
|
||||
domain="[('scheduled_date', '=', (context_today() + datetime.timedelta(days=1)).strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="This Week" name="filter_this_week"
|
||||
domain="[('scheduled_date', '>=', (context_today() - datetime.timedelta(days=context_today().weekday())).strftime('%Y-%m-%d')),
|
||||
('scheduled_date', '<=', (context_today() + datetime.timedelta(days=6-context_today().weekday())).strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="Pending" name="filter_pending" domain="[('status', '=', 'pending')]"/>
|
||||
<filter string="Scheduled" name="filter_scheduled" domain="[('status', '=', 'scheduled')]"/>
|
||||
<filter string="En Route" name="filter_en_route" domain="[('status', '=', 'en_route')]"/>
|
||||
<filter string="In Progress" name="filter_in_progress" domain="[('status', '=', 'in_progress')]"/>
|
||||
<filter string="Completed" name="filter_completed" domain="[('status', '=', 'completed')]"/>
|
||||
<filter string="Active" name="filter_active" domain="[('status', 'not in', ['cancelled', 'completed'])]"/>
|
||||
<separator/>
|
||||
<filter string="My Tasks" name="filter_my_tasks"
|
||||
domain="['|', ('technician_id', '=', uid), ('additional_technician_ids', 'in', [uid])]"/>
|
||||
<filter string="Deliveries" name="filter_deliveries" domain="[('task_type', '=', 'delivery')]"/>
|
||||
<filter string="Repairs" name="filter_repairs" domain="[('task_type', '=', 'repair')]"/>
|
||||
<filter string="POD Required" name="filter_pod" domain="[('pod_required', '=', True)]"/>
|
||||
<filter string="Has Purchase Order" name="has_po"
|
||||
domain="[('purchase_order_id', '!=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="Local Tasks" name="filter_local"
|
||||
domain="[('x_fc_sync_source', '=', False)]"/>
|
||||
<filter string="Synced Tasks" name="filter_synced"
|
||||
domain="[('x_fc_sync_source', '!=', False)]"/>
|
||||
<separator/>
|
||||
<!-- Group By -->
|
||||
<filter string="Technician" name="group_technician" context="{'group_by': 'technician_id'}"/>
|
||||
<filter string="Date" name="group_date" context="{'group_by': 'scheduled_date'}"/>
|
||||
<filter string="Status" name="group_status" context="{'group_by': 'status'}"/>
|
||||
<filter string="Task Type" name="group_type" context="{'group_by': 'task_type'}"/>
|
||||
<filter string="Client" name="group_client" context="{'group_by': 'partner_id'}"/>
|
||||
</search>
|
||||
<!-- Stat buttons: View Case + Purchase Order -->
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<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>
|
||||
</xpath>
|
||||
|
||||
<!-- Add facility_id, sale_order_id, purchase_order_id after priority -->
|
||||
<xpath expr="//field[@name='priority']" position="after">
|
||||
<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'"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Add Rental Inspection tab after Completion tab -->
|
||||
<xpath expr="//page[@name='completion']" position="after">
|
||||
<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>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- FORM VIEW -->
|
||||
<!-- LIST VIEW EXTENSION -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_technician_task_form" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.task.form</field>
|
||||
<record id="view_technician_task_list_claims" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.task.list.claims</field>
|
||||
<field name="model">fusion.technician.task</field>
|
||||
<field name="inherit_id" ref="fusion_tasks.view_technician_task_list"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Technician Task">
|
||||
<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"/>
|
||||
<xpath expr="//field[@name='pod_required']" position="after">
|
||||
<field name="sale_order_id" optional="hide"/>
|
||||
<field name="purchase_order_id" optional="hide"/>
|
||||
<field name="x_fc_source_label" string="Source" optional="show"
|
||||
widget="badge" decoration-info="x_fc_is_shadow"
|
||||
decoration-success="not x_fc_is_shadow"/>
|
||||
</list>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- KANBAN VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<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 -->
|
||||
<!-- MENU ITEMS - Field Service under Claims app -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
<!-- Main Tasks Action (List/Kanban) -->
|
||||
<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) -->
|
||||
<!-- Field Service parent menu under Claims -->
|
||||
<menuitem id="menu_technician_management"
|
||||
name="Field Service"
|
||||
parent="fusion_claims.menu_adp_claims_root"
|
||||
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"
|
||||
name="Delivery Map"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_map_view"
|
||||
action="fusion_tasks.action_technician_map_view"
|
||||
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"
|
||||
name="Today's Tasks"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_tasks_today"
|
||||
action="fusion_tasks.action_technician_tasks_today"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_technician_schedule"
|
||||
name="Schedule"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_schedule"
|
||||
action="fusion_tasks.action_technician_schedule"
|
||||
sequence="15"/>
|
||||
|
||||
<menuitem id="menu_technician_tasks_pending"
|
||||
name="Pending Tasks"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_tasks_pending"
|
||||
action="fusion_tasks.action_technician_tasks_pending"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_technician_tasks"
|
||||
name="All Tasks"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_tasks"
|
||||
action="fusion_tasks.action_technician_tasks"
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem id="menu_technician_my_tasks"
|
||||
name="My Tasks"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_my_tasks"
|
||||
action="fusion_tasks.action_technician_my_tasks"
|
||||
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>
|
||||
|
||||
@@ -30,4 +30,5 @@ from . import odsp_discretionary_wizard
|
||||
from . import odsp_pre_approved_wizard
|
||||
from . import odsp_ready_delivery_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
|
||||
BIN
fusion_claims/wizard/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
fusion_claims/wizard/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user