Initial commit
This commit is contained in:
410
fusion_claims/wizard/status_change_reason_wizard.py
Normal file
410
fusion_claims/wizard/status_change_reason_wizard.py
Normal file
@@ -0,0 +1,410 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from markupsafe import Markup
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class StatusChangeReasonWizard(models.TransientModel):
|
||||
"""Wizard to capture reason when changing to specific statuses."""
|
||||
_name = 'fusion.status.change.reason.wizard'
|
||||
_description = 'Status Change Reason Wizard'
|
||||
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Sale Order',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
|
||||
new_status = fields.Selection(
|
||||
selection=[
|
||||
('rejected', 'Rejected by ADP'), # New: Initial rejection (within 24 hours)
|
||||
('denied', 'Application Denied'), # Funding denied (after 2-3 weeks)
|
||||
('withdrawn', 'Application Withdrawn'),
|
||||
('on_hold', 'On Hold'),
|
||||
('cancelled', 'Cancelled'),
|
||||
('needs_correction', 'Application Needs Correction'),
|
||||
],
|
||||
string='New Status',
|
||||
required=True,
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# REJECTION REASON FIELDS (for 'rejected' status - initial submission rejection)
|
||||
# ==========================================================================
|
||||
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',
|
||||
help='Select the reason ADP rejected the submission',
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# DENIAL REASON FIELDS (for 'denied' status - funding denial after review)
|
||||
# ==========================================================================
|
||||
denial_reason = fields.Selection(
|
||||
selection=[
|
||||
('eligibility', 'Client Eligibility Issues'),
|
||||
('recent_funding', 'Previous Funding Within 5 Years'),
|
||||
('medical_justification', 'Insufficient Medical Justification'),
|
||||
('equipment_not_covered', 'Equipment Not Covered by ADP'),
|
||||
('documentation_incomplete', 'Documentation Incomplete'),
|
||||
('other', 'Other'),
|
||||
],
|
||||
string='Denial Reason',
|
||||
help='Select the reason ADP denied the funding',
|
||||
)
|
||||
|
||||
reason = fields.Text(
|
||||
string='Reason / Additional Details',
|
||||
help='Please provide additional details for this status change.',
|
||||
)
|
||||
|
||||
# For on_hold: track the previous status
|
||||
previous_status = fields.Char(
|
||||
string='Previous Status',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
# Computed field to determine if reason is required
|
||||
reason_required = fields.Boolean(
|
||||
compute='_compute_reason_required',
|
||||
)
|
||||
|
||||
@api.depends('new_status', 'rejection_reason', 'denial_reason')
|
||||
def _compute_reason_required(self):
|
||||
"""Reason text is required for 'other' selections or non-rejection/denial statuses."""
|
||||
for wizard in self:
|
||||
if wizard.new_status == 'rejected':
|
||||
# Reason required if rejection_reason is 'other'
|
||||
wizard.reason_required = wizard.rejection_reason == 'other'
|
||||
elif wizard.new_status == 'denied':
|
||||
# Reason required if denial_reason is 'other'
|
||||
wizard.reason_required = wizard.denial_reason == 'other'
|
||||
else:
|
||||
# For other statuses (on_hold, cancelled, etc.), reason is always required
|
||||
wizard.reason_required = True
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
"""Set defaults from context."""
|
||||
res = super().default_get(fields_list)
|
||||
|
||||
if self.env.context.get('active_model') == 'sale.order':
|
||||
order_id = self.env.context.get('active_id')
|
||||
if order_id:
|
||||
order = self.env['sale.order'].browse(order_id)
|
||||
res['sale_order_id'] = order_id
|
||||
res['previous_status'] = order.x_fc_adp_application_status
|
||||
|
||||
if self.env.context.get('default_new_status'):
|
||||
res['new_status'] = self.env.context.get('default_new_status')
|
||||
|
||||
return res
|
||||
|
||||
def _get_status_label(self, status):
|
||||
"""Get human-readable label for status."""
|
||||
labels = {
|
||||
'rejected': 'Rejected by ADP',
|
||||
'denied': 'Application Denied',
|
||||
'withdrawn': 'Application Withdrawn',
|
||||
'on_hold': 'On Hold',
|
||||
'cancelled': 'Cancelled',
|
||||
'needs_correction': 'Application Needs Correction',
|
||||
}
|
||||
return labels.get(status, status)
|
||||
|
||||
def _get_status_icon(self, status):
|
||||
"""Get FontAwesome icon for status."""
|
||||
icons = {
|
||||
'rejected': 'fa-times',
|
||||
'denied': 'fa-times-circle',
|
||||
'withdrawn': 'fa-undo',
|
||||
'on_hold': 'fa-pause-circle',
|
||||
'cancelled': 'fa-ban',
|
||||
'needs_correction': 'fa-exclamation-triangle',
|
||||
}
|
||||
return icons.get(status, 'fa-info-circle')
|
||||
|
||||
def _get_rejection_reason_label(self, reason):
|
||||
"""Get human-readable label for rejection reason."""
|
||||
labels = {
|
||||
'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',
|
||||
}
|
||||
return labels.get(reason, reason)
|
||||
|
||||
def _get_denial_reason_label(self, reason):
|
||||
"""Get human-readable label for denial reason."""
|
||||
labels = {
|
||||
'eligibility': 'Client Eligibility Issues',
|
||||
'recent_funding': 'Previous Funding Within 5 Years',
|
||||
'medical_justification': 'Insufficient Medical Justification',
|
||||
'equipment_not_covered': 'Equipment Not Covered by ADP',
|
||||
'documentation_incomplete': 'Documentation Incomplete',
|
||||
'other': 'Other',
|
||||
}
|
||||
return labels.get(reason, reason)
|
||||
|
||||
def action_confirm(self):
|
||||
"""Confirm status change and post reason to chatter."""
|
||||
self.ensure_one()
|
||||
|
||||
order = self.sale_order_id
|
||||
new_status = self.new_status
|
||||
reason = self.reason or ''
|
||||
|
||||
# Build chatter message
|
||||
status_label = self._get_status_label(new_status)
|
||||
icon = self._get_status_icon(new_status)
|
||||
user_name = self.env.user.name
|
||||
change_date = fields.Date.today().strftime('%B %d, %Y')
|
||||
|
||||
# Color scheme for different status types
|
||||
status_colors = {
|
||||
'rejected': ('#e74c3c', '#fff5f5', '#f5c6cb'), # Red (lighter)
|
||||
'denied': ('#dc3545', '#fff5f5', '#f5c6cb'), # Red
|
||||
'withdrawn': ('#6c757d', '#f8f9fa', '#dee2e6'), # Gray
|
||||
'on_hold': ('#fd7e14', '#fff8f0', '#ffecd0'), # Orange
|
||||
'cancelled': ('#dc3545', '#fff5f5', '#f5c6cb'), # Red
|
||||
'needs_correction': ('#ffc107', '#fffbf0', '#ffeeba'), # Yellow
|
||||
}
|
||||
header_color, bg_color, border_color = status_colors.get(new_status, ('#17a2b8', '#f0f9ff', '#bee5eb'))
|
||||
|
||||
# For on_hold, also store the previous status and hold date
|
||||
update_vals = {'x_fc_adp_application_status': new_status}
|
||||
|
||||
# =================================================================
|
||||
# REJECTED: ADP rejected submission (within 24 hours)
|
||||
# =================================================================
|
||||
if new_status == 'rejected':
|
||||
rejection_reason = self.rejection_reason
|
||||
rejection_label = self._get_rejection_reason_label(rejection_reason)
|
||||
|
||||
# Store rejection details in sale order
|
||||
current_count = order.x_fc_rejection_count or 0
|
||||
update_vals.update({
|
||||
'x_fc_rejection_reason': rejection_reason,
|
||||
'x_fc_rejection_reason_other': reason if rejection_reason == 'other' else False,
|
||||
'x_fc_rejection_date': fields.Date.today(),
|
||||
'x_fc_rejection_count': current_count + 1,
|
||||
})
|
||||
|
||||
# Build rejection message
|
||||
details_html = ''
|
||||
if rejection_reason == 'other' and reason:
|
||||
details_html = f'<p><strong>Details:</strong> {reason}</p>'
|
||||
|
||||
message_body = f'''
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading"><i class="fa {icon}"></i> Submission Rejected by ADP</h5>
|
||||
<ul>
|
||||
<li><strong>Rejection #:</strong> {current_count + 1}</li>
|
||||
<li><strong>Reason:</strong> {rejection_label}</li>
|
||||
<li><strong>Date:</strong> {change_date}</li>
|
||||
<li><strong>Recorded By:</strong> {user_name}</li>
|
||||
</ul>
|
||||
{details_html}
|
||||
<hr>
|
||||
<p class="mb-0"><i class="fa fa-info-circle"></i> <strong>Next Step:</strong> Correct the issue and resubmit the application.</p>
|
||||
</div>
|
||||
'''
|
||||
|
||||
# =================================================================
|
||||
# DENIED: Funding denied by ADP (after 2-3 weeks review)
|
||||
# =================================================================
|
||||
elif new_status == 'denied':
|
||||
denial_reason = self.denial_reason
|
||||
denial_label = self._get_denial_reason_label(denial_reason)
|
||||
|
||||
# Store denial details in sale order
|
||||
update_vals.update({
|
||||
'x_fc_denial_reason': denial_reason,
|
||||
'x_fc_denial_reason_other': reason if denial_reason == 'other' else False,
|
||||
'x_fc_denial_date': fields.Date.today(),
|
||||
})
|
||||
|
||||
# Build denial message
|
||||
details_html = ''
|
||||
if denial_reason == 'other' and reason:
|
||||
details_html = f'<p><strong>Details:</strong> {reason}</p>'
|
||||
|
||||
message_body = f'''
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading"><i class="fa {icon}"></i> Funding Denied by ADP</h5>
|
||||
<ul>
|
||||
<li><strong>Denial Reason:</strong> {denial_label}</li>
|
||||
<li><strong>Date:</strong> {change_date}</li>
|
||||
<li><strong>Recorded By:</strong> {user_name}</li>
|
||||
</ul>
|
||||
{details_html}
|
||||
</div>
|
||||
'''
|
||||
|
||||
# =================================================================
|
||||
# ON HOLD: Application put on hold
|
||||
# Message is posted by _send_on_hold_email() to avoid duplicates
|
||||
# =================================================================
|
||||
elif new_status == 'on_hold':
|
||||
update_vals['x_fc_on_hold_date'] = fields.Date.today()
|
||||
update_vals['x_fc_previous_status_before_hold'] = self.previous_status
|
||||
# Don't post message here - _send_on_hold_email() will post the message
|
||||
message_body = None
|
||||
elif new_status == 'withdrawn':
|
||||
# Don't post message here - _send_withdrawal_email() will post the message
|
||||
message_body = None
|
||||
elif new_status == 'cancelled':
|
||||
# Cancelled has its own detailed message posted later
|
||||
message_body = None
|
||||
else:
|
||||
message_body = f'''
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h5 class="alert-heading"><i class="fa {icon}"></i> Status Changed: {status_label}</h5>
|
||||
<ul>
|
||||
<li><strong>Changed By:</strong> {user_name}</li>
|
||||
<li><strong>Date:</strong> {change_date}</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<p class="mb-0"><strong>Reason:</strong> {reason}</p>
|
||||
</div>
|
||||
'''
|
||||
|
||||
# Post to chatter (except for cancelled which has its own detailed message)
|
||||
if message_body:
|
||||
order.message_post(
|
||||
body=Markup(message_body),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
# Update the status (this will trigger the write() method)
|
||||
# Use context to skip the wizard trigger and skip auto-email for needs_correction
|
||||
# (we send it below with the reason text)
|
||||
write_ctx = {'skip_status_validation': True}
|
||||
if new_status == 'needs_correction':
|
||||
write_ctx['skip_correction_email'] = True
|
||||
order.with_context(**write_ctx).write(update_vals)
|
||||
|
||||
# =================================================================
|
||||
# NEEDS CORRECTION: Send email with the reason from this wizard
|
||||
# =================================================================
|
||||
if new_status == 'needs_correction':
|
||||
order._send_correction_needed_email(reason=reason)
|
||||
|
||||
# =================================================================
|
||||
# WITHDRAWN: Send email notification to all parties
|
||||
# =================================================================
|
||||
if new_status == 'withdrawn':
|
||||
order._send_withdrawal_email(reason=reason)
|
||||
|
||||
# =================================================================
|
||||
# ON HOLD: Send email notification to all parties
|
||||
# =================================================================
|
||||
if new_status == 'on_hold':
|
||||
order._send_on_hold_email(reason=reason)
|
||||
|
||||
# =================================================================
|
||||
# CANCELLED: Also cancel the sale order and all related invoices
|
||||
# =================================================================
|
||||
if new_status == 'cancelled':
|
||||
cancelled_invoices = []
|
||||
cancelled_so = False
|
||||
user_name = self.env.user.name
|
||||
cancel_date = fields.Date.today().strftime('%B %d, %Y')
|
||||
|
||||
# Cancel related invoices first
|
||||
invoices = order.invoice_ids.filtered(lambda inv: inv.state != 'cancel')
|
||||
for invoice in invoices:
|
||||
try:
|
||||
# Post cancellation reason to invoice chatter
|
||||
inv_msg = Markup(f'''
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading"><i class="fa fa-ban"></i> Invoice Cancelled</h5>
|
||||
<ul>
|
||||
<li><strong>Related Order:</strong> {order.name}</li>
|
||||
<li><strong>Cancelled By:</strong> {user_name}</li>
|
||||
<li><strong>Date:</strong> {cancel_date}</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<p class="mb-0"><strong>Reason:</strong> {reason}</p>
|
||||
</div>
|
||||
''')
|
||||
invoice.message_post(
|
||||
body=inv_msg,
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
# Cancel the invoice (button_cancel or button_draft then cancel)
|
||||
if invoice.state == 'posted':
|
||||
invoice.button_draft()
|
||||
invoice.button_cancel()
|
||||
cancelled_invoices.append(invoice.name)
|
||||
except Exception as e:
|
||||
warn_msg = Markup(f'''
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p class="mb-0"><i class="fa fa-exclamation-triangle"></i> <strong>Warning:</strong> Could not cancel invoice {invoice.name}</p>
|
||||
<p class="mb-0 small">{str(e)}</p>
|
||||
</div>
|
||||
''')
|
||||
order.message_post(
|
||||
body=warn_msg,
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
# Cancel the sale order itself
|
||||
if order.state not in ('cancel', 'done'):
|
||||
try:
|
||||
order._action_cancel()
|
||||
cancelled_so = True
|
||||
except Exception as e:
|
||||
warn_msg = Markup(f'''
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<p class="mb-0"><i class="fa fa-exclamation-triangle"></i> <strong>Warning:</strong> Could not cancel sale order</p>
|
||||
<p class="mb-0 small">{str(e)}</p>
|
||||
</div>
|
||||
''')
|
||||
order.message_post(
|
||||
body=warn_msg,
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
# Build cancellation summary
|
||||
invoice_list_html = ''
|
||||
if cancelled_invoices:
|
||||
invoice_items = ''.join([f'<li>{inv}</li>' for inv in cancelled_invoices])
|
||||
invoice_list_html = f'<li><strong>Invoices Cancelled:</strong><ul>{invoice_items}</ul></li>'
|
||||
|
||||
# Post comprehensive summary to chatter
|
||||
so_status = 'Cancelled' if cancelled_so else 'Not applicable'
|
||||
summary_msg = Markup(f'''
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h5 class="alert-heading"><i class="fa fa-ban"></i> Application Cancelled</h5>
|
||||
<ul>
|
||||
<li><strong>Cancelled By:</strong> {user_name}</li>
|
||||
<li><strong>Date:</strong> {cancel_date}</li>
|
||||
<li><strong>Sale Order:</strong> {so_status}</li>
|
||||
{invoice_list_html}
|
||||
</ul>
|
||||
<hr>
|
||||
<p class="mb-0"><strong>Reason for Cancellation:</strong> {reason}</p>
|
||||
</div>
|
||||
''')
|
||||
order.message_post(
|
||||
body=summary_msg,
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
Reference in New Issue
Block a user