# -*- coding: utf-8 -*- import base64 import logging from io import BytesIO from odoo import models, fields, api _logger = logging.getLogger(__name__) class FusionSaSignatureTemplate(models.Model): _name = 'fusion.sa.signature.template' _description = 'SA Mobility Signature Position Template' _order = 'name' name = fields.Char(string='Template Name', required=True) active = fields.Boolean(default=True) notes = fields.Text(string='Notes') sa_default_sig_page = fields.Integer(string='Default Signature Page', default=2) # Absolute PDF point coordinates. Y = distance from top of page. sa_sig_name_x = fields.Integer(string='Name X', default=105) sa_sig_name_y = fields.Integer(string='Name Y from top', default=97) sa_sig_date_x = fields.Integer(string='Date X', default=430) sa_sig_date_y = fields.Integer(string='Date Y from top', default=97) sa_sig_x = fields.Integer(string='Signature X', default=72) sa_sig_y = fields.Integer(string='Signature Y from top', default=68) sa_sig_w = fields.Integer(string='Signature Width', default=190) sa_sig_h = fields.Integer(string='Signature Height', default=25) preview_pdf = fields.Binary( string='Sample PDF', help='Upload a sample SA Mobility approval form to preview signature placement.', attachment=True, ) preview_pdf_filename = fields.Char(string='PDF Filename') preview_pdf_page = fields.Integer( string='Preview Page', default=0, help='Page to render preview for. 0 = use Default Signature Page.', ) preview_image = fields.Binary( string='Preview', readonly=True, compute='_compute_preview_image', ) @api.depends( 'preview_pdf', 'preview_pdf_page', 'sa_default_sig_page', 'sa_sig_name_x', 'sa_sig_name_y', 'sa_sig_date_x', 'sa_sig_date_y', 'sa_sig_x', 'sa_sig_y', 'sa_sig_w', 'sa_sig_h', ) def _compute_preview_image(self): for rec in self: if not rec.preview_pdf: rec.preview_image = False continue try: rec.preview_image = rec._render_preview() except Exception as e: _logger.warning("SA template preview render failed: %s", e) rec.preview_image = False def _render_preview(self): """Render the sample PDF page with colored markers for all signature positions.""" self.ensure_one() from odoo.tools.pdf import PdfFileReader pdf_bytes = base64.b64decode(self.preview_pdf) reader = PdfFileReader(BytesIO(pdf_bytes)) num_pages = reader.getNumPages() page_num = self.preview_pdf_page or self.sa_default_sig_page or 2 page_idx = page_num - 1 if page_idx < 0 or page_idx >= num_pages: return False try: from pdf2image import convert_from_bytes except ImportError: _logger.warning("pdf2image not installed") return False images = convert_from_bytes( pdf_bytes, first_page=page_idx + 1, last_page=page_idx + 1, dpi=150, ) if not images: return False from PIL import ImageDraw, ImageFont img = images[0] draw = ImageDraw.Draw(img) page = reader.getPage(page_idx) page_w_pts = float(page.mediaBox.getWidth()) page_h_pts = float(page.mediaBox.getHeight()) img_w, img_h = img.size sx = img_w / page_w_pts sy = img_h / page_h_pts try: font_b = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14) font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12) font_sm = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10) except Exception: font_b = font = font_sm = ImageFont.load_default() def _draw_sample_text(label, x_pts, y_top_pts, color, sample_text): px_x = int(x_pts * sx) px_y = int(y_top_pts * sy) draw.text((px_x, px_y - 16), sample_text, fill=color, font=font_b) draw.text((px_x, px_y + 2), label, fill=color, font=font_sm) def _draw_box(label, x_pts, y_top_pts, w_pts, h_pts, color): px_x = int(x_pts * sx) px_y = int(y_top_pts * sy) pw = int(w_pts * sx) ph = int(h_pts * sy) for off in range(3): draw.rectangle( [px_x - off, px_y - off, px_x + pw + off, px_y + ph + off], outline=color, ) draw.text((px_x + 4, px_y + 4), label, fill=color, font=font_sm) _draw_sample_text( "Name", self.sa_sig_name_x, self.sa_sig_name_y, 'blue', "John Smith", ) _draw_sample_text( "Date", self.sa_sig_date_x, self.sa_sig_date_y, 'purple', "2026-02-17", ) _draw_box( "Signature", self.sa_sig_x, self.sa_sig_y, self.sa_sig_w, self.sa_sig_h, 'red', ) buf = BytesIO() img.save(buf, format='PNG') return base64.b64encode(buf.getvalue()) def get_sa_coordinates(self, page_h=792): """Convert to ReportLab bottom-origin coordinates. Template stores Y as distance from TOP of page. ReportLab uses Y from BOTTOM. For text (name/date): baseline Y = page_h - y_from_top For signature image: drawImage Y is bottom-left corner, so Y = page_h - y_from_top - height """ self.ensure_one() return { 'name_x': self.sa_sig_name_x, 'name_y': page_h - self.sa_sig_name_y, 'date_x': self.sa_sig_date_x, 'date_y': page_h - self.sa_sig_date_y, 'sig_x': self.sa_sig_x, 'sig_y': page_h - self.sa_sig_y - self.sa_sig_h, 'sig_w': self.sa_sig_w, 'sig_h': self.sa_sig_h, }