# -*- 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'
Notes: {self.notes}
' if self.notes else '' ) body = Markup( 'Date: {today}
' '{detail}
' '' 'Original: {orig_name}
' '{notes}' '