# -*- coding: utf-8 -*- # Copyright 2024-2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) import base64 import logging import uuid from datetime import timedelta from markupsafe import Markup from odoo import models, fields, api, _ from odoo.exceptions import UserError _logger = logging.getLogger(__name__) SIGNER_TYPE_SELECTION = [ ('client', 'Client (Self)'), ('spouse', 'Spouse'), ('parent', 'Parent'), ('legal_guardian', 'Legal Guardian'), ('poa', 'Power of Attorney'), ('public_trustee', 'Public Trustee'), ] SIGNER_TYPE_TO_RELATIONSHIP = { 'spouse': 'Spouse', 'parent': 'Parent', 'legal_guardian': 'Legal Guardian', 'poa': 'Power of Attorney', 'public_trustee': 'Public Trustee', } class Page11SignRequest(models.Model): _name = 'fusion.page11.sign.request' _description = 'ADP Page 11 Remote Signing Request' _inherit = ['fusion.email.builder.mixin'] _order = 'create_date desc' sale_order_id = fields.Many2one( 'sale.order', string='Sale Order', required=True, ondelete='cascade', index=True, ) access_token = fields.Char( string='Access Token', required=True, copy=False, default=lambda self: str(uuid.uuid4()), index=True, ) state = fields.Selection([ ('draft', 'Draft'), ('sent', 'Sent'), ('signed', 'Signed'), ('expired', 'Expired'), ('cancelled', 'Cancelled'), ], string='Status', default='draft', required=True, tracking=True) signer_email = fields.Char(string='Recipient Email', required=True) signer_type = fields.Selection( SIGNER_TYPE_SELECTION, string='Signer Type', default='client', required=True, ) signer_name = fields.Char(string='Signer Name') signer_relationship = fields.Char(string='Relationship to Client') signature_data = fields.Binary(string='Signature', attachment=True) signed_pdf = fields.Binary(string='Signed PDF', attachment=True) signed_pdf_filename = fields.Char(string='Signed PDF Filename') signed_date = fields.Datetime(string='Signed Date') sent_date = fields.Datetime(string='Sent Date') expiry_date = fields.Datetime(string='Expiry Date') consent_declaration_accepted = fields.Boolean(string='Declaration Accepted') consent_signed_by = fields.Selection([ ('applicant', 'Applicant'), ('agent', 'Agent'), ], string='Signed By') client_first_name = fields.Char(string='Client First Name') client_last_name = fields.Char(string='Client Last Name') client_health_card = fields.Char(string='Health Card Number') client_health_card_version = fields.Char(string='Health Card Version') agent_first_name = fields.Char(string='Agent First Name') agent_last_name = fields.Char(string='Agent Last Name') agent_middle_initial = fields.Char(string='Agent Middle Initial') agent_phone = fields.Char(string='Agent Phone') agent_unit = fields.Char(string='Agent Unit Number') agent_street_number = fields.Char(string='Agent Street Number') agent_street = fields.Char(string='Agent Street Name') agent_city = fields.Char(string='Agent City') agent_province = fields.Char(string='Agent Province', default='Ontario') agent_postal_code = fields.Char(string='Agent Postal Code') custom_message = fields.Text(string='Custom Message') company_id = fields.Many2one( 'res.company', string='Company', related='sale_order_id.company_id', store=True, ) def name_get(self): return [ (r.id, f"Page 11 - {r.sale_order_id.name} ({r.state})") for r in self ] def _send_signing_email(self): """Build and send the signing request email.""" self.ensure_one() base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') sign_url = f'{base_url}/page11/sign/{self.access_token}' order = self.sale_order_id client_name = order.partner_id.name or 'N/A' sections = [ ('Case Details', [ ('Client', client_name), ('Case Reference', order.name), ]), ] if order.x_fc_authorizer_id: sections[0][1].append(('Authorizer', order.x_fc_authorizer_id.name)) if order.x_fc_assessment_start_date: sections[0][1].append(( 'Assessment Date', order.x_fc_assessment_start_date.strftime('%B %d, %Y'), )) note_parts = [] if self.custom_message: note_parts.append(self.custom_message) days_left = 7 if self.expiry_date: delta = self.expiry_date - fields.Datetime.now() days_left = max(1, delta.days) note_parts.append( f'This link will expire in {days_left} days. ' 'Please complete the signing at your earliest convenience.' ) note_text = '

