Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,397 @@
# -*- 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
import json
import logging
_logger = logging.getLogger(__name__)
class SubmissionVerificationWizard(models.TransientModel):
"""Wizard to verify which device types are being submitted in the ADP application.
This is Stage 1 of the two-stage verification system:
- Stage 1 (Submission): Verify what device types are being applied for
- Stage 2 (Approval): Verify what device types were approved by ADP
"""
_name = 'fusion_claims.submission.verification.wizard'
_description = 'ADP Submission Verification Wizard'
# ==========================================================================
# MAIN FIELDS
# ==========================================================================
sale_order_id = fields.Many2one(
'sale.order',
string='Sale Order',
required=True,
readonly=True,
)
line_ids = fields.One2many(
'fusion_claims.submission.verification.wizard.line',
'wizard_id',
string='Device Type Lines',
)
# Store device type mapping as JSON - needed because readonly fields aren't sent back
# Format: {"line_index": "device_type_name", ...}
device_type_mapping = fields.Text(
string='Device Type Mapping (JSON)',
help='Internal field to store device type names by line index',
)
# Summary fields
total_device_types = fields.Integer(
string='Total Device Types',
compute='_compute_summary',
)
selected_device_types = fields.Integer(
string='Selected Device Types',
compute='_compute_summary',
)
# ==========================================================================
# DOCUMENT UPLOAD FIELDS (for Submit Application mode)
# ==========================================================================
is_submit_mode = fields.Boolean(
string='Submit Mode',
default=False,
help='True if this wizard is being used to submit the application (not just verify)',
)
final_application = fields.Binary(
string='Final Submitted Application',
help='Upload the final application PDF being submitted to ADP',
)
final_application_filename = fields.Char(
string='Final Application Filename',
)
xml_file = fields.Binary(
string='XML Data File',
help='Upload the XML data file for ADP submission',
)
xml_filename = fields.Char(
string='XML Filename',
)
# ==========================================================================
# COMPUTED METHODS
# ==========================================================================
@api.depends('line_ids', 'line_ids.selected')
def _compute_summary(self):
for wizard in self:
wizard.total_device_types = len(wizard.line_ids)
wizard.selected_device_types = len(wizard.line_ids.filtered(lambda l: l.selected))
# ==========================================================================
# DEFAULT GET - Populate with device types from order
# ==========================================================================
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
active_id = self._context.get('active_id')
if not active_id:
return res
order = self.env['sale.order'].browse(active_id)
res['sale_order_id'] = order.id
# Set submit mode based on context
res['is_submit_mode'] = self._context.get('submit_application', False)
# Group order lines by device type
ADPDevice = self.env['fusion.adp.device.code'].sudo()
device_type_data = {} # {device_type: {'products': [], 'total_amount': 0}}
for so_line in order.order_line:
# Skip non-product lines
if so_line.display_type in ('line_section', 'line_note'):
continue
if not so_line.product_id or so_line.product_uom_qty <= 0:
continue
# Get device code and look up device type
device_code = so_line._get_adp_device_code()
if not device_code:
continue
adp_device = ADPDevice.search([
('device_code', '=', device_code),
('active', '=', True)
], limit=1)
if not adp_device:
continue
device_type = adp_device.device_type or 'Unknown'
if device_type not in device_type_data:
device_type_data[device_type] = {
'products': [],
'total_amount': 0,
'adp_portion': 0,
}
device_type_data[device_type]['products'].append({
'name': so_line.product_id.display_name,
'code': device_code,
'qty': so_line.product_uom_qty,
'price': so_line.price_subtotal,
})
device_type_data[device_type]['total_amount'] += so_line.price_subtotal
device_type_data[device_type]['adp_portion'] += so_line.x_fc_adp_portion
# Check if there are previously submitted device types
previous_selection = {}
if order.x_fc_submitted_device_types:
try:
previous_selection = json.loads(order.x_fc_submitted_device_types)
_logger.info(f"Loaded previous selection from order {order.id}: {previous_selection}")
except (json.JSONDecodeError, TypeError) as e:
_logger.warning(f"Failed to parse submitted_device_types for order {order.id}: {e}")
else:
_logger.info(f"No previous selection found for order {order.id}")
# Build wizard lines and device type mapping
lines_data = []
device_type_mapping = {} # {line_index: device_type_name}
for idx, (device_type, data) in enumerate(sorted(device_type_data.items())):
product_list = ', '.join([
f"{p['name']} ({p['code']}) x{p['qty']}"
for p in data['products']
])
# Check if previously selected
was_selected = previous_selection.get(device_type, False)
_logger.info(f"Device type '{device_type}' - was_selected: {was_selected} (previous_selection keys: {list(previous_selection.keys())})")
# Store mapping by index
device_type_mapping[str(idx)] = device_type
lines_data.append((0, 0, {
'device_type': device_type,
'product_details': product_list,
'product_count': len(data['products']),
'total_amount': data['total_amount'],
'adp_portion': data['adp_portion'],
'selected': was_selected, # Default to previously selected state
}))
res['line_ids'] = lines_data
res['device_type_mapping'] = json.dumps(device_type_mapping)
_logger.info(f"Created device_type_mapping: {device_type_mapping}")
return res
# ==========================================================================
# ACTION METHODS
# ==========================================================================
def action_confirm_submission(self):
"""Confirm the selected device types and store them for Stage 2 comparison."""
self.ensure_one()
# Load the device type mapping (stored during default_get)
# This is needed because readonly fields aren't sent back from the form
device_type_mapping = {}
if self.device_type_mapping:
try:
device_type_mapping = json.loads(self.device_type_mapping)
except (json.JSONDecodeError, TypeError):
pass
_logger.info(f"Loaded device_type_mapping: {device_type_mapping}")
# Get selected lines and map them to device types using the stored mapping
selected_device_types = []
for idx, line in enumerate(self.line_ids):
if line.selected:
# Get device type from mapping (use index as key)
device_type = device_type_mapping.get(str(idx))
if device_type:
selected_device_types.append(device_type)
_logger.info(f"Line {idx} selected - device_type from mapping: {device_type}")
else:
# Fallback to line.device_type (might be False due to readonly issue)
_logger.warning(f"Line {idx} selected but no mapping found, line.device_type={line.device_type}")
if line.device_type:
selected_device_types.append(line.device_type)
if not selected_device_types:
raise UserError(
"Please select at least one device type that is being submitted.\n\n"
"If none of these device types are being applied for, "
"please verify the order lines have the correct ADP device codes."
)
# Store the selection as JSON
selection_data = {
device_type: True for device_type in selected_device_types
}
_logger.info(f"Saving selection for order {self.sale_order_id.id}: {selection_data}")
# Update the sale order
self.sale_order_id.write({
'x_fc_submission_verified': True,
'x_fc_submitted_device_types': json.dumps(selection_data),
})
_logger.info(f"Saved x_fc_submitted_device_types: {json.dumps(selection_data)}")
# Post to chatter with nice card style
from markupsafe import Markup
from datetime import date
# Build HTML list from selected device types
device_list_html = ''.join([
f'<li>{device_type}</li>'
for device_type in selected_device_types
])
self.sale_order_id.message_post(
body=Markup(
'<div style="background: #e8f4fd; border-left: 4px solid #17a2b8; padding: 12px; margin: 8px 0; border-radius: 4px;">'
'<h4 style="color: #17a2b8; margin: 0 0 8px 0;"><i class="fa fa-check-square"/> Submission Verification Complete</h4>'
f'<p style="margin: 0;"><strong>Date:</strong> {date.today().strftime("%B %d, %Y")}</p>'
'<p style="margin: 8px 0 4px 0;"><strong>Device Types:</strong></p>'
f'<ul style="margin: 0; padding-left: 20px;">{device_list_html}</ul>'
f'<p style="margin: 8px 0 0 0; color: #666; font-size: 0.9em;">Verified by {self.env.user.name}</p>'
'</div>'
),
message_type='notification',
subtype_xmlid='mail.mt_note',
)
# If we're in submit mode (changing status to submitted), update status and save documents
if self.is_submit_mode:
# Validate documents are uploaded
if not self.final_application:
raise UserError("Please upload the Final Submitted Application PDF.")
if not self.xml_file:
raise UserError("Please upload the XML Data File.")
# Validate file types
if self.final_application_filename and not self.final_application_filename.lower().endswith('.pdf'):
raise UserError(f"Final Application must be a PDF file. Uploaded: '{self.final_application_filename}'")
if self.xml_filename and not self.xml_filename.lower().endswith('.xml'):
raise UserError(f"XML file must have .xml extension. Uploaded: '{self.xml_filename}'")
# Determine target status (submitted or resubmitted)
current_status = self.sale_order_id.x_fc_adp_application_status
new_status = 'resubmitted' if current_status == 'needs_correction' else 'submitted'
# Update status and save documents with skip flag
self.sale_order_id.with_context(skip_status_validation=True).write({
'x_fc_adp_application_status': new_status,
'x_fc_final_submitted_application': self.final_application,
'x_fc_final_application_filename': self.final_application_filename,
'x_fc_xml_file': self.xml_file,
'x_fc_xml_filename': self.xml_filename,
})
# Post status change to chatter with nice card style
status_label = 'Resubmitted' if new_status == 'resubmitted' else 'Submitted'
self.sale_order_id.message_post(
body=Markup(
'<div style="background: #d4edda; border-left: 4px solid #28a745; padding: 12px; margin: 8px 0; border-radius: 4px;">'
f'<h4 style="color: #28a745; margin: 0 0 8px 0;"><i class="fa fa-paper-plane"/> Application {status_label}</h4>'
f'<p style="margin: 0;"><strong>Date:</strong> {date.today().strftime("%B %d, %Y")}</p>'
'<p style="margin: 8px 0 4px 0;"><strong>Device Types Submitted:</strong></p>'
f'<ul style="margin: 0; padding-left: 20px;">{device_list_html}</ul>'
f'<p style="margin: 8px 0 0 0; color: #666; font-size: 0.9em;">Submitted by {self.env.user.name}</p>'
'</div>'
),
message_type='notification',
subtype_xmlid='mail.mt_note',
)
return {
'type': 'ir.actions.act_window_close',
'infos': {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Application Submitted'),
'message': _('Application submitted with %d device type(s).') % len(selected_device_types),
'type': 'success',
'sticky': False,
}
}
}
return {
'type': 'ir.actions.act_window_close',
'infos': {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Submission Verified'),
'message': _('%d device type(s) confirmed for submission.') % len(selected_device_types),
'type': 'success',
'sticky': False,
}
}
}
def action_select_all(self):
"""Select all device types."""
self.ensure_one()
for line in self.line_ids:
line.selected = True
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}
class SubmissionVerificationWizardLine(models.TransientModel):
"""Lines for the submission verification wizard - grouped by device type."""
_name = 'fusion_claims.submission.verification.wizard.line'
_description = 'Submission Verification Wizard Line'
wizard_id = fields.Many2one(
'fusion_claims.submission.verification.wizard',
string='Wizard',
required=True,
ondelete='cascade',
)
device_type = fields.Char(
string='Device Type',
readonly=True,
)
product_details = fields.Text(
string='Products',
readonly=True,
help='List of products in this device type',
)
product_count = fields.Integer(
string='# Products',
readonly=True,
)
total_amount = fields.Monetary(
string='Total Amount',
readonly=True,
currency_field='currency_id',
)
adp_portion = fields.Monetary(
string='ADP Portion',
readonly=True,
currency_field='currency_id',
)
currency_id = fields.Many2one(
'res.currency',
related='wizard_id.sale_order_id.currency_id',
readonly=True,
)
selected = fields.Boolean(
string='Submitting',
default=False,
help='Check if this device type is being submitted in the ADP application',
)