This commit is contained in:
gsinghpal
2026-03-09 15:21:22 -04:00
parent a3e85a23ef
commit acd3fc455e
243 changed files with 20459 additions and 4197 deletions

View File

@@ -30,4 +30,5 @@ from . import odsp_discretionary_wizard
from . import odsp_pre_approved_wizard
from . import odsp_ready_delivery_wizard
from . import odsp_submit_to_odsp_wizard
from . import ltc_repair_create_so_wizard
from . import ltc_repair_create_so_wizard
from . import send_page11_wizard

View File

@@ -34,18 +34,42 @@ class ApplicationReceivedWizard(models.TransientModel):
signed_pages_11_12 = fields.Binary(
string='Signed Pages 11 & 12',
required=True,
help='Upload the signed pages 11 and 12 from the application',
help='Upload the signed pages 11 and 12 from the application. '
'Not required if a remote signing request has been sent.',
)
signed_pages_filename = fields.Char(
string='Pages Filename',
)
has_pending_page11_request = fields.Boolean(
compute='_compute_has_pending_page11_request',
)
has_signed_page11 = fields.Boolean(
compute='_compute_has_pending_page11_request',
)
notes = fields.Text(
string='Notes',
help='Any notes about the received application',
)
@api.depends('sale_order_id')
def _compute_has_pending_page11_request(self):
for wiz in self:
order = wiz.sale_order_id
if order:
requests = order.page11_sign_request_ids
wiz.has_pending_page11_request = bool(
requests.filtered(lambda r: r.state in ('draft', 'sent'))
)
wiz.has_signed_page11 = bool(
order.x_fc_signed_pages_11_12
or requests.filtered(lambda r: r.state == 'signed')
)
else:
wiz.has_pending_page11_request = False
wiz.has_signed_page11 = False
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
@@ -53,7 +77,6 @@ class ApplicationReceivedWizard(models.TransientModel):
if active_id:
order = self.env['sale.order'].browse(active_id)
res['sale_order_id'] = order.id
# Pre-fill if documents already exist
if order.x_fc_original_application:
res['original_application'] = order.x_fc_original_application
res['original_application_filename'] = order.x_fc_original_application_filename
@@ -91,20 +114,33 @@ class ApplicationReceivedWizard(models.TransientModel):
if order.x_fc_adp_application_status not in ('assessment_completed', 'waiting_for_application'):
raise UserError("Can only receive application from 'Waiting for Application' status.")
# Validate files are uploaded
if not self.original_application:
raise UserError("Please upload the Original ADP Application.")
if not self.signed_pages_11_12:
raise UserError("Please upload the Signed Pages 11 & 12.")
# Update sale order with documents
order.with_context(skip_status_validation=True).write({
page11_covered = bool(
self.signed_pages_11_12
or order.x_fc_signed_pages_11_12
or order.page11_sign_request_ids.filtered(
lambda r: r.state in ('sent', 'signed')
)
)
if not page11_covered:
raise UserError(
"Signed Pages 11 & 12 are required.\n\n"
"You can either upload the file here, or use the "
"'Request Page 11 Signature' button on the sale order "
"to send it for remote signing before confirming."
)
vals = {
'x_fc_adp_application_status': 'application_received',
'x_fc_original_application': self.original_application,
'x_fc_original_application_filename': self.original_application_filename,
'x_fc_signed_pages_11_12': self.signed_pages_11_12,
'x_fc_signed_pages_filename': self.signed_pages_filename,
})
}
if self.signed_pages_11_12:
vals['x_fc_signed_pages_11_12'] = self.signed_pages_11_12
vals['x_fc_signed_pages_filename'] = self.signed_pages_filename
order.with_context(skip_status_validation=True).write(vals)
# Post to chatter
from datetime import date
@@ -128,3 +164,15 @@ class ApplicationReceivedWizard(models.TransientModel):
)
return {'type': 'ir.actions.act_window_close'}
def action_request_page11_signature(self):
"""Open the Page 11 remote signing wizard from within the Application Received wizard."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Request Page 11 Signature',
'res_model': 'fusion_claims.send.page11.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_sale_order_id': self.sale_order_id.id},
}

View File

@@ -10,10 +10,12 @@
<strong><i class="fa fa-info-circle"/> Upload Required Documents</strong>
<p class="mb-0">Please upload the ADP application documents received from the client.</p>
</div>
<field name="sale_order_id" invisible="1"/>
<field name="has_pending_page11_request" invisible="1"/>
<field name="has_signed_page11" invisible="1"/>
<group>
<field name="sale_order_id" invisible="1"/>
<group string="Original ADP Application">
<field name="original_application" filename="original_application_filename"
widget="binary" class="oe_inline"/>
@@ -24,6 +26,28 @@
<field name="signed_pages_11_12" filename="signed_pages_filename"
widget="binary" class="oe_inline"/>
<field name="signed_pages_filename" invisible="1"/>
<div invisible="has_signed_page11" class="mt-2">
<span class="text-muted small">Don't have signed pages? </span>
<button name="action_request_page11_signature" type="object"
string="Request Remote Signature"
class="btn btn-sm btn-outline-warning"
icon="fa-pencil-square-o"
help="Send Page 11 to a family member or agent for digital signing"/>
</div>
<div invisible="not has_pending_page11_request" class="mt-2">
<div class="alert alert-warning mb-0 py-2 px-3">
<i class="fa fa-clock-o"/> A remote signing request has been sent.
You can proceed without uploading signed pages -- they will be auto-filled when signed.
</div>
</div>
<div invisible="not has_signed_page11 or signed_pages_11_12" class="mt-2">
<div class="alert alert-success mb-0 py-2 px-3">
<i class="fa fa-check-circle"/> Page 11 has been signed remotely.
</div>
</div>
</group>
</group>

View File

@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from datetime import timedelta
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class SendPage11Wizard(models.TransientModel):
_name = 'fusion_claims.send.page11.wizard'
_description = 'Send Page 11 for Remote Signing'
sale_order_id = fields.Many2one(
'sale.order', string='Sale Order',
required=True, readonly=True,
)
signer_email = fields.Char(string='Recipient Email', required=True)
signer_type = fields.Selection([
('client', 'Client (Self)'),
('spouse', 'Spouse'),
('parent', 'Parent'),
('legal_guardian', 'Legal Guardian'),
('poa', 'Power of Attorney'),
('public_trustee', 'Public Trustee'),
], string='Signer Type', default='client', required=True)
signer_name = fields.Char(string='Signer Name', required=True)
custom_message = fields.Text(
string='Personal Message',
help='Optional message to include in the signing request email.',
)
expiry_days = fields.Integer(
string='Link Valid For (days)', default=7, required=True,
)
client_name = fields.Char(string='Client', readonly=True)
case_ref = fields.Char(string='Case Reference', readonly=True)
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
active_id = self.env.context.get('active_id')
if not active_id:
return res
order = self.env['sale.order'].browse(active_id)
res['sale_order_id'] = order.id
res['client_name'] = order.partner_id.name or ''
res['case_ref'] = order.name or ''
res['signer_name'] = order.partner_id.name or ''
res['signer_email'] = order.partner_id.email or ''
return res
def action_send(self):
"""Create a signing request and send the email."""
self.ensure_one()
if not self.signer_email:
raise UserError(_("Please enter the recipient's email address."))
if self.expiry_days < 1:
raise UserError(_("Expiry must be at least 1 day."))
request = self.env['fusion.page11.sign.request'].create({
'sale_order_id': self.sale_order_id.id,
'signer_email': self.signer_email,
'signer_type': self.signer_type,
'signer_name': self.signer_name,
'custom_message': self.custom_message,
'expiry_date': fields.Datetime.now() + timedelta(days=self.expiry_days),
'consent_signed_by': 'applicant' if self.signer_type == 'client' else 'agent',
'signer_relationship': dict(self._fields['signer_type'].selection).get(
self.signer_type, ''
) if self.signer_type != 'client' else '',
})
request._send_signing_email()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Page 11 Signing Request Sent'),
'message': _(
'Signing request has been sent to %s.',
self.signer_email,
),
'type': 'success',
'sticky': False,
'next': {'type': 'ir.actions.act_window_close'},
},
}

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_send_page11_wizard_form" model="ir.ui.view">
<field name="name">fusion_claims.send.page11.wizard.form</field>
<field name="model">fusion_claims.send.page11.wizard</field>
<field name="arch" type="xml">
<form string="Send Page 11 for Signing">
<group>
<group string="Case Information">
<field name="sale_order_id" invisible="1"/>
<field name="client_name"/>
<field name="case_ref"/>
</group>
<group string="Recipient">
<field name="signer_name"/>
<field name="signer_email" widget="email"/>
<field name="signer_type"/>
<field name="expiry_days"/>
</group>
</group>
<group string="Personal Message (Optional)">
<field name="custom_message" nolabel="1" placeholder="Add a personal note to include in the email..."/>
</group>
<footer>
<button name="action_send" type="object" string="Send Signing Request" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_send_page11_wizard" model="ir.actions.act_window">
<field name="name">Request Page 11 Signature</field>
<field name="res_model">fusion_claims.send.page11.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'default_sale_order_id': active_id}</field>
</record>
</odoo>

View File

@@ -61,6 +61,18 @@ class StatusChangeReasonWizard(models.TransientModel):
help='Select the reason ADP denied the funding',
)
# ==========================================================================
# WITHDRAWAL INTENT (for 'withdrawn' status)
# ==========================================================================
withdrawal_intent = fields.Selection(
selection=[
('cancel', 'Cancel Application'),
('resubmit', 'Withdraw for Correction & Resubmission'),
],
string='What would you like to do after withdrawal?',
default='resubmit',
)
reason = fields.Text(
string='Reason / Additional Details',
help='Please provide additional details for this status change.',
@@ -181,8 +193,10 @@ class StatusChangeReasonWizard(models.TransientModel):
}
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
# Build initial update vals
update_vals = {'x_fc_adp_application_status': new_status}
if new_status == 'withdrawn':
update_vals['x_fc_previous_status_before_withdrawal'] = self.previous_status
# =================================================================
# REJECTED: ADP rejected submission (within 24 hours)
@@ -261,7 +275,7 @@ class StatusChangeReasonWizard(models.TransientModel):
# 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
# Handled entirely below based on withdrawal_intent
message_body = None
elif new_status == 'cancelled':
# Cancelled has its own detailed message posted later
@@ -302,10 +316,129 @@ class StatusChangeReasonWizard(models.TransientModel):
order._send_correction_needed_email(reason=reason)
# =================================================================
# WITHDRAWN: Send email notification to all parties
# WITHDRAWN: Branch based on withdrawal intent
# =================================================================
if new_status == 'withdrawn':
order._send_withdrawal_email(reason=reason)
intent = self.withdrawal_intent
if intent == 'cancel':
# ---------------------------------------------------------
# WITHDRAW & CANCEL: Cancel invoices + SO
# ---------------------------------------------------------
cancelled_invoices = []
cancelled_so = False
# Cancel related invoices first
invoices = order.invoice_ids.filtered(lambda inv: inv.state != 'cancel')
for invoice in invoices:
try:
inv_msg = Markup(f'''
<div class="alert alert-danger" role="alert">
<h5 class="alert-heading"><i class="fa fa-ban"></i> Invoice Cancelled (Withdrawal)</h5>
<ul>
<li><strong>Related Order:</strong> {order.name}</li>
<li><strong>Cancelled By:</strong> {user_name}</li>
<li><strong>Date:</strong> {change_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',
)
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>'
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 Withdrawn &amp; Cancelled</h5>
<ul>
<li><strong>Withdrawn By:</strong> {user_name}</li>
<li><strong>Date:</strong> {change_date}</li>
<li><strong>Sale Order:</strong> {so_status}</li>
{invoice_list_html}
</ul>
<hr>
<p class="mb-0"><strong>Reason:</strong> {reason}</p>
</div>
''')
order.message_post(
body=summary_msg,
message_type='notification',
subtype_xmlid='mail.mt_note',
)
order._send_withdrawal_email(reason=reason, intent='cancel')
else:
# ---------------------------------------------------------
# WITHDRAW & RESUBMIT: Return to ready_submission
# ---------------------------------------------------------
order.with_context(skip_status_validation=True).write({
'x_fc_adp_application_status': 'ready_submission',
'x_fc_previous_status_before_withdrawal': self.previous_status,
})
resubmit_msg = Markup(f'''
<div class="alert alert-info" role="alert">
<h5 class="alert-heading"><i class="fa fa-undo"></i> Application Withdrawn for Correction</h5>
<ul>
<li><strong>Withdrawn By:</strong> {user_name}</li>
<li><strong>Date:</strong> {change_date}</li>
<li><strong>Status Returned To:</strong> Ready for Submission</li>
</ul>
<hr>
<p class="mb-0"><strong>Reason:</strong> {reason}</p>
<p class="mb-0 mt-2"><i class="fa fa-info-circle"></i> Make corrections and click <strong>Submit Application</strong> to resubmit.</p>
</div>
''')
order.message_post(
body=resubmit_msg,
message_type='notification',
subtype_xmlid='mail.mt_note',
)
order._send_withdrawal_email(reason=reason, intent='resubmit')
# =================================================================
# ON HOLD: Send email notification to all parties

View File

@@ -26,7 +26,7 @@
<div class="alert alert-info mb-3 rounded-0" role="alert" invisible="new_status != 'withdrawn'"
style="margin: -16px -16px 16px -16px; padding: 12px 16px;">
<i class="fa fa-undo me-2"/> You are about to <strong>Withdraw</strong> this application. Please provide the reason for withdrawal.
<i class="fa fa-undo me-2"/> You are about to <strong>Withdraw</strong> this application. Please select what you would like to do after withdrawal.
</div>
<div class="alert alert-secondary mb-3 rounded-0" role="alert" invisible="new_status != 'on_hold'"
@@ -45,6 +45,26 @@
</div>
<div class="px-3 pb-3">
<!-- WITHDRAWAL INTENT (for 'withdrawn' status) -->
<group invisible="new_status != 'withdrawn'">
<group>
<field name="withdrawal_intent"
required="new_status == 'withdrawn'"
widget="radio"
options="{'horizontal': false}"/>
</group>
</group>
<div class="alert alert-danger mt-2 mb-3" role="alert"
invisible="new_status != 'withdrawn' or withdrawal_intent != 'cancel'">
<i class="fa fa-exclamation-triangle me-2"/>
This will <strong>permanently cancel</strong> the sale order and all related invoices. This action cannot be undone.
</div>
<div class="alert alert-info mt-2 mb-3" role="alert"
invisible="new_status != 'withdrawn' or withdrawal_intent != 'resubmit'">
<i class="fa fa-info-circle me-2"/>
The application will return to <strong>Ready for Submission</strong> status. You can make corrections and resubmit.
</div>
<!-- REJECTION REASON (for 'rejected' status) -->
<group invisible="new_status != 'rejected'">
<group>