updates
This commit is contained in:
30
fusion_claims/fusion_claims/LICENSE
Normal file
30
fusion_claims/fusion_claims/LICENSE
Normal file
@@ -0,0 +1,30 @@
|
||||
Odoo Proprietary License v1.0
|
||||
|
||||
This software and associated files (the "Software") may only be used (executed,
|
||||
modified, executed after modifications) if you have purchased a valid license
|
||||
from the authors, typically via Odoo Apps, or if you have received a written
|
||||
agreement from the authors of the Software.
|
||||
|
||||
You may develop Odoo modules that use the Software as a library (typically
|
||||
by depending on it, importing it and using its resources), but without copying
|
||||
any source code or material from the Software. You may distribute those
|
||||
modules under the license of your choice, provided that this license is
|
||||
compatible with the terms of the Odoo Proprietary License (For example:
|
||||
LGPL, MIT, or proprietary licenses similar to this one).
|
||||
|
||||
It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
or modified copies of the Software.
|
||||
|
||||
The above copyright notice and this permission notice must be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
|
||||
199
fusion_claims/fusion_claims/README.md
Normal file
199
fusion_claims/fusion_claims/README.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Fusion Central
|
||||
|
||||
**Complete ADP Billing Solution for Odoo 19**
|
||||
|
||||
Part of the Fusion Claim Assistant product family by Nexa Systems Inc.
|
||||
|
||||
## Overview
|
||||
|
||||
Fusion Central is a comprehensive solution for managing ADP (Assistive Devices Program) billing in Odoo. It provides automatic calculation of ADP and client portions, professional PDF reports, and seamless integration with your existing workflow.
|
||||
|
||||
## Features
|
||||
|
||||
### Automatic Portion Calculations
|
||||
- **Per-line calculations** on sale orders and invoices
|
||||
- **Client type based rules:**
|
||||
- REG: 75% ADP / 25% Client
|
||||
- ODS, OWP, ACS, LTC, SEN, CCA: 100% ADP / 0% Client
|
||||
- Automatic recalculation on quantity or price changes
|
||||
- Totals displayed on document headers
|
||||
|
||||
### Professional PDF Reports
|
||||
- **Portrait and Landscape** orientations available
|
||||
- Quotation, Sale Order, and Invoice reports
|
||||
- Uses Odoo's default company header/footer via `web.external_layout`
|
||||
- Includes:
|
||||
- Company logo and address (from company settings)
|
||||
- Billing and delivery address boxes with borders
|
||||
- ADP device codes from products (`x_adp_code`)
|
||||
- Per-line ADP and client portions with color coding
|
||||
- Serial number column
|
||||
- Authorizer and sales rep information
|
||||
- Payment terms
|
||||
- Signature section (if signed)
|
||||
|
||||
### Report Templates
|
||||
|
||||
| Report | Template ID | Model | Description |
|
||||
|--------|-------------|-------|-------------|
|
||||
| Quotation/Order (Portrait) | `fusion_claims.report_saleorder_portrait` | sale.order | Standard portrait layout |
|
||||
| Quotation/Order (Landscape - ADP) | `fusion_claims.report_saleorder_landscape` | sale.order | Landscape with full ADP columns |
|
||||
| Invoice (Portrait) | `fusion_claims.report_invoice_portrait` | account.move | Standard portrait layout |
|
||||
| Invoice (Landscape) | `fusion_claims.report_invoice_landscape` | account.move | Landscape with full ADP columns |
|
||||
|
||||
### Report Styling
|
||||
|
||||
All reports follow a consistent style:
|
||||
- **Font**: Arial, 10-12pt depending on section
|
||||
- **Headers**: Blue background (#0066a1) with white text
|
||||
- **ADP Portion Column**: Blue header (#1976d2), light blue rows (#e3f2fd)
|
||||
- **Client Portion Column**: Orange header (#e65100), light orange rows (#fff3e0)
|
||||
- **Bordered Tables**: All tables have 1px solid black borders
|
||||
- **Totals**: Right-aligned with proper borders
|
||||
|
||||
### Configurable Settings
|
||||
- Field mappings for existing Odoo Studio fields
|
||||
- One-click field creation for new installations
|
||||
- HTML editors for payment terms and refund policy
|
||||
- Store address configuration
|
||||
- Field mapping configuration
|
||||
|
||||
### ADP Claim Export
|
||||
- Export invoices to comma-separated TXT format
|
||||
- Compatible with ADP submission systems
|
||||
- Automatic file naming with submission numbers
|
||||
- Optional Documents app integration
|
||||
|
||||
## Installation
|
||||
|
||||
1. Copy the `fusion_claims` folder to your Odoo addons directory
|
||||
2. Update the apps list in Odoo
|
||||
3. Install "Fusion Central" from the Apps menu
|
||||
|
||||
## Configuration
|
||||
|
||||
### Quick Setup (Settings → Sales → Fusion Central)
|
||||
|
||||
1. **Create Fields:**
|
||||
- Click "Create Sale Order Fields"
|
||||
- Click "Create Invoice Fields"
|
||||
- Click "Create Product Fields"
|
||||
|
||||
2. **Or Detect Existing Fields:**
|
||||
- Click "Detect Existing Fields" if you have existing custom fields
|
||||
|
||||
3. **Configure Company Info:**
|
||||
- Store Address Line 1 & 2
|
||||
- Company Tagline
|
||||
- E-Transfer Email
|
||||
- Cheque Payable To
|
||||
|
||||
4. **Add Payment Terms:**
|
||||
- Use the HTML editor to format your payment terms
|
||||
|
||||
5. **Add Refund Policy:**
|
||||
- Enable "Include Refund Policy Page"
|
||||
- Paste your refund policy HTML
|
||||
|
||||
### Field Mappings
|
||||
|
||||
The module uses the following fields:
|
||||
|
||||
#### Sale Order Fields
|
||||
| Field | Technical Name | Description |
|
||||
|-------|---------------|-------------|
|
||||
| Sale Type | x_fc_sale_type | ADP, ADP/ODSP, REG, etc. |
|
||||
| Client Type | x_fc_client_type | REG, ODS, OWP, ACS, LTC, SEN, CCA |
|
||||
| Authorizer | x_fc_authorizer_id | Many2one to res.partner |
|
||||
| Claim Number | x_fc_claim_number | ADP claim reference |
|
||||
| Delivery Date | x_fc_adp_delivery_date | Device delivery date |
|
||||
| Client Ref 1 | x_fc_client_ref_1 | Client reference 1 |
|
||||
| Client Ref 2 | x_fc_client_ref_2 | Client reference 2 |
|
||||
| Service Start | x_fc_service_start_date | Service start date |
|
||||
| Service End | x_fc_service_end_date | Service end date |
|
||||
|
||||
#### Sale Order Line Fields
|
||||
| Field | Technical Name | Description |
|
||||
|-------|---------------|-------------|
|
||||
| Serial Number | x_fc_serial_number | Device serial number |
|
||||
| ADP Portion | x_fc_adp_portion | Calculated ADP amount |
|
||||
| Client Portion | x_fc_client_portion | Calculated client amount |
|
||||
| Device Placement | x_fc_device_placement | L/R/N/A placement |
|
||||
|
||||
#### Invoice Fields
|
||||
| Field | Technical Name | Description |
|
||||
|-------|---------------|-------------|
|
||||
| Invoice Type | x_fc_invoice_type | ADP, ADP Client Portion, ODSP, WSIB, etc. |
|
||||
| Claim Number | x_fc_claim_number | ADP claim reference |
|
||||
| Authorizer | x_fc_authorizer_id | Many2one to res.partner |
|
||||
| Delivery Date | x_fc_adp_delivery_date | Device delivery date |
|
||||
|
||||
#### Product Fields
|
||||
| Field | Technical Name | Description |
|
||||
|-------|---------------|-------------|
|
||||
| ADP Code | x_adp_code | ADP device code for billing |
|
||||
|
||||
## Usage
|
||||
|
||||
### Sale Orders
|
||||
1. Create a sale order
|
||||
2. Set Sale Type to "ADP" or "ADP/ODSP"
|
||||
3. Set Client Type (REG, ODS, etc.)
|
||||
4. Add products - ADP and Client portions calculate automatically
|
||||
5. Print using Portrait or Landscape report
|
||||
|
||||
### Invoices
|
||||
1. Create invoice (or generate from sale order)
|
||||
2. Set Invoice Type to "ADP" or "ADP/ODSP"
|
||||
3. Portions display automatically
|
||||
4. Use "Export ADP" button to generate claim file
|
||||
5. Print using Portrait or Landscape report
|
||||
|
||||
## Odoo 19 Compatibility Notes
|
||||
|
||||
This module has been updated for Odoo 19 with the following changes:
|
||||
|
||||
1. **QWeb Templates**:
|
||||
- Removed `hasattr` calls (not supported in Odoo 19 QWeb)
|
||||
- Changed `product_uom` to `product_uom_id`
|
||||
- Changed `tax_id` to `tax_ids`
|
||||
- Use `t-else=""` instead of checking for `display_type == False`
|
||||
|
||||
2. **Report Templates**:
|
||||
- Use only `web.external_layout` for headers/footers
|
||||
- Removed duplicate header/footer template calls
|
||||
- Added explicit CSS borders (Bootstrap border classes don't render in PDF)
|
||||
|
||||
3. **Field Names**:
|
||||
- All fields use the `x_fc_*` naming convention
|
||||
|
||||
## Requirements
|
||||
|
||||
- Odoo 19.0
|
||||
- Dependencies: base, sale, sale_management, account
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 19.0.1.0.0 (December 2025)
|
||||
- Updated for Odoo 19 compatibility
|
||||
- Rewrote all report templates with consistent styling
|
||||
- Fixed QWeb template issues (hasattr, field names)
|
||||
- Added explicit CSS borders for PDF rendering
|
||||
- Improved authorizer field display (Many2one handling)
|
||||
- Removed duplicate header/footer calls
|
||||
- Standardized all field mappings to x_fc_* fields
|
||||
|
||||
### Version 18.0.1.0.0
|
||||
- Initial release for Odoo 18
|
||||
|
||||
## Support
|
||||
|
||||
**Developer:** Nexa Systems Inc.
|
||||
**Website:** https://www.nexasystems.ca
|
||||
**Email:** support@nexasystems.ca
|
||||
|
||||
## License
|
||||
|
||||
Odoo Proprietary License v1.0 (OPL-1)
|
||||
|
||||
Copyright © 2024-2025 Nexa Systems Inc. All rights reserved.
|
||||
15
fusion_claims/fusion_claims/__init__.py
Normal file
15
fusion_claims/fusion_claims/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
|
||||
def _load_adp_device_codes(env):
|
||||
"""
|
||||
Post-init hook to load ADP Mobility Manual device codes.
|
||||
Called on module install AND upgrade.
|
||||
"""
|
||||
env['fusion.adp.device.code']._load_packaged_device_codes()
|
||||
180
fusion_claims/fusion_claims/__manifest__.py
Normal file
180
fusion_claims/fusion_claims/__manifest__.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
{
|
||||
'name': 'Fusion Claims',
|
||||
'version': '19.0.5.0.0',
|
||||
'category': 'Sales',
|
||||
'summary': 'Complete ADP Claims Management with Dashboard, Sales Integration, Billing Automation, and Two-Stage Verification.',
|
||||
'description': """
|
||||
Fusion Claims
|
||||
=============
|
||||
|
||||
Part of the Fusion Claim Assistant product family by Nexa Systems Inc.
|
||||
|
||||
Fusion Claims is a comprehensive standalone application for managing ADP (Assistive Devices Program)
|
||||
claims in Odoo. It provides its own sales management views, dashboard, claim workflow management,
|
||||
automatic calculation of ADP and client portions, and professional PDF reports.
|
||||
|
||||
Key Features:
|
||||
-------------
|
||||
• Standalone application with its own menu and views
|
||||
• Dashboard for claims overview and KPIs
|
||||
• Integrated sales management (custom views separate from standard Sales app)
|
||||
• Complete ADP claim workflow (Quotation → Ready for Submission → Application Submitted →
|
||||
Application Approved → Ready to Bill → Billed to ADP → Case Closed)
|
||||
• Automatic ADP/Client portion calculations with deductions (PCT/AMT)
|
||||
• Support for multiple client types (REG=75%/25%, ODS/OWP/ACS=100%/0%)
|
||||
• Split invoicing (Client Invoice + ADP Invoice)
|
||||
• ADP Device Codes reference from Mobility Manual (JSON/CSV import)
|
||||
• Device Approval Verification wizard - confirm which devices were approved by ADP
|
||||
• Approval status tracking on order lines and invoices with visual indicators
|
||||
• Professional PDF reports in Portrait and Landscape orientations
|
||||
• ADP claim export to TXT format for billing
|
||||
• Kanban board for ADP claim status tracking
|
||||
• Field mapping for existing Odoo Studio fields
|
||||
• Data persistence - fields survive module uninstall
|
||||
|
||||
Sales Management:
|
||||
-----------------
|
||||
• Custom quotation and order views with ADP fields
|
||||
• ADP portion and client portion columns
|
||||
• Serial number tracking per line item
|
||||
• Device placement (Left/Right/N/A)
|
||||
• Client type selection and automatic calculations
|
||||
|
||||
Claim Fields:
|
||||
-------------
|
||||
• Claim Number, Client Reference 1 & 2
|
||||
• ADP Delivery Date, Service Start/End Dates
|
||||
• Authorizer tracking
|
||||
• Deduction Type (Percentage or Amount)
|
||||
|
||||
Report Features:
|
||||
----------------
|
||||
• Company logo and multi-store addresses
|
||||
• Billing and delivery address boxes
|
||||
• Authorizer and sales rep information
|
||||
• Per-line ADP portion, client portion, taxes, and totals
|
||||
• ADP device codes from products
|
||||
• Serial numbers section
|
||||
• Customizable payment terms
|
||||
• Optional refund policy page
|
||||
|
||||
Copyright © 2024-2025 Nexa Systems Inc. All rights reserved.
|
||||
""",
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://www.nexasystems.ca',
|
||||
'maintainer': 'Nexa Systems Inc.',
|
||||
'support': 'support@nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'price': 0.00,
|
||||
'currency': 'CAD',
|
||||
'depends': [
|
||||
'base',
|
||||
'sale',
|
||||
'sale_management',
|
||||
'sale_margin',
|
||||
'account',
|
||||
'sales_team',
|
||||
'stock',
|
||||
'calendar',
|
||||
'ai',
|
||||
'fusion_ringcentral',
|
||||
],
|
||||
'external_dependencies': {
|
||||
'python': ['pdf2image', 'PIL'],
|
||||
},
|
||||
'data': [
|
||||
'security/security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/ir_config_parameter_data.xml',
|
||||
'data/pdf_template_data.xml',
|
||||
'data/mail_activity_type_data.xml',
|
||||
'data/ir_cron_data.xml',
|
||||
'data/ir_actions_server_data.xml',
|
||||
'data/stock_location_data.xml',
|
||||
'data/product_labor_data.xml',
|
||||
'wizard/status_change_reason_wizard_views.xml',
|
||||
'views/res_company_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/account_move_views.xml',
|
||||
'views/account_journal_views.xml',
|
||||
'wizard/adp_export_wizard_views.xml',
|
||||
'wizard/device_import_wizard_views.xml',
|
||||
'wizard/device_approval_wizard_views.xml',
|
||||
'wizard/submission_verification_wizard_views.xml',
|
||||
|
||||
'wizard/account_payment_register_views.xml',
|
||||
'wizard/case_close_verification_wizard_views.xml',
|
||||
'wizard/schedule_assessment_wizard_views.xml',
|
||||
'wizard/assessment_completed_wizard_views.xml',
|
||||
'wizard/application_received_wizard_views.xml',
|
||||
'wizard/ready_for_submission_wizard_views.xml',
|
||||
'wizard/ready_to_bill_wizard_views.xml',
|
||||
'wizard/field_mapping_config_wizard_views.xml',
|
||||
'wizard/ready_for_delivery_wizard_views.xml',
|
||||
'wizard/send_to_mod_wizard_views.xml',
|
||||
'wizard/mod_awaiting_funding_wizard_views.xml',
|
||||
'wizard/mod_funding_approved_wizard_views.xml',
|
||||
'wizard/mod_pca_received_wizard_views.xml',
|
||||
'wizard/odsp_sa_mobility_wizard_views.xml',
|
||||
'wizard/odsp_discretionary_wizard_views.xml',
|
||||
'wizard/odsp_submit_to_odsp_wizard_views.xml',
|
||||
'wizard/odsp_pre_approved_wizard_views.xml',
|
||||
'wizard/odsp_ready_delivery_wizard_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/pdf_template_inherit_views.xml',
|
||||
'views/dashboard_views.xml',
|
||||
'views/client_profile_views.xml',
|
||||
'wizard/xml_import_wizard_views.xml',
|
||||
'views/adp_claims_views.xml',
|
||||
'views/submission_history_views.xml',
|
||||
'views/fusion_loaner_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',
|
||||
'report/sale_report_landscape.xml',
|
||||
'report/invoice_report_portrait.xml',
|
||||
'report/invoice_report_landscape.xml',
|
||||
'report/report_proof_of_delivery.xml',
|
||||
'report/report_proof_of_delivery_standard.xml',
|
||||
'report/report_proof_of_pickup.xml',
|
||||
'report/report_rental_agreement.xml',
|
||||
'report/report_grab_bar_waiver.xml',
|
||||
'report/report_accessibility_contract.xml',
|
||||
'report/report_mod_quotation.xml',
|
||||
'report/report_mod_invoice.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'data/ai_agent_data.xml',
|
||||
],
|
||||
'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',
|
||||
'fusion_claims/static/src/js/status_selection_filter.js',
|
||||
'fusion_claims/static/src/js/gallery_preview.js',
|
||||
'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/xml/document_preview.xml',
|
||||
'fusion_claims/static/src/xml/fusion_task_map_view.xml',
|
||||
],
|
||||
},
|
||||
'images': ['static/description/icon.png'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': True,
|
||||
'post_init_hook': '_load_adp_device_codes',
|
||||
}
|
||||
93
fusion_claims/fusion_claims/data/ai_agent_data.xml
Normal file
93
fusion_claims/fusion_claims/data/ai_agent_data.xml
Normal file
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- ================================================================= -->
|
||||
<!-- AI TOOLS: Server Actions for Client Data Queries (MUST BE FIRST) -->
|
||||
<!-- ================================================================= -->
|
||||
|
||||
<!-- Tool 1: Search Client Profiles -->
|
||||
<record id="ai_tool_search_clients" model="ir.actions.server">
|
||||
<field name="name">Fusion: Search Client Profiles</field>
|
||||
<field name="state">code</field>
|
||||
<field name="use_in_ai" eval="True"/>
|
||||
<field name="model_id" ref="ai.model_ai_agent"/>
|
||||
<field name="code">
|
||||
ai['result'] = record._fc_tool_search_clients(search_term, city_filter, condition_filter)
|
||||
</field>
|
||||
<field name="ai_tool_description">Search for client profiles in Fusion Claims. Can search by name, health card number, city, or medical condition. Returns matching profiles with basic info and financial summaries.</field>
|
||||
<field name="ai_tool_schema">{"type": "object", "properties": {"search_term": {"type": "string", "description": "Search term to match against name, health card number, or city"}, "city_filter": {"type": "string", "description": "Filter by city name"}, "condition_filter": {"type": "string", "description": "Filter by medical condition (e.g., CVA, diabetes)"}}, "required": []}</field>
|
||||
</record>
|
||||
|
||||
<!-- Tool 2: Get Client Details -->
|
||||
<record id="ai_tool_client_details" model="ir.actions.server">
|
||||
<field name="name">Fusion: Get Client Details</field>
|
||||
<field name="state">code</field>
|
||||
<field name="use_in_ai" eval="True"/>
|
||||
<field name="model_id" ref="ai.model_ai_agent"/>
|
||||
<field name="code">
|
||||
ai['result'] = record._fc_tool_client_details(profile_id)
|
||||
</field>
|
||||
<field name="ai_tool_description">Get detailed information about a specific client profile including personal info, medical status, benefits, claims history, and ADP application history. Requires profile_id from a previous search.</field>
|
||||
<field name="ai_tool_schema">{"type": "object", "properties": {"profile_id": {"type": "number", "description": "ID of the client profile to get details for"}}, "required": ["profile_id"]}</field>
|
||||
</record>
|
||||
|
||||
<!-- Tool 3: Get Aggregated Stats -->
|
||||
<record id="ai_tool_client_stats" model="ir.actions.server">
|
||||
<field name="name">Fusion: Get Claims Statistics</field>
|
||||
<field name="state">code</field>
|
||||
<field name="use_in_ai" eval="True"/>
|
||||
<field name="model_id" ref="ai.model_ai_agent"/>
|
||||
<field name="code">
|
||||
ai['result'] = record._fc_tool_claims_stats()
|
||||
</field>
|
||||
<field name="ai_tool_description">Get aggregated statistics about Fusion Claims data: total profiles, total orders, breakdown by sale type, breakdown by workflow status, and top cities by client count. No parameters needed.</field>
|
||||
<field name="ai_tool_schema">{"type": "object", "properties": {}, "required": []}</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- AI TOPIC (references tools above) -->
|
||||
<!-- ================================================================= -->
|
||||
<record id="ai_topic_client_intelligence" model="ai.topic">
|
||||
<field name="name">Fusion Claims Client Intelligence</field>
|
||||
<field name="description">Query client profiles, ADP claims, funding history, medical conditions, and device information.</field>
|
||||
<field name="instructions">You help users find information about ADP clients, claims, medical conditions, devices, and funding history. Use the Fusion search/details/stats tools to query data.</field>
|
||||
<field name="tool_ids" eval="[(6, 0, [
|
||||
ref('fusion_claims.ai_tool_search_clients'),
|
||||
ref('fusion_claims.ai_tool_client_details'),
|
||||
ref('fusion_claims.ai_tool_client_stats'),
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- AI AGENT (references topic above) -->
|
||||
<!-- ================================================================= -->
|
||||
<record id="ai_agent_fusion_claims" model="ai.agent">
|
||||
<field name="name">Fusion Claims Intelligence</field>
|
||||
<field name="subtitle">Ask about clients, ADP claims, funding history, medical conditions, and devices.</field>
|
||||
<field name="llm_model">gpt-4.1</field>
|
||||
<field name="response_style">analytical</field>
|
||||
<field name="restrict_to_sources" eval="False"/>
|
||||
<field name="system_prompt">You are Fusion Claims Intelligence, an AI assistant for ADP claims management.
|
||||
|
||||
You help staff find information about clients, medical conditions, mobility devices, funding history, and claim status.
|
||||
|
||||
Capabilities:
|
||||
1. Search client profiles by name, health card number, city, or medical condition
|
||||
2. Get detailed client information including claims history and ADP applications
|
||||
3. Provide aggregated statistics about claims, funding types, and demographics
|
||||
|
||||
Response guidelines:
|
||||
- Be concise and data-driven
|
||||
- Format monetary values with $ and commas
|
||||
- Include key identifiers (name, health card, city) when listing clients
|
||||
- Include order number, status, and amounts when discussing claims
|
||||
- If asked about a specific client, search first, then get details
|
||||
- Always provide the profile ID for record lookup
|
||||
|
||||
Key terminology:
|
||||
- ADP = Assistive Devices Program (Ontario government)
|
||||
- Client Type REG = Regular (75% ADP / 25% Client), ODS/OWP/ACS = 100% ADP
|
||||
- Sale Types: ADP, ODSP, WSIB, Insurance, March of Dimes, Muscular Dystrophy, Hardship Funding
|
||||
- Sections: 2a = Walkers, 2b = Manual Wheelchairs, 2c = Power Bases/Scooters, 2d = Seating</field>
|
||||
<field name="topic_ids" eval="[(6, 0, [ref('ai_topic_client_intelligence')])]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
17057
fusion_claims/fusion_claims/data/device_codes/adp_mobility_manual.json
Normal file
17057
fusion_claims/fusion_claims/data/device_codes/adp_mobility_manual.json
Normal file
File diff suppressed because it is too large
Load Diff
30
fusion_claims/fusion_claims/data/ir_actions_server_data.xml
Normal file
30
fusion_claims/fusion_claims/data/ir_actions_server_data.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?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>
|
||||
</odoo>
|
||||
157
fusion_claims/fusion_claims/data/ir_config_parameter_data.xml
Normal file
157
fusion_claims/fusion_claims/data/ir_config_parameter_data.xml
Normal file
@@ -0,0 +1,157 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!--
|
||||
Default configuration parameters for Fusion Claims.
|
||||
noupdate="1" ensures these are ONLY set on first install,
|
||||
never overwritten during module upgrades.
|
||||
-->
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- ADP Billing -->
|
||||
<record id="config_vendor_code" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.vendor_code</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
<record id="config_adp_posting_base_date" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.adp_posting_base_date</field>
|
||||
<field name="value">2026-01-23</field>
|
||||
</record>
|
||||
<record id="config_adp_posting_frequency_days" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.adp_posting_frequency_days</field>
|
||||
<field name="value">14</field>
|
||||
</record>
|
||||
|
||||
<!-- Email Notifications -->
|
||||
<record id="config_enable_email_notifications" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.enable_email_notifications</field>
|
||||
<field name="value">True</field>
|
||||
</record>
|
||||
<record id="config_application_reminder_days" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.application_reminder_days</field>
|
||||
<field name="value">4</field>
|
||||
</record>
|
||||
<record id="config_application_reminder_2_days" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.application_reminder_2_days</field>
|
||||
<field name="value">4</field>
|
||||
</record>
|
||||
|
||||
<!-- AI Settings -->
|
||||
<record id="config_ai_model" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.ai_model</field>
|
||||
<field name="value">gpt-4o-mini</field>
|
||||
</record>
|
||||
<record id="config_auto_parse_xml" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.auto_parse_xml</field>
|
||||
<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>
|
||||
<field name="value">x_fc_sale_type</field>
|
||||
</record>
|
||||
<record id="config_field_so_client_type" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.field_so_client_type</field>
|
||||
<field name="value">x_fc_client_type</field>
|
||||
</record>
|
||||
<record id="config_field_so_authorizer" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.field_so_authorizer</field>
|
||||
<field name="value">x_fc_authorizer_id</field>
|
||||
</record>
|
||||
<record id="config_field_invoice_type" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.field_invoice_type</field>
|
||||
<field name="value">x_fc_invoice_type</field>
|
||||
</record>
|
||||
<record id="config_field_inv_client_type" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.field_inv_client_type</field>
|
||||
<field name="value">x_fc_client_type</field>
|
||||
</record>
|
||||
<record id="config_field_inv_authorizer" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.field_inv_authorizer</field>
|
||||
<field name="value">x_fc_authorizer_id</field>
|
||||
</record>
|
||||
<record id="config_field_product_code" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.field_product_code</field>
|
||||
<field name="value">x_fc_adp_device_code</field>
|
||||
</record>
|
||||
<record id="config_field_sol_serial" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.field_sol_serial</field>
|
||||
<field name="value">x_fc_serial_number</field>
|
||||
</record>
|
||||
<record id="config_field_aml_serial" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.field_aml_serial</field>
|
||||
<field name="value">x_fc_serial_number</field>
|
||||
</record>
|
||||
|
||||
<!-- March of Dimes -->
|
||||
<record id="config_mod_default_email" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.mod_default_email</field>
|
||||
<field name="value">hvmp@marchofdimes.ca</field>
|
||||
</record>
|
||||
|
||||
<!-- Twilio SMS -->
|
||||
<record id="config_twilio_enabled" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.twilio_enabled</field>
|
||||
<field name="value">False</field>
|
||||
</record>
|
||||
<record id="config_twilio_account_sid" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.twilio_account_sid</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
<record id="config_twilio_auth_token" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.twilio_auth_token</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
<record id="config_twilio_phone_number" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.twilio_phone_number</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
|
||||
<!-- MOD Follow-up Settings -->
|
||||
<record id="config_mod_followup_interval" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.mod_followup_interval_days</field>
|
||||
<field name="value">14</field>
|
||||
</record>
|
||||
<record id="config_mod_followup_escalation" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.mod_followup_escalation_days</field>
|
||||
<field name="value">3</field>
|
||||
</record>
|
||||
|
||||
<!-- ODSP Settings -->
|
||||
<record id="config_sa_mobility_email" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.sa_mobility_email</field>
|
||||
<field name="value">samobility@ontario.ca</field>
|
||||
</record>
|
||||
<record id="config_sa_mobility_phone" model="ir.config_parameter">
|
||||
<field name="key">fusion_claims.sa_mobility_phone</field>
|
||||
<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>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
183
fusion_claims/fusion_claims/data/ir_cron_data.xml
Normal file
183
fusion_claims/fusion_claims/data/ir_cron_data.xml
Normal file
@@ -0,0 +1,183 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<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>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_renew_delivery_reminders()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Cron Job: Renew ADP Billing Reminders -->
|
||||
<record id="ir_cron_renew_billing_reminders" model="ir.cron">
|
||||
<field name="name">Fusion Claims: Renew Billing Reminders</field>
|
||||
<field name="model_id" ref="account.model_account_move"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_renew_billing_reminders()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Cron Job: Renew ADP Correction Reminders -->
|
||||
<record id="ir_cron_renew_correction_reminders" model="ir.cron">
|
||||
<field name="name">Fusion Claims: Renew Correction Reminders</field>
|
||||
<field name="model_id" ref="account.model_account_move"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_renew_correction_reminders()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Cron Job: Auto-Close Cases (1 month after billed) -->
|
||||
<record id="ir_cron_auto_close_cases" model="ir.cron">
|
||||
<field name="name">Fusion Claims: Auto-Close Billed Cases</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_auto_close_billed_cases()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Cron Job: Auto-Close ODSP Cases (7 days after payment received) -->
|
||||
<record id="ir_cron_auto_close_odsp_paid" model="ir.cron">
|
||||
<field name="name">Fusion Claims: Auto-Close ODSP Paid Cases</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_auto_close_odsp_paid_cases()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Cron Job: Send Application Reminder Emails (4 days after assessment) -->
|
||||
<record id="ir_cron_application_reminder" model="ir.cron">
|
||||
<field name="name">Fusion Claims: First Application Reminder</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_send_application_reminders()</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=9, minute=0, second=0)"/>
|
||||
</record>
|
||||
|
||||
<!-- Cron Job: Send Second Application Reminder Emails (X days after first reminder) -->
|
||||
<record id="ir_cron_application_reminder_2" model="ir.cron">
|
||||
<field name="name">Fusion Claims: Second Application Reminder</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_send_application_reminders_2()</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=9, minute=0, second=0)"/>
|
||||
</record>
|
||||
|
||||
<!-- Cron Job: Acceptance Reminder Emails (next business day after submission) -->
|
||||
<record id="ir_cron_acceptance_reminder" model="ir.cron">
|
||||
<field name="name">Fusion Claims: Acceptance Reminder Emails</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_send_acceptance_reminders()</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=9, minute=0, second=0)"/>
|
||||
</record>
|
||||
|
||||
<!-- Cron Job: Loaner Equipment Reminders - moved to separate file after views -->
|
||||
<!-- Will be added via ir_cron_loaner_data.xml -->
|
||||
|
||||
<!-- Cron Job: MOD Bi-weekly Follow-up Scheduler -->
|
||||
<record id="ir_cron_mod_followup_scheduler" model="ir.cron">
|
||||
<field name="name">Fusion Claims: MOD Follow-up Scheduler</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_mod_schedule_followups()</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=8, minute=0, second=0)"/>
|
||||
</record>
|
||||
|
||||
<!-- Cron Job: MOD Follow-up Escalation (auto-email when activity overdue) -->
|
||||
<record id="ir_cron_mod_followup_escalation" model="ir.cron">
|
||||
<field name="name">Fusion Claims: MOD Follow-up Escalation</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_mod_escalate_followups()</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=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"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_calculate_travel_times()</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)"/>
|
||||
</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">5</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>
|
||||
55
fusion_claims/fusion_claims/data/mail_activity_type_data.xml
Normal file
55
fusion_claims/fusion_claims/data/mail_activity_type_data.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<odoo>
|
||||
<!-- ADP Delivery Reminder Activity Type -->
|
||||
<record id="mail_activity_type_adp_delivery" model="mail.activity.type">
|
||||
<field name="name">ADP Delivery Reminder</field>
|
||||
<field name="summary">Complete delivery for ADP billing</field>
|
||||
<field name="icon">fa-truck</field>
|
||||
<field name="delay_count">0</field>
|
||||
<field name="delay_unit">days</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="category">default</field>
|
||||
<field name="decoration_type">warning</field>
|
||||
</record>
|
||||
|
||||
<!-- ADP Billing Deadline Activity Type -->
|
||||
<record id="mail_activity_type_adp_billing" model="mail.activity.type">
|
||||
<field name="name">ADP Billing Deadline</field>
|
||||
<field name="summary">Complete ADP billing by Wednesday 6 PM</field>
|
||||
<field name="icon">fa-file-invoice-dollar</field>
|
||||
<field name="delay_count">0</field>
|
||||
<field name="delay_unit">days</field>
|
||||
<field name="res_model">account.move</field>
|
||||
<field name="category">default</field>
|
||||
<field name="decoration_type">warning</field>
|
||||
</record>
|
||||
|
||||
<!-- ADP Correction Required Activity Type -->
|
||||
<record id="mail_activity_type_adp_correction" model="mail.activity.type">
|
||||
<field name="name">ADP Correction Required</field>
|
||||
<field name="summary">Invoice needs correction and resubmission</field>
|
||||
<field name="icon">fa-exclamation-triangle</field>
|
||||
<field name="delay_count">0</field>
|
||||
<field name="delay_unit">days</field>
|
||||
<field name="res_model">account.move</field>
|
||||
<field name="category">default</field>
|
||||
<field name="decoration_type">danger</field>
|
||||
</record>
|
||||
|
||||
<!-- MOD Follow-up Call Activity Type -->
|
||||
<record id="mail_activity_type_mod_followup" model="mail.activity.type">
|
||||
<field name="name">MOD Follow-up Call</field>
|
||||
<field name="summary">Call client for March of Dimes case update</field>
|
||||
<field name="icon">fa-phone</field>
|
||||
<field name="delay_count">14</field>
|
||||
<field name="delay_unit">days</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="category">default</field>
|
||||
<field name="decoration_type">warning</field>
|
||||
</record>
|
||||
</odoo>
|
||||
174
fusion_claims/fusion_claims/data/mail_template_data.xml
Normal file
174
fusion_claims/fusion_claims/data/mail_template_data.xml
Normal file
@@ -0,0 +1,174 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
|
||||
Email templates for ADP sales with landscape reports.
|
||||
Design: Professional, dark/light mode safe, anti-spam friendly.
|
||||
-->
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- ADP Quotation Email Template (Landscape Report) -->
|
||||
<!-- =============================================================== -->
|
||||
<record id="email_template_adp_quotation" model="mail.template">
|
||||
<field name="name">ADP: Send Quotation (Landscape)</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="subject">{{ object.company_id.name }} - Quotation {{ object.name or 'n/a' }}</field>
|
||||
<field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
|
||||
<field name="partner_to">{{ object.partner_id.id }}</field>
|
||||
<field name="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="height:4px;background-color:#2B6CB0;"></div>
|
||||
<div style="background:#ffffff;padding:32px 28px;border:1px solid #e2e8f0;border-top:none;">
|
||||
<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>.
|
||||
</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>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<field name="report_template_ids" eval="[(4, ref('fusion_claims.action_report_saleorder_landscape'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- ADP Sales Order Confirmation Email Template -->
|
||||
<!-- =============================================================== -->
|
||||
<record id="email_template_adp_sales_order" model="mail.template">
|
||||
<field name="name">ADP: Sales Order Confirmation (Landscape)</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="subject">{{ object.company_id.name }} - Order Confirmation {{ object.name or 'n/a' }}</field>
|
||||
<field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
|
||||
<field name="partner_to">{{ object.partner_id.id }}</field>
|
||||
<field name="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="height:4px;background-color:#38a169;"></div>
|
||||
<div style="background:#ffffff;padding:32px 28px;border:1px solid #e2e8f0;border-top:none;">
|
||||
<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.
|
||||
</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>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<field name="report_template_ids" eval="[(4, ref('fusion_claims.action_report_saleorder_landscape'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- ADP Client Invoice Email Template -->
|
||||
<!-- =============================================================== -->
|
||||
<record id="email_template_adp_invoice" model="mail.template">
|
||||
<field name="name">ADP: Send Invoice (Landscape)</field>
|
||||
<field name="model_id" ref="account.model_account_move"/>
|
||||
<field name="subject">{{ object.company_id.name }} - Invoice {{ object.name or 'Draft' }}</field>
|
||||
<field name="email_from">{{ (object.invoice_user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
|
||||
<field name="partner_to">{{ object.partner_id.id }}</field>
|
||||
<field name="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="height:4px;background-color:#2B6CB0;"></div>
|
||||
<div style="background:#ffffff;padding:32px 28px;border:1px solid #e2e8f0;border-top:none;">
|
||||
<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>.
|
||||
</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>
|
||||
<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>
|
||||
</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;">
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</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>
|
||||
</t>
|
||||
<t t-set="sig" t-value="object.invoice_user_id.signature or object.user_id.signature"/>
|
||||
<t t-if="not is_html_empty(sig)" data-o-mail-quote-container="1">
|
||||
<div data-o-mail-quote="1">--<br data-o-mail-quote="1"/><t t-out="sig or ''" data-o-mail-quote="1"/></div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<field name="report_template_ids" eval="[(4, ref('fusion_claims.action_report_invoice_landscape'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
7
fusion_claims/fusion_claims/data/pdf_template_data.xml
Normal file
7
fusion_claims/fusion_claims/data/pdf_template_data.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- ODSP SA Mobility signing template is created via the UI or odoo shell.
|
||||
It lives in fusion.pdf.template (category=odsp) with 3 fields:
|
||||
sa_client_name (text), sa_sign_date (date), sa_signature (signature)
|
||||
Managed via Configuration > PDF Templates using the drag-and-drop editor. -->
|
||||
</odoo>
|
||||
15
fusion_claims/fusion_claims/data/product_labor_data.xml
Normal file
15
fusion_claims/fusion_claims/data/product_labor_data.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="product_labor_hourly" model="product.template">
|
||||
<field name="name">LABOR - CHARGED HOURLY - NON REFUNDABLE</field>
|
||||
<field name="default_code">LABOR</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">75.00</field>
|
||||
<field name="uom_id" ref="uom.product_uom_hour"/>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
<field name="taxes_id" eval="[(6, 0, [ref('account.1_hst_sale_tax_13')])]"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
41
fusion_claims/fusion_claims/data/stock_location_data.xml
Normal file
41
fusion_claims/fusion_claims/data/stock_location_data.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Loaner Stock Location -->
|
||||
<record id="stock_location_loaner" model="stock.location">
|
||||
<field name="name">Loaner Stock</field>
|
||||
<field name="usage">internal</field>
|
||||
<field name="location_id" ref="stock.stock_location_stock"/>
|
||||
</record>
|
||||
|
||||
<!-- Sequence for Loaner Checkout -->
|
||||
<record id="seq_loaner_checkout" model="ir.sequence">
|
||||
<field name="name">Loaner Checkout Sequence</field>
|
||||
<field name="code">fusion.loaner.checkout</field>
|
||||
<field name="prefix">LOAN/</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
<!-- Loaner Product Categories -->
|
||||
<record id="product_category_loaner" model="product.category">
|
||||
<field name="name">Loaner Equipment</field>
|
||||
</record>
|
||||
<record id="product_category_loaner_rollator" model="product.category">
|
||||
<field name="name">Rollators</field>
|
||||
<field name="parent_id" ref="product_category_loaner"/>
|
||||
</record>
|
||||
<record id="product_category_loaner_wheelchair" model="product.category">
|
||||
<field name="name">Wheelchairs</field>
|
||||
<field name="parent_id" ref="product_category_loaner"/>
|
||||
</record>
|
||||
<record id="product_category_loaner_powerchair" model="product.category">
|
||||
<field name="name">Powerchairs</field>
|
||||
<field name="parent_id" ref="product_category_loaner"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
34
fusion_claims/fusion_claims/models/__init__.py
Normal file
34
fusion_claims/fusion_claims/models/__init__.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# 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
|
||||
from . import fusion_central_config
|
||||
from . import fusion_adp_device_code
|
||||
from . import product_template
|
||||
from . import product_product
|
||||
from . import sale_order
|
||||
from . import sale_order_line
|
||||
from . import account_move
|
||||
from . import account_move_line
|
||||
from . import account_payment
|
||||
from . import account_payment_method_line
|
||||
from . import submission_history
|
||||
from . import fusion_loaner_checkout
|
||||
from . import fusion_loaner_history
|
||||
from . import client_profile
|
||||
from . import adp_application_data
|
||||
from . import xml_parser
|
||||
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
|
||||
1217
fusion_claims/fusion_claims/models/account_move.py
Normal file
1217
fusion_claims/fusion_claims/models/account_move.py
Normal file
File diff suppressed because it is too large
Load Diff
247
fusion_claims/fusion_claims/models/account_move_line.py
Normal file
247
fusion_claims/fusion_claims/models/account_move_line.py
Normal file
@@ -0,0 +1,247 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
# ==========================================================================
|
||||
# SERIAL NUMBER AND DEVICE PLACEMENT
|
||||
# ==========================================================================
|
||||
x_fc_serial_number = fields.Char(
|
||||
string='Serial Number',
|
||||
help='Serial number for this product',
|
||||
)
|
||||
x_fc_device_placement = fields.Selection(
|
||||
selection=[
|
||||
('L', 'Left'),
|
||||
('R', 'Right'),
|
||||
('NA', 'N/A'),
|
||||
],
|
||||
string='Device Placement',
|
||||
default='NA',
|
||||
help='Device placement position (Left/Right/N/A)',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# DEDUCTION FIELDS
|
||||
# ==========================================================================
|
||||
x_fc_deduction_type = fields.Selection(
|
||||
selection=[
|
||||
('none', 'No Deduction'),
|
||||
('pct', 'Percentage'),
|
||||
('amt', 'Amount'),
|
||||
],
|
||||
string='Deduction Type',
|
||||
default='none',
|
||||
help='Type of ADP deduction applied to this line',
|
||||
)
|
||||
x_fc_deduction_value = fields.Float(
|
||||
string='Deduction Value',
|
||||
digits='Product Price',
|
||||
help='Deduction value (percentage if PCT, dollar amount if AMT)',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# ADP REFERENCE FIELDS
|
||||
# ==========================================================================
|
||||
x_fc_adp_max_price = fields.Float(
|
||||
string='ADP Max Price',
|
||||
digits='Product Price',
|
||||
help='Maximum price ADP will cover for this device (from mobility manual)',
|
||||
)
|
||||
x_fc_sn_required = fields.Boolean(
|
||||
string='S/N Required',
|
||||
help='Is serial number required for this device?',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# ADP DEVICE APPROVAL TRACKING
|
||||
# ==========================================================================
|
||||
x_fc_adp_approved = fields.Boolean(
|
||||
string='ADP Approved',
|
||||
default=False,
|
||||
help='Was this device approved by ADP in the application approval?',
|
||||
)
|
||||
x_fc_adp_device_type = fields.Char(
|
||||
string='ADP Device Type',
|
||||
help='Device type from ADP mobility manual (for approval matching)',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# ADP PORTIONS - Stored fields set during invoice creation
|
||||
# ==========================================================================
|
||||
x_fc_adp_portion = fields.Monetary(
|
||||
string='ADP Portion',
|
||||
currency_field='currency_id',
|
||||
help='ADP portion for this line (calculated during invoice creation from device codes database)',
|
||||
)
|
||||
x_fc_client_portion = fields.Monetary(
|
||||
string='Client Portion',
|
||||
currency_field='currency_id',
|
||||
help='Client portion for this line (calculated during invoice creation from device codes database)',
|
||||
)
|
||||
|
||||
def _compute_adp_portions(self):
|
||||
"""Compute ADP and client portions based on device codes database.
|
||||
|
||||
This is called during invoice type/client type changes to recalculate portions.
|
||||
"""
|
||||
self.action_recalculate_portions()
|
||||
|
||||
def action_recalculate_portions(self):
|
||||
"""Manually recalculate ADP and client portions based on device codes database.
|
||||
|
||||
This can be called to recalculate portions if values are incorrect.
|
||||
Uses the same logic as invoice creation.
|
||||
"""
|
||||
ADPDevice = self.env['fusion.adp.device.code'].sudo()
|
||||
|
||||
for line in self:
|
||||
move = line.move_id
|
||||
if not move or move.move_type not in ['out_invoice', 'out_refund']:
|
||||
continue
|
||||
|
||||
if not line.product_id or line.quantity <= 0:
|
||||
continue
|
||||
|
||||
# Get client type
|
||||
client_type = move._get_client_type()
|
||||
if client_type == 'REG':
|
||||
base_adp_pct = 0.75
|
||||
base_client_pct = 0.25
|
||||
else:
|
||||
base_adp_pct = 1.0
|
||||
base_client_pct = 0.0
|
||||
|
||||
# Get ADP price from device codes database (priority)
|
||||
device_code = line._get_adp_device_code()
|
||||
adp_price = 0
|
||||
|
||||
if device_code:
|
||||
adp_device = ADPDevice.search([
|
||||
('device_code', '=', device_code),
|
||||
('active', '=', True)
|
||||
], limit=1)
|
||||
if adp_device:
|
||||
adp_price = adp_device.adp_price or 0
|
||||
|
||||
# Fallback to product fields
|
||||
if not adp_price and line.product_id:
|
||||
product_tmpl = line.product_id.product_tmpl_id
|
||||
if hasattr(product_tmpl, 'x_fc_adp_price'):
|
||||
adp_price = getattr(product_tmpl, 'x_fc_adp_price', 0) or 0
|
||||
# (Studio field fallback removed)
|
||||
|
||||
# Fallback to line max price or unit price
|
||||
if not adp_price:
|
||||
adp_price = line.x_fc_adp_max_price or line.price_unit
|
||||
|
||||
qty = line.quantity
|
||||
adp_base_total = adp_price * qty
|
||||
|
||||
# Apply deductions
|
||||
if line.x_fc_deduction_type == 'pct' and line.x_fc_deduction_value:
|
||||
effective_adp_pct = base_adp_pct * (line.x_fc_deduction_value / 100)
|
||||
adp_portion = adp_base_total * effective_adp_pct
|
||||
client_portion = adp_base_total - adp_portion
|
||||
elif line.x_fc_deduction_type == 'amt' and line.x_fc_deduction_value:
|
||||
base_adp_amount = adp_base_total * base_adp_pct
|
||||
adp_portion = max(0, base_adp_amount - line.x_fc_deduction_value)
|
||||
client_portion = adp_base_total - adp_portion
|
||||
else:
|
||||
adp_portion = adp_base_total * base_adp_pct
|
||||
client_portion = adp_base_total * base_client_pct
|
||||
|
||||
line.write({
|
||||
'x_fc_adp_portion': adp_portion,
|
||||
'x_fc_client_portion': client_portion,
|
||||
'x_fc_adp_max_price': adp_price,
|
||||
})
|
||||
|
||||
# ==========================================================================
|
||||
# GETTER METHODS
|
||||
# ==========================================================================
|
||||
def _get_adp_device_code(self):
|
||||
"""Get ADP device code from product.
|
||||
|
||||
Checks multiple sources in order and validates against ADP device database:
|
||||
1. x_fc_adp_device_code (module field) - verified in ADP database
|
||||
2. x_adp_code (Studio/custom field) - verified in ADP database
|
||||
3. default_code - verified in ADP database
|
||||
4. Code in parentheses in product name (e.g., "Product Name (SE0001109)")
|
||||
|
||||
Returns empty string if no valid ADP code found.
|
||||
"""
|
||||
import re
|
||||
|
||||
self.ensure_one()
|
||||
if not self.product_id:
|
||||
return ''
|
||||
|
||||
product_tmpl = self.product_id.product_tmpl_id
|
||||
ADPDevice = self.env['fusion.adp.device.code'].sudo()
|
||||
|
||||
# 1. Check x_fc_adp_device_code (module field)
|
||||
code = ''
|
||||
if hasattr(product_tmpl, 'x_fc_adp_device_code'):
|
||||
code = getattr(product_tmpl, 'x_fc_adp_device_code', '') or ''
|
||||
|
||||
# Verify code exists in ADP database
|
||||
if code and ADPDevice.search_count([('device_code', '=', code), ('active', '=', True)]) > 0:
|
||||
return code
|
||||
|
||||
# 2. Check x_adp_code (Studio/custom field)
|
||||
if hasattr(product_tmpl, 'x_adp_code'):
|
||||
code = getattr(product_tmpl, 'x_adp_code', '') or ''
|
||||
if code and ADPDevice.search_count([('device_code', '=', code), ('active', '=', True)]) > 0:
|
||||
return code
|
||||
|
||||
# 3. Check default_code - ONLY if it's a valid ADP code
|
||||
code = self.product_id.default_code or ''
|
||||
if code and ADPDevice.search_count([('device_code', '=', code), ('active', '=', True)]) > 0:
|
||||
return code
|
||||
|
||||
# 4. Try to extract code from product name in parentheses
|
||||
# E.g., "[MXA-1618] GEOMATRIX SILVERBACK MAX BACKREST - ACTIVE (SE0001109)"
|
||||
product_name = self.product_id.name or ''
|
||||
match = re.search(r'\(([A-Z]{2}\d{7})\)', product_name)
|
||||
if match:
|
||||
code = match.group(1)
|
||||
if ADPDevice.search_count([('device_code', '=', code), ('active', '=', True)]) > 0:
|
||||
return code
|
||||
|
||||
# 5. Last resort: check if there's a linked sale order line with ADP code
|
||||
if self.sale_line_ids:
|
||||
for sale_line in self.sale_line_ids:
|
||||
if hasattr(sale_line, '_get_adp_device_code'):
|
||||
sale_code = sale_line._get_adp_device_code()
|
||||
if sale_code:
|
||||
return sale_code
|
||||
|
||||
# No valid ADP code found - return empty to skip this line in export
|
||||
return ''
|
||||
|
||||
def _get_serial_number(self):
|
||||
"""Get serial number from mapped field or native field."""
|
||||
self.ensure_one()
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
field_name = ICP.get_param('fusion_claims.field_aml_serial', 'x_fc_serial_number')
|
||||
|
||||
# Try mapped field first
|
||||
if hasattr(self, field_name):
|
||||
value = getattr(self, field_name, None)
|
||||
if value:
|
||||
return value
|
||||
|
||||
# Fallback to native field
|
||||
return self.x_fc_serial_number or ''
|
||||
|
||||
def _get_device_placement(self):
|
||||
"""Get device placement."""
|
||||
self.ensure_one()
|
||||
return self.x_fc_device_placement or 'NA'
|
||||
21
fusion_claims/fusion_claims/models/account_payment.py
Normal file
21
fusion_claims/fusion_claims/models/account_payment.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
_inherit = 'account.payment'
|
||||
|
||||
x_fc_card_last_four = fields.Char(
|
||||
string='Card Last 4 Digits',
|
||||
size=4,
|
||||
help='Last 4 digits of the card used for payment (for card payments only)',
|
||||
)
|
||||
|
||||
x_fc_payment_note = fields.Char(
|
||||
string='Payment Note',
|
||||
help='Additional note for this payment (e.g., transaction reference)',
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class AccountPaymentMethodLine(models.Model):
|
||||
_inherit = 'account.payment.method.line'
|
||||
|
||||
x_fc_requires_card_digits = fields.Boolean(
|
||||
string='Requires Card Digits',
|
||||
default=False,
|
||||
help='If checked, the user must enter the last 4 digits of the card when using this payment method.',
|
||||
)
|
||||
670
fusion_claims/fusion_claims/models/adp_application_data.py
Normal file
670
fusion_claims/fusion_claims/models/adp_application_data.py
Normal file
@@ -0,0 +1,670 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionAdpApplicationData(models.Model):
|
||||
_name = 'fusion.adp.application.data'
|
||||
_description = 'ADP Application Data (Parsed XML)'
|
||||
_order = 'application_date desc, id desc'
|
||||
_rec_name = 'display_name'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# LINKAGE
|
||||
# ------------------------------------------------------------------
|
||||
profile_id = fields.Many2one(
|
||||
'fusion.client.profile', string='Client Profile',
|
||||
ondelete='cascade', index=True,
|
||||
)
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order', string='Sale Order',
|
||||
ondelete='set null', index=True,
|
||||
)
|
||||
display_name = fields.Char(
|
||||
string='Name', compute='_compute_display_name', store=True,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# COMPLETE XML DATA (for round-trip export fidelity)
|
||||
# ------------------------------------------------------------------
|
||||
xml_data_json = fields.Text(
|
||||
string='Complete XML Data (JSON)',
|
||||
help='Complete 1:1 JSON representation of all ~300 XML fields for export',
|
||||
)
|
||||
raw_xml = fields.Text(string='Raw XML Data')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# APPLICATION METADATA
|
||||
# ------------------------------------------------------------------
|
||||
device_category = fields.Selection([
|
||||
('AA', 'Ambulation Aids (Section 2a)'),
|
||||
('MD', 'Mobility Devices (Section 2b/2c)'),
|
||||
('PS', 'Positioning/Seating (Section 2d)'),
|
||||
('MX', 'Mixed/Multiple Sections'),
|
||||
], string='Device Category')
|
||||
version_number = fields.Char(string='Form Version')
|
||||
application_date = fields.Date(string='Application Date')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 1 - APPLICANT BIOGRAPHICAL INFORMATION
|
||||
# ------------------------------------------------------------------
|
||||
applicant_last_name = fields.Char(string='Last Name')
|
||||
applicant_first_name = fields.Char(string='First Name')
|
||||
applicant_middle_initial = fields.Char(string='Middle Initial')
|
||||
health_card_number = fields.Char(string='Health Card Number', index=True)
|
||||
health_card_version = fields.Char(string='Health Card Version')
|
||||
date_of_birth = fields.Date(string='Date of Birth')
|
||||
ltch_name = fields.Char(string='Long-Term Care Home')
|
||||
|
||||
# Address (individual fields, not combined)
|
||||
unit_number = fields.Char(string='Unit Number')
|
||||
street_number = fields.Char(string='Street Number')
|
||||
street_name = fields.Char(string='Street Name')
|
||||
rural_route = fields.Char(string='Lot/Concession/Rural Route')
|
||||
city = fields.Char(string='City', index=True)
|
||||
province = fields.Char(string='Province')
|
||||
postal_code = fields.Char(string='Postal Code')
|
||||
|
||||
# Contact
|
||||
home_phone = fields.Char(string='Home Phone')
|
||||
business_phone = fields.Char(string='Business Phone')
|
||||
phone_extension = fields.Char(string='Phone Extension')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 1 - CONFIRMATION OF BENEFITS
|
||||
# ------------------------------------------------------------------
|
||||
receives_social_assistance = fields.Boolean(string='Receives Social Assistance')
|
||||
benefit_type = fields.Char(string='Benefit Program')
|
||||
benefit_owp = fields.Boolean(string='Ontario Works Program (OWP)')
|
||||
benefit_odsp = fields.Boolean(string='Ontario Disability Support Program (ODSP)')
|
||||
benefit_acsd = fields.Boolean(string='Assistance to Children with Severe Disabilities (ACSD)')
|
||||
wsib_eligible = fields.Boolean(string='WSIB Eligible')
|
||||
vac_eligible = fields.Boolean(string='Veterans Affairs Canada (VAC) Eligible')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 2 - DEVICES AND ELIGIBILITY
|
||||
# ------------------------------------------------------------------
|
||||
medical_condition = fields.Text(string='Medical Condition / Diagnosis')
|
||||
mobility_status = fields.Text(string='Functional Mobility Status')
|
||||
|
||||
# Previously funded equipment
|
||||
prev_funded_none = fields.Boolean(string='None Previously Funded')
|
||||
prev_funded_forearm = fields.Boolean(string='Forearm Crutches (Previously)')
|
||||
prev_funded_wheeled = fields.Boolean(string='Wheeled Walker (Previously)')
|
||||
prev_funded_manual = fields.Boolean(string='Manual Wheelchair (Previously)')
|
||||
prev_funded_power = fields.Boolean(string='Power Wheelchair (Previously)')
|
||||
prev_funded_addon = fields.Boolean(string='Power Add-On Device (Previously)')
|
||||
prev_funded_scooter = fields.Boolean(string='Power Scooter (Previously)')
|
||||
prev_funded_seating = fields.Boolean(string='Positioning Devices (Previously)')
|
||||
prev_funded_tilt = fields.Boolean(string='Power Tilt System (Previously)')
|
||||
prev_funded_recline = fields.Boolean(string='Power Recline System (Previously)')
|
||||
prev_funded_legrests = fields.Boolean(string='Power Elevating Leg Rests (Previously)')
|
||||
prev_funded_frame = fields.Boolean(string='Paediatric Standing Frame (Previously)')
|
||||
prev_funded_stroller = fields.Boolean(string='Paediatric Specialty Stroller (Previously)')
|
||||
|
||||
# Devices currently required
|
||||
device_forearm_crutches = fields.Boolean(string='Forearm Crutches')
|
||||
device_wheeled_walker = fields.Boolean(string='Wheeled Walker')
|
||||
device_manual_wheelchair = fields.Boolean(string='Manual Wheelchair')
|
||||
device_ambulation_manual = fields.Boolean(string='Ambulation Aid + Manual Wheelchair')
|
||||
device_dependent_wheelchair = fields.Boolean(string='Manual Wheelchair (Dependent)')
|
||||
device_dynamic_tilt = fields.Boolean(string='Manual Dynamic Tilt Wheelchair')
|
||||
device_manual_dynamic = fields.Boolean(string='Manual Dynamic Tilt (Dependent)')
|
||||
device_manual_power_addon = fields.Boolean(string='Manual Wheelchair with Power Add-On')
|
||||
device_power_base = fields.Boolean(string='Power Base Only')
|
||||
device_power_scooter = fields.Boolean(string='Power Scooter Only')
|
||||
device_ambulation_power = fields.Boolean(string='Ambulation Aid + Power Base/Scooter')
|
||||
device_positioning = fields.Boolean(string='Positioning Devices (Seating)')
|
||||
device_high_tech = fields.Boolean(string='High Technology Power Base')
|
||||
device_standing_frame = fields.Boolean(string='Paediatric Standing Frame')
|
||||
device_adp_funded_mods = fields.Boolean(string='Modifications to ADP Funded Device(s)')
|
||||
device_non_adp_funded_mods = fields.Boolean(string='Modifications to Non ADP Funded Device(s)')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 2A - AMBULATION AIDS (Walkers)
|
||||
# ------------------------------------------------------------------
|
||||
s2a_base_device = fields.Char(string='Walker Type')
|
||||
s2a_paediatric_frame = fields.Char(string='Paediatric Frame')
|
||||
s2a_forearm_crutches = fields.Char(string='Forearm Crutches Type')
|
||||
s2a_none = fields.Char(string='None Selected')
|
||||
s2a_reason = fields.Char(string='Reason for Application')
|
||||
s2a_replacement_status = fields.Char(string='Replacement - Mobility Status Change')
|
||||
s2a_replacement_size = fields.Char(string='Replacement - Body Size Change')
|
||||
s2a_replacement_adp = fields.Char(string='Replacement - Equipment Worn Out')
|
||||
s2a_replacement_special = fields.Char(string='Replacement - Special Circumstances')
|
||||
s2a_confirm1 = fields.Char(string='Confirmation 1')
|
||||
s2a_confirm2 = fields.Char(string='Confirmation 2')
|
||||
s2a_confirm3 = fields.Char(string='Confirmation 3')
|
||||
s2a_confirm4 = fields.Char(string='Confirmation 4')
|
||||
s2a_confirm5 = fields.Char(string='Confirmation 5')
|
||||
s2a_confirm6 = fields.Char(string='Confirmation 6')
|
||||
# Prescription
|
||||
s2a_seat_height = fields.Char(string='Seat Height')
|
||||
s2a_seat_height_unit = fields.Char(string='Seat Height Unit')
|
||||
s2a_handle_height = fields.Char(string='Push Handle Height')
|
||||
s2a_handle_height_unit = fields.Char(string='Handle Height Unit')
|
||||
s2a_hand_grips = fields.Char(string='Hand Grips')
|
||||
s2a_forearm_attachments = fields.Char(string='Forearm Attachments')
|
||||
s2a_width_handles = fields.Char(string='Width Between Push Handles')
|
||||
s2a_width_handles_unit = fields.Char(string='Width Handles Unit')
|
||||
s2a_client_weight = fields.Char(string='Client Weight')
|
||||
s2a_client_weight_unit = fields.Char(string='Client Weight Unit')
|
||||
s2a_brakes = fields.Char(string='Brakes')
|
||||
s2a_brake_type = fields.Char(string='Brake Type')
|
||||
s2a_num_wheels = fields.Char(string='Number of Wheels')
|
||||
s2a_wheel_size = fields.Char(string='Wheel Size')
|
||||
s2a_back_support = fields.Char(string='Back Support')
|
||||
# ADP options
|
||||
s2a_adp_walker = fields.Char(string='ADP Adolescent Walker')
|
||||
s2a_adp_frame = fields.Char(string='ADP Adolescent Frame')
|
||||
s2a_adp_standing = fields.Char(string='ADP Adolescent Standing')
|
||||
# Custom modifications
|
||||
s2a_custom = fields.Char(string='Custom Modifications Required')
|
||||
s2a_cost_labour = fields.Char(string='Cost of Labour')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 2B - MANUAL WHEELCHAIRS
|
||||
# ------------------------------------------------------------------
|
||||
s2b_base_device = fields.Char(string='Manual Wheelchair Type')
|
||||
s2b_power_addon = fields.Char(string='Power Add-On Device')
|
||||
s2b_reason = fields.Char(string='Reason for Application')
|
||||
s2b_replacement_status = fields.Char(string='Replacement - Mobility Status')
|
||||
s2b_replacement_size = fields.Char(string='Replacement - Body Size')
|
||||
s2b_replacement_adp = fields.Char(string='Replacement - Equipment Worn')
|
||||
s2b_replacement_special = fields.Char(string='Replacement - Special')
|
||||
s2b_confirm1 = fields.Char(string='Confirmation 1')
|
||||
s2b_confirm2 = fields.Char(string='Confirmation 2')
|
||||
s2b_confirm3 = fields.Char(string='Confirmation 3')
|
||||
s2b_confirm4 = fields.Char(string='Confirmation 4')
|
||||
s2b_confirm5 = fields.Char(string='Confirmation 5')
|
||||
s2b_confirm6 = fields.Char(string='Confirmation 6')
|
||||
s2b_confirm7 = fields.Char(string='Confirmation 7')
|
||||
s2b_confirm8 = fields.Char(string='Confirmation 8')
|
||||
s2b_confirm9 = fields.Char(string='Confirmation 9')
|
||||
s2b_confirm10 = fields.Char(string='Confirmation 10')
|
||||
s2b_confirm11 = fields.Char(string='Confirmation 11')
|
||||
s2b_confirm12 = fields.Char(string='Confirmation 12')
|
||||
s2b_confirm13 = fields.Char(string='Confirmation 13')
|
||||
# Prescription
|
||||
s2b_seat_width = fields.Char(string='Seat Width')
|
||||
s2b_seat_width_unit = fields.Char(string='Seat Width Unit')
|
||||
s2b_seat_depth = fields.Char(string='Seat Depth')
|
||||
s2b_seat_depth_unit = fields.Char(string='Seat Depth Unit')
|
||||
s2b_floor_height = fields.Char(string='Finished Seat to Floor Height')
|
||||
s2b_floor_height_unit = fields.Char(string='Floor Height Unit')
|
||||
s2b_cane_height = fields.Char(string='Back Cane Height')
|
||||
s2b_cane_height_unit = fields.Char(string='Cane Height Unit')
|
||||
s2b_back_height = fields.Char(string='Finished Back Height')
|
||||
s2b_back_height_unit = fields.Char(string='Back Height Unit')
|
||||
s2b_rest_length = fields.Char(string='Finished Leg Rest Length')
|
||||
s2b_rest_length_unit = fields.Char(string='Rest Length Unit')
|
||||
s2b_client_weight = fields.Char(string='Client Weight')
|
||||
s2b_client_weight_unit = fields.Char(string='Client Weight Unit')
|
||||
# Add-on options
|
||||
s2b_adjustable_tension = fields.Boolean(string='Adjustable Tension Back Upholstery')
|
||||
s2b_heavy_duty = fields.Boolean(string='Heavy Duty Cross Braces & Upholstery')
|
||||
s2b_recliner = fields.Boolean(string='Recliner Option')
|
||||
s2b_footplates = fields.Boolean(string='Angle Adjustable Footplates')
|
||||
s2b_legrests = fields.Boolean(string='Elevating Legrests')
|
||||
s2b_spoke = fields.Boolean(string='Spoke Protectors')
|
||||
s2b_projected = fields.Boolean(string='Projected Handrims')
|
||||
s2b_standard_manual = fields.Boolean(string='Standard Manual with Dynamic Tilt')
|
||||
s2b_grade_aids = fields.Boolean(string='Grade Aids')
|
||||
s2b_caster_pin = fields.Boolean(string='Caster Pin Locks')
|
||||
s2b_amputee_axle = fields.Boolean(string='Amputee Axle Plates')
|
||||
s2b_quick_release = fields.Boolean(string='Quick Release Axles')
|
||||
s2b_stroller = fields.Boolean(string='Stroller Handles/Paediatric')
|
||||
s2b_oxygen = fields.Boolean(string='Oxygen Tank Holder')
|
||||
s2b_ventilator = fields.Boolean(string='Ventilator Tray')
|
||||
s2b_titanium = fields.Boolean(string='Titanium Frame')
|
||||
s2b_clothing_guards = fields.Boolean(string='Clothing Guards')
|
||||
s2b_one_arm = fields.Boolean(string='One Arm/Lever Drive')
|
||||
s2b_uni_lateral = fields.Boolean(string='Uni-Lateral Wheel Lock')
|
||||
s2b_plastic = fields.Boolean(string='Plastic Coated Handrims')
|
||||
s2b_rationale = fields.Text(string='Clinical Rationale')
|
||||
s2b_custom = fields.Char(string='Custom Modifications Required')
|
||||
s2b_cost_labour = fields.Char(string='Cost of Labour')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 2C - POWER BASES AND POWER SCOOTERS
|
||||
# ------------------------------------------------------------------
|
||||
s2c_base_device = fields.Char(string='Power Base/Scooter Type')
|
||||
s2c_reason = fields.Char(string='Reason for Application')
|
||||
s2c_replacement_status = fields.Char(string='Replacement - Mobility Status')
|
||||
s2c_replacement_size = fields.Char(string='Replacement - Body Size')
|
||||
s2c_replacement_adp = fields.Char(string='Replacement - Equipment Worn')
|
||||
s2c_replacement_special = fields.Char(string='Replacement - Special')
|
||||
s2c_confirm1 = fields.Char(string='Power Base Confirmation 1')
|
||||
s2c_confirm2 = fields.Char(string='Power Base Confirmation 2')
|
||||
s2c_confirm3 = fields.Char(string='Scooter Confirmation 1')
|
||||
s2c_confirm4 = fields.Char(string='Scooter Confirmation 2')
|
||||
s2c_confirm5 = fields.Char(string='Scooter Confirmation 3')
|
||||
# Prescription
|
||||
s2c_seat_width = fields.Char(string='Seat Width')
|
||||
s2c_seat_width_unit = fields.Char(string='Seat Width Unit')
|
||||
s2c_back_height = fields.Char(string='Finished Back Height')
|
||||
s2c_back_height_unit = fields.Char(string='Back Height Unit')
|
||||
s2c_floor_height = fields.Char(string='Finished Seat to Floor Height')
|
||||
s2c_floor_height_unit = fields.Char(string='Floor Height Unit')
|
||||
s2c_rest_length = fields.Char(string='Leg Rest Length')
|
||||
s2c_rest_length_unit = fields.Char(string='Rest Length Unit')
|
||||
s2c_seat_depth = fields.Char(string='Seat Depth')
|
||||
s2c_seat_depth_unit = fields.Char(string='Seat Depth Unit')
|
||||
s2c_client_weight = fields.Char(string='Client Weight')
|
||||
s2c_client_weight_unit = fields.Char(string='Client Weight Unit')
|
||||
# Add-on options
|
||||
s2c_adjustable_tension = fields.Boolean(string='Adjustable Tension Back Upholstery')
|
||||
s2c_midline = fields.Boolean(string='Midline Control')
|
||||
s2c_manual_recline = fields.Boolean(string='Manual Recline Option')
|
||||
s2c_footplates = fields.Boolean(string='Angle Adjustable Footplates')
|
||||
s2c_legrests = fields.Boolean(string='Manual Elevating Legrests')
|
||||
s2c_swingaway = fields.Boolean(string='Swingaway Mounting Bracket')
|
||||
s2c_one_piece = fields.Boolean(string='One Piece 90/90 Front Riggings')
|
||||
s2c_seat_package_1 = fields.Boolean(string='Seat Package 1 for Power Bases')
|
||||
s2c_seat_package_2 = fields.Boolean(string='Seat Package 2 for Power Bases')
|
||||
s2c_oxygen = fields.Boolean(string='Oxygen Tank Holder')
|
||||
s2c_ventilator = fields.Boolean(string='Ventilator Tray')
|
||||
# Specialty controls
|
||||
s2c_sp_controls_1 = fields.Boolean(string='Specialty Controls 1 - Non Standard Joystick')
|
||||
s2c_sp_controls_2 = fields.Boolean(string='Specialty Controls 2 - Chin/Rim Control')
|
||||
s2c_sp_controls_3 = fields.Boolean(string='Specialty Controls 3 - Simple Touch')
|
||||
s2c_sp_controls_4 = fields.Boolean(string='Specialty Controls 4 - Proximity Control')
|
||||
s2c_sp_controls_5 = fields.Boolean(string='Specialty Controls 5 - Breath Control')
|
||||
s2c_sp_controls_6 = fields.Boolean(string='Specialty Controls 6 - Scanners')
|
||||
s2c_auto_correction = fields.Boolean(string='Auto Correction System')
|
||||
s2c_rationale = fields.Text(string='Clinical Rationale')
|
||||
# Power positioning
|
||||
s2c_power_tilt = fields.Boolean(string='Power Tilt Only')
|
||||
s2c_power_recline = fields.Boolean(string='Power Recline Only')
|
||||
s2c_tilt_and_recline = fields.Boolean(string='Power Tilt and Recline')
|
||||
s2c_power_elevating = fields.Boolean(string='Power Elevating Footrests')
|
||||
s2c_control_box = fields.Boolean(string='Multi-Function Control Box')
|
||||
s2c_custom = fields.Char(string='Custom Modifications Required')
|
||||
s2c_cost_labour = fields.Char(string='Cost of Labour')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 2D - POSITIONING DEVICES (SEATING) FOR MOBILITY
|
||||
# ------------------------------------------------------------------
|
||||
# Seat Cushion
|
||||
s2d_seat_modular = fields.Boolean(string='Seat Cushion - Modular')
|
||||
s2d_seat_custom = fields.Boolean(string='Seat Cushion - Custom Fabricated')
|
||||
s2d_seat_cover_modular = fields.Boolean(string='Seat Cover - Modular')
|
||||
s2d_seat_cover_custom = fields.Boolean(string='Seat Cover - Custom Fabricated')
|
||||
s2d_seat_option_modular = fields.Boolean(string='Seat Options - Modular')
|
||||
s2d_seat_option_custom = fields.Boolean(string='Seat Options - Custom Fabricated')
|
||||
s2d_seat_hardware_modular = fields.Boolean(string='Seat Hardware - Modular')
|
||||
s2d_seat_hardware_custom = fields.Boolean(string='Seat Hardware - Custom Fabricated')
|
||||
s2d_adductor_modular = fields.Boolean(string='Pommel/Adductors - Modular')
|
||||
s2d_adductor_custom = fields.Boolean(string='Pommel/Adductors - Custom Fabricated')
|
||||
s2d_pommel_custom = fields.Boolean(string='Pommel Hardware - Custom Fabricated')
|
||||
# Back Support
|
||||
s2d_back_modular = fields.Boolean(string='Back Support - Modular')
|
||||
s2d_back_custom = fields.Boolean(string='Back Support - Custom Fabricated')
|
||||
s2d_back_option_modular = fields.Boolean(string='Back Options - Modular')
|
||||
s2d_back_option_custom = fields.Boolean(string='Back Options - Custom Fabricated')
|
||||
s2d_back_cover_custom = fields.Boolean(string='Back Cover - Custom Fabricated')
|
||||
s2d_back_hardware_modular = fields.Boolean(string='Back Hardware - Modular')
|
||||
s2d_back_hardware_custom = fields.Boolean(string='Back Hardware - Custom Fabricated')
|
||||
# Complete Assembly
|
||||
s2d_complete_modular = fields.Boolean(string='Complete Assembly - Modular')
|
||||
s2d_complete_custom = fields.Boolean(string='Complete Assembly - Custom Fabricated')
|
||||
# Headrest/Neckrest
|
||||
s2d_headrest_modular = fields.Boolean(string='Headrest/Neckrest - Modular')
|
||||
s2d_headrest_custom = fields.Boolean(string='Headrest/Neckrest - Custom Fabricated')
|
||||
s2d_head_option_custom = fields.Boolean(string='Headrest Options - Custom Fabricated')
|
||||
s2d_head_hardware_modular = fields.Boolean(string='Headrest Hardware - Modular')
|
||||
s2d_head_hardware_custom = fields.Boolean(string='Headrest Hardware - Custom Fabricated')
|
||||
# Positioning Belts
|
||||
s2d_belt_modular = fields.Boolean(string='Positioning Belt - Modular')
|
||||
s2d_belt_custom = fields.Boolean(string='Positioning Belt - Custom Fabricated')
|
||||
s2d_belt_option_custom = fields.Boolean(string='Belt Options - Custom Fabricated')
|
||||
# Arm Supports
|
||||
s2d_arm_modular = fields.Boolean(string='Arm Support - Modular')
|
||||
s2d_arm_custom = fields.Boolean(string='Arm Support - Custom Fabricated')
|
||||
s2d_arm_option_modular = fields.Boolean(string='Arm Options - Modular')
|
||||
s2d_arm_option_custom = fields.Boolean(string='Arm Options - Custom Fabricated')
|
||||
s2d_arm_hardware_modular = fields.Boolean(string='Arm Hardware - Modular')
|
||||
s2d_arm_hardware_custom = fields.Boolean(string='Arm Hardware - Custom Fabricated')
|
||||
# Tray
|
||||
s2d_tray_modular = fields.Boolean(string='Tray - Modular')
|
||||
s2d_tray_custom = fields.Boolean(string='Tray - Custom Fabricated')
|
||||
s2d_tray_option_modular = fields.Boolean(string='Tray Options - Modular')
|
||||
s2d_tray_option_custom = fields.Boolean(string='Tray Options - Custom Fabricated')
|
||||
# Lateral Supports
|
||||
s2d_lateral_modular = fields.Boolean(string='Lateral Support - Modular')
|
||||
s2d_lateral_custom = fields.Boolean(string='Lateral Support - Custom Fabricated')
|
||||
s2d_lateral_option_custom = fields.Boolean(string='Lateral Options - Custom Fabricated')
|
||||
s2d_lateral_hardware_custom = fields.Boolean(string='Lateral Hardware - Custom Fabricated')
|
||||
# Foot/Leg Supports
|
||||
s2d_foot_modular = fields.Boolean(string='Foot/Leg Support - Modular')
|
||||
s2d_foot_custom = fields.Boolean(string='Foot/Leg Support - Custom Fabricated')
|
||||
s2d_foot_option_modular = fields.Boolean(string='Foot Options - Modular')
|
||||
s2d_foot_option_custom = fields.Boolean(string='Foot Options - Custom Fabricated')
|
||||
s2d_foot_hardware_modular = fields.Boolean(string='Foot Hardware - Modular')
|
||||
s2d_foot_hardware_custom = fields.Boolean(string='Foot Hardware - Custom Fabricated')
|
||||
# Seating reason and confirmations
|
||||
s2d_reason = fields.Char(string='Reason for Application')
|
||||
s2d_replacement_status = fields.Char(string='Replacement - Mobility Status')
|
||||
s2d_replacement_size = fields.Char(string='Replacement - Body Size')
|
||||
s2d_replacement_adp = fields.Char(string='Replacement - Equipment Worn')
|
||||
s2d_replacement_special = fields.Char(string='Replacement - Special')
|
||||
s2d_confirm1 = fields.Char(string='Seating Confirmation 1')
|
||||
s2d_confirm2 = fields.Char(string='Seating Confirmation 2')
|
||||
s2d_custom = fields.Char(string='Custom Modifications Required')
|
||||
s2d_cost_labour = fields.Char(string='Cost of Labour')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 3 - APPLICANT CONSENT AND SIGNATURE
|
||||
# ------------------------------------------------------------------
|
||||
consent_date = fields.Date(string='Consent Date')
|
||||
consent_signed_by = fields.Selection([
|
||||
('applicant', 'Applicant'),
|
||||
('agent', 'Agent'),
|
||||
], string='Signed By')
|
||||
# Agent/Contact info (if signed by agent)
|
||||
agent_relationship = fields.Char(string='Agent Relationship')
|
||||
agent_last_name = fields.Char(string='Agent Last Name')
|
||||
agent_first_name = fields.Char(string='Agent First Name')
|
||||
agent_middle_initial = fields.Char(string='Agent Middle Initial')
|
||||
agent_unit = fields.Char(string='Agent Unit')
|
||||
agent_street_no = fields.Char(string='Agent Street Number')
|
||||
agent_street_name = fields.Char(string='Agent Street Name')
|
||||
agent_rural_route = fields.Char(string='Agent Rural Route')
|
||||
agent_city = fields.Char(string='Agent City')
|
||||
agent_province = fields.Char(string='Agent Province')
|
||||
agent_postal_code = fields.Char(string='Agent Postal Code')
|
||||
agent_home_phone = fields.Char(string='Agent Home Phone')
|
||||
agent_bus_phone = fields.Char(string='Agent Business Phone')
|
||||
agent_phone_ext = fields.Char(string='Agent Phone Ext')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 4 - AUTHORIZER
|
||||
# ------------------------------------------------------------------
|
||||
authorizer_last_name = fields.Char(string='Authorizer Last Name')
|
||||
authorizer_first_name = fields.Char(string='Authorizer First Name')
|
||||
authorizer_phone = fields.Char(string='Authorizer Phone')
|
||||
authorizer_phone_ext = fields.Char(string='Authorizer Phone Ext')
|
||||
authorizer_adp_number = fields.Char(string='Authorizer ADP Registration Number')
|
||||
assessment_date = fields.Date(string='Assessment Date')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 4 - VENDOR 1
|
||||
# ------------------------------------------------------------------
|
||||
vendor_business_name = fields.Char(string='Vendor Business Name')
|
||||
vendor_adp_number = fields.Char(string='Vendor ADP Registration Number')
|
||||
vendor_representative = fields.Char(string='Vendor Representative (Last, First)')
|
||||
vendor_position = fields.Char(string='Vendor Position Title')
|
||||
vendor_location = fields.Char(string='Vendor Location')
|
||||
vendor_phone = fields.Char(string='Vendor Phone')
|
||||
vendor_phone_ext = fields.Char(string='Vendor Phone Ext')
|
||||
vendor_sign_date = fields.Date(string='Vendor Sign Date')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 4 - VENDOR 2
|
||||
# ------------------------------------------------------------------
|
||||
vendor2_business_name = fields.Char(string='Vendor 2 Business Name')
|
||||
vendor2_adp_number = fields.Char(string='Vendor 2 ADP Registration')
|
||||
vendor2_representative = fields.Char(string='Vendor 2 Representative')
|
||||
vendor2_position = fields.Char(string='Vendor 2 Position')
|
||||
vendor2_location = fields.Char(string='Vendor 2 Location')
|
||||
vendor2_phone = fields.Char(string='Vendor 2 Phone')
|
||||
vendor2_phone_ext = fields.Char(string='Vendor 2 Phone Ext')
|
||||
vendor2_sign_date = fields.Date(string='Vendor 2 Sign Date')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 4 - EQUIPMENT SPEC & PROOF OF DELIVERY
|
||||
# ------------------------------------------------------------------
|
||||
equip_vendor_invoice_no = fields.Char(string='Vendor Invoice Number')
|
||||
equip_vendor_adp_reg = fields.Char(string='Vendor ADP Reg (Page 12)')
|
||||
equip_cell1 = fields.Char(string='ADP Device Code')
|
||||
equip_cell2 = fields.Char(string='Description of Item')
|
||||
equip_cell3 = fields.Char(string='Base Device')
|
||||
equip_cell4 = fields.Char(string='ADP Portion')
|
||||
equip_cell5 = fields.Char(string='Client Portion')
|
||||
pod_received_by = fields.Char(string='Proof of Delivery - Received By')
|
||||
pod_date = fields.Date(string='Proof of Delivery Date')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SECTION 4 - NOTE TO ADP (sections submitted checklist)
|
||||
# ------------------------------------------------------------------
|
||||
note_section1 = fields.Boolean(string='Section 1 Submitted')
|
||||
note_section2a = fields.Boolean(string='Section 2a Submitted')
|
||||
note_section2b = fields.Boolean(string='Section 2b Submitted')
|
||||
note_section2c = fields.Boolean(string='Section 2c Submitted')
|
||||
note_section2d = fields.Boolean(string='Section 2d Submitted')
|
||||
note_section3and4 = fields.Boolean(string='Section 3 & 4 Submitted')
|
||||
note_vendor_replacement = fields.Char(string='Vendor Quote - Replacement')
|
||||
note_vendor_custom = fields.Char(string='Vendor Quote - Custom Modifications')
|
||||
note_funding_chart = fields.Char(string='Justification for Funding Chart')
|
||||
note_letter = fields.Char(string='Letter of Rationale')
|
||||
|
||||
# Computed summary
|
||||
sections_submitted = fields.Char(
|
||||
string='Sections Submitted',
|
||||
compute='_compute_sections_submitted', store=True,
|
||||
)
|
||||
|
||||
# Legacy compat fields
|
||||
base_device = fields.Char(
|
||||
string='Base Device Selected',
|
||||
compute='_compute_base_device', store=True,
|
||||
)
|
||||
reason_for_application = fields.Char(
|
||||
string='Reason for Application',
|
||||
compute='_compute_reason', store=True,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# COMPUTED
|
||||
# ------------------------------------------------------------------
|
||||
@api.depends('applicant_last_name', 'applicant_first_name', 'application_date')
|
||||
def _compute_display_name(self):
|
||||
for rec in self:
|
||||
name_parts = [rec.applicant_last_name or '', rec.applicant_first_name or '']
|
||||
name = ', '.join(p for p in name_parts if p) or 'Unknown'
|
||||
date_str = rec.application_date.strftime('%Y-%m-%d') if rec.application_date else 'No Date'
|
||||
rec.display_name = f'{name} ({date_str})'
|
||||
|
||||
@api.depends('note_section1', 'note_section2a', 'note_section2b',
|
||||
'note_section2c', 'note_section2d', 'note_section3and4')
|
||||
def _compute_sections_submitted(self):
|
||||
for rec in self:
|
||||
parts = []
|
||||
if rec.note_section1:
|
||||
parts.append('1')
|
||||
if rec.note_section2a:
|
||||
parts.append('2a')
|
||||
if rec.note_section2b:
|
||||
parts.append('2b')
|
||||
if rec.note_section2c:
|
||||
parts.append('2c')
|
||||
if rec.note_section2d:
|
||||
parts.append('2d')
|
||||
if rec.note_section3and4:
|
||||
parts.append('3+4')
|
||||
rec.sections_submitted = ', '.join(parts) if parts else ''
|
||||
|
||||
@api.depends('s2a_base_device', 's2b_base_device', 's2c_base_device')
|
||||
def _compute_base_device(self):
|
||||
for rec in self:
|
||||
rec.base_device = rec.s2a_base_device or rec.s2b_base_device or rec.s2c_base_device or ''
|
||||
|
||||
@api.depends('s2a_reason', 's2b_reason', 's2c_reason', 's2d_reason')
|
||||
def _compute_reason(self):
|
||||
for rec in self:
|
||||
rec.reason_for_application = rec.s2a_reason or rec.s2b_reason or rec.s2c_reason or rec.s2d_reason or ''
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# XML EXPORT
|
||||
# ------------------------------------------------------------------
|
||||
def action_export_xml(self):
|
||||
"""Reconstruct ADP XML from stored JSON data."""
|
||||
self.ensure_one()
|
||||
if not self.xml_data_json:
|
||||
# Fall back to raw_xml if available
|
||||
if self.raw_xml:
|
||||
xml_content = self.raw_xml.encode('utf-8')
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': f'{self.applicant_last_name}_{self.applicant_first_name}_data.xml',
|
||||
'type': 'binary',
|
||||
'datas': base64.b64encode(xml_content),
|
||||
'mimetype': 'application/xml',
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': f'/web/content/{attachment.id}?download=true',
|
||||
'target': 'new',
|
||||
}
|
||||
return False
|
||||
|
||||
try:
|
||||
data = json.loads(self.xml_data_json)
|
||||
xml_str = self._json_to_xml(data)
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': f'{self.applicant_last_name or "export"}_{self.applicant_first_name or "data"}_data.xml',
|
||||
'type': 'binary',
|
||||
'datas': base64.b64encode(xml_str.encode('utf-8')),
|
||||
'mimetype': 'application/xml',
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': f'/web/content/{attachment.id}?download=true',
|
||||
'target': 'new',
|
||||
}
|
||||
except Exception as e:
|
||||
_logger.exception('XML export error: %s', e)
|
||||
return False
|
||||
|
||||
def _json_to_xml(self, data):
|
||||
"""Reconstruct the ADP XML from flat JSON dictionary."""
|
||||
# Build the XML tree following the exact ADP structure
|
||||
root = ET.Element('form1')
|
||||
form = ET.SubElement(root, 'Form')
|
||||
|
||||
# Simple top-level fields
|
||||
self._set_el(form, 'deviceCategory', data.get('deviceCategory', ''))
|
||||
self._set_el(form, 'VersionNumber', data.get('VersionNumber', ''))
|
||||
|
||||
# Section 1
|
||||
s1 = ET.SubElement(form, 'section1')
|
||||
s1_fields = [
|
||||
'applicantLastname', 'applicantFirstname', 'applicantMiddleinitial',
|
||||
'healthNo', 'versionNo', 'DateOfBirth', 'nameLTCH',
|
||||
'unitNo', 'streetNo', 'streetName', 'rrRoute',
|
||||
'city', 'province', 'postalCode',
|
||||
'homePhone', 'busPhone', 'phoneExtension',
|
||||
]
|
||||
for f in s1_fields:
|
||||
self._set_el(s1, f, data.get(f'section1.{f}', ''))
|
||||
|
||||
# Confirmation of benefit
|
||||
cob = ET.SubElement(s1, 'confirmationOfBenefit')
|
||||
for f in ['q1Yn', 'q1Ifyes', 'q2Yn', 'q3Yn']:
|
||||
self._set_el(cob, f, data.get(f'section1.confirmationOfBenefit.{f}', ''))
|
||||
|
||||
# Section 2
|
||||
s2 = ET.SubElement(form, 'section2')
|
||||
|
||||
# Devices and Eligibility
|
||||
de = ET.SubElement(s2, 'devicesandEligibility')
|
||||
de_fields = [
|
||||
'condition', 'status', 'none', 'forearm', 'wheeled', 'manual',
|
||||
'power', 'addOn', 'scooter', 'seating', 'tiltSystem', 'reclineSystem',
|
||||
'legRests', 'frame', 'stroller', 'deviceForearm', 'deviceWheeled',
|
||||
'deviceManual', 'deviceAmbulation', 'deviceDependent', 'deviceDynamic',
|
||||
'manualDyanmic', 'manualWheelchair', 'powerBase', 'powerScooter',
|
||||
'ambulation', 'positioning', 'highTech', 'standingFrame',
|
||||
'adpFunded', 'nonADPFunded',
|
||||
]
|
||||
for f in de_fields:
|
||||
self._set_el(de, f, data.get(f'section2.devicesandEligibility.{f}', ''))
|
||||
|
||||
# Sections 2a, 2b, 2c, 2d
|
||||
for section_key in ['section2a', 'section2b', 'section2c', 'section2d']:
|
||||
section_data = {k.split(f'section2.{section_key}.')[1]: v
|
||||
for k, v in data.items()
|
||||
if k.startswith(f'section2.{section_key}.')}
|
||||
sec = ET.SubElement(s2, section_key)
|
||||
# Preserve field order from the data keys
|
||||
ordered_keys = [k.split(f'section2.{section_key}.')[1]
|
||||
for k in sorted(data.keys())
|
||||
if k.startswith(f'section2.{section_key}.')]
|
||||
for f in ordered_keys:
|
||||
self._set_el(sec, f, section_data.get(f, ''))
|
||||
|
||||
# Section 3
|
||||
s3 = ET.SubElement(form, 'section3')
|
||||
sig = ET.SubElement(s3, 'sig')
|
||||
for f in ['signature', 'person', 'Date']:
|
||||
self._set_el(sig, f, data.get(f'section3.sig.{f}', ''))
|
||||
contact = ET.SubElement(s3, 'contact')
|
||||
contact_fields = [
|
||||
'relationship', 'applicantLastname', 'applicantFirstname',
|
||||
'applicantMiddleinitial', 'unitNo', 'streetNo', 'streetName',
|
||||
'rrRoute', 'city', 'province', 'postalCode',
|
||||
'homePhone', 'busPhone', 'phoneExtension',
|
||||
]
|
||||
for f in contact_fields:
|
||||
self._set_el(contact, f, data.get(f'section3.contact.{f}', ''))
|
||||
|
||||
# Section 4
|
||||
s4 = ET.SubElement(form, 'section4')
|
||||
# Authorizer
|
||||
auth = ET.SubElement(s4, 'authorizer')
|
||||
for f in ['authorizerLastname', 'authorizerFirstname', 'busPhone',
|
||||
'phoneExtension', 'adpNo', 'signature', 'Date']:
|
||||
self._set_el(auth, f, data.get(f'section4.authorizer.{f}', ''))
|
||||
# Vendor
|
||||
vendor = ET.SubElement(s4, 'vendor')
|
||||
for f in ['vendorBusName', 'adpVendorRegNo', 'vendorLastfirstname',
|
||||
'positionTitle', 'vendorLocation', 'busPhone',
|
||||
'phoneExtension', 'signature', 'Date']:
|
||||
self._set_el(vendor, f, data.get(f'section4.vendor.{f}', ''))
|
||||
# Vendor 2
|
||||
v2 = ET.SubElement(s4, 'vendor2')
|
||||
for f in ['vendorBusName', 'adpVendorRegNo', 'vendorLastfirstname',
|
||||
'positionTitle', 'vendorLocation', 'busPhone',
|
||||
'phoneExtension', 'signature', 'Date']:
|
||||
self._set_el(v2, f, data.get(f'section4.vendor2.{f}', ''))
|
||||
# Equipment Spec
|
||||
eq = ET.SubElement(s4, 'equipmentSpec')
|
||||
self._set_el(eq, 'vendorInvoiceNo', data.get('section4.equipmentSpec.vendorInvoiceNo', ''))
|
||||
self._set_el(eq, 'vendorADPRegNo', data.get('section4.equipmentSpec.vendorADPRegNo', ''))
|
||||
t2 = ET.SubElement(eq, 'Table2')
|
||||
r1 = ET.SubElement(t2, 'Row1')
|
||||
for c in ['Cell1', 'Cell2', 'Cell3', 'Cell4', 'Cell5']:
|
||||
self._set_el(r1, c, data.get(f'section4.equipmentSpec.Table2.Row1.{c}', ''))
|
||||
# Proof of delivery
|
||||
pod = ET.SubElement(s4, 'proofOfDelivery')
|
||||
for f in ['signature', 'receivedBy', 'Date']:
|
||||
self._set_el(pod, f, data.get(f'section4.proofOfDelivery.{f}', ''))
|
||||
# Note to ADP
|
||||
note = ET.SubElement(s4, 'noteToADP')
|
||||
for f in ['section1', 'section2a', 'section2b', 'section2c', 'section2d',
|
||||
'section3and4', 'vendorReplacement', 'vendorCustom',
|
||||
'fundingChart', 'letter']:
|
||||
self._set_el(note, f, data.get(f'section4.noteToADP.{f}', ''))
|
||||
|
||||
# Convert to string
|
||||
tree = ET.ElementTree(root)
|
||||
ET.indent(tree, space='')
|
||||
import io
|
||||
buf = io.BytesIO()
|
||||
tree.write(buf, encoding='unicode', xml_declaration=True)
|
||||
return buf.getvalue()
|
||||
|
||||
@staticmethod
|
||||
def _set_el(parent, tag, value):
|
||||
"""Create a child element, self-closing if empty."""
|
||||
el = ET.SubElement(parent, tag)
|
||||
if value:
|
||||
el.text = str(value)
|
||||
262
fusion_claims/fusion_claims/models/adp_posting_schedule.py
Normal file
262
fusion_claims/fusion_claims/models/adp_posting_schedule.py
Normal file
@@ -0,0 +1,262 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
import logging
|
||||
from datetime import date, timedelta
|
||||
from odoo import models, api
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ADPPostingScheduleMixin(models.AbstractModel):
|
||||
"""Mixin providing ADP posting schedule calculation methods.
|
||||
|
||||
This mixin can be inherited by any model that needs to calculate
|
||||
ADP posting dates and deadlines.
|
||||
|
||||
Posting Schedule Logic:
|
||||
- Posting days occur every N days (default 14) from a base date
|
||||
- Submission deadline: Wednesday 6 PM before posting day
|
||||
- Delivery reminder: Tuesday of posting week
|
||||
- Billing reminder: Monday of posting week
|
||||
- Payment processed: Posting day + 7 days
|
||||
- Payment received: Posting day + 10 days
|
||||
"""
|
||||
_name = 'fusion_claims.adp.posting.schedule.mixin'
|
||||
_description = 'ADP Posting Schedule Mixin'
|
||||
|
||||
@api.model
|
||||
def _get_adp_posting_base_date(self):
|
||||
"""Get the configured base posting date from settings."""
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
base_date_str = ICP.get_param('fusion_claims.adp_posting_base_date', '2026-01-23')
|
||||
try:
|
||||
return date.fromisoformat(base_date_str)
|
||||
except (ValueError, TypeError):
|
||||
return date(2026, 1, 23)
|
||||
|
||||
@api.model
|
||||
def _get_adp_posting_frequency(self):
|
||||
"""Get the configured posting frequency in days from settings."""
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
frequency = ICP.get_param('fusion_claims.adp_posting_frequency_days', '14')
|
||||
try:
|
||||
return int(frequency)
|
||||
except (ValueError, TypeError):
|
||||
return 14
|
||||
|
||||
@api.model
|
||||
def _get_next_posting_date(self, from_date=None):
|
||||
"""Calculate the next ADP posting date from a given date.
|
||||
|
||||
Args:
|
||||
from_date: The date to calculate from (default: today)
|
||||
|
||||
Returns:
|
||||
date: The next posting date
|
||||
"""
|
||||
if from_date is None:
|
||||
from_date = date.today()
|
||||
elif hasattr(from_date, 'date'):
|
||||
from_date = from_date.date()
|
||||
|
||||
base_date = self._get_adp_posting_base_date()
|
||||
frequency = self._get_adp_posting_frequency()
|
||||
|
||||
if frequency <= 0:
|
||||
frequency = 14
|
||||
|
||||
# Calculate days since base date
|
||||
days_diff = (from_date - base_date).days
|
||||
|
||||
if days_diff < 0:
|
||||
# from_date is before base_date, so base_date is the next posting date
|
||||
return base_date
|
||||
|
||||
# Calculate how many complete cycles have passed
|
||||
cycles_passed = days_diff // frequency
|
||||
|
||||
# The next posting date is (cycles_passed + 1) * frequency days from base
|
||||
next_posting = base_date + timedelta(days=(cycles_passed + 1) * frequency)
|
||||
|
||||
# If from_date equals a posting date, return the next one
|
||||
if days_diff % frequency == 0:
|
||||
return next_posting
|
||||
|
||||
return next_posting
|
||||
|
||||
@api.model
|
||||
def _get_current_posting_date(self, from_date=None):
|
||||
"""Get the posting date for the current cycle (may be in the past).
|
||||
|
||||
Args:
|
||||
from_date: The date to calculate from (default: today)
|
||||
|
||||
Returns:
|
||||
date: The current cycle's posting date
|
||||
"""
|
||||
if from_date is None:
|
||||
from_date = date.today()
|
||||
elif hasattr(from_date, 'date'):
|
||||
from_date = from_date.date()
|
||||
|
||||
base_date = self._get_adp_posting_base_date()
|
||||
frequency = self._get_adp_posting_frequency()
|
||||
|
||||
if frequency <= 0:
|
||||
frequency = 14
|
||||
|
||||
days_diff = (from_date - base_date).days
|
||||
|
||||
if days_diff < 0:
|
||||
return base_date
|
||||
|
||||
cycles_passed = days_diff // frequency
|
||||
return base_date + timedelta(days=cycles_passed * frequency)
|
||||
|
||||
@api.model
|
||||
def _get_posting_week_wednesday(self, posting_date):
|
||||
"""Get the Wednesday before the posting date (submission deadline day).
|
||||
|
||||
The submission deadline is Wednesday 6 PM of the posting week.
|
||||
Posting day is typically Friday, so Wednesday is 2 days before.
|
||||
|
||||
Args:
|
||||
posting_date: The posting date
|
||||
|
||||
Returns:
|
||||
date: The Wednesday before posting date
|
||||
"""
|
||||
if hasattr(posting_date, 'date'):
|
||||
posting_date = posting_date.date()
|
||||
|
||||
# Find the Wednesday of the same week
|
||||
# weekday(): Monday=0, Tuesday=1, Wednesday=2, Thursday=3, Friday=4
|
||||
days_since_wednesday = (posting_date.weekday() - 2) % 7
|
||||
if days_since_wednesday == 0 and posting_date.weekday() != 2:
|
||||
days_since_wednesday = 7
|
||||
|
||||
return posting_date - timedelta(days=days_since_wednesday)
|
||||
|
||||
@api.model
|
||||
def _get_posting_week_tuesday(self, posting_date):
|
||||
"""Get the Tuesday of the posting week (delivery reminder date).
|
||||
|
||||
Args:
|
||||
posting_date: The posting date
|
||||
|
||||
Returns:
|
||||
date: The Tuesday of posting week
|
||||
"""
|
||||
if hasattr(posting_date, 'date'):
|
||||
posting_date = posting_date.date()
|
||||
|
||||
# Find the Tuesday of the same week
|
||||
days_since_tuesday = (posting_date.weekday() - 1) % 7
|
||||
if days_since_tuesday == 0 and posting_date.weekday() != 1:
|
||||
days_since_tuesday = 7
|
||||
|
||||
return posting_date - timedelta(days=days_since_tuesday)
|
||||
|
||||
@api.model
|
||||
def _get_posting_week_monday(self, posting_date):
|
||||
"""Get the Monday of the posting week (billing reminder date).
|
||||
|
||||
Args:
|
||||
posting_date: The posting date
|
||||
|
||||
Returns:
|
||||
date: The Monday of posting week
|
||||
"""
|
||||
if hasattr(posting_date, 'date'):
|
||||
posting_date = posting_date.date()
|
||||
|
||||
# Find the Monday of the same week
|
||||
days_since_monday = posting_date.weekday() # Monday=0
|
||||
return posting_date - timedelta(days=days_since_monday)
|
||||
|
||||
@api.model
|
||||
def _get_expected_payment_date(self, posting_date):
|
||||
"""Get the expected payment received date (posting + 10 days).
|
||||
|
||||
Args:
|
||||
posting_date: The posting date
|
||||
|
||||
Returns:
|
||||
date: The expected payment received date
|
||||
"""
|
||||
if hasattr(posting_date, 'date'):
|
||||
posting_date = posting_date.date()
|
||||
|
||||
return posting_date + timedelta(days=10)
|
||||
|
||||
@api.model
|
||||
def _get_payment_processed_date(self, posting_date):
|
||||
"""Get the payment processed date (posting + 7 days).
|
||||
|
||||
Args:
|
||||
posting_date: The posting date
|
||||
|
||||
Returns:
|
||||
date: The payment processed date
|
||||
"""
|
||||
if hasattr(posting_date, 'date'):
|
||||
posting_date = posting_date.date()
|
||||
|
||||
return posting_date + timedelta(days=7)
|
||||
|
||||
@api.model
|
||||
def _get_adp_billing_reminder_user(self):
|
||||
"""Get the configured billing reminder user from settings."""
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
user_id_str = ICP.get_param('fusion_claims.adp_billing_reminder_user_id', '')
|
||||
if user_id_str:
|
||||
try:
|
||||
user_id = int(user_id_str)
|
||||
return self.env['res.users'].browse(user_id).exists()
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return self.env['res.users']
|
||||
|
||||
@api.model
|
||||
def _get_adp_correction_reminder_users(self):
|
||||
"""Get the configured correction reminder users from settings."""
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
user_ids_str = ICP.get_param('fusion_claims.adp_correction_reminder_user_ids', '')
|
||||
if user_ids_str:
|
||||
try:
|
||||
user_ids = [int(x.strip()) for x in user_ids_str.split(',') if x.strip()]
|
||||
return self.env['res.users'].browse(user_ids).exists()
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return self.env['res.users']
|
||||
|
||||
@api.model
|
||||
def _is_past_submission_deadline(self, posting_date=None, check_time=True):
|
||||
"""Check if we're past the submission deadline for a posting cycle.
|
||||
|
||||
Args:
|
||||
posting_date: The posting date to check (default: next posting date)
|
||||
check_time: If True, checks if past 6 PM on Wednesday
|
||||
|
||||
Returns:
|
||||
bool: True if past deadline
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
if posting_date is None:
|
||||
posting_date = self._get_next_posting_date()
|
||||
|
||||
wednesday = self._get_posting_week_wednesday(posting_date)
|
||||
today = date.today()
|
||||
|
||||
if today > wednesday:
|
||||
return True
|
||||
elif today == wednesday and check_time:
|
||||
# Check if past 6 PM (18:00)
|
||||
now = datetime.now()
|
||||
return now.hour >= 18
|
||||
|
||||
return False
|
||||
164
fusion_claims/fusion_claims/models/ai_agent_ext.py
Normal file
164
fusion_claims/fusion_claims/models/ai_agent_ext.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AIAgentFusionClaims(models.Model):
|
||||
"""Extend ai.agent with Fusion Claims tool methods."""
|
||||
_inherit = 'ai.agent'
|
||||
|
||||
def _fc_tool_search_clients(self, search_term=None, city_filter=None, condition_filter=None):
|
||||
"""AI Tool: Search client profiles."""
|
||||
Profile = self.env['fusion.client.profile'].sudo()
|
||||
domain = []
|
||||
if search_term:
|
||||
domain = ['|', '|', '|',
|
||||
('first_name', 'ilike', search_term),
|
||||
('last_name', 'ilike', search_term),
|
||||
('health_card_number', 'ilike', search_term),
|
||||
('city', 'ilike', search_term),
|
||||
]
|
||||
if city_filter:
|
||||
domain.append(('city', 'ilike', city_filter))
|
||||
if condition_filter:
|
||||
domain.append(('medical_condition', 'ilike', condition_filter))
|
||||
|
||||
profiles = Profile.search(domain, limit=20)
|
||||
results = []
|
||||
for p in profiles:
|
||||
results.append({
|
||||
'id': p.id,
|
||||
'name': p.display_name,
|
||||
'health_card': p.health_card_number or '',
|
||||
'dob': str(p.date_of_birth) if p.date_of_birth else '',
|
||||
'city': p.city or '',
|
||||
'condition': (p.medical_condition or '')[:100],
|
||||
'claims': p.claim_count,
|
||||
'total_adp': float(p.total_adp_funded),
|
||||
'total_client': float(p.total_client_portion),
|
||||
})
|
||||
return json.dumps({'count': len(results), 'profiles': results})
|
||||
|
||||
def _fc_tool_client_details(self, profile_id):
|
||||
"""AI Tool: Get detailed client information."""
|
||||
Profile = self.env['fusion.client.profile'].sudo()
|
||||
profile = Profile.browse(int(profile_id))
|
||||
if not profile.exists():
|
||||
return json.dumps({'error': 'Profile not found'})
|
||||
|
||||
# Get orders
|
||||
orders = []
|
||||
if profile.partner_id:
|
||||
for o in self.env['sale.order'].sudo().search([
|
||||
('partner_id', '=', profile.partner_id.id),
|
||||
('x_fc_sale_type', '!=', False),
|
||||
], limit=20):
|
||||
orders.append({
|
||||
'name': o.name,
|
||||
'sale_type': o.x_fc_sale_type,
|
||||
'status': o.x_fc_adp_application_status or '',
|
||||
'adp_total': float(o.x_fc_adp_portion_total),
|
||||
'client_total': float(o.x_fc_client_portion_total),
|
||||
'total': float(o.amount_total),
|
||||
'date': str(o.date_order.date()) if o.date_order else '',
|
||||
})
|
||||
|
||||
# Get applications
|
||||
apps = []
|
||||
for a in profile.application_data_ids[:10]:
|
||||
apps.append({
|
||||
'date': str(a.application_date) if a.application_date else '',
|
||||
'device': a.base_device or '',
|
||||
'category': a.device_category or '',
|
||||
'reason': a.reason_for_application or '',
|
||||
'condition': (a.medical_condition or '')[:100],
|
||||
'authorizer': f'{a.authorizer_first_name or ""} {a.authorizer_last_name or ""}'.strip(),
|
||||
})
|
||||
|
||||
return json.dumps({
|
||||
'profile': {
|
||||
'id': profile.id,
|
||||
'name': profile.display_name,
|
||||
'first_name': profile.first_name,
|
||||
'last_name': profile.last_name,
|
||||
'health_card': profile.health_card_number or '',
|
||||
'dob': str(profile.date_of_birth) if profile.date_of_birth else '',
|
||||
'city': profile.city or '',
|
||||
'province': profile.province or '',
|
||||
'postal_code': profile.postal_code or '',
|
||||
'phone': profile.home_phone or '',
|
||||
'condition': profile.medical_condition or '',
|
||||
'mobility': profile.mobility_status or '',
|
||||
'benefits': {
|
||||
'social_assistance': profile.receives_social_assistance,
|
||||
'type': profile.benefit_type or '',
|
||||
'wsib': profile.wsib_eligible,
|
||||
'vac': profile.vac_eligible,
|
||||
},
|
||||
'claims_count': profile.claim_count,
|
||||
'total_adp': float(profile.total_adp_funded),
|
||||
'total_client': float(profile.total_client_portion),
|
||||
'total_amount': float(profile.total_amount),
|
||||
'last_assessment': str(profile.last_assessment_date) if profile.last_assessment_date else '',
|
||||
},
|
||||
'orders': orders,
|
||||
'applications': apps,
|
||||
})
|
||||
|
||||
def _fc_tool_claims_stats(self):
|
||||
"""AI Tool: Get aggregated claims statistics."""
|
||||
SO = self.env['sale.order'].sudo()
|
||||
Profile = self.env['fusion.client.profile'].sudo()
|
||||
|
||||
total_profiles = Profile.search_count([])
|
||||
total_orders = SO.search_count([('x_fc_sale_type', '!=', False)])
|
||||
|
||||
# By sale type
|
||||
type_data = SO.read_group(
|
||||
[('x_fc_sale_type', '!=', False)],
|
||||
['x_fc_sale_type', 'amount_total:sum'],
|
||||
['x_fc_sale_type'],
|
||||
)
|
||||
by_type = {}
|
||||
for r in type_data:
|
||||
by_type[r['x_fc_sale_type']] = {
|
||||
'count': r['x_fc_sale_type_count'],
|
||||
'total': float(r['amount_total'] or 0),
|
||||
}
|
||||
|
||||
# By status
|
||||
status_data = SO.read_group(
|
||||
[('x_fc_sale_type', '!=', False), ('x_fc_adp_application_status', '!=', False)],
|
||||
['x_fc_adp_application_status'],
|
||||
['x_fc_adp_application_status'],
|
||||
)
|
||||
by_status = {}
|
||||
for r in status_data:
|
||||
by_status[r['x_fc_adp_application_status']] = r['x_fc_adp_application_status_count']
|
||||
|
||||
# By city (top 10)
|
||||
city_data = Profile.read_group(
|
||||
[('city', '!=', False)],
|
||||
['city'],
|
||||
['city'],
|
||||
limit=10,
|
||||
orderby='city_count desc',
|
||||
)
|
||||
by_city = {}
|
||||
for r in city_data:
|
||||
by_city[r['city']] = r['city_count']
|
||||
|
||||
return json.dumps({
|
||||
'total_profiles': total_profiles,
|
||||
'total_orders': total_orders,
|
||||
'by_sale_type': by_type,
|
||||
'by_status': by_status,
|
||||
'top_cities': by_city,
|
||||
})
|
||||
350
fusion_claims/fusion_claims/models/client_chat.py
Normal file
350
fusion_claims/fusion_claims/models/client_chat.py
Normal file
@@ -0,0 +1,350 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionClientChatSession(models.Model):
|
||||
_name = 'fusion.client.chat.session'
|
||||
_description = 'Client Intelligence Chat Session'
|
||||
_order = 'create_date desc'
|
||||
|
||||
name = fields.Char(string='Session Title', required=True,
|
||||
default=lambda self: f'Chat - {fields.Date.today()}')
|
||||
profile_id = fields.Many2one(
|
||||
'fusion.client.profile', string='Client Profile',
|
||||
ondelete='set null',
|
||||
help='If set, chat is scoped to this specific client',
|
||||
)
|
||||
user_id = fields.Many2one(
|
||||
'res.users', string='User', default=lambda self: self.env.user,
|
||||
required=True,
|
||||
)
|
||||
message_ids = fields.One2many(
|
||||
'fusion.client.chat.message', 'session_id', string='Messages',
|
||||
)
|
||||
state = fields.Selection([
|
||||
('active', 'Active'),
|
||||
('archived', 'Archived'),
|
||||
], default='active', string='State')
|
||||
|
||||
# Input field for the form view
|
||||
user_input = fields.Text(string='Your Question')
|
||||
|
||||
def action_send_message(self):
|
||||
"""Process user message and generate AI response."""
|
||||
self.ensure_one()
|
||||
if not self.user_input or not self.user_input.strip():
|
||||
return
|
||||
|
||||
question = self.user_input.strip()
|
||||
|
||||
# Create user message
|
||||
self.env['fusion.client.chat.message'].create({
|
||||
'session_id': self.id,
|
||||
'role': 'user',
|
||||
'content': question,
|
||||
})
|
||||
|
||||
# Generate AI response
|
||||
try:
|
||||
response = self._generate_ai_response(question)
|
||||
except Exception as e:
|
||||
_logger.exception('AI chat error: %s', e)
|
||||
response = f'Sorry, I encountered an error processing your question. Error: {str(e)}'
|
||||
|
||||
# Create assistant message
|
||||
msg = self.env['fusion.client.chat.message'].create({
|
||||
'session_id': self.id,
|
||||
'role': 'assistant',
|
||||
'content': response,
|
||||
})
|
||||
|
||||
# Clear input
|
||||
self.user_input = False
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.client.chat.session',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.id,
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def _generate_ai_response(self, question):
|
||||
"""Generate an AI-powered response to the user question.
|
||||
|
||||
Uses OpenAI API to analyze the question, query relevant data,
|
||||
and formulate a response.
|
||||
"""
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
api_key = ICP.get_param('fusion_claims.ai_api_key', '')
|
||||
if not api_key:
|
||||
return self._generate_local_response(question)
|
||||
|
||||
ai_model = ICP.get_param('fusion_claims.ai_model', 'gpt-4o-mini')
|
||||
|
||||
# Build context about available data
|
||||
context_data = self._build_data_context(question)
|
||||
|
||||
# Build system prompt
|
||||
system_prompt = self._build_system_prompt()
|
||||
|
||||
# Build messages
|
||||
messages = [{'role': 'system', 'content': system_prompt}]
|
||||
|
||||
# Add conversation history (last 10 messages)
|
||||
history = self.message_ids.sorted('create_date')[-10:]
|
||||
for msg in history:
|
||||
messages.append({'role': msg.role, 'content': msg.content})
|
||||
|
||||
# Add current question with data context
|
||||
user_msg = question
|
||||
if context_data:
|
||||
user_msg += f'\n\n--- Retrieved Data ---\n{context_data}'
|
||||
messages.append({'role': 'user', 'content': user_msg})
|
||||
|
||||
# Call OpenAI API
|
||||
try:
|
||||
import requests
|
||||
response = requests.post(
|
||||
'https://api.openai.com/v1/chat/completions',
|
||||
headers={
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
json={
|
||||
'model': ai_model,
|
||||
'messages': messages,
|
||||
'max_tokens': 2000,
|
||||
'temperature': 0.3,
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
return result['choices'][0]['message']['content']
|
||||
except ImportError:
|
||||
return self._generate_local_response(question)
|
||||
except Exception as e:
|
||||
_logger.warning('OpenAI API error: %s', e)
|
||||
return self._generate_local_response(question)
|
||||
|
||||
def _generate_local_response(self, question):
|
||||
"""Generate a response without AI, using direct database queries.
|
||||
|
||||
This is the fallback when no API key is configured.
|
||||
"""
|
||||
question_lower = question.lower()
|
||||
Profile = self.env['fusion.client.profile']
|
||||
SaleOrder = self.env['sale.order']
|
||||
|
||||
# If scoped to a specific profile
|
||||
if self.profile_id:
|
||||
profile = self.profile_id
|
||||
orders = SaleOrder.search([
|
||||
('partner_id', '=', profile.partner_id.id),
|
||||
('x_fc_sale_type', '!=', False),
|
||||
]) if profile.partner_id else SaleOrder
|
||||
|
||||
lines = []
|
||||
lines.append(f'**Client: {profile.display_name}**')
|
||||
lines.append(f'- Health Card: {profile.health_card_number or "N/A"}')
|
||||
lines.append(f'- Date of Birth: {profile.date_of_birth or "N/A"}')
|
||||
lines.append(f'- City: {profile.city or "N/A"}')
|
||||
lines.append(f'- Medical Condition: {profile.medical_condition or "N/A"}')
|
||||
lines.append(f'- Mobility Status: {profile.mobility_status or "N/A"}')
|
||||
lines.append(f'- Total Claims: {len(orders)}')
|
||||
lines.append(f'- Total ADP Funded: ${profile.total_adp_funded:,.2f}')
|
||||
lines.append(f'- Total Client Portion: ${profile.total_client_portion:,.2f}')
|
||||
|
||||
if orders:
|
||||
lines.append('\n**Claims History:**')
|
||||
for order in orders[:10]:
|
||||
status = dict(order._fields['x_fc_adp_application_status'].selection).get(
|
||||
order.x_fc_adp_application_status, order.x_fc_adp_application_status or 'N/A'
|
||||
)
|
||||
lines.append(
|
||||
f'- {order.name}: {order.x_fc_sale_type or "N/A"} | '
|
||||
f'Status: {status} | '
|
||||
f'ADP: ${order.x_fc_adp_portion_total:,.2f} | '
|
||||
f'Client: ${order.x_fc_client_portion_total:,.2f}'
|
||||
)
|
||||
|
||||
apps = profile.application_data_ids[:5]
|
||||
if apps:
|
||||
lines.append('\n**Application History:**')
|
||||
for app in apps:
|
||||
lines.append(
|
||||
f'- {app.application_date or "No date"}: '
|
||||
f'{app.device_category or "N/A"} | '
|
||||
f'Device: {app.base_device or "N/A"} | '
|
||||
f'Reason: {app.reason_for_application or "N/A"}'
|
||||
)
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
# Global queries
|
||||
total_profiles = Profile.search_count([])
|
||||
total_orders = SaleOrder.search_count([('x_fc_sale_type', '!=', False)])
|
||||
|
||||
if 'how many' in question_lower or 'count' in question_lower:
|
||||
if 'client' in question_lower or 'profile' in question_lower:
|
||||
return f'There are currently **{total_profiles}** client profiles in the system.'
|
||||
if 'claim' in question_lower or 'order' in question_lower or 'case' in question_lower:
|
||||
return f'There are currently **{total_orders}** claims/orders in the system.'
|
||||
|
||||
# Search for specific client
|
||||
if 'find' in question_lower or 'search' in question_lower or 'show' in question_lower:
|
||||
# Try to extract name from question
|
||||
words = question.split()
|
||||
profiles = Profile.search([], limit=20)
|
||||
for word in words:
|
||||
if len(word) > 2 and word[0].isupper():
|
||||
found = Profile.search([
|
||||
'|',
|
||||
('first_name', 'ilike', word),
|
||||
('last_name', 'ilike', word),
|
||||
], limit=5)
|
||||
if found:
|
||||
profiles = found
|
||||
break
|
||||
|
||||
if profiles:
|
||||
lines = [f'Found **{len(profiles)}** matching profile(s):']
|
||||
for p in profiles[:10]:
|
||||
lines.append(
|
||||
f'- **{p.display_name}** | HC: {p.health_card_number or "N/A"} | '
|
||||
f'City: {p.city or "N/A"} | Claims: {p.claim_count}'
|
||||
)
|
||||
return '\n'.join(lines)
|
||||
|
||||
return (
|
||||
f'I have access to **{total_profiles}** client profiles and **{total_orders}** claims. '
|
||||
f'You can ask me questions like:\n'
|
||||
f'- "How many clients are from Brampton?"\n'
|
||||
f'- "Find client Raymond Wellesley"\n'
|
||||
f'- "Show all clients with CVA diagnosis"\n\n'
|
||||
f'For more intelligent responses, configure an OpenAI API key in '
|
||||
f'Fusion Claims > Configuration > Settings.'
|
||||
)
|
||||
|
||||
def _build_system_prompt(self):
|
||||
"""Build the system prompt for the AI."""
|
||||
profile_context = ''
|
||||
if self.profile_id:
|
||||
p = self.profile_id
|
||||
profile_context = f"""
|
||||
You are currently looking at a specific client profile:
|
||||
- Name: {p.display_name}
|
||||
- Health Card: {p.health_card_number or 'N/A'}
|
||||
- DOB: {p.date_of_birth or 'N/A'}
|
||||
- City: {p.city or 'N/A'}
|
||||
- Medical Condition: {p.medical_condition or 'N/A'}
|
||||
- Mobility Status: {p.mobility_status or 'N/A'}
|
||||
- Total Claims: {p.claim_count}
|
||||
- Total ADP Funded: ${p.total_adp_funded:,.2f}
|
||||
- Total Client Portion: ${p.total_client_portion:,.2f}
|
||||
"""
|
||||
|
||||
return f"""You are a helpful AI assistant for Fusion Claims, a healthcare equipment claims management system.
|
||||
You help users find information about clients, their ADP (Assistive Devices Program) claims, funding history,
|
||||
medical conditions, and devices.
|
||||
|
||||
Available data includes:
|
||||
- Client profiles with personal info, health card numbers, addresses, medical conditions
|
||||
- ADP application data parsed from XML submissions
|
||||
- Sale orders with funding type, status, ADP/client portions
|
||||
- Device information (wheelchairs, walkers, power bases, seating)
|
||||
|
||||
Funding types: ADP, ODSP, WSIB, March of Dimes, Muscular Dystrophy, Insurance, Hardship Funding, Rentals, Direct/Private
|
||||
Client types: REG (75%/25%), ODS, OWP, ACS, LTC, SEN, CCA (100%/0%)
|
||||
{profile_context}
|
||||
Answer concisely and include specific data when available. Format monetary values with $ and commas."""
|
||||
|
||||
def _build_data_context(self, question):
|
||||
"""Query relevant data based on the question to provide context to AI."""
|
||||
question_lower = question.lower()
|
||||
context_parts = []
|
||||
|
||||
Profile = self.env['fusion.client.profile']
|
||||
SaleOrder = self.env['sale.order']
|
||||
|
||||
if self.profile_id:
|
||||
# Scoped to specific client - load their data
|
||||
p = self.profile_id
|
||||
orders = SaleOrder.search([
|
||||
('partner_id', '=', p.partner_id.id),
|
||||
('x_fc_sale_type', '!=', False),
|
||||
], limit=20) if p.partner_id else SaleOrder
|
||||
|
||||
if orders:
|
||||
order_data = []
|
||||
for o in orders:
|
||||
order_data.append({
|
||||
'name': o.name,
|
||||
'sale_type': o.x_fc_sale_type,
|
||||
'status': o.x_fc_adp_application_status,
|
||||
'adp_total': o.x_fc_adp_portion_total,
|
||||
'client_total': o.x_fc_client_portion_total,
|
||||
'amount_total': o.amount_total,
|
||||
'date': str(o.date_order) if o.date_order else '',
|
||||
})
|
||||
context_parts.append(f'Orders: {json.dumps(order_data)}')
|
||||
|
||||
apps = p.application_data_ids[:10]
|
||||
if apps:
|
||||
app_data = []
|
||||
for a in apps:
|
||||
app_data.append({
|
||||
'date': str(a.application_date) if a.application_date else '',
|
||||
'device_category': a.device_category,
|
||||
'base_device': a.base_device or '',
|
||||
'condition': a.medical_condition or '',
|
||||
'reason': a.reason_for_application or '',
|
||||
'authorizer': f'{a.authorizer_first_name} {a.authorizer_last_name}'.strip(),
|
||||
})
|
||||
context_parts.append(f'Applications: {json.dumps(app_data)}')
|
||||
else:
|
||||
# Global query - provide summary stats
|
||||
total_profiles = Profile.search_count([])
|
||||
total_orders = SaleOrder.search_count([('x_fc_sale_type', '!=', False)])
|
||||
|
||||
# City distribution
|
||||
if 'city' in question_lower or 'cities' in question_lower or 'where' in question_lower:
|
||||
city_data = SaleOrder.read_group(
|
||||
[('x_fc_sale_type', '!=', False), ('partner_id.city', '!=', False)],
|
||||
['partner_id'],
|
||||
['partner_id'],
|
||||
limit=20,
|
||||
)
|
||||
context_parts.append(f'Total profiles: {total_profiles}, Total orders: {total_orders}')
|
||||
|
||||
context_parts.append(f'Summary: {total_profiles} profiles, {total_orders} orders')
|
||||
|
||||
return '\n'.join(context_parts) if context_parts else ''
|
||||
|
||||
|
||||
class FusionClientChatMessage(models.Model):
|
||||
_name = 'fusion.client.chat.message'
|
||||
_description = 'Chat Message'
|
||||
_order = 'create_date asc'
|
||||
|
||||
session_id = fields.Many2one(
|
||||
'fusion.client.chat.session', string='Session',
|
||||
required=True, ondelete='cascade',
|
||||
)
|
||||
role = fields.Selection([
|
||||
('user', 'User'),
|
||||
('assistant', 'Assistant'),
|
||||
], string='Role', required=True)
|
||||
content = fields.Text(string='Content', required=True)
|
||||
timestamp = fields.Datetime(
|
||||
string='Timestamp', default=fields.Datetime.now,
|
||||
)
|
||||
298
fusion_claims/fusion_claims/models/client_profile.py
Normal file
298
fusion_claims/fusion_claims/models/client_profile.py
Normal file
@@ -0,0 +1,298 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FusionClientProfile(models.Model):
|
||||
_name = 'fusion.client.profile'
|
||||
_description = 'Client Profile'
|
||||
_order = 'last_name, first_name'
|
||||
_rec_name = 'display_name'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PERSONAL INFORMATION (from ADP XML Section 1)
|
||||
# ------------------------------------------------------------------
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner', string='Odoo Contact',
|
||||
help='Linked contact record in Odoo',
|
||||
)
|
||||
first_name = fields.Char(string='First Name', tracking=True)
|
||||
last_name = fields.Char(string='Last Name', tracking=True)
|
||||
middle_initial = fields.Char(string='Middle Initial')
|
||||
display_name = fields.Char(
|
||||
string='Name', compute='_compute_display_name', store=True,
|
||||
)
|
||||
health_card_number = fields.Char(
|
||||
string='Health Card Number', index=True, tracking=True,
|
||||
help='Ontario Health Card Number (10 digits)',
|
||||
)
|
||||
health_card_version = fields.Char(string='Health Card Version')
|
||||
date_of_birth = fields.Date(string='Date of Birth', tracking=True)
|
||||
ltch_name = fields.Char(
|
||||
string='Long-Term Care Home',
|
||||
help='Name of LTCH if applicable',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# ADDRESS
|
||||
# ------------------------------------------------------------------
|
||||
unit_number = fields.Char(string='Unit Number')
|
||||
street_number = fields.Char(string='Street Number')
|
||||
street_name = fields.Char(string='Street Name')
|
||||
rural_route = fields.Char(string='Lot/Concession/Rural Route')
|
||||
city = fields.Char(string='City', index=True, tracking=True)
|
||||
province = fields.Char(string='Province', default='ON')
|
||||
postal_code = fields.Char(string='Postal Code')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# CONTACT
|
||||
# ------------------------------------------------------------------
|
||||
home_phone = fields.Char(string='Home Phone')
|
||||
business_phone = fields.Char(string='Business Phone')
|
||||
phone_extension = fields.Char(string='Phone Extension')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# BENEFITS ELIGIBILITY (from XML confirmationOfBenefit)
|
||||
# ------------------------------------------------------------------
|
||||
receives_social_assistance = fields.Boolean(
|
||||
string='Receives Social Assistance', tracking=True,
|
||||
)
|
||||
benefit_type = fields.Selection([
|
||||
('owp', 'Ontario Works Program (OWP)'),
|
||||
('odsp', 'Ontario Disability Support Program (ODSP)'),
|
||||
('acsd', 'Assistance to Children with Severe Disabilities (ACSD)'),
|
||||
], string='Benefit Type', tracking=True)
|
||||
wsib_eligible = fields.Boolean(string='WSIB Eligible', tracking=True)
|
||||
vac_eligible = fields.Boolean(
|
||||
string='Veterans Affairs Canada Eligible', tracking=True,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# CURRENT MEDICAL STATUS (updated from latest XML)
|
||||
# ------------------------------------------------------------------
|
||||
medical_condition = fields.Text(
|
||||
string='Medical Condition/Diagnosis', tracking=True,
|
||||
help='Current presenting medical condition from latest ADP application',
|
||||
)
|
||||
mobility_status = fields.Text(
|
||||
string='Functional Mobility Status', tracking=True,
|
||||
help='Current functional mobility status from latest ADP application',
|
||||
)
|
||||
last_assessment_date = fields.Date(
|
||||
string='Last Assessment Date', tracking=True,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# RELATIONSHIPS
|
||||
# ------------------------------------------------------------------
|
||||
application_data_ids = fields.One2many(
|
||||
'fusion.adp.application.data', 'profile_id',
|
||||
string='ADP Applications',
|
||||
)
|
||||
# Chat is handled via Odoo's native AI agent (discuss.channel with ai_chat type)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# COMPUTED FIELDS
|
||||
# ------------------------------------------------------------------
|
||||
claim_count = fields.Integer(
|
||||
string='Claims', compute='_compute_claim_stats', store=True,
|
||||
)
|
||||
total_adp_funded = fields.Monetary(
|
||||
string='Total ADP Funded', compute='_compute_claim_stats', store=True,
|
||||
currency_field='currency_id',
|
||||
)
|
||||
total_client_portion = fields.Monetary(
|
||||
string='Total Client Portion', compute='_compute_claim_stats', store=True,
|
||||
currency_field='currency_id',
|
||||
)
|
||||
total_amount = fields.Monetary(
|
||||
string='Total Amount', compute='_compute_claim_stats', store=True,
|
||||
currency_field='currency_id',
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', string='Currency',
|
||||
default=lambda self: self.env.company.currency_id,
|
||||
)
|
||||
application_count = fields.Integer(
|
||||
string='Applications', compute='_compute_application_count',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# AI ANALYSIS (auto-computed from application data)
|
||||
# ------------------------------------------------------------------
|
||||
ai_summary = fields.Text(
|
||||
string='Summary',
|
||||
compute='_compute_ai_analysis',
|
||||
)
|
||||
ai_risk_flags = fields.Text(
|
||||
string='Risk Flags',
|
||||
compute='_compute_ai_analysis',
|
||||
)
|
||||
ai_last_analyzed = fields.Datetime(string='Last AI Analysis')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# COMPUTED METHODS
|
||||
# ------------------------------------------------------------------
|
||||
@api.depends('first_name', 'last_name')
|
||||
def _compute_display_name(self):
|
||||
for profile in self:
|
||||
parts = [profile.last_name or '', profile.first_name or '']
|
||||
profile.display_name = ', '.join(p for p in parts if p) or 'New Profile'
|
||||
|
||||
@api.depends('partner_id', 'partner_id.sale_order_ids',
|
||||
'partner_id.sale_order_ids.x_fc_adp_portion_total',
|
||||
'partner_id.sale_order_ids.x_fc_client_portion_total',
|
||||
'partner_id.sale_order_ids.amount_total')
|
||||
def _compute_claim_stats(self):
|
||||
for profile in self:
|
||||
if profile.partner_id:
|
||||
orders = self.env['sale.order'].search([
|
||||
('partner_id', '=', profile.partner_id.id),
|
||||
('x_fc_sale_type', '!=', False),
|
||||
])
|
||||
profile.claim_count = len(orders)
|
||||
profile.total_adp_funded = sum(orders.mapped('x_fc_adp_portion_total'))
|
||||
profile.total_client_portion = sum(orders.mapped('x_fc_client_portion_total'))
|
||||
profile.total_amount = sum(orders.mapped('amount_total'))
|
||||
else:
|
||||
profile.claim_count = 0
|
||||
profile.total_adp_funded = 0
|
||||
profile.total_client_portion = 0
|
||||
profile.total_amount = 0
|
||||
|
||||
def _compute_application_count(self):
|
||||
for profile in self:
|
||||
profile.application_count = len(profile.application_data_ids)
|
||||
|
||||
@api.depends('application_data_ids', 'application_data_ids.application_date',
|
||||
'application_data_ids.base_device', 'application_data_ids.reason_for_application')
|
||||
def _compute_ai_analysis(self):
|
||||
for profile in self:
|
||||
apps = profile.application_data_ids.sorted('application_date', reverse=True)
|
||||
|
||||
# --- SUMMARY ---
|
||||
summary_lines = []
|
||||
|
||||
# Number of applications
|
||||
app_count = len(apps)
|
||||
summary_lines.append(f"Total Applications: {app_count}")
|
||||
|
||||
# Last funding history
|
||||
if apps:
|
||||
latest = apps[0]
|
||||
date_str = latest.application_date.strftime('%B %d, %Y') if latest.application_date else 'Unknown date'
|
||||
device = latest.base_device or 'Not specified'
|
||||
category = dict(latest._fields['device_category'].selection).get(
|
||||
latest.device_category, latest.device_category or 'N/A'
|
||||
) if latest.device_category else 'N/A'
|
||||
summary_lines.append(f"Last Application: {date_str}")
|
||||
summary_lines.append(f"Last Device: {device} ({category})")
|
||||
|
||||
# Reason for last application
|
||||
reason = latest.reason_for_application or 'Not specified'
|
||||
summary_lines.append(f"Reason: {reason}")
|
||||
|
||||
# Authorizer
|
||||
auth_name = f"{latest.authorizer_first_name or ''} {latest.authorizer_last_name or ''}".strip()
|
||||
if auth_name:
|
||||
summary_lines.append(f"Authorizer: {auth_name}")
|
||||
|
||||
# All devices received (unique)
|
||||
devices = set()
|
||||
for a in apps:
|
||||
if a.base_device:
|
||||
devices.add(a.base_device)
|
||||
if len(devices) > 1:
|
||||
summary_lines.append(f"All Devices: {', '.join(sorted(devices))}")
|
||||
else:
|
||||
summary_lines.append("No applications on file.")
|
||||
|
||||
profile.ai_summary = '\n'.join(summary_lines)
|
||||
|
||||
# --- RISK FLAGS ---
|
||||
risk_lines = []
|
||||
|
||||
if app_count >= 2:
|
||||
# Calculate frequency
|
||||
dated_apps = [a for a in apps if a.application_date]
|
||||
if len(dated_apps) >= 2:
|
||||
dates = sorted([a.application_date for a in dated_apps])
|
||||
total_span = (dates[-1] - dates[0]).days
|
||||
if total_span > 0:
|
||||
avg_days = total_span / (len(dates) - 1)
|
||||
if avg_days < 365:
|
||||
risk_lines.append(
|
||||
f"High Frequency: {app_count} applications over "
|
||||
f"{total_span} days (avg {avg_days:.0f} days apart)"
|
||||
)
|
||||
elif avg_days < 730:
|
||||
risk_lines.append(
|
||||
f"Moderate Frequency: {app_count} applications over "
|
||||
f"{total_span // 365} year(s) (avg {avg_days:.0f} days apart)"
|
||||
)
|
||||
else:
|
||||
risk_lines.append(
|
||||
f"Normal Frequency: {app_count} applications over "
|
||||
f"{total_span // 365} year(s) (avg {avg_days:.0f} days apart)"
|
||||
)
|
||||
|
||||
# Check for multiple replacements
|
||||
replacements = [a for a in apps if a.reason_for_application and 'replacement' in a.reason_for_application.lower()]
|
||||
if len(replacements) >= 2:
|
||||
risk_lines.append(f"Multiple Replacements: {len(replacements)} replacement applications")
|
||||
|
||||
if not risk_lines:
|
||||
risk_lines.append("No flags identified.")
|
||||
|
||||
profile.ai_risk_flags = '\n'.join(risk_lines)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# ACTIONS
|
||||
# ------------------------------------------------------------------
|
||||
def action_view_claims(self):
|
||||
"""Open sale orders for this client."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': f'Claims - {self.display_name}',
|
||||
'res_model': 'sale.order',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('partner_id', '=', self.partner_id.id), ('x_fc_sale_type', '!=', False)],
|
||||
}
|
||||
|
||||
def action_view_applications(self):
|
||||
"""Open parsed ADP application data for this client."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': f'Applications - {self.display_name}',
|
||||
'res_model': 'fusion.adp.application.data',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('profile_id', '=', self.id)],
|
||||
}
|
||||
|
||||
def action_open_ai_chat(self):
|
||||
"""Open AI chat about this client using Odoo's native AI agent."""
|
||||
self.ensure_one()
|
||||
agent = self.env.ref('fusion_claims.ai_agent_fusion_claims', raise_if_not_found=False)
|
||||
if agent:
|
||||
# Create channel with client context so the AI knows which client
|
||||
channel = agent._create_ai_chat_channel()
|
||||
# Post an initial context message about this client
|
||||
initial_prompt = (
|
||||
f"I want to ask about client {self.display_name} "
|
||||
f"(Profile ID: {self.id}, Health Card: {self.health_card_number or 'N/A'}). "
|
||||
f"Please look up their details."
|
||||
)
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'agent_chat_action',
|
||||
'params': {
|
||||
'channelId': channel.id,
|
||||
'user_prompt': initial_prompt,
|
||||
},
|
||||
}
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
162
fusion_claims/fusion_claims/models/dashboard.py
Normal file
162
fusion_claims/fusion_claims/models/dashboard.py
Normal file
@@ -0,0 +1,162 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
CASE_TYPE_SELECTION = [
|
||||
('adp', 'ADP'),
|
||||
('odsp', 'ODSP'),
|
||||
('march_of_dimes', 'March of Dimes'),
|
||||
('hardship', 'Hardship Funding'),
|
||||
('acsd', 'ACSD'),
|
||||
('muscular_dystrophy', 'Muscular Dystrophy'),
|
||||
('insurance', 'Insurance'),
|
||||
('wsib', 'WSIB'),
|
||||
]
|
||||
|
||||
TYPE_DOMAINS = {
|
||||
'adp': [('x_fc_sale_type', 'in', ['adp', 'adp_odsp'])],
|
||||
'odsp': [('x_fc_sale_type', 'in', ['odsp', 'adp_odsp'])],
|
||||
'march_of_dimes': [('x_fc_sale_type', '=', 'march_of_dimes')],
|
||||
'hardship': [('x_fc_sale_type', '=', 'hardship')],
|
||||
'acsd': [('x_fc_client_type', '=', 'ACS')],
|
||||
'muscular_dystrophy': [('x_fc_sale_type', '=', 'muscular_dystrophy')],
|
||||
'insurance': [('x_fc_sale_type', '=', 'insurance')],
|
||||
'wsib': [('x_fc_sale_type', '=', 'wsib')],
|
||||
}
|
||||
|
||||
TYPE_LABELS = dict(CASE_TYPE_SELECTION)
|
||||
|
||||
|
||||
class FusionClaimsDashboard(models.TransientModel):
|
||||
_name = 'fusion.claims.dashboard'
|
||||
_description = 'Fusion Claims Dashboard'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char(default='Dashboard', readonly=True)
|
||||
|
||||
# Case counts by funding type
|
||||
adp_count = fields.Integer(compute='_compute_stats')
|
||||
odsp_count = fields.Integer(compute='_compute_stats')
|
||||
march_of_dimes_count = fields.Integer(compute='_compute_stats')
|
||||
hardship_count = fields.Integer(compute='_compute_stats')
|
||||
acsd_count = fields.Integer(compute='_compute_stats')
|
||||
muscular_dystrophy_count = fields.Integer(compute='_compute_stats')
|
||||
insurance_count = fields.Integer(compute='_compute_stats')
|
||||
wsib_count = fields.Integer(compute='_compute_stats')
|
||||
total_profiles = fields.Integer(compute='_compute_stats')
|
||||
|
||||
# Panel selectors (4 panels)
|
||||
panel1_type = fields.Selection(CASE_TYPE_SELECTION, string='Window 1', default='adp')
|
||||
panel2_type = fields.Selection(CASE_TYPE_SELECTION, string='Window 2', default='odsp')
|
||||
panel3_type = fields.Selection(CASE_TYPE_SELECTION, string='Window 3', default='march_of_dimes')
|
||||
panel4_type = fields.Selection(CASE_TYPE_SELECTION, string='Window 4', default='hardship')
|
||||
|
||||
# Panel HTML
|
||||
panel1_html = fields.Html(compute='_compute_panels', sanitize=False)
|
||||
panel2_html = fields.Html(compute='_compute_panels', sanitize=False)
|
||||
panel3_html = fields.Html(compute='_compute_panels', sanitize=False)
|
||||
panel4_html = fields.Html(compute='_compute_panels', sanitize=False)
|
||||
panel1_title = fields.Char(compute='_compute_panels')
|
||||
panel2_title = fields.Char(compute='_compute_panels')
|
||||
panel3_title = fields.Char(compute='_compute_panels')
|
||||
panel4_title = fields.Char(compute='_compute_panels')
|
||||
|
||||
def _compute_stats(self):
|
||||
SO = self.env['sale.order'].sudo()
|
||||
Profile = self.env['fusion.client.profile'].sudo()
|
||||
for rec in self:
|
||||
rec.adp_count = SO.search_count(TYPE_DOMAINS['adp'])
|
||||
rec.odsp_count = SO.search_count(TYPE_DOMAINS['odsp'])
|
||||
rec.march_of_dimes_count = SO.search_count(TYPE_DOMAINS['march_of_dimes'])
|
||||
rec.hardship_count = SO.search_count(TYPE_DOMAINS['hardship'])
|
||||
rec.acsd_count = SO.search_count(TYPE_DOMAINS['acsd'])
|
||||
rec.muscular_dystrophy_count = SO.search_count(TYPE_DOMAINS['muscular_dystrophy'])
|
||||
rec.insurance_count = SO.search_count(TYPE_DOMAINS['insurance'])
|
||||
rec.wsib_count = SO.search_count(TYPE_DOMAINS['wsib'])
|
||||
rec.total_profiles = Profile.search_count([])
|
||||
|
||||
@api.depends('panel1_type', 'panel2_type', 'panel3_type', 'panel4_type')
|
||||
def _compute_panels(self):
|
||||
SO = self.env['sale.order'].sudo()
|
||||
for rec in self:
|
||||
for i in range(1, 5):
|
||||
ptype = getattr(rec, f'panel{i}_type') or 'adp'
|
||||
domain = TYPE_DOMAINS.get(ptype, [])
|
||||
orders = SO.search(domain, order='create_date desc', limit=50)
|
||||
count = SO.search_count(domain)
|
||||
title = f'Window {i} - {TYPE_LABELS.get(ptype, ptype)} ({count} cases)'
|
||||
html = rec._build_top_list(orders)
|
||||
setattr(rec, f'panel{i}_title', title)
|
||||
setattr(rec, f'panel{i}_html', html)
|
||||
|
||||
def _build_top_list(self, orders):
|
||||
if not orders:
|
||||
return '<p class="text-muted text-center py-4">No cases found</p>'
|
||||
rows = []
|
||||
for o in orders:
|
||||
status = o.x_fc_adp_application_status or ''
|
||||
status_label = dict(o._fields['x_fc_adp_application_status'].selection).get(status, status)
|
||||
rows.append(
|
||||
f'<tr>'
|
||||
f'<td><a href="/odoo/sales/{o.id}">{o.name}</a></td>'
|
||||
f'<td>{o.partner_id.name or ""}</td>'
|
||||
f'<td>{status_label}</td>'
|
||||
f'<td class="text-end">${o.amount_total:,.2f}</td>'
|
||||
f'</tr>'
|
||||
)
|
||||
return (
|
||||
'<table class="table table-sm table-hover mb-0">'
|
||||
'<thead><tr><th>Order</th><th>Client</th><th>Status</th><th class="text-end">Total</th></tr></thead>'
|
||||
'<tbody>' + ''.join(rows) + '</tbody></table>'
|
||||
)
|
||||
|
||||
def action_open_order(self, order_id):
|
||||
"""Open a specific sale order with breadcrumbs."""
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Sale Order',
|
||||
'res_model': 'sale.order',
|
||||
'view_mode': 'form',
|
||||
'res_id': order_id,
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_open_adp(self):
|
||||
return self._open_type_action('adp')
|
||||
|
||||
def action_open_odsp(self):
|
||||
return self._open_type_action('odsp')
|
||||
|
||||
def action_open_march(self):
|
||||
return self._open_type_action('march_of_dimes')
|
||||
|
||||
def action_open_hardship(self):
|
||||
return self._open_type_action('hardship')
|
||||
|
||||
def action_open_acsd(self):
|
||||
return self._open_type_action('acsd')
|
||||
|
||||
def action_open_muscular(self):
|
||||
return self._open_type_action('muscular_dystrophy')
|
||||
|
||||
def action_open_insurance(self):
|
||||
return self._open_type_action('insurance')
|
||||
|
||||
def action_open_wsib(self):
|
||||
return self._open_type_action('wsib')
|
||||
|
||||
def action_open_profiles(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window', 'name': 'Client Profiles',
|
||||
'res_model': 'fusion.client.profile', 'view_mode': 'list,form',
|
||||
}
|
||||
|
||||
def _open_type_action(self, type_key):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': f'{TYPE_LABELS.get(type_key, type_key)} Cases',
|
||||
'res_model': 'sale.order', 'view_mode': 'list,form',
|
||||
'domain': TYPE_DOMAINS.get(type_key, []),
|
||||
}
|
||||
242
fusion_claims/fusion_claims/models/email_builder_mixin.py
Normal file
242
fusion_claims/fusion_claims/models/email_builder_mixin.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# -*- 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')
|
||||
390
fusion_claims/fusion_claims/models/fusion_adp_device_code.py
Normal file
390
fusion_claims/fusion_claims/models/fusion_adp_device_code.py
Normal file
@@ -0,0 +1,390 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.misc import file_path as get_resource_path
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionADPDeviceCode(models.Model):
|
||||
_name = 'fusion.adp.device.code'
|
||||
_description = 'ADP Device Code Reference (Mobility Manual)'
|
||||
_order = 'device_type, device_code'
|
||||
|
||||
def _register_hook(self):
|
||||
"""
|
||||
Called when the model is loaded.
|
||||
Re-loads device codes from packaged JSON on module upgrade.
|
||||
"""
|
||||
super()._register_hook()
|
||||
# Use with_context to check if this is a module upgrade
|
||||
# The data will be loaded via post_init_hook on install,
|
||||
# and via this hook on upgrade (when module is reloaded)
|
||||
try:
|
||||
self.sudo()._load_packaged_device_codes()
|
||||
except Exception as e:
|
||||
_logger.warning("Could not auto-load device codes: %s", str(e))
|
||||
|
||||
# ==========================================================================
|
||||
# MAIN FIELDS
|
||||
# ==========================================================================
|
||||
device_code = fields.Char(
|
||||
string='Device Code',
|
||||
required=True,
|
||||
index=True,
|
||||
help='ADP device code from the mobility manual',
|
||||
)
|
||||
name = fields.Char(
|
||||
string='Description',
|
||||
compute='_compute_name',
|
||||
store=True,
|
||||
)
|
||||
device_type = fields.Char(
|
||||
string='Device Type',
|
||||
index=True,
|
||||
help='Device type/category (e.g., Adult Wheeled Walker Type 1)',
|
||||
)
|
||||
manufacturer = fields.Char(
|
||||
string='Manufacturer',
|
||||
index=True,
|
||||
help='Device manufacturer',
|
||||
)
|
||||
device_description = fields.Char(
|
||||
string='Device Description',
|
||||
help='Detailed device description from mobility manual',
|
||||
)
|
||||
max_quantity = fields.Integer(
|
||||
string='Max Quantity',
|
||||
default=1,
|
||||
help='Maximum quantity that can be billed per claim',
|
||||
)
|
||||
adp_price = fields.Float(
|
||||
string='ADP Price',
|
||||
digits='Product Price',
|
||||
help='Maximum price ADP will cover for this device',
|
||||
)
|
||||
sn_required = fields.Boolean(
|
||||
string='Serial Number Required',
|
||||
default=False,
|
||||
help='Is serial number required for this device?',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# TRACKING
|
||||
# ==========================================================================
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
)
|
||||
last_updated = fields.Datetime(
|
||||
string='Last Updated',
|
||||
default=fields.Datetime.now,
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# SQL CONSTRAINTS
|
||||
# ==========================================================================
|
||||
_sql_constraints = [
|
||||
('device_code_uniq', 'unique(device_code)',
|
||||
'Device code must be unique!'),
|
||||
]
|
||||
|
||||
# ==========================================================================
|
||||
# COMPUTED FIELDS
|
||||
# ==========================================================================
|
||||
@api.depends('device_code', 'adp_price', 'device_type', 'device_description')
|
||||
def _compute_name(self):
|
||||
for record in self:
|
||||
if record.device_code:
|
||||
if record.device_description:
|
||||
record.name = f"{record.device_code} - {record.device_description} (${record.adp_price:.2f})"
|
||||
else:
|
||||
record.name = f"{record.device_code} (${record.adp_price:.2f})"
|
||||
else:
|
||||
record.name = ''
|
||||
|
||||
# ==========================================================================
|
||||
# DEVICE TYPE LOOKUP (for wizard display)
|
||||
# ==========================================================================
|
||||
@api.model
|
||||
def get_device_type_for_code(self, device_code):
|
||||
"""Get the device type for a given device code."""
|
||||
if not device_code:
|
||||
return ''
|
||||
device = self.search([('device_code', '=', device_code), ('active', '=', True)], limit=1)
|
||||
return device.device_type or ''
|
||||
|
||||
@api.model
|
||||
def get_unique_device_types(self):
|
||||
"""Get list of unique device types from the database."""
|
||||
self.flush_model()
|
||||
self.env.cr.execute("""
|
||||
SELECT DISTINCT device_type
|
||||
FROM fusion_adp_device_code
|
||||
WHERE device_type IS NOT NULL AND device_type != '' AND active = TRUE
|
||||
ORDER BY device_type
|
||||
""")
|
||||
return [row[0] for row in self.env.cr.fetchall()]
|
||||
|
||||
# ==========================================================================
|
||||
# LOOKUP METHODS
|
||||
# ==========================================================================
|
||||
@api.model
|
||||
def get_device_info(self, device_code):
|
||||
"""Get device info by code."""
|
||||
if not device_code:
|
||||
return None
|
||||
|
||||
device = self.search([('device_code', '=', device_code), ('active', '=', True)], limit=1)
|
||||
if device:
|
||||
return {
|
||||
'device_code': device.device_code,
|
||||
'max_quantity': device.max_quantity,
|
||||
'adp_price': device.adp_price,
|
||||
'sn_required': device.sn_required,
|
||||
}
|
||||
return None
|
||||
|
||||
@api.model
|
||||
def validate_device_code(self, device_code):
|
||||
"""Check if a device code exists in the mobility manual."""
|
||||
if not device_code:
|
||||
return False
|
||||
return bool(self.search([('device_code', '=', device_code), ('active', '=', True)], limit=1))
|
||||
|
||||
# ==========================================================================
|
||||
# TEXT CLEANING UTILITIES
|
||||
# ==========================================================================
|
||||
@staticmethod
|
||||
def _clean_text(text):
|
||||
"""Clean text from weird characters, normalize encoding."""
|
||||
if not text:
|
||||
return ''
|
||||
# Convert to string if not already
|
||||
text = str(text)
|
||||
# Remove or replace problematic characters
|
||||
# Replace curly quotes with straight quotes
|
||||
text = text.replace('"', '"').replace('"', '"')
|
||||
text = text.replace(''', "'").replace(''', "'")
|
||||
# Remove non-printable characters except newlines
|
||||
text = ''.join(char if char.isprintable() or char in '\n\r\t' else ' ' for char in text)
|
||||
# Normalize multiple spaces
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
# Strip leading/trailing whitespace
|
||||
return text.strip()
|
||||
|
||||
@staticmethod
|
||||
def _parse_price(price_str):
|
||||
"""Parse price string like '$64.00' or '$2,578.00' to float."""
|
||||
if not price_str:
|
||||
return 0.0
|
||||
# Remove currency symbols, commas, spaces, quotes
|
||||
price_str = str(price_str).strip()
|
||||
price_str = re.sub(r'[\$,"\'\s]', '', price_str)
|
||||
try:
|
||||
return float(price_str)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
# ==========================================================================
|
||||
# IMPORT FROM JSON
|
||||
# ==========================================================================
|
||||
@api.model
|
||||
def import_from_json(self, json_data):
|
||||
"""
|
||||
Import device codes from JSON data.
|
||||
|
||||
Expected format (enhanced with device type, manufacturer, description):
|
||||
[
|
||||
{
|
||||
"Device Type": "Adult Wheeled Walker Type 1",
|
||||
"Manufacturer": "Drive Medical",
|
||||
"Device Description": "One Button Or Dual Trigger Release",
|
||||
"Device Code": "MW1D50005",
|
||||
"Quantity": 1,
|
||||
"ADP Price": 64.00,
|
||||
"SN Required": "Yes"
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
if isinstance(json_data, str):
|
||||
try:
|
||||
data = json.loads(json_data)
|
||||
except json.JSONDecodeError as e:
|
||||
raise UserError(_("Invalid JSON data: %s") % str(e))
|
||||
else:
|
||||
data = json_data
|
||||
|
||||
if not isinstance(data, list):
|
||||
raise UserError(_("Expected a list of device codes"))
|
||||
|
||||
created = 0
|
||||
updated = 0
|
||||
errors = []
|
||||
|
||||
for idx, item in enumerate(data):
|
||||
try:
|
||||
device_code = self._clean_text(item.get('Device Code', '') or item.get('device_code', ''))
|
||||
if not device_code:
|
||||
errors.append(f"Row {idx + 1}: Missing device code")
|
||||
continue
|
||||
|
||||
# Parse fields with cleaning
|
||||
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 quantity
|
||||
qty_val = item.get('Quantity', 1) or item.get('Qty', 1) or item.get('quantity', 1)
|
||||
max_qty = int(qty_val) if qty_val else 1
|
||||
|
||||
# Parse price (handles both raw number and string format)
|
||||
price_val = item.get('ADP Price', 0) or item.get('Approved Price', 0) or item.get('adp_price', 0)
|
||||
if isinstance(price_val, (int, float)):
|
||||
adp_price = float(price_val)
|
||||
else:
|
||||
adp_price = self._parse_price(price_val)
|
||||
|
||||
# Parse serial requirement - handle boolean, string, and various formats
|
||||
sn_raw = item.get('SN Required') or item.get('Serial') or item.get('SN') or item.get('sn_required') or 'No'
|
||||
|
||||
# Handle boolean values directly
|
||||
if isinstance(sn_raw, bool):
|
||||
sn_required = sn_raw
|
||||
else:
|
||||
sn_val = str(sn_raw).upper().strip()
|
||||
sn_required = sn_val in ('YES', 'Y', 'TRUE', '1', 'T')
|
||||
|
||||
# Check if exists
|
||||
existing = self.search([('device_code', '=', device_code)], limit=1)
|
||||
|
||||
vals = {
|
||||
'device_type': device_type,
|
||||
'manufacturer': manufacturer,
|
||||
'device_description': device_description,
|
||||
'max_quantity': max_qty,
|
||||
'adp_price': adp_price,
|
||||
'sn_required': sn_required,
|
||||
'last_updated': fields.Datetime.now(),
|
||||
'active': True,
|
||||
}
|
||||
|
||||
if existing:
|
||||
existing.write(vals)
|
||||
updated += 1
|
||||
else:
|
||||
vals['device_code'] = device_code
|
||||
self.create(vals)
|
||||
created += 1
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Row {idx + 1}: {str(e)}")
|
||||
|
||||
return {
|
||||
'created': created,
|
||||
'updated': updated,
|
||||
'errors': errors,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def import_from_csv_file(self, file_path):
|
||||
"""Import device codes from a CSV file (ADP Mobility Manual format).
|
||||
|
||||
Expected CSV columns: Device Type, Manufacturer, Device Description, Device Code, Qty, Approved Price, Serial
|
||||
"""
|
||||
import csv
|
||||
|
||||
try:
|
||||
data = []
|
||||
with open(file_path, 'r', encoding='utf-8-sig') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
# Skip empty rows
|
||||
device_code = (row.get('Device Code', '') or '').strip()
|
||||
if not device_code:
|
||||
continue
|
||||
|
||||
data.append({
|
||||
'Device Type': row.get('Device Type', ''),
|
||||
'Manufacturer': row.get('Manufacturer', ''),
|
||||
'Device Description': row.get('Device Description', ''),
|
||||
'Device Code': device_code,
|
||||
'Quantity': row.get('Qty', 1),
|
||||
'ADP Price': row.get(' Approved Price ', '') or row.get('Approved Price', ''),
|
||||
'SN Required': row.get('Serial', 'No'),
|
||||
})
|
||||
|
||||
return self.import_from_json(data)
|
||||
except FileNotFoundError:
|
||||
raise UserError(_("File not found: %s") % file_path)
|
||||
except Exception as e:
|
||||
raise UserError(_("Error reading CSV file: %s") % str(e))
|
||||
|
||||
@api.model
|
||||
def import_from_file(self, file_path):
|
||||
"""Import device codes from a JSON file."""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return self.import_from_json(data)
|
||||
except FileNotFoundError:
|
||||
raise UserError(_("File not found: %s") % file_path)
|
||||
except json.JSONDecodeError as e:
|
||||
raise UserError(_("Invalid JSON file: %s") % str(e))
|
||||
except Exception as e:
|
||||
raise UserError(_("Error reading file: %s") % str(e))
|
||||
|
||||
# ==========================================================================
|
||||
# AUTO-LOAD FROM PACKAGED DATA FILE
|
||||
# ==========================================================================
|
||||
@api.model
|
||||
def _load_packaged_device_codes(self):
|
||||
"""
|
||||
Load device codes from the packaged JSON file.
|
||||
Called automatically on module install/upgrade via post_init_hook.
|
||||
|
||||
The JSON file is located at: fusion_claims/data/device_codes/adp_mobility_manual.json
|
||||
"""
|
||||
_logger.info("Loading ADP Mobility Manual device codes from packaged data file...")
|
||||
|
||||
# Get the path to the packaged JSON file
|
||||
try:
|
||||
json_path = get_resource_path('fusion_claims/data/device_codes/adp_mobility_manual.json')
|
||||
except FileNotFoundError:
|
||||
json_path = None
|
||||
|
||||
if not json_path or not os.path.exists(json_path):
|
||||
_logger.warning("ADP Mobility Manual JSON file not found at expected location.")
|
||||
return {'created': 0, 'updated': 0, 'errors': ['JSON file not found']}
|
||||
|
||||
try:
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
result = self.import_from_json(data)
|
||||
|
||||
_logger.info(
|
||||
"ADP Mobility Manual import complete: %d created, %d updated, %d errors",
|
||||
result.get('created', 0),
|
||||
result.get('updated', 0),
|
||||
len(result.get('errors', []))
|
||||
)
|
||||
|
||||
if result.get('errors'):
|
||||
for error in result['errors'][:10]: # Log first 10 errors
|
||||
_logger.warning("Import error: %s", error)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
_logger.error("Error loading ADP Mobility Manual: %s", str(e))
|
||||
return {'created': 0, 'updated': 0, 'errors': [str(e)]}
|
||||
126
fusion_claims/fusion_claims/models/fusion_central_config.py
Normal file
126
fusion_claims/fusion_claims/models/fusion_central_config.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
import logging
|
||||
from odoo import models, api, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionCentralConfig(models.TransientModel):
|
||||
_name = 'fusion_claims.config'
|
||||
_description = 'Fusion Central Configuration Manager'
|
||||
|
||||
# =========================================================================
|
||||
# ACTION METHODS
|
||||
# =========================================================================
|
||||
|
||||
def action_detect_existing_fields(self):
|
||||
"""Detect existing custom x_* fields and map them."""
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
IrModelFields = self.env['ir.model.fields'].sudo()
|
||||
detected = []
|
||||
detected_details = []
|
||||
|
||||
# Search for all custom fields on relevant models
|
||||
models_to_search = ['sale.order', 'sale.order.line', 'account.move', 'account.move.line', 'product.template']
|
||||
|
||||
# Find all custom x_* fields
|
||||
all_custom_fields = IrModelFields.search([
|
||||
('model', 'in', models_to_search),
|
||||
('name', '=like', 'x_%'),
|
||||
('state', '=', 'manual'),
|
||||
])
|
||||
|
||||
_logger.debug("Found %d custom fields across models", len(all_custom_fields))
|
||||
|
||||
# Field patterns to detect (model, keywords, param_key, display_name)
|
||||
# Keywords are checked if they appear anywhere in the field name
|
||||
# NOTE: param_key must match the config_parameter in res_config_settings.py
|
||||
field_mappings = [
|
||||
# Sale Order header fields
|
||||
('sale.order', ['sale_type', 'saletype', 'type_of_sale'], 'fusion_claims.field_sale_type', 'Sale Type'),
|
||||
('sale.order', ['client_type', 'clienttype', 'customer_type'], 'fusion_claims.field_so_client_type', 'SO Client Type'),
|
||||
('sale.order', ['authorizer', 'authorized', 'approver'], 'fusion_claims.field_so_authorizer', 'SO Authorizer'),
|
||||
('sale.order', ['claim_number', 'claimnumber', 'claim_no', 'claim_num'], 'fusion_claims.field_so_claim_number', 'SO Claim Number'),
|
||||
('sale.order', ['client_ref_1', 'clientref1', 'reference_1'], 'fusion_claims.field_so_client_ref_1', 'SO Client Ref 1'),
|
||||
('sale.order', ['client_ref_2', 'clientref2', 'reference_2'], 'fusion_claims.field_so_client_ref_2', 'SO Client Ref 2'),
|
||||
('sale.order', ['delivery_date', 'deliverydate', 'adp_delivery'], 'fusion_claims.field_so_delivery_date', 'SO Delivery Date'),
|
||||
('sale.order', ['service_start', 'servicestart'], 'fusion_claims.field_so_service_start', 'SO Service Start'),
|
||||
('sale.order', ['service_end', 'serviceend'], 'fusion_claims.field_so_service_end', 'SO Service End'),
|
||||
('sale.order', ['adp_status', 'adpstatus'], 'fusion_claims.field_so_adp_status', 'SO ADP Status'),
|
||||
# Sale Order line fields
|
||||
('sale.order.line', ['serial', 'sn', 's_n'], 'fusion_claims.field_sol_serial', 'SO Line Serial'),
|
||||
('sale.order.line', ['placement', 'device_placement'], 'fusion_claims.field_sol_placement', 'SO Line Placement'),
|
||||
# Invoice header fields
|
||||
('account.move', ['invoice_type', 'invoicetype', 'inv_type', 'type_of_invoice'], 'fusion_claims.field_invoice_type', 'Invoice Type'),
|
||||
('account.move', ['client_type', 'clienttype', 'customer_type'], 'fusion_claims.field_inv_client_type', 'Invoice Client Type'),
|
||||
('account.move', ['authorizer', 'authorized', 'approver'], 'fusion_claims.field_inv_authorizer', 'Invoice Authorizer'),
|
||||
('account.move', ['claim_number', 'claimnumber', 'claim_no'], 'fusion_claims.field_inv_claim_number', 'Invoice Claim Number'),
|
||||
('account.move', ['client_ref_1', 'clientref1', 'reference_1'], 'fusion_claims.field_inv_client_ref_1', 'Invoice Client Ref 1'),
|
||||
('account.move', ['client_ref_2', 'clientref2', 'reference_2'], 'fusion_claims.field_inv_client_ref_2', 'Invoice Client Ref 2'),
|
||||
('account.move', ['delivery_date', 'deliverydate', 'adp_delivery'], 'fusion_claims.field_inv_delivery_date', 'Invoice Delivery Date'),
|
||||
('account.move', ['service_start', 'servicestart'], 'fusion_claims.field_inv_service_start', 'Invoice Service Start'),
|
||||
('account.move', ['service_end', 'serviceend'], 'fusion_claims.field_inv_service_end', 'Invoice Service End'),
|
||||
# Invoice line fields
|
||||
('account.move.line', ['serial', 'sn', 's_n'], 'fusion_claims.field_aml_serial', 'Invoice Line Serial'),
|
||||
('account.move.line', ['placement', 'device_placement'], 'fusion_claims.field_aml_placement', 'Invoice Line Placement'),
|
||||
# Product fields
|
||||
('product.template', ['adp_device', 'adp_code', 'adp_sku', 'device_code', 'sku'], 'fusion_claims.field_product_code', 'Product ADP Code'),
|
||||
]
|
||||
|
||||
for model, keywords, param_key, display_name in field_mappings:
|
||||
# Find fields on this model that contain any of the keywords
|
||||
model_fields = all_custom_fields.filtered(lambda f: f.model == model)
|
||||
|
||||
model_fields_sorted = sorted(model_fields, key=lambda f: f.name)
|
||||
|
||||
matched_field = None
|
||||
for field in model_fields_sorted:
|
||||
field_name_lower = field.name.lower()
|
||||
for keyword in keywords:
|
||||
if keyword in field_name_lower:
|
||||
# Skip our own x_fc_* fields - we want to find other custom fields
|
||||
if field.name.startswith('x_fc_'):
|
||||
continue
|
||||
matched_field = field
|
||||
break
|
||||
if matched_field:
|
||||
break
|
||||
|
||||
if matched_field:
|
||||
ICP.set_param(param_key, matched_field.name)
|
||||
detected.append(matched_field.name)
|
||||
detected_details.append(f"• {display_name}: {matched_field.name} ({model})")
|
||||
_logger.debug("Mapped %s -> %s on %s", param_key, matched_field.name, model)
|
||||
|
||||
# Also list any unmapped custom fields for reference
|
||||
unmapped = []
|
||||
for field in all_custom_fields:
|
||||
if field.name not in detected:
|
||||
unmapped.append(f"{field.model}.{field.name}")
|
||||
|
||||
if detected_details:
|
||||
message = _("Detected and mapped %d fields:\n%s") % (len(detected), "\n".join(detected_details))
|
||||
if unmapped:
|
||||
message += _("\n\nOther custom fields found (not mapped):\n• ") + "\n• ".join(unmapped[:10])
|
||||
if len(unmapped) > 10:
|
||||
message += f"\n... and {len(unmapped) - 10} more"
|
||||
message += _("\n\n⚠️ IMPORTANT: Save settings and reload page to see changes.")
|
||||
else:
|
||||
message = _("No matching fields found.\n\nCustom fields found:\n• ") + "\n• ".join(unmapped[:15]) if unmapped else _("No custom fields found on relevant models.")
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _("Field Detection Complete"),
|
||||
'message': message,
|
||||
'type': 'success' if detected else 'warning',
|
||||
'sticky': True,
|
||||
}
|
||||
}
|
||||
|
||||
# (Migration and field protection methods removed)
|
||||
799
fusion_claims/fusion_claims/models/fusion_loaner_checkout.py
Normal file
799
fusion_claims/fusion_claims/models/fusion_loaner_checkout.py
Normal file
@@ -0,0 +1,799 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from markupsafe import Markup
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionLoanerCheckout(models.Model):
|
||||
"""Track loaner equipment checkouts and returns."""
|
||||
_name = 'fusion.loaner.checkout'
|
||||
_description = 'Loaner Equipment Checkout'
|
||||
_order = 'checkout_date desc, id desc'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin', 'fusion.email.builder.mixin']
|
||||
|
||||
# =========================================================================
|
||||
# REFERENCE FIELDS
|
||||
# =========================================================================
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: _('New'),
|
||||
)
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Sale Order',
|
||||
ondelete='set null',
|
||||
tracking=True,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Client',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
authorizer_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Authorizer',
|
||||
help='Therapist/Authorizer associated with this loaner',
|
||||
)
|
||||
sales_rep_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Sales Rep',
|
||||
default=lambda self: self.env.user,
|
||||
tracking=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# PRODUCT & SERIAL
|
||||
# =========================================================================
|
||||
|
||||
product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string='Product',
|
||||
required=True,
|
||||
domain="[('x_fc_can_be_loaned', '=', True)]",
|
||||
tracking=True,
|
||||
)
|
||||
lot_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Serial Number',
|
||||
domain="[('product_id', '=', product_id)]",
|
||||
tracking=True,
|
||||
)
|
||||
product_description = fields.Text(
|
||||
string='Product Description',
|
||||
related='product_id.description_sale',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# DATES
|
||||
# =========================================================================
|
||||
|
||||
checkout_date = fields.Date(
|
||||
string='Checkout Date',
|
||||
required=True,
|
||||
default=fields.Date.context_today,
|
||||
tracking=True,
|
||||
)
|
||||
loaner_period_days = fields.Integer(
|
||||
string='Loaner Period (Days)',
|
||||
default=7,
|
||||
help='Number of free loaner days before rental conversion',
|
||||
)
|
||||
expected_return_date = fields.Date(
|
||||
string='Expected Return Date',
|
||||
compute='_compute_expected_return_date',
|
||||
store=True,
|
||||
)
|
||||
actual_return_date = fields.Date(
|
||||
string='Actual Return Date',
|
||||
tracking=True,
|
||||
)
|
||||
days_out = fields.Integer(
|
||||
string='Days Out',
|
||||
compute='_compute_days_out',
|
||||
)
|
||||
days_overdue = fields.Integer(
|
||||
string='Days Overdue',
|
||||
compute='_compute_days_overdue',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# STATUS
|
||||
# =========================================================================
|
||||
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('checked_out', 'Checked Out'),
|
||||
('overdue', 'Overdue'),
|
||||
('rental_pending', 'Rental Conversion Pending'),
|
||||
('returned', 'Returned'),
|
||||
('converted_rental', 'Converted to Rental'),
|
||||
('lost', 'Lost/Write-off'),
|
||||
], string='Status', default='draft', tracking=True, required=True)
|
||||
|
||||
# =========================================================================
|
||||
# LOCATION
|
||||
# =========================================================================
|
||||
|
||||
delivery_address = fields.Text(
|
||||
string='Delivery Address',
|
||||
help='Where the loaner was delivered',
|
||||
)
|
||||
return_location_id = fields.Many2one(
|
||||
'stock.location',
|
||||
string='Return Location',
|
||||
domain="[('usage', '=', 'internal')]",
|
||||
help='Where the loaner was returned to (store, warehouse, etc.)',
|
||||
tracking=True,
|
||||
)
|
||||
checked_out_by_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Checked Out By',
|
||||
default=lambda self: self.env.user,
|
||||
)
|
||||
returned_to_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Returned To',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# CHECKOUT CONDITION
|
||||
# =========================================================================
|
||||
|
||||
checkout_condition = fields.Selection([
|
||||
('excellent', 'Excellent'),
|
||||
('good', 'Good'),
|
||||
('fair', 'Fair'),
|
||||
('needs_repair', 'Needs Repair'),
|
||||
], string='Checkout Condition', default='excellent')
|
||||
checkout_notes = fields.Text(
|
||||
string='Checkout Notes',
|
||||
)
|
||||
checkout_photo_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'fusion_loaner_checkout_photo_rel',
|
||||
'checkout_id',
|
||||
'attachment_id',
|
||||
string='Checkout Photos',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# RETURN CONDITION
|
||||
# =========================================================================
|
||||
|
||||
return_condition = fields.Selection([
|
||||
('excellent', 'Excellent'),
|
||||
('good', 'Good'),
|
||||
('fair', 'Fair'),
|
||||
('needs_repair', 'Needs Repair'),
|
||||
('damaged', 'Damaged'),
|
||||
], string='Return Condition')
|
||||
return_notes = fields.Text(
|
||||
string='Return Notes',
|
||||
)
|
||||
return_photo_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'fusion_loaner_return_photo_rel',
|
||||
'checkout_id',
|
||||
'attachment_id',
|
||||
string='Return Photos',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# REMINDER TRACKING
|
||||
# =========================================================================
|
||||
|
||||
reminder_day5_sent = fields.Boolean(
|
||||
string='Day 5 Reminder Sent',
|
||||
default=False,
|
||||
)
|
||||
reminder_day8_sent = fields.Boolean(
|
||||
string='Day 8 Warning Sent',
|
||||
default=False,
|
||||
)
|
||||
reminder_day10_sent = fields.Boolean(
|
||||
string='Day 10 Final Notice Sent',
|
||||
default=False,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# RENTAL CONVERSION
|
||||
# =========================================================================
|
||||
|
||||
rental_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Rental Order',
|
||||
help='Sale order created when loaner converted to rental',
|
||||
)
|
||||
rental_conversion_date = fields.Date(
|
||||
string='Rental Conversion Date',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# STOCK MOVES
|
||||
# =========================================================================
|
||||
|
||||
checkout_move_id = fields.Many2one(
|
||||
'stock.move',
|
||||
string='Checkout Stock Move',
|
||||
)
|
||||
return_move_id = fields.Many2one(
|
||||
'stock.move',
|
||||
string='Return Stock Move',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# HISTORY
|
||||
# =========================================================================
|
||||
|
||||
history_ids = fields.One2many(
|
||||
'fusion.loaner.history',
|
||||
'checkout_id',
|
||||
string='History',
|
||||
)
|
||||
history_count = fields.Integer(
|
||||
compute='_compute_history_count',
|
||||
string='History Count',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# COMPUTED FIELDS
|
||||
# =========================================================================
|
||||
|
||||
@api.depends('checkout_date', 'loaner_period_days')
|
||||
def _compute_expected_return_date(self):
|
||||
for record in self:
|
||||
if record.checkout_date and record.loaner_period_days:
|
||||
record.expected_return_date = record.checkout_date + timedelta(days=record.loaner_period_days)
|
||||
else:
|
||||
record.expected_return_date = False
|
||||
|
||||
@api.depends('checkout_date', 'actual_return_date')
|
||||
def _compute_days_out(self):
|
||||
today = fields.Date.today()
|
||||
for record in self:
|
||||
if record.checkout_date:
|
||||
end_date = record.actual_return_date or today
|
||||
record.days_out = (end_date - record.checkout_date).days
|
||||
else:
|
||||
record.days_out = 0
|
||||
|
||||
@api.depends('expected_return_date', 'actual_return_date', 'state')
|
||||
def _compute_days_overdue(self):
|
||||
today = fields.Date.today()
|
||||
for record in self:
|
||||
if record.state in ('returned', 'converted_rental', 'lost'):
|
||||
record.days_overdue = 0
|
||||
elif record.expected_return_date:
|
||||
end_date = record.actual_return_date or today
|
||||
overdue = (end_date - record.expected_return_date).days
|
||||
record.days_overdue = max(0, overdue)
|
||||
else:
|
||||
record.days_overdue = 0
|
||||
|
||||
def _compute_history_count(self):
|
||||
for record in self:
|
||||
record.history_count = len(record.history_ids)
|
||||
|
||||
# =========================================================================
|
||||
# ONCHANGE
|
||||
# =========================================================================
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
if self.product_id:
|
||||
self.loaner_period_days = self.product_id.x_fc_loaner_period_days or 7
|
||||
self.lot_id = False
|
||||
|
||||
@api.onchange('sale_order_id')
|
||||
def _onchange_sale_order_id(self):
|
||||
if self.sale_order_id:
|
||||
self.partner_id = self.sale_order_id.partner_id
|
||||
self.authorizer_id = self.sale_order_id.x_fc_authorizer_id
|
||||
self.sales_rep_id = self.sale_order_id.user_id
|
||||
self.delivery_address = self.sale_order_id.partner_shipping_id.contact_address if self.sale_order_id.partner_shipping_id else ''
|
||||
|
||||
# =========================================================================
|
||||
# CRUD
|
||||
# =========================================================================
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('fusion.loaner.checkout') or _('New')
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
record._log_history('create', 'Loaner checkout created')
|
||||
return records
|
||||
|
||||
# =========================================================================
|
||||
# ACTIONS
|
||||
# =========================================================================
|
||||
|
||||
def action_checkout(self):
|
||||
"""Confirm the loaner checkout."""
|
||||
self.ensure_one()
|
||||
if self.state != 'draft':
|
||||
raise UserError(_("Can only checkout from draft state."))
|
||||
|
||||
if not self.product_id:
|
||||
raise UserError(_("Please select a product."))
|
||||
|
||||
self.write({'state': 'checked_out'})
|
||||
self._log_history('checkout', f'Loaner checked out to {self.partner_id.name}')
|
||||
# Stock move is non-blocking -- use savepoint so failure doesn't roll back checkout
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
self._create_checkout_stock_move()
|
||||
except Exception as e:
|
||||
_logger.warning(f"Stock move failed for checkout {self.name} (non-blocking): {e}")
|
||||
self._send_checkout_email()
|
||||
|
||||
# Post to chatter
|
||||
self.message_post(
|
||||
body=Markup(
|
||||
'<div class="alert alert-success">'
|
||||
f'<strong>Loaner Checked Out</strong><br/>'
|
||||
f'Product: {self.product_id.name}<br/>'
|
||||
f'Serial: {self.lot_id.name if self.lot_id else "N/A"}<br/>'
|
||||
f'Expected Return: {self.expected_return_date}'
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_return(self):
|
||||
"""Open return wizard."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Return Loaner'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.loaner.return.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_checkout_id': self.id,
|
||||
},
|
||||
}
|
||||
|
||||
def action_process_return(self, return_condition, return_notes=None, return_photos=None, return_location_id=None):
|
||||
"""Process the loaner return."""
|
||||
self.ensure_one()
|
||||
if self.state not in ('checked_out', 'overdue', 'rental_pending'):
|
||||
raise UserError(_("Cannot return a loaner that is not checked out."))
|
||||
|
||||
vals = {
|
||||
'state': 'returned',
|
||||
'actual_return_date': fields.Date.today(),
|
||||
'return_condition': return_condition,
|
||||
'return_notes': return_notes,
|
||||
'returned_to_id': self.env.user.id,
|
||||
}
|
||||
if return_location_id:
|
||||
vals['return_location_id'] = return_location_id
|
||||
if return_photos:
|
||||
vals['return_photo_ids'] = [(6, 0, return_photos)]
|
||||
|
||||
self.write(vals)
|
||||
self._log_history('return', f'Loaner returned in {return_condition} condition')
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
self._create_return_stock_move()
|
||||
except Exception as e:
|
||||
_logger.warning(f"Stock move failed for return {self.name} (non-blocking): {e}")
|
||||
self._send_return_email()
|
||||
|
||||
# Post to chatter
|
||||
self.message_post(
|
||||
body=Markup(
|
||||
'<div class="alert alert-info">'
|
||||
f'<strong>Loaner Returned</strong><br/>'
|
||||
f'Condition: {return_condition}<br/>'
|
||||
f'Days Out: {self.days_out}'
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def action_mark_lost(self):
|
||||
"""Mark loaner as lost."""
|
||||
self.ensure_one()
|
||||
self.write({'state': 'lost'})
|
||||
self._log_history('lost', 'Loaner marked as lost/write-off')
|
||||
|
||||
self.message_post(
|
||||
body=Markup(
|
||||
'<div class="alert alert-danger">'
|
||||
'<strong>Loaner Marked as Lost</strong><br/>'
|
||||
f'Product: {self.product_id.name}<br/>'
|
||||
f'Serial: {self.lot_id.name if self.lot_id else "N/A"}'
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def action_convert_to_rental(self):
|
||||
"""Flag for rental conversion."""
|
||||
self.ensure_one()
|
||||
self.write({
|
||||
'state': 'rental_pending',
|
||||
'rental_conversion_date': fields.Date.today(),
|
||||
})
|
||||
self._log_history('rental_pending', 'Loaner flagged for rental conversion')
|
||||
self._send_rental_conversion_email()
|
||||
|
||||
def action_view_history(self):
|
||||
"""View loaner history."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Loaner History'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.loaner.history',
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('checkout_id', '=', self.id)],
|
||||
'context': {'default_checkout_id': self.id},
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# STOCK MOVES
|
||||
# =========================================================================
|
||||
|
||||
def _get_loaner_location(self):
|
||||
"""Get the loaner stock location."""
|
||||
location = self.env.ref('fusion_claims.stock_location_loaner', raise_if_not_found=False)
|
||||
if not location:
|
||||
# Fallback to main stock
|
||||
location = self.env.ref('stock.stock_location_stock')
|
||||
return location
|
||||
|
||||
def _get_customer_location(self):
|
||||
"""Get customer location for stock moves."""
|
||||
return self.env.ref('stock.stock_location_customers')
|
||||
|
||||
def _create_checkout_stock_move(self):
|
||||
"""Create stock move for checkout. Non-blocking -- checkout proceeds even if move fails."""
|
||||
if not self.lot_id:
|
||||
return # No serial tracking
|
||||
|
||||
try:
|
||||
source_location = self._get_loaner_location()
|
||||
dest_location = self._get_customer_location()
|
||||
|
||||
move_vals = {
|
||||
'name': f'Loaner Checkout: {self.name}',
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.product_id.uom_id.id,
|
||||
'location_id': source_location.id,
|
||||
'location_dest_id': dest_location.id,
|
||||
'origin': self.name,
|
||||
'company_id': self.company_id.id,
|
||||
'procure_method': 'make_to_stock',
|
||||
}
|
||||
move = self.env['stock.move'].sudo().create(move_vals)
|
||||
move._action_confirm()
|
||||
move._action_assign()
|
||||
|
||||
# Set the lot on move line
|
||||
if move.move_line_ids:
|
||||
move.move_line_ids.write({'lot_id': self.lot_id.id})
|
||||
|
||||
move._action_done()
|
||||
self.checkout_move_id = move.id
|
||||
except Exception as e:
|
||||
_logger.warning(f"Could not create checkout stock move (non-blocking): {e}")
|
||||
|
||||
def _create_return_stock_move(self):
|
||||
"""Create stock move for return. Uses return_location_id if set, otherwise Loaner Stock."""
|
||||
if not self.lot_id:
|
||||
return
|
||||
|
||||
try:
|
||||
source_location = self._get_customer_location()
|
||||
dest_location = self.return_location_id or self._get_loaner_location()
|
||||
|
||||
move_vals = {
|
||||
'name': f'Loaner Return: {self.name}',
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.product_id.uom_id.id,
|
||||
'location_id': source_location.id,
|
||||
'location_dest_id': dest_location.id,
|
||||
'origin': self.name,
|
||||
'company_id': self.company_id.id,
|
||||
'procure_method': 'make_to_stock',
|
||||
}
|
||||
move = self.env['stock.move'].sudo().create(move_vals)
|
||||
move._action_confirm()
|
||||
move._action_assign()
|
||||
|
||||
if move.move_line_ids:
|
||||
move.move_line_ids.write({'lot_id': self.lot_id.id})
|
||||
|
||||
move._action_done()
|
||||
self.return_move_id = move.id
|
||||
except Exception as e:
|
||||
_logger.warning(f"Could not create return stock move: {e}")
|
||||
|
||||
# =========================================================================
|
||||
# HISTORY LOGGING
|
||||
# =========================================================================
|
||||
|
||||
def _log_history(self, action, notes=None):
|
||||
"""Log action to history."""
|
||||
self.ensure_one()
|
||||
self.env['fusion.loaner.history'].create({
|
||||
'checkout_id': self.id,
|
||||
'lot_id': self.lot_id.id if self.lot_id else False,
|
||||
'action': action,
|
||||
'notes': notes,
|
||||
})
|
||||
|
||||
# =========================================================================
|
||||
# EMAIL METHODS
|
||||
# =========================================================================
|
||||
|
||||
def _get_email_recipients(self):
|
||||
"""Get all email recipients for loaner notifications."""
|
||||
recipients = {
|
||||
'client_email': self.partner_id.email if self.partner_id else None,
|
||||
'authorizer_email': self.authorizer_id.email if self.authorizer_id else None,
|
||||
'sales_rep_email': self.sales_rep_id.email if self.sales_rep_id else None,
|
||||
'office_emails': [],
|
||||
}
|
||||
|
||||
# Get office emails from company
|
||||
company = self.company_id or self.env.company
|
||||
office_partners = company.sudo().x_fc_office_notification_ids
|
||||
recipients['office_emails'] = [p.email for p in office_partners if p.email]
|
||||
|
||||
return recipients
|
||||
|
||||
def _send_checkout_email(self):
|
||||
"""Send checkout confirmation email to all parties."""
|
||||
self.ensure_one()
|
||||
recipients = self._get_email_recipients()
|
||||
to_emails = [e for e in [recipients['client_email'], recipients['authorizer_email']] if e]
|
||||
cc_emails = [e for e in [recipients['sales_rep_email']] if e] + recipients['office_emails']
|
||||
if not to_emails:
|
||||
return False
|
||||
|
||||
client_name = self.partner_id.name or 'Client'
|
||||
product_name = self.product_id.name or 'Product'
|
||||
expected_return = self.expected_return_date.strftime('%B %d, %Y') if self.expected_return_date else 'N/A'
|
||||
|
||||
body_html = self._email_build(
|
||||
title='Loaner Equipment Checkout',
|
||||
summary=f'Loaner equipment has been checked out for <strong>{client_name}</strong>.',
|
||||
email_type='info',
|
||||
sections=[('Loaner Details', [
|
||||
('Reference', self.name),
|
||||
('Product', product_name),
|
||||
('Serial Number', self.lot_id.name if self.lot_id else None),
|
||||
('Checkout Date', self.checkout_date.strftime('%B %d, %Y') if self.checkout_date else None),
|
||||
('Expected Return', expected_return),
|
||||
('Loaner Period', f'{self.loaner_period_days} days'),
|
||||
])],
|
||||
note='<strong>Important:</strong> Please return the loaner equipment by the expected return date. '
|
||||
'If not returned on time, rental charges may apply.',
|
||||
note_color='#d69e2e',
|
||||
)
|
||||
try:
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'subject': f'Loaner Checkout - {product_name} - {self.name}',
|
||||
'body_html': body_html,
|
||||
'email_to': ', '.join(to_emails),
|
||||
'email_cc': ', '.join(cc_emails) if cc_emails else '',
|
||||
'model': 'fusion.loaner.checkout', 'res_id': self.id,
|
||||
}).send()
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to send checkout email for {self.name}: {e}")
|
||||
return False
|
||||
|
||||
def _send_return_email(self):
|
||||
"""Send return confirmation email."""
|
||||
self.ensure_one()
|
||||
recipients = self._get_email_recipients()
|
||||
to_emails = [e for e in [recipients['client_email']] if e]
|
||||
cc_emails = [e for e in [recipients['sales_rep_email']] if e]
|
||||
if not to_emails:
|
||||
return False
|
||||
|
||||
client_name = self.partner_id.name or 'Client'
|
||||
product_name = self.product_id.name or 'Product'
|
||||
|
||||
body_html = self._email_build(
|
||||
title='Loaner Equipment Returned',
|
||||
summary=f'Thank you for returning the loaner equipment, <strong>{client_name}</strong>.',
|
||||
email_type='success',
|
||||
sections=[('Return Details', [
|
||||
('Reference', self.name),
|
||||
('Product', product_name),
|
||||
('Return Date', self.actual_return_date.strftime('%B %d, %Y') if self.actual_return_date else None),
|
||||
('Condition', self.return_condition or None),
|
||||
('Days Out', str(self.days_out)),
|
||||
])],
|
||||
)
|
||||
try:
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'subject': f'Loaner Returned - {product_name} - {self.name}',
|
||||
'body_html': body_html,
|
||||
'email_to': ', '.join(to_emails),
|
||||
'email_cc': ', '.join(cc_emails) if cc_emails else '',
|
||||
'model': 'fusion.loaner.checkout', 'res_id': self.id,
|
||||
}).send()
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to send return email for {self.name}: {e}")
|
||||
return False
|
||||
|
||||
def _send_rental_conversion_email(self):
|
||||
"""Send rental conversion notification."""
|
||||
self.ensure_one()
|
||||
recipients = self._get_email_recipients()
|
||||
to_emails = [e for e in [recipients['client_email'], recipients['authorizer_email']] if e]
|
||||
cc_emails = [e for e in [recipients['sales_rep_email']] if e] + recipients['office_emails']
|
||||
if not to_emails and not cc_emails:
|
||||
return False
|
||||
|
||||
client_name = self.partner_id.name or 'Client'
|
||||
product_name = self.product_id.name or 'Product'
|
||||
weekly_rate = self.product_id.x_fc_rental_price_weekly or 0
|
||||
monthly_rate = self.product_id.x_fc_rental_price_monthly or 0
|
||||
|
||||
body_html = self._email_build(
|
||||
title='Loaner Rental Conversion Notice',
|
||||
summary=f'The loaner equipment for <strong>{client_name}</strong> has exceeded the free loaner period.',
|
||||
email_type='urgent',
|
||||
sections=[('Equipment Details', [
|
||||
('Reference', self.name),
|
||||
('Product', product_name),
|
||||
('Days Out', str(self.days_out)),
|
||||
('Days Overdue', str(self.days_overdue)),
|
||||
('Weekly Rental Rate', f'${weekly_rate:.2f}'),
|
||||
('Monthly Rental Rate', f'${monthly_rate:.2f}'),
|
||||
])],
|
||||
note='<strong>Action required:</strong> Please return the equipment or contact us to arrange '
|
||||
'a rental agreement. Rental charges will apply until the equipment is returned.',
|
||||
note_color='#c53030',
|
||||
)
|
||||
email_to = ', '.join(to_emails) if to_emails else ', '.join(cc_emails[:1])
|
||||
email_cc = ', '.join(cc_emails) if to_emails else ', '.join(cc_emails[1:])
|
||||
try:
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'subject': f'Loaner Rental Conversion - {product_name} - {self.name}',
|
||||
'body_html': body_html,
|
||||
'email_to': email_to, 'email_cc': email_cc,
|
||||
'model': 'fusion.loaner.checkout', 'res_id': self.id,
|
||||
}).send()
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to send rental conversion email for {self.name}: {e}")
|
||||
return False
|
||||
|
||||
def _send_reminder_email(self, reminder_type):
|
||||
"""Send reminder email based on type (day5, day8, day10)."""
|
||||
self.ensure_one()
|
||||
recipients = self._get_email_recipients()
|
||||
client_name = self.partner_id.name or 'Client'
|
||||
product_name = self.product_id.name or 'Product'
|
||||
expected_return = self.expected_return_date.strftime('%B %d, %Y') if self.expected_return_date else 'N/A'
|
||||
|
||||
if reminder_type == 'day5':
|
||||
to_emails = [e for e in [recipients['sales_rep_email']] if e] + recipients['office_emails']
|
||||
cc_emails = []
|
||||
subject = f'Loaner Reminder: {product_name} - Day 5'
|
||||
email_type = 'attention'
|
||||
message = (f'The loaner equipment for {client_name} has been out for 5 days. '
|
||||
f'Please follow up to arrange return.')
|
||||
elif reminder_type == 'day8':
|
||||
to_emails = [e for e in [recipients['client_email']] if e]
|
||||
cc_emails = [e for e in [recipients['sales_rep_email']] if e] + recipients['office_emails']
|
||||
subject = f'Loaner Return Reminder - {product_name}'
|
||||
email_type = 'attention'
|
||||
message = (f'Your loaner equipment has been out for 8 days. '
|
||||
f'Please return it soon or it may be converted to a rental.')
|
||||
else:
|
||||
to_emails = [e for e in [recipients['client_email'], recipients['authorizer_email']] if e]
|
||||
cc_emails = [e for e in [recipients['sales_rep_email']] if e] + recipients['office_emails']
|
||||
subject = f'Loaner Return Required - {product_name}'
|
||||
email_type = 'urgent'
|
||||
message = (f'Your loaner equipment has been out for {self.days_out} days. '
|
||||
f'If not returned, rental charges will apply.')
|
||||
|
||||
if not to_emails:
|
||||
return False
|
||||
|
||||
body_html = self._email_build(
|
||||
title='Loaner Equipment Reminder',
|
||||
summary=message,
|
||||
email_type=email_type,
|
||||
sections=[('Loaner Details', [
|
||||
('Reference', self.name),
|
||||
('Client', client_name),
|
||||
('Product', product_name),
|
||||
('Days Out', str(self.days_out)),
|
||||
('Expected Return', expected_return),
|
||||
])],
|
||||
)
|
||||
try:
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'subject': subject,
|
||||
'body_html': body_html,
|
||||
'email_to': ', '.join(to_emails),
|
||||
'email_cc': ', '.join(cc_emails) if cc_emails else '',
|
||||
'model': 'fusion.loaner.checkout', 'res_id': self.id,
|
||||
}).send()
|
||||
return True
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to send {reminder_type} reminder for {self.name}: {e}")
|
||||
return False
|
||||
|
||||
# =========================================================================
|
||||
# CRON METHODS
|
||||
# =========================================================================
|
||||
|
||||
@api.model
|
||||
def _cron_check_overdue_loaners(self):
|
||||
"""Daily cron to check for overdue loaners and send reminders."""
|
||||
today = fields.Date.today()
|
||||
|
||||
# Find all active loaners
|
||||
active_loaners = self.search([
|
||||
('state', 'in', ['checked_out', 'overdue', 'rental_pending']),
|
||||
])
|
||||
|
||||
for loaner in active_loaners:
|
||||
days_out = loaner.days_out
|
||||
|
||||
# Update overdue status
|
||||
if loaner.state == 'checked_out' and loaner.expected_return_date and today > loaner.expected_return_date:
|
||||
loaner.write({'state': 'overdue'})
|
||||
loaner._log_history('overdue', f'Loaner is now overdue by {loaner.days_overdue} days')
|
||||
|
||||
# Day 5 reminder
|
||||
if days_out >= 5 and not loaner.reminder_day5_sent:
|
||||
loaner._send_reminder_email('day5')
|
||||
loaner.reminder_day5_sent = True
|
||||
loaner._log_history('reminder_sent', 'Day 5 reminder sent')
|
||||
|
||||
# Day 8 warning
|
||||
if days_out >= 8 and not loaner.reminder_day8_sent:
|
||||
loaner._send_reminder_email('day8')
|
||||
loaner.reminder_day8_sent = True
|
||||
loaner._log_history('reminder_sent', 'Day 8 rental warning sent')
|
||||
|
||||
# Day 10 final notice
|
||||
if days_out >= 10 and not loaner.reminder_day10_sent:
|
||||
loaner._send_reminder_email('day10')
|
||||
loaner.reminder_day10_sent = True
|
||||
loaner._log_history('reminder_sent', 'Day 10 final notice sent')
|
||||
|
||||
# Flag for rental conversion
|
||||
if loaner.state != 'rental_pending':
|
||||
loaner.action_convert_to_rental()
|
||||
105
fusion_claims/fusion_claims/models/fusion_loaner_history.py
Normal file
105
fusion_claims/fusion_claims/models/fusion_loaner_history.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionLoanerHistory(models.Model):
|
||||
"""Audit trail for loaner equipment actions."""
|
||||
_name = 'fusion.loaner.history'
|
||||
_description = 'Loaner History Log'
|
||||
_order = 'action_date desc, id desc'
|
||||
|
||||
# =========================================================================
|
||||
# REFERENCE FIELDS
|
||||
# =========================================================================
|
||||
|
||||
checkout_id = fields.Many2one(
|
||||
'fusion.loaner.checkout',
|
||||
string='Checkout Record',
|
||||
ondelete='cascade',
|
||||
required=True,
|
||||
)
|
||||
lot_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
string='Serial Number',
|
||||
help='The serial number this action relates to',
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string='Product',
|
||||
related='checkout_id.product_id',
|
||||
store=True,
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Client',
|
||||
related='checkout_id.partner_id',
|
||||
store=True,
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# ACTION DETAILS
|
||||
# =========================================================================
|
||||
|
||||
action = fields.Selection([
|
||||
('create', 'Created'),
|
||||
('checkout', 'Checked Out'),
|
||||
('return', 'Returned'),
|
||||
('condition_update', 'Condition Updated'),
|
||||
('reminder_sent', 'Reminder Sent'),
|
||||
('overdue', 'Marked Overdue'),
|
||||
('rental_pending', 'Rental Conversion Pending'),
|
||||
('rental_converted', 'Converted to Rental'),
|
||||
('lost', 'Marked as Lost'),
|
||||
('note', 'Note Added'),
|
||||
], string='Action', required=True)
|
||||
|
||||
action_date = fields.Datetime(
|
||||
string='Date/Time',
|
||||
default=fields.Datetime.now,
|
||||
required=True,
|
||||
)
|
||||
user_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='User',
|
||||
default=lambda self: self.env.user,
|
||||
required=True,
|
||||
)
|
||||
notes = fields.Text(
|
||||
string='Notes',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# DISPLAY
|
||||
# =========================================================================
|
||||
|
||||
def _get_action_label(self):
|
||||
"""Get human-readable action label."""
|
||||
action_labels = dict(self._fields['action'].selection)
|
||||
return action_labels.get(self.action, self.action)
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for record in self:
|
||||
name = f"{record.checkout_id.name} - {record._get_action_label()}"
|
||||
result.append((record.id, name))
|
||||
return result
|
||||
|
||||
# =========================================================================
|
||||
# SEARCH BY SERIAL
|
||||
# =========================================================================
|
||||
|
||||
@api.model
|
||||
def get_history_by_serial(self, lot_id):
|
||||
"""Get all history for a specific serial number."""
|
||||
return self.search([('lot_id', '=', lot_id)], order='action_date desc')
|
||||
|
||||
@api.model
|
||||
def get_history_by_product(self, product_id):
|
||||
"""Get all history for a specific product."""
|
||||
return self.search([('product_id', '=', product_id)], order='action_date desc')
|
||||
185
fusion_claims/fusion_claims/models/product_product.py
Normal file
185
fusion_claims/fusion_claims/models/product_product.py
Normal file
@@ -0,0 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = 'product.product'
|
||||
|
||||
def get_adp_device_code(self):
|
||||
"""
|
||||
Get ADP device code from the field mapped in fusion settings.
|
||||
|
||||
The field name is configured in Settings → Sales → Fusion Central →
|
||||
Field Mappings → Product ADP Code Field.
|
||||
|
||||
Checks the mapped field on the product variant first, then on template.
|
||||
Returns the value from the mapped field, or empty string if not found.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Get the mapped field name from fusion settings
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
field_name = ICP.get_param('fusion_claims.field_product_code', 'x_fc_adp_device_code')
|
||||
|
||||
if not field_name:
|
||||
return ''
|
||||
|
||||
# Check if the mapped field exists on the product variant (product.product)
|
||||
if field_name in self._fields:
|
||||
value = getattr(self, field_name, '') or ''
|
||||
if value:
|
||||
return value
|
||||
|
||||
# Check if the mapped field exists on the product template
|
||||
if self.product_tmpl_id and field_name in self.product_tmpl_id._fields:
|
||||
value = getattr(self.product_tmpl_id, field_name, '') or ''
|
||||
if value:
|
||||
return value
|
||||
|
||||
return ''
|
||||
|
||||
def get_adp_price(self):
|
||||
"""
|
||||
Get ADP price from the field mapped in fusion settings.
|
||||
|
||||
The field name is configured in Settings → Sales → Fusion Central →
|
||||
Field Mappings → Product ADP Price Field.
|
||||
|
||||
Checks the mapped field on the product variant first, then on template.
|
||||
Returns the value from the mapped field, or 0.0 if not found.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Get the mapped field name from fusion settings
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
field_name = ICP.get_param('fusion_claims.field_product_adp_price', 'x_fc_adp_price')
|
||||
|
||||
if not field_name:
|
||||
return 0.0
|
||||
|
||||
# Check if the mapped field exists on the product variant (product.product)
|
||||
if field_name in self._fields:
|
||||
value = getattr(self, field_name, 0.0) or 0.0
|
||||
if value:
|
||||
return value
|
||||
|
||||
# Check if the mapped field exists on the product template
|
||||
if self.product_tmpl_id and field_name in self.product_tmpl_id._fields:
|
||||
value = getattr(self.product_tmpl_id, field_name, 0.0) or 0.0
|
||||
if value:
|
||||
return value
|
||||
|
||||
return 0.0
|
||||
|
||||
def is_non_adp_funded(self):
|
||||
"""
|
||||
Check if this product has a NON-ADP, NON-FUNDED, or UNFUNDED device code.
|
||||
|
||||
Products with these device codes are not covered by ADP and should have:
|
||||
- ADP portion = 0
|
||||
- Client portion = full amount
|
||||
- NOT included in ADP invoices (only in client invoices)
|
||||
|
||||
Returns True if the product is NOT funded by ADP.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Get the ADP device code
|
||||
adp_code = self.get_adp_device_code()
|
||||
if not adp_code:
|
||||
return False
|
||||
|
||||
# Check for non-funded codes (case-insensitive)
|
||||
# These product codes indicate items NOT covered by ADP funding:
|
||||
# - NON-ADP, NON-FUNDED, UNFUNDED: Explicitly not ADP funded
|
||||
# - ACS: Accessibility items (client pays 100%)
|
||||
# - ODS: ODSP items (client pays 100%)
|
||||
# - OWP: Ontario Works items (client pays 100%)
|
||||
non_funded_codes = [
|
||||
'NON-ADP', 'NON ADP', 'NONADP',
|
||||
'NON-FUNDED', 'NON FUNDED', 'NONFUNDED',
|
||||
'UNFUNDED', 'NOT-FUNDED', 'NOT FUNDED', 'NOTFUNDED',
|
||||
'ACS', 'ODS', 'OWP'
|
||||
]
|
||||
adp_code_upper = adp_code.upper().strip()
|
||||
|
||||
for non_funded in non_funded_codes:
|
||||
if adp_code_upper == non_funded or adp_code_upper.startswith(non_funded):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def action_sync_adp_price_from_database(self):
|
||||
"""
|
||||
Update product's ADP price from the device codes database.
|
||||
|
||||
Looks up the product's ADP device code in the fusion.adp.device.code table
|
||||
and updates the product's x_fc_adp_price field with the database value.
|
||||
|
||||
Returns a notification with the result.
|
||||
"""
|
||||
ADPDevice = self.env['fusion.adp.device.code'].sudo()
|
||||
updated = []
|
||||
not_found = []
|
||||
no_code = []
|
||||
|
||||
for product in self:
|
||||
device_code = product.get_adp_device_code()
|
||||
if not device_code:
|
||||
no_code.append(product.name)
|
||||
continue
|
||||
|
||||
adp_device = ADPDevice.search([
|
||||
('device_code', '=', device_code),
|
||||
('active', '=', True)
|
||||
], limit=1)
|
||||
|
||||
if adp_device and adp_device.adp_price:
|
||||
# Update product template
|
||||
product_tmpl = product.product_tmpl_id
|
||||
old_price = 0
|
||||
|
||||
if hasattr(product_tmpl, 'x_fc_adp_price'):
|
||||
old_price = getattr(product_tmpl, 'x_fc_adp_price', 0) or 0
|
||||
product_tmpl.sudo().write({'x_fc_adp_price': adp_device.adp_price})
|
||||
updated.append({
|
||||
'name': product.name,
|
||||
'code': device_code,
|
||||
'old_price': old_price,
|
||||
'new_price': adp_device.adp_price,
|
||||
})
|
||||
else:
|
||||
not_found.append(f"{product.name} ({device_code})")
|
||||
|
||||
# Build result message
|
||||
message_parts = []
|
||||
if updated:
|
||||
msg = f"<strong>Updated {len(updated)} product(s):</strong><ul>"
|
||||
for u in updated:
|
||||
msg += f"<li>{u['name']}: ${u['old_price']:.2f} → ${u['new_price']:.2f}</li>"
|
||||
msg += "</ul>"
|
||||
message_parts.append(msg)
|
||||
|
||||
if not_found:
|
||||
message_parts.append(f"<strong>Not found in database:</strong> {', '.join(not_found)}")
|
||||
|
||||
if no_code:
|
||||
message_parts.append(f"<strong>No ADP code:</strong> {', '.join(no_code)}")
|
||||
|
||||
if not message_parts:
|
||||
message_parts.append("No products to process.")
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'ADP Price Sync',
|
||||
'message': '<br/>'.join(message_parts),
|
||||
'type': 'success' if updated else 'warning',
|
||||
'sticky': True,
|
||||
}
|
||||
}
|
||||
109
fusion_claims/fusion_claims/models/product_template.py
Normal file
109
fusion_claims/fusion_claims/models/product_template.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
# ==========================================================================
|
||||
# ADP PRODUCT FIELDS
|
||||
# These are the module's own fields - independent of Odoo Studio
|
||||
# ==========================================================================
|
||||
|
||||
x_fc_adp_device_code = fields.Char(
|
||||
string='ADP Device Code',
|
||||
help='Device code used for ADP claims export',
|
||||
copy=True,
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
x_fc_adp_price = fields.Float(
|
||||
string='ADP Price',
|
||||
digits='Product Price',
|
||||
help='ADP retail price for this product. Used in ADP reports and claims.',
|
||||
copy=True,
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
x_fc_is_adp_product = fields.Boolean(
|
||||
string='Is ADP Product',
|
||||
compute='_compute_is_adp_product',
|
||||
store=True,
|
||||
help='Indicates if this product has ADP pricing set up',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# LOANER PRODUCT FIELDS
|
||||
# ==========================================================================
|
||||
|
||||
x_fc_can_be_loaned = fields.Boolean(
|
||||
string='Can be Loaned',
|
||||
default=False,
|
||||
help='If checked, this product can be loaned out to clients',
|
||||
)
|
||||
x_fc_loaner_period_days = fields.Integer(
|
||||
string='Loaner Period (Days)',
|
||||
default=7,
|
||||
help='Default number of free loaner days before rental conversion',
|
||||
)
|
||||
x_fc_rental_price_weekly = fields.Float(
|
||||
string='Weekly Rental Price',
|
||||
digits='Product Price',
|
||||
help='Rental price per week if loaner converts to rental',
|
||||
)
|
||||
x_fc_rental_price_monthly = fields.Float(
|
||||
string='Monthly Rental Price',
|
||||
digits='Product Price',
|
||||
help='Rental price per month if loaner converts to rental',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# COMPUTED FIELDS
|
||||
# ==========================================================================
|
||||
|
||||
@api.depends('x_fc_adp_device_code', 'x_fc_adp_price')
|
||||
def _compute_is_adp_product(self):
|
||||
"""Determine if this is an ADP product based on having device code or price."""
|
||||
for product in self:
|
||||
product.x_fc_is_adp_product = bool(
|
||||
product.x_fc_adp_device_code or product.x_fc_adp_price
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# HELPER METHODS
|
||||
# ==========================================================================
|
||||
|
||||
def get_adp_price(self):
|
||||
"""
|
||||
Get ADP price with fallback to Studio field.
|
||||
|
||||
Checks in order:
|
||||
1. x_fc_adp_price (module field)
|
||||
2. list_price (default product price)
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if self.x_fc_adp_price:
|
||||
return self.x_fc_adp_price
|
||||
|
||||
return self.list_price or 0.0
|
||||
|
||||
def get_adp_device_code(self):
|
||||
"""
|
||||
Get ADP device code.
|
||||
|
||||
Checks in order:
|
||||
1. x_fc_adp_device_code (module field)
|
||||
2. default_code (internal reference)
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if self.x_fc_adp_device_code:
|
||||
return self.x_fc_adp_device_code
|
||||
|
||||
return self.default_code or ''
|
||||
|
||||
73
fusion_claims/fusion_claims/models/push_subscription.py
Normal file
73
fusion_claims/fusion_claims/models/push_subscription.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# -*- 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,
|
||||
})
|
||||
69
fusion_claims/fusion_claims/models/res_company.py
Normal file
69
fusion_claims/fusion_claims/models/res_company.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
# Store/Location Information
|
||||
x_fc_store_address_1 = fields.Char(
|
||||
string='Store Address Line 1',
|
||||
help='First store/location address for reports (e.g., "Main Store - 123 Street, City, Province, Postal")',
|
||||
)
|
||||
x_fc_store_address_2 = fields.Char(
|
||||
string='Store Address Line 2',
|
||||
help='Second store/location address for reports (optional)',
|
||||
)
|
||||
x_fc_company_tagline = fields.Char(
|
||||
string='Company Tagline',
|
||||
help='Company tagline/slogan for reports (e.g., "Enhancing Accessibility, Improving Lives.")',
|
||||
)
|
||||
|
||||
# Payment Information
|
||||
x_fc_etransfer_email = fields.Char(
|
||||
string='E-Transfer Email',
|
||||
help='Email address for Interac e-Transfers',
|
||||
)
|
||||
x_fc_cheque_payable_to = fields.Char(
|
||||
string='Cheque Payable To',
|
||||
help='Name for cheque payments (defaults to company name if empty)',
|
||||
)
|
||||
x_fc_payment_terms_html = fields.Html(
|
||||
string='Payment Terms',
|
||||
help='Payment terms and conditions displayed on reports (supports HTML formatting)',
|
||||
sanitize=True,
|
||||
sanitize_overridable=True,
|
||||
)
|
||||
|
||||
# Refund Policy
|
||||
x_fc_include_refund_page = fields.Boolean(
|
||||
string='Include Refund Policy Page',
|
||||
default=True,
|
||||
help='Include a separate refund policy page at the end of reports',
|
||||
)
|
||||
x_fc_refund_policy_html = fields.Html(
|
||||
string='Refund Policy',
|
||||
help='Full refund policy displayed on a separate page (supports HTML formatting)',
|
||||
sanitize=True,
|
||||
sanitize_overridable=True,
|
||||
)
|
||||
|
||||
# Office Notification Recipients
|
||||
x_fc_office_notification_ids = fields.Many2many(
|
||||
'res.partner',
|
||||
'fc_company_office_notification_partners_rel',
|
||||
'company_id',
|
||||
'partner_id',
|
||||
string='Office Notification Recipients',
|
||||
help='Contacts who will receive a copy (CC) of all automated ADP notifications',
|
||||
)
|
||||
|
||||
def _get_cheque_payable_name(self):
|
||||
"""Get the name for cheque payments, defaulting to company name."""
|
||||
self.ensure_one()
|
||||
return self.x_fc_cheque_payable_to or self.name
|
||||
|
||||
602
fusion_claims/fusion_claims/models/res_config_settings.py
Normal file
602
fusion_claims/fusion_claims/models/res_config_settings.py
Normal file
@@ -0,0 +1,602 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
# =========================================================================
|
||||
# COMPANY SETTINGS (Related to res.company)
|
||||
# =========================================================================
|
||||
|
||||
fc_store_address_1 = fields.Char(
|
||||
related='company_id.x_fc_store_address_1',
|
||||
readonly=False,
|
||||
string='Store Address Line 1',
|
||||
)
|
||||
fc_store_address_2 = fields.Char(
|
||||
related='company_id.x_fc_store_address_2',
|
||||
readonly=False,
|
||||
string='Store Address Line 2',
|
||||
)
|
||||
fc_company_tagline = fields.Char(
|
||||
related='company_id.x_fc_company_tagline',
|
||||
readonly=False,
|
||||
string='Company Tagline',
|
||||
)
|
||||
fc_etransfer_email = fields.Char(
|
||||
related='company_id.x_fc_etransfer_email',
|
||||
readonly=False,
|
||||
string='E-Transfer Email',
|
||||
)
|
||||
fc_cheque_payable_to = fields.Char(
|
||||
related='company_id.x_fc_cheque_payable_to',
|
||||
readonly=False,
|
||||
string='Cheque Payable To',
|
||||
)
|
||||
fc_payment_terms_html = fields.Html(
|
||||
related='company_id.x_fc_payment_terms_html',
|
||||
readonly=False,
|
||||
string='Payment Terms',
|
||||
)
|
||||
fc_include_refund_page = fields.Boolean(
|
||||
related='company_id.x_fc_include_refund_page',
|
||||
readonly=False,
|
||||
string='Include Refund Policy Page',
|
||||
)
|
||||
fc_refund_policy_html = fields.Html(
|
||||
related='company_id.x_fc_refund_policy_html',
|
||||
readonly=False,
|
||||
string='Refund Policy',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# ADP BILLING SETTINGS
|
||||
# =========================================================================
|
||||
|
||||
fc_vendor_code = fields.Char(
|
||||
string='ADP Vendor Code',
|
||||
config_parameter='fusion_claims.vendor_code',
|
||||
help='Your ADP vendor/location code for claim submissions',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# FIELD MAPPINGS
|
||||
# =========================================================================
|
||||
|
||||
fc_field_sale_type = fields.Char(
|
||||
string='Sale Type Field',
|
||||
config_parameter='fusion_claims.field_sale_type',
|
||||
help='Field name for sale type on sale.order',
|
||||
)
|
||||
fc_field_so_client_type = fields.Char(
|
||||
string='SO Client Type Field',
|
||||
config_parameter='fusion_claims.field_so_client_type',
|
||||
help='Field name for client type on sale.order',
|
||||
)
|
||||
fc_field_so_authorizer = fields.Char(
|
||||
string='SO Authorizer Field',
|
||||
config_parameter='fusion_claims.field_so_authorizer',
|
||||
help='Field name for authorizer on sale.order',
|
||||
)
|
||||
fc_field_invoice_type = fields.Char(
|
||||
string='Invoice Type Field',
|
||||
config_parameter='fusion_claims.field_invoice_type',
|
||||
help='Field name for invoice type on account.move',
|
||||
)
|
||||
fc_field_inv_client_type = fields.Char(
|
||||
string='Invoice Client Type Field',
|
||||
config_parameter='fusion_claims.field_inv_client_type',
|
||||
help='Field name for client type on account.move',
|
||||
)
|
||||
fc_field_inv_authorizer = fields.Char(
|
||||
string='Invoice Authorizer Field',
|
||||
config_parameter='fusion_claims.field_inv_authorizer',
|
||||
help='Field name for authorizer on account.move',
|
||||
)
|
||||
fc_field_product_code = fields.Char(
|
||||
string='Product ADP Code Field',
|
||||
config_parameter='fusion_claims.field_product_code',
|
||||
help='Field name for ADP device code on product.template',
|
||||
)
|
||||
fc_field_sol_serial = fields.Char(
|
||||
string='SO Line Serial Field',
|
||||
config_parameter='fusion_claims.field_sol_serial',
|
||||
help='Field name for serial number on sale.order.line',
|
||||
)
|
||||
fc_field_aml_serial = fields.Char(
|
||||
string='Invoice Line Serial Field',
|
||||
config_parameter='fusion_claims.field_aml_serial',
|
||||
help='Field name for serial number on account.move.line',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# ADDITIONAL SALE ORDER FIELD MAPPINGS
|
||||
# =========================================================================
|
||||
|
||||
fc_field_so_claim_number = fields.Char(
|
||||
string='SO Claim Number Field',
|
||||
config_parameter='fusion_claims.field_so_claim_number',
|
||||
help='Field name for claim number on sale.order',
|
||||
)
|
||||
fc_field_so_client_ref_1 = fields.Char(
|
||||
string='SO Client Ref 1 Field',
|
||||
config_parameter='fusion_claims.field_so_client_ref_1',
|
||||
help='Field name for client reference 1 on sale.order',
|
||||
)
|
||||
fc_field_so_client_ref_2 = fields.Char(
|
||||
string='SO Client Ref 2 Field',
|
||||
config_parameter='fusion_claims.field_so_client_ref_2',
|
||||
help='Field name for client reference 2 on sale.order',
|
||||
)
|
||||
fc_field_so_delivery_date = fields.Char(
|
||||
string='SO Delivery Date Field',
|
||||
config_parameter='fusion_claims.field_so_delivery_date',
|
||||
help='Field name for ADP delivery date on sale.order',
|
||||
)
|
||||
fc_field_so_adp_status = fields.Char(
|
||||
string='SO ADP Status Field',
|
||||
config_parameter='fusion_claims.field_so_adp_status',
|
||||
help='Field name for ADP status on sale.order',
|
||||
)
|
||||
fc_field_so_service_start = fields.Char(
|
||||
string='SO Service Start Date Field',
|
||||
config_parameter='fusion_claims.field_so_service_start',
|
||||
help='Field name for service start date on sale.order',
|
||||
)
|
||||
fc_field_so_service_end = fields.Char(
|
||||
string='SO Service End Date Field',
|
||||
config_parameter='fusion_claims.field_so_service_end',
|
||||
help='Field name for service end date on sale.order',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# ADDITIONAL INVOICE FIELD MAPPINGS
|
||||
# =========================================================================
|
||||
|
||||
fc_field_inv_claim_number = fields.Char(
|
||||
string='Invoice Claim Number Field',
|
||||
config_parameter='fusion_claims.field_inv_claim_number',
|
||||
help='Field name for claim number on account.move',
|
||||
)
|
||||
fc_field_inv_client_ref_1 = fields.Char(
|
||||
string='Invoice Client Ref 1 Field',
|
||||
config_parameter='fusion_claims.field_inv_client_ref_1',
|
||||
help='Field name for client reference 1 on account.move',
|
||||
)
|
||||
fc_field_inv_client_ref_2 = fields.Char(
|
||||
string='Invoice Client Ref 2 Field',
|
||||
config_parameter='fusion_claims.field_inv_client_ref_2',
|
||||
help='Field name for client reference 2 on account.move',
|
||||
)
|
||||
fc_field_inv_delivery_date = fields.Char(
|
||||
string='Invoice Delivery Date Field',
|
||||
config_parameter='fusion_claims.field_inv_delivery_date',
|
||||
help='Field name for ADP delivery date on account.move',
|
||||
)
|
||||
fc_field_inv_service_start = fields.Char(
|
||||
string='Invoice Service Start Date Field',
|
||||
config_parameter='fusion_claims.field_inv_service_start',
|
||||
help='Field name for service start date on account.move',
|
||||
)
|
||||
fc_field_inv_service_end = fields.Char(
|
||||
string='Invoice Service End Date Field',
|
||||
config_parameter='fusion_claims.field_inv_service_end',
|
||||
help='Field name for service end date on account.move',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# SALE ORDER LINE FIELD MAPPINGS
|
||||
# =========================================================================
|
||||
|
||||
fc_field_sol_placement = fields.Char(
|
||||
string='SO Line Placement Field',
|
||||
config_parameter='fusion_claims.field_sol_placement',
|
||||
help='Field name for device placement on sale.order.line',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# INVOICE LINE FIELD MAPPINGS
|
||||
# =========================================================================
|
||||
|
||||
fc_field_aml_placement = fields.Char(
|
||||
string='Invoice Line Placement Field',
|
||||
config_parameter='fusion_claims.field_aml_placement',
|
||||
help='Field name for device placement on account.move.line',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# PRODUCT FIELD MAPPINGS
|
||||
# =========================================================================
|
||||
|
||||
fc_field_product_adp_price = fields.Char(
|
||||
string='Product ADP Price Field',
|
||||
config_parameter='fusion_claims.field_product_adp_price',
|
||||
help='Field name for ADP price on product.template',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# HEADER-LEVEL SERIAL NUMBER MAPPINGS
|
||||
# =========================================================================
|
||||
|
||||
fc_field_so_primary_serial = fields.Char(
|
||||
string='SO Primary Serial Field',
|
||||
config_parameter='fusion_claims.field_so_primary_serial',
|
||||
help='Field name for primary serial number on sale.order (header level)',
|
||||
)
|
||||
fc_field_inv_primary_serial = fields.Char(
|
||||
string='Invoice Primary Serial Field',
|
||||
config_parameter='fusion_claims.field_inv_primary_serial',
|
||||
help='Field name for primary serial number on account.move (header level)',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# ADP POSTING SCHEDULE SETTINGS
|
||||
# =========================================================================
|
||||
|
||||
fc_adp_posting_base_date = fields.Char(
|
||||
string='ADP Posting Base Date',
|
||||
config_parameter='fusion_claims.adp_posting_base_date',
|
||||
help='Reference date for calculating bi-weekly posting schedule (a known posting day). Format: YYYY-MM-DD',
|
||||
)
|
||||
fc_adp_posting_frequency_days = fields.Integer(
|
||||
string='Posting Frequency (Days)',
|
||||
config_parameter='fusion_claims.adp_posting_frequency_days',
|
||||
help='Number of days between ADP posting cycles (typically 14 days)',
|
||||
)
|
||||
fc_adp_billing_reminder_user_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Billing Deadline Reminder Person',
|
||||
# NOTE: stored manually via get_values/set_values (not config_parameter)
|
||||
# because Many2one + config_parameter causes double-write conflicts
|
||||
help='Person to remind on Monday to complete ADP billing by Wednesday 6 PM',
|
||||
)
|
||||
fc_adp_correction_reminder_user_ids = fields.Many2many(
|
||||
'res.users',
|
||||
'fc_config_correction_reminder_users_rel',
|
||||
'config_id',
|
||||
'user_id',
|
||||
string='Correction Alert Recipients',
|
||||
help='People to notify when an ADP invoice needs correction/resubmission',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# EMAIL NOTIFICATION SETTINGS
|
||||
# =========================================================================
|
||||
|
||||
fc_enable_email_notifications = fields.Boolean(
|
||||
string='Enable Automated Email Notifications',
|
||||
config_parameter='fusion_claims.enable_email_notifications',
|
||||
help='Enable/disable automated email notifications for ADP workflow events',
|
||||
)
|
||||
fc_office_notification_ids = fields.Many2many(
|
||||
related='company_id.x_fc_office_notification_ids',
|
||||
readonly=False,
|
||||
string='Office Notification Recipients',
|
||||
)
|
||||
fc_application_reminder_days = fields.Integer(
|
||||
string='First Reminder Days',
|
||||
config_parameter='fusion_claims.application_reminder_days',
|
||||
help='Number of days after assessment completion to send first application reminder to therapist',
|
||||
)
|
||||
fc_application_reminder_2_days = fields.Integer(
|
||||
string='Second Reminder Days (After First)',
|
||||
config_parameter='fusion_claims.application_reminder_2_days',
|
||||
help='Number of days after first reminder to send second application reminder to therapist',
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# WORKFLOW LOCK SETTINGS
|
||||
# =========================================================================
|
||||
|
||||
fc_allow_sale_type_override = fields.Boolean(
|
||||
string='Allow Sale Type Override',
|
||||
config_parameter='fusion_claims.allow_sale_type_override',
|
||||
help='If enabled, allows changing Sale Type even after application is submitted (for cases where additional benefits are discovered)',
|
||||
)
|
||||
|
||||
fc_allow_document_lock_override = fields.Boolean(
|
||||
string='Allow Document Lock Override',
|
||||
config_parameter='fusion_claims.allow_document_lock_override',
|
||||
help='When enabled, users in the "Document Lock Override" group can edit locked documents on old cases. '
|
||||
'Disable this once all legacy cases have been processed to enforce strict workflow.',
|
||||
)
|
||||
|
||||
fc_designated_vendor_signer = fields.Many2one(
|
||||
'res.users',
|
||||
string='Designated Vendor Signer',
|
||||
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
|
||||
# ------------------------------------------------------------------
|
||||
fc_ai_api_key = fields.Char(
|
||||
string='AI API Key',
|
||||
config_parameter='fusion_claims.ai_api_key',
|
||||
help='OpenAI API key for Client Intelligence chat',
|
||||
)
|
||||
fc_ai_model = fields.Selection([
|
||||
('gpt-4o-mini', 'GPT-4o Mini (Fast, Lower Cost)'),
|
||||
('gpt-4o', 'GPT-4o (Best Quality)'),
|
||||
('gpt-4.1-mini', 'GPT-4.1 Mini'),
|
||||
('gpt-4.1', 'GPT-4.1'),
|
||||
], string='AI Model',
|
||||
config_parameter='fusion_claims.ai_model',
|
||||
)
|
||||
fc_auto_parse_xml = fields.Boolean(
|
||||
string='Auto-Parse XML Files',
|
||||
config_parameter='fusion_claims.auto_parse_xml',
|
||||
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
|
||||
# ------------------------------------------------------------------
|
||||
fc_twilio_enabled = fields.Boolean(
|
||||
string='Enable Twilio SMS',
|
||||
config_parameter='fusion_claims.twilio_enabled',
|
||||
help='Enable SMS notifications via Twilio for assessment bookings and key status updates',
|
||||
)
|
||||
fc_twilio_account_sid = fields.Char(
|
||||
string='Twilio Account SID',
|
||||
config_parameter='fusion_claims.twilio_account_sid',
|
||||
groups='fusion_claims.group_fusion_claims_manager',
|
||||
)
|
||||
fc_twilio_auth_token = fields.Char(
|
||||
string='Twilio Auth Token',
|
||||
config_parameter='fusion_claims.twilio_auth_token',
|
||||
groups='fusion_claims.group_fusion_claims_manager',
|
||||
)
|
||||
fc_twilio_phone_number = fields.Char(
|
||||
string='Twilio Phone Number',
|
||||
config_parameter='fusion_claims.twilio_phone_number',
|
||||
help='Your Twilio phone number for sending SMS (e.g. +1234567890)',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# MARCH OF DIMES SETTINGS
|
||||
# ------------------------------------------------------------------
|
||||
fc_mod_default_email = fields.Char(
|
||||
string='MOD Default Email',
|
||||
config_parameter='fusion_claims.mod_default_email',
|
||||
help='Default email for sending quotations and documents to March of Dimes (e.g. hvmp@marchofdimes.ca)',
|
||||
)
|
||||
fc_mod_vendor_code = fields.Char(
|
||||
string='March of Dimes Vendor Code',
|
||||
config_parameter='fusion_claims.mod_vendor_code',
|
||||
help='Your vendor code assigned by March of Dimes (e.g. TRD0001234)',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# MOD FOLLOW-UP SETTINGS
|
||||
# ------------------------------------------------------------------
|
||||
fc_mod_followup_interval_days = fields.Integer(
|
||||
string='Follow-up Interval (Days)',
|
||||
config_parameter='fusion_claims.mod_followup_interval_days',
|
||||
help='Number of days between follow-up reminders for MOD cases awaiting funding (default: 14)',
|
||||
)
|
||||
fc_mod_followup_escalation_days = fields.Integer(
|
||||
string='Escalation Delay (Days)',
|
||||
config_parameter='fusion_claims.mod_followup_escalation_days',
|
||||
help='Days after a follow-up activity is due before auto-sending email to client (default: 3)',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# ODSP CONFIGURATION
|
||||
# ------------------------------------------------------------------
|
||||
fc_sa_mobility_email = fields.Char(
|
||||
string='SA Mobility Email',
|
||||
config_parameter='fusion_claims.sa_mobility_email',
|
||||
help='Email address for SA Mobility submissions (default: samobility@ontario.ca)',
|
||||
)
|
||||
fc_sa_mobility_phone = fields.Char(
|
||||
string='SA Mobility Phone',
|
||||
config_parameter='fusion_claims.sa_mobility_phone',
|
||||
help='SA Mobility phone number (default: 1-888-222-5099)',
|
||||
)
|
||||
fc_odsp_default_office_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Default ODSP Office',
|
||||
domain="[('x_fc_contact_type', '=', 'odsp_office')]",
|
||||
help='Default ODSP office contact for new ODSP cases',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super().get_values()
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
|
||||
# Get billing reminder user
|
||||
billing_user_id = ICP.get_param('fusion_claims.adp_billing_reminder_user_id', False)
|
||||
if billing_user_id:
|
||||
try:
|
||||
res['fc_adp_billing_reminder_user_id'] = int(billing_user_id)
|
||||
except (ValueError, TypeError):
|
||||
res['fc_adp_billing_reminder_user_id'] = False
|
||||
|
||||
# Get correction reminder users (stored as comma-separated IDs)
|
||||
correction_user_ids = ICP.get_param('fusion_claims.adp_correction_reminder_user_ids', '')
|
||||
if correction_user_ids:
|
||||
try:
|
||||
user_ids = [int(x.strip()) for x in correction_user_ids.split(',') if x.strip()]
|
||||
res['fc_adp_correction_reminder_user_ids'] = [(6, 0, user_ids)]
|
||||
except (ValueError, TypeError):
|
||||
res['fc_adp_correction_reminder_user_ids'] = [(6, 0, [])]
|
||||
|
||||
# Get designated vendor signer
|
||||
vendor_signer_id = ICP.get_param('fusion_claims.designated_vendor_signer', False)
|
||||
if vendor_signer_id:
|
||||
try:
|
||||
res['fc_designated_vendor_signer'] = int(vendor_signer_id)
|
||||
except (ValueError, TypeError):
|
||||
res['fc_designated_vendor_signer'] = False
|
||||
|
||||
# Get default ODSP office
|
||||
odsp_office_id = ICP.get_param('fusion_claims.odsp_default_office_id', False)
|
||||
if odsp_office_id:
|
||||
try:
|
||||
res['fc_odsp_default_office_id'] = int(odsp_office_id)
|
||||
except (ValueError, TypeError):
|
||||
res['fc_odsp_default_office_id'] = False
|
||||
|
||||
return res
|
||||
|
||||
def set_values(self):
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
|
||||
# --- Protect sensitive config_parameter fields from accidental blanking ---
|
||||
# These are keys where a blank/default value should NOT overwrite
|
||||
# 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 = {}
|
||||
for key in _protected_keys:
|
||||
val = ICP.get_param(key, '')
|
||||
if val:
|
||||
_existing[key] = val
|
||||
|
||||
super().set_values()
|
||||
|
||||
# Restore any protected values that were blanked by the save
|
||||
for key, old_val in _existing.items():
|
||||
new_val = ICP.get_param(key, '')
|
||||
if not new_val and old_val:
|
||||
ICP.set_param(key, old_val)
|
||||
_logger.warning(
|
||||
"Settings protection: restored %s (was blanked during save)", key
|
||||
)
|
||||
|
||||
# Store billing reminder user (Many2one - manual handling)
|
||||
if self.fc_adp_billing_reminder_user_id:
|
||||
ICP.set_param('fusion_claims.adp_billing_reminder_user_id',
|
||||
str(self.fc_adp_billing_reminder_user_id.id))
|
||||
# Only clear if explicitly set to empty AND there was no existing value
|
||||
elif not ICP.get_param('fusion_claims.adp_billing_reminder_user_id', ''):
|
||||
ICP.set_param('fusion_claims.adp_billing_reminder_user_id', '')
|
||||
|
||||
# Store correction reminder users as comma-separated IDs
|
||||
if self.fc_adp_correction_reminder_user_ids:
|
||||
user_ids = ','.join(str(u.id) for u in self.fc_adp_correction_reminder_user_ids)
|
||||
ICP.set_param('fusion_claims.adp_correction_reminder_user_ids', user_ids)
|
||||
# Only clear if explicitly empty AND no existing value
|
||||
elif not ICP.get_param('fusion_claims.adp_correction_reminder_user_ids', ''):
|
||||
ICP.set_param('fusion_claims.adp_correction_reminder_user_ids', '')
|
||||
|
||||
# Office notification recipients are stored via related field on res.company
|
||||
# No need to store in ir.config_parameter
|
||||
|
||||
# Store designated vendor signer (Many2one - manual handling)
|
||||
if self.fc_designated_vendor_signer:
|
||||
ICP.set_param('fusion_claims.designated_vendor_signer',
|
||||
str(self.fc_designated_vendor_signer.id))
|
||||
elif not ICP.get_param('fusion_claims.designated_vendor_signer', ''):
|
||||
ICP.set_param('fusion_claims.designated_vendor_signer', '')
|
||||
|
||||
# Store default ODSP office (Many2one - manual handling)
|
||||
if self.fc_odsp_default_office_id:
|
||||
ICP.set_param('fusion_claims.odsp_default_office_id',
|
||||
str(self.fc_odsp_default_office_id.id))
|
||||
elif not ICP.get_param('fusion_claims.odsp_default_office_id', ''):
|
||||
ICP.set_param('fusion_claims.odsp_default_office_id', '')
|
||||
|
||||
# =========================================================================
|
||||
# ACTION METHODS
|
||||
# =========================================================================
|
||||
|
||||
def action_open_field_mapping_wizard(self):
|
||||
"""Open the field mapping configuration wizard."""
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Field Mapping Configuration',
|
||||
'res_model': 'fusion_claims.field_mapping_config',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {},
|
||||
}
|
||||
|
||||
82
fusion_claims/fusion_claims/models/res_partner.py
Normal file
82
fusion_claims/fusion_claims/models/res_partner.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
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.',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# CONTACT TYPE
|
||||
# ==========================================================================
|
||||
x_fc_contact_type = fields.Selection(
|
||||
selection=[
|
||||
('adp_customer', 'ADP Customer'),
|
||||
('adp_odsp_customer', 'ADP-ODSP Customer'),
|
||||
('odsp_customer', 'ODSP Customer'),
|
||||
('mod_customer', 'MOD Customer'),
|
||||
('private_customer', 'Private Customer'),
|
||||
('wsib_customer', 'WSIB Customer'),
|
||||
('acsd_customer', 'ACSD Customer'),
|
||||
('private_insurance', 'Private Insurance'),
|
||||
('adp_agent', 'ADP Agent'),
|
||||
('odsp_agent', 'ODSP Agent'),
|
||||
('muscular_dystrophy', 'Muscular Dystrophy'),
|
||||
('occupational_therapist', 'Occupational Therapist'),
|
||||
('physiotherapist', 'Physiotherapist'),
|
||||
('vendor', 'Vendor'),
|
||||
('funding_agency', 'Funding Agency'),
|
||||
('government_agency', 'Government Agency'),
|
||||
('company_contact', 'Company Contact'),
|
||||
('long_term_care_home', 'Long Term Care Home'),
|
||||
('retirement_home', 'Retirement Home'),
|
||||
('odsp_office', 'ODSP Office'),
|
||||
('other', 'Other'),
|
||||
],
|
||||
string='Contact Type',
|
||||
tracking=True,
|
||||
index=True,
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# ODSP FIELDS
|
||||
# ==========================================================================
|
||||
x_fc_odsp_member_id = fields.Char(
|
||||
string='ODSP Member ID',
|
||||
size=9,
|
||||
tracking=True,
|
||||
help='9-digit Ontario Disability Support Program Member ID',
|
||||
)
|
||||
x_fc_case_worker_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='ODSP Case Worker',
|
||||
tracking=True,
|
||||
help='ODSP Case Worker assigned to this client',
|
||||
)
|
||||
x_fc_date_of_birth = fields.Date(
|
||||
string='Date of Birth',
|
||||
tracking=True,
|
||||
)
|
||||
x_fc_healthcard_number = fields.Char(
|
||||
string='Healthcard Number',
|
||||
tracking=True,
|
||||
)
|
||||
x_fc_is_odsp_office = fields.Boolean(
|
||||
compute='_compute_is_odsp_office',
|
||||
string='Is ODSP Office',
|
||||
store=True,
|
||||
)
|
||||
|
||||
@api.depends('x_fc_contact_type')
|
||||
def _compute_is_odsp_office(self):
|
||||
for partner in self:
|
||||
partner.x_fc_is_odsp_office = partner.x_fc_contact_type == 'odsp_office'
|
||||
26
fusion_claims/fusion_claims/models/res_users.py
Normal file
26
fusion_claims/fusion_claims/models/res_users.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- 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,
|
||||
)
|
||||
7817
fusion_claims/fusion_claims/models/sale_order.py
Normal file
7817
fusion_claims/fusion_claims/models/sale_order.py
Normal file
File diff suppressed because it is too large
Load Diff
362
fusion_claims/fusion_claims/models/sale_order_line.py
Normal file
362
fusion_claims/fusion_claims/models/sale_order_line.py
Normal file
@@ -0,0 +1,362 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
|
||||
# ==========================================================================
|
||||
# PARENT FIELD FOR VISIBILITY (used by Studio conditions)
|
||||
# ==========================================================================
|
||||
x_fc_is_adp_sale = fields.Boolean(
|
||||
compute='_compute_is_adp_sale_line',
|
||||
string='Is ADP Sale',
|
||||
store=False,
|
||||
help='True if parent order is an ADP sale - used for column visibility',
|
||||
)
|
||||
|
||||
def _compute_is_adp_sale_line(self):
|
||||
"""Check if parent order is an ADP sale."""
|
||||
for line in self:
|
||||
is_adp = False
|
||||
if line.order_id and line.order_id.x_fc_sale_type:
|
||||
is_adp = 'adp' in str(line.order_id.x_fc_sale_type).lower()
|
||||
line.x_fc_is_adp_sale = is_adp
|
||||
|
||||
@api.depends('product_id', 'product_id.default_code')
|
||||
def _compute_adp_device_type(self):
|
||||
"""Compute ADP device type from the product's device code."""
|
||||
ADPDevice = self.env['fusion.adp.device.code'].sudo()
|
||||
for line in self:
|
||||
device_type = ''
|
||||
if line.product_id:
|
||||
# Get the device code from product (default_code or custom field)
|
||||
device_code = line._get_adp_device_code()
|
||||
if device_code:
|
||||
# Look up device type in ADP database
|
||||
adp_device = ADPDevice.search([
|
||||
('device_code', '=', device_code),
|
||||
('active', '=', True)
|
||||
], limit=1)
|
||||
if adp_device:
|
||||
device_type = adp_device.device_type or ''
|
||||
line.x_fc_adp_device_type = device_type
|
||||
|
||||
# ==========================================================================
|
||||
# SERIAL NUMBER AND DEVICE PLACEMENT
|
||||
# ==========================================================================
|
||||
x_fc_serial_number = fields.Char(
|
||||
string='Serial Number',
|
||||
help='Serial number for this product',
|
||||
)
|
||||
x_fc_device_placement = fields.Selection(
|
||||
selection=[
|
||||
('L', 'Left'),
|
||||
('R', 'Right'),
|
||||
('NA', 'N/A'),
|
||||
],
|
||||
string='Device Placement',
|
||||
default='NA',
|
||||
help='Device placement position (Left/Right/N/A)',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# DEDUCTION FIELDS
|
||||
# ==========================================================================
|
||||
x_fc_deduction_type = fields.Selection(
|
||||
selection=[
|
||||
('none', 'No Deduction'),
|
||||
('pct', 'Percentage'),
|
||||
('amt', 'Amount'),
|
||||
],
|
||||
string='Deduction Type',
|
||||
default='none',
|
||||
help='Type of ADP deduction applied to this line',
|
||||
)
|
||||
x_fc_deduction_value = fields.Float(
|
||||
string='Deduction Value',
|
||||
digits='Product Price',
|
||||
help='Deduction value (percentage if PCT, dollar amount if AMT)',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# ADP REFERENCE FIELDS
|
||||
# ==========================================================================
|
||||
x_fc_adp_max_price = fields.Float(
|
||||
string='ADP Max Price',
|
||||
digits='Product Price',
|
||||
help='Maximum price ADP will cover for this device (from mobility manual)',
|
||||
)
|
||||
x_fc_sn_required = fields.Boolean(
|
||||
string='S/N Required',
|
||||
help='Is serial number required for this device?',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# ADP DEVICE APPROVAL TRACKING
|
||||
# ==========================================================================
|
||||
x_fc_adp_approved = fields.Boolean(
|
||||
string='ADP Approved',
|
||||
default=False,
|
||||
help='Was this device approved by ADP in the application approval?',
|
||||
)
|
||||
x_fc_adp_device_type = fields.Char(
|
||||
string='ADP Device Type',
|
||||
compute='_compute_adp_device_type',
|
||||
store=True,
|
||||
help='Device type from ADP mobility manual (for approval matching)',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# COMPUTED ADP PORTIONS
|
||||
# ==========================================================================
|
||||
x_fc_adp_portion = fields.Monetary(
|
||||
string='ADP Portion',
|
||||
compute='_compute_adp_portions',
|
||||
store=True,
|
||||
currency_field='currency_id',
|
||||
help='ADP portion for this line',
|
||||
)
|
||||
x_fc_client_portion = fields.Monetary(
|
||||
string='Client Portion',
|
||||
compute='_compute_adp_portions',
|
||||
store=True,
|
||||
currency_field='currency_id',
|
||||
help='Client portion for this line',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# COMPUTE ADP PORTIONS WITH DEDUCTIONS AND APPROVAL STATUS
|
||||
# ==========================================================================
|
||||
@api.depends('price_subtotal', 'product_uom_qty', 'price_unit', 'product_id',
|
||||
'order_id.x_fc_sale_type', 'order_id.x_fc_client_type',
|
||||
'order_id.x_fc_device_verification_complete',
|
||||
'x_fc_deduction_type', 'x_fc_deduction_value', 'x_fc_adp_max_price',
|
||||
'x_fc_adp_approved')
|
||||
def _compute_adp_portions(self):
|
||||
"""Compute ADP and client portions based on product's ADP price, client type, and approval status.
|
||||
|
||||
IMPORTANT:
|
||||
1. If a product has NON-ADP code (NON-ADP, NON-FUNDED, etc.): Client pays 100%
|
||||
2. If a product is NOT in the ADP device database: Client pays 100%
|
||||
3. If a device is NOT approved by ADP: Client pays 100%
|
||||
4. Only products with valid ADP codes that are approved get the 75%/25% (or 100%/0%) split
|
||||
"""
|
||||
ADPDevice = self.env['fusion.adp.device.code'].sudo()
|
||||
|
||||
for line in self:
|
||||
# Get sale type and client type from parent order
|
||||
order = line.order_id
|
||||
if not order:
|
||||
line.x_fc_adp_portion = 0
|
||||
line.x_fc_client_portion = 0
|
||||
continue
|
||||
|
||||
# Check if this is an ADP sale
|
||||
if not order._is_adp_sale():
|
||||
line.x_fc_adp_portion = 0
|
||||
line.x_fc_client_portion = 0
|
||||
continue
|
||||
|
||||
# Skip non-product lines
|
||||
if not line.product_id or line.product_uom_qty <= 0:
|
||||
line.x_fc_adp_portion = 0
|
||||
line.x_fc_client_portion = 0
|
||||
continue
|
||||
|
||||
# =================================================================
|
||||
# CHECK 1: Is this a NON-ADP funded product?
|
||||
# Products with NON-ADP, NON-FUNDED, UNFUNDED codes = 100% client
|
||||
# =================================================================
|
||||
if line.product_id.is_non_adp_funded():
|
||||
line.x_fc_adp_portion = 0
|
||||
line.x_fc_client_portion = line.price_subtotal
|
||||
continue
|
||||
|
||||
# =================================================================
|
||||
# CHECK 2: Does this product have a valid ADP device code?
|
||||
# Products without valid ADP codes in the database = 100% client
|
||||
# =================================================================
|
||||
device_code = line._get_adp_device_code()
|
||||
is_adp_device = False
|
||||
if device_code:
|
||||
# Check if this code exists in the ADP mobility manual database
|
||||
is_adp_device = ADPDevice.search_count([
|
||||
('device_code', '=', device_code),
|
||||
('active', '=', True)
|
||||
]) > 0
|
||||
|
||||
# If product has NO valid ADP code in database: client pays 100%
|
||||
if not is_adp_device:
|
||||
line.x_fc_adp_portion = 0
|
||||
line.x_fc_client_portion = line.price_subtotal
|
||||
continue
|
||||
|
||||
# =================================================================
|
||||
# CHECK 3: If this is an ADP device but NOT approved: 100% client
|
||||
# =================================================================
|
||||
if order.x_fc_device_verification_complete and not line.x_fc_adp_approved:
|
||||
line.x_fc_adp_portion = 0
|
||||
line.x_fc_client_portion = line.price_subtotal
|
||||
continue
|
||||
|
||||
# =================================================================
|
||||
# STANDARD CALCULATION: Product is a valid, approved ADP device
|
||||
# =================================================================
|
||||
|
||||
# Get client type and determine base percentages
|
||||
client_type = order._get_client_type()
|
||||
if client_type == 'REG':
|
||||
# REG: 75% ADP, 25% Client
|
||||
base_adp_pct = 0.75
|
||||
base_client_pct = 0.25
|
||||
else:
|
||||
# ODS, OWP, ACS, LTC, SEN, CCA: 100% ADP, 0% Client
|
||||
base_adp_pct = 1.0
|
||||
base_client_pct = 0.0
|
||||
|
||||
# Get the ADP price from the product
|
||||
adp_price = 0
|
||||
if line.product_id and line.product_id.product_tmpl_id:
|
||||
product_tmpl = line.product_id.product_tmpl_id
|
||||
if hasattr(product_tmpl, 'x_fc_adp_price'):
|
||||
adp_price = product_tmpl.x_fc_adp_price or 0
|
||||
if not adp_price and line.x_fc_adp_max_price:
|
||||
adp_price = line.x_fc_adp_max_price
|
||||
if not adp_price:
|
||||
adp_price = line.price_unit
|
||||
|
||||
qty = line.product_uom_qty
|
||||
adp_base_total = adp_price * qty
|
||||
|
||||
# Apply deductions
|
||||
if line.x_fc_deduction_type == 'pct' and line.x_fc_deduction_value:
|
||||
# PCT: ADP only covers deduction_value% of their portion
|
||||
effective_adp_pct = base_adp_pct * (line.x_fc_deduction_value / 100)
|
||||
effective_client_pct = 1 - effective_adp_pct
|
||||
adp_portion = adp_base_total * effective_adp_pct
|
||||
client_portion = adp_base_total * effective_client_pct
|
||||
elif line.x_fc_deduction_type == 'amt' and line.x_fc_deduction_value:
|
||||
# AMT: Subtract fixed amount from ADP portion
|
||||
base_adp_amount = adp_base_total * base_adp_pct
|
||||
adp_portion = max(0, base_adp_amount - line.x_fc_deduction_value)
|
||||
client_portion = adp_base_total - adp_portion
|
||||
else:
|
||||
# No deduction - standard calculation based on ADP price
|
||||
adp_portion = adp_base_total * base_adp_pct
|
||||
client_portion = adp_base_total * base_client_pct
|
||||
|
||||
line.x_fc_adp_portion = adp_portion
|
||||
line.x_fc_client_portion = client_portion
|
||||
|
||||
# ==========================================================================
|
||||
# GETTER METHODS
|
||||
# ==========================================================================
|
||||
def _get_adp_device_code(self):
|
||||
"""Get ADP device code from product.
|
||||
|
||||
Checks multiple sources in order:
|
||||
1. x_fc_adp_device_code (module field)
|
||||
2. x_adp_code (Studio/custom field)
|
||||
3. default_code
|
||||
4. Code in parentheses in product name (e.g., "Product Name (SE0001109)")
|
||||
"""
|
||||
import re
|
||||
|
||||
self.ensure_one()
|
||||
if not self.product_id:
|
||||
return ''
|
||||
|
||||
product_tmpl = self.product_id.product_tmpl_id
|
||||
ADPDevice = self.env['fusion.adp.device.code'].sudo()
|
||||
|
||||
# 1. Check x_fc_adp_device_code (module field)
|
||||
code = ''
|
||||
if hasattr(product_tmpl, 'x_fc_adp_device_code'):
|
||||
code = getattr(product_tmpl, 'x_fc_adp_device_code', '') or ''
|
||||
|
||||
# Verify code exists in ADP database
|
||||
if code and ADPDevice.search_count([('device_code', '=', code), ('active', '=', True)]) > 0:
|
||||
return code
|
||||
|
||||
# 2. Check x_adp_code (Studio/custom field)
|
||||
if hasattr(product_tmpl, 'x_adp_code'):
|
||||
code = getattr(product_tmpl, 'x_adp_code', '') or ''
|
||||
if code and ADPDevice.search_count([('device_code', '=', code), ('active', '=', True)]) > 0:
|
||||
return code
|
||||
|
||||
# 3. Check default_code
|
||||
code = self.product_id.default_code or ''
|
||||
if code and ADPDevice.search_count([('device_code', '=', code), ('active', '=', True)]) > 0:
|
||||
return code
|
||||
|
||||
# 4. Try to extract code from product name in parentheses
|
||||
# E.g., "[MXA-1618] GEOMATRIX SILVERBACK MAX BACKREST - ACTIVE (SE0001109)"
|
||||
product_name = self.product_id.name or ''
|
||||
# Find all codes in parentheses
|
||||
matches = re.findall(r'\(([A-Z0-9]+)\)', product_name)
|
||||
for potential_code in matches:
|
||||
if ADPDevice.search_count([('device_code', '=', potential_code), ('active', '=', True)]) > 0:
|
||||
return potential_code
|
||||
|
||||
# 5. Final fallback - return default_code even if not in ADP database
|
||||
return self.product_id.default_code or ''
|
||||
|
||||
def _get_serial_number(self):
|
||||
"""Get serial number from mapped field or native field."""
|
||||
self.ensure_one()
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
field_name = ICP.get_param('fusion_claims.field_sol_serial', 'x_fc_serial_number')
|
||||
|
||||
# Try mapped field first
|
||||
if hasattr(self, field_name):
|
||||
value = getattr(self, field_name, None)
|
||||
if value:
|
||||
return value
|
||||
|
||||
# Fallback to native field
|
||||
return self.x_fc_serial_number or ''
|
||||
|
||||
def _get_device_placement(self):
|
||||
"""Get device placement."""
|
||||
self.ensure_one()
|
||||
return self.x_fc_device_placement or 'NA'
|
||||
|
||||
# ==========================================================================
|
||||
# INVOICE LINE PREPARATION
|
||||
# ==========================================================================
|
||||
def _prepare_invoice_line(self, **optional_values):
|
||||
"""Override to copy ADP line fields to the invoice line."""
|
||||
vals = super()._prepare_invoice_line(**optional_values)
|
||||
vals.update({
|
||||
'x_fc_serial_number': self.x_fc_serial_number,
|
||||
'x_fc_device_placement': self.x_fc_device_placement,
|
||||
'x_fc_deduction_type': self.x_fc_deduction_type,
|
||||
'x_fc_deduction_value': self.x_fc_deduction_value,
|
||||
'x_fc_adp_max_price': self.x_fc_adp_max_price,
|
||||
'x_fc_sn_required': self.x_fc_sn_required,
|
||||
'x_fc_adp_approved': self.x_fc_adp_approved,
|
||||
'x_fc_adp_device_type': self.x_fc_adp_device_type,
|
||||
})
|
||||
return vals
|
||||
|
||||
# ==========================================================================
|
||||
# ONCHANGE FOR ADP MAX PRICE LOOKUP
|
||||
# ==========================================================================
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_adp_info(self):
|
||||
"""Lookup ADP info from device codes when product changes."""
|
||||
if self.product_id:
|
||||
# Try to find device code in the reference table
|
||||
device_code = self._get_adp_device_code()
|
||||
if device_code:
|
||||
adp_device = self.env['fusion.adp.device.code'].sudo().search([
|
||||
('device_code', '=', device_code)
|
||||
], limit=1)
|
||||
if adp_device:
|
||||
self.x_fc_adp_max_price = adp_device.adp_price
|
||||
self.x_fc_sn_required = adp_device.sn_required
|
||||
237
fusion_claims/fusion_claims/models/submission_history.py
Normal file
237
fusion_claims/fusion_claims/models/submission_history.py
Normal file
@@ -0,0 +1,237 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from markupsafe import Markup
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionSubmissionHistory(models.Model):
|
||||
"""Track submission history for ADP applications.
|
||||
|
||||
Each record represents one submission or resubmission to ADP,
|
||||
including the documents submitted, the result, and any rejection reasons.
|
||||
"""
|
||||
_name = 'fusion.submission.history'
|
||||
_description = 'ADP Submission History'
|
||||
_order = 'submission_date desc, id desc'
|
||||
_rec_name = 'display_name'
|
||||
|
||||
# ==========================================================================
|
||||
# RELATIONSHIPS
|
||||
# ==========================================================================
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Sale Order',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
index=True,
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# SUBMISSION DETAILS
|
||||
# ==========================================================================
|
||||
display_name = fields.Char(
|
||||
string='Display Name',
|
||||
compute='_compute_display_name',
|
||||
store=True,
|
||||
)
|
||||
|
||||
submission_number = fields.Integer(
|
||||
string='Submission #',
|
||||
default=1,
|
||||
help='Sequence number for this submission (1 = first submission, 2+ = resubmissions)',
|
||||
)
|
||||
|
||||
submission_type = fields.Selection(
|
||||
selection=[
|
||||
('initial', 'Initial Submission'),
|
||||
('resubmission', 'Resubmission'),
|
||||
('correction', 'Correction'),
|
||||
],
|
||||
string='Type',
|
||||
default='initial',
|
||||
)
|
||||
|
||||
submission_date = fields.Date(
|
||||
string='Submission Date',
|
||||
default=fields.Date.today,
|
||||
required=True,
|
||||
)
|
||||
|
||||
submitted_by_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Submitted By',
|
||||
default=lambda self: self.env.user,
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# DOCUMENTS SUBMITTED (copies at time of submission)
|
||||
# ==========================================================================
|
||||
final_application = fields.Binary(
|
||||
string='Final Application (PDF)',
|
||||
attachment=True,
|
||||
help='Copy of the final application PDF at time of submission',
|
||||
)
|
||||
final_application_filename = fields.Char(
|
||||
string='Final Application Filename',
|
||||
)
|
||||
|
||||
xml_file = fields.Binary(
|
||||
string='XML File',
|
||||
attachment=True,
|
||||
help='Copy of the XML file at time of submission',
|
||||
)
|
||||
xml_filename = fields.Char(
|
||||
string='XML Filename',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# RESULT TRACKING
|
||||
# ==========================================================================
|
||||
result = fields.Selection(
|
||||
selection=[
|
||||
('pending', 'Pending'),
|
||||
('accepted', 'Accepted'),
|
||||
('rejected', 'Rejected'),
|
||||
('approved', 'Approved'),
|
||||
('denied', 'Denied'),
|
||||
],
|
||||
string='Result',
|
||||
default='pending',
|
||||
)
|
||||
|
||||
result_date = fields.Date(
|
||||
string='Result Date',
|
||||
help='Date when the result was received from ADP',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# REJECTION DETAILS (if rejected)
|
||||
# ==========================================================================
|
||||
rejection_reason = fields.Selection(
|
||||
selection=[
|
||||
('name_correction', 'Name Correction Needed'),
|
||||
('healthcard_correction', 'Health Card Correction Needed'),
|
||||
('duplicate_claim', 'Duplicate Claim Exists'),
|
||||
('xml_format_error', 'XML Format/Validation Error'),
|
||||
('missing_info', 'Missing Required Information'),
|
||||
('other', 'Other'),
|
||||
],
|
||||
string='Rejection Reason',
|
||||
)
|
||||
|
||||
rejection_details = fields.Text(
|
||||
string='Rejection Details',
|
||||
help='Additional details about the rejection',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# CORRECTION NOTES (for resubmissions)
|
||||
# ==========================================================================
|
||||
correction_notes = fields.Text(
|
||||
string='Correction Notes',
|
||||
help='Notes about what was corrected for this resubmission',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# COMPUTED FIELDS
|
||||
# ==========================================================================
|
||||
@api.depends('sale_order_id', 'submission_number', 'submission_type')
|
||||
def _compute_display_name(self):
|
||||
for record in self:
|
||||
order_name = record.sale_order_id.name or 'New'
|
||||
type_label = dict(record._fields['submission_type'].selection).get(
|
||||
record.submission_type, record.submission_type
|
||||
)
|
||||
record.display_name = f"{order_name} - Submission #{record.submission_number} ({type_label})"
|
||||
|
||||
# ==========================================================================
|
||||
# HELPER METHODS
|
||||
# ==========================================================================
|
||||
@api.model
|
||||
def create_from_submission(self, sale_order, submission_type='initial', correction_notes=None):
|
||||
"""Create a submission history record from a sale order submission.
|
||||
|
||||
Args:
|
||||
sale_order: The sale.order record being submitted
|
||||
submission_type: 'initial', 'resubmission', or 'correction'
|
||||
correction_notes: Optional notes about corrections made
|
||||
|
||||
Returns:
|
||||
The created fusion.submission.history record
|
||||
"""
|
||||
# Get next submission number
|
||||
existing_count = self.search_count([('sale_order_id', '=', sale_order.id)])
|
||||
submission_number = existing_count + 1
|
||||
|
||||
# If submission_number > 1, it's a resubmission
|
||||
if submission_number > 1 and submission_type == 'initial':
|
||||
submission_type = 'resubmission'
|
||||
|
||||
vals = {
|
||||
'sale_order_id': sale_order.id,
|
||||
'submission_number': submission_number,
|
||||
'submission_type': submission_type,
|
||||
'submission_date': fields.Date.today(),
|
||||
'submitted_by_id': self.env.user.id,
|
||||
'correction_notes': correction_notes,
|
||||
}
|
||||
|
||||
# Copy current documents
|
||||
if sale_order.x_fc_final_submitted_application:
|
||||
vals['final_application'] = sale_order.x_fc_final_submitted_application
|
||||
vals['final_application_filename'] = sale_order.x_fc_final_application_filename
|
||||
|
||||
if sale_order.x_fc_xml_file:
|
||||
vals['xml_file'] = sale_order.x_fc_xml_file
|
||||
vals['xml_filename'] = sale_order.x_fc_xml_filename
|
||||
|
||||
record = self.create(vals)
|
||||
|
||||
# Post to chatter
|
||||
sale_order.message_post(
|
||||
body=Markup(
|
||||
'<div style="border-left: 3px solid #3498db; padding-left: 12px; margin: 8px 0;">'
|
||||
'<p style="margin: 0 0 8px 0; font-weight: 600; color: #2980b9;">'
|
||||
f'<i class="fa fa-paper-plane"></i> Submission #{submission_number} Recorded</p>'
|
||||
'<table style="font-size: 13px; color: #555;">'
|
||||
f'<tr><td style="padding: 2px 8px 2px 0; font-weight: 500;">Type:</td><td>{dict(self._fields["submission_type"].selection).get(submission_type)}</td></tr>'
|
||||
f'<tr><td style="padding: 2px 8px 2px 0; font-weight: 500;">Date:</td><td>{fields.Date.today().strftime("%B %d, %Y")}</td></tr>'
|
||||
f'<tr><td style="padding: 2px 8px 2px 0; font-weight: 500;">By:</td><td>{self.env.user.name}</td></tr>'
|
||||
'</table>'
|
||||
+ (f'<p style="margin: 8px 0 0 0; font-size: 12px;"><strong>Corrections:</strong> {correction_notes}</p>' if correction_notes else '') +
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
return record
|
||||
|
||||
def update_result(self, result, rejection_reason=None, rejection_details=None):
|
||||
"""Update the result of a submission.
|
||||
|
||||
Args:
|
||||
result: 'accepted', 'rejected', 'approved', or 'denied'
|
||||
rejection_reason: Selection value for rejection reason
|
||||
rejection_details: Text details for rejection
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
vals = {
|
||||
'result': result,
|
||||
'result_date': fields.Date.today(),
|
||||
}
|
||||
|
||||
if result == 'rejected':
|
||||
vals['rejection_reason'] = rejection_reason
|
||||
vals['rejection_details'] = rejection_details
|
||||
|
||||
self.write(vals)
|
||||
|
||||
return self
|
||||
409
fusion_claims/fusion_claims/models/task_sync.py
Normal file
409
fusion_claims/fusion_claims/models/task_sync.py
Normal file
@@ -0,0 +1,409 @@
|
||||
# -*- 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', 'task_type', 'status',
|
||||
'scheduled_date', 'time_start', 'time_end', 'duration_hours',
|
||||
'address_street', 'address_street2', 'address_city', 'address_zip',
|
||||
'address_lat', 'address_lng', 'priority', 'partner_id',
|
||||
]
|
||||
|
||||
|
||||
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."""
|
||||
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
|
||||
|
||||
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,
|
||||
'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)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PULL: cron-based full reconciliation
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _cron_pull_remote_tasks(self):
|
||||
"""Cron job: pull tasks from all active remote instances."""
|
||||
configs = self.sudo().search([('active', '=', True)])
|
||||
for config in configs:
|
||||
try:
|
||||
config._pull_tasks_from_remote()
|
||||
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."""
|
||||
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),
|
||||
('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()
|
||||
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 ''
|
||||
|
||||
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,
|
||||
'task_type': rt.get('task_type', 'delivery'),
|
||||
'status': rt.get('status', 'scheduled'),
|
||||
'scheduled_date': rt.get('scheduled_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_lat': rt.get('address_lat', 0),
|
||||
'address_lng': rt.get('address_lng', 0),
|
||||
'priority': rt.get('priority', 'normal'),
|
||||
'x_fc_sync_client_name': client_name,
|
||||
}
|
||||
|
||||
existing = Task.search([('x_fc_sync_uuid', '=', sync_uuid)], limit=1)
|
||||
if existing:
|
||||
existing.write(vals)
|
||||
else:
|
||||
vals['sale_order_id'] = False
|
||||
Task.create([vals])
|
||||
|
||||
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:
|
||||
stale_shadows.write({'active': False, 'status': 'cancelled'})
|
||||
_logger.info("Deactivated %d stale shadow tasks from %s",
|
||||
len(stale_shadows), self.instance_id)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 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.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),
|
||||
])
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Sync Complete',
|
||||
'message': f'Synced from {self.name}. {shadow_count} shadow task(s) now visible.',
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
116
fusion_claims/fusion_claims/models/technician_location.py
Normal file
116
fusion_claims/fusion_claims/models/technician_location.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# -*- 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'),
|
||||
], string='Source', default='portal')
|
||||
|
||||
@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)."""
|
||||
self.env.cr.execute("""
|
||||
SELECT DISTINCT ON (user_id)
|
||||
user_id, latitude, longitude, accuracy, logged_at
|
||||
FROM fusion_technician_location
|
||||
WHERE logged_at > NOW() - INTERVAL '24 hours'
|
||||
ORDER BY user_id, logged_at DESC
|
||||
""")
|
||||
rows = self.env.cr.dictfetchall()
|
||||
result = []
|
||||
for row in rows:
|
||||
user = self.env['res.users'].sudo().browse(row['user_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']),
|
||||
})
|
||||
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,
|
||||
)
|
||||
2307
fusion_claims/fusion_claims/models/technician_task.py
Normal file
2307
fusion_claims/fusion_claims/models/technician_task.py
Normal file
File diff suppressed because it is too large
Load Diff
735
fusion_claims/fusion_claims/models/xml_parser.py
Normal file
735
fusion_claims/fusion_claims/models/xml_parser.py
Normal file
@@ -0,0 +1,735 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionXmlParser(models.AbstractModel):
|
||||
"""Utility to parse ADP application XML files and create/update
|
||||
client profiles and application data records.
|
||||
|
||||
Captures ALL ~300 XML fields for round-trip export fidelity.
|
||||
"""
|
||||
_name = 'fusion.xml.parser'
|
||||
_description = 'ADP XML Parser'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PUBLIC API
|
||||
# ------------------------------------------------------------------
|
||||
@api.model
|
||||
def parse_from_binary(self, binary_data, sale_order=None):
|
||||
"""Parse from binary field (base64 encoded).
|
||||
Returns tuple (profile, application_data) or (False, False).
|
||||
"""
|
||||
if not binary_data:
|
||||
return False, False
|
||||
try:
|
||||
xml_content = base64.b64decode(binary_data).decode('utf-8')
|
||||
except Exception as e:
|
||||
_logger.warning('Failed to decode XML binary: %s', e)
|
||||
return False, False
|
||||
return self.parse_and_create(xml_content, sale_order)
|
||||
|
||||
@api.model
|
||||
def parse_and_create(self, xml_content, sale_order=None):
|
||||
"""Parse raw XML string, create/update profile and application data.
|
||||
Returns tuple (profile, application_data) or (False, False).
|
||||
"""
|
||||
try:
|
||||
root = ET.fromstring(xml_content)
|
||||
except ET.ParseError as e:
|
||||
_logger.warning('Failed to parse ADP XML: %s', e)
|
||||
return False, False
|
||||
|
||||
form = root.find('Form')
|
||||
if form is None:
|
||||
form = root
|
||||
|
||||
# Step 1: Build complete JSON dict (every field, dot-notation keys)
|
||||
json_dict = self._xml_to_json(form)
|
||||
|
||||
# Step 2: Extract individual model fields from JSON
|
||||
model_vals = self._json_to_model_vals(json_dict)
|
||||
model_vals['raw_xml'] = xml_content
|
||||
model_vals['xml_data_json'] = json.dumps(json_dict, ensure_ascii=False)
|
||||
|
||||
# Step 3: Create/update profile
|
||||
profile = self._find_or_create_profile(model_vals, sale_order)
|
||||
|
||||
# Step 4: Create application data record
|
||||
model_vals['profile_id'] = profile.id
|
||||
model_vals['sale_order_id'] = sale_order.id if sale_order else False
|
||||
app_data = self.env['fusion.adp.application.data'].create(model_vals)
|
||||
|
||||
return profile, app_data
|
||||
|
||||
@api.model
|
||||
def reparse_existing(self, app_data_record):
|
||||
"""Re-parse an existing application data record from its raw_xml.
|
||||
Updates all fields in place without creating a new record.
|
||||
"""
|
||||
if not app_data_record.raw_xml:
|
||||
return False
|
||||
try:
|
||||
root = ET.fromstring(app_data_record.raw_xml)
|
||||
except ET.ParseError as e:
|
||||
_logger.warning('Failed to re-parse XML: %s', e)
|
||||
return False
|
||||
|
||||
form = root.find('Form')
|
||||
if form is None:
|
||||
form = root
|
||||
|
||||
json_dict = self._xml_to_json(form)
|
||||
model_vals = self._json_to_model_vals(json_dict)
|
||||
model_vals['xml_data_json'] = json.dumps(json_dict, ensure_ascii=False)
|
||||
|
||||
# Remove fields that shouldn't be overwritten
|
||||
model_vals.pop('raw_xml', None)
|
||||
model_vals.pop('profile_id', None)
|
||||
model_vals.pop('sale_order_id', None)
|
||||
|
||||
app_data_record.write(model_vals)
|
||||
|
||||
# Also update the linked profile
|
||||
if app_data_record.profile_id:
|
||||
profile_vals = {}
|
||||
if model_vals.get('medical_condition'):
|
||||
profile_vals['medical_condition'] = model_vals['medical_condition']
|
||||
if model_vals.get('mobility_status'):
|
||||
profile_vals['mobility_status'] = model_vals['mobility_status']
|
||||
if model_vals.get('applicant_first_name'):
|
||||
profile_vals['first_name'] = model_vals['applicant_first_name']
|
||||
if model_vals.get('applicant_last_name'):
|
||||
profile_vals['last_name'] = model_vals['applicant_last_name']
|
||||
assessment = model_vals.get('assessment_date')
|
||||
if assessment:
|
||||
profile_vals['last_assessment_date'] = assessment
|
||||
if profile_vals:
|
||||
app_data_record.profile_id.write(profile_vals)
|
||||
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# STEP 1: XML -> FLAT JSON DICT (every field preserved)
|
||||
# ------------------------------------------------------------------
|
||||
def _xml_to_json(self, form):
|
||||
"""Convert the entire Form element to a flat JSON dict with dot-notation keys."""
|
||||
d = OrderedDict()
|
||||
d['deviceCategory'] = self._t(form, 'deviceCategory')
|
||||
d['VersionNumber'] = self._t(form, 'VersionNumber')
|
||||
|
||||
# Section 1
|
||||
s1 = form.find('section1')
|
||||
if s1 is not None:
|
||||
for tag in ['applicantLastname', 'applicantFirstname', 'applicantMiddleinitial',
|
||||
'healthNo', 'versionNo', 'DateOfBirth', 'nameLTCH',
|
||||
'unitNo', 'streetNo', 'streetName', 'rrRoute',
|
||||
'city', 'province', 'postalCode',
|
||||
'homePhone', 'busPhone', 'phoneExtension']:
|
||||
d[f'section1.{tag}'] = self._t(s1, tag)
|
||||
cob = s1.find('confirmationOfBenefit')
|
||||
if cob is not None:
|
||||
for tag in ['q1Yn', 'q1Ifyes', 'q2Yn', 'q3Yn']:
|
||||
d[f'section1.confirmationOfBenefit.{tag}'] = self._t(cob, tag)
|
||||
|
||||
# Section 2
|
||||
s2 = form.find('section2')
|
||||
if s2 is not None:
|
||||
de = s2.find('devicesandEligibility')
|
||||
if de is not None:
|
||||
for tag in ['condition', 'status', 'none', 'forearm', 'wheeled', 'manual',
|
||||
'power', 'addOn', 'scooter', 'seating', 'tiltSystem', 'reclineSystem',
|
||||
'legRests', 'frame', 'stroller', 'deviceForearm', 'deviceWheeled',
|
||||
'deviceManual', 'deviceAmbulation', 'deviceDependent', 'deviceDynamic',
|
||||
'manualDyanmic', 'manualWheelchair', 'powerBase', 'powerScooter',
|
||||
'ambulation', 'positioning', 'highTech', 'standingFrame',
|
||||
'adpFunded', 'nonADPFunded']:
|
||||
d[f'section2.devicesandEligibility.{tag}'] = self._t(de, tag)
|
||||
|
||||
# Section 2a
|
||||
s2a = s2.find('section2a')
|
||||
if s2a is not None:
|
||||
for tag in ['walker', 'paediatricFrame', 'forearmCrutches', 'none',
|
||||
'reason', 'replacementStatus', 'replacementSize', 'replacementADP', 'replacementSpecial',
|
||||
'confirmation1', 'confirmation2', 'confirmation3', 'confirmation4', 'confirmation5', 'confirmation6',
|
||||
'seatHeight', 'seatHeightmeasurement', 'handleHeight', 'handleHeightmeasurement',
|
||||
'handGrips', 'forearm', 'widthHandles', 'widthHandlesmeasurement',
|
||||
'clientWeight', 'clientWeightmeasurement',
|
||||
'brakes', 'brakeType', 'noWheels', 'wheelSize', 'backSupport',
|
||||
'adpWalker', 'adpFrame', 'adpStanding',
|
||||
'nonADP1', 'nonADP2', 'nonADP3', 'nonADP4', 'nonADP5', 'nonADP6', 'nonADP7', 'nonADP8', 'nonADP9',
|
||||
'setup1', 'setup2', 'setup3', 'setup4', 'setup5', 'setup6', 'setup7', 'setup8', 'setup9',
|
||||
'setup10', 'setup11', 'setup12', 'setup13', 'setup14', 'setup15', 'setup16', 'setup17', 'setup18',
|
||||
'custom', 'costLabour']:
|
||||
d[f'section2.section2a.{tag}'] = self._t(s2a, tag)
|
||||
|
||||
# Section 2b
|
||||
s2b = s2.find('section2b')
|
||||
if s2b is not None:
|
||||
for tag in ['baseDevice', 'powerAddOndevice',
|
||||
'reason', 'replacementStatus', 'replacementSize', 'replacementADP', 'replacementSpecial',
|
||||
'confirmation1', 'confirmation2', 'confirmation3', 'confirmation4', 'confirmation5',
|
||||
'confirmation6', 'confirmation7', 'confirmation8', 'confirmation9', 'confirmation10',
|
||||
'confirmation11', 'confirmation12', 'confirmation13',
|
||||
'seatWidth', 'seatWidthmeasurement', 'seatDepth', 'seatDepthmeasurement',
|
||||
'floorHeight', 'floorHeightmeasurement', 'caneHeight', 'caneHeightmeasurement',
|
||||
'backHeight', 'backHeightmeasurement', 'restLength', 'restLengthmeasurement',
|
||||
'clientWeight', 'clientWeightmeasurement',
|
||||
'adjustableTension', 'heavyDuty', 'recliner', 'footplates', 'legrests',
|
||||
'spoke', 'projected', 'standardManual', 'gradeAids', 'casterPin',
|
||||
'amputeeAxle', 'quickRelease', 'stroller', 'oxygen', 'ventilator',
|
||||
'titanium', 'clothingGuards', 'oneArm', 'uniLateral', 'plastic',
|
||||
'rationale',
|
||||
'nonADP1', 'nonADP2', 'nonADP3', 'nonADP4', 'nonADP5', 'nonADP6', 'nonADP7', 'nonADP8', 'nonADP9',
|
||||
'setup1', 'setup2', 'setup3', 'setup4', 'setup5', 'setup6', 'setup7', 'setup8', 'setup9',
|
||||
'setup10', 'setup11', 'setup12', 'setup13', 'setup14', 'setup15', 'setup16', 'setup17', 'setup18',
|
||||
'custom', 'costLabour']:
|
||||
d[f'section2.section2b.{tag}'] = self._t(s2b, tag)
|
||||
|
||||
# Section 2c
|
||||
s2c = s2.find('section2c')
|
||||
if s2c is not None:
|
||||
for tag in ['baseDevice',
|
||||
'reason', 'replacementStatus', 'replacementSize', 'replacementADP', 'replacementSpecial',
|
||||
'confirmation1', 'confirmation2', 'confirmation3', 'confirmation4', 'confirmation5',
|
||||
'seatWidth', 'seatWidthmeasurement', 'backHeight', 'backHeightmeasurement',
|
||||
'floorHeight', 'floorHeightmeasurement', 'restLength', 'restLengthmeasurement',
|
||||
'seatDepth', 'seatDepthmeasurement', 'clientWeight', 'clientWeightmeasurement',
|
||||
'adjustableTension', 'midline', 'manualRecline', 'footplates', 'legrests',
|
||||
'swingaway', 'onePiece', 'seatPackage1', 'seatPackage2', 'oxygen', 'ventilator',
|
||||
'spControls1', 'spControls2', 'spControls3', 'spControls4', 'spControls5', 'spControls6',
|
||||
'autoCorrection', 'rationale',
|
||||
'powerTilt', 'powerRecline', 'tiltAndRecline', 'powerElevating', 'ControlBox',
|
||||
'nonADP1', 'nonADP2', 'nonADP3', 'nonADP4', 'nonADP5', 'nonADP6', 'nonADP7', 'nonADP8', 'nonADP9',
|
||||
'setup1', 'setup2', 'setup3', 'setup4', 'setup5', 'setup6', 'setup7', 'setup8', 'setup9',
|
||||
'setup10', 'setup11', 'setup12', 'setup13', 'setup14', 'setup15', 'setup16', 'setup17', 'setup18',
|
||||
'custom', 'costLabour']:
|
||||
d[f'section2.section2c.{tag}'] = self._t(s2c, tag)
|
||||
|
||||
# Section 2d
|
||||
s2d = s2.find('section2d')
|
||||
if s2d is not None:
|
||||
for tag in ['seatM', 'seatCF', 'coverM', 'coverCF', 'optionM', 'optionCF', 'hardwareM', 'hardwareCF',
|
||||
'adductorM', 'adductorCF', 'pommelCF',
|
||||
'backM', 'backCF', 'supportoptionM', 'supportoptionCF', 'backcoverCF', 'backHardwareM', 'backHardwareCF',
|
||||
'completeM', 'completeCF',
|
||||
'headrestM', 'headrestCF', 'headoptionCF', 'headhardwareM', 'headhardwareCF',
|
||||
'beltM', 'beltCF', 'beltoptionCF',
|
||||
'armsupportM', 'armsupportCF', 'armoptionM', 'armoptionCF', 'armhardwareM', 'armhardwareCF',
|
||||
'trayM', 'trayCF', 'trayoptionM', 'trayoptionCF',
|
||||
'lateralsupportM', 'lateralsupportCF', 'lateraloptionCF', 'lateralhardwareCF',
|
||||
'footsupportM', 'footsupportCF', 'footoptionM', 'footoptionCF', 'foothardwareM', 'foothardwareCF',
|
||||
'reason', 'replacementStatus', 'replacementSize', 'replacementADP', 'replacementSpecial',
|
||||
'confirmation1', 'confirmation2',
|
||||
'nonADP1', 'nonADP2', 'nonADP3', 'nonADP4', 'nonADP5', 'nonADP6', 'nonADP7', 'nonADP8', 'nonADP9',
|
||||
'setup1', 'setup2', 'setup3', 'setup4', 'setup5', 'setup6', 'setup7', 'setup8', 'setup9',
|
||||
'setup10', 'setup11', 'setup12', 'setup13', 'setup14', 'setup15', 'setup16', 'setup17', 'setup18',
|
||||
'custom', 'costLabour']:
|
||||
d[f'section2.section2d.{tag}'] = self._t(s2d, tag)
|
||||
|
||||
# Section 3
|
||||
s3 = form.find('section3')
|
||||
if s3 is not None:
|
||||
sig = s3.find('sig')
|
||||
if sig is not None:
|
||||
for tag in ['signature', 'person', 'Date']:
|
||||
d[f'section3.sig.{tag}'] = self._t(sig, tag)
|
||||
contact = s3.find('contact')
|
||||
if contact is not None:
|
||||
for tag in ['relationship', 'applicantLastname', 'applicantFirstname', 'applicantMiddleinitial',
|
||||
'unitNo', 'streetNo', 'streetName', 'rrRoute',
|
||||
'city', 'province', 'postalCode', 'homePhone', 'busPhone', 'phoneExtension']:
|
||||
d[f'section3.contact.{tag}'] = self._t(contact, tag)
|
||||
|
||||
# Section 4
|
||||
s4 = form.find('section4')
|
||||
if s4 is not None:
|
||||
auth = s4.find('authorizer')
|
||||
if auth is not None:
|
||||
for tag in ['authorizerLastname', 'authorizerFirstname', 'busPhone', 'phoneExtension', 'adpNo', 'signature', 'Date']:
|
||||
d[f'section4.authorizer.{tag}'] = self._t(auth, tag)
|
||||
vendor = s4.find('vendor')
|
||||
if vendor is not None:
|
||||
for tag in ['vendorBusName', 'adpVendorRegNo', 'vendorLastfirstname', 'positionTitle', 'vendorLocation', 'busPhone', 'phoneExtension', 'signature', 'Date']:
|
||||
d[f'section4.vendor.{tag}'] = self._t(vendor, tag)
|
||||
v2 = s4.find('vendor2')
|
||||
if v2 is not None:
|
||||
for tag in ['vendorBusName', 'adpVendorRegNo', 'vendorLastfirstname', 'positionTitle', 'vendorLocation', 'busPhone', 'phoneExtension', 'signature', 'Date']:
|
||||
d[f'section4.vendor2.{tag}'] = self._t(v2, tag)
|
||||
eq = s4.find('equipmentSpec')
|
||||
if eq is not None:
|
||||
d['section4.equipmentSpec.vendorInvoiceNo'] = self._t(eq, 'vendorInvoiceNo')
|
||||
d['section4.equipmentSpec.vendorADPRegNo'] = self._t(eq, 'vendorADPRegNo')
|
||||
t2 = eq.find('Table2')
|
||||
if t2 is not None:
|
||||
r1 = t2.find('Row1')
|
||||
if r1 is not None:
|
||||
for tag in ['Cell1', 'Cell2', 'Cell3', 'Cell4', 'Cell5']:
|
||||
d[f'section4.equipmentSpec.Table2.Row1.{tag}'] = self._t(r1, tag)
|
||||
pod = s4.find('proofOfDelivery')
|
||||
if pod is not None:
|
||||
for tag in ['signature', 'receivedBy', 'Date']:
|
||||
d[f'section4.proofOfDelivery.{tag}'] = self._t(pod, tag)
|
||||
note = s4.find('noteToADP')
|
||||
if note is not None:
|
||||
for tag in ['section1', 'section2a', 'section2b', 'section2c', 'section2d',
|
||||
'section3and4', 'vendorReplacement', 'vendorCustom', 'fundingChart', 'letter']:
|
||||
d[f'section4.noteToADP.{tag}'] = self._t(note, tag)
|
||||
|
||||
return d
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# STEP 2: JSON DICT -> MODEL FIELD VALUES
|
||||
# ------------------------------------------------------------------
|
||||
def _json_to_model_vals(self, d):
|
||||
"""Map flat JSON dict to fusion.adp.application.data field values."""
|
||||
g = d.get # shorthand
|
||||
vals = {}
|
||||
|
||||
# Metadata
|
||||
vals['device_category'] = g('deviceCategory', '') or 'MD'
|
||||
vals['version_number'] = g('VersionNumber', '')
|
||||
|
||||
# Section 1 - Applicant
|
||||
vals['applicant_last_name'] = g('section1.applicantLastname', '')
|
||||
vals['applicant_first_name'] = g('section1.applicantFirstname', '')
|
||||
vals['applicant_middle_initial'] = g('section1.applicantMiddleinitial', '')
|
||||
vals['health_card_number'] = g('section1.healthNo', '')
|
||||
vals['health_card_version'] = g('section1.versionNo', '')
|
||||
vals['date_of_birth'] = self._pd(g('section1.DateOfBirth', ''))
|
||||
vals['ltch_name'] = g('section1.nameLTCH', '')
|
||||
vals['unit_number'] = g('section1.unitNo', '')
|
||||
vals['street_number'] = g('section1.streetNo', '')
|
||||
vals['street_name'] = g('section1.streetName', '')
|
||||
vals['rural_route'] = g('section1.rrRoute', '')
|
||||
vals['city'] = g('section1.city', '')
|
||||
vals['province'] = g('section1.province', '')
|
||||
vals['postal_code'] = g('section1.postalCode', '')
|
||||
vals['home_phone'] = g('section1.homePhone', '')
|
||||
vals['business_phone'] = g('section1.busPhone', '')
|
||||
vals['phone_extension'] = g('section1.phoneExtension', '')
|
||||
|
||||
# Benefits
|
||||
q1 = g('section1.confirmationOfBenefit.q1Yn', '').lower()
|
||||
vals['receives_social_assistance'] = q1 == 'yes'
|
||||
q1type = g('section1.confirmationOfBenefit.q1Ifyes', '').lower()
|
||||
vals['benefit_owp'] = 'owp' in q1type if q1type else False
|
||||
vals['benefit_odsp'] = 'odsp' in q1type if q1type else False
|
||||
vals['benefit_acsd'] = 'acsd' in q1type if q1type else False
|
||||
if vals['benefit_owp']:
|
||||
vals['benefit_type'] = 'owp'
|
||||
elif vals['benefit_odsp']:
|
||||
vals['benefit_type'] = 'odsp'
|
||||
elif vals['benefit_acsd']:
|
||||
vals['benefit_type'] = 'acsd'
|
||||
vals['wsib_eligible'] = g('section1.confirmationOfBenefit.q2Yn', '').lower() == 'yes'
|
||||
vals['vac_eligible'] = g('section1.confirmationOfBenefit.q3Yn', '').lower() == 'yes'
|
||||
|
||||
# Section 2 - Devices & Eligibility
|
||||
vals['medical_condition'] = g('section2.devicesandEligibility.condition', '')
|
||||
vals['mobility_status'] = g('section2.devicesandEligibility.status', '')
|
||||
|
||||
# Previously funded
|
||||
vals['prev_funded_none'] = bool(g('section2.devicesandEligibility.none', ''))
|
||||
vals['prev_funded_forearm'] = bool(g('section2.devicesandEligibility.forearm', ''))
|
||||
vals['prev_funded_wheeled'] = bool(g('section2.devicesandEligibility.wheeled', ''))
|
||||
vals['prev_funded_manual'] = bool(g('section2.devicesandEligibility.manual', ''))
|
||||
vals['prev_funded_power'] = bool(g('section2.devicesandEligibility.power', ''))
|
||||
vals['prev_funded_addon'] = bool(g('section2.devicesandEligibility.addOn', ''))
|
||||
vals['prev_funded_scooter'] = bool(g('section2.devicesandEligibility.scooter', ''))
|
||||
vals['prev_funded_seating'] = bool(g('section2.devicesandEligibility.seating', ''))
|
||||
vals['prev_funded_tilt'] = bool(g('section2.devicesandEligibility.tiltSystem', ''))
|
||||
vals['prev_funded_recline'] = bool(g('section2.devicesandEligibility.reclineSystem', ''))
|
||||
vals['prev_funded_legrests'] = bool(g('section2.devicesandEligibility.legRests', ''))
|
||||
vals['prev_funded_frame'] = bool(g('section2.devicesandEligibility.frame', ''))
|
||||
vals['prev_funded_stroller'] = bool(g('section2.devicesandEligibility.stroller', ''))
|
||||
|
||||
# Devices currently required
|
||||
vals['device_forearm_crutches'] = bool(g('section2.devicesandEligibility.deviceForearm', ''))
|
||||
vals['device_wheeled_walker'] = bool(g('section2.devicesandEligibility.deviceWheeled', ''))
|
||||
vals['device_manual_wheelchair'] = bool(g('section2.devicesandEligibility.deviceManual', ''))
|
||||
vals['device_ambulation_manual'] = bool(g('section2.devicesandEligibility.deviceAmbulation', ''))
|
||||
vals['device_dependent_wheelchair'] = bool(g('section2.devicesandEligibility.deviceDependent', ''))
|
||||
vals['device_dynamic_tilt'] = bool(g('section2.devicesandEligibility.deviceDynamic', ''))
|
||||
vals['device_manual_dynamic'] = bool(g('section2.devicesandEligibility.manualDyanmic', ''))
|
||||
vals['device_manual_power_addon'] = bool(g('section2.devicesandEligibility.manualWheelchair', ''))
|
||||
vals['device_power_base'] = bool(g('section2.devicesandEligibility.powerBase', ''))
|
||||
vals['device_power_scooter'] = bool(g('section2.devicesandEligibility.powerScooter', ''))
|
||||
vals['device_ambulation_power'] = bool(g('section2.devicesandEligibility.ambulation', ''))
|
||||
vals['device_positioning'] = bool(g('section2.devicesandEligibility.positioning', ''))
|
||||
vals['device_high_tech'] = bool(g('section2.devicesandEligibility.highTech', ''))
|
||||
vals['device_standing_frame'] = bool(g('section2.devicesandEligibility.standingFrame', ''))
|
||||
vals['device_adp_funded_mods'] = bool(g('section2.devicesandEligibility.adpFunded', ''))
|
||||
vals['device_non_adp_funded_mods'] = bool(g('section2.devicesandEligibility.nonADPFunded', ''))
|
||||
|
||||
# Section 2a - Walkers
|
||||
vals['s2a_base_device'] = g('section2.section2a.walker', '')
|
||||
vals['s2a_paediatric_frame'] = g('section2.section2a.paediatricFrame', '')
|
||||
vals['s2a_forearm_crutches'] = g('section2.section2a.forearmCrutches', '')
|
||||
vals['s2a_none'] = g('section2.section2a.none', '')
|
||||
vals['s2a_reason'] = g('section2.section2a.reason', '')
|
||||
vals['s2a_replacement_status'] = g('section2.section2a.replacementStatus', '')
|
||||
vals['s2a_replacement_size'] = g('section2.section2a.replacementSize', '')
|
||||
vals['s2a_replacement_adp'] = g('section2.section2a.replacementADP', '')
|
||||
vals['s2a_replacement_special'] = g('section2.section2a.replacementSpecial', '')
|
||||
for i in range(1, 7):
|
||||
vals[f's2a_confirm{i}'] = g(f'section2.section2a.confirmation{i}', '')
|
||||
vals['s2a_seat_height'] = g('section2.section2a.seatHeight', '')
|
||||
vals['s2a_seat_height_unit'] = g('section2.section2a.seatHeightmeasurement', '')
|
||||
vals['s2a_handle_height'] = g('section2.section2a.handleHeight', '')
|
||||
vals['s2a_handle_height_unit'] = g('section2.section2a.handleHeightmeasurement', '')
|
||||
vals['s2a_hand_grips'] = g('section2.section2a.handGrips', '')
|
||||
vals['s2a_forearm_attachments'] = g('section2.section2a.forearm', '')
|
||||
vals['s2a_width_handles'] = g('section2.section2a.widthHandles', '')
|
||||
vals['s2a_width_handles_unit'] = g('section2.section2a.widthHandlesmeasurement', '')
|
||||
vals['s2a_client_weight'] = g('section2.section2a.clientWeight', '')
|
||||
vals['s2a_client_weight_unit'] = g('section2.section2a.clientWeightmeasurement', '')
|
||||
vals['s2a_brakes'] = g('section2.section2a.brakes', '')
|
||||
vals['s2a_brake_type'] = g('section2.section2a.brakeType', '')
|
||||
vals['s2a_num_wheels'] = g('section2.section2a.noWheels', '')
|
||||
vals['s2a_wheel_size'] = g('section2.section2a.wheelSize', '')
|
||||
vals['s2a_back_support'] = g('section2.section2a.backSupport', '')
|
||||
vals['s2a_adp_walker'] = g('section2.section2a.adpWalker', '')
|
||||
vals['s2a_adp_frame'] = g('section2.section2a.adpFrame', '')
|
||||
vals['s2a_adp_standing'] = g('section2.section2a.adpStanding', '')
|
||||
vals['s2a_custom'] = g('section2.section2a.custom', '')
|
||||
vals['s2a_cost_labour'] = g('section2.section2a.costLabour', '')
|
||||
|
||||
# Section 2b - Manual Wheelchairs
|
||||
vals['s2b_base_device'] = g('section2.section2b.baseDevice', '')
|
||||
vals['s2b_power_addon'] = g('section2.section2b.powerAddOndevice', '')
|
||||
vals['s2b_reason'] = g('section2.section2b.reason', '')
|
||||
vals['s2b_replacement_status'] = g('section2.section2b.replacementStatus', '')
|
||||
vals['s2b_replacement_size'] = g('section2.section2b.replacementSize', '')
|
||||
vals['s2b_replacement_adp'] = g('section2.section2b.replacementADP', '')
|
||||
vals['s2b_replacement_special'] = g('section2.section2b.replacementSpecial', '')
|
||||
for i in range(1, 14):
|
||||
vals[f's2b_confirm{i}'] = g(f'section2.section2b.confirmation{i}', '')
|
||||
vals['s2b_seat_width'] = g('section2.section2b.seatWidth', '')
|
||||
vals['s2b_seat_width_unit'] = g('section2.section2b.seatWidthmeasurement', '')
|
||||
vals['s2b_seat_depth'] = g('section2.section2b.seatDepth', '')
|
||||
vals['s2b_seat_depth_unit'] = g('section2.section2b.seatDepthmeasurement', '')
|
||||
vals['s2b_floor_height'] = g('section2.section2b.floorHeight', '')
|
||||
vals['s2b_floor_height_unit'] = g('section2.section2b.floorHeightmeasurement', '')
|
||||
vals['s2b_cane_height'] = g('section2.section2b.caneHeight', '')
|
||||
vals['s2b_cane_height_unit'] = g('section2.section2b.caneHeightmeasurement', '')
|
||||
vals['s2b_back_height'] = g('section2.section2b.backHeight', '')
|
||||
vals['s2b_back_height_unit'] = g('section2.section2b.backHeightmeasurement', '')
|
||||
vals['s2b_rest_length'] = g('section2.section2b.restLength', '')
|
||||
vals['s2b_rest_length_unit'] = g('section2.section2b.restLengthmeasurement', '')
|
||||
vals['s2b_client_weight'] = g('section2.section2b.clientWeight', '')
|
||||
vals['s2b_client_weight_unit'] = g('section2.section2b.clientWeightmeasurement', '')
|
||||
vals['s2b_adjustable_tension'] = bool(g('section2.section2b.adjustableTension', ''))
|
||||
vals['s2b_heavy_duty'] = bool(g('section2.section2b.heavyDuty', ''))
|
||||
vals['s2b_recliner'] = bool(g('section2.section2b.recliner', ''))
|
||||
vals['s2b_footplates'] = bool(g('section2.section2b.footplates', ''))
|
||||
vals['s2b_legrests'] = bool(g('section2.section2b.legrests', ''))
|
||||
vals['s2b_spoke'] = bool(g('section2.section2b.spoke', ''))
|
||||
vals['s2b_projected'] = bool(g('section2.section2b.projected', ''))
|
||||
vals['s2b_standard_manual'] = bool(g('section2.section2b.standardManual', ''))
|
||||
vals['s2b_grade_aids'] = bool(g('section2.section2b.gradeAids', ''))
|
||||
vals['s2b_caster_pin'] = bool(g('section2.section2b.casterPin', ''))
|
||||
vals['s2b_amputee_axle'] = bool(g('section2.section2b.amputeeAxle', ''))
|
||||
vals['s2b_quick_release'] = bool(g('section2.section2b.quickRelease', ''))
|
||||
vals['s2b_stroller'] = bool(g('section2.section2b.stroller', ''))
|
||||
vals['s2b_oxygen'] = bool(g('section2.section2b.oxygen', ''))
|
||||
vals['s2b_ventilator'] = bool(g('section2.section2b.ventilator', ''))
|
||||
vals['s2b_titanium'] = bool(g('section2.section2b.titanium', ''))
|
||||
vals['s2b_clothing_guards'] = bool(g('section2.section2b.clothingGuards', ''))
|
||||
vals['s2b_one_arm'] = bool(g('section2.section2b.oneArm', ''))
|
||||
vals['s2b_uni_lateral'] = bool(g('section2.section2b.uniLateral', ''))
|
||||
vals['s2b_plastic'] = bool(g('section2.section2b.plastic', ''))
|
||||
vals['s2b_rationale'] = g('section2.section2b.rationale', '')
|
||||
vals['s2b_custom'] = g('section2.section2b.custom', '')
|
||||
vals['s2b_cost_labour'] = g('section2.section2b.costLabour', '')
|
||||
|
||||
# Section 2c - Power Bases / Scooters
|
||||
vals['s2c_base_device'] = g('section2.section2c.baseDevice', '')
|
||||
vals['s2c_reason'] = g('section2.section2c.reason', '')
|
||||
vals['s2c_replacement_status'] = g('section2.section2c.replacementStatus', '')
|
||||
vals['s2c_replacement_size'] = g('section2.section2c.replacementSize', '')
|
||||
vals['s2c_replacement_adp'] = g('section2.section2c.replacementADP', '')
|
||||
vals['s2c_replacement_special'] = g('section2.section2c.replacementSpecial', '')
|
||||
for i in range(1, 6):
|
||||
vals[f's2c_confirm{i}'] = g(f'section2.section2c.confirmation{i}', '')
|
||||
vals['s2c_seat_width'] = g('section2.section2c.seatWidth', '')
|
||||
vals['s2c_seat_width_unit'] = g('section2.section2c.seatWidthmeasurement', '')
|
||||
vals['s2c_back_height'] = g('section2.section2c.backHeight', '')
|
||||
vals['s2c_back_height_unit'] = g('section2.section2c.backHeightmeasurement', '')
|
||||
vals['s2c_floor_height'] = g('section2.section2c.floorHeight', '')
|
||||
vals['s2c_floor_height_unit'] = g('section2.section2c.floorHeightmeasurement', '')
|
||||
vals['s2c_rest_length'] = g('section2.section2c.restLength', '')
|
||||
vals['s2c_rest_length_unit'] = g('section2.section2c.restLengthmeasurement', '')
|
||||
vals['s2c_seat_depth'] = g('section2.section2c.seatDepth', '')
|
||||
vals['s2c_seat_depth_unit'] = g('section2.section2c.seatDepthmeasurement', '')
|
||||
vals['s2c_client_weight'] = g('section2.section2c.clientWeight', '')
|
||||
vals['s2c_client_weight_unit'] = g('section2.section2c.clientWeightmeasurement', '')
|
||||
vals['s2c_adjustable_tension'] = bool(g('section2.section2c.adjustableTension', ''))
|
||||
vals['s2c_midline'] = bool(g('section2.section2c.midline', ''))
|
||||
vals['s2c_manual_recline'] = bool(g('section2.section2c.manualRecline', ''))
|
||||
vals['s2c_footplates'] = bool(g('section2.section2c.footplates', ''))
|
||||
vals['s2c_legrests'] = bool(g('section2.section2c.legrests', ''))
|
||||
vals['s2c_swingaway'] = bool(g('section2.section2c.swingaway', ''))
|
||||
vals['s2c_one_piece'] = bool(g('section2.section2c.onePiece', ''))
|
||||
vals['s2c_seat_package_1'] = bool(g('section2.section2c.seatPackage1', ''))
|
||||
vals['s2c_seat_package_2'] = bool(g('section2.section2c.seatPackage2', ''))
|
||||
vals['s2c_oxygen'] = bool(g('section2.section2c.oxygen', ''))
|
||||
vals['s2c_ventilator'] = bool(g('section2.section2c.ventilator', ''))
|
||||
vals['s2c_sp_controls_1'] = bool(g('section2.section2c.spControls1', ''))
|
||||
vals['s2c_sp_controls_2'] = bool(g('section2.section2c.spControls2', ''))
|
||||
vals['s2c_sp_controls_3'] = bool(g('section2.section2c.spControls3', ''))
|
||||
vals['s2c_sp_controls_4'] = bool(g('section2.section2c.spControls4', ''))
|
||||
vals['s2c_sp_controls_5'] = bool(g('section2.section2c.spControls5', ''))
|
||||
vals['s2c_sp_controls_6'] = bool(g('section2.section2c.spControls6', ''))
|
||||
vals['s2c_auto_correction'] = bool(g('section2.section2c.autoCorrection', ''))
|
||||
vals['s2c_rationale'] = g('section2.section2c.rationale', '')
|
||||
vals['s2c_power_tilt'] = bool(g('section2.section2c.powerTilt', ''))
|
||||
vals['s2c_power_recline'] = bool(g('section2.section2c.powerRecline', ''))
|
||||
vals['s2c_tilt_and_recline'] = bool(g('section2.section2c.tiltAndRecline', ''))
|
||||
vals['s2c_power_elevating'] = bool(g('section2.section2c.powerElevating', ''))
|
||||
vals['s2c_control_box'] = bool(g('section2.section2c.ControlBox', ''))
|
||||
vals['s2c_custom'] = g('section2.section2c.custom', '')
|
||||
vals['s2c_cost_labour'] = g('section2.section2c.costLabour', '')
|
||||
|
||||
# Section 2d - Positioning/Seating
|
||||
vals['s2d_seat_modular'] = bool(g('section2.section2d.seatM', ''))
|
||||
vals['s2d_seat_custom'] = bool(g('section2.section2d.seatCF', ''))
|
||||
vals['s2d_seat_cover_modular'] = bool(g('section2.section2d.coverM', ''))
|
||||
vals['s2d_seat_cover_custom'] = bool(g('section2.section2d.coverCF', ''))
|
||||
vals['s2d_seat_option_modular'] = bool(g('section2.section2d.optionM', ''))
|
||||
vals['s2d_seat_option_custom'] = bool(g('section2.section2d.optionCF', ''))
|
||||
vals['s2d_seat_hardware_modular'] = bool(g('section2.section2d.hardwareM', ''))
|
||||
vals['s2d_seat_hardware_custom'] = bool(g('section2.section2d.hardwareCF', ''))
|
||||
vals['s2d_adductor_modular'] = bool(g('section2.section2d.adductorM', ''))
|
||||
vals['s2d_adductor_custom'] = bool(g('section2.section2d.adductorCF', ''))
|
||||
vals['s2d_pommel_custom'] = bool(g('section2.section2d.pommelCF', ''))
|
||||
vals['s2d_back_modular'] = bool(g('section2.section2d.backM', ''))
|
||||
vals['s2d_back_custom'] = bool(g('section2.section2d.backCF', ''))
|
||||
vals['s2d_back_option_modular'] = bool(g('section2.section2d.supportoptionM', ''))
|
||||
vals['s2d_back_option_custom'] = bool(g('section2.section2d.supportoptionCF', ''))
|
||||
vals['s2d_back_cover_custom'] = bool(g('section2.section2d.backcoverCF', ''))
|
||||
vals['s2d_back_hardware_modular'] = bool(g('section2.section2d.backHardwareM', ''))
|
||||
vals['s2d_back_hardware_custom'] = bool(g('section2.section2d.backHardwareCF', ''))
|
||||
vals['s2d_complete_modular'] = bool(g('section2.section2d.completeM', ''))
|
||||
vals['s2d_complete_custom'] = bool(g('section2.section2d.completeCF', ''))
|
||||
vals['s2d_headrest_modular'] = bool(g('section2.section2d.headrestM', ''))
|
||||
vals['s2d_headrest_custom'] = bool(g('section2.section2d.headrestCF', ''))
|
||||
vals['s2d_head_option_custom'] = bool(g('section2.section2d.headoptionCF', ''))
|
||||
vals['s2d_head_hardware_modular'] = bool(g('section2.section2d.headhardwareM', ''))
|
||||
vals['s2d_head_hardware_custom'] = bool(g('section2.section2d.headhardwareCF', ''))
|
||||
vals['s2d_belt_modular'] = bool(g('section2.section2d.beltM', ''))
|
||||
vals['s2d_belt_custom'] = bool(g('section2.section2d.beltCF', ''))
|
||||
vals['s2d_belt_option_custom'] = bool(g('section2.section2d.beltoptionCF', ''))
|
||||
vals['s2d_arm_modular'] = bool(g('section2.section2d.armsupportM', ''))
|
||||
vals['s2d_arm_custom'] = bool(g('section2.section2d.armsupportCF', ''))
|
||||
vals['s2d_arm_option_modular'] = bool(g('section2.section2d.armoptionM', ''))
|
||||
vals['s2d_arm_option_custom'] = bool(g('section2.section2d.armoptionCF', ''))
|
||||
vals['s2d_arm_hardware_modular'] = bool(g('section2.section2d.armhardwareM', ''))
|
||||
vals['s2d_arm_hardware_custom'] = bool(g('section2.section2d.armhardwareCF', ''))
|
||||
vals['s2d_tray_modular'] = bool(g('section2.section2d.trayM', ''))
|
||||
vals['s2d_tray_custom'] = bool(g('section2.section2d.trayCF', ''))
|
||||
vals['s2d_tray_option_modular'] = bool(g('section2.section2d.trayoptionM', ''))
|
||||
vals['s2d_tray_option_custom'] = bool(g('section2.section2d.trayoptionCF', ''))
|
||||
vals['s2d_lateral_modular'] = bool(g('section2.section2d.lateralsupportM', ''))
|
||||
vals['s2d_lateral_custom'] = bool(g('section2.section2d.lateralsupportCF', ''))
|
||||
vals['s2d_lateral_option_custom'] = bool(g('section2.section2d.lateraloptionCF', ''))
|
||||
vals['s2d_lateral_hardware_custom'] = bool(g('section2.section2d.lateralhardwareCF', ''))
|
||||
vals['s2d_foot_modular'] = bool(g('section2.section2d.footsupportM', ''))
|
||||
vals['s2d_foot_custom'] = bool(g('section2.section2d.footsupportCF', ''))
|
||||
vals['s2d_foot_option_modular'] = bool(g('section2.section2d.footoptionM', ''))
|
||||
vals['s2d_foot_option_custom'] = bool(g('section2.section2d.footoptionCF', ''))
|
||||
vals['s2d_foot_hardware_modular'] = bool(g('section2.section2d.foothardwareM', ''))
|
||||
vals['s2d_foot_hardware_custom'] = bool(g('section2.section2d.foothardwareCF', ''))
|
||||
vals['s2d_reason'] = g('section2.section2d.reason', '')
|
||||
vals['s2d_replacement_status'] = g('section2.section2d.replacementStatus', '')
|
||||
vals['s2d_replacement_size'] = g('section2.section2d.replacementSize', '')
|
||||
vals['s2d_replacement_adp'] = g('section2.section2d.replacementADP', '')
|
||||
vals['s2d_replacement_special'] = g('section2.section2d.replacementSpecial', '')
|
||||
vals['s2d_confirm1'] = g('section2.section2d.confirmation1', '')
|
||||
vals['s2d_confirm2'] = g('section2.section2d.confirmation2', '')
|
||||
vals['s2d_custom'] = g('section2.section2d.custom', '')
|
||||
vals['s2d_cost_labour'] = g('section2.section2d.costLabour', '')
|
||||
|
||||
# Section 3 - Consent
|
||||
vals['consent_date'] = self._pd(g('section3.sig.Date', ''))
|
||||
person = g('section3.sig.person', '').lower()
|
||||
vals['consent_signed_by'] = 'applicant' if 'applicant' in person else ('agent' if 'agent' in person else False)
|
||||
vals['agent_relationship'] = g('section3.contact.relationship', '')
|
||||
vals['agent_last_name'] = g('section3.contact.applicantLastname', '')
|
||||
vals['agent_first_name'] = g('section3.contact.applicantFirstname', '')
|
||||
vals['agent_middle_initial'] = g('section3.contact.applicantMiddleinitial', '')
|
||||
vals['agent_unit'] = g('section3.contact.unitNo', '')
|
||||
vals['agent_street_no'] = g('section3.contact.streetNo', '')
|
||||
vals['agent_street_name'] = g('section3.contact.streetName', '')
|
||||
vals['agent_rural_route'] = g('section3.contact.rrRoute', '')
|
||||
vals['agent_city'] = g('section3.contact.city', '')
|
||||
vals['agent_province'] = g('section3.contact.province', '')
|
||||
vals['agent_postal_code'] = g('section3.contact.postalCode', '')
|
||||
vals['agent_home_phone'] = g('section3.contact.homePhone', '')
|
||||
vals['agent_bus_phone'] = g('section3.contact.busPhone', '')
|
||||
vals['agent_phone_ext'] = g('section3.contact.phoneExtension', '')
|
||||
|
||||
# Section 4 - Authorizer
|
||||
vals['authorizer_last_name'] = g('section4.authorizer.authorizerLastname', '')
|
||||
vals['authorizer_first_name'] = g('section4.authorizer.authorizerFirstname', '')
|
||||
vals['authorizer_phone'] = g('section4.authorizer.busPhone', '')
|
||||
vals['authorizer_phone_ext'] = g('section4.authorizer.phoneExtension', '')
|
||||
vals['authorizer_adp_number'] = g('section4.authorizer.adpNo', '')
|
||||
vals['assessment_date'] = self._pd(g('section4.authorizer.Date', ''))
|
||||
vals['application_date'] = vals['consent_date'] or vals['assessment_date']
|
||||
|
||||
# Section 4 - Vendor 1
|
||||
vals['vendor_business_name'] = g('section4.vendor.vendorBusName', '')
|
||||
vals['vendor_adp_number'] = g('section4.vendor.adpVendorRegNo', '')
|
||||
vals['vendor_representative'] = g('section4.vendor.vendorLastfirstname', '')
|
||||
vals['vendor_position'] = g('section4.vendor.positionTitle', '')
|
||||
vals['vendor_location'] = g('section4.vendor.vendorLocation', '')
|
||||
vals['vendor_phone'] = g('section4.vendor.busPhone', '')
|
||||
vals['vendor_phone_ext'] = g('section4.vendor.phoneExtension', '')
|
||||
vals['vendor_sign_date'] = self._pd(g('section4.vendor.Date', ''))
|
||||
|
||||
# Section 4 - Vendor 2
|
||||
vals['vendor2_business_name'] = g('section4.vendor2.vendorBusName', '')
|
||||
vals['vendor2_adp_number'] = g('section4.vendor2.adpVendorRegNo', '')
|
||||
vals['vendor2_representative'] = g('section4.vendor2.vendorLastfirstname', '')
|
||||
vals['vendor2_position'] = g('section4.vendor2.positionTitle', '')
|
||||
vals['vendor2_location'] = g('section4.vendor2.vendorLocation', '')
|
||||
vals['vendor2_phone'] = g('section4.vendor2.busPhone', '')
|
||||
vals['vendor2_phone_ext'] = g('section4.vendor2.phoneExtension', '')
|
||||
vals['vendor2_sign_date'] = self._pd(g('section4.vendor2.Date', ''))
|
||||
|
||||
# Equipment Spec
|
||||
vals['equip_vendor_invoice_no'] = g('section4.equipmentSpec.vendorInvoiceNo', '')
|
||||
vals['equip_vendor_adp_reg'] = g('section4.equipmentSpec.vendorADPRegNo', '')
|
||||
vals['equip_cell1'] = g('section4.equipmentSpec.Table2.Row1.Cell1', '')
|
||||
vals['equip_cell2'] = g('section4.equipmentSpec.Table2.Row1.Cell2', '')
|
||||
vals['equip_cell3'] = g('section4.equipmentSpec.Table2.Row1.Cell3', '')
|
||||
vals['equip_cell4'] = g('section4.equipmentSpec.Table2.Row1.Cell4', '')
|
||||
vals['equip_cell5'] = g('section4.equipmentSpec.Table2.Row1.Cell5', '')
|
||||
vals['pod_received_by'] = g('section4.proofOfDelivery.receivedBy', '')
|
||||
vals['pod_date'] = self._pd(g('section4.proofOfDelivery.Date', ''))
|
||||
|
||||
# Note to ADP
|
||||
vals['note_section1'] = bool(g('section4.noteToADP.section1', ''))
|
||||
vals['note_section2a'] = bool(g('section4.noteToADP.section2a', ''))
|
||||
vals['note_section2b'] = bool(g('section4.noteToADP.section2b', ''))
|
||||
vals['note_section2c'] = bool(g('section4.noteToADP.section2c', ''))
|
||||
vals['note_section2d'] = bool(g('section4.noteToADP.section2d', ''))
|
||||
vals['note_section3and4'] = bool(g('section4.noteToADP.section3and4', ''))
|
||||
vals['note_vendor_replacement'] = g('section4.noteToADP.vendorReplacement', '')
|
||||
vals['note_vendor_custom'] = g('section4.noteToADP.vendorCustom', '')
|
||||
vals['note_funding_chart'] = g('section4.noteToADP.fundingChart', '')
|
||||
vals['note_letter'] = g('section4.noteToADP.letter', '')
|
||||
|
||||
return vals
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PROFILE MANAGEMENT
|
||||
# ------------------------------------------------------------------
|
||||
def _find_or_create_profile(self, vals, sale_order=None):
|
||||
"""Find or create a client profile from parsed application data."""
|
||||
Profile = self.env['fusion.client.profile']
|
||||
hc = (vals.get('health_card_number') or '').strip()
|
||||
first = (vals.get('applicant_first_name') or '').strip()
|
||||
last = (vals.get('applicant_last_name') or '').strip()
|
||||
dob = vals.get('date_of_birth')
|
||||
|
||||
profile = False
|
||||
if hc:
|
||||
profile = Profile.search([('health_card_number', '=', hc)], limit=1)
|
||||
if not profile and first and last and dob:
|
||||
profile = Profile.search([
|
||||
('first_name', '=ilike', first),
|
||||
('last_name', '=ilike', last),
|
||||
('date_of_birth', '=', dob),
|
||||
], limit=1)
|
||||
|
||||
profile_vals = {
|
||||
'first_name': first,
|
||||
'last_name': last,
|
||||
'middle_initial': vals.get('applicant_middle_initial', ''),
|
||||
'health_card_number': hc,
|
||||
'health_card_version': vals.get('health_card_version', ''),
|
||||
'date_of_birth': dob,
|
||||
'ltch_name': vals.get('ltch_name', ''),
|
||||
'unit_number': vals.get('unit_number', ''),
|
||||
'street_number': vals.get('street_number', ''),
|
||||
'street_name': vals.get('street_name', ''),
|
||||
'rural_route': vals.get('rural_route', ''),
|
||||
'city': vals.get('city', ''),
|
||||
'province': vals.get('province', '') or 'ON',
|
||||
'postal_code': vals.get('postal_code', ''),
|
||||
'home_phone': vals.get('home_phone', ''),
|
||||
'business_phone': vals.get('business_phone', ''),
|
||||
'phone_extension': vals.get('phone_extension', ''),
|
||||
'medical_condition': vals.get('medical_condition', ''),
|
||||
'mobility_status': vals.get('mobility_status', ''),
|
||||
}
|
||||
if vals.get('receives_social_assistance'):
|
||||
profile_vals['receives_social_assistance'] = True
|
||||
profile_vals['benefit_type'] = vals.get('benefit_type')
|
||||
if vals.get('wsib_eligible'):
|
||||
profile_vals['wsib_eligible'] = True
|
||||
if vals.get('vac_eligible'):
|
||||
profile_vals['vac_eligible'] = True
|
||||
if vals.get('assessment_date'):
|
||||
profile_vals['last_assessment_date'] = vals['assessment_date']
|
||||
|
||||
# Link to partner
|
||||
if sale_order and sale_order.partner_id:
|
||||
profile_vals['partner_id'] = sale_order.partner_id.id
|
||||
elif not profile or not profile.partner_id:
|
||||
partner = self._find_partner(first, last)
|
||||
if partner:
|
||||
profile_vals['partner_id'] = partner.id
|
||||
|
||||
if profile:
|
||||
profile.write(profile_vals)
|
||||
else:
|
||||
profile = Profile.create(profile_vals)
|
||||
|
||||
return profile
|
||||
|
||||
def _find_partner(self, first_name, last_name):
|
||||
"""Try to find a matching res.partner."""
|
||||
if not first_name or not last_name:
|
||||
return False
|
||||
Partner = self.env['res.partner']
|
||||
partner = Partner.search([('name', 'ilike', f'{first_name} {last_name}')], limit=1)
|
||||
if not partner:
|
||||
partner = Partner.search([('name', 'ilike', f'{last_name}, {first_name}')], limit=1)
|
||||
return partner or False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# HELPERS
|
||||
# ------------------------------------------------------------------
|
||||
@staticmethod
|
||||
def _t(element, tag):
|
||||
"""Get text of child element, empty string if missing."""
|
||||
child = element.find(tag)
|
||||
if child is not None and child.text:
|
||||
return child.text.strip()
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def _pd(date_str):
|
||||
"""Parse date string, return date or False."""
|
||||
if not date_str:
|
||||
return False
|
||||
for fmt in ('%Y/%m/%d', '%Y-%m-%d', '%Y%m%d'):
|
||||
try:
|
||||
return datetime.strptime(date_str.strip(), fmt).date()
|
||||
except ValueError:
|
||||
continue
|
||||
return False
|
||||
410
fusion_claims/fusion_claims/report/invoice_report_landscape.xml
Normal file
410
fusion_claims/fusion_claims/report/invoice_report_landscape.xml
Normal file
@@ -0,0 +1,410 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
Landscape Invoice Report Template
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_invoice_landscape">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-set="is_adp" t-value="doc.x_fc_is_adp_invoice"/>
|
||||
|
||||
<style>
|
||||
.fc-landscape { font-family: Arial, sans-serif; font-size: 11pt; }
|
||||
.fc-landscape table { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
|
||||
.fc-landscape table.bordered, .fc-landscape table.bordered th, .fc-landscape table.bordered td { border: 1px solid #000; }
|
||||
.fc-landscape th { background-color: #0066a1; color: white; padding: 8px 10px; font-weight: bold; font-size: 10pt; }
|
||||
.fc-landscape td { padding: 6px 8px; vertical-align: top; font-size: 10pt; }
|
||||
.fc-landscape .text-center { text-align: center; }
|
||||
.fc-landscape .text-end { text-align: right; }
|
||||
.fc-landscape .text-start { text-align: left; }
|
||||
.fc-landscape .adp-bg { background-color: #e3f2fd; }
|
||||
.fc-landscape .client-bg { background-color: #fff3e0; }
|
||||
.fc-landscape .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fc-landscape .note-row { font-style: italic; }
|
||||
.fc-landscape h2 { color: #0066a1; margin: 10px 0; font-size: 18pt; }
|
||||
.fc-landscape .info-table td { padding: 8px 12px; font-size: 11pt; }
|
||||
.fc-landscape .info-table th { background-color: #f5f5f5; color: #333; font-size: 10pt; padding: 6px 12px; }
|
||||
.fc-landscape .totals-table { border: 1px solid #000; }
|
||||
.fc-landscape .totals-table td { border: 1px solid #000; padding: 8px 12px; font-size: 11pt; }
|
||||
</style>
|
||||
|
||||
<div class="fc-landscape">
|
||||
<div class="page">
|
||||
|
||||
<!-- Document Title -->
|
||||
<h2 style="text-align: left;">
|
||||
<span t-if="doc.move_type == 'out_invoice' and doc.state == 'posted'">Invoice </span>
|
||||
<span t-elif="doc.move_type == 'out_invoice' and doc.state == 'draft'">Draft Invoice </span>
|
||||
<span t-elif="doc.move_type == 'out_refund'">Credit Note </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h2>
|
||||
|
||||
<!-- Address Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">BILLING ADDRESS</th>
|
||||
<th style="width: 50%;">DELIVERY ADDRESS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="height: 70px; font-size: 12pt;">
|
||||
<div t-field="doc.partner_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="height: 70px; font-size: 12pt;">
|
||||
<t t-if="doc.partner_shipping_id">
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div t-field="doc.partner_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Invoice Info Table -->
|
||||
<table class="bordered info-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>INVOICE DATE</th>
|
||||
<th>DUE DATE</th>
|
||||
<th>CLIENT TYPE</th>
|
||||
<th>SOURCE</th>
|
||||
<th>SALES REP</th>
|
||||
<th>AUTHORIZER</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.invoice_date"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.invoice_date_due"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_client_type or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.invoice_origin"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.invoice_user_id"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="doc.x_fc_authorizer_id" t-field="doc.x_fc_authorizer_id"/>
|
||||
<span t-else="">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- ADP Info Table (only for ADP invoices) -->
|
||||
<t t-if="is_adp">
|
||||
<table class="bordered info-table">
|
||||
<thead>
|
||||
<tr class="adp-bg">
|
||||
<th style="background-color: #e3f2fd; color: #333;">CLAIM #</th>
|
||||
<th style="background-color: #e3f2fd; color: #333;">APPLICATION TYPE</th>
|
||||
<th style="background-color: #e3f2fd; color: #333;">CLIENT REF 2</th>
|
||||
<th style="background-color: #e3f2fd; color: #333;">DELIVERY DATE</th>
|
||||
<th style="background-color: #e3f2fd; color: #333;">AUTHORIZATION</th>
|
||||
<th style="background-color: #e3f2fd; color: #333;">APPROVAL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="adp-bg">
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_claim_number or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<!-- Get application type from linked sale order -->
|
||||
<t t-set="linked_so" t-value="doc.invoice_line_ids.mapped('sale_line_ids.order_id')[:1]"/>
|
||||
<t t-if="linked_so and linked_so.x_fc_reason_for_application">
|
||||
<t t-set="app_type" t-value="dict(linked_so._fields.get('x_fc_reason_for_application') and linked_so._fields['x_fc_reason_for_application'].selection or []).get(linked_so.x_fc_reason_for_application, '-')"/>
|
||||
<span t-esc="app_type"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_client_ref_2 or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="doc.x_fc_adp_delivery_date">
|
||||
<span t-field="doc.x_fc_adp_delivery_date"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<!-- Get authorization date from linked sale order -->
|
||||
<t t-if="linked_so and linked_so.x_fc_claim_authorization_date">
|
||||
<span t-field="linked_so.x_fc_claim_authorization_date"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<!-- Get approval date from linked sale order -->
|
||||
<t t-if="linked_so and linked_so.x_fc_claim_approval_date">
|
||||
<span t-field="linked_so.x_fc_claim_approval_date"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<!-- Invoice Lines Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" style="width: 8%;">ADP CODE</th>
|
||||
<th class="text-start" style="width: 22%;">DESCRIPTION</th>
|
||||
<th class="text-center" style="width: 8%;">SERIAL #</th>
|
||||
<th t-if="is_adp" class="text-center" style="width: 5%;">PLCMT</th>
|
||||
<th class="text-center" style="width: 5%;">QTY</th>
|
||||
<th class="text-center" style="width: 10%;">UNIT PRICE</th>
|
||||
<th class="text-center" style="width: 10%; background-color: #1976d2; color: white;">ADP PORTION</th>
|
||||
<th class="text-center" style="width: 10%; background-color: #e65100; color: white;">CLIENT PORTION</th>
|
||||
<th class="text-center" style="width: 10%;">TAX</th>
|
||||
<th class="text-center" style="width: 10%;">TOTAL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.invoice_line_ids" t-as="line">
|
||||
<!-- Section Header -->
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<tr class="section-row">
|
||||
<td t-att-colspan="'10' if is_adp else '9'">
|
||||
<strong><span t-field="line.name"/></strong>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Note Line -->
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row">
|
||||
<td t-att-colspan="'10' if is_adp else '9'">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Product Line (display_type is False/None/empty for actual products) -->
|
||||
<t t-else="">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="line.product_id.x_fc_adp_device_code"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="line.name">
|
||||
<t t-set="clean_name" t-value="line.name"/>
|
||||
<t t-if="'] ' in line.name">
|
||||
<t t-set="clean_name" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<t t-esc="clean_name"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="line.x_fc_serial_number or ''"/>
|
||||
</td>
|
||||
<td t-if="is_adp" class="text-center">
|
||||
<span t-esc="line.x_fc_device_placement or 'N/A'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.quantity"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<!-- Show ADP price if available, otherwise unit price -->
|
||||
<t t-if="line.product_id.product_tmpl_id.x_fc_adp_price">
|
||||
<span t-esc="line.product_id.product_tmpl_id.x_fc_adp_price" t-options="{'widget': 'monetary', 'display_currency': doc.currency_id}"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-end adp-bg">
|
||||
<span t-field="line.x_fc_adp_portion" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
<td class="text-end client-bg">
|
||||
<span t-field="line.x_fc_client_portion" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-esc="', '.join([(tax.invoice_label or tax.name) for tax in line.tax_ids]) or 'NO TAX'"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Payment Terms and Totals Row -->
|
||||
<div class="row" style="margin-top: 15px;">
|
||||
<div class="col-7">
|
||||
<t t-if="doc.invoice_payment_term_id.note">
|
||||
<strong>Payment Terms:</strong><br/>
|
||||
<span t-field="doc.invoice_payment_term_id.note"/>
|
||||
</t>
|
||||
<t t-if="doc.payment_reference">
|
||||
<div style="margin-top: 10px;">
|
||||
<strong>Payment Reference:</strong>
|
||||
<span t-field="doc.payment_reference"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-5" style="text-align: right;">
|
||||
<!-- Totals Table with borders -->
|
||||
<table class="totals-table" style="width: auto; margin-left: auto;">
|
||||
<tr>
|
||||
<td style="min-width: 200px;">Subtotal</td>
|
||||
<td class="text-end" style="min-width: 150px;">
|
||||
<span t-field="doc.amount_untaxed" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Show actual invoice totals based on invoice portion type -->
|
||||
<t t-if="doc.x_fc_adp_invoice_portion == 'adp'">
|
||||
<tr class="adp-bg">
|
||||
<td><strong>Total ADP Portion</strong></td>
|
||||
<td class="text-end">
|
||||
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="client-bg">
|
||||
<td><strong>Total Client Portion</strong></td>
|
||||
<td class="text-end">
|
||||
<span t-field="doc.x_fc_sibling_client_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-elif="doc.x_fc_adp_invoice_portion == 'client'">
|
||||
<tr class="adp-bg">
|
||||
<td><strong>Total ADP Portion</strong></td>
|
||||
<td class="text-end">
|
||||
<span t-field="doc.x_fc_sibling_adp_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="client-bg">
|
||||
<td><strong>Total Client Portion</strong></td>
|
||||
<td class="text-end">
|
||||
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<tr class="adp-bg">
|
||||
<td><strong>Total ADP Portion</strong></td>
|
||||
<td class="text-end">
|
||||
<span t-field="doc.x_fc_adp_portion_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="client-bg">
|
||||
<td><strong>Total Client Portion</strong></td>
|
||||
<td class="text-end">
|
||||
<span t-field="doc.x_fc_client_portion_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<tr>
|
||||
<td>Taxes</td>
|
||||
<td class="text-end">
|
||||
<span t-field="doc.amount_tax" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Grand Total</strong></td>
|
||||
<td class="text-end"><strong>
|
||||
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</strong></td>
|
||||
</tr>
|
||||
<t t-if="doc.amount_residual and doc.amount_residual != doc.amount_total">
|
||||
<tr>
|
||||
<td><strong>Amount Due</strong></td>
|
||||
<td class="text-end"><strong>
|
||||
<span t-field="doc.amount_residual" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</strong></td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Details Section -->
|
||||
<t t-if="doc.payment_state != 'invoicing_legacy'">
|
||||
<t t-set="payments_vals" t-value="doc.sudo().invoice_payments_widget and doc.sudo().invoice_payments_widget.get('content') or []"/>
|
||||
<t t-if="payments_vals or doc.payment_state == 'paid'">
|
||||
<table class="bordered" style="margin-top: 15px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="4" style="background-color: #28a745; color: white;">
|
||||
<t t-if="doc.payment_state == 'paid'">✓ PAYMENT DETAILS - PAID IN FULL</t>
|
||||
<t t-elif="doc.payment_state == 'partial'">PAYMENT DETAILS - PARTIALLY PAID</t>
|
||||
<t t-else="">PAYMENT DETAILS</t>
|
||||
</th>
|
||||
</tr>
|
||||
<tr style="background-color: #f5f5f5;">
|
||||
<th style="width: 25%;">Date</th>
|
||||
<th style="width: 30%;">Payment Method</th>
|
||||
<th style="width: 20%;">Card #</th>
|
||||
<th style="width: 25%;" class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="payments_vals" t-as="payment_vals">
|
||||
<tr t-if="not payment_vals.get('is_exchange')">
|
||||
<td>
|
||||
<t t-if="payment_vals.get('is_refund')">Reversed </t>
|
||||
<span t-out="payment_vals.get('date')" t-options='{"widget": "date"}'/>
|
||||
</td>
|
||||
<td>
|
||||
<span t-out="payment_vals.get('payment_method_name') or '-'"/>
|
||||
</td>
|
||||
<td>
|
||||
<!-- Get card info from payment record -->
|
||||
<t t-set="payment_rec" t-value="doc.env['account.payment'].sudo().browse(payment_vals.get('account_payment_id'))"/>
|
||||
<t t-if="payment_rec and payment_rec.x_fc_card_last_four">
|
||||
****<span t-esc="payment_rec.x_fc_card_last_four"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-out="payment_vals.get('amount')" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-if="doc.amount_residual and doc.amount_residual > 0 and doc.payment_state != 'paid'">
|
||||
<tr style="background-color: #fff3e0;">
|
||||
<td colspan="3"><strong>Amount Due</strong></td>
|
||||
<td class="text-end"><strong><span t-field="doc.amount_residual" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/></strong></td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!-- Notes -->
|
||||
<t t-if="doc.narration">
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>Notes:</strong>
|
||||
<div t-field="doc.narration"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
377
fusion_claims/fusion_claims/report/invoice_report_portrait.xml
Normal file
377
fusion_claims/fusion_claims/report/invoice_report_portrait.xml
Normal file
@@ -0,0 +1,377 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_invoice_portrait">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-set="is_adp" t-value="doc.x_fc_is_adp_invoice"/>
|
||||
|
||||
<style>
|
||||
.fc-report { font-family: Arial, sans-serif; font-size: 10pt; }
|
||||
.fc-report table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
|
||||
.fc-report table.bordered, .fc-report table.bordered th, .fc-report table.bordered td { border: 1px solid #000; }
|
||||
.fc-report th { background-color: #f0f0f0; padding: 6px 8px; font-weight: bold; text-align: center; }
|
||||
.fc-report td { padding: 6px 8px; vertical-align: top; }
|
||||
.fc-report .text-center { text-align: center; }
|
||||
.fc-report .text-end { text-align: right; }
|
||||
.fc-report .text-start { text-align: left; }
|
||||
.fc-report .adp-bg { background-color: #e3f2fd; }
|
||||
.fc-report .client-bg { background-color: #fff3e0; }
|
||||
.fc-report .section-row { background-color: #f8f8f8; font-weight: bold; }
|
||||
.fc-report .note-row { font-style: italic; }
|
||||
.fc-report h4 { color: #005a83; margin: 0 0 15px 0; }
|
||||
.fc-report .totals-table { border: 1px solid #000; border-collapse: collapse; }
|
||||
.fc-report .totals-table td { border: 1px solid #000; padding: 6px 8px; }
|
||||
</style>
|
||||
|
||||
<div class="fc-report">
|
||||
<div class="page">
|
||||
|
||||
<!-- Document Title -->
|
||||
<h4 style="text-align: left;">
|
||||
<span t-if="doc.move_type == 'out_invoice' and doc.state == 'posted'">Invoice </span>
|
||||
<span t-elif="doc.move_type == 'out_invoice' and doc.state == 'draft'">Draft Invoice </span>
|
||||
<span t-elif="doc.move_type == 'out_refund'">Credit Note </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h4>
|
||||
|
||||
<!-- Address Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">BILLING ADDRESS</th>
|
||||
<th style="width: 50%;">DELIVERY ADDRESS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="height: 70px; font-size: 11pt;">
|
||||
<div t-field="doc.partner_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="height: 70px; font-size: 11pt;">
|
||||
<t t-if="doc.partner_shipping_id">
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div t-field="doc.partner_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Invoice Info Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 20%;">INVOICE DATE</th>
|
||||
<th style="width: 20%;">DUE DATE</th>
|
||||
<th style="width: 20%;">SOURCE</th>
|
||||
<th style="width: 20%;">SALESPERSON</th>
|
||||
<th style="width: 20%;">
|
||||
<t t-if="is_adp">APPLICATION TYPE</t>
|
||||
<t t-else="">INVOICE TYPE</t>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.invoice_date"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.invoice_date_due"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.invoice_origin"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.invoice_user_id"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="is_adp">
|
||||
<!-- Get application type from linked sale order -->
|
||||
<t t-set="linked_so" t-value="doc.invoice_line_ids.mapped('sale_line_ids.order_id')[:1]"/>
|
||||
<t t-if="linked_so and linked_so.x_fc_reason_for_application">
|
||||
<t t-set="app_type" t-value="dict(linked_so._fields.get('x_fc_reason_for_application') and linked_so._fields['x_fc_reason_for_application'].selection or []).get(linked_so.x_fc_reason_for_application, '-')"/>
|
||||
<span t-esc="app_type"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-esc="doc.x_fc_invoice_type or '-'"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- ADP Info Table (only for ADP invoices) -->
|
||||
<t t-if="is_adp">
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr class="adp-bg">
|
||||
<th style="width: 25%;">CLAIM NUMBER</th>
|
||||
<th style="width: 25%;">CLIENT TYPE</th>
|
||||
<th style="width: 25%;">DELIVERY DATE</th>
|
||||
<th style="width: 25%;">AUTHORIZER</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="adp-bg">
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_claim_number or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_client_type or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="doc.x_fc_adp_delivery_date">
|
||||
<span t-field="doc.x_fc_adp_delivery_date"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="doc.x_fc_authorizer_id" t-field="doc.x_fc_authorizer_id"/>
|
||||
<span t-else="">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<!-- Invoice Lines Table -->
|
||||
<table class="bordered" style="font-size: 9pt;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" style="width: 10%;">ADP CODE</th>
|
||||
<th class="text-start" style="width: 22%;">DESCRIPTION</th>
|
||||
<th class="text-center" style="width: 10%;">SERIAL #</th>
|
||||
<th class="text-center" style="width: 6%;">QTY</th>
|
||||
<th class="text-center" style="width: 9%;">UNIT PRICE</th>
|
||||
<th class="text-center" style="width: 7%;">TAXES</th>
|
||||
<th t-if="is_adp" class="text-center" style="width: 11%; background-color: #1976d2; color: white;">ADP PORTION</th>
|
||||
<th t-if="is_adp" class="text-center" style="width: 11%; background-color: #e65100; color: white;">CLIENT PORTION</th>
|
||||
<th class="text-center" style="width: 11%;">AMOUNT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.invoice_line_ids" t-as="line">
|
||||
<!-- Section Header -->
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<tr class="section-row">
|
||||
<td t-att-colspan="'9' if is_adp else '7'">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Note Line -->
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row">
|
||||
<td t-att-colspan="'9' if is_adp else '7'">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Product Line (display_type is False/None/empty for actual products) -->
|
||||
<t t-else="">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="line.product_id.x_fc_adp_device_code"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="line.name">
|
||||
<t t-set="clean_name" t-value="line.name"/>
|
||||
<t t-if="'] ' in line.name">
|
||||
<t t-set="clean_name" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<t t-esc="clean_name"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="line.x_fc_serial_number or ''"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.quantity"/>
|
||||
<span t-field="line.product_uom_id" groups="uom.group_uom"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<!-- Show ADP price if available, otherwise unit price -->
|
||||
<t t-if="line.product_id.product_tmpl_id.x_fc_adp_price">
|
||||
<span t-esc="line.product_id.product_tmpl_id.x_fc_adp_price" t-options="{'widget': 'monetary', 'display_currency': doc.currency_id}"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-field="line.price_unit"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-esc="', '.join([(tax.invoice_label or tax.name) for tax in line.tax_ids]) or 'NO TAX'"/>
|
||||
</td>
|
||||
<td t-if="is_adp" class="text-center adp-bg">
|
||||
<span t-field="line.x_fc_adp_portion"/>
|
||||
</td>
|
||||
<td t-if="is_adp" class="text-center client-bg">
|
||||
<span t-field="line.x_fc_client_portion"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.price_subtotal"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Payment Terms and Totals Row -->
|
||||
<div class="row" style="margin-top: 15px;">
|
||||
<div class="col-6">
|
||||
<t t-if="doc.invoice_payment_term_id.note">
|
||||
<strong>Payment Terms:</strong><br/>
|
||||
<span t-field="doc.invoice_payment_term_id.note"/>
|
||||
</t>
|
||||
<t t-if="doc.payment_reference">
|
||||
<div style="margin-top: 10px;">
|
||||
<strong>Payment Reference:</strong>
|
||||
<span t-field="doc.payment_reference"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-6" style="text-align: right;">
|
||||
<!-- Totals Table with borders -->
|
||||
<table class="totals-table" style="width: auto; margin-left: auto;">
|
||||
<tr>
|
||||
<td style="min-width: 140px;">Subtotal</td>
|
||||
<td class="text-end" style="min-width: 100px;"><span t-field="doc.amount_untaxed"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Taxes</td>
|
||||
<td class="text-end"><span t-field="doc.amount_tax"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Total</strong></td>
|
||||
<td class="text-end"><strong><span t-field="doc.amount_total"/></strong></td>
|
||||
</tr>
|
||||
<t t-if="is_adp">
|
||||
<!-- Show actual invoice totals, not calculated portions -->
|
||||
<!-- On ADP invoice (75%): this invoice = ADP portion, sibling = Client portion -->
|
||||
<!-- On Client invoice (25%): this invoice = Client portion, sibling = ADP portion -->
|
||||
<t t-if="doc.x_fc_adp_invoice_portion == 'adp'">
|
||||
<tr class="adp-bg">
|
||||
<td><strong>Total ADP Portion</strong></td>
|
||||
<td class="text-end"><span t-field="doc.amount_total"/></td>
|
||||
</tr>
|
||||
<tr class="client-bg">
|
||||
<td><strong>Total Client Portion</strong></td>
|
||||
<td class="text-end"><span t-field="doc.x_fc_sibling_client_total"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-elif="doc.x_fc_adp_invoice_portion == 'client'">
|
||||
<tr class="adp-bg">
|
||||
<td><strong>Total ADP Portion</strong></td>
|
||||
<td class="text-end"><span t-field="doc.x_fc_sibling_adp_total"/></td>
|
||||
</tr>
|
||||
<tr class="client-bg">
|
||||
<td><strong>Total Client Portion</strong></td>
|
||||
<td class="text-end"><span t-field="doc.amount_total"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<!-- Fallback for full invoices or unknown portion -->
|
||||
<tr class="adp-bg">
|
||||
<td><strong>Total ADP Portion</strong></td>
|
||||
<td class="text-end"><span t-field="doc.x_fc_adp_portion_total"/></td>
|
||||
</tr>
|
||||
<tr class="client-bg">
|
||||
<td><strong>Total Client Portion</strong></td>
|
||||
<td class="text-end"><span t-field="doc.x_fc_client_portion_total"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="doc.amount_residual and doc.amount_residual != doc.amount_total">
|
||||
<tr>
|
||||
<td><strong>Amount Due</strong></td>
|
||||
<td class="text-end"><strong><span t-field="doc.amount_residual"/></strong></td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Details Section -->
|
||||
<t t-if="doc.payment_state != 'invoicing_legacy'">
|
||||
<t t-set="payments_vals" t-value="doc.sudo().invoice_payments_widget and doc.sudo().invoice_payments_widget.get('content') or []"/>
|
||||
<t t-if="payments_vals or doc.payment_state == 'paid'">
|
||||
<table class="bordered" style="margin-top: 15px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="4" style="background-color: #28a745; color: white;">
|
||||
<t t-if="doc.payment_state == 'paid'">✓ PAYMENT DETAILS - PAID IN FULL</t>
|
||||
<t t-elif="doc.payment_state == 'partial'">PAYMENT DETAILS - PARTIALLY PAID</t>
|
||||
<t t-else="">PAYMENT DETAILS</t>
|
||||
</th>
|
||||
</tr>
|
||||
<tr style="background-color: #f5f5f5;">
|
||||
<th style="width: 25%;">Date</th>
|
||||
<th style="width: 30%;">Payment Method</th>
|
||||
<th style="width: 20%;">Card #</th>
|
||||
<th style="width: 25%;" class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="payments_vals" t-as="payment_vals">
|
||||
<tr t-if="not payment_vals.get('is_exchange')">
|
||||
<td>
|
||||
<t t-if="payment_vals.get('is_refund')">Reversed </t>
|
||||
<span t-out="payment_vals.get('date')" t-options='{"widget": "date"}'/>
|
||||
</td>
|
||||
<td>
|
||||
<span t-out="payment_vals.get('payment_method_name') or '-'"/>
|
||||
</td>
|
||||
<td>
|
||||
<!-- Get card info from payment record -->
|
||||
<t t-set="payment_rec" t-value="doc.env['account.payment'].sudo().browse(payment_vals.get('account_payment_id'))"/>
|
||||
<t t-if="payment_rec and payment_rec.x_fc_card_last_four">
|
||||
****<span t-esc="payment_rec.x_fc_card_last_four"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-out="payment_vals.get('amount')" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-if="doc.amount_residual and doc.amount_residual > 0 and doc.payment_state != 'paid'">
|
||||
<tr style="background-color: #fff3e0;">
|
||||
<td colspan="3"><strong>Amount Due</strong></td>
|
||||
<td class="text-end"><strong><span t-field="doc.amount_residual"/></strong></td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!-- Notes -->
|
||||
<t t-if="doc.narration">
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>Notes:</strong>
|
||||
<div t-field="doc.narration"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,342 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
Accessibility Equipment Purchase and Installation Agreement - Compact Layout
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_accessibility_contract">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-set="company" t-value="doc.company_id"/>
|
||||
|
||||
<style>
|
||||
.fc-contract { font-family: Arial, sans-serif; font-size: 8pt; line-height: 1.3; }
|
||||
.fc-contract h1 { color: #0066a1; font-size: 14pt; text-align: center; margin: 5px 0 10px 0; }
|
||||
.fc-contract h2 { color: #0066a1; font-size: 9pt; margin: 6px 0 3px 0; font-weight: bold; }
|
||||
.fc-contract h4 { color: #0066a1; margin: 0 0 10px 0; font-size: 13pt; }
|
||||
.fc-contract p { margin: 2px 0; text-align: justify; }
|
||||
.fc-contract .intro { margin-bottom: 8px; font-size: 8pt; }
|
||||
.fc-contract ul { margin: 2px 0 2px 15px; padding: 0; }
|
||||
.fc-contract li { margin-bottom: 1px; }
|
||||
.fc-contract table { width: 100%; border-collapse: collapse; }
|
||||
.fc-contract table.bordered, .fc-contract table.bordered th, .fc-contract table.bordered td { border: 1px solid #000; }
|
||||
.fc-contract th { background-color: #0066a1; color: white; padding: 4px 6px; font-weight: bold; text-align: center; font-size: 8pt; }
|
||||
.fc-contract td { padding: 3px 5px; vertical-align: top; font-size: 8pt; }
|
||||
.fc-contract .text-center { text-align: center; }
|
||||
.fc-contract .text-end { text-align: right; }
|
||||
.fc-contract .info-header { background-color: #f5f5f5; color: #333; }
|
||||
.fc-contract .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fc-contract .note-row { font-style: italic; }
|
||||
.fc-contract .totals-table { border: 1px solid #000; border-collapse: collapse; }
|
||||
.fc-contract .totals-table td { border: 1px solid #000; padding: 4px 6px; }
|
||||
|
||||
/* Two-column layout for terms */
|
||||
.fc-contract .terms-container { column-count: 2; column-gap: 20px; margin-top: 8px; }
|
||||
.fc-contract .term-section { break-inside: avoid; margin-bottom: 6px; }
|
||||
|
||||
/* Declaration */
|
||||
.fc-contract .declaration { margin-top: 12px; padding: 8px; background-color: #fff8e1; border: 2px solid #ffc107; }
|
||||
.fc-contract .declaration-title { font-weight: bold; font-size: 9pt; text-align: center; margin-bottom: 5px; }
|
||||
.fc-contract .declaration-text { font-size: 8pt; text-align: center; font-weight: bold; }
|
||||
|
||||
/* Clean signature layout */
|
||||
.fc-contract .signatures-container { margin-top: 15px; }
|
||||
.fc-contract .sig-row { display: table; width: 100%; margin-bottom: 20px; }
|
||||
.fc-contract .sig-col { display: table-cell; width: 48%; vertical-align: top; }
|
||||
.fc-contract .sig-spacer { display: table-cell; width: 4%; }
|
||||
.fc-contract .sig-title { font-weight: bold; font-size: 9pt; color: #0066a1; margin-bottom: 8px; border-bottom: 2px solid #0066a1; padding-bottom: 3px; }
|
||||
.fc-contract .sig-field { margin-bottom: 12px; }
|
||||
.fc-contract .sig-line { border-bottom: 1px solid #000; min-height: 25px; }
|
||||
.fc-contract .sig-label { font-size: 7pt; color: #666; margin-top: 2px; }
|
||||
</style>
|
||||
|
||||
<div class="fc-contract">
|
||||
<div class="page">
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- PAGE 1: CONTRACT DETAILS -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- Document Title -->
|
||||
<h4>Accessibility Contract <span t-field="doc.name"/></h4>
|
||||
|
||||
<!-- Address Table -->
|
||||
<table class="bordered" style="margin-bottom: 8px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">BILLING ADDRESS</th>
|
||||
<th style="width: 50%;">INSTALLATION ADDRESS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="height: 50px;">
|
||||
<div t-field="doc.partner_invoice_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="height: 50px;">
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Order Info Table -->
|
||||
<table class="bordered" style="margin-bottom: 8px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="info-header" style="width: 25%;">CONTRACT DATE</th>
|
||||
<th class="info-header" style="width: 25%;">VALID UNTIL</th>
|
||||
<th class="info-header" style="width: 25%;">SALESPERSON</th>
|
||||
<th class="info-header" style="width: 25%;">PAYMENT TERMS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.date_order" t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="doc.validity_date"><span t-field="doc.validity_date"/></t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center"><span t-field="doc.user_id"/></td>
|
||||
<td class="text-center"><span t-esc="doc.payment_term_id.name or '-'"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Order Lines Table -->
|
||||
<table class="bordered" style="margin-bottom: 8px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" style="width: 12%;">CODE</th>
|
||||
<th style="width: 43%;">DESCRIPTION</th>
|
||||
<th class="text-center" style="width: 8%;">QTY</th>
|
||||
<th class="text-center" style="width: 12%;">UNIT PRICE</th>
|
||||
<th class="text-center" style="width: 10%;">TAX</th>
|
||||
<th class="text-center" style="width: 15%;">AMOUNT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.order_line" t-as="line">
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<tr class="section-row">
|
||||
<td colspan="6"><span t-field="line.name"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row">
|
||||
<td colspan="6"><span t-field="line.name"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-elif="not line.display_type">
|
||||
<tr>
|
||||
<td class="text-center"><span t-esc="line.product_id.default_code or ''"/></td>
|
||||
<td>
|
||||
<t t-if="line.name">
|
||||
<t t-set="clean_name" t-value="line.name"/>
|
||||
<t t-if="'] ' in line.name">
|
||||
<t t-set="clean_name" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<t t-esc="clean_name"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
|
||||
</td>
|
||||
<td class="text-end"><span t-field="line.price_unit"/></td>
|
||||
<td class="text-center">
|
||||
<t t-esc="', '.join([(tax.invoice_label or tax.name) for tax in line.tax_ids]) or '-'"/>
|
||||
</td>
|
||||
<td class="text-end"><span t-field="line.price_subtotal"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Totals - Right Aligned -->
|
||||
<table style="width: 100%; margin-bottom: 10px;">
|
||||
<tr>
|
||||
<td style="width: 60%; vertical-align: top;">
|
||||
<t t-if="doc.payment_term_id.note">
|
||||
<strong>Payment Terms:</strong> <span t-field="doc.payment_term_id.note"/>
|
||||
</t>
|
||||
</td>
|
||||
<td style="width: 40%;">
|
||||
<table class="totals-table" style="width: 100%;">
|
||||
<tr>
|
||||
<td>Subtotal</td>
|
||||
<td class="text-end"><span t-field="doc.amount_untaxed"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Taxes</td>
|
||||
<td class="text-end"><span t-field="doc.amount_tax"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>TOTAL</strong></td>
|
||||
<td class="text-end"><strong><span t-field="doc.amount_total"/></strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Notes if any -->
|
||||
<t t-if="doc.note">
|
||||
<div style="margin-bottom: 8px; padding: 5px; background-color: #f9f9f9; border: 1px solid #ddd; font-size: 8pt;">
|
||||
<strong>Notes:</strong> <span t-field="doc.note"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- TERMS AND CONDITIONS - TWO COLUMN LAYOUT -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<h1 style="font-size: 11pt; margin: 10px 0 5px 0;">ACCESSIBILITY CONTRACT TERMS AND CONDITIONS</h1>
|
||||
|
||||
<div class="intro">
|
||||
<p>This Accessibility Contract ("Contract") is between <strong><t t-esc="company.name"/></strong> ("Company") and the Customer for the purchase, delivery, and installation of accessibility equipment including Stairlifts, Platform Lifts, Ceiling Lifts, Ramps, and Door Openers.</p>
|
||||
</div>
|
||||
|
||||
<div class="terms-container">
|
||||
|
||||
<div class="term-section">
|
||||
<h2>1. Scope of Contract</h2>
|
||||
<p>This Contract governs the sale, delivery, installation, and ownership of the equipment specified. By signing, the Customer agrees to all terms herein.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>2. Delivery</h2>
|
||||
<p>The Company will make reasonable efforts to deliver as agreed. Delivery dates are estimates, not guarantees. The Company is not liable for delays. The Customer cannot cancel due to delays unless agreed in writing.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>3. Deposits</h2>
|
||||
<p>A deposit of <strong>70%</strong> of total value (or as specified) is required at order. Upon receipt, equipment preparation begins. If cancelled, the Company may recover all costs incurred including materials, labor, and administrative expenses.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>4. Power Supply</h2>
|
||||
<p>The Customer must ensure a suitable power source is available at the installation site. Electrical outlet installation is not included unless stated in the order.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>5. Concrete Work (Porch Lifts)</h2>
|
||||
<p>For Porch Lifts requiring a concrete base, the Company provides specifications. Unless agreed otherwise, the Customer arranges and pays for concrete work. The Company is not liable for issues from third-party concrete work.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>6. Ownership and Title</h2>
|
||||
<p>Title remains with the Company until full payment. The Company may reclaim equipment upon non-payment or breach of this Contract.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>7. Warranty</h2>
|
||||
<p>Equipment is covered by limited warranty as specified. Warranty excludes: accidental damages, normal wear and tear, damages from misuse or lack of maintenance. Claims must be in writing.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>8. Returns and Refunds</h2>
|
||||
<p>Once submitted for production, orders are final and non-refundable. No returns permitted. Post-submission modifications incur additional charges at Company's discretion.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>9. Liability</h2>
|
||||
<p>The Company is not liable for injuries or damages from: misuse, unauthorized modifications, failure to follow instructions, lack of maintenance, or use beyond intended purpose. Customer indemnifies Company from related claims.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>10. Indemnification</h2>
|
||||
<p>Customer indemnifies Company from claims arising from installation site conditions including structural issues, electrical faults, or inadequate site preparation.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>11. Buying Back Goods</h2>
|
||||
<p>The Company does not buy back goods unless explicitly agreed in writing.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>12. Force Majeure</h2>
|
||||
<p>Company is not liable for delays from events beyond control: natural disasters, strikes, supply chain disruptions, or governmental actions.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>13. Entire Agreement</h2>
|
||||
<p>This Contract constitutes the entire understanding and supersedes all prior agreements, written or oral.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>14. Governing Law</h2>
|
||||
<p>This Contract is governed by laws of the jurisdiction where the Company operates.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>15. Acceptance</h2>
|
||||
<p>By signing, Customer acknowledges reading, understanding, and agreeing to all terms and conditions.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Declaration -->
|
||||
<div class="declaration">
|
||||
<div class="declaration-title">CUSTOMER DECLARATION</div>
|
||||
<div class="declaration-text">
|
||||
I HAVE READ, UNDERSTOOD AND AGREE TO ALL CONDITIONS SET FORTH IN THIS ACCESSIBILITY CONTRACT AND AGREE TO PAY THE AMOUNT OWED TO <t t-esc="company.name.upper()"/> AS SPECIFIED.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clean Signature Section - Side by Side -->
|
||||
<div class="signatures-container">
|
||||
<div class="sig-row">
|
||||
<!-- Customer Signature -->
|
||||
<div class="sig-col">
|
||||
<div class="sig-title">CUSTOMER</div>
|
||||
<div class="sig-field">
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-label">Full Name (Print)</div>
|
||||
</div>
|
||||
<div class="sig-field">
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-label">Signature</div>
|
||||
</div>
|
||||
<div class="sig-field">
|
||||
<div class="sig-line" style="width: 50%;"></div>
|
||||
<div class="sig-label">Date</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sig-spacer"></div>
|
||||
|
||||
<!-- Company Representative Signature -->
|
||||
<div class="sig-col">
|
||||
<div class="sig-title">FOR <t t-esc="company.name.upper()"/></div>
|
||||
<div class="sig-field">
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-label">Authorized Representative Name (Print)</div>
|
||||
</div>
|
||||
<div class="sig-field">
|
||||
<div class="sig-line"></div>
|
||||
<div class="sig-label">Signature</div>
|
||||
</div>
|
||||
<div class="sig-field">
|
||||
<div class="sig-line" style="width: 50%;"></div>
|
||||
<div class="sig-label">Date</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
184
fusion_claims/fusion_claims/report/report_actions.xml
Normal file
184
fusion_claims/fusion_claims/report/report_actions.xml
Normal file
@@ -0,0 +1,184 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<odoo>
|
||||
<!-- Landscape Paper Format -->
|
||||
<record id="paperformat_a4_landscape" model="report.paperformat">
|
||||
<field name="name">A4 Landscape (Fusion Central)</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="format">A4</field>
|
||||
<field name="orientation">Landscape</field>
|
||||
<field name="margin_top">20</field>
|
||||
<field name="margin_bottom">20</field>
|
||||
<field name="margin_left">7</field>
|
||||
<field name="margin_right">7</field>
|
||||
<field name="header_line" eval="False"/>
|
||||
<field name="header_spacing">20</field>
|
||||
<field name="dpi">90</field>
|
||||
</record>
|
||||
|
||||
<!-- Sale Order / Quotation Reports - Only Portrait -->
|
||||
<record id="action_report_saleorder_portrait" model="ir.actions.report">
|
||||
<field name="name">Quotation / Order (Portrait - ADP)</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_claims.report_saleorder_portrait</field>
|
||||
<field name="report_file">fusion_claims.report_saleorder_portrait</field>
|
||||
<field name="print_report_name">(object.state in ('draft', 'sent') and 'Quotation - %s' % object.name) or 'Order - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<!-- Landscape report - REMOVED FROM MENU (no binding) -->
|
||||
<record id="action_report_saleorder_landscape" model="ir.actions.report">
|
||||
<field name="name">Quotation / Order (Landscape - ADP)</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_claims.report_saleorder_landscape</field>
|
||||
<field name="report_file">fusion_claims.report_saleorder_landscape</field>
|
||||
<field name="print_report_name">'%s - %s' % (object.name, object.partner_id.name)</field>
|
||||
<!-- No binding_model_id - removed from print menu -->
|
||||
<field name="binding_type">report</field>
|
||||
<field name="paperformat_id" ref="paperformat_a4_landscape"/>
|
||||
</record>
|
||||
|
||||
<!-- Invoice Reports -->
|
||||
<record id="action_report_invoice_portrait" model="ir.actions.report">
|
||||
<field name="name">Invoice (Portrait)</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_claims.report_invoice_portrait</field>
|
||||
<field name="report_file">fusion_claims.report_invoice_portrait</field>
|
||||
<field name="print_report_name">'Invoice - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="account.model_account_move"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<!-- Landscape Invoice - REMOVED FROM MENU (no binding) -->
|
||||
<record id="action_report_invoice_landscape" model="ir.actions.report">
|
||||
<field name="name">Invoice (Landscape - ADP)</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_claims.report_invoice_landscape</field>
|
||||
<field name="report_file">fusion_claims.report_invoice_landscape</field>
|
||||
<field name="print_report_name">'Invoice - %s' % object.name</field>
|
||||
<!-- No binding_model_id - removed from print menu -->
|
||||
<field name="binding_type">report</field>
|
||||
<field name="paperformat_id" ref="paperformat_a4_landscape"/>
|
||||
</record>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- ADP Proof of Delivery Report -->
|
||||
<!-- =============================================================== -->
|
||||
<record id="action_report_proof_of_delivery" model="ir.actions.report">
|
||||
<field name="name">ADP Proof of Delivery</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_claims.report_proof_of_delivery</field>
|
||||
<field name="report_file">fusion_claims.report_proof_of_delivery</field>
|
||||
<field name="print_report_name">'ADP Proof of Delivery - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Standard Proof of Delivery Report -->
|
||||
<!-- =============================================================== -->
|
||||
<record id="action_report_proof_of_delivery_standard" model="ir.actions.report">
|
||||
<field name="name">Proof of Delivery</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_claims.report_proof_of_delivery_standard</field>
|
||||
<field name="report_file">fusion_claims.report_proof_of_delivery_standard</field>
|
||||
<field name="print_report_name">'Proof of Delivery - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Proof of Pickup Report (for rental returns) -->
|
||||
<!-- =============================================================== -->
|
||||
<record id="action_report_proof_of_pickup" model="ir.actions.report">
|
||||
<field name="name">Proof of Pickup</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_claims.report_proof_of_pickup</field>
|
||||
<field name="report_file">fusion_claims.report_proof_of_pickup</field>
|
||||
<field name="print_report_name">'Proof of Pickup - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Rental Agreement Report -->
|
||||
<!-- =============================================================== -->
|
||||
<record id="action_report_rental_agreement" model="ir.actions.report">
|
||||
<field name="name">Rental Agreement</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_claims.report_rental_agreement</field>
|
||||
<field name="report_file">fusion_claims.report_rental_agreement</field>
|
||||
<field name="print_report_name">'Rental Agreement - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Grab Bar Installation Waiver Report -->
|
||||
<!-- =============================================================== -->
|
||||
<record id="action_report_grab_bar_waiver" model="ir.actions.report">
|
||||
<field name="name">Grab Bar Installation Waiver</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_claims.report_grab_bar_waiver</field>
|
||||
<field name="report_file">fusion_claims.report_grab_bar_waiver</field>
|
||||
<field name="print_report_name">'Grab Bar Waiver - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Accessibility Equipment Contract Report -->
|
||||
<!-- =============================================================== -->
|
||||
<record id="action_report_accessibility_contract" model="ir.actions.report">
|
||||
<field name="name">Accessibility Equipment Contract</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_claims.report_accessibility_contract</field>
|
||||
<field name="report_file">fusion_claims.report_accessibility_contract</field>
|
||||
<field name="print_report_name">'Accessibility Contract - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- March of Dimes Quotation Report -->
|
||||
<!-- =============================================================== -->
|
||||
<record id="action_report_mod_quotation" model="ir.actions.report">
|
||||
<field name="name">March of Dimes Quotation</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_claims.report_mod_quotation</field>
|
||||
<field name="report_file">fusion_claims.report_mod_quotation</field>
|
||||
<field name="print_report_name">'MOD Quotation - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- March of Dimes Invoice Report -->
|
||||
<!-- =============================================================== -->
|
||||
<record id="action_report_mod_invoice" model="ir.actions.report">
|
||||
<field name="name">March of Dimes Invoice</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_claims.report_mod_invoice</field>
|
||||
<field name="report_file">fusion_claims.report_mod_invoice</field>
|
||||
<field name="print_report_name">'MOD Invoice - %s' % object.name</field>
|
||||
<field name="binding_model_id" ref="account.model_account_move"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
</odoo>
|
||||
184
fusion_claims/fusion_claims/report/report_grab_bar_waiver.xml
Normal file
184
fusion_claims/fusion_claims/report/report_grab_bar_waiver.xml
Normal file
@@ -0,0 +1,184 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
Grab Bar Installation Waiver and Liability Release Agreement
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_grab_bar_waiver">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-set="company" t-value="doc.company_id"/>
|
||||
|
||||
<style>
|
||||
.fc-waiver { font-family: Arial, sans-serif; font-size: 10pt; line-height: 1.5; }
|
||||
.fc-waiver h1 { color: #0066a1; font-size: 16pt; text-align: center; margin: 10px 0 20px 0; }
|
||||
.fc-waiver h2 { color: #333; font-size: 11pt; margin: 15px 0 8px 0; }
|
||||
.fc-waiver p { margin: 8px 0; text-align: justify; }
|
||||
.fc-waiver .intro { margin-bottom: 15px; font-style: italic; }
|
||||
.fc-waiver .section { margin-bottom: 15px; }
|
||||
.fc-waiver .section-title { font-weight: bold; font-size: 10pt; margin-bottom: 5px; }
|
||||
.fc-waiver .subsection { margin-left: 20px; margin-bottom: 8px; }
|
||||
.fc-waiver .subsection-title { font-weight: bold; }
|
||||
.fc-waiver .signature-section { margin-top: 30px; }
|
||||
.fc-waiver .signature-box { border: 1px solid #000; padding: 20px; margin-top: 15px; }
|
||||
.fc-waiver .signature-line { border-bottom: 1px solid #000; min-height: 40px; margin-bottom: 5px; }
|
||||
.fc-waiver .signature-label { font-size: 9pt; color: #666; margin-bottom: 20px; }
|
||||
.fc-waiver .acknowledgment { font-size: 9pt; margin: 20px 0; padding: 10px; background-color: #fff8e1; border-left: 3px solid #ffc107; }
|
||||
.fc-waiver table { width: 100%; border-collapse: collapse; }
|
||||
.fc-waiver .info-table td { padding: 5px 10px; }
|
||||
.fc-waiver .info-table th { background-color: #f5f5f5; color: #333; text-align: left; padding: 5px 10px; width: 30%; }
|
||||
</style>
|
||||
|
||||
<div class="fc-waiver">
|
||||
<div class="page">
|
||||
|
||||
<!-- Document Title -->
|
||||
<h1>GRAB BAR INSTALLATION WAIVER AND LIABILITY RELEASE AGREEMENT</h1>
|
||||
|
||||
<!-- Introduction -->
|
||||
<div class="intro">
|
||||
<p>This Waiver and Release Agreement ("Agreement") is entered into by and between the undersigned customer ("Customer") and <t t-esc="company.name"/> ("Installer") as of the date of signature below. By signing this Agreement, the Customer acknowledges and agrees to the following terms and conditions:</p>
|
||||
</div>
|
||||
|
||||
<!-- Customer Information -->
|
||||
<table class="info-table" style="border: 1px solid #ccc; margin-bottom: 20px;">
|
||||
<tr>
|
||||
<th style="border: 1px solid #ccc;">Customer Name</th>
|
||||
<td style="border: 1px solid #ccc;"><t t-esc="doc.partner_id.name"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="border: 1px solid #ccc;">Address</th>
|
||||
<td style="border: 1px solid #ccc;">
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['address'], 'no_marker': True}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="border: 1px solid #ccc;">Order Reference</th>
|
||||
<td style="border: 1px solid #ccc;"><t t-esc="doc.name"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Section 1: Scope of Work -->
|
||||
<div class="section">
|
||||
<div class="section-title">1. Scope of Work</div>
|
||||
<p>The Installer agrees to install grab bars as per the Customer's request. The Customer understands and acknowledges that the installation process may involve drilling into walls, tiles, or other surfaces, and the Installer will take reasonable care to perform the work professionally.</p>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Liability Waiver -->
|
||||
<div class="section">
|
||||
<div class="section-title">2. Liability Waiver</div>
|
||||
<p>To the fullest extent permitted by law, the Customer agrees to release, waive, and hold harmless the Installer, its employees, agents, and subcontractors from any and all liability, claims, demands, damages, or causes of action arising from or related to the installation of grab bars, including but not limited to:</p>
|
||||
|
||||
<div class="subsection">
|
||||
<p><span class="subsection-title">a. Water Leaks</span><br/>
|
||||
The Installer is not responsible for any water leaks that may occur in the wall or plumbing as a result of the installation process.</p>
|
||||
</div>
|
||||
|
||||
<div class="subsection">
|
||||
<p><span class="subsection-title">b. Rust or Corrosion</span><br/>
|
||||
The Installer is not liable for any rust, corrosion, or degradation of the grab bars over time due to environmental factors or improper maintenance.</p>
|
||||
</div>
|
||||
|
||||
<div class="subsection">
|
||||
<p><span class="subsection-title">c. Falls or Misuse</span><br/>
|
||||
The Installer is not responsible for any injuries, falls, or accidents resulting from the misuse or improper use of the grab bars after installation.</p>
|
||||
</div>
|
||||
|
||||
<div class="subsection">
|
||||
<p><span class="subsection-title">d. Change of Mind</span><br/>
|
||||
The Customer acknowledges that once the grab bars are installed, no refunds or returns will be provided under any circumstance.</p>
|
||||
</div>
|
||||
|
||||
<div class="subsection">
|
||||
<p><span class="subsection-title">e. Tile Damage</span><br/>
|
||||
The Installer is not responsible for any cracks, chips, or other damage to tiles or grout during the installation process.</p>
|
||||
</div>
|
||||
|
||||
<div class="subsection">
|
||||
<p><span class="subsection-title">f. Plumbing Damage</span><br/>
|
||||
The Installer is not liable for any damage to plumbing or hidden pipes within the wall during the installation process.</p>
|
||||
</div>
|
||||
|
||||
<div class="subsection">
|
||||
<p><span class="subsection-title">g. Structural Damage</span><br/>
|
||||
The Installer is not responsible for any structural damage to the house, including but not limited to damage to walls, floors, or support structures.</p>
|
||||
</div>
|
||||
|
||||
<div class="subsection">
|
||||
<p><span class="subsection-title">h. Floor Damage</span><br/>
|
||||
The Installer is not liable for any damage to flooring that may occur during the installation process.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 3: Order Cancellation Policy -->
|
||||
<div class="section">
|
||||
<div class="section-title">3. Order Cancellation Policy</div>
|
||||
<p>The Customer may cancel the order at any time <strong>before installation begins</strong> without incurring additional charges, except for any applicable delivery fees.</p>
|
||||
<p>If the Installer has already arrived at the installation location and the Customer decides to cancel the order, the Customer agrees to pay a delivery & installation fee.</p>
|
||||
</div>
|
||||
|
||||
<!-- Section 4: No Warranties -->
|
||||
<div class="section">
|
||||
<div class="section-title">4. No Warranties</div>
|
||||
<p>The Installer provides no warranties, express or implied, regarding the quality, durability, or fitness of the grab bars or the installation process. The grab bars are installed "as is" and at the Customer's own risk.</p>
|
||||
</div>
|
||||
|
||||
<!-- Section 5: Indemnification -->
|
||||
<div class="section">
|
||||
<div class="section-title">5. Indemnification</div>
|
||||
<p>The Customer agrees to indemnify and hold harmless the Installer, its employees, agents, and subcontractors from any claims, damages, or expenses (including attorney's fees) arising from or related to the installation of grab bars.</p>
|
||||
</div>
|
||||
|
||||
<!-- Section 6: Acknowledgement -->
|
||||
<div class="section">
|
||||
<div class="section-title">6. Acknowledgement</div>
|
||||
<p>By signing this Agreement, the Customer acknowledges that they have read, understood, and agreed to all terms and conditions outlined above. The Customer further acknowledges that they have had the opportunity to ask questions regarding the installation process and the terms of this Agreement.</p>
|
||||
</div>
|
||||
|
||||
<!-- Section 7: Governing Law -->
|
||||
<div class="section">
|
||||
<div class="section-title">7. Governing Law</div>
|
||||
<p>This Agreement shall be governed by and construed in accordance with the laws of the Province of Ontario.</p>
|
||||
</div>
|
||||
|
||||
<!-- Acknowledgment Box -->
|
||||
<div class="acknowledgment">
|
||||
<p style="margin: 0;"><strong>By signing this form, I acknowledge that I have read the waiver agreement and understand the terms and conditions set forth herein.</strong></p>
|
||||
</div>
|
||||
|
||||
<!-- Signature Section -->
|
||||
<div class="signature-section">
|
||||
<div class="signature-box">
|
||||
<table style="width: 100%; border: none;">
|
||||
<tr>
|
||||
<td style="width: 60%; padding: 15px; border: none;">
|
||||
<div class="signature-label">FULL NAME (PRINT)</div>
|
||||
<div class="signature-line"></div>
|
||||
</td>
|
||||
<td style="width: 40%; padding: 15px; border: none;">
|
||||
<div class="signature-label">DATE</div>
|
||||
<div class="signature-line"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" style="padding: 15px; border: none;">
|
||||
<div class="signature-label">SIGNATURE</div>
|
||||
<div class="signature-line" style="min-height: 60px;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
227
fusion_claims/fusion_claims/report/report_mod_invoice.xml
Normal file
227
fusion_claims/fusion_claims/report/report_mod_invoice.xml
Normal file
@@ -0,0 +1,227 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Fusion Claims - March of Dimes Invoice Report
|
||||
Meets PCA invoice requirements:
|
||||
- Billed to March of Dimes Canada
|
||||
- Vendor name, address, HST Number
|
||||
- Client details (delivery address)
|
||||
- Approved devices/equipment list with sections and notes
|
||||
- HVMP contribution amount with taxes
|
||||
- HVMP Reference Number
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_mod_invoice">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<!-- Get sale order for MOD fields -->
|
||||
<t t-set="so" t-value="doc.x_fc_source_sale_order_id"/>
|
||||
|
||||
<style>
|
||||
.fc-mod-inv { font-family: Arial, sans-serif; font-size: 10pt; }
|
||||
.fc-mod-inv table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
|
||||
.fc-mod-inv table.bordered, .fc-mod-inv table.bordered th, .fc-mod-inv table.bordered td { border: 1px solid #000; }
|
||||
.fc-mod-inv th { background-color: #1a5276; color: white; padding: 6px 8px; font-weight: bold; text-align: center; }
|
||||
.fc-mod-inv td { padding: 6px 8px; vertical-align: top; }
|
||||
.fc-mod-inv .text-center { text-align: center; }
|
||||
.fc-mod-inv .text-end { text-align: right; }
|
||||
.fc-mod-inv .text-start { text-align: left; }
|
||||
.fc-mod-inv .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fc-mod-inv .note-row { font-style: italic; color: #555; font-size: 9pt; }
|
||||
.fc-mod-inv h4 { color: #1a5276; margin: 0 0 15px 0; font-size: 16pt; }
|
||||
.fc-mod-inv .req-box { border: 2px solid #1a5276; padding: 8px 12px; margin: 6px 0; background-color: #fafafa; }
|
||||
</style>
|
||||
|
||||
<div class="fc-mod-inv">
|
||||
<div class="page">
|
||||
|
||||
<!-- Title -->
|
||||
<h4>
|
||||
INVOICE <span t-field="doc.name"/>
|
||||
</h4>
|
||||
|
||||
<!-- Billed To / Vendor Info -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">BILLED TO</th>
|
||||
<th style="width: 50%;">VENDOR</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="height: 80px; font-size: 11pt;">
|
||||
<div t-field="doc.partner_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="height: 80px; font-size: 11pt;">
|
||||
<strong t-field="doc.company_id.name"/><br/>
|
||||
<span t-if="doc.company_id.street" t-field="doc.company_id.street"/><br/>
|
||||
<t t-if="doc.company_id.street2">
|
||||
<span t-field="doc.company_id.street2"/><br/>
|
||||
</t>
|
||||
<span t-if="doc.company_id.city" t-field="doc.company_id.city"/>,
|
||||
<span t-if="doc.company_id.state_id" t-field="doc.company_id.state_id.name"/>
|
||||
<span t-if="doc.company_id.zip" t-field="doc.company_id.zip"/><br/>
|
||||
<t t-if="doc.company_id.vat">
|
||||
<strong>HST #:</strong> <span t-field="doc.company_id.vat"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Client (Delivery Address) and HVMP Details -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">DELIVERY ADDRESS (CLIENT)</th>
|
||||
<th style="width: 50%;">HVMP REFERENCE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size: 11pt;">
|
||||
<div t-if="doc.partner_shipping_id" t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
|
||||
<div t-elif="so" t-field="so.partner_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="font-size: 11pt;">
|
||||
<t t-if="so and so.x_fc_case_reference">
|
||||
<strong>HVMP Ref #:</strong>
|
||||
<span t-esc="so.x_fc_case_reference"/><br/>
|
||||
</t>
|
||||
<strong>Invoice Date:</strong>
|
||||
<span t-field="doc.invoice_date" t-options="{'widget': 'date'}"/><br/>
|
||||
<strong>Sale Order:</strong>
|
||||
<span t-if="so" t-esc="so.name"/>
|
||||
<span t-if="not so">-</span><br/>
|
||||
<t t-if="so and so.x_fc_case_worker">
|
||||
<strong>Case Worker:</strong>
|
||||
<span t-esc="so.x_fc_case_worker.name"/><br/>
|
||||
</t>
|
||||
<t t-if="so and so.x_fc_mod_payment_commitment">
|
||||
<strong>Payment Commitment:</strong>
|
||||
<span t-esc="'${:,.2f}'.format(so.x_fc_mod_payment_commitment)"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Approved Equipment / Invoice Lines (including sections and notes) -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 10%;">SKU</th>
|
||||
<th style="width: 35%;">APPROVED EQUIPMENT / DESCRIPTION</th>
|
||||
<th style="width: 8%;">QTY</th>
|
||||
<th style="width: 13%;">UNIT PRICE</th>
|
||||
<th style="width: 14%;">TAXES</th>
|
||||
<th style="width: 20%;">AMOUNT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.invoice_line_ids.sorted('sequence')" t-as="line">
|
||||
<!-- Skip payment term lines -->
|
||||
<t t-if="line.display_type == 'payment_term'"/>
|
||||
<!-- Section lines -->
|
||||
<t t-elif="line.display_type == 'line_section'">
|
||||
<tr class="section-row">
|
||||
<td colspan="6" style="padding: 4px 8px;">
|
||||
<strong t-esc="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Note lines -->
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row">
|
||||
<td colspan="6" style="padding: 2px 8px;">
|
||||
<em t-esc="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Product lines -->
|
||||
<t t-else="">
|
||||
<tr>
|
||||
<td class="text-center" style="font-size: 9pt;">
|
||||
<t t-if="line.product_id and line.product_id.default_code"
|
||||
t-esc="line.product_id.default_code"/>
|
||||
</td>
|
||||
<td>
|
||||
<strong t-if="line.product_id" t-esc="line.product_id.name"/>
|
||||
<t t-if="line.name and (not line.product_id or line.name != line.product_id.name)">
|
||||
<t t-set="clean_desc" t-value="line.name"/>
|
||||
<t t-if="'] ' in (line.name or '')">
|
||||
<t t-set="clean_desc" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<t t-if="not line.product_id or clean_desc != line.product_id.name">
|
||||
<br t-if="line.product_id"/>
|
||||
<span t-esc="clean_desc" style="font-size: 9pt; color: #555;"/>
|
||||
</t>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.quantity"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_unit"/>
|
||||
</td>
|
||||
<td class="text-center" style="font-size: 9pt;">
|
||||
<t t-foreach="line.tax_ids" t-as="tax">
|
||||
<span t-esc="tax.invoice_label or tax.name"/>
|
||||
<t t-if="not tax_last">, </t>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_subtotal"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Totals -->
|
||||
<div class="row">
|
||||
<div class="col-6"/>
|
||||
<div class="col-6">
|
||||
<table class="bordered">
|
||||
<tr>
|
||||
<td class="text-end" style="width: 60%;"><strong>Subtotal</strong></td>
|
||||
<td class="text-end" style="width: 40%;"><span t-field="doc.amount_untaxed"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-end">Taxes</td>
|
||||
<td class="text-end"><span t-field="doc.amount_tax"/></td>
|
||||
</tr>
|
||||
<tr style="font-size: 12pt;">
|
||||
<td class="text-end" style="background-color: #eaf2f8;">
|
||||
<strong>HVMP Contribution Amount</strong>
|
||||
</td>
|
||||
<td class="text-end" style="background-color: #eaf2f8;">
|
||||
<strong><span t-field="doc.amount_total"/></strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Note -->
|
||||
<div class="req-box mt-3">
|
||||
<strong>Payment Schedule (as per PCA):</strong>
|
||||
<ul style="margin: 4px 0 0 0;">
|
||||
<li>Initial Payment (90%): Upon receipt of this invoice</li>
|
||||
<li>Final Payment (10%): Upon receipt of signed proof of delivery and installation</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
250
fusion_claims/fusion_claims/report/report_mod_quotation.xml
Normal file
250
fusion_claims/fusion_claims/report/report_mod_quotation.xml
Normal file
@@ -0,0 +1,250 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Fusion Claims - March of Dimes Quotation Report
|
||||
Portrait format with estimated completion time
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_mod_quotation">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
|
||||
<style>
|
||||
.fc-mod { font-family: Arial, sans-serif; font-size: 10pt; }
|
||||
.fc-mod table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
|
||||
.fc-mod table.bordered, .fc-mod table.bordered th, .fc-mod table.bordered td { border: 1px solid #000; }
|
||||
.fc-mod th { background-color: #1a5276; color: white; padding: 6px 8px; font-weight: bold; text-align: center; }
|
||||
.fc-mod td { padding: 6px 8px; vertical-align: top; }
|
||||
.fc-mod .text-center { text-align: center; }
|
||||
.fc-mod .text-end { text-align: right; }
|
||||
.fc-mod .text-start { text-align: left; }
|
||||
.fc-mod .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fc-mod h4 { color: #1a5276; margin: 0 0 15px 0; font-size: 16pt; }
|
||||
.fc-mod .info-header { background-color: #eaf2f8; color: #333; }
|
||||
.fc-mod .mod-accent { color: #1a5276; font-weight: bold; }
|
||||
.fc-mod .highlight-box { border: 2px solid #1a5276; padding: 10px; margin: 10px 0; background-color: #eaf2f8; }
|
||||
</style>
|
||||
|
||||
<div class="fc-mod">
|
||||
<div class="page">
|
||||
|
||||
<!-- Document Title -->
|
||||
<h4>
|
||||
March of Dimes Quotation
|
||||
<span t-field="doc.name"/>
|
||||
</h4>
|
||||
|
||||
<!-- Address Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">CLIENT</th>
|
||||
<th style="width: 50%;">SERVICE ADDRESS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="height: 70px; font-size: 11pt;">
|
||||
<div t-field="doc.partner_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="height: 70px; font-size: 11pt;">
|
||||
<div t-if="doc.partner_shipping_id"
|
||||
t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
<div t-else=""
|
||||
t-field="doc.partner_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Order Info -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="info-header" style="width: 20%;">DATE</th>
|
||||
<th class="info-header" style="width: 20%;">SALESPERSON</th>
|
||||
<th class="info-header" style="width: 20%;">AUTHORIZER/OT</th>
|
||||
<th class="info-header" style="width: 20%;">PRODUCT TYPE</th>
|
||||
<th class="info-header" style="width: 20%;">EXPIRATION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.date_order" t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.user_id"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="doc.x_fc_authorizer_id" t-field="doc.x_fc_authorizer_id"/>
|
||||
<span t-else="">-</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-set="prod_type_label" t-value="dict(doc._fields['x_fc_mod_product_type'].selection or []).get(doc.x_fc_mod_product_type, '-')"/>
|
||||
<span t-esc="prod_type_label"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.validity_date"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Estimated Completion -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="info-header" style="width: 50%;">EST. COMPLETION TIME</th>
|
||||
<th class="info-header" style="width: 50%;">EST. COMPLETION DATE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-if="doc.x_fc_mod_estimated_weeks">
|
||||
<t t-esc="doc.x_fc_mod_estimated_weeks"/> weeks
|
||||
</span>
|
||||
<span t-else="">To be confirmed</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="doc.x_fc_estimated_completion_date"
|
||||
t-field="doc.x_fc_estimated_completion_date"
|
||||
t-options="{'widget': 'date'}"/>
|
||||
<span t-else="">To be confirmed</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Order Lines -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 10%;">SKU</th>
|
||||
<th style="width: 35%;">DESCRIPTION</th>
|
||||
<th style="width: 8%;">QTY</th>
|
||||
<th style="width: 15%;">UNIT PRICE</th>
|
||||
<th style="width: 12%;">TAX</th>
|
||||
<th style="width: 20%;">AMOUNT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.order_line" t-as="line">
|
||||
<t t-if="not line.display_type">
|
||||
<tr>
|
||||
<td class="text-center" style="font-size: 9pt;">
|
||||
<t t-esc="line.product_id.default_code or '-'"/>
|
||||
</td>
|
||||
<td>
|
||||
<strong t-field="line.product_id.name"/>
|
||||
<t t-if="line.name and line.name != line.product_id.name">
|
||||
<t t-set="clean_desc" t-value="line.name"/>
|
||||
<t t-if="'] ' in (line.name or '')">
|
||||
<t t-set="clean_desc" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<t t-if="clean_desc != line.product_id.name">
|
||||
<br/>
|
||||
<span t-esc="clean_desc" style="font-size: 9pt; color: #555;"/>
|
||||
</t>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="line.product_uom_qty"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_unit"/>
|
||||
</td>
|
||||
<td class="text-center" style="font-size: 9pt;">
|
||||
<t t-if="line.tax_ids">
|
||||
<t t-esc="', '.join(t.invoice_label or t.name for t in line.tax_ids)"/>
|
||||
</t>
|
||||
<span t-else="">-</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_subtotal"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<tr class="section-row">
|
||||
<td colspan="6"><span t-field="line.name"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_note'">
|
||||
<tr>
|
||||
<td colspan="6" style="font-style: italic;">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Totals -->
|
||||
<div class="row">
|
||||
<div class="col-6"/>
|
||||
<div class="col-6">
|
||||
<table class="bordered">
|
||||
<tr>
|
||||
<td class="text-end" style="width: 60%;"><strong>Subtotal</strong></td>
|
||||
<td class="text-end" style="width: 40%;"><span t-field="doc.amount_untaxed"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-end">Taxes</td>
|
||||
<td class="text-end"><span t-field="doc.amount_tax"/></td>
|
||||
</tr>
|
||||
<tr style="font-size: 12pt;">
|
||||
<td class="text-end" style="background-color: #eaf2f8;">
|
||||
<strong>Total</strong>
|
||||
</td>
|
||||
<td class="text-end" style="background-color: #eaf2f8;">
|
||||
<strong><span t-field="doc.amount_total"/></strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div t-if="doc.note" class="mt-3">
|
||||
<strong>Notes:</strong>
|
||||
<div t-field="doc.note"/>
|
||||
</div>
|
||||
|
||||
<!-- Terms and Conditions -->
|
||||
<div class="mt-4" style="font-size: 9pt; color: #555;">
|
||||
<strong>Terms & Conditions:</strong>
|
||||
<ul style="margin-top: 4px;">
|
||||
<li>This quotation is valid for 30 days from the date above unless otherwise specified.</li>
|
||||
<li>All drawings must be signed and accepted by the client before production begins.</li>
|
||||
<li>Estimated completion time is subject to manufacturer lead times and weather conditions.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Signature Block -->
|
||||
<div class="row mt-5">
|
||||
<div class="col-6">
|
||||
<div style="border-top: 1px solid #000; padding-top: 4px; width: 80%;">
|
||||
Client Signature / Date
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div style="border-top: 1px solid #000; padding-top: 4px; width: 80%;">
|
||||
Authorized Representative / Date
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
303
fusion_claims/fusion_claims/report/report_proof_of_delivery.xml
Normal file
303
fusion_claims/fusion_claims/report/report_proof_of_delivery.xml
Normal file
@@ -0,0 +1,303 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
ADP Proof of Delivery Document
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_proof_of_delivery">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-set="is_adp" t-value="doc.x_fc_is_adp_sale"/>
|
||||
|
||||
<style>
|
||||
.fc-pod { font-family: Arial, sans-serif; font-size: 10pt; }
|
||||
.fc-pod table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
|
||||
.fc-pod table.bordered, .fc-pod table.bordered th, .fc-pod table.bordered td { border: 1px solid #000; }
|
||||
.fc-pod th { background-color: #0066a1; color: white; padding: 6px 8px; font-weight: bold; font-size: 9pt; }
|
||||
.fc-pod td { padding: 5px 6px; vertical-align: top; font-size: 9pt; }
|
||||
.fc-pod .text-center { text-align: center; }
|
||||
.fc-pod .text-end { text-align: right; }
|
||||
.fc-pod .text-start { text-align: left; }
|
||||
.fc-pod .adp-bg { background-color: #e3f2fd; }
|
||||
.fc-pod .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fc-pod .note-row { font-style: italic; }
|
||||
.fc-pod h2 { color: #0066a1; margin: 8px 0; font-size: 16pt; }
|
||||
.fc-pod .info-table td { padding: 6px 10px; font-size: 10pt; }
|
||||
.fc-pod .info-table th { background-color: #f5f5f5; color: #333; font-size: 9pt; padding: 5px 10px; }
|
||||
.fc-pod .signature-section { margin-top: 20px; border: 1px solid #000; padding: 15px; }
|
||||
.fc-pod .signature-line { border-bottom: 1px solid #000; min-height: 30px; margin-bottom: 5px; }
|
||||
.fc-pod .signature-label { font-size: 8pt; color: #666; margin-bottom: 15px; }
|
||||
.fc-pod .policy-section { font-size: 8pt; margin-top: 15px; padding: 10px; background-color: #f9f9f9; border: 1px solid #ddd; }
|
||||
.fc-pod .policy-title { font-weight: bold; font-size: 9pt; margin-bottom: 5px; }
|
||||
.fc-pod .acknowledgment { font-size: 9pt; margin: 15px 0; padding: 10px; background-color: #fff8e1; border-left: 3px solid #ffc107; }
|
||||
</style>
|
||||
|
||||
<div class="fc-pod">
|
||||
<div class="page">
|
||||
|
||||
<!-- Document Title -->
|
||||
<h2 style="text-align: center; margin-bottom: 15px;">
|
||||
ADP PROOF OF DELIVERY
|
||||
</h2>
|
||||
|
||||
<!-- Order Reference -->
|
||||
<div style="text-align: center; margin-bottom: 15px; font-size: 11pt;">
|
||||
<strong>Order Reference:</strong> <span t-field="doc.name"/>
|
||||
<t t-if="doc.x_fc_claim_number">
|
||||
<span style="margin-left: 30px;"><strong>Claim Number:</strong> <span t-esc="doc.x_fc_claim_number"/></span>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Customer Info Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">CUSTOMER INFORMATION</th>
|
||||
<th style="width: 50%;">DELIVERY ADDRESS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="height: 60px; font-size: 10pt;">
|
||||
<div t-field="doc.partner_invoice_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="height: 60px; font-size: 10pt;">
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Order Info Table -->
|
||||
<table class="bordered info-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ORDER DATE</th>
|
||||
<th>DELIVERY DATE</th>
|
||||
<th>CLIENT TYPE</th>
|
||||
<th>SALES REP</th>
|
||||
<th>AUTHORIZER</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.date_order" t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="doc.x_fc_adp_delivery_date">
|
||||
<span t-field="doc.x_fc_adp_delivery_date"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_client_type or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.user_id"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="doc.x_fc_authorizer_id" t-field="doc.x_fc_authorizer_id"/>
|
||||
<span t-else="">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Products Delivered Table (NO PRICES) -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" style="width: 12%;">ADP CODE</th>
|
||||
<th class="text-start" style="width: 40%;">DESCRIPTION</th>
|
||||
<th class="text-center" style="width: 15%;">SERIAL #</th>
|
||||
<th t-if="is_adp" class="text-center" style="width: 8%;">PLACEMENT</th>
|
||||
<th class="text-center" style="width: 8%;">QTY</th>
|
||||
<th class="text-center" style="width: 17%;">DEVICE TYPE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.order_line" t-as="line">
|
||||
<!-- Section Header -->
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<tr class="section-row">
|
||||
<td t-att-colspan="'6' if is_adp else '5'">
|
||||
<strong><span t-field="line.name"/></strong>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Note Line -->
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row">
|
||||
<td t-att-colspan="'6' if is_adp else '5'">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Product Line -->
|
||||
<t t-elif="not line.display_type">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="line.product_id.x_fc_adp_device_code"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="line.name">
|
||||
<t t-set="clean_name" t-value="line.name"/>
|
||||
<t t-if="'] ' in line.name">
|
||||
<t t-set="clean_name" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<t t-esc="clean_name"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="line.x_fc_serial_number or ''"/>
|
||||
</td>
|
||||
<td t-if="is_adp" class="text-center">
|
||||
<span t-esc="line.x_fc_device_placement or 'N/A'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="line.x_fc_adp_device_type or ''"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Acknowledgment Section -->
|
||||
<div class="acknowledgment">
|
||||
<p style="margin: 0;">
|
||||
<strong>Full Refund Policy:</strong> <span t-out="doc.company_id.website or ''"/><t t-if="doc.company_id.website">/return-policy/</t>
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0;">
|
||||
By signing below, you are acknowledging that you had the opportunity to inspect the product and found it to be free from any reasonably discoverable defects. You are also agreeing that you had the opportunity to review the entire transaction and the refund policy on the back side of the page, and it has satisfactorily addressed any questions or concerns you had, and nothing has been promised to you that has not been put into writing. This Delivery Confirmation Form is hereby incorporated into this transaction.
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0; font-weight: bold; text-transform: uppercase;">
|
||||
I HAVE RECEIVED ALL OF THE PRODUCTS AND SERVICES PROMISED TO ME IN CONNECTION WITH THIS TRANSACTION AT THE TIME OF DELIVERY.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Signature Section -->
|
||||
<div class="signature-section">
|
||||
<t t-if="doc.x_fc_pod_signature">
|
||||
<!-- Captured Digital Signature -->
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="signature-label">PRINT NAME</div>
|
||||
<div class="signature-line" style="padding: 5px 0; font-size: 12pt; font-weight: bold;">
|
||||
<t t-esc="doc.x_fc_pod_client_name or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="signature-label">DATE</div>
|
||||
<div class="signature-line" style="padding: 5px 0; font-size: 12pt;">
|
||||
<t t-if="doc.x_fc_pod_signature_date">
|
||||
<span t-field="doc.x_fc_pod_signature_date"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div class="signature-label">SIGNATURE</div>
|
||||
<div style="min-height: 60px; padding: 5px; border-bottom: 1px solid #000;">
|
||||
<img t-att-src="'data:image/png;base64,' + doc.x_fc_pod_signature.decode('utf-8')"
|
||||
style="max-height: 80px; max-width: 300px;"
|
||||
alt="Client Signature"/>
|
||||
</div>
|
||||
<t t-if="doc.x_fc_pod_signed_by_user_id">
|
||||
<div style="font-size: 7pt; color: #666; margin-top: 5px;">
|
||||
Collected by: <t t-esc="doc.x_fc_pod_signed_by_user_id.name"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<!-- Empty signature lines for manual signing -->
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="signature-label">PRINT NAME</div>
|
||||
<div class="signature-line"></div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="signature-label">DATE</div>
|
||||
<div class="signature-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div class="signature-label">SIGNATURE</div>
|
||||
<div class="signature-line" style="min-height: 50px;"></div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Page Break for Policy -->
|
||||
<div style="page-break-before: always;"></div>
|
||||
|
||||
<!-- Refund Policy Page -->
|
||||
<h2 style="text-align: center; margin-bottom: 15px;">
|
||||
RETURN AND REFUND POLICY
|
||||
</h2>
|
||||
|
||||
<div class="policy-section" style="font-size: 9pt;">
|
||||
<p style="margin: 0 0 10px 0;">
|
||||
<strong><t t-esc="doc.company_id.name"/></strong> does not accept any returns or refunds for custom-built orders or any modified products.
|
||||
</p>
|
||||
<p style="margin: 0 0 10px 0;">
|
||||
The following policy applies to all <t t-esc="doc.company_id.name"/> customers:
|
||||
</p>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Change Order and Cancellation Policy</div>
|
||||
<p style="margin: 5px 0;">
|
||||
<t t-esc="doc.company_id.name"/> sells custom Mobility products according to customer specifications. Quote, and order acknowledgments must be reviewed for accuracy.
|
||||
</p>
|
||||
<p style="margin: 5px 0;">
|
||||
Customized, specially ordered, or fabricated mobility and accessibility devices are not returnable or eligible for a refund. We also do not accept returns or exchanges for items that have come in contact with the skin. This includes, but is not limited to, lift chairs, seating, and positioning products, including wheelchair cushions and back supports, TENS units, patient lifts, blood pressure units, breast pumps, bathroom safety items, pillows, bedding, and mattresses, bracing, and continence care products.
|
||||
</p>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Restocking Fees</div>
|
||||
<p style="margin: 5px 0;">
|
||||
Product or parts returns for warranty items (i.e. product defects) are subject to <t t-esc="doc.company_id.name"/>'s evaluation but not subject to a restocking fee.
|
||||
</p>
|
||||
<p style="margin: 5px 0;">
|
||||
<strong>30% Restocking Fee:</strong> Some products require a 30% restocking fee for returns. These include: Patient lifts, Hospital Beds, Transport Wheelchairs, Standard Rollators & Walkers
|
||||
</p>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Non-Returnable Items</div>
|
||||
<p style="margin: 5px 0;">
|
||||
There are certain products that are custom-made by the manufacturer and are non-returnable. These include:
|
||||
</p>
|
||||
<ul style="margin: 5px 0 5px 20px; padding: 0;">
|
||||
<li>Built to order items / Custom wheelchairs, Rigid ultra lightweight wheelchairs</li>
|
||||
<li>Type/Category 2, 3, 4 & 5 Wheelchair</li>
|
||||
<li>Tilt & Reclining wheelchairs</li>
|
||||
<li>Sports wheelchairs</li>
|
||||
<li>Custom Power Wheelchairs</li>
|
||||
<li>All Spare Parts</li>
|
||||
<li>Adaptive Strollers</li>
|
||||
<li>Geri chairs</li>
|
||||
<li>Custom-built lift chairs/Recliners, including those with upgraded fabrics, heat and massage, Power Pillow, footrest extension or left-hand control options are non-returnable.</li>
|
||||
<li>Custom-built stairlifts, porch-lifts, vertical platform lifts, custom ceiling lifts.</li>
|
||||
<li>Open-Box Items</li>
|
||||
<li>Wheelchair scales</li>
|
||||
</ul>
|
||||
<p style="margin: 5px 0;">
|
||||
<strong>For hygienic reasons, the following products are non-returnable:</strong> Bathroom Safety products, Incontinence products, Cushions and Backs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,289 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
Standard Proof of Delivery Document
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_proof_of_delivery_standard">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
|
||||
<style>
|
||||
.fc-pod { font-family: Arial, sans-serif; font-size: 10pt; }
|
||||
.fc-pod table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
|
||||
.fc-pod table.bordered, .fc-pod table.bordered th, .fc-pod table.bordered td { border: 1px solid #000; }
|
||||
.fc-pod th { background-color: #0066a1; color: white; padding: 6px 8px; font-weight: bold; font-size: 9pt; }
|
||||
.fc-pod td { padding: 5px 6px; vertical-align: top; font-size: 9pt; }
|
||||
.fc-pod .text-center { text-align: center; }
|
||||
.fc-pod .text-end { text-align: right; }
|
||||
.fc-pod .text-start { text-align: left; }
|
||||
.fc-pod .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fc-pod .note-row { font-style: italic; }
|
||||
.fc-pod h2 { color: #0066a1; margin: 8px 0; font-size: 16pt; }
|
||||
.fc-pod .info-table td { padding: 6px 10px; font-size: 10pt; }
|
||||
.fc-pod .info-table th { background-color: #f5f5f5; color: #333; font-size: 9pt; padding: 5px 10px; }
|
||||
.fc-pod .signature-section { margin-top: 20px; border: 1px solid #000; padding: 15px; }
|
||||
.fc-pod .signature-line { border-bottom: 1px solid #000; min-height: 30px; margin-bottom: 5px; }
|
||||
.fc-pod .signature-label { font-size: 8pt; color: #666; margin-bottom: 15px; }
|
||||
.fc-pod .policy-section { font-size: 8pt; margin-top: 15px; padding: 10px; background-color: #f9f9f9; border: 1px solid #ddd; }
|
||||
.fc-pod .policy-title { font-weight: bold; font-size: 9pt; margin-bottom: 5px; }
|
||||
.fc-pod .acknowledgment { font-size: 9pt; margin: 15px 0; padding: 10px; background-color: #fff8e1; border-left: 3px solid #ffc107; }
|
||||
</style>
|
||||
|
||||
<div class="fc-pod">
|
||||
<div class="page">
|
||||
|
||||
<!-- Document Title -->
|
||||
<h2 style="text-align: center; margin-bottom: 15px;">
|
||||
PROOF OF DELIVERY
|
||||
</h2>
|
||||
|
||||
<!-- Order Reference -->
|
||||
<div style="text-align: center; margin-bottom: 15px; font-size: 11pt;">
|
||||
<strong>Order Reference:</strong> <span t-field="doc.name"/>
|
||||
</div>
|
||||
|
||||
<!-- Customer Info Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">CUSTOMER INFORMATION</th>
|
||||
<th style="width: 50%;">DELIVERY ADDRESS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="height: 60px; font-size: 10pt;">
|
||||
<div t-field="doc.partner_invoice_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="height: 60px; font-size: 10pt;">
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Order Info Table -->
|
||||
<table class="bordered info-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ORDER DATE</th>
|
||||
<th>DELIVERY DATE</th>
|
||||
<th>SALES REP</th>
|
||||
<th>AUTHORIZER</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.date_order" t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="doc.commitment_date">
|
||||
<span t-field="doc.commitment_date" t-options="{'widget': 'date'}"/>
|
||||
</t>
|
||||
<t t-elif="doc.x_fc_adp_delivery_date">
|
||||
<span t-field="doc.x_fc_adp_delivery_date"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.user_id"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="doc.x_fc_authorizer_id" t-field="doc.x_fc_authorizer_id"/>
|
||||
<span t-else="">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Products Delivered Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" style="width: 15%;">PRODUCT CODE</th>
|
||||
<th class="text-start" style="width: 50%;">DESCRIPTION</th>
|
||||
<th class="text-center" style="width: 20%;">SERIAL #</th>
|
||||
<th class="text-center" style="width: 15%;">QTY</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.order_line" t-as="line">
|
||||
<!-- Section Header -->
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<tr class="section-row">
|
||||
<td colspan="4">
|
||||
<strong><span t-field="line.name"/></strong>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Note Line -->
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row">
|
||||
<td colspan="4">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Product Line -->
|
||||
<t t-elif="not line.display_type">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-esc="line.product_id.default_code or ''"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="line.name">
|
||||
<t t-set="clean_name" t-value="line.name"/>
|
||||
<t t-if="'] ' in line.name">
|
||||
<t t-set="clean_name" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<t t-esc="clean_name"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="line.x_fc_serial_number or ''"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Acknowledgment Section -->
|
||||
<div class="acknowledgment">
|
||||
<p style="margin: 0;">
|
||||
<strong>Full Refund Policy:</strong> <span t-out="doc.company_id.website or ''"/><t t-if="doc.company_id.website">/return-policy/</t>
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0;">
|
||||
By signing below, you are acknowledging that you had the opportunity to inspect the product and found it to be free from any reasonably discoverable defects. You are also agreeing that you had the opportunity to review the entire transaction and the refund policy on the back side of the page, and it has satisfactorily addressed any questions or concerns you had, and nothing has been promised to you that has not been put into writing. This Delivery Confirmation Form is hereby incorporated into this transaction.
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0; font-weight: bold; text-transform: uppercase;">
|
||||
I HAVE RECEIVED ALL OF THE PRODUCTS AND SERVICES PROMISED TO ME IN CONNECTION WITH THIS TRANSACTION AT THE TIME OF DELIVERY.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Signature Section -->
|
||||
<div class="signature-section">
|
||||
<t t-if="doc.x_fc_pod_signature">
|
||||
<!-- Captured Digital Signature -->
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="signature-label">PRINT NAME</div>
|
||||
<div class="signature-line" style="padding: 5px 0; font-size: 12pt; font-weight: bold;">
|
||||
<t t-out="doc.x_fc_pod_client_name or ''"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="signature-label">DATE</div>
|
||||
<div class="signature-line" style="padding: 5px 0; font-size: 12pt;">
|
||||
<t t-if="doc.x_fc_pod_signature_date">
|
||||
<span t-field="doc.x_fc_pod_signature_date"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div class="signature-label">SIGNATURE</div>
|
||||
<div style="min-height: 60px; padding: 5px; border-bottom: 1px solid #000;">
|
||||
<img t-att-src="'data:image/png;base64,' + doc.x_fc_pod_signature.decode('utf-8')"
|
||||
style="max-height: 80px; max-width: 300px;"
|
||||
alt="Client Signature"/>
|
||||
</div>
|
||||
<t t-if="doc.x_fc_pod_signed_by_user_id">
|
||||
<div style="font-size: 7pt; color: #666; margin-top: 5px;">
|
||||
Collected by: <t t-out="doc.x_fc_pod_signed_by_user_id.name"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<!-- Empty signature lines for manual signing -->
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="signature-label">PRINT NAME</div>
|
||||
<div class="signature-line"></div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="signature-label">DATE</div>
|
||||
<div class="signature-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div class="signature-label">SIGNATURE</div>
|
||||
<div class="signature-line" style="min-height: 50px;"></div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Page Break for Policy -->
|
||||
<div style="page-break-before: always;"></div>
|
||||
|
||||
<!-- Refund Policy Page -->
|
||||
<h2 style="text-align: center; margin-bottom: 15px;">
|
||||
RETURN AND REFUND POLICY
|
||||
</h2>
|
||||
|
||||
<div class="policy-section" style="font-size: 9pt;">
|
||||
<p style="margin: 0 0 10px 0;">
|
||||
<strong><t t-esc="doc.company_id.name"/></strong> does not accept any returns or refunds for custom-built orders or any modified products.
|
||||
</p>
|
||||
<p style="margin: 0 0 10px 0;">
|
||||
The following policy applies to all <t t-esc="doc.company_id.name"/> customers:
|
||||
</p>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Change Order and Cancellation Policy</div>
|
||||
<p style="margin: 5px 0;">
|
||||
<t t-esc="doc.company_id.name"/> sells custom Mobility products according to customer specifications. Quote, and order acknowledgments must be reviewed for accuracy.
|
||||
</p>
|
||||
<p style="margin: 5px 0;">
|
||||
Customized, specially ordered, or fabricated mobility and accessibility devices are not returnable or eligible for a refund. We also do not accept returns or exchanges for items that have come in contact with the skin. This includes, but is not limited to, lift chairs, seating, and positioning products, including wheelchair cushions and back supports, TENS units, patient lifts, blood pressure units, breast pumps, bathroom safety items, pillows, bedding, and mattresses, bracing, and continence care products.
|
||||
</p>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Restocking Fees</div>
|
||||
<p style="margin: 5px 0;">
|
||||
Product or parts returns for warranty items (i.e. product defects) are subject to <t t-esc="doc.company_id.name"/>'s evaluation but not subject to a restocking fee.
|
||||
</p>
|
||||
<p style="margin: 5px 0;">
|
||||
<strong>30% Restocking Fee:</strong> Some products require a 30% restocking fee for returns. These include: Patient lifts, Hospital Beds, Transport Wheelchairs, Standard Rollators & Walkers
|
||||
</p>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Non-Returnable Items</div>
|
||||
<p style="margin: 5px 0;">
|
||||
There are certain products that are custom-made by the manufacturer and are non-returnable. These include:
|
||||
</p>
|
||||
<ul style="margin: 5px 0 5px 20px; padding: 0;">
|
||||
<li>Built to order items / Custom wheelchairs, Rigid ultra lightweight wheelchairs</li>
|
||||
<li>Type/Category 2, 3, 4 & 5 Wheelchair</li>
|
||||
<li>Tilt & Reclining wheelchairs</li>
|
||||
<li>Sports wheelchairs</li>
|
||||
<li>Custom Power Wheelchairs</li>
|
||||
<li>All Spare Parts</li>
|
||||
<li>Adaptive Strollers</li>
|
||||
<li>Geri chairs</li>
|
||||
<li>Custom-built lift chairs/Recliners, including those with upgraded fabrics, heat and massage, Power Pillow, footrest extension or left-hand control options are non-returnable.</li>
|
||||
<li>Custom-built stairlifts, porch-lifts, vertical platform lifts, custom ceiling lifts.</li>
|
||||
<li>Open-Box Items</li>
|
||||
<li>Wheelchair scales</li>
|
||||
</ul>
|
||||
<p style="margin: 5px 0;">
|
||||
<strong>For hygienic reasons, the following products are non-returnable:</strong> Bathroom Safety products, Incontinence products, Cushions and Backs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
260
fusion_claims/fusion_claims/report/report_proof_of_pickup.xml
Normal file
260
fusion_claims/fusion_claims/report/report_proof_of_pickup.xml
Normal file
@@ -0,0 +1,260 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
Proof of Pickup Document (for rental returns)
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_proof_of_pickup">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
|
||||
<style>
|
||||
.fc-pop { font-family: Arial, sans-serif; font-size: 10pt; }
|
||||
.fc-pop table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
|
||||
.fc-pop table.bordered, .fc-pop table.bordered th, .fc-pop table.bordered td { border: 1px solid #000; }
|
||||
.fc-pop th { background-color: #2e7d32; color: white; padding: 6px 8px; font-weight: bold; font-size: 9pt; }
|
||||
.fc-pop td { padding: 5px 6px; vertical-align: top; font-size: 9pt; }
|
||||
.fc-pop .text-center { text-align: center; }
|
||||
.fc-pop .text-end { text-align: right; }
|
||||
.fc-pop .text-start { text-align: left; }
|
||||
.fc-pop .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fc-pop .note-row { font-style: italic; }
|
||||
.fc-pop h2 { color: #2e7d32; margin: 8px 0; font-size: 16pt; }
|
||||
.fc-pop .info-table td { padding: 6px 10px; font-size: 10pt; }
|
||||
.fc-pop .info-table th { background-color: #f5f5f5; color: #333; font-size: 9pt; padding: 5px 10px; }
|
||||
.fc-pop .signature-section { margin-top: 20px; border: 1px solid #000; padding: 15px; }
|
||||
.fc-pop .signature-line { border-bottom: 1px solid #000; min-height: 30px; margin-bottom: 5px; }
|
||||
.fc-pop .signature-label { font-size: 8pt; color: #666; margin-bottom: 15px; }
|
||||
.fc-pop .policy-section { font-size: 8pt; margin-top: 15px; padding: 10px; background-color: #f9f9f9; border: 1px solid #ddd; }
|
||||
.fc-pop .policy-title { font-weight: bold; font-size: 9pt; margin-bottom: 5px; }
|
||||
.fc-pop .acknowledgment { font-size: 9pt; margin: 15px 0; padding: 10px; background-color: #e8f5e9; border-left: 3px solid #4caf50; }
|
||||
</style>
|
||||
|
||||
<div class="fc-pop">
|
||||
<div class="page">
|
||||
|
||||
<!-- Document Title -->
|
||||
<h2 style="text-align: center; margin-bottom: 15px;">
|
||||
PROOF OF PICKUP
|
||||
</h2>
|
||||
|
||||
<!-- Order Reference -->
|
||||
<div style="text-align: center; margin-bottom: 15px; font-size: 11pt;">
|
||||
<strong>Order Reference:</strong> <span t-field="doc.name"/>
|
||||
</div>
|
||||
|
||||
<!-- Customer Info Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">CUSTOMER INFORMATION</th>
|
||||
<th style="width: 50%;">PICKUP ADDRESS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="height: 60px; font-size: 10pt;">
|
||||
<div t-field="doc.partner_invoice_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="height: 60px; font-size: 10pt;">
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address', 'phone'], 'no_marker': True}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Order Info Table -->
|
||||
<table class="bordered info-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ORDER DATE</th>
|
||||
<th>PICKUP DATE</th>
|
||||
<th>SALES REP</th>
|
||||
<th>AUTHORIZER</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.date_order" t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="doc.commitment_date">
|
||||
<span t-field="doc.commitment_date" t-options="{'widget': 'date'}"/>
|
||||
</t>
|
||||
<t t-elif="doc.x_fc_adp_delivery_date">
|
||||
<span t-field="doc.x_fc_adp_delivery_date"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.user_id"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="doc.x_fc_authorizer_id" t-field="doc.x_fc_authorizer_id"/>
|
||||
<span t-else="">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Products Picked Up Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" style="width: 15%;">PRODUCT CODE</th>
|
||||
<th class="text-start" style="width: 50%;">DESCRIPTION</th>
|
||||
<th class="text-center" style="width: 20%;">SERIAL #</th>
|
||||
<th class="text-center" style="width: 15%;">QTY</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.order_line" t-as="line">
|
||||
<!-- Section Header -->
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<tr class="section-row">
|
||||
<td colspan="4">
|
||||
<strong><span t-field="line.name"/></strong>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Note Line -->
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row">
|
||||
<td colspan="4">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Product Line -->
|
||||
<t t-elif="not line.display_type">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-esc="line.product_id.default_code or ''"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="line.name">
|
||||
<t t-set="clean_name" t-value="line.name"/>
|
||||
<t t-if="'] ' in line.name">
|
||||
<t t-set="clean_name" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<t t-esc="clean_name"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="line.x_fc_serial_number or ''"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Acknowledgment Section -->
|
||||
<div class="acknowledgment">
|
||||
<p style="margin: 0;">
|
||||
<strong>Equipment Return Acknowledgment</strong>
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0;">
|
||||
By signing below, you are confirming that you have returned the above listed equipment to <t t-esc="doc.company_id.name"/>. You acknowledge that all items listed have been picked up by our authorized representative, and you no longer have possession of these products. You are also confirming that the equipment was returned in the same condition as when it was delivered, normal wear and tear excepted, and that you are aware of any applicable charges related to the rental period or equipment condition.
|
||||
</p>
|
||||
<p style="margin: 10px 0 0 0; font-weight: bold; text-transform: uppercase;">
|
||||
I CONFIRM THAT ALL OF THE PRODUCTS LISTED ABOVE HAVE BEEN RETURNED AND PICKED UP BY AN AUTHORIZED REPRESENTATIVE OF <t t-esc="doc.company_id.name.upper()"/>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Signature Section -->
|
||||
<div class="signature-section">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="signature-label">PRINT NAME</div>
|
||||
<div class="signature-line"></div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="signature-label">DATE</div>
|
||||
<div class="signature-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div class="signature-label">SIGNATURE</div>
|
||||
<div class="signature-line" style="min-height: 50px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page Break for Policy -->
|
||||
<div style="page-break-before: always;"></div>
|
||||
|
||||
<!-- Return Policy Page -->
|
||||
<h2 style="text-align: center; margin-bottom: 15px;">
|
||||
RENTAL RETURN TERMS AND CONDITIONS
|
||||
</h2>
|
||||
|
||||
<div class="policy-section" style="font-size: 9pt;">
|
||||
<p style="margin: 0 0 10px 0;">
|
||||
<strong><t t-esc="doc.company_id.name"/></strong> - Rental Equipment Return Policy
|
||||
</p>
|
||||
<p style="margin: 0 0 10px 0;">
|
||||
The following terms apply to all rental equipment returns:
|
||||
</p>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Equipment Condition</div>
|
||||
<p style="margin: 5px 0;">
|
||||
All rental equipment must be returned in the same condition as when delivered, allowing for normal wear and tear. The customer is responsible for any damage beyond normal use. Equipment will be inspected upon return, and any damage or missing parts will be assessed and charged accordingly.
|
||||
</p>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Rental Period</div>
|
||||
<p style="margin: 5px 0;">
|
||||
Rental charges are calculated based on the agreed rental period. Early returns may be subject to minimum rental fees as specified in the rental agreement. Late returns may incur additional daily rental charges.
|
||||
</p>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Pickup and Scheduling</div>
|
||||
<p style="margin: 5px 0;">
|
||||
Equipment pickup must be scheduled in advance. The customer is responsible for ensuring the equipment is accessible and ready for pickup at the scheduled time. Missed pickup appointments may result in continued rental charges.
|
||||
</p>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Cleaning Requirements</div>
|
||||
<p style="margin: 5px 0;">
|
||||
Equipment should be returned clean and sanitized. Additional cleaning fees may apply if equipment is returned in an unsanitary condition.
|
||||
</p>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Damage Assessment</div>
|
||||
<p style="margin: 5px 0;">
|
||||
Upon pickup, our representative will conduct an inspection of the equipment. Any damage will be documented and the customer will be notified of repair or replacement costs. The following are examples of damage that may result in charges:
|
||||
</p>
|
||||
<ul style="margin: 5px 0 5px 20px; padding: 0;">
|
||||
<li>Broken or bent frames</li>
|
||||
<li>Torn or stained upholstery</li>
|
||||
<li>Missing parts or accessories</li>
|
||||
<li>Non-functional electrical components</li>
|
||||
<li>Excessive wear on wheels or tires</li>
|
||||
<li>Water damage or rust</li>
|
||||
<li>Modifications made without authorization</li>
|
||||
</ul>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Security Deposit</div>
|
||||
<p style="margin: 5px 0;">
|
||||
If a security deposit was collected at the time of rental, it will be refunded within 14 business days after the equipment has been inspected and cleared, less any applicable charges for damage, cleaning, or extended rental periods.
|
||||
</p>
|
||||
|
||||
<div class="policy-title" style="margin-top: 15px;">Contact Information</div>
|
||||
<p style="margin: 5px 0;">
|
||||
For questions regarding your rental return or any charges, please contact <t t-esc="doc.company_id.name"/> customer service.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
340
fusion_claims/fusion_claims/report/report_rental_agreement.xml
Normal file
340
fusion_claims/fusion_claims/report/report_rental_agreement.xml
Normal file
@@ -0,0 +1,340 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
Rental Agreement Document - Compact 2-Page Layout
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_rental_agreement">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-set="company" t-value="doc.company_id"/>
|
||||
|
||||
<style>
|
||||
.fc-rental { font-family: Arial, sans-serif; font-size: 8pt; line-height: 1.3; }
|
||||
.fc-rental h1 { color: #0066a1; font-size: 14pt; text-align: center; margin: 5px 0 10px 0; }
|
||||
.fc-rental h2 { color: #0066a1; font-size: 9pt; margin: 6px 0 3px 0; font-weight: bold; }
|
||||
.fc-rental p { margin: 2px 0; text-align: justify; }
|
||||
.fc-rental .parties { font-size: 8pt; margin-bottom: 8px; }
|
||||
.fc-rental .intro { margin-bottom: 8px; font-size: 8pt; }
|
||||
.fc-rental table { width: 100%; border-collapse: collapse; }
|
||||
.fc-rental table.bordered, .fc-rental table.bordered th, .fc-rental table.bordered td { border: 1px solid #000; }
|
||||
.fc-rental th { background-color: #0066a1; color: white; padding: 4px 6px; font-weight: bold; font-size: 8pt; }
|
||||
.fc-rental td { padding: 3px 5px; vertical-align: top; font-size: 8pt; }
|
||||
.fc-rental .text-center { text-align: center; }
|
||||
.fc-rental .info-header { background-color: #f5f5f5; color: #333; font-weight: bold; }
|
||||
|
||||
/* Two-column layout for terms */
|
||||
.fc-rental .terms-container { column-count: 2; column-gap: 20px; margin-top: 10px; }
|
||||
.fc-rental .term-section { break-inside: avoid; margin-bottom: 8px; }
|
||||
|
||||
/* Credit card section - 15% taller */
|
||||
.fc-rental .cc-section { margin-top: 12px; padding: 12px; border: 2px solid #0066a1; background-color: #f8f9fa; }
|
||||
.fc-rental .cc-title { font-size: 10pt; font-weight: bold; color: #0066a1; margin-bottom: 10px; text-align: center; }
|
||||
.fc-rental .cc-box { border: 1px solid #000; display: inline-block; width: 21px; height: 21px; text-align: center; background: white; }
|
||||
.fc-rental .authorization-text { font-size: 7pt; margin-top: 10px; font-style: italic; }
|
||||
|
||||
/* Signature - 40% taller */
|
||||
.fc-rental .signature-section { margin-top: 15px; }
|
||||
.fc-rental .signature-box { border: 1px solid #000; padding: 12px; }
|
||||
.fc-rental .signature-line { border-bottom: 1px solid #000; min-height: 35px; margin-bottom: 5px; }
|
||||
.fc-rental .signature-label { font-size: 7pt; color: #666; }
|
||||
</style>
|
||||
|
||||
<div class="fc-rental">
|
||||
<div class="page">
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- PAGE 1: TERMS AND CONDITIONS -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<h1>RENTAL AGREEMENT</h1>
|
||||
|
||||
<!-- Parties - Compact -->
|
||||
<div class="parties">
|
||||
<strong>BETWEEN:</strong> <t t-esc="company.name"/> ("Company")
|
||||
<strong style="margin-left: 20px;">AND:</strong> <t t-esc="doc.partner_id.name"/> ("Renter")
|
||||
</div>
|
||||
|
||||
<!-- Introduction -->
|
||||
<div class="intro">
|
||||
<p><t t-esc="company.name"/> rents to the Renter medical equipment (hospital beds, patient lifts, trapeze, over-bed tables, mobility scooters, electric wheelchairs, manual wheelchairs, stairlifts, ceiling lifts and lift chairs) subject to the terms and conditions set forth in this Rental Agreement.</p>
|
||||
</div>
|
||||
|
||||
<!-- Terms and Conditions in Two Columns -->
|
||||
<div class="terms-container">
|
||||
|
||||
<div class="term-section">
|
||||
<h2>1. Ownership and Condition of Equipment</h2>
|
||||
<p>The medical equipment is the property of <t t-esc="company.name"/> and is provided in good condition. The Renter shall return the equipment in the same condition as when received, subject to normal wear and tear. <t t-esc="company.name"/> reserves the right to inspect the equipment upon its return and may repossess it without prior notice if it is being used in violation of this agreement.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>2. Cancellation Policy</h2>
|
||||
<p>The Renter may cancel the order before delivery and will be charged twenty-five percent (25%) of the total rental cost. If the order is canceled during the rental period after delivery, no refund will be provided.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>3. Security Deposit</h2>
|
||||
<p>The security deposit will be returned after an inspection of the equipment. If the equipment has any damage, the cost of repairs will be deducted from the security deposit. If the security deposit is insufficient to cover the damages, the credit card on file will be charged for the remaining amount. Security deposit refunds may take 4 to 15 business days to process. <t t-esc="company.name"/> is not responsible for delays caused by the Renter's financial institution.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>4. Liability for Loss or Damage</h2>
|
||||
<p><t t-esc="company.name"/> shall not be liable for any loss of or damage to property left, lost, damaged, stolen, stored, or transported by the Renter or any other person using the medical equipment. The Renter assumes all risks associated with such loss or damage and waives any claims against <t t-esc="company.name"/>. The Renter agrees to defend, indemnify, and hold <t t-esc="company.name"/> harmless against all claims arising from such loss or damage.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>5. Risk and Liability</h2>
|
||||
<p>The Renter assumes all risk and liability for any loss, damage, injury, or death resulting from the use or operation of the medical equipment. <t t-esc="company.name"/> is not responsible for any acts or omissions of the Renter or the Renter's agents, servants, or employees.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>6. Renter Responsibilities</h2>
|
||||
<p>The Renter is responsible for the full cost of replacement for any damage, loss, theft, or destruction of the medical equipment. <t t-esc="company.name"/> may charge the Renter's credit card for repair or replacement costs as deemed necessary. The equipment must not be used by individuals under the age of 18, under the influence of intoxicants or narcotics, or in an unsafe manner.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>7. Indemnification</h2>
|
||||
<p>The Renter shall indemnify, defend, and hold harmless <t t-esc="company.name"/>, its agents, officers, and employees, from any claims, demands, actions, or causes of action arising from the use or operation of the medical equipment, except where caused by <t t-esc="company.name"/>'s gross negligence or willful misconduct.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>8. Accident Notification</h2>
|
||||
<p>The Renter must immediately notify <t t-esc="company.name"/> of any accidents, damages, or incidents involving the medical equipment.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>9. Costs and Expenses</h2>
|
||||
<p>The Renter agrees to cover all costs, expenses, and attorney's fees incurred by <t t-esc="company.name"/> in collecting overdue payments, recovering possession of the equipment, or enforcing claims for damage or loss.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>10. Independent Status</h2>
|
||||
<p>The Renter or any driver of the equipment shall not be considered an agent or employee of <t t-esc="company.name"/>.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>11. Binding Obligations</h2>
|
||||
<p>Any individual signing this agreement on behalf of a corporation or other entity shall be personally liable for all obligations under this agreement. This agreement is binding upon the heirs, executors, administrators, and assigns of the Renter.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>12. Refusal of Service</h2>
|
||||
<p><t t-esc="company.name"/> reserves the right to refuse rental to any individual or entity at its sole discretion.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>13. Governing Law</h2>
|
||||
<p>This Agreement shall be governed by and construed in accordance with the laws of the jurisdiction in which <t t-esc="company.name"/> operates.</p>
|
||||
</div>
|
||||
|
||||
<div class="term-section">
|
||||
<h2>14. Entire Agreement</h2>
|
||||
<p>This Agreement constitutes the entire understanding between the parties concerning the rental of medical equipment and supersedes all prior agreements, representations, or understandings, whether written or oral.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- PAGE 2: RENTAL DETAILS, PAYMENT, AND SIGNATURE -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<div style="page-break-before: always;"></div>
|
||||
|
||||
<h1>RENTAL DETAILS</h1>
|
||||
|
||||
<!-- Customer Info and Rental Period Side by Side -->
|
||||
<table style="width: 100%; margin-bottom: 10px;">
|
||||
<tr>
|
||||
<td style="width: 50%; vertical-align: top; padding-right: 10px;">
|
||||
<table class="bordered" style="width: 100%;">
|
||||
<tr>
|
||||
<th colspan="2" class="info-header" style="background-color: #0066a1; color: white;">RENTER INFORMATION</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 35%; font-weight: bold; background-color: #f5f5f5;">Name</td>
|
||||
<td><t t-esc="doc.partner_id.name"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold; background-color: #f5f5f5;">Address</td>
|
||||
<td>
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['address'], 'no_marker': True}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold; background-color: #f5f5f5;">Phone</td>
|
||||
<td><t t-esc="doc.partner_id.phone or doc.partner_id.mobile or ''"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold; background-color: #f5f5f5;">Order Ref</td>
|
||||
<td><t t-esc="doc.name"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td style="width: 50%; vertical-align: top; padding-left: 10px;">
|
||||
<table class="bordered" style="width: 100%;">
|
||||
<tr>
|
||||
<th colspan="2" class="info-header" style="background-color: #0066a1; color: white;">RENTAL PERIOD</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 40%; font-weight: bold; background-color: #f5f5f5;">Start Date</td>
|
||||
<td>
|
||||
<t t-if="doc.rental_start_date">
|
||||
<span t-field="doc.rental_start_date" t-options="{'widget': 'date'}"/>
|
||||
</t>
|
||||
<t t-else=""><span style="color: #999;">Not specified</span></t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold; background-color: #f5f5f5;">Return Date</td>
|
||||
<td>
|
||||
<t t-if="doc.rental_return_date">
|
||||
<span t-field="doc.rental_return_date" t-options="{'widget': 'date'}"/>
|
||||
</t>
|
||||
<t t-else=""><span style="color: #999;">Not specified</span></t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold; background-color: #f5f5f5;">Duration</td>
|
||||
<td>
|
||||
<t t-if="doc.duration_days">
|
||||
<span t-esc="doc.duration_days"/> Day<t t-if="doc.duration_days != 1">s</t>
|
||||
<t t-if="doc.remaining_hours and doc.remaining_hours > 0">
|
||||
, <t t-esc="doc.remaining_hours"/> Hr<t t-if="doc.remaining_hours != 1">s</t>
|
||||
</t>
|
||||
</t>
|
||||
<t t-elif="doc.rental_start_date and doc.rental_return_date"><span>Less than 1 day</span></t>
|
||||
<t t-else=""><span style="color: #999;">Not specified</span></t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold; background-color: #f5f5f5;">Total Amount</td>
|
||||
<td><strong><span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/></strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Equipment List - Compact -->
|
||||
<table class="bordered" style="margin-bottom: 10px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" style="width: 15%;">PRODUCT CODE</th>
|
||||
<th style="width: 55%;">DESCRIPTION</th>
|
||||
<th class="text-center" style="width: 15%;">SERIAL #</th>
|
||||
<th class="text-center" style="width: 15%;">QTY</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.order_line" t-as="line">
|
||||
<t t-if="not line.display_type">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-esc="line.product_id.default_code or ''"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="line.name">
|
||||
<t t-set="clean_name" t-value="line.name"/>
|
||||
<t t-if="'] ' in clean_name">
|
||||
<t t-set="clean_name" t-value="clean_name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<t t-if="' to ' in clean_name and '\n' in clean_name">
|
||||
<t t-set="clean_name" t-value="clean_name.split('\n')[0]"/>
|
||||
</t>
|
||||
<t t-esc="clean_name"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="line.x_fc_serial_number or ''"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Credit Card Authorization - Compact -->
|
||||
<div class="cc-section">
|
||||
<div class="cc-title">CREDIT CARD PAYMENT AUTHORIZATION</div>
|
||||
|
||||
<table style="width: 100%; border: none;">
|
||||
<tr>
|
||||
<td style="width: 20%; padding: 5px 4px; border: none;"><strong>Card #:</strong></td>
|
||||
<td style="padding: 5px 4px; border: none;">
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin: 0 3px;">-</span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin: 0 3px;">-</span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin: 0 3px;">-</span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px 4px; border: none;"><strong>Exp Date:</strong></td>
|
||||
<td style="padding: 5px 4px; border: none;">
|
||||
<span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin: 0 2px;">/</span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin-left: 20px;"><strong>CVV:</strong></span>
|
||||
<span class="cc-box"></span><span class="cc-box"></span><span class="cc-box"></span>
|
||||
<span style="margin-left: 20px;"><strong>Security Deposit:</strong> $___________</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px 4px; border: none;"><strong>Cardholder:</strong></td>
|
||||
<td style="padding: 5px 4px; border: none;">
|
||||
<div style="border-bottom: 1px solid #000; min-height: 18px; width: 100%;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" style="padding: 5px 4px; border: none;">
|
||||
<strong>Billing Address (if different):</strong>
|
||||
<div style="border-bottom: 1px solid #000; min-height: 18px; width: 100%; margin-top: 4px;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="authorization-text">
|
||||
<p>I authorize <t t-esc="company.name"/> to charge the credit card indicated in this authorization form according to the terms outlined above. I certify that I am an authorized user of this credit card and will not dispute the payment. By signing this form, I acknowledge that I have read the rental agreement and understand the terms and conditions. I understand that if the rented item is not returned on the agreed return date, additional charges will be incurred. *Payments for monthly rental items will be charged on the re-rental date until the item is returned.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signature Section - Compact -->
|
||||
<div class="signature-section">
|
||||
<div class="signature-box">
|
||||
<table style="width: 100%; border: none;">
|
||||
<tr>
|
||||
<td style="width: 40%; padding: 5px; border: none;">
|
||||
<div class="signature-label">FULL NAME (PRINT)</div>
|
||||
<div class="signature-line"></div>
|
||||
</td>
|
||||
<td style="width: 40%; padding: 5px; border: none;">
|
||||
<div class="signature-label">SIGNATURE</div>
|
||||
<div class="signature-line"></div>
|
||||
</td>
|
||||
<td style="width: 20%; padding: 5px; border: none;">
|
||||
<div class="signature-label">DATE</div>
|
||||
<div class="signature-line"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
203
fusion_claims/fusion_claims/report/report_templates.xml
Normal file
203
fusion_claims/fusion_claims/report/report_templates.xml
Normal file
@@ -0,0 +1,203 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<odoo>
|
||||
<!-- Shared Report Header Template -->
|
||||
<template id="report_header_fusion_claims">
|
||||
<div class="fc-header">
|
||||
<div class="row">
|
||||
<!-- Company Logo -->
|
||||
<div class="col-3">
|
||||
<img t-if="company.logo" t-att-src="image_data_uri(company.logo)"
|
||||
style="max-height: 80px; max-width: 200px;" alt="Company Logo"/>
|
||||
</div>
|
||||
<!-- Company Info -->
|
||||
<div class="col-9 text-end">
|
||||
<h3 class="mb-0" t-field="company.name"/>
|
||||
<div t-if="company.x_fc_store_address_1" class="small">
|
||||
<t t-esc="company.x_fc_store_address_1"/>
|
||||
</div>
|
||||
<div t-if="company.x_fc_store_address_2" class="small">
|
||||
<t t-esc="company.x_fc_store_address_2"/>
|
||||
</div>
|
||||
<div t-if="company.x_fc_company_tagline" class="small fst-italic text-muted">
|
||||
<t t-esc="company.x_fc_company_tagline"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Address Boxes Template -->
|
||||
<template id="report_address_boxes">
|
||||
<div class="row mt-4">
|
||||
<!-- Billing Address -->
|
||||
<div class="col-6">
|
||||
<div class="border p-2">
|
||||
<strong>BILLING ADDRESS</strong>
|
||||
<div t-field="doc.partner_id"
|
||||
t-options='{"widget": "contact", "fields": ["name", "address"], "no_marker": True}'/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delivery Address -->
|
||||
<div class="col-6">
|
||||
<div class="border p-2">
|
||||
<strong>DELIVERY ADDRESS</strong>
|
||||
<div t-if="doc.partner_shipping_id"
|
||||
t-field="doc.partner_shipping_id"
|
||||
t-options='{"widget": "contact", "fields": ["name", "address"], "no_marker": True}'/>
|
||||
<div t-else="" t-field="doc.partner_id"
|
||||
t-options='{"widget": "contact", "fields": ["name", "address"], "no_marker": True}'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Serial Numbers Section Template -->
|
||||
<template id="report_serial_numbers">
|
||||
<t t-set="serials" t-value="[]"/>
|
||||
<!-- Collect serial numbers from order lines -->
|
||||
<t t-if="doc._name == 'sale.order'">
|
||||
<t t-foreach="doc.order_line" t-as="line">
|
||||
<t t-if="line.x_fc_serial_number">
|
||||
<t t-set="serials" t-value="serials + [{'product': line.product_id.name, 'serial': line.x_fc_serial_number}]"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="doc._name == 'account.move'">
|
||||
<t t-foreach="doc.invoice_line_ids" t-as="line">
|
||||
<t t-if="line.x_fc_serial_number">
|
||||
<t t-set="serials" t-value="serials + [{'product': line.product_id.name, 'serial': line.x_fc_serial_number}]"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<div t-if="serials" class="mt-3 border p-2">
|
||||
<strong>SERIAL NUMBERS</strong>
|
||||
<ul class="mb-0 ps-3">
|
||||
<li t-foreach="serials" t-as="s">
|
||||
<t t-esc="s['product']"/>: <strong t-esc="s['serial']"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Payment Terms Template -->
|
||||
<template id="report_payment_terms">
|
||||
<div t-if="company.x_fc_payment_terms_html" class="mt-3">
|
||||
<strong>Payment Communication: </strong><span t-esc="doc.name"/>
|
||||
<div class="mt-2" t-raw="company.x_fc_payment_terms_html"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Refund Policy Page Template -->
|
||||
<template id="report_refund_policy_page">
|
||||
<div t-if="company.x_fc_include_refund_page and company.x_fc_refund_policy_html"
|
||||
class="page" style="page-break-before: always;">
|
||||
<t t-call="fusion_claims.report_header_fusion_claims"/>
|
||||
<div class="mt-4">
|
||||
<t t-raw="company.x_fc_refund_policy_html"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Footer Template -->
|
||||
<template id="report_footer_fusion_claims">
|
||||
<div class="fc-footer border-top pt-2 mt-3">
|
||||
<div class="row small">
|
||||
<div class="col-12 text-center">
|
||||
<span t-if="company.phone">Call & Fax: <t t-esc="company.phone"/></span>
|
||||
<span t-if="company.email"> | <t t-esc="company.email"/></span>
|
||||
<span t-if="company.vat"> | HST # <t t-esc="company.vat"/></span>
|
||||
<span t-if="company.website"> | Shop online: <t t-esc="company.website"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Report Styles -->
|
||||
<template id="report_styles_fusion_claims">
|
||||
<style>
|
||||
.fc-header { margin-bottom: 20px; }
|
||||
.fc-footer { margin-top: 20px; }
|
||||
.fc-table { width: 100%; border-collapse: collapse; }
|
||||
.fc-table th { background-color: #0077b6; color: white; padding: 8px; text-align: left; }
|
||||
.fc-table td { padding: 6px; border-bottom: 1px solid #ddd; }
|
||||
.fc-table .section-header { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fc-table .text-end { text-align: right; }
|
||||
.fc-totals { margin-top: 20px; }
|
||||
.fc-totals table { width: 300px; float: right; }
|
||||
.fc-totals td { padding: 4px 8px; }
|
||||
.fc-totals .total-row { font-weight: bold; background-color: #0077b6; color: white; }
|
||||
</style>
|
||||
</template>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- Default Invoice Report: Add SKU column, clean description -->
|
||||
<!-- ================================================================= -->
|
||||
<template id="report_invoice_sku_column"
|
||||
inherit_id="account.report_invoice_document">
|
||||
|
||||
<!-- 1. Add SKU header before Description -->
|
||||
<xpath expr="//th[@name='th_description']" position="before">
|
||||
<th name="th_sku" class="text-start" style="width: 12%;"><span>SKU</span></th>
|
||||
</xpath>
|
||||
|
||||
<!-- 2. Add SKU cell before description for product lines -->
|
||||
<xpath expr="//td[@name='account_invoice_line_name']" position="before">
|
||||
<td name="td_sku" class="text-start">
|
||||
<span t-esc="line.product_id.default_code or ''"/>
|
||||
</td>
|
||||
</xpath>
|
||||
|
||||
<!-- 3. Replace description to strip [internal_ref] prefix -->
|
||||
<xpath expr="//td[@name='account_invoice_line_name']" position="replace">
|
||||
<td name="account_invoice_line_name" t-att-class="padding_class">
|
||||
<t t-if="line.name">
|
||||
<t t-set="clean_desc" t-value="line.name"/>
|
||||
<t t-if="'] ' in (line.name or '')">
|
||||
<t t-set="clean_desc" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<span t-out="clean_desc" t-options="{'widget': 'text'}"/>
|
||||
</t>
|
||||
</td>
|
||||
</xpath>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- Default Sale Order Report: Add SKU column, clean description -->
|
||||
<!-- ================================================================= -->
|
||||
<template id="report_saleorder_sku_column"
|
||||
inherit_id="sale.report_saleorder_document">
|
||||
|
||||
<!-- 1. Add SKU header before Description -->
|
||||
<xpath expr="//th[@name='th_description']" position="before">
|
||||
<th name="th_sku" class="text-start" style="width: 12%;"><span>SKU</span></th>
|
||||
</xpath>
|
||||
|
||||
<!-- 2. Add SKU cell before description for product lines -->
|
||||
<xpath expr="//td[@name='td_product_name']" position="before">
|
||||
<td name="td_sku" class="text-start">
|
||||
<span t-esc="line.product_id.default_code or ''"/>
|
||||
</td>
|
||||
</xpath>
|
||||
|
||||
<!-- 3. Replace description to strip [internal_ref] prefix -->
|
||||
<xpath expr="//td[@name='td_product_name']" position="replace">
|
||||
<td name="td_product_name" t-att-class="padding_class">
|
||||
<t t-if="line.name">
|
||||
<t t-set="clean_desc" t-value="line.name"/>
|
||||
<t t-if="'] ' in (line.name or '')">
|
||||
<t t-set="clean_desc" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<span t-out="clean_desc" t-options="{'widget': 'text'}"/>
|
||||
</t>
|
||||
</td>
|
||||
</xpath>
|
||||
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
303
fusion_claims/fusion_claims/report/sale_report_landscape.xml
Normal file
303
fusion_claims/fusion_claims/report/sale_report_landscape.xml
Normal file
@@ -0,0 +1,303 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
Landscape ADP Report Template
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_saleorder_landscape">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-set="is_adp" t-value="doc.x_fc_is_adp_sale"/>
|
||||
|
||||
<style>
|
||||
.fc-landscape { font-family: Arial, sans-serif; font-size: 11pt; }
|
||||
.fc-landscape table { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
|
||||
.fc-landscape table.bordered, .fc-landscape table.bordered th, .fc-landscape table.bordered td { border: 1px solid #000; }
|
||||
.fc-landscape th { background-color: #0066a1; color: white; padding: 8px 10px; font-weight: bold; font-size: 10pt; }
|
||||
.fc-landscape td { padding: 6px 8px; vertical-align: top; font-size: 10pt; }
|
||||
.fc-landscape .text-center { text-align: center; }
|
||||
.fc-landscape .text-end { text-align: right; }
|
||||
.fc-landscape .text-start { text-align: left; }
|
||||
.fc-landscape .adp-bg { background-color: #e3f2fd; }
|
||||
.fc-landscape .client-bg { background-color: #fff3e0; }
|
||||
.fc-landscape .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fc-landscape .note-row { font-style: italic; }
|
||||
.fc-landscape h2 { color: #0066a1; margin: 10px 0; font-size: 18pt; }
|
||||
.fc-landscape .info-table td { padding: 8px 12px; font-size: 11pt; }
|
||||
.fc-landscape .info-table th { background-color: #f5f5f5; color: #333; font-size: 10pt; padding: 6px 12px; }
|
||||
.fc-landscape .totals-table { border: 1px solid #000; }
|
||||
.fc-landscape .totals-table td { border: 1px solid #000; padding: 8px 12px; font-size: 11pt; }
|
||||
</style>
|
||||
|
||||
<div class="fc-landscape">
|
||||
<div class="page">
|
||||
|
||||
<!-- Document Title -->
|
||||
<h2 style="text-align: left;">
|
||||
<span t-if="doc.state in ['draft','sent']">Quotation </span>
|
||||
<span t-else="">Sales Order </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h2>
|
||||
|
||||
<!-- Address Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">BILLING ADDRESS</th>
|
||||
<th style="width: 50%;">DELIVERY ADDRESS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="height: 70px; font-size: 12pt;">
|
||||
<div t-field="doc.partner_invoice_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="height: 70px; font-size: 12pt;">
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Order Info Table -->
|
||||
<table class="bordered info-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ORDER DATE</th>
|
||||
<th>VALIDITY</th>
|
||||
<th>CLIENT TYPE</th>
|
||||
<th>SALES REP</th>
|
||||
<th>AUTHORIZER</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.date_order" t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.validity_date"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_client_type or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.user_id"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="doc.x_fc_authorizer_id" t-field="doc.x_fc_authorizer_id"/>
|
||||
<span t-else="">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- ADP Info Table (only for ADP orders) -->
|
||||
<t t-if="is_adp">
|
||||
<table class="bordered info-table">
|
||||
<thead>
|
||||
<tr class="adp-bg">
|
||||
<th style="background-color: #e3f2fd; color: #333;">CLAIM NUMBER</th>
|
||||
<th style="background-color: #e3f2fd; color: #333;">APPLICATION TYPE</th>
|
||||
<th style="background-color: #e3f2fd; color: #333;">CLIENT REF 2</th>
|
||||
<th style="background-color: #e3f2fd; color: #333;">DELIVERY DATE</th>
|
||||
<th style="background-color: #e3f2fd; color: #333;">AUTHORIZATION</th>
|
||||
<th style="background-color: #e3f2fd; color: #333;">APPROVAL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="adp-bg">
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_claim_number or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<!-- Show Application Type (Reason for Application) -->
|
||||
<t t-set="app_type" t-value="dict(doc._fields.get('x_fc_reason_for_application') and doc._fields['x_fc_reason_for_application'].selection or []).get(doc.x_fc_reason_for_application, '-')"/>
|
||||
<span t-esc="app_type"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_client_ref_2 or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="doc.x_fc_adp_delivery_date">
|
||||
<span t-field="doc.x_fc_adp_delivery_date"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="doc.x_fc_claim_authorization_date">
|
||||
<span t-field="doc.x_fc_claim_authorization_date"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="doc.x_fc_claim_approval_date">
|
||||
<span t-field="doc.x_fc_claim_approval_date"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<!-- Order Lines Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" style="width: 8%;">ADP CODE</th>
|
||||
<th class="text-start" style="width: 22%;">DESCRIPTION</th>
|
||||
<th class="text-center" style="width: 8%;">SERIAL #</th>
|
||||
<th t-if="is_adp" class="text-center" style="width: 5%;">PLCMT</th>
|
||||
<th class="text-center" style="width: 5%;">QTY</th>
|
||||
<th class="text-center" style="width: 10%;">UNIT PRICE</th>
|
||||
<th class="text-center" style="width: 10%; background-color: #1976d2; color: white;">ADP PORTION</th>
|
||||
<th class="text-center" style="width: 10%; background-color: #e65100; color: white;">CLIENT PORTION</th>
|
||||
<th class="text-center" style="width: 10%;">TAX</th>
|
||||
<th class="text-center" style="width: 10%;">TOTAL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.order_line" t-as="line">
|
||||
<!-- Section Header -->
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<tr class="section-row">
|
||||
<td t-att-colspan="'10' if is_adp else '9'">
|
||||
<strong><span t-field="line.name"/></strong>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Note Line -->
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row">
|
||||
<td t-att-colspan="'10' if is_adp else '9'">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Product Line -->
|
||||
<t t-elif="not line.display_type">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="line.product_id.x_fc_adp_device_code"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="line.name">
|
||||
<t t-set="clean_name" t-value="line.name"/>
|
||||
<t t-if="'] ' in line.name">
|
||||
<t t-set="clean_name" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<t t-esc="clean_name"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="line.x_fc_serial_number or ''"/>
|
||||
</td>
|
||||
<td t-if="is_adp" class="text-center">
|
||||
<span t-esc="line.x_fc_device_placement or 'N/A'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<!-- Show ADP price if available, otherwise unit price -->
|
||||
<t t-if="line.product_id.product_tmpl_id.x_fc_adp_price">
|
||||
<span t-esc="line.product_id.product_tmpl_id.x_fc_adp_price" t-options="{'widget': 'monetary', 'display_currency': doc.currency_id}"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-end adp-bg">
|
||||
<span t-field="line.x_fc_adp_portion" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
<td class="text-end client-bg">
|
||||
<span t-field="line.x_fc_client_portion" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-esc="', '.join([(tax.invoice_label or tax.name) for tax in line.tax_ids]) or 'NO TAX SALE'"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Payment Terms and Totals Row -->
|
||||
<div class="row" style="margin-top: 15px;">
|
||||
<div class="col-7">
|
||||
<t t-if="doc.payment_term_id.note">
|
||||
<strong>Payment Terms:</strong><br/>
|
||||
<span t-field="doc.payment_term_id.note"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-5" style="text-align: right;">
|
||||
<!-- Totals Table with borders -->
|
||||
<table class="totals-table" style="width: auto; margin-left: auto;">
|
||||
<tr>
|
||||
<td style="min-width: 200px;">Subtotal</td>
|
||||
<td class="text-end" style="min-width: 150px;">
|
||||
<span t-field="doc.amount_untaxed" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="adp-bg">
|
||||
<td><strong>Total ADP Portion</strong></td>
|
||||
<td class="text-end">
|
||||
<span t-field="doc.x_fc_adp_portion_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="client-bg">
|
||||
<td><strong>Total Client Portion</strong></td>
|
||||
<td class="text-end">
|
||||
<span t-field="doc.x_fc_client_portion_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Taxes</td>
|
||||
<td class="text-end">
|
||||
<span t-field="doc.amount_tax" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Grand Total</strong></td>
|
||||
<td class="text-end"><strong>
|
||||
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms and Conditions -->
|
||||
<t t-if="doc.note">
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>Terms and Conditions:</strong>
|
||||
<div t-field="doc.note"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Signature -->
|
||||
<t t-if="doc.signature">
|
||||
<div style="margin-top: 20px; text-align: right;">
|
||||
<strong>Signature</strong><br/>
|
||||
<img t-att-src="image_data_uri(doc.signature)" style="max-height: 4cm; max-width: 8cm;"/><br/>
|
||||
<span t-field="doc.signed_by"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
281
fusion_claims/fusion_claims/report/sale_report_portrait.xml
Normal file
281
fusion_claims/fusion_claims/report/sale_report_portrait.xml
Normal file
@@ -0,0 +1,281 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_saleorder_portrait">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-set="is_adp" t-value="doc.x_fc_is_adp_sale"/>
|
||||
|
||||
<style>
|
||||
.fc-report { font-family: Arial, sans-serif; font-size: 10pt; }
|
||||
.fc-report table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
|
||||
.fc-report table.bordered, .fc-report table.bordered th, .fc-report table.bordered td { border: 1px solid #000; }
|
||||
.fc-report th { background-color: #0066a1; color: white; padding: 6px 8px; font-weight: bold; text-align: center; }
|
||||
.fc-report td { padding: 6px 8px; vertical-align: top; }
|
||||
.fc-report .text-center { text-align: center; }
|
||||
.fc-report .text-end { text-align: right; }
|
||||
.fc-report .text-start { text-align: left; }
|
||||
.fc-report .adp-bg { background-color: #e3f2fd; }
|
||||
.fc-report .client-bg { background-color: #fff3e0; }
|
||||
.fc-report .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fc-report .note-row { font-style: italic; }
|
||||
.fc-report h4 { color: #0066a1; margin: 0 0 15px 0; font-size: 16pt; }
|
||||
.fc-report .totals-table { border: 1px solid #000; border-collapse: collapse; }
|
||||
.fc-report .totals-table td { border: 1px solid #000; padding: 6px 8px; }
|
||||
.fc-report .info-header { background-color: #f5f5f5; color: #333; }
|
||||
.fc-report .adp-header { background-color: #e3f2fd; color: #333; }
|
||||
</style>
|
||||
|
||||
<div class="fc-report">
|
||||
<div class="page">
|
||||
|
||||
<!-- Document Title -->
|
||||
<h4>
|
||||
<span t-if="doc.state in ['draft','sent']">Quotation </span>
|
||||
<span t-else="">Sales Order </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h4>
|
||||
|
||||
<!-- Address Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">BILLING ADDRESS</th>
|
||||
<th style="width: 50%;">SHIPPING ADDRESS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="height: 70px; font-size: 12pt;">
|
||||
<div t-field="doc.partner_invoice_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="height: 70px; font-size: 12pt;">
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Order Info Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="info-header" style="width: 25%;">ORDER DATE</th>
|
||||
<th class="info-header" style="width: 25%;">EXPIRATION</th>
|
||||
<th class="info-header" style="width: 25%;">SALESPERSON</th>
|
||||
<th class="info-header" style="width: 25%;">
|
||||
<t t-if="is_adp">APPLICATION TYPE</t>
|
||||
<t t-else="">SALE TYPE</t>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.date_order" t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.validity_date"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.user_id"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="is_adp">
|
||||
<!-- Show Application Type (Reason for Application) for ADP sales -->
|
||||
<t t-set="app_type" t-value="dict(doc._fields.get('x_fc_reason_for_application') and doc._fields['x_fc_reason_for_application'].selection or []).get(doc.x_fc_reason_for_application, '-')"/>
|
||||
<span t-esc="app_type"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-esc="doc.x_fc_sale_type or '-'"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- ADP Info Table (only for ADP orders) -->
|
||||
<t t-if="is_adp">
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="adp-header" style="width: 25%;">CLAIM NUMBER</th>
|
||||
<th class="adp-header" style="width: 25%;">CLIENT TYPE</th>
|
||||
<th class="adp-header" style="width: 25%;">DELIVERY DATE</th>
|
||||
<th class="adp-header" style="width: 25%;">AUTHORIZER</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="adp-bg">
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_claim_number or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="doc.x_fc_client_type or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="doc.x_fc_adp_delivery_date">
|
||||
<span t-field="doc.x_fc_adp_delivery_date"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="doc.x_fc_authorizer_id" t-field="doc.x_fc_authorizer_id"/>
|
||||
<span t-else="">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<!-- Order Lines Table -->
|
||||
<table class="bordered" style="font-size: 9pt;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-center" style="width: 10%;">ADP CODE</th>
|
||||
<th class="text-start" style="width: 25%;">DESCRIPTION</th>
|
||||
<th t-if="is_adp" class="text-center" style="width: 10%;">SERIAL #</th>
|
||||
<th class="text-center" style="width: 7%;">QTY</th>
|
||||
<th class="text-center" style="width: 10%;">UNIT PRICE</th>
|
||||
<th class="text-center" style="width: 8%;">TAXES</th>
|
||||
<th t-if="is_adp" class="text-center" style="width: 10%; background-color: #1976d2; color: white;">ADP PORTION</th>
|
||||
<th t-if="is_adp" class="text-center" style="width: 10%; background-color: #e65100; color: white;">CLIENT PORTION</th>
|
||||
<th class="text-center" style="width: 10%;">AMOUNT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.order_line" t-as="line">
|
||||
<!-- Section Header -->
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<tr class="section-row">
|
||||
<td t-att-colspan="'9' if is_adp else '6'">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Note Line -->
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row">
|
||||
<td t-att-colspan="'9' if is_adp else '6'">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Product Line -->
|
||||
<t t-elif="not line.display_type">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="line.product_id.x_fc_adp_device_code"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="line.name">
|
||||
<t t-set="clean_name" t-value="line.name"/>
|
||||
<t t-if="'] ' in line.name">
|
||||
<t t-set="clean_name" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<t t-esc="clean_name"/>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="is_adp" class="text-center">
|
||||
<span t-esc="line.x_fc_serial_number or ''"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<!-- Show ADP price if available, otherwise unit price -->
|
||||
<t t-if="line.product_id.product_tmpl_id.x_fc_adp_price">
|
||||
<span t-esc="line.product_id.product_tmpl_id.x_fc_adp_price" t-options="{'widget': 'monetary', 'display_currency': doc.currency_id}"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-field="line.price_unit"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-esc="', '.join([(tax.invoice_label or tax.name) for tax in line.tax_ids]) or 'NO TAX'"/>
|
||||
</td>
|
||||
<td t-if="is_adp" class="text-end adp-bg">
|
||||
<span t-field="line.x_fc_adp_portion"/>
|
||||
</td>
|
||||
<td t-if="is_adp" class="text-end client-bg">
|
||||
<span t-field="line.x_fc_client_portion"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_subtotal"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Payment Terms and Totals Row -->
|
||||
<div class="row" style="margin-top: 15px;">
|
||||
<div class="col-6">
|
||||
<t t-if="doc.payment_term_id.note">
|
||||
<strong>Payment Terms:</strong><br/>
|
||||
<span t-field="doc.payment_term_id.note"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-6" style="text-align: right;">
|
||||
<!-- Totals Table with borders - aligned right, auto width -->
|
||||
<table class="totals-table" style="width: auto; margin-left: auto;">
|
||||
<tr>
|
||||
<td style="min-width: 140px;">Subtotal</td>
|
||||
<td class="text-end" style="min-width: 100px;"><span t-field="doc.amount_untaxed"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Taxes</td>
|
||||
<td class="text-end"><span t-field="doc.amount_tax"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Total</strong></td>
|
||||
<td class="text-end"><strong><span t-field="doc.amount_total"/></strong></td>
|
||||
</tr>
|
||||
<t t-if="is_adp">
|
||||
<tr class="adp-bg">
|
||||
<td><strong>Total ADP Portion</strong></td>
|
||||
<td class="text-end"><span t-field="doc.x_fc_adp_portion_total"/></td>
|
||||
</tr>
|
||||
<tr class="client-bg">
|
||||
<td><strong>Total Client Portion</strong></td>
|
||||
<td class="text-end"><span t-field="doc.x_fc_client_portion_total"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms and Conditions -->
|
||||
<t t-if="doc.note">
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>Terms and Conditions:</strong>
|
||||
<div t-field="doc.note"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Signature -->
|
||||
<t t-if="doc.signature">
|
||||
<div style="margin-top: 20px; text-align: right;">
|
||||
<strong>Signature</strong><br/>
|
||||
<img t-att-src="image_data_uri(doc.signature)" style="max-height: 4cm; max-width: 8cm;"/><br/>
|
||||
<span t-field="doc.signed_by"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ADP Mobility Manual CSV to JSON Converter
|
||||
|
||||
This script reads the ADP Mobility Manual CSV file, cleans the data,
|
||||
and outputs a JSON file that can be imported into Odoo's fusion.adp.device.code model.
|
||||
|
||||
Usage:
|
||||
python import_adp_mobility_manual.py input.csv output.json
|
||||
|
||||
Or run without arguments to use default paths.
|
||||
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def clean_text(text):
|
||||
"""Clean text from weird characters, normalize encoding."""
|
||||
if not text:
|
||||
return ''
|
||||
# Convert to string if not already
|
||||
text = str(text)
|
||||
# Replace curly quotes with straight quotes
|
||||
text = text.replace('"', '"').replace('"', '"')
|
||||
text = text.replace(''', "'").replace(''', "'")
|
||||
# Replace various dashes with standard hyphen
|
||||
text = text.replace('–', '-').replace('—', '-')
|
||||
# Remove non-printable characters except newlines
|
||||
text = ''.join(char if char.isprintable() or char in '\n\r\t' else ' ' for char in text)
|
||||
# Normalize multiple spaces
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
# Strip leading/trailing whitespace
|
||||
return text.strip()
|
||||
|
||||
|
||||
def parse_price(price_str):
|
||||
"""Parse price string like '$64.00' or '$2,578.00' to float."""
|
||||
if not price_str:
|
||||
return 0.0
|
||||
# Remove currency symbols, commas, spaces, quotes
|
||||
price_str = str(price_str).strip()
|
||||
price_str = re.sub(r'[\$,"\'\s]', '', price_str)
|
||||
try:
|
||||
return float(price_str)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def convert_csv_to_json(input_path, output_path):
|
||||
"""Convert ADP Mobility Manual CSV to JSON format."""
|
||||
data = []
|
||||
errors = []
|
||||
skipped = 0
|
||||
|
||||
# Try different encodings
|
||||
encodings = ['utf-8-sig', 'utf-8', 'latin-1', 'cp1252']
|
||||
content = None
|
||||
|
||||
for encoding in encodings:
|
||||
try:
|
||||
with open(input_path, 'r', encoding=encoding) as f:
|
||||
content = f.read()
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
if content is None:
|
||||
print(f"Error: Could not read file with any known encoding")
|
||||
return None
|
||||
|
||||
# Parse CSV
|
||||
reader = csv.DictReader(content.splitlines())
|
||||
|
||||
for idx, row in enumerate(reader, start=2): # Start at 2 (header is line 1)
|
||||
try:
|
||||
# Get device code - skip if empty
|
||||
device_code = clean_text(row.get('Device Code', ''))
|
||||
if not device_code:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Get device type
|
||||
device_type = clean_text(row.get('Device Type', ''))
|
||||
if not device_type:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Get manufacturer
|
||||
manufacturer = clean_text(row.get('Manufacturer', ''))
|
||||
|
||||
# Get device description - clean it
|
||||
device_description = clean_text(row.get('Device Description', ''))
|
||||
|
||||
# Parse quantity
|
||||
qty_str = row.get('Qty', '1') or '1'
|
||||
try:
|
||||
quantity = int(qty_str)
|
||||
except ValueError:
|
||||
quantity = 1
|
||||
|
||||
# Parse price (handle both ' Approved Price ' with spaces and 'Approved Price')
|
||||
price = 0.0
|
||||
for key in row.keys():
|
||||
if 'price' in key.lower() and 'approved' in key.lower():
|
||||
price = parse_price(row.get(key, ''))
|
||||
break
|
||||
|
||||
# Parse serial requirement
|
||||
serial_str = clean_text(row.get('Serial', 'No')).upper()
|
||||
sn_required = serial_str in ('YES', 'Y', 'TRUE', '1')
|
||||
|
||||
data.append({
|
||||
'Device Type': device_type,
|
||||
'Manufacturer': manufacturer,
|
||||
'Device Description': device_description,
|
||||
'Device Code': device_code,
|
||||
'Quantity': quantity,
|
||||
'ADP Price': price,
|
||||
'SN Required': 'Yes' if sn_required else 'No',
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Row {idx}: {str(e)}")
|
||||
|
||||
# Write JSON output
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Print summary
|
||||
print(f"\n{'='*60}")
|
||||
print(f"ADP Mobility Manual Import Summary")
|
||||
print(f"{'='*60}")
|
||||
print(f"Input file: {input_path}")
|
||||
print(f"Output file: {output_path}")
|
||||
print(f"Records processed: {len(data)}")
|
||||
print(f"Records skipped: {skipped}")
|
||||
print(f"Errors: {len(errors)}")
|
||||
|
||||
if errors:
|
||||
print(f"\nFirst 10 errors:")
|
||||
for err in errors[:10]:
|
||||
print(f" - {err}")
|
||||
|
||||
# Print device type summary
|
||||
device_types = {}
|
||||
for item in data:
|
||||
dt = item['Device Type']
|
||||
device_types[dt] = device_types.get(dt, 0) + 1
|
||||
|
||||
print(f"\nDevice Types ({len(device_types)} unique):")
|
||||
for dt in sorted(device_types.keys())[:20]:
|
||||
print(f" - {dt}: {device_types[dt]} devices")
|
||||
if len(device_types) > 20:
|
||||
print(f" ... and {len(device_types) - 20} more")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"JSON file ready for import into Odoo!")
|
||||
print(f"Use: Sales > Configuration > ADP Device Codes > Import")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def main():
|
||||
# Default paths
|
||||
default_input = r"C:\Users\gur_p\Downloads\ADP-Mobility-Manual.csv"
|
||||
default_output = r"C:\Users\gur_p\Downloads\ADP-Mobility-Manual-cleaned.json"
|
||||
|
||||
if len(sys.argv) >= 3:
|
||||
input_path = sys.argv[1]
|
||||
output_path = sys.argv[2]
|
||||
elif len(sys.argv) == 2:
|
||||
input_path = sys.argv[1]
|
||||
output_path = os.path.splitext(input_path)[0] + '-cleaned.json'
|
||||
else:
|
||||
input_path = default_input
|
||||
output_path = default_output
|
||||
|
||||
if not os.path.exists(input_path):
|
||||
print(f"Error: Input file not found: {input_path}")
|
||||
print(f"\nUsage: python {sys.argv[0]} input.csv [output.json]")
|
||||
sys.exit(1)
|
||||
|
||||
convert_csv_to_json(input_path, output_path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
75
fusion_claims/fusion_claims/security/ir.model.access.csv
Normal file
75
fusion_claims/fusion_claims/security/ir.model.access.csv
Normal file
@@ -0,0 +1,75 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_fusion_claims_config_user,fusion.central.config.user,model_fusion_claims_config,base.group_user,1,1,1,1
|
||||
access_fusion_claims_export_wizard_user,fusion.central.export.wizard.user,model_fusion_claims_export_wizard,account.group_account_invoice,1,1,1,1
|
||||
access_fusion_claims_export_wizard_manager,fusion.central.export.wizard.manager,model_fusion_claims_export_wizard,account.group_account_manager,1,1,1,1
|
||||
access_fusion_adp_device_code_user,fusion.adp.device.code.user,model_fusion_adp_device_code,base.group_user,1,0,0,0
|
||||
access_fusion_adp_device_code_sales,fusion.adp.device.code.sales,model_fusion_adp_device_code,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_adp_device_code_manager,fusion.adp.device.code.manager,model_fusion_adp_device_code,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_device_import_wizard,fusion.central.device.import.wizard,model_fusion_claims_device_import_wizard,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_device_approval_wizard,fusion.device.approval.wizard.user,model_fusion_claims_device_approval_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_device_approval_wizard_line,fusion.device.approval.wizard.line.user,model_fusion_claims_device_approval_wizard_line,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_submission_verification_wizard,fusion.submission.verification.wizard.user,model_fusion_claims_submission_verification_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_submission_verification_wizard_line,fusion.submission.verification.wizard.line.user,model_fusion_claims_submission_verification_wizard_line,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_status_change_reason_wizard,fusion.status.change.reason.wizard.user,model_fusion_status_change_reason_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_case_close_verification_wizard,fusion.case.close.verification.wizard.user,model_fusion_claims_case_close_verification_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_schedule_assessment_wizard,fusion.schedule.assessment.wizard.user,model_fusion_claims_schedule_assessment_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_assessment_completed_wizard,fusion.assessment.completed.wizard.user,model_fusion_claims_assessment_completed_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_application_received_wizard,fusion.application.received.wizard.user,model_fusion_claims_application_received_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_ready_for_submission_wizard,fusion.ready.for.submission.wizard.user,model_fusion_claims_ready_for_submission_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_ready_to_bill_wizard,fusion.ready.to.bill.wizard.user,model_fusion_claims_ready_to_bill_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_submission_history_user,fusion.submission.history.user,model_fusion_submission_history,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_submission_history_manager,fusion.submission.history.manager,model_fusion_submission_history,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_loaner_checkout_user,fusion.loaner.checkout.user,model_fusion_loaner_checkout,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_loaner_checkout_manager,fusion.loaner.checkout.manager,model_fusion_loaner_checkout,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_loaner_history_user,fusion.loaner.history.user,model_fusion_loaner_history,sales_team.group_sale_salesman,1,0,0,0
|
||||
access_fusion_loaner_history_manager,fusion.loaner.history.manager,model_fusion_loaner_history,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_loaner_checkout_wizard,fusion.loaner.checkout.wizard.user,model_fusion_loaner_checkout_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_loaner_return_wizard,fusion.loaner.return.wizard.user,model_fusion_loaner_return_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_ready_for_delivery_wizard,fusion.ready.for.delivery.wizard.user,model_fusion_ready_for_delivery_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_client_profile_user,fusion.client.profile.user,model_fusion_client_profile,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_client_profile_manager,fusion.client.profile.manager,model_fusion_client_profile,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_adp_application_data_user,fusion.adp.application.data.user,model_fusion_adp_application_data,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_adp_application_data_manager,fusion.adp.application.data.manager,model_fusion_adp_application_data,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_client_chat_session_user,fusion.client.chat.session.user,model_fusion_client_chat_session,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_client_chat_session_manager,fusion.client.chat.session.manager,model_fusion_client_chat_session,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_client_chat_message_user,fusion.client.chat.message.user,model_fusion_client_chat_message,sales_team.group_sale_salesman,1,1,1,0
|
||||
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
|
||||
access_fusion_mod_awaiting_wizard_manager,fusion_claims.mod.awaiting.funding.wizard.manager,model_fusion_claims_mod_awaiting_funding_wizard,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_mod_approved_wizard_user,fusion_claims.mod.funding.approved.wizard.user,model_fusion_claims_mod_funding_approved_wizard,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_mod_approved_wizard_manager,fusion_claims.mod.funding.approved.wizard.manager,model_fusion_claims_mod_funding_approved_wizard,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_mod_approved_line_user,fusion_claims.mod.funding.approved.wizard.line.user,model_fusion_claims_mod_funding_approved_wizard_line,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_mod_approved_line_manager,fusion_claims.mod.funding.approved.wizard.line.manager,model_fusion_claims_mod_funding_approved_wizard_line,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_mod_pca_wizard_user,fusion_claims.mod.pca.received.wizard.user,model_fusion_claims_mod_pca_received_wizard,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_mod_pca_wizard_manager,fusion_claims.mod.pca.received.wizard.manager,model_fusion_claims_mod_pca_received_wizard,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_sa_mobility_wizard_user,fusion_claims.sa.mobility.wizard.user,model_fusion_claims_sa_mobility_wizard,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_sa_mobility_wizard_manager,fusion_claims.sa.mobility.wizard.manager,model_fusion_claims_sa_mobility_wizard,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_sa_mobility_part_line_user,fusion_claims.sa.mobility.part.line.user,model_fusion_claims_sa_mobility_part_line,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_sa_mobility_part_line_manager,fusion_claims.sa.mobility.part.line.manager,model_fusion_claims_sa_mobility_part_line,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_sa_mobility_labour_line_user,fusion_claims.sa.mobility.labour.line.user,model_fusion_claims_sa_mobility_labour_line,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_sa_mobility_labour_line_manager,fusion_claims.sa.mobility.labour.line.manager,model_fusion_claims_sa_mobility_labour_line,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_sa_mobility_fee_line_user,fusion_claims.sa.mobility.fee.line.user,model_fusion_claims_sa_mobility_fee_line,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_sa_mobility_fee_line_manager,fusion_claims.sa.mobility.fee.line.manager,model_fusion_claims_sa_mobility_fee_line,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_discretionary_wizard_user,fusion_claims.discretionary.benefit.wizard.user,model_fusion_claims_discretionary_benefit_wizard,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_discretionary_wizard_manager,fusion_claims.discretionary.benefit.wizard.manager,model_fusion_claims_discretionary_benefit_wizard,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_odsp_pre_approved_wizard_user,fusion_claims.odsp.pre.approved.wizard.user,model_fusion_claims_odsp_pre_approved_wizard,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_odsp_pre_approved_wizard_manager,fusion_claims.odsp.pre.approved.wizard.manager,model_fusion_claims_odsp_pre_approved_wizard,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_odsp_ready_delivery_wizard_user,fusion_claims.odsp.ready.delivery.wizard.user,model_fusion_claims_odsp_ready_delivery_wizard,sales_team.group_sale_salesman,1,1,1,0
|
||||
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
|
||||
|
141
fusion_claims/fusion_claims/security/security.xml
Normal file
141
fusion_claims/fusion_claims/security/security.xml
Normal file
@@ -0,0 +1,141 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- ================================================================== -->
|
||||
<!-- MODULE CATEGORY (required for user settings section rendering) -->
|
||||
<!-- Odoo 19 organizes privileges by ir.module.category. -->
|
||||
<!-- Without this, groups fall into the generic Extra Rights list. -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="module_category_fusion_claims" model="ir.module.category">
|
||||
<field name="name">Fusion Claims</field>
|
||||
<field name="sequence">45</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- FUSION CLAIMS PRIVILEGE (Odoo 19 pattern) -->
|
||||
<!-- Linked to module_category_fusion_claims so all groups appear -->
|
||||
<!-- under a "FUSION CLAIMS" section in user settings. -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="res_groups_privilege_fusion_claims" model="res.groups.privilege">
|
||||
<field name="name">Fusion Claims</field>
|
||||
<field name="sequence">45</field>
|
||||
<field name="category_id" ref="module_category_fusion_claims"/>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- USER GROUP -->
|
||||
<!-- Implies sales_team.group_sale_salesman so existing access rules -->
|
||||
<!-- continue working without changes to ir.model.access.csv -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="group_fusion_claims_user" model="res.groups">
|
||||
<field name="name">User</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user')), (4, ref('sales_team.group_sale_salesman'))]"/>
|
||||
<field name="privilege_id" ref="res_groups_privilege_fusion_claims"/>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- MANAGER GROUP -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="group_fusion_claims_manager" model="res.groups">
|
||||
<field name="name">Administrator</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="privilege_id" ref="res_groups_privilege_fusion_claims"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_fusion_claims_user')), (4, ref('sales_team.group_sale_manager'))]"/>
|
||||
<field name="user_ids" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- DOCUMENT LOCK OVERRIDE GROUP -->
|
||||
<!-- Only users in this group can edit locked documents when the -->
|
||||
<!-- "Allow Document Lock Override" setting is enabled. -->
|
||||
<!-- Not implied by Manager. Must be explicitly assigned. -->
|
||||
<record id="group_document_lock_override" model="res.groups">
|
||||
<field name="name">Document Lock Override</field>
|
||||
<field name="privilege_id" ref="res_groups_privilege_fusion_claims"/>
|
||||
<field name="comment">Can edit locked documents on old/legacy cases when the override setting is enabled. Assign only to specific trusted 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>
|
||||
BIN
fusion_claims/fusion_claims/static/description/icon.png
Normal file
BIN
fusion_claims/fusion_claims/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
fusion_claims/fusion_claims/static/img/nexa_favicon.png
Normal file
BIN
fusion_claims/fusion_claims/static/img/nexa_favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
fusion_claims/fusion_claims/static/img/nexa_logo.png
Normal file
BIN
fusion_claims/fusion_claims/static/img/nexa_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
36
fusion_claims/fusion_claims/static/nuke_cache.html
Normal file
36
fusion_claims/fusion_claims/static/nuke_cache.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Odoo Cache Reset</title></head>
|
||||
<body style="font-family: Arial, sans-serif; padding: 40px; text-align: center;">
|
||||
<h1>Odoo Cache Reset</h1>
|
||||
<div id="status" style="font-size: 18px; margin: 20px;">Working...</div>
|
||||
<script>
|
||||
async function nukeEverything() {
|
||||
const status = document.getElementById("status");
|
||||
let log = "";
|
||||
if ("serviceWorker" in navigator) {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
for (let reg of registrations) {
|
||||
await reg.unregister();
|
||||
log += "<p style=\"color:green\">Unregistered service worker: " + reg.scope + "</p>";
|
||||
}
|
||||
if (registrations.length === 0) log += "<p>No service workers found.</p>";
|
||||
}
|
||||
if ("caches" in window) {
|
||||
const names = await caches.keys();
|
||||
for (let name of names) {
|
||||
await caches.delete(name);
|
||||
log += "<p style=\"color:green\">Deleted cache: " + name + "</p>";
|
||||
}
|
||||
if (names.length === 0) log += "<p>No caches found.</p>";
|
||||
}
|
||||
try { localStorage.clear(); log += "<p style=\"color:green\">Cleared localStorage</p>"; } catch(e) {}
|
||||
try { sessionStorage.clear(); log += "<p style=\"color:green\">Cleared sessionStorage</p>"; } catch(e) {}
|
||||
log += "<h2 style=\"color:blue; margin-top:30px;\">Done! Click below to go to Odoo.</h2>";
|
||||
log += "<a href=\"/odoo\" style=\"display:inline-block;padding:15px 30px;background:#714b67;color:white;text-decoration:none;border-radius:5px;font-size:18px;margin-top:10px;\">Go to Odoo Backend</a>";
|
||||
status.innerHTML = log;
|
||||
}
|
||||
nukeEverything();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,386 @@
|
||||
// =====================================================================
|
||||
// Fusion Task Map View - Sidebar + Google Maps
|
||||
// Theme-aware: uses Odoo/Bootstrap variables for dark mode support
|
||||
// =====================================================================
|
||||
|
||||
$sidebar-width: 340px;
|
||||
$transition-speed: .25s;
|
||||
|
||||
.o_fusion_task_map_view {
|
||||
height: 100%;
|
||||
|
||||
.o_content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main wrapper: sidebar + map side by side ────────────────────────
|
||||
.fc_map_wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// ── Sidebar ─────────────────────────────────────────────────────────
|
||||
.fc_sidebar {
|
||||
width: $sidebar-width;
|
||||
min-width: $sidebar-width;
|
||||
max-width: $sidebar-width;
|
||||
background: var(--o-view-background-color, $o-view-background-color);
|
||||
border-right: 1px solid $border-color;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width $transition-speed ease, min-width $transition-speed ease,
|
||||
max-width $transition-speed ease, opacity $transition-speed ease;
|
||||
overflow: hidden;
|
||||
|
||||
&--collapsed {
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fc_sidebar_header {
|
||||
padding: 14px 16px 12px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
flex-shrink: 0;
|
||||
|
||||
h6 {
|
||||
font-size: 14px;
|
||||
color: $headings-color;
|
||||
}
|
||||
}
|
||||
|
||||
.fc_sidebar_body {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 6px 0;
|
||||
|
||||
&::-webkit-scrollbar { width: 5px; }
|
||||
&::-webkit-scrollbar-track { background: transparent; }
|
||||
&::-webkit-scrollbar-thumb { background: $border-color; border-radius: 4px; }
|
||||
}
|
||||
|
||||
.fc_sidebar_footer {
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid $border-color;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fc_sidebar_empty {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
// ── Day filter chips ────────────────────────────────────────────────
|
||||
.fc_day_filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.fc_day_chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
color: $text-muted;
|
||||
cursor: pointer;
|
||||
transition: all .15s;
|
||||
line-height: 18px;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba($primary, .3);
|
||||
color: $body-color;
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: #fff !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
&--all {
|
||||
color: $body-color;
|
||||
font-weight: 500;
|
||||
&:hover { background: rgba($primary, .1); }
|
||||
}
|
||||
}
|
||||
|
||||
.fc_day_chip_count {
|
||||
font-size: 10px;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.fc_group_hidden_tag {
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
color: $text-muted;
|
||||
background: rgba($secondary, .1);
|
||||
padding: 0 5px;
|
||||
border-radius: 3px;
|
||||
margin-left: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// Collapsed toggle button (floating)
|
||||
.fc_sidebar_toggle_btn {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 15;
|
||||
background: var(--o-view-background-color, $o-view-background-color);
|
||||
border: 1px solid $border-color;
|
||||
border-left: none;
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 12px 6px;
|
||||
cursor: pointer;
|
||||
box-shadow: 2px 0 6px rgba(0,0,0,.08);
|
||||
color: $text-muted;
|
||||
transition: background .15s;
|
||||
|
||||
&:hover {
|
||||
background: $o-gray-100;
|
||||
color: $body-color;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Group headers ───────────────────────────────────────────────────
|
||||
.fc_group_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: $text-muted;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
background: rgba($secondary, .08);
|
||||
border-bottom: 1px solid $border-color;
|
||||
transition: background .15s;
|
||||
|
||||
&:hover {
|
||||
background: rgba($secondary, .15);
|
||||
}
|
||||
|
||||
.fa-caret-right,
|
||||
.fa-caret-down {
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.fc_group_label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fc_group_badge {
|
||||
background: rgba($secondary, .2);
|
||||
color: $body-color;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 7px;
|
||||
border-radius: 10px;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// ── Task cards ──────────────────────────────────────────────────────
|
||||
.fc_group_tasks {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.fc_task_card {
|
||||
margin: 3px 10px;
|
||||
padding: 10px 12px;
|
||||
background: var(--o-view-background-color, $o-view-background-color);
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all .15s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: rgba($primary, .05);
|
||||
border-color: rgba($primary, .2);
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,.06);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: rgba($primary, .1) !important;
|
||||
border-color: rgba($primary, .35) !important;
|
||||
box-shadow: 0 0 0 2px rgba($primary, .15);
|
||||
}
|
||||
}
|
||||
|
||||
.fc_task_card_top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.fc_task_num {
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 1px 8px;
|
||||
border-radius: 4px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.fc_task_status {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fc_task_client {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: $headings-color;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fc_task_meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 11px;
|
||||
color: $body-color;
|
||||
margin-bottom: 3px;
|
||||
|
||||
.fa { opacity: .5; }
|
||||
}
|
||||
|
||||
.fc_task_detail {
|
||||
font-size: 11px;
|
||||
color: $body-color;
|
||||
margin-bottom: 2px;
|
||||
.fa { opacity: .5; }
|
||||
}
|
||||
|
||||
.fc_task_address {
|
||||
font-size: 10px;
|
||||
color: $text-muted;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.fc_task_bottom_row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fc_task_travel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
color: $body-color;
|
||||
background: rgba($secondary, .1);
|
||||
padding: 1px 8px;
|
||||
border-radius: 4px;
|
||||
.fa { opacity: .5; }
|
||||
}
|
||||
|
||||
.fc_task_source {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
padding: 1px 8px;
|
||||
border-radius: 4px;
|
||||
.fa { opacity: .8; }
|
||||
}
|
||||
|
||||
// ── Map area ────────────────────────────────────────────────────────
|
||||
.fc_map_area {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fc_map_legend_bar {
|
||||
flex: 0 0 auto;
|
||||
font-size: 12px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.fc_map_container {
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
// ── Google Maps InfoWindow override (always light bg) ───────────────
|
||||
// InfoWindow is rendered by Google outside our DOM; we style via
|
||||
// the .gm-style-iw container that Google injects.
|
||||
.gm-style-iw-d {
|
||||
overflow: auto !important;
|
||||
}
|
||||
.gm-style .gm-style-iw-c {
|
||||
padding: 0 !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
// ── Responsive ──────────────────────────────────────────────────────
|
||||
@media (max-width: 768px) {
|
||||
.fc_map_wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
.fc_sidebar {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
max-height: 40vh;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
&--collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.fc_sidebar_toggle_btn {
|
||||
top: auto;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid $border-color;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.fc_map_area {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/** @odoo-module **/
|
||||
/**
|
||||
* Image compression for file uploads on Odoo.
|
||||
*
|
||||
* Problem: On iPhone, selecting 4+ photos (5-15MB each) causes the
|
||||
* browser tab to crash because Odoo converts each to a base64 data URL
|
||||
* before uploading. 7 photos = 50-100MB of strings in memory.
|
||||
*
|
||||
* Solution: Intercept at the FileUploader level, compress each image
|
||||
* via Canvas BEFORE the data URL conversion. A 5MB photo becomes ~300KB.
|
||||
*
|
||||
* The FileUploader.onFileChange is completely overridden (not wrapped)
|
||||
* to avoid any DataTransfer API issues on iPhone Safari.
|
||||
*/
|
||||
import { AttachmentUploadService } from "@mail/core/common/attachment_upload_service";
|
||||
import { FileUploader } from "@web/views/fields/file_handler";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { getDataURLFromFile } from "@web/core/utils/urls";
|
||||
import { checkFileSize } from "@web/core/utils/files";
|
||||
|
||||
const IMAGE_TYPES = new Set([
|
||||
"image/jpeg", "image/png", "image/webp", "image/bmp",
|
||||
"image/heic", "image/heif",
|
||||
]);
|
||||
const MAX_DIMENSION = 1280; // Conservative for mobile memory
|
||||
const JPEG_QUALITY = 0.80;
|
||||
const SKIP_THRESHOLD = 500 * 1024; // 500KB
|
||||
|
||||
/**
|
||||
* Compress an image File via Canvas API.
|
||||
* Returns the original file if anything fails.
|
||||
*/
|
||||
function compressImageFile(file) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const img = new Image();
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
const cleanup = () => {
|
||||
try { URL.revokeObjectURL(objectUrl); } catch(e) {}
|
||||
try { img.src = ""; } catch(e) {}
|
||||
};
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup();
|
||||
resolve(file); // Timeout fallback after 10s
|
||||
}, 10000);
|
||||
img.onload = () => {
|
||||
try {
|
||||
clearTimeout(timeout);
|
||||
let w = img.naturalWidth || img.width;
|
||||
let h = img.naturalHeight || img.height;
|
||||
if (w > MAX_DIMENSION || h > MAX_DIMENSION) {
|
||||
const ratio = Math.min(MAX_DIMENSION / w, MAX_DIMENSION / h);
|
||||
w = Math.round(w * ratio);
|
||||
h = Math.round(h * ratio);
|
||||
}
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
canvas.getContext("2d").drawImage(img, 0, 0, w, h);
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
cleanup();
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
if (!blob) { resolve(file); return; }
|
||||
const name = file.name.replace(/\.[^.]+$/, "") + ".jpg";
|
||||
resolve(new File([blob], name, {
|
||||
type: "image/jpeg",
|
||||
lastModified: file.lastModified,
|
||||
}));
|
||||
},
|
||||
"image/jpeg",
|
||||
JPEG_QUALITY
|
||||
);
|
||||
} catch (e) {
|
||||
clearTimeout(timeout);
|
||||
cleanup();
|
||||
resolve(file);
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
clearTimeout(timeout);
|
||||
cleanup();
|
||||
resolve(file);
|
||||
};
|
||||
img.src = objectUrl;
|
||||
} catch (e) {
|
||||
resolve(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Override FileUploader.onFileChange to compress images before
|
||||
* converting to data URLs. This prevents the massive memory spike
|
||||
* that crashes mobile browsers.
|
||||
*
|
||||
* We re-implement onFileChange instead of wrapping it to avoid
|
||||
* DataTransfer API issues on iPhone Safari.
|
||||
*/
|
||||
patch(FileUploader.prototype, {
|
||||
async onFileChange(ev) {
|
||||
const rawFiles = ev.target?.files;
|
||||
if (!rawFiles || !rawFiles.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if any file needs compression
|
||||
let hasLargeImages = false;
|
||||
for (const f of rawFiles) {
|
||||
if (IMAGE_TYPES.has(f.type) && f.size > SKIP_THRESHOLD) {
|
||||
hasLargeImages = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No large images -- use standard Odoo behavior
|
||||
if (!hasLargeImages) {
|
||||
return super.onFileChange(ev);
|
||||
}
|
||||
|
||||
// Process files one at a time with compression
|
||||
const files = [...rawFiles].filter((f) => this.validFileType(f));
|
||||
const target = ev.target;
|
||||
|
||||
for (const file of files) {
|
||||
let processedFile = file;
|
||||
|
||||
// Compress large images
|
||||
if (IMAGE_TYPES.has(file.type) && file.size > SKIP_THRESHOLD) {
|
||||
try {
|
||||
processedFile = await compressImageFile(file);
|
||||
} catch (e) {
|
||||
processedFile = file; // fallback to original
|
||||
}
|
||||
}
|
||||
|
||||
// Size check
|
||||
if (this.props.checkSize && !checkFileSize(processedFile.size, this.notification)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.state.isUploading = true;
|
||||
try {
|
||||
const data = await getDataURLFromFile(processedFile);
|
||||
if (!processedFile.size) {
|
||||
this.notification.add(
|
||||
`There was a problem while uploading: ${processedFile.name}`,
|
||||
{ type: "danger" }
|
||||
);
|
||||
continue;
|
||||
}
|
||||
await this.props.onUploaded({
|
||||
name: processedFile.name,
|
||||
size: processedFile.size,
|
||||
type: processedFile.type,
|
||||
data: data.split(",")[1],
|
||||
objectUrl:
|
||||
processedFile.type === "application/pdf"
|
||||
? URL.createObjectURL(processedFile)
|
||||
: null,
|
||||
});
|
||||
} catch (e) {
|
||||
// Skip this file on error, continue with others
|
||||
} finally {
|
||||
this.state.isUploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input so same file can be re-selected
|
||||
target.value = null;
|
||||
if (this.props.multiUpload && this.props.onUploadComplete) {
|
||||
this.props.onUploadComplete({});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Safety net for drag-drop and paste uploads that bypass FileUploader.
|
||||
*/
|
||||
patch(AttachmentUploadService.prototype, {
|
||||
async upload(thread, composer, file, options) {
|
||||
if (file && IMAGE_TYPES.has(file.type) && file.size > SKIP_THRESHOLD) {
|
||||
try {
|
||||
file = await compressImageFile(file);
|
||||
} catch (e) {
|
||||
// Use original file
|
||||
}
|
||||
}
|
||||
return super.upload(thread, composer, file, options);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/** @odoo-module **/
|
||||
// Fusion Claims - Calendar Store Hours Restriction
|
||||
// Copyright 2024-2026 Nexa Systems Inc.
|
||||
// License OPL-1
|
||||
//
|
||||
// Restricts the technician task calendar view to only show store hours.
|
||||
// Default: 9:00 AM - 6:00 PM (configurable in Settings).
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { CalendarRenderer } from "@web/views/calendar/calendar_renderer";
|
||||
|
||||
patch(CalendarRenderer.prototype, {
|
||||
get fcOptions() {
|
||||
const options = super.fcOptions;
|
||||
// Only restrict hours for the technician task calendar
|
||||
if (this.props.model?.resModel === "fusion.technician.task") {
|
||||
options.slotMinTime = "08:00:00";
|
||||
options.slotMaxTime = "19:00:00";
|
||||
}
|
||||
return options;
|
||||
},
|
||||
});
|
||||
40
fusion_claims/fusion_claims/static/src/js/chatter_resize.js
Normal file
40
fusion_claims/fusion_claims/static/src/js/chatter_resize.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/** @odoo-module **/
|
||||
// Fusion Claims - Chatter Topbar Tooltips
|
||||
// Copyright 2024-2026 Nexa Systems Inc.
|
||||
// License OPL-1
|
||||
//
|
||||
// Adds title (tooltip) attributes to chatter topbar buttons that have
|
||||
// their text hidden via CSS (icon-only mode).
|
||||
|
||||
const TOOLTIPS = {
|
||||
'.o-mail-Chatter-sendMessage': 'Send Message',
|
||||
'.o-mail-Chatter-logNote': 'Log Note',
|
||||
'button[data-hotkey="shift+w"]': 'WhatsApp',
|
||||
'.o-mail-Chatter-activity': 'Schedule Activity',
|
||||
'.fusion-notes-mic-btn': 'Record Voice Note',
|
||||
'.o-mail-Chatter-messageAuthorizer': 'Message Authorizer',
|
||||
};
|
||||
|
||||
function applyTooltips() {
|
||||
for (const [selector, title] of Object.entries(TOOLTIPS)) {
|
||||
for (const btn of document.querySelectorAll(selector)) {
|
||||
if (!btn.getAttribute('title')) {
|
||||
btn.setAttribute('title', title);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run on DOM changes (OWL re-renders)
|
||||
const observer = new MutationObserver(() => applyTooltips());
|
||||
|
||||
// Start observing once DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
applyTooltips();
|
||||
});
|
||||
} else {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
applyTooltips();
|
||||
}
|
||||
296
fusion_claims/fusion_claims/static/src/js/document_preview.js
Normal file
296
fusion_claims/fusion_claims/static/src/js/document_preview.js
Normal file
@@ -0,0 +1,296 @@
|
||||
/** @odoo-module **/
|
||||
// Fusion Claims - Document Preview (PDF and XML)
|
||||
// Copyright 2024-2025 Nexa Systems Inc.
|
||||
// License OPL-1
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { Component, useState, onMounted } from "@odoo/owl";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
/**
|
||||
* PDF Document Preview Dialog Component
|
||||
* Uses Odoo's built-in PDF.js viewer for XFA/protected PDF support
|
||||
*/
|
||||
export class DocumentPreviewDialog extends Component {
|
||||
static template = "fusion_claims.DocumentPreviewDialog";
|
||||
static components = { Dialog };
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
isLoading: true,
|
||||
isMaximized: false
|
||||
});
|
||||
}
|
||||
|
||||
getViewerUrl() {
|
||||
const pdfUrl = `/web/content/${this.props.attachmentId}`;
|
||||
const encodedUrl = encodeURIComponent(pdfUrl);
|
||||
return `/web/static/lib/pdfjs/web/viewer.html?file=${encodedUrl}#pagemode=none`;
|
||||
}
|
||||
|
||||
onIframeLoad() {
|
||||
this.state.isLoading = false;
|
||||
}
|
||||
|
||||
toggleMaximize() {
|
||||
this.state.isMaximized = !this.state.isMaximized;
|
||||
}
|
||||
|
||||
getDialogSize() {
|
||||
return this.state.isMaximized ? 'fullscreen' : 'xl';
|
||||
}
|
||||
|
||||
getFrameStyle() {
|
||||
return this.state.isMaximized
|
||||
? 'height: calc(98vh - 100px); width: 100%;'
|
||||
: 'height: calc(85vh - 100px); width: 100%;';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* XML Viewer Dialog Component
|
||||
* Displays XML content with syntax highlighting
|
||||
*/
|
||||
export class XMLViewerDialog extends Component {
|
||||
static template = "fusion_claims.XMLViewerDialog";
|
||||
static components = { Dialog };
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
isLoading: true,
|
||||
isMaximized: false,
|
||||
xmlContent: '',
|
||||
formattedXml: '',
|
||||
error: null
|
||||
});
|
||||
this.notification = useService("notification");
|
||||
|
||||
onMounted(async () => {
|
||||
await this.loadXmlContent();
|
||||
});
|
||||
}
|
||||
|
||||
async loadXmlContent() {
|
||||
try {
|
||||
const response = await fetch(`/web/content/${this.props.attachmentId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load XML file');
|
||||
}
|
||||
const xmlText = await response.text();
|
||||
this.state.xmlContent = xmlText;
|
||||
this.state.formattedXml = this.formatXml(xmlText);
|
||||
this.state.isLoading = false;
|
||||
} catch (error) {
|
||||
this.state.error = error.message;
|
||||
this.state.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
formatXml(xml) {
|
||||
// Format XML with indentation and syntax highlighting
|
||||
let formatted = '';
|
||||
let indent = 0;
|
||||
const tab = ' ';
|
||||
|
||||
// Split by tags
|
||||
xml = xml.replace(/>\s*</g, '><');
|
||||
const nodes = xml.split(/(<[^>]+>)/g).filter(n => n.trim());
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.startsWith('</')) {
|
||||
// Closing tag - decrease indent
|
||||
indent = Math.max(0, indent - 1);
|
||||
formatted += tab.repeat(indent) + this.highlightXml(node) + '\n';
|
||||
} else if (node.startsWith('<?') || node.startsWith('<!')) {
|
||||
// Declaration or comment
|
||||
formatted += this.highlightXml(node) + '\n';
|
||||
} else if (node.startsWith('<') && node.endsWith('/>')) {
|
||||
// Self-closing tag
|
||||
formatted += tab.repeat(indent) + this.highlightXml(node) + '\n';
|
||||
} else if (node.startsWith('<')) {
|
||||
// Opening tag
|
||||
formatted += tab.repeat(indent) + this.highlightXml(node) + '\n';
|
||||
indent++;
|
||||
} else {
|
||||
// Text content
|
||||
const trimmed = node.trim();
|
||||
if (trimmed) {
|
||||
formatted += tab.repeat(indent) + this.escapeHtml(trimmed) + '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
highlightXml(str) {
|
||||
// Escape HTML first
|
||||
str = this.escapeHtml(str);
|
||||
|
||||
// Highlight tag names
|
||||
str = str.replace(/<(\/?)([\w:-]+)/g,
|
||||
'<$1<span class="xml-tag">$2</span>');
|
||||
|
||||
// Highlight attributes
|
||||
str = str.replace(/([\w:-]+)=("[^&]*")/g,
|
||||
'<span class="xml-attr">$1</span>=<span class="xml-value">$2</span>');
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
escapeHtml(str) {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
toggleMaximize() {
|
||||
this.state.isMaximized = !this.state.isMaximized;
|
||||
}
|
||||
|
||||
getDialogSize() {
|
||||
return this.state.isMaximized ? 'fullscreen' : 'xl';
|
||||
}
|
||||
|
||||
async copyToClipboard() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.state.xmlContent);
|
||||
this.notification.add(_t("XML copied to clipboard"), { type: 'success' });
|
||||
} catch (error) {
|
||||
this.notification.add(_t("Failed to copy to clipboard"), { type: 'warning' });
|
||||
}
|
||||
}
|
||||
|
||||
downloadXml() {
|
||||
window.open(`/web/content/${this.props.attachmentId}?download=true`, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Client action to preview a PDF document
|
||||
*/
|
||||
async function previewDocumentAction(env, action) {
|
||||
const attachmentId = action.params?.attachment_id;
|
||||
const title = action.params?.title || "Document Preview";
|
||||
|
||||
if (!attachmentId) {
|
||||
env.services.notification.add(
|
||||
_t("No document has been uploaded yet."),
|
||||
{ type: 'warning', title: _t("No Document") }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
env.services.dialog.add(DocumentPreviewDialog, {
|
||||
attachmentId: attachmentId,
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Client action to preview an XML document
|
||||
*/
|
||||
async function previewXmlAction(env, action) {
|
||||
const attachmentId = action.params?.attachment_id;
|
||||
const title = action.params?.title || "XML Viewer";
|
||||
|
||||
if (!attachmentId) {
|
||||
env.services.notification.add(
|
||||
_t("No XML file has been uploaded yet."),
|
||||
{ type: 'warning', title: _t("No Document") }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
env.services.dialog.add(XMLViewerDialog, {
|
||||
attachmentId: attachmentId,
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
// Register client actions
|
||||
registry.category("actions").add("fusion_claims.preview_document", previewDocumentAction);
|
||||
registry.category("actions").add("fusion_claims.preview_xml", previewXmlAction);
|
||||
|
||||
/**
|
||||
* Image Preview Dialog Component
|
||||
* Full-screen image preview with navigation
|
||||
*/
|
||||
export class ImagePreviewDialog extends Component {
|
||||
static template = "fusion_claims.ImagePreviewDialog";
|
||||
static components = { Dialog };
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
currentIndex: this.props.initialIndex || 0,
|
||||
isLoading: true
|
||||
});
|
||||
}
|
||||
|
||||
get currentImage() {
|
||||
return this.props.images[this.state.currentIndex];
|
||||
}
|
||||
|
||||
get imageUrl() {
|
||||
return `/web/image/${this.currentImage.id}`;
|
||||
}
|
||||
|
||||
get hasMultiple() {
|
||||
return this.props.images.length > 1;
|
||||
}
|
||||
|
||||
get currentPosition() {
|
||||
return `${this.state.currentIndex + 1} / ${this.props.images.length}`;
|
||||
}
|
||||
|
||||
onImageLoad() {
|
||||
this.state.isLoading = false;
|
||||
}
|
||||
|
||||
previousImage() {
|
||||
if (this.state.currentIndex > 0) {
|
||||
this.state.isLoading = true;
|
||||
this.state.currentIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
nextImage() {
|
||||
if (this.state.currentIndex < this.props.images.length - 1) {
|
||||
this.state.isLoading = true;
|
||||
this.state.currentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
downloadImage() {
|
||||
window.open(`/web/content/${this.currentImage.id}?download=true`, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Client action to preview images
|
||||
*/
|
||||
async function previewImageAction(env, action) {
|
||||
const images = action.params?.images || [];
|
||||
const initialIndex = action.params?.initial_index || 0;
|
||||
const title = action.params?.title || "Image Preview";
|
||||
|
||||
if (!images.length) {
|
||||
env.services.notification.add(
|
||||
_t("No images available."),
|
||||
{ type: 'warning', title: _t("No Images") }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
env.services.dialog.add(ImagePreviewDialog, {
|
||||
images: images,
|
||||
initialIndex: initialIndex,
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
registry.category("actions").add("fusion_claims.preview_image", previewImageAction);
|
||||
@@ -0,0 +1,678 @@
|
||||
/** @odoo-module **/
|
||||
// Fusion Claims - Google Maps Task View with Sidebar
|
||||
// Copyright 2024-2026 Nexa Systems Inc.
|
||||
// License OPL-1
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardViewProps } from "@web/views/standard_view_props";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useModelWithSampleData } from "@web/model/model";
|
||||
import { useSetupAction } from "@web/search/action_hook";
|
||||
import { usePager } from "@web/search/pager_hook";
|
||||
import { useSearchBarToggler } from "@web/search/search_bar/search_bar_toggler";
|
||||
import { RelationalModel } from "@web/model/relational_model/relational_model";
|
||||
import { Layout } from "@web/search/layout";
|
||||
import { SearchBar } from "@web/search/search_bar/search_bar";
|
||||
import { CogMenu } from "@web/search/cog_menu/cog_menu";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import {
|
||||
Component,
|
||||
onMounted,
|
||||
onPatched,
|
||||
onWillUnmount,
|
||||
useRef,
|
||||
useState,
|
||||
} from "@odoo/owl";
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────
|
||||
const STATUS_COLORS = {
|
||||
scheduled: "#3b82f6",
|
||||
en_route: "#f59e0b",
|
||||
in_progress: "#8b5cf6",
|
||||
completed: "#10b981",
|
||||
cancelled: "#ef4444",
|
||||
rescheduled: "#f97316",
|
||||
};
|
||||
const STATUS_LABELS = {
|
||||
scheduled: "Scheduled",
|
||||
en_route: "En Route",
|
||||
in_progress: "In Progress",
|
||||
completed: "Completed",
|
||||
cancelled: "Cancelled",
|
||||
rescheduled: "Rescheduled",
|
||||
};
|
||||
const STATUS_ICONS = {
|
||||
scheduled: "fa-clock-o",
|
||||
en_route: "fa-truck",
|
||||
in_progress: "fa-wrench",
|
||||
completed: "fa-check-circle",
|
||||
cancelled: "fa-times-circle",
|
||||
rescheduled: "fa-calendar",
|
||||
};
|
||||
|
||||
// Date group keys
|
||||
const GROUP_YESTERDAY = "yesterday";
|
||||
const GROUP_TODAY = "today";
|
||||
const GROUP_TOMORROW = "tomorrow";
|
||||
const GROUP_THIS_WEEK = "this_week";
|
||||
const GROUP_LATER = "later";
|
||||
const GROUP_LABELS = {
|
||||
[GROUP_YESTERDAY]: "Yesterday",
|
||||
[GROUP_TODAY]: "Today",
|
||||
[GROUP_TOMORROW]: "Tomorrow",
|
||||
[GROUP_THIS_WEEK]: "This Week",
|
||||
[GROUP_LATER]: "Upcoming",
|
||||
};
|
||||
|
||||
// Pin colours by day group
|
||||
const DAY_COLORS = {
|
||||
[GROUP_YESTERDAY]: "#9ca3af", // Gray
|
||||
[GROUP_TODAY]: "#ef4444", // Red
|
||||
[GROUP_TOMORROW]: "#3b82f6", // Blue
|
||||
[GROUP_THIS_WEEK]: "#10b981", // Green
|
||||
[GROUP_LATER]: "#a855f7", // Purple
|
||||
};
|
||||
const DAY_ICONS = {
|
||||
[GROUP_YESTERDAY]: "fa-history",
|
||||
[GROUP_TODAY]: "fa-exclamation-circle",
|
||||
[GROUP_TOMORROW]: "fa-arrow-right",
|
||||
[GROUP_THIS_WEEK]: "fa-calendar",
|
||||
[GROUP_LATER]: "fa-calendar-o",
|
||||
};
|
||||
|
||||
// ── SVG numbered pin ────────────────────────────────────────────────
|
||||
function numberedPinSvg(fill, number) {
|
||||
const txt = String(number);
|
||||
const fontSize = txt.length > 2 ? 13 : 16;
|
||||
return (
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="38" height="50" viewBox="0 0 38 50">` +
|
||||
`<ellipse cx="19" cy="48" rx="8" ry="2.5" fill="rgba(0,0,0,.25)"/>` +
|
||||
`<path d="M19 0C8.51 0 0 8.51 0 19c0 14 19 31 19 31s19-17 19-31C38 8.51 29.49 0 19 0z" fill="${fill}" stroke="#fff" stroke-width="2"/>` +
|
||||
`<text x="19" y="${fontSize > 13 ? 24 : 23}" text-anchor="middle" fill="#fff" font-size="${fontSize}" font-family="Arial,Helvetica,sans-serif" font-weight="bold">#${txt}</text>` +
|
||||
`</svg>`
|
||||
);
|
||||
}
|
||||
function numberedPinUri(fill, number) {
|
||||
return "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(numberedPinSvg(fill, number));
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────
|
||||
let _gmapsPromise = null;
|
||||
function loadGoogleMaps(apiKey) {
|
||||
if (window.google && window.google.maps) return Promise.resolve();
|
||||
if (_gmapsPromise) return _gmapsPromise;
|
||||
_gmapsPromise = new Promise((resolve, reject) => {
|
||||
const cb = "_fc_gmap_" + Date.now();
|
||||
window[cb] = () => { delete window[cb]; resolve(); };
|
||||
const s = document.createElement("script");
|
||||
s.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&callback=${cb}`;
|
||||
s.async = true; s.defer = true;
|
||||
s.onerror = () => { _gmapsPromise = null; reject(new Error("Google Maps script failed")); };
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
return _gmapsPromise;
|
||||
}
|
||||
|
||||
function initialsOf(name) {
|
||||
if (!name) return "?";
|
||||
const p = name.trim().split(/\s+/);
|
||||
return p.length >= 2
|
||||
? (p[0][0] + p[p.length - 1][0]).toUpperCase()
|
||||
: p[0].substring(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
/** Return "YYYY-MM-DD" for a JS Date in local tz */
|
||||
function localDateStr(d) {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/** Convert float hours (e.g. 13.5) to "1:30 PM" */
|
||||
function floatToTime12(flt) {
|
||||
if (!flt && flt !== 0) return "";
|
||||
let h = Math.floor(flt);
|
||||
let m = Math.round((flt - h) * 60);
|
||||
if (m === 60) { h++; m = 0; }
|
||||
const ampm = h >= 12 ? "PM" : "AM";
|
||||
const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
||||
return `${h12}:${String(m).padStart(2, "0")} ${ampm}`;
|
||||
}
|
||||
|
||||
/** Classify a "YYYY-MM-DD" string into one of our group keys */
|
||||
function classifyDate(dateStr) {
|
||||
if (!dateStr) return GROUP_LATER;
|
||||
const now = new Date();
|
||||
const todayStr = localDateStr(now);
|
||||
|
||||
const yest = new Date(now);
|
||||
yest.setDate(yest.getDate() - 1);
|
||||
const yesterdayStr = localDateStr(yest);
|
||||
|
||||
const tmr = new Date(now);
|
||||
tmr.setDate(tmr.getDate() + 1);
|
||||
const tomorrowStr = localDateStr(tmr);
|
||||
|
||||
// End of this week (Sunday)
|
||||
const endOfWeek = new Date(now);
|
||||
endOfWeek.setDate(endOfWeek.getDate() + (7 - endOfWeek.getDay()));
|
||||
const endOfWeekStr = localDateStr(endOfWeek);
|
||||
|
||||
if (dateStr === yesterdayStr) return GROUP_YESTERDAY;
|
||||
if (dateStr === todayStr) return GROUP_TODAY;
|
||||
if (dateStr === tomorrowStr) return GROUP_TOMORROW;
|
||||
if (dateStr <= endOfWeekStr && dateStr > tomorrowStr) return GROUP_THIS_WEEK;
|
||||
if (dateStr < yesterdayStr) return GROUP_YESTERDAY; // older lumped with yesterday
|
||||
return GROUP_LATER;
|
||||
}
|
||||
|
||||
const SOURCE_COLORS = {
|
||||
westin: "#0d6efd",
|
||||
mobility: "#198754",
|
||||
};
|
||||
|
||||
/** Group + sort tasks, returning { groupKey: { label, tasks[], count } } */
|
||||
function groupTasks(tasksData, localInstanceId) {
|
||||
// Sort by date ASC, time ASC
|
||||
const sorted = [...tasksData].sort((a, b) => {
|
||||
const da = a.scheduled_date || "";
|
||||
const db = b.scheduled_date || "";
|
||||
if (da !== db) return da < db ? -1 : 1;
|
||||
return (a.time_start || 0) - (b.time_start || 0);
|
||||
});
|
||||
|
||||
const groups = {};
|
||||
const order = [GROUP_YESTERDAY, GROUP_TODAY, GROUP_TOMORROW, GROUP_THIS_WEEK, GROUP_LATER];
|
||||
for (const key of order) {
|
||||
groups[key] = {
|
||||
key,
|
||||
label: GROUP_LABELS[key],
|
||||
dayColor: DAY_COLORS[key] || "#6b7280",
|
||||
dayIcon: DAY_ICONS[key] || "fa-circle",
|
||||
tasks: [],
|
||||
count: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let globalIdx = 0;
|
||||
for (const task of sorted) {
|
||||
globalIdx++;
|
||||
const g = classifyDate(task.scheduled_date);
|
||||
task._scheduleNum = globalIdx;
|
||||
task._group = g;
|
||||
task._dayColor = DAY_COLORS[g] || "#6b7280"; // Pin colour by day
|
||||
task._statusColor = STATUS_COLORS[task.status] || "#6b7280";
|
||||
task._statusLabel = STATUS_LABELS[task.status] || task.status || "";
|
||||
task._statusIcon = STATUS_ICONS[task.status] || "fa-circle";
|
||||
task._clientName = task.x_fc_sync_client_name || (task.partner_id ? task.partner_id[1] : "N/A");
|
||||
task._techName = task.technician_id ? task.technician_id[1] : "Unassigned";
|
||||
task._typeLbl = task.task_type
|
||||
? task.task_type.charAt(0).toUpperCase() + task.task_type.slice(1).replace("_", " ")
|
||||
: "Task";
|
||||
task._timeRange = `${task.time_start_display || floatToTime12(task.time_start)} - ${task.time_end_display || ""}`;
|
||||
const src = task.x_fc_sync_source || localInstanceId || "";
|
||||
task._sourceLabel = src ? src.charAt(0).toUpperCase() + src.slice(1) : "";
|
||||
task._sourceColor = SOURCE_COLORS[src] || "#6c757d";
|
||||
task._hasCoords = task.address_lat && task.address_lng && task.address_lat !== 0 && task.address_lng !== 0;
|
||||
groups[g].tasks.push(task);
|
||||
groups[g].count++;
|
||||
}
|
||||
|
||||
// Return only non-empty groups in order
|
||||
return order.map((k) => groups[k]).filter((g) => g.count > 0);
|
||||
}
|
||||
|
||||
|
||||
// ── Controller ──────────────────────────────────────────────────────
|
||||
export class FusionTaskMapController extends Component {
|
||||
static template = "fusion_claims.FusionTaskMapView";
|
||||
static components = { Layout, SearchBar, CogMenu };
|
||||
static props = {
|
||||
...standardViewProps,
|
||||
Model: Function,
|
||||
modelParams: Object,
|
||||
Renderer: { type: Function, optional: true },
|
||||
buttonTemplate: String,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.actionService = useService("action");
|
||||
this.mapRef = useRef("mapContainer");
|
||||
|
||||
this.state = useState({
|
||||
loading: true,
|
||||
error: null,
|
||||
showTasks: true,
|
||||
showTechnicians: true,
|
||||
showTraffic: 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)
|
||||
visibleGroups: {
|
||||
[GROUP_YESTERDAY]: false, // hidden by default
|
||||
[GROUP_TODAY]: true,
|
||||
[GROUP_TOMORROW]: true,
|
||||
[GROUP_THIS_WEEK]: false, // hidden by default
|
||||
[GROUP_LATER]: false, // hidden by default
|
||||
},
|
||||
});
|
||||
|
||||
// Yesterday collapsed by default in sidebar list
|
||||
this.state.collapsedGroups[GROUP_YESTERDAY] = true;
|
||||
this.state.collapsedGroups[GROUP_LATER] = true;
|
||||
|
||||
this.map = null;
|
||||
this.taskMarkers = [];
|
||||
this.taskMarkerMap = {}; // id → marker
|
||||
this.techMarkers = [];
|
||||
this.infoWindow = null;
|
||||
this.apiKey = "";
|
||||
this.tasksData = [];
|
||||
this.locationsData = [];
|
||||
|
||||
const Model = this.props.Model;
|
||||
this.model = useModelWithSampleData(Model, this.props.modelParams);
|
||||
useSetupAction({ getLocalState: () => this._meta() });
|
||||
usePager(() => ({
|
||||
offset: this._meta().offset || 0,
|
||||
limit: this._meta().limit || 80,
|
||||
total: this.model.data?.count || this._meta().resCount || 0,
|
||||
onUpdate: ({ offset, limit }) => this.model.load({ offset, limit }),
|
||||
}));
|
||||
this.searchBarToggler = useSearchBarToggler();
|
||||
this.display = { controlPanel: {} };
|
||||
this._lastDomainStr = "";
|
||||
|
||||
onMounted(async () => {
|
||||
window.__fusionMapOpenTask = (id) => this.openTask(id);
|
||||
await this._loadAndRender();
|
||||
this._lastDomainStr = JSON.stringify(this._getDomain());
|
||||
});
|
||||
onPatched(() => {
|
||||
const cur = JSON.stringify(this._getDomain());
|
||||
if (cur !== this._lastDomainStr && this.map) {
|
||||
this._lastDomainStr = cur;
|
||||
this._onModelUpdate();
|
||||
}
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
this._clearMarkers();
|
||||
window.__fusionMapOpenTask = () => {};
|
||||
});
|
||||
}
|
||||
|
||||
// ── Model helpers (safe access across different Model types) ────
|
||||
_meta() {
|
||||
// RelationalModel uses .config, MapModel uses .metaData
|
||||
return this.model.metaData || this.model.config || {};
|
||||
}
|
||||
_getDomain() {
|
||||
const m = this._meta();
|
||||
return m.domain || [];
|
||||
}
|
||||
|
||||
// ── Data ─────────────────────────────────────────────────────────
|
||||
async _loadAndRender() {
|
||||
try {
|
||||
const domain = this._getDomain();
|
||||
const result = await this.orm.call("fusion.technician.task", "get_map_data", [domain]);
|
||||
this.apiKey = result.api_key;
|
||||
this.localInstanceId = result.local_instance_id || "";
|
||||
this.tasksData = result.tasks || [];
|
||||
this.locationsData = result.locations || [];
|
||||
this.state.taskCount = this.tasksData.length;
|
||||
this.state.techCount = this.locationsData.length;
|
||||
this.state.groups = groupTasks(this.tasksData, this.localInstanceId);
|
||||
|
||||
if (!this.apiKey) {
|
||||
this.state.error = _t("Google Maps API key not configured. Go to Settings > Fusion Claims.");
|
||||
this.state.loading = false;
|
||||
return;
|
||||
}
|
||||
await loadGoogleMaps(this.apiKey);
|
||||
if (this.mapRef.el) this._initMap();
|
||||
this.state.loading = false;
|
||||
} catch (e) {
|
||||
console.error("FusionTaskMap load error:", e);
|
||||
this.state.error = String(e);
|
||||
this.state.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async _onModelUpdate() {
|
||||
if (!this.map) return;
|
||||
try {
|
||||
const domain = this._getDomain();
|
||||
const result = await this.orm.call("fusion.technician.task", "get_map_data", [domain]);
|
||||
this.localInstanceId = result.local_instance_id || this.localInstanceId || "";
|
||||
this.tasksData = result.tasks || [];
|
||||
this.locationsData = result.locations || [];
|
||||
this.state.taskCount = this.tasksData.length;
|
||||
this.state.techCount = this.locationsData.length;
|
||||
this.state.groups = groupTasks(this.tasksData, this.localInstanceId);
|
||||
this._renderMarkers();
|
||||
} catch (e) {
|
||||
console.error("FusionTaskMap update error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Map ──────────────────────────────────────────────────────────
|
||||
_initMap() {
|
||||
if (!this.mapRef.el) return;
|
||||
this.map = new google.maps.Map(this.mapRef.el, {
|
||||
zoom: 10,
|
||||
center: { lat: 43.7, lng: -79.4 },
|
||||
mapTypeControl: true,
|
||||
streetViewControl: false,
|
||||
fullscreenControl: true,
|
||||
zoomControl: true,
|
||||
styles: [{ featureType: "poi", stylers: [{ visibility: "off" }] }],
|
||||
});
|
||||
// Traffic layer (on by default, toggleable)
|
||||
this.trafficLayer = new google.maps.TrafficLayer();
|
||||
this.trafficLayer.setMap(this.map);
|
||||
|
||||
this.infoWindow = new google.maps.InfoWindow();
|
||||
// Close popup when clicking anywhere on the map
|
||||
this.map.addListener("click", () => {
|
||||
this.infoWindow.close();
|
||||
});
|
||||
// Clear sidebar highlight when popup closes (by any means)
|
||||
this.infoWindow.addListener("closeclick", () => {
|
||||
this.state.activeTaskId = null;
|
||||
});
|
||||
this._renderMarkers();
|
||||
}
|
||||
|
||||
_clearMarkers() {
|
||||
for (const m of this.taskMarkers) m.setMap(null);
|
||||
for (const m of this.techMarkers) m.setMap(null);
|
||||
this.taskMarkers = [];
|
||||
this.taskMarkerMap = {};
|
||||
this.techMarkers = [];
|
||||
}
|
||||
|
||||
_renderMarkers() {
|
||||
this._clearMarkers();
|
||||
const bounds = new google.maps.LatLngBounds();
|
||||
let hasBounds = false;
|
||||
|
||||
// Task pins: only show groups that are enabled in the day filter
|
||||
if (this.state.showTasks) {
|
||||
for (const group of this.state.groups) {
|
||||
const groupVisible = this.state.visibleGroups[group.key] !== false;
|
||||
for (const task of group.tasks) {
|
||||
if (!task.address_lat || !task.address_lng) continue;
|
||||
if (!groupVisible) continue;
|
||||
const pos = { lat: task.address_lat, lng: task.address_lng };
|
||||
const num = task._scheduleNum;
|
||||
const color = task._dayColor;
|
||||
|
||||
const marker = new google.maps.Marker({
|
||||
position: pos,
|
||||
map: this.map,
|
||||
title: `#${num} ${task.name} - ${task._clientName}`,
|
||||
icon: {
|
||||
url: numberedPinUri(color, num),
|
||||
scaledSize: new google.maps.Size(38, 50),
|
||||
anchor: new google.maps.Point(19, 50),
|
||||
},
|
||||
zIndex: 10 + num,
|
||||
});
|
||||
|
||||
marker.addListener("click", () => this._openTaskPopup(task, marker));
|
||||
this.taskMarkers.push(marker);
|
||||
this.taskMarkerMap[task.id] = marker;
|
||||
bounds.extend(pos);
|
||||
hasBounds = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Technician markers
|
||||
if (this.state.showTechnicians) {
|
||||
for (const loc of this.locationsData) {
|
||||
if (!loc.latitude || !loc.longitude) continue;
|
||||
const pos = { lat: loc.latitude, lng: loc.longitude };
|
||||
const initials = initialsOf(loc.name);
|
||||
const svg =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">` +
|
||||
`<rect x="2" y="2" width="44" height="44" rx="12" ry="12" fill="#1d4ed8" stroke="#fff" stroke-width="3"/>` +
|
||||
`<text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-family="Arial,Helvetica,sans-serif" font-weight="bold">${initials}</text>` +
|
||||
`</svg>`;
|
||||
const marker = new google.maps.Marker({
|
||||
position: pos,
|
||||
map: this.map,
|
||||
title: loc.name,
|
||||
icon: {
|
||||
url: "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(svg),
|
||||
scaledSize: new google.maps.Size(44, 44),
|
||||
anchor: new google.maps.Point(22, 22),
|
||||
},
|
||||
zIndex: 100,
|
||||
});
|
||||
marker.addListener("click", () => {
|
||||
this.infoWindow.setContent(`
|
||||
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;min-width:200px;color:#1f2937;">
|
||||
<div style="background:#1d4ed8;color:#fff;padding:10px 14px;">
|
||||
<strong><i class="fa fa-user" style="margin-right:6px;"></i>${loc.name}</strong>
|
||||
</div>
|
||||
<div style="padding:12px 14px;font-size:13px;line-height:1.8;color:#1f2937;">
|
||||
<div><strong style="color:#374151;">Last seen:</strong> <span style="color:#111827;">${loc.logged_at || "Unknown"}</span></div>
|
||||
<div><strong style="color:#374151;">Accuracy:</strong> <span style="color:#111827;">${loc.accuracy ? Math.round(loc.accuracy) + "m" : "N/A"}</span></div>
|
||||
</div>
|
||||
</div>`);
|
||||
this.infoWindow.open(this.map, marker);
|
||||
});
|
||||
this.techMarkers.push(marker);
|
||||
bounds.extend(pos);
|
||||
hasBounds = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasBounds) {
|
||||
this.map.fitBounds(bounds);
|
||||
if (this.taskMarkers.length + this.techMarkers.length === 1) {
|
||||
this.map.setZoom(14);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_openTaskPopup(task, marker) {
|
||||
const c = task._dayColor;
|
||||
const html = `
|
||||
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;min-width:270px;max-width:360px;color:#1f2937;position:relative;">
|
||||
<div style="background:${c};color:#fff;padding:10px 14px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<strong style="font-size:14px;">#${task._scheduleNum} ${task.name}</strong>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:11px;background:rgba(255,255,255,.2);padding:2px 8px;border-radius:10px;">${task._statusLabel}</span>
|
||||
<button onclick="document.querySelector('.gm-ui-hover-effect')?.click()" title="Close"
|
||||
style="background:rgba(255,255,255,.2);border:none;color:#fff;width:24px;height:24px;border-radius:50%;cursor:pointer;font-size:16px;line-height:1;display:flex;align-items:center;justify-content:center;">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:12px 14px;font-size:13px;line-height:1.9;color:#1f2937;">
|
||||
<div><strong style="color:#374151;">Client:</strong> <span style="color:#111827;">${task._clientName}</span></div>
|
||||
<div><strong style="color:#374151;">Type:</strong> <span style="color:#111827;">${task._typeLbl}</span></div>
|
||||
<div><strong style="color:#374151;">Technician:</strong> <span style="color:#111827;">${task._techName}</span></div>
|
||||
<div><strong style="color:#374151;">Date:</strong> <span style="color:#111827;">${task.scheduled_date || ""}</span></div>
|
||||
<div><strong style="color:#374151;">Time:</strong> <span style="color:#111827;">${task._timeRange}</span></div>
|
||||
${task.address_display ? `<div><strong style="color:#374151;">Address:</strong> <span style="color:#111827;">${task.address_display}</span></div>` : ""}
|
||||
${task.travel_time_minutes ? `<div><strong style="color:#374151;">Travel:</strong> <span style="color:#111827;">${task.travel_time_minutes} min</span></div>` : ""}
|
||||
</div>
|
||||
<div style="padding:8px 14px 12px;border-top:1px solid #e5e7eb;display:flex;gap:10px;">
|
||||
<button onclick="window.__fusionMapOpenTask(${task.id})"
|
||||
style="background:${c};color:#fff;border:none;padding:6px 16px;border-radius:6px;cursor:pointer;font-size:13px;font-weight:600;">
|
||||
Open Task
|
||||
</button>
|
||||
<a href="https://www.google.com/maps/dir/?api=1&destination=${task.address_lat && task.address_lng ? task.address_lat + ',' + task.address_lng : encodeURIComponent(task.address_display || "")}"
|
||||
target="_blank" style="color:${c};text-decoration:none;font-size:13px;font-weight:600;line-height:32px;">
|
||||
Navigate →
|
||||
</a>
|
||||
</div>
|
||||
</div>`;
|
||||
this.infoWindow.setContent(html);
|
||||
this.infoWindow.open(this.map, marker);
|
||||
}
|
||||
|
||||
// ── Sidebar actions ─────────────────────────────────────────────
|
||||
toggleSidebar() {
|
||||
this.state.sidebarOpen = !this.state.sidebarOpen;
|
||||
// Trigger map resize after CSS transition
|
||||
if (this.map) {
|
||||
setTimeout(() => google.maps.event.trigger(this.map, "resize"), 320);
|
||||
}
|
||||
}
|
||||
|
||||
toggleGroup(groupKey) {
|
||||
this.state.collapsedGroups[groupKey] = !this.state.collapsedGroups[groupKey];
|
||||
}
|
||||
|
||||
isGroupCollapsed(groupKey) {
|
||||
return !!this.state.collapsedGroups[groupKey];
|
||||
}
|
||||
|
||||
focusTask(taskId) {
|
||||
this.state.activeTaskId = taskId;
|
||||
const marker = this.taskMarkerMap[taskId];
|
||||
if (marker && this.map) {
|
||||
this.map.panTo(marker.getPosition());
|
||||
this.map.setZoom(15);
|
||||
// Find the task data
|
||||
for (const g of this.state.groups) {
|
||||
for (const t of g.tasks) {
|
||||
if (t.id === taskId) {
|
||||
this._openTaskPopup(t, marker);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Day filter toggle ────────────────────────────────────────────
|
||||
toggleDayFilter(groupKey) {
|
||||
this.state.visibleGroups[groupKey] = !this.state.visibleGroups[groupKey];
|
||||
this._renderMarkers();
|
||||
}
|
||||
|
||||
isGroupVisible(groupKey) {
|
||||
return this.state.visibleGroups[groupKey] !== false;
|
||||
}
|
||||
|
||||
showAllDays() {
|
||||
for (const k of Object.keys(this.state.visibleGroups)) {
|
||||
this.state.visibleGroups[k] = true;
|
||||
}
|
||||
this._renderMarkers();
|
||||
}
|
||||
|
||||
showTodayOnly() {
|
||||
for (const k of Object.keys(this.state.visibleGroups)) {
|
||||
this.state.visibleGroups[k] = k === GROUP_TODAY;
|
||||
}
|
||||
this._renderMarkers();
|
||||
}
|
||||
|
||||
// ── Top bar actions ─────────────────────────────────────────────
|
||||
toggleTraffic() {
|
||||
this.state.showTraffic = !this.state.showTraffic;
|
||||
if (this.trafficLayer) {
|
||||
this.trafficLayer.setMap(this.state.showTraffic ? this.map : null);
|
||||
}
|
||||
}
|
||||
toggleTasks() {
|
||||
this.state.showTasks = !this.state.showTasks;
|
||||
this._renderMarkers();
|
||||
}
|
||||
toggleTechnicians() {
|
||||
this.state.showTechnicians = !this.state.showTechnicians;
|
||||
this._renderMarkers();
|
||||
}
|
||||
onRefresh() {
|
||||
this.state.loading = true;
|
||||
this._loadAndRender();
|
||||
}
|
||||
openTask(taskId) {
|
||||
this.actionService.switchView("form", { resId: taskId });
|
||||
}
|
||||
createNewTask() {
|
||||
this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "fusion.technician.task",
|
||||
views: [[false, "form"]],
|
||||
target: "new",
|
||||
context: { default_task_type: "delivery", dialog_size: "extra-large" },
|
||||
}, {
|
||||
onClose: () => {
|
||||
// Refresh map data after dialog closes (task may have been created)
|
||||
this.onRefresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.__fusionMapOpenTask = () => {};
|
||||
|
||||
// ── Minimal ArchParser for <map> tags (no web_map dependency) ───────
|
||||
class FusionMapArchParser {
|
||||
parse(xmlDoc, models, modelName) {
|
||||
const fieldNames = [];
|
||||
const activeFields = {};
|
||||
if (xmlDoc && xmlDoc.querySelectorAll) {
|
||||
for (const fieldEl of xmlDoc.querySelectorAll("field")) {
|
||||
const name = fieldEl.getAttribute("name");
|
||||
if (name) {
|
||||
fieldNames.push(name);
|
||||
activeFields[name] = { attrs: {}, options: {} };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { fieldNames, activeFields };
|
||||
}
|
||||
}
|
||||
|
||||
// ── View registration (self-contained, no @web_map dependency) ──────
|
||||
const fusionTaskMapView = {
|
||||
type: "map",
|
||||
display_name: _t("Map"),
|
||||
icon: "oi-view-map",
|
||||
multiRecord: true,
|
||||
searchMenuTypes: ["filter", "groupBy", "favorite"],
|
||||
Controller: FusionTaskMapController,
|
||||
Model: RelationalModel,
|
||||
ArchParser: FusionMapArchParser,
|
||||
buttonTemplate: "fusion_claims.FusionTaskMapView.Buttons",
|
||||
props(genericProps, view, config) {
|
||||
const { resModel, fields } = genericProps;
|
||||
let archInfo = { fieldNames: [], activeFields: {} };
|
||||
if (view && view.arch) {
|
||||
archInfo = new FusionMapArchParser().parse(view.arch);
|
||||
}
|
||||
return {
|
||||
...genericProps,
|
||||
buttonTemplate: "fusion_claims.FusionTaskMapView.Buttons",
|
||||
Model: RelationalModel,
|
||||
modelParams: {
|
||||
config: {
|
||||
resModel,
|
||||
fields,
|
||||
activeFields: archInfo.activeFields || {},
|
||||
isMonoRecord: false,
|
||||
},
|
||||
state: {
|
||||
domain: genericProps.domain || [],
|
||||
context: genericProps.context || {},
|
||||
groupBy: genericProps.groupBy || [],
|
||||
orderBy: genericProps.orderBy || [],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
registry.category("views").add("fusion_task_map", fusionTaskMapView);
|
||||
120
fusion_claims/fusion_claims/static/src/js/gallery_preview.js
Normal file
120
fusion_claims/fusion_claims/static/src/js/gallery_preview.js
Normal file
@@ -0,0 +1,120 @@
|
||||
/** @odoo-module **/
|
||||
// Fusion Claims - Gallery Preview
|
||||
// Uses Odoo's native FileViewer (same as chatter)
|
||||
// Copyright 2024-2025 Nexa Systems Inc.
|
||||
// License OPL-1
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { Many2ManyBinaryField } from "@web/views/fields/many2many_binary/many2many_binary_field";
|
||||
import { useFileViewer } from "@web/core/file_viewer/file_viewer_hook";
|
||||
import { onMounted, onWillUnmount } from "@odoo/owl";
|
||||
|
||||
/**
|
||||
* Patch Many2ManyBinaryField to use Odoo's native FileViewer
|
||||
* when inside our gallery section (fc-gallery-content class)
|
||||
*/
|
||||
patch(Many2ManyBinaryField.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
// Use Odoo's native file viewer hook (same as chatter)
|
||||
this.fileViewer = useFileViewer();
|
||||
|
||||
// Bind the click handler
|
||||
this._onGalleryClick = this._onGalleryClick.bind(this);
|
||||
|
||||
onMounted(() => {
|
||||
// Find if we're inside a gallery section
|
||||
const el = this.__owl__.bdom?.el;
|
||||
if (el) {
|
||||
const gallery = el.closest('.fc-gallery-content');
|
||||
if (gallery) {
|
||||
// Add click listener to intercept downloads
|
||||
el.addEventListener('click', this._onGalleryClick, true);
|
||||
this._galleryElement = el;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
if (this._galleryElement) {
|
||||
this._galleryElement.removeEventListener('click', this._onGalleryClick, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle clicks on attachments in gallery - intercept and use FileViewer
|
||||
*/
|
||||
_onGalleryClick(ev) {
|
||||
// Check if click is anywhere inside an attachment box
|
||||
const attachmentBox = ev.target.closest('.o_attachment');
|
||||
|
||||
if (!attachmentBox) {
|
||||
return; // Not an attachment click
|
||||
}
|
||||
|
||||
// Skip if clicking on the delete button
|
||||
if (ev.target.closest('.o_attachment_delete')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get file ID from any link or image within the attachment box
|
||||
let fileId = null;
|
||||
|
||||
// Try to get from link href
|
||||
const link = attachmentBox.querySelector('a[href*="/web/content/"], a[href*="/web/image/"]');
|
||||
if (link) {
|
||||
const href = link.getAttribute('href') || '';
|
||||
const match = href.match(/\/web\/(?:content|image)\/(\d+)/);
|
||||
if (match) {
|
||||
fileId = parseInt(match[1], 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get from image src
|
||||
if (!fileId) {
|
||||
const imgEl = attachmentBox.querySelector('img[src*="/web/image/"]');
|
||||
if (imgEl) {
|
||||
const src = imgEl.getAttribute('src') || '';
|
||||
const match = src.match(/\/web\/image\/(\d+)/);
|
||||
if (match) {
|
||||
fileId = parseInt(match[1], 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileId) {
|
||||
return; // Couldn't determine file ID
|
||||
}
|
||||
|
||||
// Prevent download
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// Get all files and transform to FileViewer format
|
||||
const files = this.files.map(file => {
|
||||
const mimetype = file.mimetype || 'image/png';
|
||||
const isImage = mimetype.startsWith('image/');
|
||||
const isPdf = mimetype === 'application/pdf';
|
||||
|
||||
return {
|
||||
id: file.id,
|
||||
name: file.name || 'File',
|
||||
mimetype: mimetype,
|
||||
isImage: isImage,
|
||||
isPdf: isPdf,
|
||||
isViewable: isImage || isPdf,
|
||||
defaultSource: isImage ? `/web/image/${file.id}` : `/web/content/${file.id}`,
|
||||
downloadUrl: `/web/content/${file.id}?download=true`,
|
||||
};
|
||||
});
|
||||
|
||||
// Find the clicked file and open FileViewer
|
||||
const clickedFile = files.find(f => f.id === fileId);
|
||||
|
||||
if (clickedFile && this.fileViewer) {
|
||||
this.fileViewer.open(clickedFile, files);
|
||||
}
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
/** @odoo-module **/
|
||||
// Fusion Claims - Preview Button Widget
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { DocumentPreviewDialog } from "./document_preview";
|
||||
|
||||
class PreviewButtonComponent extends Component {
|
||||
static template = "fusion_claims.PreviewButtonWidget";
|
||||
static props = { "*": true };
|
||||
|
||||
setup() {
|
||||
this.dialog = useService("dialog");
|
||||
this.notification = useService("notification");
|
||||
}
|
||||
|
||||
onClick() {
|
||||
const record = this.props.record;
|
||||
if (!record || !record.data) {
|
||||
this.notification.add("No document to preview.", { type: "warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
const attField = record.data.attachment_id;
|
||||
let attachmentId = null;
|
||||
if (Array.isArray(attField)) {
|
||||
attachmentId = attField[0];
|
||||
} else if (attField && typeof attField === "object" && attField.id) {
|
||||
attachmentId = attField.id;
|
||||
} else if (typeof attField === "number") {
|
||||
attachmentId = attField;
|
||||
}
|
||||
|
||||
const fileName = record.data.file_name || "Document Preview";
|
||||
|
||||
if (!attachmentId) {
|
||||
this.notification.add("No document to preview.", { type: "warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialog.add(DocumentPreviewDialog, {
|
||||
attachmentId: attachmentId,
|
||||
title: fileName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("view_widgets").add("preview_button", {
|
||||
component: PreviewButtonComponent,
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/** @odoo-module **/
|
||||
/**
|
||||
* Copyright 2024-2025 Nexa Systems Inc.
|
||||
* License OPL-1 (Odoo Proprietary License v1.0)
|
||||
*
|
||||
* Custom Selection Field that filters out wizard-required statuses from dropdown.
|
||||
* These statuses can only be set via dedicated action buttons that open reason wizards.
|
||||
*/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { SelectionField, selectionField } from "@web/views/fields/selection/selection_field";
|
||||
|
||||
// Statuses that can ONLY be set via buttons/wizards
|
||||
// These are hidden from the dropdown to enforce workflow integrity
|
||||
const CONTROLLED_STATUSES = [
|
||||
// Early workflow stages
|
||||
'assessment_scheduled', // Must use "Schedule Assessment" button
|
||||
'assessment_completed', // Must use "Complete Assessment" button
|
||||
'application_received', // Must use "Application Received" button
|
||||
'ready_submission', // Must use "Ready for Submission" button
|
||||
// Submission and approval stages
|
||||
'submitted', // Must use "Submit Application" button
|
||||
'resubmitted', // Must use "Submit Application" button
|
||||
'approved', // Must use "Mark as Approved" button
|
||||
'approved_deduction', // Must use "Mark as Approved" button
|
||||
// Billing stages
|
||||
'ready_bill', // Must use "Ready to Bill" button
|
||||
'billed', // Must use "Mark as Billed" button
|
||||
'case_closed', // Must use "Close Case" button
|
||||
// Special statuses (require reason wizard)
|
||||
'on_hold', // Must use "Put On Hold" button
|
||||
'withdrawn', // Must use "Withdraw" button
|
||||
'denied', // Must use "Denied" button
|
||||
'cancelled', // Must use "Cancel" button
|
||||
'needs_correction', // Must use "Needs Correction" button
|
||||
];
|
||||
|
||||
export class FilteredStatusSelectionField extends SelectionField {
|
||||
/**
|
||||
* Override to filter out wizard-required statuses from the options.
|
||||
* The current status is always kept so the field displays correctly.
|
||||
*/
|
||||
get options() {
|
||||
const allOptions = super.options;
|
||||
const currentValue = this.props.record.data[this.props.name];
|
||||
|
||||
// Filter out wizard-required statuses, but keep current value
|
||||
return allOptions.filter(option => {
|
||||
const [value] = option;
|
||||
// Keep the option if it's the current value OR if it's not a controlled status
|
||||
return value === currentValue || !CONTROLLED_STATUSES.includes(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
FilteredStatusSelectionField.template = "web.SelectionField";
|
||||
|
||||
export const filteredStatusSelectionField = {
|
||||
...selectionField,
|
||||
component: FilteredStatusSelectionField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("filtered_status_selection", filteredStatusSelectionField);
|
||||
@@ -0,0 +1,30 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { TaxTotalsComponent } from "@account/components/tax_totals/tax_totals";
|
||||
|
||||
/**
|
||||
* Patch TaxTotalsComponent to handle cases where subtotals is undefined
|
||||
* This fixes the "Invalid loop expression: 'undefined' is not iterable" error
|
||||
* that occurs when invoices have no tax configuration.
|
||||
*/
|
||||
patch(TaxTotalsComponent.prototype, {
|
||||
formatData(props) {
|
||||
// Call the original formatData method
|
||||
super.formatData(props);
|
||||
|
||||
// If totals exists but subtotals is undefined, set it to empty array
|
||||
if (this.totals && this.totals.subtotals === undefined) {
|
||||
this.totals.subtotals = [];
|
||||
}
|
||||
|
||||
// Also ensure each subtotal has tax_groups array
|
||||
if (this.totals && this.totals.subtotals) {
|
||||
for (const subtotal of this.totals.subtotals) {
|
||||
if (subtotal.tax_groups === undefined) {
|
||||
subtotal.tax_groups = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
872
fusion_claims/fusion_claims/static/src/scss/fusion_claims.scss
Normal file
872
fusion_claims/fusion_claims/static/src/scss/fusion_claims.scss
Normal file
@@ -0,0 +1,872 @@
|
||||
// Fusion Central - Backend Styles
|
||||
// Copyright 2024-2025 Nexa Systems Inc.
|
||||
// License OPL-1
|
||||
|
||||
.o_fusion_central {
|
||||
// Settings page styling
|
||||
.fc-settings-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h5 {
|
||||
color: #0077b6;
|
||||
border-bottom: 2px solid #0077b6;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
// Status indicators
|
||||
.fc-status-created {
|
||||
color: #28a745;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fc-status-pending {
|
||||
color: #ffc107;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// ADP portion columns styling
|
||||
.o_list_view {
|
||||
.o_field_monetary.fc-adp-portion {
|
||||
color: #0077b6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.o_field_monetary.fc-client-portion {
|
||||
color: #28a745;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATUS BUTTONS: Theme-friendly (light + dark mode)
|
||||
// Uses Odoo CSS variables with safe fallbacks.
|
||||
// =============================================================================
|
||||
|
||||
// Good / confirmed / within period (green tint)
|
||||
.fc-btn-status-good {
|
||||
background-color: rgba(40, 167, 69, 0.12) !important;
|
||||
color: #1e7e34 !important;
|
||||
border: 1px solid rgba(40, 167, 69, 0.35) !important;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: rgba(40, 167, 69, 0.22) !important;
|
||||
color: #1e7e34 !important;
|
||||
border-color: rgba(40, 167, 69, 0.5) !important;
|
||||
}
|
||||
|
||||
.fa { color: inherit !important; }
|
||||
}
|
||||
|
||||
// Bad / not confirmed / overdue (red tint)
|
||||
.fc-btn-status-bad {
|
||||
background-color: rgba(220, 53, 69, 0.12) !important;
|
||||
color: #bd2130 !important;
|
||||
border: 1px solid rgba(220, 53, 69, 0.35) !important;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: rgba(220, 53, 69, 0.22) !important;
|
||||
color: #bd2130 !important;
|
||||
border-color: rgba(220, 53, 69, 0.5) !important;
|
||||
}
|
||||
|
||||
.fa { color: inherit !important; }
|
||||
}
|
||||
|
||||
// Dark mode overrides
|
||||
html.dark, .o_dark {
|
||||
.fc-btn-status-good {
|
||||
background-color: rgba(40, 167, 69, 0.18) !important;
|
||||
color: #6fcf87 !important;
|
||||
border-color: rgba(40, 167, 69, 0.4) !important;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: rgba(40, 167, 69, 0.28) !important;
|
||||
color: #6fcf87 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-btn-status-bad {
|
||||
background-color: rgba(220, 53, 69, 0.18) !important;
|
||||
color: #f08a93 !important;
|
||||
border-color: rgba(220, 53, 69, 0.4) !important;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: rgba(220, 53, 69, 0.28) !important;
|
||||
color: #f08a93 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also support Odoo's color-scheme media query for dark mode
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.o_web_client:not(.o_light) {
|
||||
.fc-btn-status-good {
|
||||
background-color: rgba(40, 167, 69, 0.18) !important;
|
||||
color: #6fcf87 !important;
|
||||
border-color: rgba(40, 167, 69, 0.4) !important;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: rgba(40, 167, 69, 0.28) !important;
|
||||
color: #6fcf87 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-btn-status-bad {
|
||||
background-color: rgba(220, 53, 69, 0.18) !important;
|
||||
color: #f08a93 !important;
|
||||
border-color: rgba(220, 53, 69, 0.4) !important;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: rgba(220, 53, 69, 0.28) !important;
|
||||
color: #f08a93 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SALE ORDER LINE LIST: Column width control
|
||||
// Odoo 19 ignores the XML width attribute on list fields.
|
||||
// We use CSS on th[data-name] with table-layout:fixed to force widths.
|
||||
// Product column has NO explicit width so it absorbs all remaining space.
|
||||
// =============================================================================
|
||||
|
||||
.o_field_one2many[name="order_line"] .o_list_table {
|
||||
table-layout: fixed !important;
|
||||
width: 100% !important;
|
||||
|
||||
// ---- Product column: gets ALL remaining space (no width set) ----
|
||||
// Truncate long product names with ellipsis
|
||||
th[data-name="product_template_id"],
|
||||
td[name="product_template_id"],
|
||||
th[data-name="product_id"],
|
||||
td[name="product_id"] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// ---- Serial Number: generous width ----
|
||||
th[data-name="x_fc_serial_number"] { width: 140px !important; }
|
||||
|
||||
// ---- Quantity columns ----
|
||||
th[data-name="product_uom_qty"] { width: 55px !important; }
|
||||
th[data-name="qty_delivered"] { width: 55px !important; }
|
||||
th[data-name="qty_invoiced"] { width: 55px !important; }
|
||||
|
||||
// ---- UoM ----
|
||||
th[data-name="product_uom_id"] { width: 50px !important; }
|
||||
|
||||
// ---- Price / Discount / Tax / Subtotal ----
|
||||
th[data-name="price_unit"] { width: 80px !important; }
|
||||
th[data-name="tax_ids"] { width: 70px !important; }
|
||||
th[data-name="discount"] { width: 45px !important; }
|
||||
th[data-name="price_subtotal"] { width: 90px !important; }
|
||||
|
||||
// ---- ADP / Client Portion ----
|
||||
th[data-name="x_fc_adp_portion"] { width: 80px !important; }
|
||||
th[data-name="x_fc_client_portion"] { width: 80px !important; }
|
||||
|
||||
// ---- sale_margin optional columns ----
|
||||
th[data-name="purchase_price"] { width: 70px !important; }
|
||||
th[data-name="margin"] { width: 65px !important; }
|
||||
th[data-name="margin_percent"] { width: 55px !important; }
|
||||
|
||||
// ---- Description (hidden by default, but set width in case user shows it) ----
|
||||
th[data-name="name"] { width: 120px !important; }
|
||||
|
||||
// Tax tags: compact badges
|
||||
td[name="tax_ids"] .badge {
|
||||
font-size: 0.72em;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
// All cells: allow text truncation
|
||||
td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive: scale text on smaller screens
|
||||
@media (max-width: 1400px) {
|
||||
.o_field_one2many[name="order_line"] .o_list_table {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.o_field_one2many[name="order_line"] .o_list_table {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
}
|
||||
|
||||
// Form view styling for ADP fields
|
||||
.o_form_view {
|
||||
.fc-adp-totals {
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #0077b6;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
|
||||
.fc-total-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.fc-total-value {
|
||||
font-size: 1.1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.fc-adp-value {
|
||||
color: #0077b6;
|
||||
}
|
||||
|
||||
.fc-client-value {
|
||||
color: #28a745;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ADP Summary Line Details - constrain product column width
|
||||
.o_fc_line_details {
|
||||
.o_list_table {
|
||||
table-layout: fixed !important;
|
||||
width: 100% !important;
|
||||
|
||||
// Product column - first column
|
||||
td:first-child,
|
||||
th:first-child {
|
||||
max-width: 300px !important;
|
||||
width: 40% !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
// Other columns - auto size
|
||||
td:not(:first-child),
|
||||
th:not(:first-child) {
|
||||
width: auto !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CHATTER WIDTH CUSTOMIZATION
|
||||
// 80%/20% split ONLY on desktop (>= 992px).
|
||||
// On mobile/tablet, Odoo's default stacking (chatter below form) takes over.
|
||||
// ONLY applies to main action forms, NOT modal dialogs/wizards.
|
||||
// =============================================================================
|
||||
|
||||
@media (min-width: 992px) {
|
||||
// Only apply to non-modal forms (forms in the main action area, not in dialogs)
|
||||
.o_action_manager > .o_action > .o_form_view .o_form_renderer {
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
|
||||
// Form content takes 80% of space
|
||||
> .o_form_sheet_bg {
|
||||
flex: 0 0 80% !important;
|
||||
width: 80% !important;
|
||||
min-width: 0 !important;
|
||||
max-width: 80% !important;
|
||||
}
|
||||
|
||||
// Chatter container - 20% of screen
|
||||
> .o-mail-ChatterContainer,
|
||||
> .o-mail-Form-chatter,
|
||||
> .o-aside {
|
||||
flex: 0 0 20% !important;
|
||||
width: 20% !important;
|
||||
min-width: 20% !important;
|
||||
max-width: 20% !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Additional backup selectors for chatter (non-modal only)
|
||||
.o_action_manager .o-mail-ChatterContainer.o-aside {
|
||||
flex: 0 0 20% !important;
|
||||
width: 20% !important;
|
||||
min-width: 20% !important;
|
||||
max-width: 20% !important;
|
||||
}
|
||||
|
||||
// Force the form sheet content to expand within its container (non-modal only)
|
||||
.o_action_manager .o_form_sheet_bg {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
// Also target the inner form sheet (non-modal only)
|
||||
.o_action_manager .o_form_sheet {
|
||||
max-width: none !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Make chatter content more compact (all screen sizes)
|
||||
.o-mail-Thread {
|
||||
.o-mail-Message {
|
||||
padding: 6px 10px !important;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
// Compact activity section
|
||||
.o-mail-Activity {
|
||||
padding: 4px 8px !important;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Icon-only chatter topbar buttons (ALL screen sizes)
|
||||
// "Send message" and "Log note" have RAW TEXT inside (no <span>).
|
||||
// "WhatsApp" and "Activity" wrap text in <span>.
|
||||
// We use font-size:0 to hide text, then inject icons via ::before.
|
||||
// =============================================================================
|
||||
|
||||
.o-mail-Chatter-topbar {
|
||||
gap: 4px;
|
||||
|
||||
// --- Send message (raw text, no span) -> envelope icon ---
|
||||
.o-mail-Chatter-sendMessage {
|
||||
font-size: 0 !important;
|
||||
padding: 8px 12px !important;
|
||||
min-width: auto;
|
||||
line-height: 1;
|
||||
|
||||
&::before {
|
||||
font-family: "Font Awesome 5 Free", FontAwesome;
|
||||
font-weight: 900;
|
||||
font-size: 15px;
|
||||
content: "\f0e0"; // fa-envelope
|
||||
}
|
||||
}
|
||||
|
||||
// --- Log note (raw text, no span) -> edit icon ---
|
||||
.o-mail-Chatter-logNote {
|
||||
font-size: 0 !important;
|
||||
padding: 8px 12px !important;
|
||||
min-width: auto;
|
||||
line-height: 1;
|
||||
|
||||
&::before {
|
||||
font-family: "Font Awesome 5 Free", FontAwesome;
|
||||
font-weight: 900;
|
||||
font-size: 15px;
|
||||
content: "\f044"; // fa-edit / fa-pencil-square-o
|
||||
}
|
||||
}
|
||||
|
||||
// --- WhatsApp (text in <span>, target via hotkey) -> WhatsApp SVG ---
|
||||
button[data-hotkey="shift+w"] {
|
||||
> span { display: none !important; }
|
||||
padding: 8px 12px !important;
|
||||
min-width: auto;
|
||||
line-height: 1;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
background-color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Activity (text in <span>) -> calendar icon ---
|
||||
.o-mail-Chatter-activity {
|
||||
> span { display: none !important; }
|
||||
padding: 8px 12px !important;
|
||||
min-width: auto;
|
||||
line-height: 1;
|
||||
|
||||
&::before {
|
||||
font-family: "Font Awesome 5 Free", FontAwesome;
|
||||
font-weight: 900;
|
||||
font-size: 15px;
|
||||
content: "\f073"; // fa-calendar
|
||||
}
|
||||
}
|
||||
|
||||
// --- Message Authorizer (text in <span>) -> custom SVG ---
|
||||
.o-mail-Chatter-messageAuthorizer {
|
||||
> span { display: none !important; }
|
||||
padding: 8px 12px !important;
|
||||
min-width: auto;
|
||||
line-height: 1;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50'%3E%3Cpath d='M 25 -0.03125 C 11.839844 -0.03125 10.148438 4.851563 10 5.40625 C 9.988281 5.453125 9.976563 5.484375 9.96875 5.53125 L 7.96875 21.5 C 7.929688 21.828125 8.070313 22.148438 8.3125 22.375 C 8.410156 22.464844 8.503906 22.546875 8.625 22.59375 C 8.363281 23.386719 8.015625 24.71875 7.75 27.03125 C 7.75 27.042969 7.75 27.050781 7.75 27.0625 C 7.304688 27.746094 7 28.65625 7 29.8125 C 7 32.0625 8.582031 33.878906 10.65625 34.40625 C 12.003906 37.898438 13.675781 41.625 15.90625 44.59375 C 18.230469 47.683594 21.238281 50 25 50 C 28.761719 50 31.769531 47.683594 34.09375 44.59375 C 36.324219 41.625 37.996094 37.898438 39.34375 34.40625 C 41.429688 33.886719 43 32.070313 43 29.8125 C 43 28.613281 42.699219 27.6875 42.25 27 C 41.984375 24.707031 41.636719 23.382813 41.375 22.59375 C 41.496094 22.546875 41.589844 22.464844 41.6875 22.375 C 41.929688 22.148438 42.074219 21.828125 42.03125 21.5 L 40.03125 5.53125 C 40.023438 5.484375 40.011719 5.453125 40 5.40625 C 39.851563 4.851563 38.160156 -0.03125 25 -0.03125 Z M 24 6 L 26 6 L 26 10 L 30 10 L 30 12 L 26 12 L 26 16 L 24 16 L 24 12 L 20 12 L 20 10 L 24 10 Z M 25 20.78125 C 29.371094 20.78125 34.777344 21.605469 38 22.15625 L 38 27.65625 L 39.15625 27.46875 C 39.15625 27.46875 39.628906 27.390625 40.0625 27.59375 C 40.496094 27.796875 41 28.15625 41 29.8125 C 41 31.300781 39.898438 32.449219 38.5 32.59375 L 37.90625 32.65625 L 37.6875 33.25 C 36.34375 36.785156 34.621094 40.554688 32.5 43.375 C 30.378906 46.195313 27.941406 48 25 48 C 22.058594 48 19.621094 46.195313 17.5 43.375 C 15.378906 40.554688 13.6875 36.785156 12.34375 33.25 L 12.125 32.65625 L 11.5 32.59375 C 10.097656 32.449219 9 31.300781 9 29.8125 C 9 28.234375 9.484375 27.878906 9.9375 27.65625 C 10.390625 27.433594 10.875 27.46875 10.875 27.46875 L 12 27.59375 L 12 22.15625 C 15.222656 21.605469 20.625 20.78125 25 20.78125 Z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50'%3E%3Cpath d='M 25 -0.03125 C 11.839844 -0.03125 10.148438 4.851563 10 5.40625 C 9.988281 5.453125 9.976563 5.484375 9.96875 5.53125 L 7.96875 21.5 C 7.929688 21.828125 8.070313 22.148438 8.3125 22.375 C 8.410156 22.464844 8.503906 22.546875 8.625 22.59375 C 8.363281 23.386719 8.015625 24.71875 7.75 27.03125 C 7.75 27.042969 7.75 27.050781 7.75 27.0625 C 7.304688 27.746094 7 28.65625 7 29.8125 C 7 32.0625 8.582031 33.878906 10.65625 34.40625 C 12.003906 37.898438 13.675781 41.625 15.90625 44.59375 C 18.230469 47.683594 21.238281 50 25 50 C 28.761719 50 31.769531 47.683594 34.09375 44.59375 C 36.324219 41.625 37.996094 37.898438 39.34375 34.40625 C 41.429688 33.886719 43 32.070313 43 29.8125 C 43 28.613281 42.699219 27.6875 42.25 27 C 41.984375 24.707031 41.636719 23.382813 41.375 22.59375 C 41.496094 22.546875 41.589844 22.464844 41.6875 22.375 C 41.929688 22.148438 42.074219 21.828125 42.03125 21.5 L 40.03125 5.53125 C 40.023438 5.484375 40.011719 5.453125 40 5.40625 C 39.851563 4.851563 38.160156 -0.03125 25 -0.03125 Z M 24 6 L 26 6 L 26 10 L 30 10 L 30 12 L 26 12 L 26 16 L 24 16 L 24 12 L 20 12 L 20 10 L 24 10 Z M 25 20.78125 C 29.371094 20.78125 34.777344 21.605469 38 22.15625 L 38 27.65625 L 39.15625 27.46875 C 39.15625 27.46875 39.628906 27.390625 40.0625 27.59375 C 40.496094 27.796875 41 28.15625 41 29.8125 C 41 31.300781 39.898438 32.449219 38.5 32.59375 L 37.90625 32.65625 L 37.6875 33.25 C 36.34375 36.785156 34.621094 40.554688 32.5 43.375 C 30.378906 46.195313 27.941406 48 25 48 C 22.058594 48 19.621094 46.195313 17.5 43.375 C 15.378906 40.554688 13.6875 36.785156 12.34375 33.25 L 12.125 32.65625 L 11.5 32.59375 C 10.097656 32.449219 9 31.300781 9 29.8125 C 9 28.234375 9.484375 27.878906 9.9375 27.65625 C 10.390625 27.433594 10.875 27.46875 10.875 27.46875 L 12 27.59375 L 12 22.15625 C 15.222656 21.605469 20.625 20.78125 25 20.78125 Z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
background-color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Mic button (already icon-only from fusion_notes) ---
|
||||
.fusion-notes-mic-btn,
|
||||
.o-mail-Chatter-voiceNote {
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// XML VIEWER STYLES
|
||||
// =============================================================================
|
||||
|
||||
.xml-viewer-content {
|
||||
overflow: auto;
|
||||
background: #1e1e1e;
|
||||
|
||||
.xml-code {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #d4d4d4;
|
||||
background: transparent;
|
||||
white-space: pre;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
// Syntax highlighting colors (VS Code dark theme inspired)
|
||||
.xml-tag {
|
||||
color: #569cd6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.xml-attr {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.xml-value {
|
||||
color: #ce9178;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ADP DOCUMENTS TILE LAYOUT
|
||||
// =============================================================================
|
||||
|
||||
.fc-document-tiles {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.fc-document-tile {
|
||||
width: 220px;
|
||||
min-width: 220px;
|
||||
background: var(--o-bg-card, var(--bs-body-bg));
|
||||
border: 1px solid var(--o-border-color, var(--bs-border-color));
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--o-action, var(--bs-primary));
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
// Preview area
|
||||
.fc-tile-preview {
|
||||
height: 140px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--o-bg-200, var(--bs-secondary-bg));
|
||||
border-bottom: 1px solid var(--o-border-color, var(--bs-border-color));
|
||||
position: relative;
|
||||
|
||||
.fc-pdf-icon {
|
||||
font-size: 48px;
|
||||
color: var(--o-danger, var(--bs-danger));
|
||||
}
|
||||
|
||||
.fc-xml-icon {
|
||||
font-size: 48px;
|
||||
color: var(--o-info, var(--bs-info));
|
||||
}
|
||||
|
||||
.fc-empty-icon {
|
||||
font-size: 48px;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.fc-thumbnail {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.fc-upload-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
i {
|
||||
font-size: 32px;
|
||||
color: white;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.fc-tile-empty:hover .fc-upload-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// Tile info area
|
||||
.fc-tile-info {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
|
||||
.fc-tile-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.fc-tile-filename {
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.fc-tile-empty-text {
|
||||
font-size: 11px;
|
||||
opacity: 0.5;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
// Actions bar
|
||||
.fc-tile-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 1px solid var(--o-border-color, var(--bs-border-color));
|
||||
background: var(--o-bg-100, var(--bs-tertiary-bg));
|
||||
padding: 8px;
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--o-action, var(--bs-primary));
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid var(--o-border-color, var(--bs-border-color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Has file state
|
||||
&.fc-tile-filled {
|
||||
border-color: var(--o-success, var(--bs-success));
|
||||
}
|
||||
|
||||
// Required field indicator
|
||||
&:has(.o_required_modifier) {
|
||||
.fc-tile-label::after {
|
||||
content: " *";
|
||||
color: var(--o-danger, var(--bs-danger));
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:not(:has(.o_field_binary[value])) {
|
||||
border-color: var(--o-warning, var(--bs-warning));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Section headers for document groups
|
||||
.fc-doc-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.fc-doc-section-header {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid var(--o-action, var(--bs-primary));
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 8px;
|
||||
color: var(--o-action, var(--bs-primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Style the upload field in tiles
|
||||
.fc-tile-upload-field {
|
||||
width: 100%;
|
||||
|
||||
.o_select_file_button {
|
||||
width: 100%;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
font-size: 12px !important;
|
||||
padding: 8px !important;
|
||||
|
||||
&:hover {
|
||||
background: var(--o-action, var(--bs-primary)) !important;
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.o_file_name {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.o_input_file {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&.o_field_binary {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.o_form_binary_progress {
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix button styling in tiles
|
||||
.fc-document-tile {
|
||||
.btn-link {
|
||||
text-decoration: none !important;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.fc-pdf-icon:hover {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// APPROVAL SCREENSHOTS GALLERY
|
||||
// =============================================================================
|
||||
|
||||
.fc-gallery-section {
|
||||
background: var(--o-bg-100, var(--bs-tertiary-bg));
|
||||
border: 2px solid var(--o-border-color, var(--bs-border-color));
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 8px;
|
||||
|
||||
.fc-gallery-header {
|
||||
padding-bottom: 12px;
|
||||
|
||||
.fc-tile-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-gallery-content {
|
||||
padding-top: 8px;
|
||||
|
||||
// Style the many2many_binary widget as a gallery
|
||||
.o_field_many2many_binary {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
gap: 12px !important;
|
||||
justify-content: flex-start !important;
|
||||
align-items: flex-start !important;
|
||||
padding-top: 4px !important;
|
||||
|
||||
// Each file item as a thumbnail card
|
||||
.o_attachments {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
gap: 12px !important;
|
||||
align-items: flex-start !important;
|
||||
|
||||
.o_attachment {
|
||||
width: 80px !important;
|
||||
height: 80px !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
overflow: hidden !important;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
cursor: pointer !important;
|
||||
position: relative !important;
|
||||
border: 2px solid transparent !important;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
|
||||
border-color: #0077b6 !important;
|
||||
}
|
||||
|
||||
// Image preview thumbnail - clicking opens in new tab
|
||||
.o_image {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover !important;
|
||||
}
|
||||
|
||||
// File icon for non-images
|
||||
.o_attachment_icon {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
background: #e9ecef !important;
|
||||
|
||||
i {
|
||||
font-size: 28px !important;
|
||||
color: #6c757d !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide filename inside thumbnail
|
||||
.o_attachment_name {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// Delete button styling
|
||||
.o_attachment_delete {
|
||||
position: absolute !important;
|
||||
top: 2px !important;
|
||||
right: 2px !important;
|
||||
background: rgba(220, 53, 69, 0.9) !important;
|
||||
color: white !important;
|
||||
border-radius: 50% !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
font-size: 10px !important;
|
||||
opacity: 0 !important;
|
||||
transition: opacity 0.2s !important;
|
||||
z-index: 5 !important;
|
||||
}
|
||||
|
||||
&:hover .o_attachment_delete {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upload button - inline compact style
|
||||
.o_attach,
|
||||
button.o_attach,
|
||||
.o_select_file_button {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
gap: 4px !important;
|
||||
padding: 6px 12px !important;
|
||||
border: 1px solid #28a745 !important;
|
||||
border-radius: 4px !important;
|
||||
background: white !important;
|
||||
color: #28a745 !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 500 !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.2s ease !important;
|
||||
height: auto !important;
|
||||
width: auto !important;
|
||||
min-height: 32px !important;
|
||||
margin-top: 4px !important;
|
||||
|
||||
&:hover {
|
||||
background: #28a745 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
i, .fa {
|
||||
font-size: 12px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-gallery-empty {
|
||||
padding: 16px;
|
||||
|
||||
.fa {
|
||||
color: #adb5bd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Google Places Autocomplete dropdown - ensure it appears above Odoo modals
|
||||
.pac-container {
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
|
||||
|
||||
204
fusion_claims/fusion_claims/static/src/xml/document_preview.xml
Normal file
204
fusion_claims/fusion_claims/static/src/xml/document_preview.xml
Normal file
@@ -0,0 +1,204 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<templates xml:space="preserve">
|
||||
<!-- PDF Document Preview Dialog -->
|
||||
<t t-name="fusion_claims.DocumentPreviewDialog">
|
||||
<Dialog size="getDialogSize()" footer="false">
|
||||
<t t-set-slot="header">
|
||||
<div class="d-flex align-items-center justify-content-between w-100">
|
||||
<div style="width: 50px"></div>
|
||||
<h4 class="modal-title text-break fw-normal mb-0">
|
||||
<i class="fa fa-file-pdf-o me-2 text-danger"/>
|
||||
<t t-esc="props.title" />
|
||||
</h4>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
t-on-click="toggleMaximize"
|
||||
t-att-title="state.isMaximized ? 'Exit Fullscreen' : 'Fullscreen'">
|
||||
<i t-attf-class="fa {{ state.isMaximized ? 'fa-compress' : 'fa-expand' }}" />
|
||||
</button>
|
||||
<a t-att-href="getViewerUrl()"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
title="Open in New Tab">
|
||||
<i class="fa fa-external-link"/>
|
||||
</a>
|
||||
<button type="button" class="btn-close ms-2" t-on-click="props.close"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div class="position-relative bg-secondary">
|
||||
<!-- Loading spinner -->
|
||||
<div t-if="state.isLoading"
|
||||
class="position-absolute w-100 h-100 d-flex justify-content-center align-items-center bg-light"
|
||||
style="z-index: 10; min-height: 400px;">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="text-muted mb-0">Loading document...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF.js viewer iframe - handles XFA/protected PDFs -->
|
||||
<iframe t-att-src="getViewerUrl()"
|
||||
class="border-0"
|
||||
t-att-style="getFrameStyle()"
|
||||
t-on-load="onIframeLoad"
|
||||
allowfullscreen="true" />
|
||||
</div>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
<!-- XML Viewer Dialog -->
|
||||
<t t-name="fusion_claims.XMLViewerDialog">
|
||||
<Dialog size="getDialogSize()" footer="false">
|
||||
<t t-set-slot="header">
|
||||
<div class="d-flex align-items-center justify-content-between w-100">
|
||||
<div style="width: 50px"></div>
|
||||
<h4 class="modal-title text-break fw-normal mb-0">
|
||||
<i class="fa fa-file-code-o me-2 text-info"/>
|
||||
<t t-esc="props.title" />
|
||||
</h4>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
t-on-click="copyToClipboard"
|
||||
title="Copy to Clipboard">
|
||||
<i class="fa fa-clipboard"/>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
t-on-click="toggleMaximize"
|
||||
t-att-title="state.isMaximized ? 'Exit Fullscreen' : 'Fullscreen'">
|
||||
<i t-attf-class="fa {{ state.isMaximized ? 'fa-compress' : 'fa-expand' }}" />
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
t-on-click="downloadXml"
|
||||
title="Download XML">
|
||||
<i class="fa fa-download"/>
|
||||
</button>
|
||||
<button type="button" class="btn-close ms-2" t-on-click="props.close"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div class="position-relative">
|
||||
<!-- Loading spinner -->
|
||||
<div t-if="state.isLoading"
|
||||
class="d-flex justify-content-center align-items-center bg-light"
|
||||
style="min-height: 400px;">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="text-muted mb-0">Loading XML...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
<div t-if="state.error" class="alert alert-danger m-3">
|
||||
<i class="fa fa-exclamation-triangle me-2"/>
|
||||
<t t-esc="state.error"/>
|
||||
</div>
|
||||
|
||||
<!-- XML content with syntax highlighting -->
|
||||
<div t-if="!state.isLoading and !state.error"
|
||||
class="xml-viewer-content"
|
||||
t-att-style="state.isMaximized ? 'height: calc(98vh - 120px);' : 'height: calc(85vh - 120px);'">
|
||||
<pre class="xml-code m-0 p-3"><code t-out="state.formattedXml"/></pre>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
<!-- Image Preview Dialog -->
|
||||
<t t-name="fusion_claims.ImagePreviewDialog">
|
||||
<Dialog size="'xl'" footer="false">
|
||||
<t t-set-slot="header">
|
||||
<div class="d-flex align-items-center justify-content-between w-100">
|
||||
<div style="width: 100px">
|
||||
<span t-if="hasMultiple" class="badge bg-secondary">
|
||||
<t t-esc="currentPosition"/>
|
||||
</span>
|
||||
</div>
|
||||
<h4 class="modal-title text-break fw-normal mb-0">
|
||||
<i class="fa fa-image me-2 text-success"/>
|
||||
<t t-esc="currentImage.name" />
|
||||
</h4>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
t-on-click="downloadImage"
|
||||
title="Download Image">
|
||||
<i class="fa fa-download"/>
|
||||
</button>
|
||||
<button type="button" class="btn-close ms-2" t-on-click="props.close"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div class="position-relative d-flex align-items-center justify-content-center bg-dark"
|
||||
style="min-height: 500px; max-height: 80vh;">
|
||||
|
||||
<!-- Loading spinner -->
|
||||
<div t-if="state.isLoading"
|
||||
class="position-absolute w-100 h-100 d-flex justify-content-center align-items-center"
|
||||
style="z-index: 10;">
|
||||
<div class="spinner-border text-light" role="status" style="width: 3rem; height: 3rem;">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Previous button -->
|
||||
<button t-if="hasMultiple and state.currentIndex > 0"
|
||||
type="button"
|
||||
class="btn btn-dark btn-lg position-absolute start-0 ms-3"
|
||||
style="z-index: 20; opacity: 0.7;"
|
||||
t-on-click="previousImage">
|
||||
<i class="fa fa-chevron-left fa-2x"/>
|
||||
</button>
|
||||
|
||||
<!-- Image -->
|
||||
<img t-att-src="imageUrl"
|
||||
class="mw-100 mh-100"
|
||||
style="object-fit: contain; max-height: 75vh;"
|
||||
t-on-load="onImageLoad"
|
||||
t-att-alt="currentImage.name"/>
|
||||
|
||||
<!-- Next button -->
|
||||
<button t-if="hasMultiple and state.currentIndex < props.images.length - 1"
|
||||
type="button"
|
||||
class="btn btn-dark btn-lg position-absolute end-0 me-3"
|
||||
style="z-index: 20; opacity: 0.7;"
|
||||
t-on-click="nextImage">
|
||||
<i class="fa fa-chevron-right fa-2x"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Thumbnail strip for multiple images -->
|
||||
<div t-if="hasMultiple" class="d-flex justify-content-center gap-2 p-3 bg-secondary">
|
||||
<t t-foreach="props.images" t-as="img" t-key="img.id">
|
||||
<div t-att-class="'border-2 rounded overflow-hidden cursor-pointer ' + (img_index === state.currentIndex ? 'border-primary' : 'border-transparent')"
|
||||
style="width: 60px; height: 60px; cursor: pointer;"
|
||||
t-on-click="() => { this.state.isLoading = true; this.state.currentIndex = img_index; }">
|
||||
<img t-att-src="'/web/image/' + img.id + '?height=60'"
|
||||
class="w-100 h-100"
|
||||
style="object-fit: cover;"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</Dialog>
|
||||
</t>
|
||||
<!-- Preview Button Widget (no-save, client-side only) -->
|
||||
<t t-name="fusion_claims.PreviewButtonWidget">
|
||||
<button type="button"
|
||||
class="btn btn-link p-0 border-0"
|
||||
title="Preview"
|
||||
t-on-click="onClick">
|
||||
<i class="fa fa-eye"/>
|
||||
</button>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,225 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_claims.FusionTaskMapView">
|
||||
<div class="o_fusion_task_map_view">
|
||||
<Layout display="display">
|
||||
<t t-set-slot="control-panel-additional-actions">
|
||||
<CogMenu/>
|
||||
</t>
|
||||
<t t-set-slot="layout-buttons">
|
||||
<t t-call="{{ props.buttonTemplate }}"/>
|
||||
</t>
|
||||
<t t-set-slot="layout-actions">
|
||||
<SearchBar toggler="searchBarToggler"/>
|
||||
</t>
|
||||
<t t-set-slot="control-panel-navigation-additional">
|
||||
<t t-component="searchBarToggler.component" t-props="searchBarToggler.props"/>
|
||||
</t>
|
||||
|
||||
<div class="fc_map_wrapper">
|
||||
|
||||
<!-- ========== SIDEBAR ========== -->
|
||||
<div t-att-class="'fc_sidebar' + (state.sidebarOpen ? '' : ' fc_sidebar--collapsed')">
|
||||
|
||||
<!-- Sidebar header -->
|
||||
<div class="fc_sidebar_header">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<h6 class="mb-0 fw-bold">
|
||||
<i class="fa fa-list-ul me-2"/>Deliveries
|
||||
<span class="badge text-bg-primary ms-1" t-esc="state.taskCount"/>
|
||||
</h6>
|
||||
<button class="btn btn-sm btn-link text-muted p-0" t-on-click="toggleSidebar"
|
||||
title="Toggle sidebar">
|
||||
<i t-att-class="'fa ' + (state.sidebarOpen ? 'fa-chevron-left' : 'fa-chevron-right')"/>
|
||||
</button>
|
||||
</div>
|
||||
<!-- New task button -->
|
||||
<button class="btn btn-primary btn-sm w-100 mt-2" t-on-click="createNewTask">
|
||||
<i class="fa fa-plus me-1"/>New Delivery Task
|
||||
</button>
|
||||
|
||||
<!-- Day filter chips -->
|
||||
<div class="fc_day_filters mt-2">
|
||||
<t t-foreach="state.groups" t-as="group" t-key="group.key + '_filter'">
|
||||
<button t-att-class="'fc_day_chip' + (isGroupVisible(group.key) ? ' fc_day_chip--active' : '')"
|
||||
t-att-style="isGroupVisible(group.key) ? 'background:' + group.dayColor + ';color:#fff;border-color:' + group.dayColor : ''"
|
||||
t-on-click="() => this.toggleDayFilter(group.key)">
|
||||
<t t-esc="group.label"/>
|
||||
<span class="fc_day_chip_count" t-esc="group.count"/>
|
||||
</button>
|
||||
</t>
|
||||
<button class="fc_day_chip fc_day_chip--all" t-on-click="showAllDays"
|
||||
title="Show all">All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar body: grouped task list -->
|
||||
<div class="fc_sidebar_body">
|
||||
<t t-foreach="state.groups" t-as="group" t-key="group.key">
|
||||
<!-- Group header (collapsible) with day color -->
|
||||
<div class="fc_group_header" t-on-click="() => this.toggleGroup(group.key)">
|
||||
<i t-att-class="'fa me-1 ' + (isGroupCollapsed(group.key) ? 'fa-caret-right' : 'fa-caret-down')"/>
|
||||
<i class="fa fa-circle me-1" style="font-size:8px;"
|
||||
t-att-style="'color:' + group.dayColor"/>
|
||||
<span class="fc_group_label" t-esc="group.label"/>
|
||||
<span t-if="!isGroupVisible(group.key)" class="fc_group_hidden_tag">hidden</span>
|
||||
<span class="fc_group_badge" t-esc="group.count"/>
|
||||
</div>
|
||||
|
||||
<!-- Group tasks -->
|
||||
<div t-if="!isGroupCollapsed(group.key)" class="fc_group_tasks">
|
||||
<t t-foreach="group.tasks" t-as="task" t-key="task.id">
|
||||
<div t-att-class="'fc_task_card' + (state.activeTaskId === task.id ? ' fc_task_card--active' : '')"
|
||||
t-on-click="() => this.focusTask(task.id)">
|
||||
|
||||
<!-- Card top row: number + status -->
|
||||
<div class="fc_task_card_top">
|
||||
<span class="fc_task_num" t-att-style="'background:' + task._dayColor">
|
||||
<t t-esc="'#' + task._scheduleNum"/>
|
||||
</span>
|
||||
<span class="fc_task_status" t-att-style="'color:' + task._statusColor">
|
||||
<i t-att-class="'fa ' + task._statusIcon" style="margin-right:3px;"/>
|
||||
<t t-esc="task._statusLabel"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Client name -->
|
||||
<div class="fc_task_client" t-esc="task._clientName"/>
|
||||
|
||||
<!-- Type + time -->
|
||||
<div class="fc_task_meta">
|
||||
<span><i class="fa fa-tag me-1"/><t t-esc="task._typeLbl"/></span>
|
||||
<span><i class="fa fa-clock-o me-1"/><t t-esc="task._timeRange"/></span>
|
||||
</div>
|
||||
|
||||
<!-- Technician + address -->
|
||||
<div class="fc_task_detail">
|
||||
<span><i class="fa fa-user me-1"/><t t-esc="task._techName"/></span>
|
||||
</div>
|
||||
<div t-if="task.address_display" class="fc_task_address">
|
||||
<i class="fa fa-map-marker me-1"/>
|
||||
<t t-esc="task.address_display"/>
|
||||
</div>
|
||||
|
||||
<!-- Travel + source -->
|
||||
<div class="fc_task_bottom_row">
|
||||
<span t-if="task.travel_time_minutes" class="fc_task_travel">
|
||||
<i class="fa fa-car me-1"/>
|
||||
<t t-esc="task.travel_time_minutes"/> min travel
|
||||
</span>
|
||||
<span t-if="task._sourceLabel" class="fc_task_source"
|
||||
t-att-style="'background:' + task._sourceColor">
|
||||
<i class="fa fa-building-o me-1"/>
|
||||
<t t-esc="task._sourceLabel"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div t-if="state.groups.length === 0 and !state.loading" class="fc_sidebar_empty">
|
||||
<i class="fa fa-inbox fa-2x text-muted d-block mb-2"/>
|
||||
<span class="text-muted">No tasks found</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar footer: technician count -->
|
||||
<div class="fc_sidebar_footer">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<svg width="14" height="14" viewBox="0 0 48 48">
|
||||
<rect x="2" y="2" width="44" height="44" rx="12" ry="12" fill="#1d4ed8" stroke="#fff" stroke-width="3"/>
|
||||
<text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-family="Arial,sans-serif" font-weight="bold">T</text>
|
||||
</svg>
|
||||
<small class="text-muted">
|
||||
<t t-esc="state.techCount"/> technician(s) online
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collapsed sidebar toggle -->
|
||||
<button t-if="!state.sidebarOpen"
|
||||
class="fc_sidebar_toggle_btn" t-on-click="toggleSidebar"
|
||||
title="Open sidebar">
|
||||
<i class="fa fa-chevron-right"/>
|
||||
</button>
|
||||
|
||||
<!-- ========== MAP AREA ========== -->
|
||||
<div class="fc_map_area">
|
||||
<!-- Legend bar -->
|
||||
<div class="fc_map_legend_bar d-flex align-items-center gap-3 px-3 py-2 border-bottom bg-view flex-wrap">
|
||||
<button class="btn btn-sm d-flex align-items-center gap-1"
|
||||
t-att-class="state.showTasks ? 'btn-primary' : 'btn-outline-secondary'"
|
||||
t-on-click="toggleTasks">
|
||||
<i class="fa fa-map-marker"/>Tasks
|
||||
<span class="badge text-bg-secondary ms-1" t-esc="state.taskCount"/>
|
||||
</button>
|
||||
<button class="btn btn-sm d-flex align-items-center gap-1"
|
||||
t-att-class="state.showTechnicians ? 'btn-primary' : 'btn-outline-secondary'"
|
||||
t-on-click="toggleTechnicians">
|
||||
<i class="fa fa-user"/>Techs
|
||||
<span class="badge text-bg-secondary ms-1" t-esc="state.techCount"/>
|
||||
</button>
|
||||
<span class="border-start mx-1" style="height:20px;"/>
|
||||
<span class="text-muted fw-bold" style="font-size:11px;">Pins:</span>
|
||||
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#ef4444;"/>Today</span>
|
||||
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#3b82f6;"/>Tomorrow</span>
|
||||
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#10b981;"/>This Week</span>
|
||||
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#a855f7;"/>Upcoming</span>
|
||||
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#9ca3af;"/>Yesterday</span>
|
||||
<span class="flex-grow-1"/>
|
||||
<button class="btn btn-sm d-flex align-items-center gap-1"
|
||||
t-att-class="state.showTraffic ? 'btn-warning' : 'btn-outline-secondary'"
|
||||
t-on-click="toggleTraffic" title="Toggle traffic layer">
|
||||
<i class="fa fa-car"/>Traffic
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" t-on-click="onRefresh" title="Refresh">
|
||||
<i class="fa fa-refresh" t-att-class="{'fa-spin': state.loading}"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Map container -->
|
||||
<div class="fc_map_container">
|
||||
<div t-ref="mapContainer" style="position:absolute;top:0;left:0;right:0;bottom:0;"/>
|
||||
|
||||
<!-- Loading -->
|
||||
<div t-if="state.loading"
|
||||
class="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center"
|
||||
style="z-index:10;background:rgba(255,255,255,.92);">
|
||||
<div class="text-center">
|
||||
<i class="fa fa-spinner fa-spin fa-3x text-primary mb-3 d-block"/>
|
||||
<span class="text-muted">Loading Google Maps...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div t-if="state.error"
|
||||
class="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center"
|
||||
style="z-index:10;background:rgba(255,255,255,.92);">
|
||||
<div class="alert alert-danger m-4" role="alert">
|
||||
<i class="fa fa-exclamation-triangle me-2"/><t t-esc="state.error"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div t-if="!state.loading and !state.error and state.taskCount === 0 and state.techCount === 0"
|
||||
class="position-absolute top-50 start-50 translate-middle text-center" style="z-index:5;">
|
||||
<div class="bg-white rounded-3 shadow p-4">
|
||||
<i class="fa fa-map-marker fa-3x text-muted mb-3 d-block"/>
|
||||
<h5>No locations to show</h5>
|
||||
<p class="text-muted mb-0">Try adjusting the filters or date range.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="fusion_claims.FusionTaskMapView.Buttons"/>
|
||||
|
||||
</templates>
|
||||
26
fusion_claims/fusion_claims/views/account_journal_views.xml
Normal file
26
fusion_claims/fusion_claims/views/account_journal_views.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<odoo>
|
||||
<!-- Add "Requires Card Digits" checkbox to payment method lines in journal form -->
|
||||
<record id="view_account_journal_form_fc" model="ir.ui.view">
|
||||
<field name="name">account.journal.form.fc</field>
|
||||
<field name="model">account.journal</field>
|
||||
<field name="inherit_id" ref="account.view_account_journal_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Add column to inbound payment methods -->
|
||||
<xpath expr="//field[@name='inbound_payment_method_line_ids']/list/field[@name='name']" position="after">
|
||||
<field name="x_fc_requires_card_digits" string="Req. Card #"
|
||||
help="Check this box if this payment method requires the last 4 digits of the card"/>
|
||||
</xpath>
|
||||
<!-- Add column to outbound payment methods -->
|
||||
<xpath expr="//field[@name='outbound_payment_method_line_ids']/list/field[@name='name']" position="after">
|
||||
<field name="x_fc_requires_card_digits" string="Req. Card #"
|
||||
help="Check this box if this payment method requires the last 4 digits of the card"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
394
fusion_claims/fusion_claims/views/account_move_views.xml
Normal file
394
fusion_claims/fusion_claims/views/account_move_views.xml
Normal file
@@ -0,0 +1,394 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<odoo>
|
||||
<!-- ===================================================================== -->
|
||||
<!-- INVOICE FORM: Header Fields -->
|
||||
<!-- ===================================================================== -->
|
||||
<record id="view_move_form_fusion_claims_header" model="ir.ui.view">
|
||||
<field name="name">account.move.form.fusion.central.header</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="priority">50</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Add hidden detection flags -->
|
||||
<xpath expr="//field[@name='move_type']" position="after">
|
||||
<field name="x_fc_is_adp_invoice" invisible="1"/>
|
||||
<field name="x_fc_show_authorizer" invisible="1"/>
|
||||
<field name="x_fc_show_authorizer_question" invisible="1"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Add Portion badge at the top (header area) for split invoices -->
|
||||
<xpath expr="//div[hasclass('oe_title')]" position="before">
|
||||
<div class="float-end" invisible="move_type not in ['out_invoice', 'out_refund'] or x_fc_adp_invoice_portion == 'full'">
|
||||
<field name="x_fc_adp_invoice_portion" widget="badge" readonly="1"
|
||||
decoration-info="x_fc_adp_invoice_portion == 'client'"
|
||||
decoration-warning="x_fc_adp_invoice_portion == 'adp'"/>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
<!-- Add Invoice Type, Authorizer Required?, Authorizer, Client Type after Journal div -->
|
||||
<xpath expr="//div[@name='journal_div']" position="after">
|
||||
<field name="x_fc_invoice_type" string="Invoice Type"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund']"/>
|
||||
|
||||
<!-- Authorizer Required? - only for odsp, direct_private, insurance, other, rental -->
|
||||
<field name="x_fc_authorizer_required" string="Authorizer Required?"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_show_authorizer_question"/>
|
||||
|
||||
<!-- Authorizer - shown based on invoice type and authorizer_required -->
|
||||
<field name="x_fc_authorizer_id" string="Authorizer"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_show_authorizer"
|
||||
options="{'no_create': False, 'no_quick_create': False}"/>
|
||||
|
||||
<field name="x_fc_client_type" string="Client Type"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- INVOICE FORM: Export Button, Sync Button, and Device Verification -->
|
||||
<!-- ===================================================================== -->
|
||||
<record id="view_move_form_fusion_claims_button" model="ir.ui.view">
|
||||
<field name="name">account.move.form.fusion.central.button</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="priority">52</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Add Export and Sync buttons to header -->
|
||||
<xpath expr="//button[@name='button_draft']" position="after">
|
||||
<field name="x_fc_is_adp_invoice" invisible="1"/>
|
||||
<field name="x_fc_needs_device_verification" invisible="1"/>
|
||||
<!-- Verify Device Approval button - only on client invoices that need verification -->
|
||||
<button name="action_open_device_approval_wizard"
|
||||
string="Verify Device Approval"
|
||||
type="object"
|
||||
class="btn-warning"
|
||||
icon="fa-check-square-o"
|
||||
invisible="not x_fc_needs_device_verification"
|
||||
help="Complete device verification to enable ADP invoice creation"/>
|
||||
<button name="action_sync_to_sale_order"
|
||||
string="Sync All"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
icon="fa-refresh"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice"
|
||||
help="Sync ADP fields from this Invoice to the Sale Order and all linked invoices"/>
|
||||
<button name="action_export_adp_claim"
|
||||
string="Export ADP"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
icon="fa-file-text-o"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or state != 'posted' or not x_fc_is_adp_invoice"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- INVOICE FORM: ADP Billing Status Statusbar -->
|
||||
<!-- ===================================================================== -->
|
||||
<record id="view_move_form_fusion_claims_billing_status" model="ir.ui.view">
|
||||
<field name="name">account.move.form.fusion.central.billing.status</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="priority">49</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Add ADP Billing Status statusbar after the main header -->
|
||||
<xpath expr="//header" position="inside">
|
||||
<field name="x_fc_is_adp_invoice" invisible="1"/>
|
||||
<field name="x_fc_is_mod_invoice" invisible="1"/>
|
||||
<field name="x_fc_adp_billing_status" widget="statusbar" nolabel="1"
|
||||
statusbar_visible="waiting,submitted,payment_issued"
|
||||
clickable="1"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice or x_fc_adp_invoice_portion != 'adp'"/>
|
||||
<!-- Send to MOD button for March of Dimes invoices -->
|
||||
<button name="action_mod_send_invoice" type="object"
|
||||
string="Send to Case Worker" class="btn-primary" icon="fa-paper-plane"
|
||||
invisible="move_type != 'out_invoice' or not x_fc_is_mod_invoice"
|
||||
help="Send this invoice to the March of Dimes case worker"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- INVOICE FORM: Device Verification Alert Banner -->
|
||||
<!-- ===================================================================== -->
|
||||
<record id="view_move_form_fusion_claims_verification_alert" model="ir.ui.view">
|
||||
<field name="name">account.move.form.fusion.central.verification.alert</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="priority">51</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Add verification alert banner at the top of the form -->
|
||||
<xpath expr="//sheet" position="before">
|
||||
<field name="x_fc_needs_device_verification" invisible="1"/>
|
||||
<field name="x_fc_adp_invoice_portion" invisible="1"/>
|
||||
<!-- Alert for client invoices needing device verification -->
|
||||
<div class="alert alert-warning d-flex align-items-center" role="alert"
|
||||
invisible="not x_fc_needs_device_verification">
|
||||
<i class="fa fa-exclamation-triangle fa-2x me-3" title="Verification Pending"/>
|
||||
<div>
|
||||
<strong>Device Verification Pending</strong>
|
||||
<p class="mb-0">
|
||||
ADP funding approval verification is required before creating the ADP invoice.
|
||||
Click <strong>Verify Device Approval</strong> to complete verification.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- INVOICE FORM: ADP Case Details Tab -->
|
||||
<!-- ===================================================================== -->
|
||||
<record id="view_move_form_fusion_claims_adp_tab" model="ir.ui.view">
|
||||
<field name="name">account.move.form.fusion.central.adp.tab</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="priority">55</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Add ADP Case Details tab in notebook -->
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<!-- Use computed x_fc_is_adp_invoice which checks BOTH Studio and module fields -->
|
||||
<field name="x_fc_is_adp_invoice" invisible="1"/>
|
||||
<page string="ADP Case Details" name="adp_case_details"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice">
|
||||
<group>
|
||||
<group string="Claim Information">
|
||||
<field name="x_fc_claim_number"/>
|
||||
<field name="x_fc_client_ref_1"/>
|
||||
<field name="x_fc_client_ref_2"/>
|
||||
</group>
|
||||
<group string="Dates">
|
||||
<field name="x_fc_adp_delivery_date"/>
|
||||
<field name="x_fc_service_start_date"/>
|
||||
<field name="x_fc_service_end_date"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Export Status">
|
||||
<field name="adp_exported" readonly="1"/>
|
||||
<field name="adp_export_date" readonly="1" invisible="not adp_exported"/>
|
||||
<field name="adp_export_count" readonly="1" invisible="not adp_exported"/>
|
||||
</group>
|
||||
</group>
|
||||
<group invisible="x_fc_adp_invoice_portion != 'adp'">
|
||||
<group string="ADP Billing Status">
|
||||
<field name="x_fc_adp_billing_status"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- INVOICE FORM: ADP Summary Tab -->
|
||||
<!-- ===================================================================== -->
|
||||
<record id="view_move_form_fusion_claims_tab" model="ir.ui.view">
|
||||
<field name="name">account.move.form.fusion.central.tab</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="priority">60</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Add ADP Summary tab in notebook -->
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<!-- Use computed x_fc_is_adp_invoice which checks BOTH Studio and module fields -->
|
||||
<page string="ADP Summary" name="adp_summary"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice">
|
||||
|
||||
<!-- 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"/>
|
||||
|
||||
<div class="alert alert-warning mb-3" role="alert" invisible="not x_fc_has_deductions">
|
||||
<strong><i class="fa fa-minus-circle"/> Deductions Applied</strong>
|
||||
<p class="mb-0">
|
||||
ADP deductions have been applied to this invoice.
|
||||
Total deduction impact: <strong><field name="x_fc_total_deduction_amount" widget="monetary" class="oe_inline"/></strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="ADP Portion Breakdown">
|
||||
<field name="x_fc_adp_portion_total" string="Total ADP Portion" widget="monetary"/>
|
||||
<field name="x_fc_client_portion_total" string="Total Client Portion" widget="monetary"/>
|
||||
</group>
|
||||
<group string="Invoice Information">
|
||||
<field name="x_fc_invoice_type" string="Invoice Type" readonly="1"/>
|
||||
<field name="x_fc_client_type" string="Client Type" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Invoice line details for ADP Summary - filtered product lines with conditional deduction columns -->
|
||||
<field name="x_fc_product_lines" mode="list" readonly="1" nolabel="1" class="w-100"
|
||||
context="{'show_deductions': x_fc_has_deductions}">
|
||||
<list string="Line Details" create="0" delete="0" limit="50"
|
||||
decoration-success="x_fc_adp_approved"
|
||||
decoration-danger="not x_fc_adp_approved and x_fc_adp_device_type"
|
||||
decoration-warning="x_fc_deduction_type and x_fc_deduction_type != 'none'">
|
||||
<field name="x_fc_adp_approved" string="✓" widget="boolean" width="40px"
|
||||
help="Device approved by ADP"/>
|
||||
<field name="name" string="Description" class="text-wrap"/>
|
||||
<field name="x_fc_adp_device_type" string="Device Type" optional="show" width="150px"/>
|
||||
<field name="x_fc_serial_number" string="S/N" optional="show" width="90px"/>
|
||||
<field name="quantity" string="Qty" width="60px"/>
|
||||
<field name="price_unit" string="Unit $" width="100px"/>
|
||||
<field name="x_fc_deduction_type" string="Ded." width="70px"
|
||||
column_invisible="not parent.x_fc_has_deductions"/>
|
||||
<field name="x_fc_deduction_value" string="Ded.$" width="80px"
|
||||
column_invisible="not parent.x_fc_has_deductions"/>
|
||||
<field name="x_fc_adp_portion" string="ADP $" width="100px"/>
|
||||
<field name="x_fc_client_portion" string="Client $" width="100px"/>
|
||||
<field name="price_subtotal" string="Total" width="100px"/>
|
||||
<field name="x_fc_device_placement" string="Plcmt" optional="hide" width="70px"/>
|
||||
</list>
|
||||
</field>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<strong>Calculation Rules:</strong>
|
||||
<ul>
|
||||
<li><strong>REG Client:</strong> ADP Portion = 75%, Client Portion = 25%</li>
|
||||
<li><strong>ODS/OWP/ACS/LTC/SEN/CCA:</strong> ADP Portion = 100%, Client Portion = 0%</li>
|
||||
<li><strong>PCT Deduction:</strong> ADP pays X% of their normal portion</li>
|
||||
<li><strong>AMT Deduction:</strong> Fixed $ amount deducted from ADP portion</li>
|
||||
</ul>
|
||||
<button name="action_recalculate_adp_portions" type="object"
|
||||
string="Recalculate Portions" class="btn-primary"
|
||||
help="Manually recalculate ADP and Client portions"/>
|
||||
</div>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- INVOICE LINE: Add S/N, ADP Portion, Client Portion columns -->
|
||||
<!-- ===================================================================== -->
|
||||
<record id="view_move_form_line_fusion_claims" model="ir.ui.view">
|
||||
<field name="name">account.move.form.line.fusion.central</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="priority">65</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Add serial number column right after product name/description - always visible for invoices -->
|
||||
<xpath expr="//field[@name='invoice_line_ids']//field[@name='name']" position="after">
|
||||
<field name="x_fc_serial_number" string="S/N" width="100px"
|
||||
column_invisible="parent.move_type not in ['out_invoice', 'out_refund']"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Add ADP Portion and Client Portion columns after taxes - only for ADP invoices -->
|
||||
<xpath expr="//field[@name='invoice_line_ids']//field[@name='tax_ids']" position="after">
|
||||
<field name="x_fc_adp_portion" string="ADP" widget="monetary" optional="show"
|
||||
column_invisible="parent.move_type not in ['out_invoice', 'out_refund'] or not parent.x_fc_is_adp_invoice"/>
|
||||
<field name="x_fc_client_portion" string="Client" widget="monetary" optional="show"
|
||||
column_invisible="parent.move_type not in ['out_invoice', 'out_refund'] or not parent.x_fc_is_adp_invoice"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- INVOICE FORM: ADP Totals in Summary Footer -->
|
||||
<!-- ===================================================================== -->
|
||||
<record id="view_move_form_fusion_claims_totals" model="ir.ui.view">
|
||||
<field name="name">account.move.form.fusion.central.totals</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="priority">70</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Add ADP portion totals inside the subtotal footer group -->
|
||||
<xpath expr="//group[hasclass('oe_subtotal_footer')]" position="inside">
|
||||
<field name="x_fc_is_adp_invoice" invisible="1"/>
|
||||
<field name="x_fc_adp_invoice_portion" invisible="1"/>
|
||||
|
||||
<!-- ADP Portion Breakdown - always show for ADP invoices -->
|
||||
<div colspan="2" invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice">
|
||||
<hr class="mt-2 mb-2"/>
|
||||
</div>
|
||||
|
||||
<!-- Total ADP Portion (sum of line ADP portions) -->
|
||||
<label for="x_fc_adp_portion_total" string="Total ADP Portion:"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice"/>
|
||||
<field name="x_fc_adp_portion_total" widget="monetary" nolabel="1"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice"/>
|
||||
|
||||
<!-- Total Client Portion (sum of line client portions) -->
|
||||
<label for="x_fc_client_portion_total" string="Total Client Portion:"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice"/>
|
||||
<field name="x_fc_client_portion_total" widget="monetary" nolabel="1"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice"/>
|
||||
|
||||
<!-- Sibling Invoice Totals -->
|
||||
<div colspan="2" invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice or x_fc_adp_invoice_portion == 'full'">
|
||||
<hr class="mt-1 mb-1"/>
|
||||
</div>
|
||||
<!-- On Client invoice (25%), show the ADP invoice total -->
|
||||
<label for="x_fc_sibling_adp_total" string="ADP Invoice Total:"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice or x_fc_adp_invoice_portion != 'client'"/>
|
||||
<field name="x_fc_sibling_adp_total" widget="monetary" nolabel="1"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice or x_fc_adp_invoice_portion != 'client'"/>
|
||||
<!-- On ADP invoice (75%), show the Client invoice total -->
|
||||
<label for="x_fc_sibling_client_total" string="Client Invoice Total:"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice or x_fc_adp_invoice_portion != 'adp'"/>
|
||||
<field name="x_fc_sibling_client_total" widget="monetary" nolabel="1"
|
||||
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice or x_fc_adp_invoice_portion != 'adp'"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- INVOICE SEARCH: Filters -->
|
||||
<!-- ===================================================================== -->
|
||||
<record id="view_account_invoice_filter_fusion_claims" model="ir.ui.view">
|
||||
<field name="name">account.move.search.fusion.central</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_account_invoice_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//search" position="inside">
|
||||
<separator/>
|
||||
<filter string="ADP Invoices" name="adp_invoices"
|
||||
domain="[('x_fc_invoice_type', 'in', ['adp', 'adp_odsp'])]"/>
|
||||
<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/>
|
||||
<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/>
|
||||
<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/>
|
||||
<filter string="Billing: Waiting" name="billing_waiting"
|
||||
domain="[('x_fc_adp_billing_status', '=', 'waiting')]"/>
|
||||
<filter string="Billing: Submitted" name="billing_submitted"
|
||||
domain="[('x_fc_adp_billing_status', '=', 'submitted')]"/>
|
||||
<filter string="Billing: Need Correction" name="billing_need_correction"
|
||||
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')]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- SERVER ACTION: Bulk Export -->
|
||||
<!-- ===================================================================== -->
|
||||
<record id="action_adp_export_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Export ADP Claims</field>
|
||||
<field name="res_model">fusion_claims.export.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="account.model_account_move"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
</record>
|
||||
</odoo>
|
||||
1163
fusion_claims/fusion_claims/views/adp_claims_views.xml
Normal file
1163
fusion_claims/fusion_claims/views/adp_claims_views.xml
Normal file
File diff suppressed because it is too large
Load Diff
87
fusion_claims/fusion_claims/views/client_chat_views.xml
Normal file
87
fusion_claims/fusion_claims/views/client_chat_views.xml
Normal file
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<!-- ================================================================= -->
|
||||
<!-- CLIENT CHAT SESSION VIEWS -->
|
||||
<!-- ================================================================= -->
|
||||
|
||||
<!-- Chat Session Form View (conversation UI) -->
|
||||
<record id="view_fusion_client_chat_session_form" model="ir.ui.view">
|
||||
<field name="name">fusion.client.chat.session.form</field>
|
||||
<field name="model">fusion.client.chat.session</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Client Intelligence Chat">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="profile_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="user_id" readonly="1"/>
|
||||
<field name="state"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Conversation History -->
|
||||
<separator string="Conversation"/>
|
||||
<field name="message_ids" nolabel="1" readonly="1">
|
||||
<list decoration-info="role == 'user'" decoration-success="role == 'assistant'"
|
||||
create="0" delete="0" edit="0">
|
||||
<field name="timestamp" width="160px"/>
|
||||
<field name="role" width="80px"/>
|
||||
<field name="content"/>
|
||||
</list>
|
||||
</field>
|
||||
|
||||
<!-- Input Area -->
|
||||
<separator string="Ask a Question"/>
|
||||
<group>
|
||||
<field name="user_input" nolabel="1"
|
||||
placeholder="Type your question here... e.g., 'How many clients are from Brampton?' or 'What devices has this client received?'"
|
||||
widget="text"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_send_message" type="object"
|
||||
string="Send" class="btn-primary" icon="fa-paper-plane"/>
|
||||
<button string="Close" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Chat Session List View -->
|
||||
<record id="view_fusion_client_chat_session_list" model="ir.ui.view">
|
||||
<field name="name">fusion.client.chat.session.list</field>
|
||||
<field name="model">fusion.client.chat.session</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Chat Sessions">
|
||||
<field name="name"/>
|
||||
<field name="profile_id"/>
|
||||
<field name="user_id"/>
|
||||
<field name="create_date"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- ACTIONS -->
|
||||
<!-- ================================================================= -->
|
||||
|
||||
<!-- Global Chat Action (create new session) -->
|
||||
<record id="action_fc_client_chat_new" model="ir.actions.act_window">
|
||||
<field name="name">Client Intelligence</field>
|
||||
<field name="res_model">fusion.client.chat.session</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">current</field>
|
||||
<field name="context">{'default_name': 'New Chat'}</field>
|
||||
</record>
|
||||
|
||||
<!-- Chat History Action -->
|
||||
<record id="action_fc_client_chat_history" model="ir.actions.act_window">
|
||||
<field name="name">Chat History</field>
|
||||
<field name="res_model">fusion.client.chat.session</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
723
fusion_claims/fusion_claims/views/client_profile_views.xml
Normal file
723
fusion_claims/fusion_claims/views/client_profile_views.xml
Normal file
@@ -0,0 +1,723 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<!-- ================================================================= -->
|
||||
<!-- CLIENT PROFILE VIEWS -->
|
||||
<!-- ================================================================= -->
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="view_fusion_client_profile_form" model="ir.ui.view">
|
||||
<field name="name">fusion.client.profile.form</field>
|
||||
<field name="model">fusion.client.profile</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Client Profile">
|
||||
<header>
|
||||
<button name="action_open_ai_chat" type="object"
|
||||
string="Ask AI" icon="fa-comments"
|
||||
class="btn-primary"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_claims" type="object"
|
||||
class="oe_stat_button" icon="fa-folder-open">
|
||||
<field name="claim_count" widget="statinfo" string="Claims"/>
|
||||
</button>
|
||||
<button name="action_view_applications" type="object"
|
||||
class="oe_stat_button" icon="fa-file-text">
|
||||
<field name="application_count" widget="statinfo" string="Applications"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title mb-3">
|
||||
<label for="partner_id" string="Odoo Contact"/>
|
||||
<h1>
|
||||
<field name="partner_id" placeholder="Link to Contact..." style="font-size: 1.5rem;"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Identification">
|
||||
<field name="first_name"/>
|
||||
<field name="middle_initial" invisible="not middle_initial"/>
|
||||
<field name="last_name"/>
|
||||
<field name="health_card_number"/>
|
||||
<field name="health_card_version"/>
|
||||
<field name="date_of_birth"/>
|
||||
<field name="ltch_name"/>
|
||||
</group>
|
||||
<group string="Financials">
|
||||
<field name="total_adp_funded" widget="monetary"/>
|
||||
<field name="total_client_portion" widget="monetary"/>
|
||||
<field name="total_amount" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="last_assessment_date"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Address & Contact" name="address">
|
||||
<group>
|
||||
<group string="Address">
|
||||
<field name="unit_number"/>
|
||||
<field name="street_number"/>
|
||||
<field name="street_name"/>
|
||||
<field name="rural_route"/>
|
||||
<field name="city"/>
|
||||
<field name="province"/>
|
||||
<field name="postal_code"/>
|
||||
</group>
|
||||
<group string="Contact">
|
||||
<field name="home_phone" widget="phone"/>
|
||||
<field name="business_phone" widget="phone"/>
|
||||
<field name="phone_extension"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Medical & Benefits" name="medical">
|
||||
<group>
|
||||
<group string="Benefits Eligibility">
|
||||
<field name="receives_social_assistance"/>
|
||||
<field name="benefit_type"
|
||||
invisible="not receives_social_assistance"/>
|
||||
<field name="wsib_eligible"/>
|
||||
<field name="vac_eligible"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Current Medical Condition / Diagnosis"/>
|
||||
<field name="medical_condition" nolabel="1"
|
||||
placeholder="Medical condition/diagnosis from latest application..."/>
|
||||
<separator string="Functional Mobility Status"/>
|
||||
<field name="mobility_status" nolabel="1"
|
||||
placeholder="Functional mobility status from latest application..."/>
|
||||
</page>
|
||||
<page string="Application History" name="applications">
|
||||
<field name="application_data_ids" nolabel="1">
|
||||
<list>
|
||||
<field name="application_date"/>
|
||||
<field name="device_category"/>
|
||||
<field name="base_device"/>
|
||||
<field name="reason_for_application"/>
|
||||
<field name="authorizer_first_name"/>
|
||||
<field name="authorizer_last_name"/>
|
||||
<field name="sections_submitted"/>
|
||||
<field name="sale_order_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="AI Analysis" name="ai">
|
||||
<group string="Summary">
|
||||
<field name="ai_summary" nolabel="1" readonly="1" widget="text"/>
|
||||
</group>
|
||||
<group string="Risk Flags">
|
||||
<field name="ai_risk_flags" nolabel="1" readonly="1" widget="text"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- List View -->
|
||||
<record id="view_fusion_client_profile_list" model="ir.ui.view">
|
||||
<field name="name">fusion.client.profile.list</field>
|
||||
<field name="model">fusion.client.profile</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Client Profiles">
|
||||
<field name="last_name"/>
|
||||
<field name="first_name"/>
|
||||
<field name="health_card_number"/>
|
||||
<field name="date_of_birth"/>
|
||||
<field name="city"/>
|
||||
<field name="claim_count"/>
|
||||
<field name="total_adp_funded" widget="monetary" sum="Total ADP"/>
|
||||
<field name="total_client_portion" widget="monetary" sum="Total Client"/>
|
||||
<field name="last_assessment_date"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View -->
|
||||
<record id="view_fusion_client_profile_search" model="ir.ui.view">
|
||||
<field name="name">fusion.client.profile.search</field>
|
||||
<field name="model">fusion.client.profile</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Client Profiles">
|
||||
<field name="first_name"/>
|
||||
<field name="last_name"/>
|
||||
<field name="health_card_number"/>
|
||||
<field name="city"/>
|
||||
<field name="postal_code"/>
|
||||
<field name="medical_condition"/>
|
||||
<separator/>
|
||||
<filter string="Has Active Claims" name="has_claims"
|
||||
domain="[('claim_count', '>', 0)]"/>
|
||||
<filter string="Receives Social Assistance" name="social_assistance"
|
||||
domain="[('receives_social_assistance', '=', True)]"/>
|
||||
<filter string="WSIB Eligible" name="wsib"
|
||||
domain="[('wsib_eligible', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="City" name="group_city" context="{'group_by': 'city'}"/>
|
||||
<filter string="Province" name="group_province" context="{'group_by': 'province'}"/>
|
||||
<filter string="Benefit Type" name="group_benefit" context="{'group_by': 'benefit_type'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_fusion_client_profiles" model="ir.actions.act_window">
|
||||
<field name="name">Client Profiles</field>
|
||||
<field name="res_model">fusion.client.profile</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No client profiles yet
|
||||
</p>
|
||||
<p>
|
||||
Client profiles are automatically created when ADP XML files are
|
||||
uploaded to sale orders or imported via Configuration > Import XML Files.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================= -->
|
||||
<!-- ADP APPLICATION DATA VIEWS -->
|
||||
<!-- ================================================================= -->
|
||||
|
||||
<!-- Application Data Form View -->
|
||||
<record id="view_fusion_adp_application_data_form" model="ir.ui.view">
|
||||
<field name="name">fusion.adp.application.data.form</field>
|
||||
<field name="model">fusion.adp.application.data</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="ADP Application Data">
|
||||
<header>
|
||||
<button name="action_export_xml" type="object"
|
||||
string="Export XML" icon="fa-download"
|
||||
class="btn-secondary"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<!-- Top summary -->
|
||||
<group>
|
||||
<group string="Application Info">
|
||||
<field name="profile_id"/>
|
||||
<field name="sale_order_id"/>
|
||||
<field name="device_category"/>
|
||||
<field name="version_number"/>
|
||||
<field name="application_date"/>
|
||||
<field name="sections_submitted"/>
|
||||
</group>
|
||||
<group string="Applicant">
|
||||
<field name="applicant_first_name"/>
|
||||
<field name="applicant_last_name"/>
|
||||
<field name="applicant_middle_initial"/>
|
||||
<field name="health_card_number"/>
|
||||
<field name="health_card_version"/>
|
||||
<field name="date_of_birth"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<!-- ========== SECTION 1 ========== -->
|
||||
<page string="Section 1 - Applicant" name="section1">
|
||||
<group>
|
||||
<group string="Address">
|
||||
<field name="unit_number"/>
|
||||
<field name="street_number"/>
|
||||
<field name="street_name"/>
|
||||
<field name="rural_route"/>
|
||||
<field name="city"/>
|
||||
<field name="province"/>
|
||||
<field name="postal_code"/>
|
||||
</group>
|
||||
<group string="Contact">
|
||||
<field name="home_phone" widget="phone"/>
|
||||
<field name="business_phone" widget="phone"/>
|
||||
<field name="phone_extension"/>
|
||||
<field name="ltch_name"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Confirmation of Benefits">
|
||||
<group>
|
||||
<field name="receives_social_assistance"/>
|
||||
<field name="benefit_owp"/>
|
||||
<field name="benefit_odsp"/>
|
||||
<field name="benefit_acsd"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="wsib_eligible"/>
|
||||
<field name="vac_eligible"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<!-- ========== SECTION 2 - DEVICES ========== -->
|
||||
<page string="Section 2 - Devices" name="section2">
|
||||
<group string="Medical Condition and Mobility Status">
|
||||
<field name="medical_condition" nolabel="1" placeholder="Presenting medical condition..."/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="mobility_status" nolabel="1" placeholder="Functional mobility status..."/>
|
||||
</group>
|
||||
|
||||
<separator string="Mobility Equipment Previously Funded by ADP"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="prev_funded_none"/>
|
||||
<field name="prev_funded_forearm"/>
|
||||
<field name="prev_funded_wheeled"/>
|
||||
<field name="prev_funded_manual"/>
|
||||
<field name="prev_funded_power"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="prev_funded_addon"/>
|
||||
<field name="prev_funded_scooter"/>
|
||||
<field name="prev_funded_seating"/>
|
||||
<field name="prev_funded_tilt"/>
|
||||
<field name="prev_funded_recline"/>
|
||||
<field name="prev_funded_legrests"/>
|
||||
<field name="prev_funded_frame"/>
|
||||
<field name="prev_funded_stroller"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<separator string="Devices Currently Required"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="device_forearm_crutches"/>
|
||||
<field name="device_wheeled_walker"/>
|
||||
<field name="device_manual_wheelchair"/>
|
||||
<field name="device_ambulation_manual"/>
|
||||
<field name="device_dependent_wheelchair"/>
|
||||
<field name="device_dynamic_tilt"/>
|
||||
<field name="device_manual_dynamic"/>
|
||||
<field name="device_manual_power_addon"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="device_power_base"/>
|
||||
<field name="device_power_scooter"/>
|
||||
<field name="device_ambulation_power"/>
|
||||
<field name="device_positioning"/>
|
||||
<field name="device_high_tech"/>
|
||||
<field name="device_standing_frame"/>
|
||||
<field name="device_adp_funded_mods"/>
|
||||
<field name="device_non_adp_funded_mods"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<!-- ========== SECTION 2A - WALKERS ========== -->
|
||||
<page string="Section 2a - Walkers" name="section2a">
|
||||
<group>
|
||||
<group string="Base Device">
|
||||
<field name="s2a_base_device"/>
|
||||
<field name="s2a_paediatric_frame"/>
|
||||
<field name="s2a_forearm_crutches"/>
|
||||
</group>
|
||||
<group string="Reason for Application">
|
||||
<field name="s2a_reason"/>
|
||||
<field name="s2a_replacement_status"/>
|
||||
<field name="s2a_replacement_size"/>
|
||||
<field name="s2a_replacement_adp"/>
|
||||
<field name="s2a_replacement_special"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Prescription Details">
|
||||
<group>
|
||||
<field name="s2a_seat_height"/>
|
||||
<field name="s2a_seat_height_unit"/>
|
||||
<field name="s2a_handle_height"/>
|
||||
<field name="s2a_handle_height_unit"/>
|
||||
<field name="s2a_hand_grips"/>
|
||||
<field name="s2a_forearm_attachments"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2a_width_handles"/>
|
||||
<field name="s2a_width_handles_unit"/>
|
||||
<field name="s2a_client_weight"/>
|
||||
<field name="s2a_client_weight_unit"/>
|
||||
<field name="s2a_brakes"/>
|
||||
<field name="s2a_brake_type"/>
|
||||
<field name="s2a_num_wheels"/>
|
||||
<field name="s2a_wheel_size"/>
|
||||
<field name="s2a_back_support"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Eligibility Confirmations">
|
||||
<group>
|
||||
<field name="s2a_confirm1"/>
|
||||
<field name="s2a_confirm2"/>
|
||||
<field name="s2a_confirm3"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2a_confirm4"/>
|
||||
<field name="s2a_confirm5"/>
|
||||
<field name="s2a_confirm6"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="s2a_custom"/>
|
||||
<field name="s2a_cost_labour"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<!-- ========== SECTION 2B - MANUAL WHEELCHAIRS ========== -->
|
||||
<page string="Section 2b - Manual Chairs" name="section2b">
|
||||
<group>
|
||||
<group string="Base Device">
|
||||
<field name="s2b_base_device"/>
|
||||
<field name="s2b_power_addon"/>
|
||||
</group>
|
||||
<group string="Reason for Application">
|
||||
<field name="s2b_reason"/>
|
||||
<field name="s2b_replacement_status"/>
|
||||
<field name="s2b_replacement_size"/>
|
||||
<field name="s2b_replacement_adp"/>
|
||||
<field name="s2b_replacement_special"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Prescription Details">
|
||||
<group>
|
||||
<field name="s2b_seat_width"/>
|
||||
<field name="s2b_seat_width_unit"/>
|
||||
<field name="s2b_seat_depth"/>
|
||||
<field name="s2b_seat_depth_unit"/>
|
||||
<field name="s2b_floor_height"/>
|
||||
<field name="s2b_floor_height_unit"/>
|
||||
<field name="s2b_cane_height"/>
|
||||
<field name="s2b_cane_height_unit"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2b_back_height"/>
|
||||
<field name="s2b_back_height_unit"/>
|
||||
<field name="s2b_rest_length"/>
|
||||
<field name="s2b_rest_length_unit"/>
|
||||
<field name="s2b_client_weight"/>
|
||||
<field name="s2b_client_weight_unit"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Additional ADP Funded Options"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="s2b_adjustable_tension"/>
|
||||
<field name="s2b_heavy_duty"/>
|
||||
<field name="s2b_recliner"/>
|
||||
<field name="s2b_footplates"/>
|
||||
<field name="s2b_legrests"/>
|
||||
<field name="s2b_spoke"/>
|
||||
<field name="s2b_projected"/>
|
||||
<field name="s2b_standard_manual"/>
|
||||
<field name="s2b_grade_aids"/>
|
||||
<field name="s2b_caster_pin"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2b_amputee_axle"/>
|
||||
<field name="s2b_quick_release"/>
|
||||
<field name="s2b_stroller"/>
|
||||
<field name="s2b_oxygen"/>
|
||||
<field name="s2b_ventilator"/>
|
||||
<field name="s2b_titanium"/>
|
||||
<field name="s2b_clothing_guards"/>
|
||||
<field name="s2b_one_arm"/>
|
||||
<field name="s2b_uni_lateral"/>
|
||||
<field name="s2b_plastic"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2b_rationale" nolabel="1" placeholder="Clinical rationale..."/>
|
||||
<field name="s2b_custom"/>
|
||||
<field name="s2b_cost_labour"/>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<!-- ========== SECTION 2C - POWER BASES / SCOOTERS ========== -->
|
||||
<page string="Section 2c - Power/Scooter" name="section2c">
|
||||
<group>
|
||||
<group string="Base Device">
|
||||
<field name="s2c_base_device"/>
|
||||
</group>
|
||||
<group string="Reason for Application">
|
||||
<field name="s2c_reason"/>
|
||||
<field name="s2c_replacement_status"/>
|
||||
<field name="s2c_replacement_size"/>
|
||||
<field name="s2c_replacement_adp"/>
|
||||
<field name="s2c_replacement_special"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Prescription Details">
|
||||
<group>
|
||||
<field name="s2c_seat_width"/>
|
||||
<field name="s2c_seat_width_unit"/>
|
||||
<field name="s2c_back_height"/>
|
||||
<field name="s2c_back_height_unit"/>
|
||||
<field name="s2c_floor_height"/>
|
||||
<field name="s2c_floor_height_unit"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2c_rest_length"/>
|
||||
<field name="s2c_rest_length_unit"/>
|
||||
<field name="s2c_seat_depth"/>
|
||||
<field name="s2c_seat_depth_unit"/>
|
||||
<field name="s2c_client_weight"/>
|
||||
<field name="s2c_client_weight_unit"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Additional ADP Funded Options"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="s2c_adjustable_tension"/>
|
||||
<field name="s2c_midline"/>
|
||||
<field name="s2c_manual_recline"/>
|
||||
<field name="s2c_footplates"/>
|
||||
<field name="s2c_legrests"/>
|
||||
<field name="s2c_swingaway"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2c_one_piece"/>
|
||||
<field name="s2c_seat_package_1"/>
|
||||
<field name="s2c_seat_package_2"/>
|
||||
<field name="s2c_oxygen"/>
|
||||
<field name="s2c_ventilator"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Specialty Controls"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="s2c_sp_controls_1"/>
|
||||
<field name="s2c_sp_controls_2"/>
|
||||
<field name="s2c_sp_controls_3"/>
|
||||
<field name="s2c_sp_controls_4"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2c_sp_controls_5"/>
|
||||
<field name="s2c_sp_controls_6"/>
|
||||
<field name="s2c_auto_correction"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Power Positioning Devices"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="s2c_power_tilt"/>
|
||||
<field name="s2c_power_recline"/>
|
||||
<field name="s2c_tilt_and_recline"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2c_power_elevating"/>
|
||||
<field name="s2c_control_box"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2c_rationale" nolabel="1" placeholder="Clinical rationale..."/>
|
||||
<field name="s2c_custom"/>
|
||||
<field name="s2c_cost_labour"/>
|
||||
</group>
|
||||
<group string="Eligibility Confirmations">
|
||||
<group>
|
||||
<field name="s2c_confirm1"/>
|
||||
<field name="s2c_confirm2"/>
|
||||
<field name="s2c_confirm3"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2c_confirm4"/>
|
||||
<field name="s2c_confirm5"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<!-- ========== SECTION 2D - POSITIONING/SEATING ========== -->
|
||||
<page string="Section 2d - Seating" name="section2d">
|
||||
<group string="Seat Cushion">
|
||||
<group>
|
||||
<field name="s2d_seat_modular"/>
|
||||
<field name="s2d_seat_custom"/>
|
||||
<field name="s2d_seat_cover_modular"/>
|
||||
<field name="s2d_seat_cover_custom"/>
|
||||
<field name="s2d_seat_option_modular"/>
|
||||
<field name="s2d_seat_option_custom"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2d_seat_hardware_modular"/>
|
||||
<field name="s2d_seat_hardware_custom"/>
|
||||
<field name="s2d_adductor_modular"/>
|
||||
<field name="s2d_adductor_custom"/>
|
||||
<field name="s2d_pommel_custom"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Back Support">
|
||||
<group>
|
||||
<field name="s2d_back_modular"/>
|
||||
<field name="s2d_back_custom"/>
|
||||
<field name="s2d_back_option_modular"/>
|
||||
<field name="s2d_back_option_custom"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2d_back_cover_custom"/>
|
||||
<field name="s2d_back_hardware_modular"/>
|
||||
<field name="s2d_back_hardware_custom"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Headrest, Belt, Arm, Tray, Lateral, Foot Supports">
|
||||
<group>
|
||||
<field name="s2d_complete_modular"/>
|
||||
<field name="s2d_complete_custom"/>
|
||||
<field name="s2d_headrest_modular"/>
|
||||
<field name="s2d_headrest_custom"/>
|
||||
<field name="s2d_head_option_custom"/>
|
||||
<field name="s2d_head_hardware_modular"/>
|
||||
<field name="s2d_head_hardware_custom"/>
|
||||
<field name="s2d_belt_modular"/>
|
||||
<field name="s2d_belt_custom"/>
|
||||
<field name="s2d_belt_option_custom"/>
|
||||
<field name="s2d_arm_modular"/>
|
||||
<field name="s2d_arm_custom"/>
|
||||
<field name="s2d_arm_option_modular"/>
|
||||
<field name="s2d_arm_option_custom"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2d_arm_hardware_modular"/>
|
||||
<field name="s2d_arm_hardware_custom"/>
|
||||
<field name="s2d_tray_modular"/>
|
||||
<field name="s2d_tray_custom"/>
|
||||
<field name="s2d_tray_option_modular"/>
|
||||
<field name="s2d_tray_option_custom"/>
|
||||
<field name="s2d_lateral_modular"/>
|
||||
<field name="s2d_lateral_custom"/>
|
||||
<field name="s2d_lateral_option_custom"/>
|
||||
<field name="s2d_lateral_hardware_custom"/>
|
||||
<field name="s2d_foot_modular"/>
|
||||
<field name="s2d_foot_custom"/>
|
||||
<field name="s2d_foot_option_modular"/>
|
||||
<field name="s2d_foot_option_custom"/>
|
||||
<field name="s2d_foot_hardware_modular"/>
|
||||
<field name="s2d_foot_hardware_custom"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Application Details">
|
||||
<group>
|
||||
<field name="s2d_reason"/>
|
||||
<field name="s2d_replacement_status"/>
|
||||
<field name="s2d_replacement_size"/>
|
||||
<field name="s2d_replacement_adp"/>
|
||||
<field name="s2d_replacement_special"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="s2d_confirm1"/>
|
||||
<field name="s2d_confirm2"/>
|
||||
<field name="s2d_custom"/>
|
||||
<field name="s2d_cost_labour"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<!-- ========== SECTION 3 - CONSENT ========== -->
|
||||
<page string="Section 3 - Consent" name="section3">
|
||||
<group>
|
||||
<group string="Applicant Signature">
|
||||
<field name="consent_date"/>
|
||||
<field name="consent_signed_by"/>
|
||||
</group>
|
||||
<group string="Agent/Contact (if applicable)">
|
||||
<field name="agent_relationship"/>
|
||||
<field name="agent_first_name"/>
|
||||
<field name="agent_last_name"/>
|
||||
<field name="agent_city"/>
|
||||
<field name="agent_province"/>
|
||||
<field name="agent_postal_code"/>
|
||||
<field name="agent_home_phone" widget="phone"/>
|
||||
<field name="agent_bus_phone" widget="phone"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<!-- ========== SECTION 4 - AUTHORIZER & VENDOR ========== -->
|
||||
<page string="Section 4 - Signatures" name="section4">
|
||||
<group>
|
||||
<group string="Authorizer">
|
||||
<field name="authorizer_first_name"/>
|
||||
<field name="authorizer_last_name"/>
|
||||
<field name="authorizer_phone" widget="phone"/>
|
||||
<field name="authorizer_phone_ext"/>
|
||||
<field name="authorizer_adp_number"/>
|
||||
<field name="assessment_date"/>
|
||||
</group>
|
||||
<group string="Vendor 1">
|
||||
<field name="vendor_business_name"/>
|
||||
<field name="vendor_adp_number"/>
|
||||
<field name="vendor_representative"/>
|
||||
<field name="vendor_position"/>
|
||||
<field name="vendor_location"/>
|
||||
<field name="vendor_phone" widget="phone"/>
|
||||
<field name="vendor_phone_ext"/>
|
||||
<field name="vendor_sign_date"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Vendor 2">
|
||||
<field name="vendor2_business_name"/>
|
||||
<field name="vendor2_adp_number"/>
|
||||
<field name="vendor2_representative"/>
|
||||
<field name="vendor2_position"/>
|
||||
<field name="vendor2_location"/>
|
||||
<field name="vendor2_phone" widget="phone"/>
|
||||
<field name="vendor2_sign_date"/>
|
||||
</group>
|
||||
<group string="Equipment Specification (Page 12)">
|
||||
<field name="equip_vendor_invoice_no"/>
|
||||
<field name="equip_vendor_adp_reg"/>
|
||||
<field name="equip_cell1"/>
|
||||
<field name="equip_cell2"/>
|
||||
<field name="equip_cell3"/>
|
||||
<field name="equip_cell4"/>
|
||||
<field name="equip_cell5"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Proof of Delivery">
|
||||
<field name="pod_received_by"/>
|
||||
<field name="pod_date"/>
|
||||
</group>
|
||||
<group string="Sections Submitted to ADP">
|
||||
<field name="note_section1"/>
|
||||
<field name="note_section2a"/>
|
||||
<field name="note_section2b"/>
|
||||
<field name="note_section2c"/>
|
||||
<field name="note_section2d"/>
|
||||
<field name="note_section3and4"/>
|
||||
<field name="note_vendor_replacement"/>
|
||||
<field name="note_vendor_custom"/>
|
||||
<field name="note_funding_chart"/>
|
||||
<field name="note_letter"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<!-- ========== RAW XML ========== -->
|
||||
<page string="Raw XML" name="raw">
|
||||
<field name="raw_xml" widget="text" readonly="1"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Application Data List View -->
|
||||
<record id="view_fusion_adp_application_data_list" model="ir.ui.view">
|
||||
<field name="name">fusion.adp.application.data.list</field>
|
||||
<field name="model">fusion.adp.application.data</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="ADP Application Data">
|
||||
<field name="applicant_last_name"/>
|
||||
<field name="applicant_first_name"/>
|
||||
<field name="health_card_number"/>
|
||||
<field name="application_date"/>
|
||||
<field name="device_category"/>
|
||||
<field name="base_device"/>
|
||||
<field name="reason_for_application"/>
|
||||
<field name="city"/>
|
||||
<field name="sections_submitted"/>
|
||||
<field name="sale_order_id"/>
|
||||
<field name="profile_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
165
fusion_claims/fusion_claims/views/dashboard_views.xml
Normal file
165
fusion_claims/fusion_claims/views/dashboard_views.xml
Normal file
@@ -0,0 +1,165 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_fusion_claims_dashboard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.claims.dashboard.form</field>
|
||||
<field name="model">fusion.claims.dashboard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Dashboard" create="0" delete="0">
|
||||
<sheet>
|
||||
<!-- ===== FUNDING CARDS (one line, bigger) ===== -->
|
||||
<div class="d-flex flex-nowrap gap-2 mb-4 overflow-auto">
|
||||
<div invisible="adp_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_adp" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="adp_count"/></div>
|
||||
<div style="font-size: 0.85rem;">ADP</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="odsp_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_odsp" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="odsp_count"/></div>
|
||||
<div style="font-size: 0.85rem;">ODSP</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="march_of_dimes_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_march" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="march_of_dimes_count"/></div>
|
||||
<div style="font-size: 0.85rem;">March of Dimes</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="hardship_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_hardship" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="hardship_count"/></div>
|
||||
<div style="font-size: 0.85rem;">Hardship</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="acsd_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_acsd" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="acsd_count"/></div>
|
||||
<div style="font-size: 0.85rem;">ACSD</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="muscular_dystrophy_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_muscular" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="muscular_dystrophy_count"/></div>
|
||||
<div style="font-size: 0.85rem;">Muscular Dystrophy</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="insurance_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_insurance" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-dark text-center py-3 px-2" style="background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="insurance_count"/></div>
|
||||
<div style="font-size: 0.85rem;">Insurance</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="wsib_count == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_wsib" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-dark text-center py-3 px-2" style="background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="wsib_count"/></div>
|
||||
<div style="font-size: 0.85rem;">WSIB</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div invisible="total_profiles == 0" style="flex: 1 1 0; min-width: 120px;">
|
||||
<button name="action_open_profiles" type="object" class="btn p-0 w-100 border-0">
|
||||
<div class="text-white text-center py-3 px-2" style="background: linear-gradient(135deg, #30cfd0 0%, #330867 100%); border-radius: 14px;">
|
||||
<div class="fw-bold" style="font-size: 1.8rem;"><field name="total_profiles"/></div>
|
||||
<div style="font-size: 0.85rem;">Profiles</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== PANEL SELECTORS (4 dropdowns) ===== -->
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-3">
|
||||
<div class="fw-bold mb-1" style="font-size: 0.8rem;">Window 1</div>
|
||||
<field name="panel1_type" nolabel="1"/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="fw-bold mb-1" style="font-size: 0.8rem;">Window 2</div>
|
||||
<field name="panel2_type" nolabel="1"/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="fw-bold mb-1" style="font-size: 0.8rem;">Window 3</div>
|
||||
<field name="panel3_type" nolabel="1"/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="fw-bold mb-1" style="font-size: 0.8rem;">Window 4</div>
|
||||
<field name="panel4_type" nolabel="1"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== TOP PANELS ROW 1 ===== -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card" style="border-radius: 14px; overflow: hidden;">
|
||||
<div class="card-header fw-bold text-white py-2" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||
<field name="panel1_title" nolabel="1"/>
|
||||
</div>
|
||||
<div class="card-body p-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<field name="panel1_html" class="w-100" nolabel="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card" style="border-radius: 14px; overflow: hidden;">
|
||||
<div class="card-header fw-bold text-white py-2" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
||||
<field name="panel2_title" nolabel="1"/>
|
||||
</div>
|
||||
<div class="card-body p-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<field name="panel2_html" class="w-100" nolabel="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== TOP PANELS ROW 2 ===== -->
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card" style="border-radius: 14px; overflow: hidden;">
|
||||
<div class="card-header fw-bold text-white py-2" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
|
||||
<field name="panel3_title" nolabel="1"/>
|
||||
</div>
|
||||
<div class="card-body p-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<field name="panel3_html" class="w-100" nolabel="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card" style="border-radius: 14px; overflow: hidden;">
|
||||
<div class="card-header fw-bold text-white py-2" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
|
||||
<field name="panel4_title" nolabel="1"/>
|
||||
</div>
|
||||
<div class="card-body p-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<field name="panel4_html" class="w-100" nolabel="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Dashboard Action -->
|
||||
<record id="action_fusion_claims_dashboard" model="ir.actions.act_window">
|
||||
<field name="name">Dashboard</field>
|
||||
<field name="res_model">fusion.claims.dashboard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_fusion_claims_dashboard_form"/>
|
||||
<field name="target">current</field>
|
||||
</record>
|
||||
</odoo>
|
||||
261
fusion_claims/fusion_claims/views/fusion_loaner_views.xml
Normal file
261
fusion_claims/fusion_claims/views/fusion_loaner_views.xml
Normal file
@@ -0,0 +1,261 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
-->
|
||||
<odoo>
|
||||
<!-- ===================================================================== -->
|
||||
<!-- LOANER CHECKOUT VIEWS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<!-- List View -->
|
||||
<record id="view_fusion_loaner_checkout_list" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.checkout.list</field>
|
||||
<field name="model">fusion.loaner.checkout</field>
|
||||
<field name="arch" type="xml">
|
||||
<list decoration-danger="state == 'overdue'"
|
||||
decoration-warning="state == 'rental_pending'"
|
||||
decoration-muted="state in ('returned', 'lost')">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="lot_id" optional="show"/>
|
||||
<field name="checkout_date"/>
|
||||
<field name="expected_return_date"/>
|
||||
<field name="days_out"/>
|
||||
<field name="state" widget="badge"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="view_fusion_loaner_checkout_form" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.checkout.form</field>
|
||||
<field name="model">fusion.loaner.checkout</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_checkout" type="object" string="Confirm Checkout"
|
||||
class="btn-primary" invisible="state != 'draft'"/>
|
||||
<button name="action_return" type="object" string="Return Loaner"
|
||||
class="btn-success" invisible="state not in ('checked_out', 'overdue', 'rental_pending')"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,checked_out,returned"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Client Information">
|
||||
<field name="partner_id"/>
|
||||
<field name="sales_rep_id"/>
|
||||
<field name="sale_order_id"/>
|
||||
</group>
|
||||
<group string="Product">
|
||||
<field name="product_id"/>
|
||||
<field name="lot_id"/>
|
||||
<field name="loaner_period_days"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Dates">
|
||||
<field name="checkout_date"/>
|
||||
<field name="expected_return_date"/>
|
||||
<field name="actual_return_date"/>
|
||||
<field name="days_out"/>
|
||||
</group>
|
||||
<group string="Condition">
|
||||
<field name="checkout_condition"/>
|
||||
<field name="checkout_notes"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View -->
|
||||
<record id="view_fusion_loaner_checkout_search" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.checkout.search</field>
|
||||
<field name="model">fusion.loaner.checkout</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- LOANER HISTORY VIEWS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<record id="view_fusion_loaner_history_list" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.history.list</field>
|
||||
<field name="model">fusion.loaner.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="action_date"/>
|
||||
<field name="checkout_id"/>
|
||||
<field name="action" widget="badge"/>
|
||||
<field name="user_id"/>
|
||||
<field name="notes"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- WIZARD VIEWS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<!-- Checkout Wizard -->
|
||||
<record id="view_loaner_checkout_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.checkout.wizard.form</field>
|
||||
<field name="model">fusion.loaner.checkout.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="lot_id"/>
|
||||
<field name="checkout_date"/>
|
||||
<field name="loaner_period_days"/>
|
||||
<field name="checkout_condition" widget="radio"/>
|
||||
<field name="checkout_notes"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_checkout" type="object" string="Checkout Loaner" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Return Wizard -->
|
||||
<record id="view_loaner_return_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.loaner.return.wizard.form</field>
|
||||
<field name="model">fusion.loaner.return.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="checkout_id" readonly="1"/>
|
||||
<field name="return_date"/>
|
||||
<field name="return_condition" widget="radio"/>
|
||||
<field name="return_notes"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_return" type="object" string="Confirm Return" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- ACTIONS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<record id="action_fusion_loaner_checkout" model="ir.actions.act_window">
|
||||
<field name="name">Loaner Equipment</field>
|
||||
<field name="res_model">fusion.loaner.checkout</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_loaner_history" model="ir.actions.act_window">
|
||||
<field name="name">Loaner History</field>
|
||||
<field name="res_model">fusion.loaner.history</field>
|
||||
<field name="view_mode">list</field>
|
||||
</record>
|
||||
|
||||
<!-- Action: Loaner Products (products that can be loaned) -->
|
||||
<record id="action_fusion_loaner_products" model="ir.actions.act_window">
|
||||
<field name="name">Loaner Products</field>
|
||||
<field name="res_model">product.template</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('x_fc_can_be_loaned', '=', True)]</field>
|
||||
<field name="context">{'default_x_fc_can_be_loaned': True, 'default_sale_ok': False, 'default_purchase_ok': False, 'default_rent_ok': True}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No loaner products configured yet
|
||||
</p>
|
||||
<p>
|
||||
Mark products as "Loaner" in the product form to add them here.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- MENUS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<menuitem id="menu_loaner_root"
|
||||
name="Loaner Management"
|
||||
parent="menu_adp_claims_root"
|
||||
sequence="58"/>
|
||||
|
||||
<menuitem id="menu_loaner_active"
|
||||
name="Active Loaners"
|
||||
parent="menu_loaner_root"
|
||||
action="action_fusion_loaner_checkout"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_loaner_history"
|
||||
name="Loaner History"
|
||||
parent="menu_loaner_root"
|
||||
action="action_fusion_loaner_history"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_loaner_products"
|
||||
name="Loaner Products"
|
||||
parent="menu_loaner_root"
|
||||
action="action_fusion_loaner_products"
|
||||
sequence="30"/>
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- PRODUCT TEMPLATE LOANER FIELDS -->
|
||||
<!-- ===================================================================== -->
|
||||
|
||||
<!-- Add "Can be Loaned" checkbox next to other product type checkboxes -->
|
||||
<record id="view_product_template_loaner_checkbox" model="ir.ui.view">
|
||||
<field name="name">product.template.loaner.checkbox</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_form_view"/>
|
||||
<field name="priority">50</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='options']" position="inside">
|
||||
<span class="d-inline-flex">
|
||||
<field name="x_fc_can_be_loaned"/>
|
||||
<label for="x_fc_can_be_loaned" string="Loaner"/>
|
||||
</span>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Loaner Settings tab (only visible when Can be Loaned is checked) -->
|
||||
<record id="view_product_template_loaner_form" model="ir.ui.view">
|
||||
<field name="name">product.template.loaner.form</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='sales']" position="after">
|
||||
<page string="Loaner Settings" name="loaner_settings" invisible="not x_fc_can_be_loaned">
|
||||
<group>
|
||||
<group string="Loaner Period">
|
||||
<field name="x_fc_loaner_period_days"/>
|
||||
</group>
|
||||
<group string="Rental Pricing (if not returned)">
|
||||
<field name="x_fc_rental_price_weekly"/>
|
||||
<field name="x_fc_rental_price_monthly"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- SA Signature Templates have been retired.
|
||||
ODSP signing positions are now managed via Configuration > PDF Templates
|
||||
(category = ODSP) using the drag-and-drop visual editor. -->
|
||||
</odoo>
|
||||
39
fusion_claims/fusion_claims/views/res_company_views.xml
Normal file
39
fusion_claims/fusion_claims/views/res_company_views.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<odoo>
|
||||
<!-- Add Fusion Central fields to Company Form -->
|
||||
<record id="view_company_form_fusion_claims" model="ir.ui.view">
|
||||
<field name="name">res.company.form.fusion.central</field>
|
||||
<field name="model">res.company</field>
|
||||
<field name="inherit_id" ref="base.view_company_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Fusion Central" name="fusion_claims">
|
||||
<group string="Store/Location Information">
|
||||
<field name="x_fc_store_address_1" placeholder="e.g., Main Store - 123 Street, City, Province, Postal"/>
|
||||
<field name="x_fc_store_address_2" placeholder="e.g., Second Location - 456 Avenue, City, Province, Postal"/>
|
||||
<field name="x_fc_company_tagline" placeholder="e.g., Your company slogan or tagline"/>
|
||||
</group>
|
||||
<group string="Payment Information">
|
||||
<field name="x_fc_etransfer_email" placeholder="e.g., payments@yourcompany.com"/>
|
||||
<field name="x_fc_cheque_payable_to" placeholder="Defaults to company name if empty"/>
|
||||
</group>
|
||||
<group string="Payment Terms (HTML)">
|
||||
<field name="x_fc_payment_terms_html" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
<group string="Refund Policy">
|
||||
<field name="x_fc_include_refund_page"/>
|
||||
</group>
|
||||
<group string="Refund Policy Content (HTML)" invisible="not x_fc_include_refund_page">
|
||||
<field name="x_fc_refund_policy_html" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
537
fusion_claims/fusion_claims/views/res_config_settings_views.xml
Normal file
537
fusion_claims/fusion_claims/views/res_config_settings_views.xml
Normal file
@@ -0,0 +1,537 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2025 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<odoo>
|
||||
<!-- Add Fusion Central Settings as a new app block -->
|
||||
<record id="res_config_settings_view_form_fusion_claims" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.fusion.central</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="inside">
|
||||
<app data-string="Fusion Claims" string="Fusion Claims" name="fusion_claims">
|
||||
<h2>ADP Billing</h2>
|
||||
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<!-- ADP Vendor Code -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">ADP Vendor Code</span>
|
||||
<div class="text-muted">Your ADP vendor/location code for claim submissions</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_vendor_code" placeholder="e.g., 1234567"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>ADP Posting Schedule</h2>
|
||||
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<!-- Posting Schedule Configuration -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Posting Cycle</span>
|
||||
<div class="text-muted">Configure the bi-weekly ADP posting schedule</div>
|
||||
<div class="mt-2">
|
||||
<div class="row mb-2">
|
||||
<label for="fc_adp_posting_base_date" class="col-5 col-form-label">Base Posting Date</label>
|
||||
<div class="col-7">
|
||||
<field name="fc_adp_posting_base_date"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<label for="fc_adp_posting_frequency_days" class="col-5 col-form-label">Frequency (Days)</label>
|
||||
<div class="col-7">
|
||||
<field name="fc_adp_posting_frequency_days"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info mt-3" role="alert">
|
||||
<strong>Schedule Info:</strong>
|
||||
<ul class="mb-0 mt-1">
|
||||
<li>Submission Deadline: Wednesday 6 PM before posting day</li>
|
||||
<li>Payment Processed: Posting day + 7 days</li>
|
||||
<li>Payment Received: Posting day + ~10 days</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reminder Recipients -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Automatic Reminders</span>
|
||||
<div class="text-muted">Configure who receives ADP billing reminders</div>
|
||||
<div class="mt-2">
|
||||
<div class="row mb-2">
|
||||
<label for="fc_adp_billing_reminder_user_id" class="col-12 col-form-label">
|
||||
Billing Deadline Person
|
||||
</label>
|
||||
<div class="col-12">
|
||||
<field name="fc_adp_billing_reminder_user_id"
|
||||
domain="[('share', '=', False)]"
|
||||
placeholder="Select user for billing reminders..."/>
|
||||
<div class="text-muted small">Reminded on Monday to complete billing by Wednesday 6 PM</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<label for="fc_adp_correction_reminder_user_ids" class="col-12 col-form-label">
|
||||
Correction Alert Recipients
|
||||
</label>
|
||||
<div class="col-12">
|
||||
<field name="fc_adp_correction_reminder_user_ids"
|
||||
widget="many2many_tags"
|
||||
domain="[('share', '=', False)]"
|
||||
placeholder="Select users for correction alerts..."/>
|
||||
<div class="text-muted small">Notified when invoices need correction/resubmission</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Email Notifications</h2>
|
||||
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<!-- Enable/Disable Notifications -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="fc_enable_email_notifications"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="fc_enable_email_notifications"/>
|
||||
<div class="text-muted">
|
||||
Send automated email notifications when ADP workflow status changes
|
||||
(assessment completed, application submitted, approved, denied, case closed, etc.)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Office CC Emails -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Office Notification Recipients</span>
|
||||
<div class="text-muted">
|
||||
These contacts will receive a copy (CC) of all automated ADP notifications
|
||||
so the office staff can stay in the loop.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_office_notification_ids"
|
||||
widget="many2many_tags"
|
||||
domain="[('user_ids.share', '=', False)]"
|
||||
options="{'no_create': True}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Application Reminder Days -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Application Reminders</span>
|
||||
<div class="text-muted">
|
||||
Automated reminders to therapist to submit the ADP application.
|
||||
</div>
|
||||
<div class="mt-2 row">
|
||||
<div class="col-4">
|
||||
<field name="fc_application_reminder_days"/>
|
||||
</div>
|
||||
<div class="col-8 pt-2 text-muted">days after assessment (first reminder)</div>
|
||||
</div>
|
||||
<div class="mt-2 row">
|
||||
<div class="col-4">
|
||||
<field name="fc_application_reminder_2_days"/>
|
||||
</div>
|
||||
<div class="col-8 pt-2 text-muted">days after first reminder (second reminder)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workflow Lock Settings -->
|
||||
<h2 class="mt-4">Workflow Settings</h2>
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="fc_allow_sale_type_override"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="fc_allow_sale_type_override"/>
|
||||
<div class="text-muted">
|
||||
Allow changing Sale Type even after application is submitted.
|
||||
Use this for cases where additional benefits (ODSP, etc.) are discovered.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="fc_allow_document_lock_override"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="fc_allow_document_lock_override"/>
|
||||
<div class="text-muted">
|
||||
Allow users in the "Document Lock Override" group to edit locked documents
|
||||
on old cases. Turn this OFF once all legacy cases are processed.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Designated Vendor Signer</span>
|
||||
<div class="text-muted">
|
||||
The user who signs Page 12 on behalf of the company.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_designated_vendor_signer"
|
||||
domain="[('share', '=', False)]"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>External APIs</h2>
|
||||
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<!-- Google Maps API Key -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Google Maps API</span>
|
||||
<div class="text-muted">
|
||||
API key for Google Maps Places autocomplete in address fields (accessibility assessments, etc.)
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_google_maps_api_key" placeholder="Enter your Google Maps API Key" password="True"/>
|
||||
</div>
|
||||
<div class="alert alert-info mt-2" role="alert">
|
||||
<i class="fa fa-info-circle"/> Enable the "Places API" in your Google Cloud Console for address autocomplete.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>AI Client Intelligence</h2>
|
||||
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<!-- AI API Key -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">AI API Key</span>
|
||||
<div class="text-muted">
|
||||
OpenAI API key for Client Intelligence chat and AI analysis features.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_ai_api_key" placeholder="sk-..." password="True"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- AI Model -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">AI Model</span>
|
||||
<div class="text-muted">
|
||||
Which AI model to use for client intelligence queries.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_ai_model"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Auto XML Parse -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="fc_auto_parse_xml"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="fc_auto_parse_xml"/>
|
||||
<div class="text-muted">
|
||||
Automatically parse ADP XML files when uploaded to sale orders
|
||||
and create/update client profiles.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Technician Management</h2>
|
||||
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<!-- Store Hours -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Store / Scheduling Hours</span>
|
||||
<div class="text-muted">
|
||||
Operating hours for technician task scheduling. Tasks can only be booked
|
||||
within these hours. Calendar view is also restricted to this range.
|
||||
</div>
|
||||
<div class="mt-2 d-flex align-items-center gap-2">
|
||||
<field name="fc_store_open_hour" widget="float_time" style="max-width: 100px;"/>
|
||||
<span>to</span>
|
||||
<field name="fc_store_close_hour" widget="float_time" style="max-width: 100px;"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Distance Matrix Toggle -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="fc_google_distance_matrix_enabled"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="fc_google_distance_matrix_enabled"/>
|
||||
<div class="text-muted">
|
||||
Calculate travel time between technician tasks using Google Distance Matrix API.
|
||||
Requires Google Maps API key above with Distance Matrix API enabled.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Start Address (Company Default / Fallback) -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Default HQ / Fallback Address</span>
|
||||
<div class="text-muted">
|
||||
Company default start location used when a technician has no personal
|
||||
start address set. Each technician can set their own start location
|
||||
in their user profile or from the portal.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_technician_start_address" placeholder="e.g. 123 Main St, Brampton, ON"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Location History Retention -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Location History Retention</span>
|
||||
<div class="text-muted">
|
||||
How many days to keep technician GPS location history before automatic cleanup.
|
||||
</div>
|
||||
<div class="mt-2 d-flex align-items-center gap-2">
|
||||
<field name="fc_location_retention_days" placeholder="30" style="max-width: 80px;"/>
|
||||
<span class="text-muted">days</span>
|
||||
</div>
|
||||
<div class="text-muted small mt-1">
|
||||
Leave empty = 30 days. Enter 0 = delete at end of each day. 1+ = keep that many days.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Push Notifications</h2>
|
||||
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<!-- Push Enable -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="fc_push_enabled"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="fc_push_enabled"/>
|
||||
<div class="text-muted">
|
||||
Send web push notifications to technicians about upcoming tasks.
|
||||
Requires VAPID keys (auto-generated on first save if empty).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Advance Minutes -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Notification Advance Time</span>
|
||||
<div class="text-muted">
|
||||
Send push notification this many minutes before a scheduled task.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_push_advance_minutes"/> minutes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- VAPID Public Key -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">VAPID Public Key</span>
|
||||
<div class="mt-2">
|
||||
<field name="fc_vapid_public_key" placeholder="Auto-generated"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- VAPID Private Key -->
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">VAPID Private Key</span>
|
||||
<div class="mt-2">
|
||||
<field name="fc_vapid_private_key" password="True" placeholder="Auto-generated"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>March of Dimes</h2>
|
||||
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Default MOD Email</span>
|
||||
<div class="text-muted">
|
||||
Email used for sending initial quotations and documents to March of Dimes.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_mod_default_email" placeholder="hvmp@marchofdimes.ca"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">March of Dimes Vendor Code</span>
|
||||
<div class="text-muted">
|
||||
Your vendor code assigned by March of Dimes.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_mod_vendor_code" placeholder="e.g. TRD0001234"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Twilio SMS</h2>
|
||||
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="fc_twilio_enabled"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="fc_twilio_enabled"/>
|
||||
<div class="text-muted">
|
||||
Send SMS confirmations for assessment bookings and key status updates via Twilio.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Twilio Phone Number</span>
|
||||
<div class="mt-2">
|
||||
<field name="fc_twilio_phone_number" placeholder="+1234567890"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Account SID</span>
|
||||
<div class="mt-2">
|
||||
<field name="fc_twilio_account_sid" password="True"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Auth Token</span>
|
||||
<div class="mt-2">
|
||||
<field name="fc_twilio_auth_token" password="True"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>March of Dimes Follow-Up</h2>
|
||||
|
||||
<div class="row mt-4 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Follow-up Interval</span>
|
||||
<div class="text-muted">
|
||||
Days between follow-up reminders for MOD cases awaiting funding.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_mod_followup_interval_days"/> days
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Auto-Email Escalation Delay</span>
|
||||
<div class="text-muted">
|
||||
Days after a follow-up activity is overdue before sending an automatic email to the client.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_mod_followup_escalation_days"/> days
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- ODSP CONFIGURATION -->
|
||||
<!-- ============================================================= -->
|
||||
<h2>ODSP Configuration</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">SA Mobility Email</span>
|
||||
<div class="text-muted">
|
||||
Email address for SA Mobility submissions (can be changed if the government email changes).
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_sa_mobility_email" placeholder="samobility@ontario.ca"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">SA Mobility Phone</span>
|
||||
<div class="text-muted">
|
||||
SA Mobility phone number for reference.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_sa_mobility_phone" placeholder="1-888-222-5099"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Default ODSP Office</span>
|
||||
<div class="text-muted">
|
||||
Default ODSP office contact for new ODSP cases.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<field name="fc_odsp_default_office_id"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden fields for field mappings (still needed for ir.config_parameter storage) -->
|
||||
<div class="d-none">
|
||||
<field name="fc_field_sale_type"/>
|
||||
<field name="fc_field_so_client_type"/>
|
||||
<field name="fc_field_so_authorizer"/>
|
||||
<field name="fc_field_invoice_type"/>
|
||||
<field name="fc_field_inv_client_type"/>
|
||||
<field name="fc_field_inv_authorizer"/>
|
||||
<field name="fc_field_product_code"/>
|
||||
<field name="fc_field_sol_serial"/>
|
||||
<field name="fc_field_aml_serial"/>
|
||||
<field name="fc_field_so_claim_number"/>
|
||||
<field name="fc_field_so_client_ref_1"/>
|
||||
<field name="fc_field_so_client_ref_2"/>
|
||||
<field name="fc_field_so_delivery_date"/>
|
||||
<field name="fc_field_so_adp_status"/>
|
||||
<field name="fc_field_so_service_start"/>
|
||||
<field name="fc_field_so_service_end"/>
|
||||
<field name="fc_field_inv_claim_number"/>
|
||||
<field name="fc_field_inv_client_ref_1"/>
|
||||
<field name="fc_field_inv_client_ref_2"/>
|
||||
<field name="fc_field_inv_delivery_date"/>
|
||||
<field name="fc_field_inv_service_start"/>
|
||||
<field name="fc_field_inv_service_end"/>
|
||||
<field name="fc_field_sol_placement"/>
|
||||
<field name="fc_field_aml_placement"/>
|
||||
<field name="fc_field_product_adp_price"/>
|
||||
<field name="fc_field_so_primary_serial"/>
|
||||
<field name="fc_field_inv_primary_serial"/>
|
||||
</div>
|
||||
</app>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
77
fusion_claims/fusion_claims/views/res_partner_views.xml
Normal file
77
fusion_claims/fusion_claims/views/res_partner_views.xml
Normal file
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Claim Assistant product family.
|
||||
-->
|
||||
<odoo>
|
||||
<!-- Add Contact Type + ODSP fields to Partner Form -->
|
||||
<record id="view_partner_form_fusion_claims" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.fusion.claims</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Our Contact Type field above the address -->
|
||||
<xpath expr="//span[@name='address_name']" position="before">
|
||||
<field name="x_fc_contact_type" placeholder="Select contact type..."/>
|
||||
</xpath>
|
||||
|
||||
<!-- ODSP section in notebook -->
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="ODSP" name="odsp_info"
|
||||
invisible="x_fc_contact_type not in ('odsp_customer', 'adp_odsp_customer', 'odsp_agent', 'odsp_office')">
|
||||
<group string="ODSP Information">
|
||||
<group>
|
||||
<field name="x_fc_odsp_member_id"
|
||||
invisible="x_fc_contact_type == 'odsp_office'"/>
|
||||
<field name="x_fc_case_worker_id"
|
||||
invisible="x_fc_contact_type == 'odsp_office'"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="x_fc_date_of_birth"
|
||||
invisible="x_fc_contact_type == 'odsp_office'"/>
|
||||
<field name="x_fc_healthcard_number"
|
||||
invisible="x_fc_contact_type == 'odsp_office'"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="ODSP Office Details"
|
||||
invisible="x_fc_contact_type != 'odsp_office'">
|
||||
<group>
|
||||
<field name="x_fc_is_odsp_office" invisible="1"/>
|
||||
<field name="email" string="Office Email"/>
|
||||
<field name="phone" string="Office Phone"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Add Contact Type to list view -->
|
||||
<record id="view_partner_tree_fusion_claims" model="ir.ui.view">
|
||||
<field name="name">res.partner.tree.fusion.claims</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='phone']" position="after">
|
||||
<field name="x_fc_contact_type" optional="hide"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Add Contact Type to search view -->
|
||||
<record id="view_partner_search_fusion_claims" model="ir.ui.view">
|
||||
<field name="name">res.partner.search.fusion.claims</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_res_partner_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='type_company']" position="after">
|
||||
<separator/>
|
||||
<filter name="filter_odsp_customer" string="ODSP Customers"
|
||||
domain="[('x_fc_contact_type', 'in', ['odsp_customer', 'adp_odsp_customer'])]"/>
|
||||
<filter name="filter_odsp_office" string="ODSP Offices"
|
||||
domain="[('x_fc_contact_type', '=', 'odsp_office')]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
2502
fusion_claims/fusion_claims/views/sale_order_views.xml
Normal file
2502
fusion_claims/fusion_claims/views/sale_order_views.xml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Submission History Tree View -->
|
||||
<record id="view_fusion_submission_history_tree" model="ir.ui.view">
|
||||
<field name="name">fusion.submission.history.tree</field>
|
||||
<field name="model">fusion.submission.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Submission History" create="0">
|
||||
<field name="submission_number"/>
|
||||
<field name="submission_type"/>
|
||||
<field name="submission_date"/>
|
||||
<field name="submitted_by_id"/>
|
||||
<field name="result" widget="badge"
|
||||
decoration-success="result == 'accepted' or result == 'approved'"
|
||||
decoration-danger="result == 'rejected' or result == 'denied'"
|
||||
decoration-info="result == 'pending'"/>
|
||||
<field name="result_date"/>
|
||||
<field name="rejection_reason" optional="hide"/>
|
||||
<field name="correction_notes" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Submission History Form View -->
|
||||
<record id="view_fusion_submission_history_form" model="ir.ui.view">
|
||||
<field name="name">fusion.submission.history.form</field>
|
||||
<field name="model">fusion.submission.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Submission History">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="display_name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Submission Details">
|
||||
<field name="sale_order_id" readonly="1"/>
|
||||
<field name="submission_number" readonly="1"/>
|
||||
<field name="submission_type" readonly="1"/>
|
||||
<field name="submission_date" readonly="1"/>
|
||||
<field name="submitted_by_id" readonly="1"/>
|
||||
</group>
|
||||
<group string="Result">
|
||||
<field name="result"/>
|
||||
<field name="result_date"/>
|
||||
<field name="rejection_reason" invisible="result != 'rejected'"/>
|
||||
<field name="rejection_details" invisible="result != 'rejected'"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group string="Documents Submitted" invisible="not final_application and not xml_file">
|
||||
<group>
|
||||
<field name="final_application" filename="final_application_filename"/>
|
||||
<field name="final_application_filename" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="xml_file" filename="xml_filename"/>
|
||||
<field name="xml_filename" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group string="Correction Notes" invisible="not correction_notes">
|
||||
<field name="correction_notes" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action to view submission history -->
|
||||
<record id="action_fusion_submission_history" model="ir.actions.act_window">
|
||||
<field name="name">Submission History</field>
|
||||
<field name="res_model">fusion.submission.history</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{}</field>
|
||||
</record>
|
||||
</odoo>
|
||||
80
fusion_claims/fusion_claims/views/task_sync_views.xml
Normal file
80
fusion_claims/fusion_claims/views/task_sync_views.xml
Normal file
@@ -0,0 +1,80 @@
|
||||
<?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>
|
||||
128
fusion_claims/fusion_claims/views/technician_location_views.xml
Normal file
128
fusion_claims/fusion_claims/views/technician_location_views.xml
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- MAP VIEW (QWeb HTML with Google Maps) -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="action_technician_location_map" model="ir.actions.act_url">
|
||||
<field name="name">Technician Map</field>
|
||||
<field name="url">/my/technician/admin/map</field>
|
||||
<field name="target">self</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- MENU ITEMS (under Technician Management) -->
|
||||
<!-- ================================================================== -->
|
||||
<menuitem id="menu_technician_locations"
|
||||
name="Location History"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_locations"
|
||||
sequence="40"/>
|
||||
|
||||
<menuitem id="menu_technician_map"
|
||||
name="Live Map"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_location_map"
|
||||
sequence="45"/>
|
||||
|
||||
<!-- 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>
|
||||
480
fusion_claims/fusion_claims/views/technician_task_views.xml
Normal file
480
fusion_claims/fusion_claims/views/technician_task_views.xml
Normal file
@@ -0,0 +1,480 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- SEQUENCE -->
|
||||
<!-- ================================================================== -->
|
||||
<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"/>
|
||||
<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>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- SEARCH VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_technician_task_search" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.task.search</field>
|
||||
<field name="model">fusion.technician.task</field>
|
||||
<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="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)]"/>
|
||||
<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)]"/>
|
||||
<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>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- FORM VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_technician_task_form" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.task.form</field>
|
||||
<field name="model">fusion.technician.task</field>
|
||||
<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="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>
|
||||
</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="task_type"/>
|
||||
<field name="priority" widget="priority"/>
|
||||
<field name="sale_order_id"/>
|
||||
</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="address_partner_id"/>
|
||||
<field name="address_street"/>
|
||||
<field name="address_street2" string="Unit/Suite #"/>
|
||||
<field name="address_buzz_code"/>
|
||||
<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>
|
||||
</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="task_type" decoration-bf="1"/>
|
||||
<field name="scheduled_date"/>
|
||||
<field name="time_start_display" string="Start"/>
|
||||
<field name="time_end_display" string="End"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="address_city"/>
|
||||
<field name="travel_time_minutes" string="Travel (min)" optional="show"/>
|
||||
<field name="status" widget="badge"
|
||||
decoration-success="status == 'completed'"
|
||||
decoration-warning="status == 'in_progress'"
|
||||
decoration-info="status in ('scheduled', 'en_route')"
|
||||
decoration-danger="status == 'cancelled'"/>
|
||||
<field name="priority" widget="priority" optional="hide"/>
|
||||
<field name="pod_required" optional="hide"/>
|
||||
<field name="sale_order_id" optional="hide"/>
|
||||
<field name="x_fc_source_label" string="Source" optional="show"
|
||||
widget="badge" decoration-info="x_fc_is_shadow"
|
||||
decoration-success="not x_fc_is_shadow"/>
|
||||
</list>
|
||||
</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="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 class="o_kanban_record_bottom mt-2">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<field name="activity_ids" widget="kanban_activity"/>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<field name="technician_id" widget="many2one_avatar_user"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- CALENDAR VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_technician_task_calendar" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.task.calendar</field>
|
||||
<field name="model">fusion.technician.task</field>
|
||||
<field name="arch" type="xml">
|
||||
<calendar string="Technician Schedule"
|
||||
date_start="datetime_start" date_stop="datetime_end"
|
||||
color="technician_id" mode="week" event_open_popup="1"
|
||||
quick_create="0">
|
||||
<!-- Displayed on the calendar card -->
|
||||
<field name="partner_id"/>
|
||||
<field name="x_fc_sync_client_name"/>
|
||||
<field name="task_type"/>
|
||||
<field name="time_start_display" string="Start"/>
|
||||
<field name="time_end_display" string="End"/>
|
||||
<!-- Popover (hover/click) details -->
|
||||
<field name="name"/>
|
||||
<field name="technician_id" avatar_field="image_128"/>
|
||||
<field name="address_display" string="Address"/>
|
||||
<field name="travel_time_minutes" string="Travel (min)"/>
|
||||
<field name="status"/>
|
||||
<field name="duration_hours" widget="float_time" string="Duration"/>
|
||||
</calendar>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- MAP VIEW (Enterprise web_map) -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_technician_task_map" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.task.map</field>
|
||||
<field name="model">fusion.technician.task</field>
|
||||
<field name="arch" type="xml">
|
||||
<map res_partner="address_partner_id" default_order="time_start"
|
||||
routing="1" js_class="fusion_task_map">
|
||||
<field name="partner_id" string="Client"/>
|
||||
<field name="task_type" string="Type"/>
|
||||
<field name="technician_id" string="Technician"/>
|
||||
<field name="time_start_display" string="Start"/>
|
||||
<field name="time_end_display" string="End"/>
|
||||
<field name="status" string="Status"/>
|
||||
<field name="travel_time_minutes" string="Travel (min)"/>
|
||||
</map>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- ACTIONS -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
<!-- Main Tasks Action (List/Kanban) -->
|
||||
<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>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- MENU ITEMS -->
|
||||
<!-- ================================================================== -->
|
||||
|
||||
<!-- Field Service - top-level menu (sequence 3 = first child = app default) -->
|
||||
<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"/>
|
||||
|
||||
<!-- Delivery Map - first item under Field Service = default landing view -->
|
||||
<menuitem id="menu_fc_delivery_map"
|
||||
name="Delivery Map"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_map_view"
|
||||
sequence="5"
|
||||
groups="fusion_claims.group_fusion_claims_user,fusion_claims.group_field_technician"/>
|
||||
|
||||
<menuitem id="menu_technician_schedule"
|
||||
name="Schedule"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_schedule"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_technician_tasks"
|
||||
name="Tasks"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_tasks"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_technician_tasks_today"
|
||||
name="Today's Tasks"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_tasks_today"
|
||||
sequence="15"/>
|
||||
|
||||
<menuitem id="menu_technician_my_tasks"
|
||||
name="My Tasks"
|
||||
parent="menu_technician_management"
|
||||
action="action_technician_my_tasks"
|
||||
sequence="25"
|
||||
groups="fusion_claims.group_field_technician"/>
|
||||
|
||||
|
||||
</odoo>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user