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:
389
fusion_claims/models/page11_sign_request.py
Normal file
389
fusion_claims/models/page11_sign_request.py
Normal file
@@ -0,0 +1,389 @@
|
||||
# -*- 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 = '<br/><br/>'.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 <strong>{client_name}</strong>.'
|
||||
),
|
||||
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 <strong>%s</strong> (%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 <strong>%s</strong> (%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 <strong>%s</strong> (%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))
|
||||
Reference in New Issue
Block a user