refactor(fusion_portal): rename from fusion_authorizer_portal + modern photo cards on accessibility selector
Rename module fusion_authorizer_portal -> fusion_portal everywhere: manifest/assets, controllers, models, views, JS (odoo.define + asset URLs), migration MODULE constants; plus cross-module refs in fusion_schedule, fusion_repairs, fusion_quotations (depends + inherit_id) and the pdf_filler import in fusion_claims. Add rename_module.sql for the one-time in-place DB rename (ir_module_module, ir_model_data, ir_ui_view.key, ir_module_module_dependency) required on installed envs before -u fusion_portal. Document the rename gotcha as rule 16 in CLAUDE.md. Redesign the Accessibility Assessment selector: replace Font Awesome icon tiles with photo-banner cards using 7 optimized images (1000x750 PNG -> 800x600 JPEG, ~8MB -> 488KB), per-type colour accent bar + centered pill button, hover lift/zoom. Images ship as module static files so they deploy/sync with the module. Drop the regenerable graphify-out cache from the module. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
6
fusion_portal/controllers/__init__.py
Normal file
6
fusion_portal/controllers/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import portal_main
|
||||
from . import portal_assessment
|
||||
from . import pdf_editor
|
||||
from . import portal_page11_sign
|
||||
218
fusion_portal/controllers/pdf_editor.py
Normal file
218
fusion_portal/controllers/pdf_editor.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Fusion PDF Field Editor Controller
|
||||
# Provides routes for the visual drag-and-drop field position editor
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionPdfEditorController(http.Controller):
|
||||
"""Controller for the PDF field position visual editor."""
|
||||
|
||||
# ================================================================
|
||||
# Editor Page
|
||||
# ================================================================
|
||||
|
||||
@http.route('/fusion/pdf-editor/<int:template_id>', type='http', auth='user', website=True)
|
||||
def pdf_field_editor(self, template_id, **kw):
|
||||
"""Render the visual field editor for a PDF template."""
|
||||
template = request.env['fusion.pdf.template'].browse(template_id)
|
||||
if not template.exists():
|
||||
return request.redirect('/web')
|
||||
|
||||
# Get preview image for page 1
|
||||
preview_url = ''
|
||||
preview = template.preview_ids.filtered(lambda p: p.page == 1)
|
||||
if preview and preview[0].image:
|
||||
preview_url = f'/web/image/fusion.pdf.template.preview/{preview[0].id}/image'
|
||||
|
||||
fields = template.field_ids.read([
|
||||
'name', 'label', 'page', 'pos_x', 'pos_y', 'width', 'height',
|
||||
'field_type', 'font_size', 'font_name', 'field_key', 'default_value', 'is_active',
|
||||
'text_align',
|
||||
])
|
||||
|
||||
return request.render('fusion_portal.portal_pdf_field_editor', {
|
||||
'template': template,
|
||||
'fields': fields,
|
||||
'preview_url': preview_url,
|
||||
})
|
||||
|
||||
# ================================================================
|
||||
# JSONRPC: Get fields for template
|
||||
# ================================================================
|
||||
|
||||
@http.route('/fusion/pdf-editor/fields', type='json', auth='user')
|
||||
def get_fields(self, template_id, **kw):
|
||||
"""Return all fields for a template."""
|
||||
template = request.env['fusion.pdf.template'].browse(template_id)
|
||||
if not template.exists():
|
||||
return []
|
||||
return template.field_ids.read([
|
||||
'name', 'label', 'page', 'pos_x', 'pos_y', 'width', 'height',
|
||||
'field_type', 'font_size', 'font_name', 'field_key', 'default_value', 'is_active',
|
||||
'text_align',
|
||||
])
|
||||
|
||||
# ================================================================
|
||||
# JSONRPC: Update field position/properties
|
||||
# ================================================================
|
||||
|
||||
@http.route('/fusion/pdf-editor/update-field', type='json', auth='user')
|
||||
def update_field(self, field_id, values, **kw):
|
||||
"""Update a field's position or properties."""
|
||||
field = request.env['fusion.pdf.template.field'].browse(field_id)
|
||||
if not field.exists():
|
||||
return {'error': 'Field not found'}
|
||||
|
||||
# Filter to allowed fields only
|
||||
allowed = {
|
||||
'name', 'label', 'page', 'pos_x', 'pos_y', 'width', 'height',
|
||||
'field_type', 'font_size', 'font_name', 'field_key', 'default_value', 'is_active',
|
||||
'text_align',
|
||||
}
|
||||
safe_values = {k: v for k, v in values.items() if k in allowed}
|
||||
if safe_values:
|
||||
field.write(safe_values)
|
||||
return {'success': True}
|
||||
|
||||
# ================================================================
|
||||
# JSONRPC: Create new field
|
||||
# ================================================================
|
||||
|
||||
@http.route('/fusion/pdf-editor/create-field', type='json', auth='user')
|
||||
def create_field(self, **kw):
|
||||
"""Create a new field on a template."""
|
||||
template_id = kw.get('template_id')
|
||||
if not template_id:
|
||||
return {'error': 'Missing template_id'}
|
||||
|
||||
vals = {
|
||||
'template_id': int(template_id),
|
||||
'name': kw.get('name', 'new_field'),
|
||||
'label': kw.get('label', 'New Field'),
|
||||
'field_type': kw.get('field_type', 'text'),
|
||||
'field_key': kw.get('field_key', kw.get('name', '')),
|
||||
'page': int(kw.get('page', 1)),
|
||||
'pos_x': float(kw.get('pos_x', 0.3)),
|
||||
'pos_y': float(kw.get('pos_y', 0.3)),
|
||||
'width': float(kw.get('width', 0.150)),
|
||||
'height': float(kw.get('height', 0.015)),
|
||||
'font_size': float(kw.get('font_size', 10)),
|
||||
}
|
||||
|
||||
field = request.env['fusion.pdf.template.field'].create(vals)
|
||||
return {'id': field.id, 'success': True}
|
||||
|
||||
# ================================================================
|
||||
# JSONRPC: Delete field
|
||||
# ================================================================
|
||||
|
||||
@http.route('/fusion/pdf-editor/delete-field', type='json', auth='user')
|
||||
def delete_field(self, field_id, **kw):
|
||||
"""Delete a field from a template."""
|
||||
field = request.env['fusion.pdf.template.field'].browse(field_id)
|
||||
if field.exists():
|
||||
field.unlink()
|
||||
return {'success': True}
|
||||
|
||||
# ================================================================
|
||||
# JSONRPC: Get page preview image URL
|
||||
# ================================================================
|
||||
|
||||
@http.route('/fusion/pdf-editor/page-image', type='json', auth='user')
|
||||
def get_page_image(self, template_id, page, **kw):
|
||||
"""Return the preview image URL for a specific page."""
|
||||
template = request.env['fusion.pdf.template'].browse(template_id)
|
||||
if not template.exists():
|
||||
return {'image_url': ''}
|
||||
|
||||
preview = template.preview_ids.filtered(lambda p: p.page == page)
|
||||
if preview and preview[0].image:
|
||||
return {'image_url': f'/web/image/fusion.pdf.template.preview/{preview[0].id}/image'}
|
||||
return {'image_url': ''}
|
||||
|
||||
# ================================================================
|
||||
# Upload page preview image (from editor)
|
||||
# ================================================================
|
||||
|
||||
@http.route('/fusion/pdf-editor/upload-preview', type='http', auth='user',
|
||||
methods=['POST'], csrf=True, website=True)
|
||||
def upload_preview_image(self, **kw):
|
||||
"""Upload a preview image for a template page directly from the editor."""
|
||||
template_id = int(kw.get('template_id', 0))
|
||||
page = int(kw.get('page', 1))
|
||||
template = request.env['fusion.pdf.template'].browse(template_id)
|
||||
if not template.exists():
|
||||
return json.dumps({'error': 'Template not found'})
|
||||
|
||||
image_file = request.httprequest.files.get('preview_image')
|
||||
if not image_file:
|
||||
return json.dumps({'error': 'No image uploaded'})
|
||||
|
||||
image_data = base64.b64encode(image_file.read())
|
||||
|
||||
# Find or create preview for this page
|
||||
preview = template.preview_ids.filtered(lambda p: p.page == page)
|
||||
if preview:
|
||||
preview[0].write({'image': image_data, 'image_filename': image_file.filename})
|
||||
else:
|
||||
request.env['fusion.pdf.template.preview'].create({
|
||||
'template_id': template_id,
|
||||
'page': page,
|
||||
'image': image_data,
|
||||
'image_filename': image_file.filename,
|
||||
})
|
||||
|
||||
_logger.info("Uploaded preview image for template %s page %d", template.name, page)
|
||||
return request.redirect(f'/fusion/pdf-editor/{template_id}')
|
||||
|
||||
# ================================================================
|
||||
# Preview: Generate sample filled PDF
|
||||
# ================================================================
|
||||
|
||||
@http.route('/fusion/pdf-editor/preview/<int:template_id>', type='http', auth='user')
|
||||
def preview_pdf(self, template_id, **kw):
|
||||
"""Generate a preview filled PDF with sample data."""
|
||||
template = request.env['fusion.pdf.template'].browse(template_id)
|
||||
if not template.exists() or not template.pdf_file:
|
||||
return request.redirect('/web')
|
||||
|
||||
# Build sample data for preview
|
||||
sample_context = {
|
||||
'client_last_name': 'Smith',
|
||||
'client_first_name': 'John',
|
||||
'client_middle_name': 'A',
|
||||
'client_health_card': '1234-567-890',
|
||||
'client_health_card_version': 'AB',
|
||||
'client_street': '123 Main Street',
|
||||
'client_unit': 'Unit 4B',
|
||||
'client_city': 'Toronto',
|
||||
'client_state': 'Ontario',
|
||||
'client_postal_code': 'M5V 2T6',
|
||||
'client_phone': '(416) 555-0123',
|
||||
'client_email': 'john.smith@example.com',
|
||||
'client_weight': '185',
|
||||
'consent_applicant': True,
|
||||
'consent_agent': False,
|
||||
'consent_date': '2026-02-08',
|
||||
'agent_last_name': '',
|
||||
'agent_first_name': '',
|
||||
}
|
||||
|
||||
try:
|
||||
pdf_bytes = template.generate_filled_pdf(sample_context)
|
||||
headers = [
|
||||
('Content-Type', 'application/pdf'),
|
||||
('Content-Disposition', f'inline; filename="preview_{template.name}.pdf"'),
|
||||
]
|
||||
return request.make_response(pdf_bytes, headers=headers)
|
||||
except Exception as e:
|
||||
_logger.error("PDF preview generation failed: %s", e)
|
||||
return request.redirect(f'/fusion/pdf-editor/{template_id}?error=preview_failed')
|
||||
1238
fusion_portal/controllers/portal_assessment.py
Normal file
1238
fusion_portal/controllers/portal_assessment.py
Normal file
File diff suppressed because it is too large
Load Diff
2827
fusion_portal/controllers/portal_main.py
Normal file
2827
fusion_portal/controllers/portal_main.py
Normal file
File diff suppressed because it is too large
Load Diff
206
fusion_portal/controllers/portal_page11_sign.py
Normal file
206
fusion_portal/controllers/portal_page11_sign.py
Normal file
@@ -0,0 +1,206 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import http, fields, _
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Page11PublicSignController(http.Controller):
|
||||
|
||||
def _get_sign_request(self, token):
|
||||
"""Look up and validate a signing request by token."""
|
||||
req = request.env['fusion.page11.sign.request'].sudo().search([
|
||||
('access_token', '=', token),
|
||||
], limit=1)
|
||||
if not req:
|
||||
return None, 'not_found'
|
||||
if req.state == 'signed':
|
||||
return req, 'already_signed'
|
||||
if req.state == 'cancelled':
|
||||
return req, 'cancelled'
|
||||
if req.state == 'expired' or (
|
||||
req.expiry_date and req.expiry_date < fields.Datetime.now()
|
||||
):
|
||||
if req.state != 'expired':
|
||||
req.state = 'expired'
|
||||
return req, 'expired'
|
||||
return req, 'ok'
|
||||
|
||||
@http.route('/page11/sign/<string:token>', type='http', auth='public',
|
||||
website=True, sitemap=False)
|
||||
def page11_sign_form(self, token, **kw):
|
||||
"""Display the Page 11 signing form."""
|
||||
sign_req, status = self._get_sign_request(token)
|
||||
|
||||
if status == 'not_found':
|
||||
return request.render(
|
||||
'fusion_portal.portal_page11_sign_invalid', {}
|
||||
)
|
||||
|
||||
if status in ('expired', 'cancelled'):
|
||||
return request.render(
|
||||
'fusion_portal.portal_page11_sign_expired',
|
||||
{'sign_request': sign_req},
|
||||
)
|
||||
|
||||
if status == 'already_signed':
|
||||
return request.render(
|
||||
'fusion_portal.portal_page11_sign_success',
|
||||
{'sign_request': sign_req, 'token': token},
|
||||
)
|
||||
|
||||
order = sign_req.sale_order_id
|
||||
partner = order.partner_id
|
||||
|
||||
assessment = request.env['fusion.assessment'].sudo().search([
|
||||
('sale_order_id', '=', order.id),
|
||||
], limit=1, order='create_date desc')
|
||||
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
google_maps_api_key = ICP.get_param('fusion_claims.google_maps_api_key', '')
|
||||
|
||||
client_first_name = ''
|
||||
client_last_name = ''
|
||||
client_middle_name = ''
|
||||
client_health_card = ''
|
||||
client_health_card_version = ''
|
||||
|
||||
if assessment:
|
||||
client_first_name = assessment.client_first_name or ''
|
||||
client_last_name = assessment.client_last_name or ''
|
||||
client_middle_name = assessment.client_middle_name or ''
|
||||
client_health_card = assessment.client_health_card or ''
|
||||
client_health_card_version = assessment.client_health_card_version or ''
|
||||
else:
|
||||
first, last = order._get_client_name_parts()
|
||||
client_first_name = first
|
||||
client_last_name = last
|
||||
|
||||
values = {
|
||||
'sign_request': sign_req,
|
||||
'order': order,
|
||||
'partner': partner,
|
||||
'assessment': assessment,
|
||||
'company': order.company_id,
|
||||
'token': token,
|
||||
'signer_type': sign_req.signer_type,
|
||||
'is_agent': sign_req.signer_type != 'client',
|
||||
'google_maps_api_key': google_maps_api_key,
|
||||
'client_first_name': client_first_name,
|
||||
'client_last_name': client_last_name,
|
||||
'client_middle_name': client_middle_name,
|
||||
'client_health_card': client_health_card,
|
||||
'client_health_card_version': client_health_card_version,
|
||||
}
|
||||
return request.render(
|
||||
'fusion_portal.portal_page11_public_sign', values,
|
||||
)
|
||||
|
||||
@http.route('/page11/sign/<string:token>/submit', type='http',
|
||||
auth='public', methods=['POST'], website=True,
|
||||
csrf=True, sitemap=False)
|
||||
def page11_sign_submit(self, token, **post):
|
||||
"""Process the submitted Page 11 signature."""
|
||||
sign_req, status = self._get_sign_request(token)
|
||||
|
||||
if status != 'ok':
|
||||
return request.redirect(f'/page11/sign/{token}')
|
||||
|
||||
signature_data = post.get('signature_data', '')
|
||||
if not signature_data:
|
||||
return request.redirect(f'/page11/sign/{token}?error=no_signature')
|
||||
|
||||
if signature_data.startswith('data:image'):
|
||||
signature_data = signature_data.split(',', 1)[1]
|
||||
|
||||
consent_accepted = post.get('consent_declaration', '') == 'on'
|
||||
if not consent_accepted:
|
||||
return request.redirect(f'/page11/sign/{token}?error=no_consent')
|
||||
|
||||
signer_name = post.get('signer_name', sign_req.signer_name or '')
|
||||
chosen_signer_type = post.get('signer_type', sign_req.signer_type or 'client')
|
||||
consent_signed_by = 'applicant' if chosen_signer_type == 'client' else 'agent'
|
||||
|
||||
signer_type_labels = {
|
||||
'spouse': 'Spouse', 'parent': 'Parent',
|
||||
'legal_guardian': 'Legal Guardian',
|
||||
'poa': 'Power of Attorney',
|
||||
'public_trustee': 'Public Trustee',
|
||||
}
|
||||
|
||||
vals = {
|
||||
'signature_data': signature_data,
|
||||
'signer_name': signer_name,
|
||||
'signer_type': chosen_signer_type,
|
||||
'consent_declaration_accepted': True,
|
||||
'consent_signed_by': consent_signed_by,
|
||||
'signed_date': fields.Datetime.now(),
|
||||
'state': 'signed',
|
||||
'client_first_name': post.get('client_first_name', ''),
|
||||
'client_last_name': post.get('client_last_name', ''),
|
||||
'client_health_card': post.get('client_health_card', ''),
|
||||
'client_health_card_version': post.get('client_health_card_version', ''),
|
||||
}
|
||||
|
||||
if consent_signed_by == 'agent':
|
||||
vals.update({
|
||||
'agent_first_name': post.get('agent_first_name', ''),
|
||||
'agent_last_name': post.get('agent_last_name', ''),
|
||||
'agent_middle_initial': post.get('agent_middle_initial', ''),
|
||||
'agent_phone': post.get('agent_phone', ''),
|
||||
'agent_unit': post.get('agent_unit', ''),
|
||||
'agent_street_number': post.get('agent_street_number', ''),
|
||||
'agent_street': post.get('agent_street', ''),
|
||||
'agent_city': post.get('agent_city', ''),
|
||||
'agent_province': post.get('agent_province', 'Ontario'),
|
||||
'agent_postal_code': post.get('agent_postal_code', ''),
|
||||
'signer_relationship': signer_type_labels.get(chosen_signer_type, chosen_signer_type),
|
||||
})
|
||||
|
||||
sign_req.sudo().write(vals)
|
||||
|
||||
try:
|
||||
sign_req.sudo()._generate_signed_pdf()
|
||||
except Exception as e:
|
||||
_logger.error("PDF generation failed for sign request %s: %s", sign_req.id, e)
|
||||
|
||||
try:
|
||||
sign_req.sudo()._update_sale_order()
|
||||
except Exception as e:
|
||||
_logger.error("Sale order update failed for sign request %s: %s", sign_req.id, e)
|
||||
|
||||
return request.render(
|
||||
'fusion_portal.portal_page11_sign_success',
|
||||
{'sign_request': sign_req, 'token': token},
|
||||
)
|
||||
|
||||
@http.route('/page11/sign/<string:token>/download', type='http',
|
||||
auth='public', website=True, sitemap=False)
|
||||
def page11_download_pdf(self, token, **kw):
|
||||
"""Download the signed Page 11 PDF."""
|
||||
sign_req = request.env['fusion.page11.sign.request'].sudo().search([
|
||||
('access_token', '=', token),
|
||||
('state', '=', 'signed'),
|
||||
], limit=1)
|
||||
|
||||
if not sign_req or not sign_req.signed_pdf:
|
||||
return request.redirect(f'/page11/sign/{token}')
|
||||
|
||||
pdf_content = base64.b64decode(sign_req.signed_pdf)
|
||||
filename = sign_req.signed_pdf_filename or 'Page11_Signed.pdf'
|
||||
|
||||
return request.make_response(
|
||||
pdf_content,
|
||||
headers=[
|
||||
('Content-Type', 'application/pdf'),
|
||||
('Content-Disposition', f'attachment; filename="{filename}"'),
|
||||
('Content-Length', str(len(pdf_content))),
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user