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:
3
fusion_portal/utils/__init__.py
Normal file
3
fusion_portal/utils/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import pdf_filler
|
||||
168
fusion_portal/utils/pdf_filler.py
Normal file
168
fusion_portal/utils/pdf_filler.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Fusion PDF Template Filler
|
||||
# Generic utility for filling any PDF template with data overlays.
|
||||
# Uses the same pattern as Odoo Enterprise Sign module (sign/utils/pdf_handling.py):
|
||||
# - Read original PDF page dimensions from mediaBox
|
||||
# - Create reportlab Canvas overlay at the same page size
|
||||
# - Convert percentage positions (0.0-1.0) to absolute PDF coordinates
|
||||
# - Merge overlay onto original page via mergePage()
|
||||
|
||||
import logging
|
||||
from io import BytesIO
|
||||
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.utils import ImageReader
|
||||
|
||||
from odoo.tools.pdf import PdfFileReader, PdfFileWriter
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PDFTemplateFiller:
|
||||
"""Generic PDF template filler. Works with any template, any number of pages."""
|
||||
|
||||
@staticmethod
|
||||
def fill_template(template_pdf_bytes, fields_by_page, context, signatures=None):
|
||||
"""Fill a PDF template by overlaying text/checkmarks/signatures at configured positions.
|
||||
|
||||
Args:
|
||||
template_pdf_bytes: bytes of the original PDF
|
||||
fields_by_page: {page_num: [field_dicts]} where page_num is 1-based
|
||||
Each field_dict has: field_key, pos_x, pos_y, width, height,
|
||||
field_type, font_size, font_name
|
||||
context: flat dict of {field_key: value} with all available data
|
||||
signatures: dict of {field_key: binary_png} for signature image fields
|
||||
|
||||
Returns:
|
||||
bytes of the filled PDF (all pages preserved)
|
||||
"""
|
||||
if signatures is None:
|
||||
signatures = {}
|
||||
|
||||
try:
|
||||
original = PdfFileReader(BytesIO(template_pdf_bytes))
|
||||
except Exception as e:
|
||||
_logger.error("Failed to read template PDF: %s", e)
|
||||
raise
|
||||
|
||||
output = PdfFileWriter()
|
||||
num_pages = original.getNumPages()
|
||||
|
||||
for page_idx in range(num_pages):
|
||||
page = original.getPage(page_idx)
|
||||
page_num = page_idx + 1 # 1-based page number
|
||||
mb = page.mediaBox
|
||||
page_w = float(mb.getWidth())
|
||||
page_h = float(mb.getHeight())
|
||||
origin_x = float(mb.getLowerLeft_x())
|
||||
origin_y = float(mb.getLowerLeft_y())
|
||||
|
||||
fields = fields_by_page.get(page_num, [])
|
||||
|
||||
if fields:
|
||||
overlay_buf = BytesIO()
|
||||
c = canvas.Canvas(
|
||||
overlay_buf,
|
||||
pagesize=(origin_x + page_w, origin_y + page_h),
|
||||
)
|
||||
|
||||
for field in fields:
|
||||
PDFTemplateFiller._draw_field(
|
||||
c, field, context, signatures,
|
||||
page_w, page_h, origin_x, origin_y,
|
||||
)
|
||||
|
||||
c.save()
|
||||
overlay_buf.seek(0)
|
||||
|
||||
# Merge overlay onto original page (same as sign module)
|
||||
overlay_pdf = PdfFileReader(overlay_buf)
|
||||
page.mergePage(overlay_pdf.getPage(0))
|
||||
|
||||
output.addPage(page)
|
||||
|
||||
result = BytesIO()
|
||||
output.write(result)
|
||||
return result.getvalue()
|
||||
|
||||
@staticmethod
|
||||
def _draw_field(c, field, context, signatures,
|
||||
page_w, page_h, origin_x=0, origin_y=0):
|
||||
"""Draw a single field onto the reportlab canvas.
|
||||
|
||||
Args:
|
||||
c: reportlab Canvas
|
||||
field: dict with field_key, pos_x, pos_y, width, height, field_type, etc.
|
||||
context: data context dict
|
||||
signatures: dict of {field_key: binary} for signature fields
|
||||
page_w: page width in PDF points
|
||||
page_h: page height in PDF points
|
||||
origin_x: mediaBox lower-left X (accounts for non-zero origin)
|
||||
origin_y: mediaBox lower-left Y (accounts for non-zero origin)
|
||||
"""
|
||||
field_key = field.get('field_key') or field.get('field_name', '')
|
||||
field_type = field.get('field_type', 'text')
|
||||
value = context.get(field_key, field.get('default_value', ''))
|
||||
|
||||
if not value and field_type != 'signature':
|
||||
return
|
||||
|
||||
# Convert percentage positions to absolute PDF coordinates.
|
||||
# pos_x/pos_y are 0.0-1.0 ratios from top-left of the visible page.
|
||||
# PDF coordinate system: origin at bottom-left, Y goes up.
|
||||
# origin_x/origin_y account for PDFs whose mediaBox doesn't start at (0,0).
|
||||
abs_x = field['pos_x'] * page_w + origin_x
|
||||
abs_y = (origin_y + page_h) - (field['pos_y'] * page_h)
|
||||
|
||||
font_name = field.get('font_name', 'Helvetica')
|
||||
font_size = field.get('font_size', 10.0)
|
||||
|
||||
if field_type in ('text', 'date'):
|
||||
c.setFont(font_name, font_size)
|
||||
text_val = str(value)
|
||||
field_h = field.get('height', 0.018) * page_h
|
||||
text_y = abs_y - field_h + (field_h - font_size) / 2
|
||||
align = field.get('text_align', 'left')
|
||||
if align == 'center':
|
||||
center_x = abs_x + (field.get('width', 0.15) * page_w) / 2
|
||||
c.drawCentredString(center_x, text_y, text_val)
|
||||
elif align == 'right':
|
||||
right_x = abs_x + field.get('width', 0.15) * page_w
|
||||
c.drawRightString(right_x, text_y, text_val)
|
||||
else:
|
||||
c.drawString(abs_x, text_y, text_val)
|
||||
|
||||
elif field_type == 'checkbox':
|
||||
if value:
|
||||
# Draw a cross mark (✗) that fills the checkbox box
|
||||
cb_w = field.get('width', 0.015) * page_w
|
||||
cb_h = field.get('height', 0.018) * page_h
|
||||
# Inset slightly so the cross doesn't touch the box edges
|
||||
pad = min(cb_w, cb_h) * 0.15
|
||||
x1 = abs_x + pad
|
||||
y1 = abs_y - cb_h + pad
|
||||
x2 = abs_x + cb_w - pad
|
||||
y2 = abs_y - pad
|
||||
c.saveState()
|
||||
c.setStrokeColorRGB(0, 0, 0)
|
||||
c.setLineWidth(1.5)
|
||||
# Draw X (two diagonal lines)
|
||||
c.line(x1, y1, x2, y2)
|
||||
c.line(x1, y2, x2, y1)
|
||||
c.restoreState()
|
||||
|
||||
elif field_type == 'signature':
|
||||
sig_data = signatures.get(field_key)
|
||||
if sig_data:
|
||||
try:
|
||||
img = ImageReader(BytesIO(sig_data))
|
||||
sig_w = field.get('width', 0.15) * page_w
|
||||
sig_h = field.get('height', 0.05) * page_h
|
||||
# Draw signature image (position from top, so adjust Y)
|
||||
c.drawImage(
|
||||
img, abs_x, abs_y - sig_h,
|
||||
width=sig_w, height=sig_h,
|
||||
mask='auto',
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.warning("Failed to draw signature for %s: %s", field_key, e)
|
||||
Reference in New Issue
Block a user