Initial commit
This commit is contained in:
397
fusion_claims/wizard/submission_verification_wizard.py
Normal file
397
fusion_claims/wizard/submission_verification_wizard.py
Normal 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',
|
||||
)
|
||||
Reference in New Issue
Block a user