feat: separate fusion field service and LTC into standalone modules, update core modules
- fusion_claims: separated field service logic, updated controllers/views - fusion_tasks: updated task views and map integration - fusion_authorizer_portal: added page 11 signing, schedule booking, migrations - fusion_shipping: new standalone shipping module (Canada Post, FedEx, DHL, Purolator) - fusion_ltc_management: new standalone LTC management module
This commit is contained in:
@@ -30,4 +30,4 @@ 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 send_page11_wizard
|
||||
@@ -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},
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ _logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AssessmentCompletedWizard(models.TransientModel):
|
||||
"""Wizard to record assessment completion date."""
|
||||
_name = 'fusion_claims.assessment.completed.wizard'
|
||||
_description = 'Assessment Completed Wizard'
|
||||
|
||||
@@ -21,18 +20,49 @@ class AssessmentCompletedWizard(models.TransientModel):
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
|
||||
is_override = fields.Boolean(
|
||||
string='Scheduling Override',
|
||||
compute='_compute_is_override',
|
||||
store=False,
|
||||
)
|
||||
|
||||
assessment_start_date = fields.Date(
|
||||
string='Assessment Start Date',
|
||||
required=True,
|
||||
help='Date the assessment was conducted',
|
||||
)
|
||||
|
||||
completion_date = fields.Date(
|
||||
string='Assessment Completion Date',
|
||||
required=True,
|
||||
default=fields.Date.context_today,
|
||||
)
|
||||
|
||||
|
||||
notes = fields.Text(
|
||||
string='Assessment Notes',
|
||||
help='Any notes from the assessment',
|
||||
string='Notes',
|
||||
help='Notes from the assessment',
|
||||
)
|
||||
|
||||
override_reason = fields.Text(
|
||||
string='Override Reason',
|
||||
help='Mandatory when skipping the scheduling step. Explain why the assessment was completed without scheduling through the system.',
|
||||
)
|
||||
|
||||
notify_authorizer = fields.Boolean(
|
||||
string='Notify Authorizer',
|
||||
default=True,
|
||||
help='Send email to the authorizer about assessment completion',
|
||||
)
|
||||
|
||||
@api.depends('sale_order_id')
|
||||
def _compute_is_override(self):
|
||||
for rec in self:
|
||||
rec.is_override = (
|
||||
rec.sale_order_id
|
||||
and rec.sale_order_id.x_fc_adp_application_status == 'quotation'
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
@@ -40,43 +70,174 @@ class AssessmentCompletedWizard(models.TransientModel):
|
||||
if active_id:
|
||||
order = self.env['sale.order'].browse(active_id)
|
||||
res['sale_order_id'] = order.id
|
||||
if order.x_fc_assessment_start_date:
|
||||
res['assessment_start_date'] = order.x_fc_assessment_start_date
|
||||
else:
|
||||
res['assessment_start_date'] = fields.Date.context_today(self)
|
||||
return res
|
||||
|
||||
def action_complete(self):
|
||||
"""Mark assessment as completed."""
|
||||
self.ensure_one()
|
||||
|
||||
|
||||
order = self.sale_order_id
|
||||
|
||||
if order.x_fc_adp_application_status != 'assessment_scheduled':
|
||||
raise UserError("Can only complete assessment from 'Assessment Scheduled' status.")
|
||||
|
||||
# Validate completion date is not before start date
|
||||
if order.x_fc_assessment_start_date and self.completion_date < order.x_fc_assessment_start_date:
|
||||
current_status = order.x_fc_adp_application_status
|
||||
is_override = current_status == 'quotation'
|
||||
|
||||
if current_status not in ('quotation', 'assessment_scheduled'):
|
||||
raise UserError(
|
||||
f"Completion date ({self.completion_date}) cannot be before "
|
||||
f"assessment start date ({order.x_fc_assessment_start_date})."
|
||||
_("Can only complete assessment from 'Quotation' or 'Assessment Scheduled' status.")
|
||||
)
|
||||
|
||||
# Update sale order
|
||||
order.with_context(skip_status_validation=True).write({
|
||||
|
||||
if is_override and not (self.override_reason or '').strip():
|
||||
raise UserError(
|
||||
_("Override Reason is mandatory when skipping the assessment scheduling step. "
|
||||
"Please explain why this assessment was completed without being scheduled through the system.")
|
||||
)
|
||||
|
||||
if self.completion_date < self.assessment_start_date:
|
||||
raise UserError(
|
||||
_("Completion date (%s) cannot be before assessment start date (%s).")
|
||||
% (self.completion_date, self.assessment_start_date)
|
||||
)
|
||||
|
||||
write_vals = {
|
||||
'x_fc_adp_application_status': 'assessment_completed',
|
||||
'x_fc_assessment_end_date': self.completion_date,
|
||||
})
|
||||
|
||||
# Post to chatter
|
||||
notes_html = f'<p style="margin: 4px 0 0 0;"><strong>Notes:</strong> {self.notes}</p>' if self.notes else ''
|
||||
|
||||
order.message_post(
|
||||
body=Markup(
|
||||
'<div style="background: #d4edda; border-left: 4px solid #28a745; padding: 12px; margin: 8px 0; border-radius: 4px;">'
|
||||
'<h4 style="color: #28a745; margin: 0 0 8px 0;"><i class="fa fa-check-square-o"/> Assessment Completed</h4>'
|
||||
f'<p style="margin: 0;"><strong>Completion Date:</strong> {self.completion_date.strftime("%B %d, %Y")}</p>'
|
||||
f'{notes_html}'
|
||||
}
|
||||
if is_override or not order.x_fc_assessment_start_date:
|
||||
write_vals['x_fc_assessment_start_date'] = self.assessment_start_date
|
||||
|
||||
order.with_context(skip_status_validation=True).write(write_vals)
|
||||
|
||||
if is_override:
|
||||
override_html = Markup(
|
||||
'<div style="background:#fff3cd;border-left:4px solid #ffc107;padding:12px;margin:8px 0;border-radius:4px;">'
|
||||
'<h4 style="color:#856404;margin:0 0 8px 0;">'
|
||||
'<i class="fa fa-exclamation-triangle"/> Assessment Scheduling Override</h4>'
|
||||
'<p style="margin:0;"><strong>Override by:</strong> %s</p>'
|
||||
'<p style="margin:4px 0 0 0;"><strong>Reason:</strong> %s</p>'
|
||||
'<p style="margin:4px 0 0 0;"><strong>Assessment Date:</strong> %s to %s</p>'
|
||||
'%s'
|
||||
'</div>'
|
||||
),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
) % (
|
||||
self.env.user.name,
|
||||
self.override_reason.strip(),
|
||||
self.assessment_start_date.strftime("%B %d, %Y"),
|
||||
self.completion_date.strftime("%B %d, %Y"),
|
||||
Markup('<p style="margin:4px 0 0 0;"><strong>Notes:</strong> %s</p>') % self.notes if self.notes else Markup(''),
|
||||
)
|
||||
order.message_post(
|
||||
body=override_html,
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
else:
|
||||
notes_html = (
|
||||
Markup('<p style="margin:4px 0 0 0;"><strong>Notes:</strong> %s</p>') % self.notes
|
||||
) if self.notes else Markup('')
|
||||
|
||||
order.message_post(
|
||||
body=Markup(
|
||||
'<div style="background:#d4edda;border-left:4px solid #28a745;padding:12px;margin:8px 0;border-radius:4px;">'
|
||||
'<h4 style="color:#28a745;margin:0 0 8px 0;">'
|
||||
'<i class="fa fa-check-square-o"/> Assessment Completed</h4>'
|
||||
'<p style="margin:0;"><strong>Completion Date:</strong> %s</p>'
|
||||
'%s'
|
||||
'</div>'
|
||||
) % (self.completion_date.strftime("%B %d, %Y"), notes_html),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
if self.notify_authorizer:
|
||||
self._send_backend_completion_email(order, is_override)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def _send_backend_completion_email(self, order, is_override):
|
||||
"""Send assessment completion email when done from backend."""
|
||||
self.ensure_one()
|
||||
|
||||
if not order._email_is_enabled():
|
||||
return
|
||||
|
||||
authorizer = order.x_fc_authorizer_id
|
||||
if not authorizer or not authorizer.email:
|
||||
_logger.info("No authorizer email for %s, skipping notification", order.name)
|
||||
return
|
||||
|
||||
to_email = authorizer.email
|
||||
cc_emails = []
|
||||
if order.user_id and order.user_id.email:
|
||||
cc_emails.append(order.user_id.email)
|
||||
company = self.env.company
|
||||
office_partners = company.sudo().x_fc_office_notification_ids
|
||||
cc_emails.extend([p.email for p in office_partners if p.email])
|
||||
|
||||
client_name = order.partner_id.name or 'Client'
|
||||
override_note = ''
|
||||
if is_override:
|
||||
override_note = (
|
||||
'<div style="background:#fff3cd;border-left:3px solid #ffc107;padding:8px 12px;'
|
||||
'margin:12px 0;border-radius:4px;">'
|
||||
'<strong>Note:</strong> This assessment was completed without being scheduled '
|
||||
'through the system. '
|
||||
f'<strong>Reason:</strong> {self.override_reason.strip()}'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
sections = [
|
||||
('Assessment Details', [
|
||||
('Client', client_name),
|
||||
('Case', order.name),
|
||||
('Assessment Date', f"{self.assessment_start_date.strftime('%B %d, %Y')} to {self.completion_date.strftime('%B %d, %Y')}"),
|
||||
('Completed by', self.env.user.name),
|
||||
]),
|
||||
]
|
||||
if self.notes:
|
||||
sections.append(('Notes', [('', self.notes)]))
|
||||
|
||||
summary = (
|
||||
f'The assessment for <strong>{client_name}</strong> ({order.name}) '
|
||||
f'has been completed on {self.completion_date.strftime("%B %d, %Y")}.'
|
||||
)
|
||||
if is_override:
|
||||
summary += f' {override_note}'
|
||||
|
||||
email_body = order._email_build(
|
||||
title='Assessment Completed',
|
||||
summary=summary,
|
||||
email_type='success',
|
||||
sections=sections,
|
||||
note='<strong>Next step:</strong> Please submit the ADP application '
|
||||
'(including pages 11-12 signed by the client) so we can proceed.',
|
||||
button_url=f'{order.get_base_url()}/web#id={order.id}&model=sale.order&view_type=form',
|
||||
button_text='View Case',
|
||||
sender_name=order.user_id.name if order.user_id else 'The Team',
|
||||
)
|
||||
|
||||
try:
|
||||
self.env['mail.mail'].sudo().create({
|
||||
'subject': f'Assessment Completed - {client_name} - {order.name}',
|
||||
'body_html': email_body,
|
||||
'email_to': to_email,
|
||||
'email_cc': ', '.join(cc_emails) if cc_emails else False,
|
||||
'model': 'sale.order',
|
||||
'res_id': order.id,
|
||||
'auto_delete': True,
|
||||
}).send()
|
||||
|
||||
order.message_post(
|
||||
body=Markup(
|
||||
'<div class="alert alert-info" role="alert">'
|
||||
'<strong>Assessment Completed email sent</strong>'
|
||||
'<ul class="mb-0 mt-1"><li>To: %s</li>'
|
||||
'<li>CC: %s</li></ul></div>'
|
||||
) % (to_email, ', '.join(cc_emails) or 'None'),
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
_logger.info("Sent backend assessment completed email for %s", order.name)
|
||||
except Exception as e:
|
||||
_logger.error("Failed to send assessment completed email for %s: %s", order.name, e)
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Assessment Completed Wizard Form View -->
|
||||
<record id="view_assessment_completed_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion_claims.assessment.completed.wizard.form</field>
|
||||
<field name="model">fusion_claims.assessment.completed.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Assessment Completed">
|
||||
<field name="sale_order_id" invisible="1"/>
|
||||
<field name="is_override" invisible="1"/>
|
||||
|
||||
<div invisible="not is_override"
|
||||
class="alert alert-warning mb-3" role="alert">
|
||||
<strong>Scheduling Override:</strong>
|
||||
This assessment was not scheduled through the system.
|
||||
A reason is required to proceed.
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<field name="sale_order_id" invisible="1"/>
|
||||
<field name="completion_date"/>
|
||||
<field name="notes" placeholder="Enter any notes from the assessment..."/>
|
||||
<group string="Assessment Dates">
|
||||
<field name="assessment_start_date"/>
|
||||
<field name="completion_date"/>
|
||||
</group>
|
||||
<group string="Details">
|
||||
<field name="notes"
|
||||
placeholder="Enter any notes from the assessment..."/>
|
||||
<field name="notify_authorizer"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group invisible="not is_override">
|
||||
<field name="override_reason"
|
||||
required="is_override"
|
||||
placeholder="e.g., Authorizer completed the assessment independently and sent us the application directly..."
|
||||
widget="text"/>
|
||||
</group>
|
||||
|
||||
<footer>
|
||||
<button name="action_complete" type="object"
|
||||
<button name="action_complete" type="object"
|
||||
string="Mark Complete" class="btn-primary"
|
||||
icon="fa-check"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
@@ -21,7 +44,6 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for the wizard -->
|
||||
<record id="action_assessment_completed_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Assessment Completed</field>
|
||||
<field name="res_model">fusion_claims.assessment.completed.wizard</field>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class LTCRepairCreateSOWizard(models.TransientModel):
|
||||
_name = 'fusion.ltc.repair.create.so.wizard'
|
||||
_description = 'LTC Repair - Link Contact & Create Sale Order'
|
||||
|
||||
repair_id = fields.Many2one(
|
||||
'fusion.ltc.repair',
|
||||
string='Repair Request',
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
client_name = fields.Char(
|
||||
string='Client Name',
|
||||
readonly=True,
|
||||
)
|
||||
action_type = fields.Selection([
|
||||
('create_new', 'Create New Contact'),
|
||||
('link_existing', 'Link to Existing Contact'),
|
||||
], string='Action', default='create_new', required=True)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Existing Contact',
|
||||
)
|
||||
|
||||
def action_confirm(self):
|
||||
self.ensure_one()
|
||||
repair = self.repair_id
|
||||
|
||||
if self.action_type == 'create_new':
|
||||
if not self.client_name:
|
||||
raise UserError(_('Client name is required to create a new contact.'))
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': self.client_name,
|
||||
})
|
||||
repair.client_id = partner
|
||||
elif self.action_type == 'link_existing':
|
||||
if not self.partner_id:
|
||||
raise UserError(_('Please select an existing contact.'))
|
||||
repair.client_id = self.partner_id
|
||||
|
||||
return repair._create_linked_sale_order()
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_ltc_repair_create_so_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.repair.create.so.wizard.form</field>
|
||||
<field name="model">fusion.ltc.repair.create.so.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Link Contact">
|
||||
<group>
|
||||
<field name="repair_id" invisible="1"/>
|
||||
<field name="client_name"/>
|
||||
<field name="action_type" widget="radio"/>
|
||||
<field name="partner_id"
|
||||
invisible="action_type != 'link_existing'"
|
||||
required="action_type == 'link_existing'"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_confirm" type="object"
|
||||
string="Confirm & Create Sale Order"
|
||||
class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary"
|
||||
special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -34,11 +34,11 @@ class ReadyForSubmissionWizard(models.TransientModel):
|
||||
# Client References (may already be filled)
|
||||
client_ref_1 = fields.Char(
|
||||
string='Client Reference 1',
|
||||
help='First client reference number (e.g., PO number)',
|
||||
help='First two letters of the client\'s first name and last two letters of their last name. Example: John Doe = JODO',
|
||||
)
|
||||
client_ref_2 = fields.Char(
|
||||
string='Client Reference 2',
|
||||
help='Second client reference number',
|
||||
help='Last four digits of the client\'s health card number. Example: 1234',
|
||||
)
|
||||
|
||||
# Reason for Application
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<field name="claim_authorization_date"/>
|
||||
</group>
|
||||
<group string="Client References">
|
||||
<field name="client_ref_1" placeholder="e.g., DOJO"/>
|
||||
<field name="client_ref_1" placeholder="e.g., JODO"/>
|
||||
<field name="client_ref_2" placeholder="e.g., 1234"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
92
fusion_claims/wizard/send_page11_wizard.py
Normal file
92
fusion_claims/wizard/send_page11_wizard.py
Normal 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'},
|
||||
},
|
||||
}
|
||||
39
fusion_claims/wizard/send_page11_wizard_views.xml
Normal file
39
fusion_claims/wizard/send_page11_wizard_views.xml
Normal 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>
|
||||
@@ -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 & 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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user