'.join(note_parts) body_html = self._email_build( title='Page 11 Signature Required', summary=( f'{order.company_id.name} requires your signature on the ' f'ADP Consent and Declaration form for {client_name}.' ), sections=sections, note=note_text, email_type='info', button_url=sign_url, button_text='Sign Now', sender_name=self.env.user.name, ) mail_values = { 'subject': f'{order.company_id.name} - Page 11 Signature Required ({order.name})', 'body_html': body_html, 'email_to': self.signer_email, 'email_from': ( self.env.user.email_formatted or order.company_id.email_formatted ), 'auto_delete': True, } mail = self.env['mail.mail'].sudo().create(mail_values) mail.send() self.write({ 'state': 'sent', 'sent_date': fields.Datetime.now(), }) signer_display = self.signer_name or self.signer_email order.message_post( body=Markup( 'Page 11 signing request sent to %s (%s).' ) % (signer_display, self.signer_email), message_type='notification', subtype_xmlid='mail.mt_note', ) def _generate_signed_pdf(self): """Generate the signed Page 11 PDF using the PDF template engine.""" self.ensure_one() order = self.sale_order_id assessment = self.env['fusion.assessment'].search([ ('sale_order_id', '=', order.id), ], limit=1, order='create_date desc') if assessment: ctx = assessment._get_pdf_context() else: ctx = self._build_pdf_context_from_order() if self.client_first_name: ctx['client_first_name'] = self.client_first_name if self.client_last_name: ctx['client_last_name'] = self.client_last_name if self.client_health_card: ctx['client_health_card'] = self.client_health_card if self.client_health_card_version: ctx['client_health_card_version'] = self.client_health_card_version ctx.update({ 'consent_signed_by': self.consent_signed_by or '', 'consent_applicant': self.consent_signed_by == 'applicant', 'consent_agent': self.consent_signed_by == 'agent', 'consent_declaration_accepted': self.consent_declaration_accepted, 'consent_date': str(fields.Date.today()), }) if self.consent_signed_by == 'agent': ctx.update({ 'agent_first_name': self.agent_first_name or '', 'agent_last_name': self.agent_last_name or '', 'agent_middle_initial': self.agent_middle_initial or '', 'agent_unit': self.agent_unit or '', 'agent_street_number': self.agent_street_number or '', 'agent_street_name': self.agent_street or '', 'agent_city': self.agent_city or '', 'agent_province': self.agent_province or '', 'agent_postal_code': self.agent_postal_code or '', 'agent_home_phone': self.agent_phone or '', 'agent_relationship': self.signer_relationship or '', 'agent_rel_spouse': self.signer_type == 'spouse', 'agent_rel_parent': self.signer_type == 'parent', 'agent_rel_poa': self.signer_type == 'poa', 'agent_rel_guardian': self.signer_type in ('legal_guardian', 'public_trustee'), }) signatures = {} if self.signature_data: signatures['signature_page_11'] = base64.b64decode(self.signature_data) template = self.env['fusion.pdf.template'].search([ ('state', '=', 'active'), ('name', 'ilike', 'adp_page_11'), ], limit=1) if not template: template = self.env['fusion.pdf.template'].search([ ('state', '=', 'active'), ('name', 'ilike', 'page 11'), ], limit=1) if not template: _logger.warning("No active PDF template found for Page 11") return None try: pdf_bytes = template.generate_filled_pdf(ctx, signatures) if pdf_bytes: first, last = order._get_client_name_parts() filename = f'{first}_{last}_Page11_Signed.pdf' self.write({ 'signed_pdf': base64.b64encode(pdf_bytes), 'signed_pdf_filename': filename, }) return pdf_bytes except Exception as e: _logger.error("Failed to generate Page 11 PDF: %s", e) return None def _build_pdf_context_from_order(self): """Build a PDF context dict from the sale order when no assessment exists.""" order = self.sale_order_id partner = order.partner_id first, last = order._get_client_name_parts() return { 'client_first_name': first, 'client_last_name': last, 'client_name': partner.name or '', 'client_street': partner.street or '', 'client_city': partner.city or '', 'client_state': partner.state_id.name if partner.state_id else 'Ontario', 'client_postal_code': partner.zip or '', 'client_phone': partner.phone or partner.mobile or '', 'client_email': partner.email or '', 'client_type': order.x_fc_client_type or '', 'client_type_reg': order.x_fc_client_type == 'REG', 'client_type_ods': order.x_fc_client_type == 'ODS', 'client_type_acs': order.x_fc_client_type == 'ACS', 'client_type_owp': order.x_fc_client_type == 'OWP', 'reference': order.name or '', 'authorizer_name': order.x_fc_authorizer_id.name if order.x_fc_authorizer_id else '', 'authorizer_phone': order.x_fc_authorizer_id.phone if order.x_fc_authorizer_id else '', 'authorizer_email': order.x_fc_authorizer_id.email if order.x_fc_authorizer_id else '', 'claim_authorization_date': str(order.x_fc_claim_authorization_date) if order.x_fc_claim_authorization_date else '', 'assessment_start_date': str(order.x_fc_assessment_start_date) if order.x_fc_assessment_start_date else '', 'assessment_end_date': str(order.x_fc_assessment_end_date) if order.x_fc_assessment_end_date else '', } def _update_sale_order(self): """Copy signing data from this request to the sale order.""" self.ensure_one() order = self.sale_order_id vals = { 'x_fc_page11_signer_type': self.signer_type, 'x_fc_page11_signer_name': self.signer_name, 'x_fc_page11_signed_date': fields.Date.today(), } if self.signer_type != 'client': vals['x_fc_page11_signer_relationship'] = ( self.signer_relationship or SIGNER_TYPE_TO_RELATIONSHIP.get(self.signer_type, '') ) if self.signed_pdf: vals['x_fc_signed_pages_11_12'] = self.signed_pdf vals['x_fc_signed_pages_filename'] = self.signed_pdf_filename order.with_context( skip_page11_check=True, skip_document_chatter=True, ).write(vals) signer_display = self.signer_name or 'N/A' if self.signed_pdf: att = self.env['ir.attachment'].sudo().create({ 'name': self.signed_pdf_filename or 'Page11_Signed.pdf', 'datas': self.signed_pdf, 'res_model': 'sale.order', 'res_id': order.id, 'mimetype': 'application/pdf', }) order.message_post( body=Markup( 'Page 11 has been signed by %s (%s).' ) % (signer_display, self.signer_email), attachment_ids=[att.id], message_type='notification', subtype_xmlid='mail.mt_note', ) else: order.message_post( body=Markup( 'Page 11 has been signed by %s (%s). ' 'PDF generation was not available.' ) % (signer_display, self.signer_email), message_type='notification', subtype_xmlid='mail.mt_note', ) def action_cancel(self): """Cancel a pending signing request.""" for rec in self: if rec.state in ('draft', 'sent'): rec.state = 'cancelled' def action_resend(self): """Resend the signing email.""" for rec in self: if rec.state in ('sent', 'expired'): rec.expiry_date = fields.Datetime.now() + timedelta(days=7) rec.access_token = str(uuid.uuid4()) rec._send_signing_email() def action_request_new_signature(self): """Create a new signing request (e.g. to re-sign after corrections).""" self.ensure_one() if self.state == 'signed': self.state = 'cancelled' 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, 'default_signer_email': self.signer_email, 'default_signer_name': self.signer_name, 'default_signer_type': self.signer_type, }, } @api.model def _cron_expire_requests(self): """Mark expired unsigned requests.""" expired = self.search([ ('state', '=', 'sent'), ('expiry_date', '<', fields.Datetime.now()), ]) if expired: expired.write({'state': 'expired'}) _logger.info("Expired %d Page 11 signing requests", len(expired))