Files
Odoo-Modules/fusion_claims/wizard/application_received_wizard.py
gsinghpal e3bec557b6 fix(claims): restore long intake_mode labels, give group full width
Reverts the label shortening and instead sets col=1 on the radio group
so the group's inner layout is a single column. With the full wizard
width available, the full labels fit on one line each.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:41:51 -04:00

305 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
import base64
import logging
from datetime import date
from markupsafe import Markup
from odoo import models, fields, api, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
try:
import pdfrw
except ImportError: # pragma: no cover
pdfrw = None
class ApplicationReceivedWizard(models.TransientModel):
"""Wizard to upload ADP application documents when application is received."""
_name = 'fusion_claims.application.received.wizard'
_description = 'Application Received Wizard'
sale_order_id = fields.Many2one(
'sale.order',
string='Sale Order',
required=True,
readonly=True,
)
intake_mode = fields.Selection(
selection=[
('bundled', 'Pages 11 & 12 are INCLUDED in the original application'),
('separate', 'Pages 11 & 12 are a SEPARATE file'),
('remote', 'Pages 11 & 12 will be SIGNED REMOTELY'),
],
string='Intake Mode',
required=True,
default='bundled',
help=(
'Bundled: a single PDF that already contains the signed pages 11 & 12.\n'
'Separate: original application + a separate PDF with the signed pages 11 & 12.\n'
'Remote: send Page 11 to a family member / agent for digital signing.'
),
)
# Document uploads
original_application = fields.Binary(
string='Original ADP Application',
required=True,
help='Upload the original ADP application PDF received from the client',
)
original_application_filename = fields.Char(string='Application Filename')
original_page_count = fields.Integer(
string='Original PDF Page Count',
compute='_compute_original_page_count',
help='Number of pages detected in the uploaded original PDF.',
)
signed_pages_11_12 = fields.Binary(
string='Signed Pages 11 & 12',
help='Upload the signed pages 11 and 12 from the application '
'(only used in Separate-file mode).',
)
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',
)
# ------------------------------------------------------------------
# COMPUTED
# ------------------------------------------------------------------
@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.depends('original_application')
def _compute_original_page_count(self):
for wiz in self:
wiz.original_page_count = wiz._count_pdf_pages(wiz.original_application)
@staticmethod
def _count_pdf_pages(b64_data):
"""Return PDF page count, or 0 if unknown/unparseable."""
if not b64_data or pdfrw is None:
return 0
try:
raw = base64.b64decode(b64_data)
reader = pdfrw.PdfReader(fdata=raw)
return len(reader.pages) if reader and reader.pages else 0
except Exception: # pragma: no cover (corrupted PDFs)
return 0
# ------------------------------------------------------------------
# DEFAULTS
# ------------------------------------------------------------------
@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
if order.x_fc_original_application:
res['original_application'] = order.x_fc_original_application
res['original_application_filename'] = order.x_fc_original_application_filename
if order.x_fc_signed_pages_11_12:
res['signed_pages_11_12'] = order.x_fc_signed_pages_11_12
res['signed_pages_filename'] = order.x_fc_signed_pages_filename
# Choose initial intake mode based on order state.
if order.x_fc_pages_11_12_in_original:
res['intake_mode'] = 'bundled'
elif order.x_fc_signed_pages_11_12:
res['intake_mode'] = 'separate'
elif order.page11_sign_request_ids.filtered(
lambda r: r.state in ('sent', 'signed')
):
res['intake_mode'] = 'remote'
else:
res['intake_mode'] = 'bundled'
return res
# ------------------------------------------------------------------
# CONSTRAINTS (filename defence-in-depth)
# ------------------------------------------------------------------
@api.constrains('original_application_filename')
def _check_application_file_type(self):
for wizard in self:
name = wizard.original_application_filename
if name and not name.lower().endswith('.pdf'):
raise UserError(
f"Original Application must be a PDF file.\n"
f"Uploaded file: '{name}'"
)
@api.constrains('signed_pages_filename')
def _check_pages_file_type(self):
for wizard in self:
name = wizard.signed_pages_filename
if name and not name.lower().endswith('.pdf'):
raise UserError(
f"Signed Pages 11 & 12 must be a PDF file.\n"
f"Uploaded file: '{name}'"
)
# ------------------------------------------------------------------
# ACTIONS
# ------------------------------------------------------------------
def action_confirm(self):
"""Save documents and mark application as received."""
self.ensure_one()
order = self.sale_order_id
if order.x_fc_adp_application_status not in (
'assessment_completed', 'waiting_for_application',
):
raise UserError(
"Can only mark application received from 'Assessment Completed' "
"or 'Waiting for Application' status."
)
if not self.original_application:
raise UserError("Please upload the Original ADP Application.")
self._validate_pdf_bytes(self.original_application, 'Original ADP Application')
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_pages_11_12_in_original': (self.intake_mode == 'bundled'),
}
if self.intake_mode == 'separate':
if not (self.signed_pages_11_12 or order.x_fc_signed_pages_11_12):
raise UserError(
"Signed Pages 11 & 12 file is required when "
"'Separate file' mode is selected."
)
if self.signed_pages_11_12:
self._validate_pdf_bytes(
self.signed_pages_11_12, '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
elif self.intake_mode == 'remote':
has_request = order.page11_sign_request_ids.filtered(
lambda r: r.state in ('sent', 'signed')
)
if not has_request:
raise UserError(
"No remote-signing request found. Click "
"'Request Remote Signature' first, or pick a different mode."
)
order.with_context(skip_status_validation=True).write(vals)
self._post_chatter(order)
return {'type': 'ir.actions.act_window_close'}
def action_request_page11_signature(self):
"""Open the Page 11 remote signing wizard from within this 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},
}
# ------------------------------------------------------------------
# HELPERS
# ------------------------------------------------------------------
@staticmethod
def _validate_pdf_bytes(b64_data, label):
"""Raise UserError if the uploaded binary is not a real PDF."""
if not b64_data:
return
try:
head = base64.b64decode(b64_data)[:5]
except Exception:
raise UserError(f"{label}: could not decode uploaded file.")
if head != b'%PDF-':
raise UserError(
f"{label} must be a PDF file "
f"(content check failed — the file does not start with %PDF-)."
)
def _post_chatter(self, order):
"""Post a mode-aware Application Received message to the chatter."""
self.ensure_one()
mode = self.intake_mode
if mode == 'bundled':
headline = 'Application Received — bundled'
detail = 'Pages 11 & 12 included in original PDF'
elif mode == 'separate':
headline = 'Application Received — separate files'
detail = 'Original + separate signed pages uploaded'
else: # remote
n = len(order.page11_sign_request_ids.filtered(
lambda r: r.state in ('sent', 'signed')
))
headline = 'Application Received — remote signature pending'
detail = f'Page 11 sent for remote signature ({n} request(s) outstanding)'
notes_html = (
f'<p style="margin: 4px 0 0 0;"><strong>Notes:</strong> {self.notes}</p>'
if self.notes else ''
)
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-file-text-o"/> {headline}</h4>'
'<p style="margin:0;"><strong>Date:</strong> {today}</p>'
'<p style="margin:8px 0 4px 0;">{detail}</p>'
'<p style="margin:0;color:#666;">'
'Original: {orig_name}</p>'
'{notes}'
'</div>'
).format(
headline=headline,
today=date.today().strftime('%B %d, %Y'),
detail=detail,
orig_name=self.original_application_filename or '(no filename)',
notes=notes_html,
)
order.message_post(
body=body,
message_type='notification',
subtype_xmlid='mail.mt_note',
)