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:
2026-03-11 16:19:52 +00:00
parent 1f79cdcaaf
commit 431052920e
274 changed files with 52782 additions and 7302 deletions

View 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))