398 lines
16 KiB
Python
398 lines
16 KiB
Python
# -*- 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',
|
|
)
|