# -*- coding: utf-8 -*- import base64 import logging from io import BytesIO from odoo import models, fields, api from odoo.exceptions import UserError _logger = logging.getLogger(__name__) class OdspReadyDeliveryWizard(models.TransientModel): _name = 'fusion_claims.odsp.ready.delivery.wizard' _description = 'Ready for Delivery - Signature Position Setup' sale_order_id = fields.Many2one( 'sale.order', string='Sale Order', readonly=True, required=True, ) approval_form = fields.Binary( string='Approval Form', readonly=True, ) approval_form_filename = fields.Char(string='Approval Form Filename') signature_page = fields.Integer( string='Signature Page', required=True, default=2, help='Page number containing the signature area (1-indexed)', ) total_pages = fields.Integer( string='Total Pages', readonly=True, compute='_compute_total_pages', ) signature_offset_x = fields.Integer( string='X Offset (pts)', default=0, help='Per-case horizontal fine-tune in points (positive = right)', ) signature_offset_y = fields.Integer( string='Y Offset (pts)', default=0, help='Per-case vertical fine-tune in points (positive = up)', ) preview_image = fields.Binary( string='Preview', readonly=True, compute='_compute_preview_image', ) @api.model def default_get(self, fields_list): res = super().default_get(fields_list) active_id = self.env.context.get('active_id') if not active_id: return res order = self.env['sale.order'].browse(active_id) res['sale_order_id'] = order.id res['approval_form'] = order.x_fc_sa_approval_form res['approval_form_filename'] = order.x_fc_sa_approval_form_filename tpl = self.env['fusion.sa.signature.template'].search([ ('active', '=', True), ], limit=1) default_page = tpl.sa_default_sig_page if tpl else 2 res['signature_page'] = order.x_fc_sa_signature_page or default_page res['signature_offset_x'] = order.x_fc_sa_signature_offset_x or 0 res['signature_offset_y'] = order.x_fc_sa_signature_offset_y or 0 return res @api.depends('approval_form') def _compute_total_pages(self): for wiz in self: if wiz.approval_form: try: from odoo.tools.pdf import PdfFileReader pdf_bytes = base64.b64decode(wiz.approval_form) reader = PdfFileReader(BytesIO(pdf_bytes)) wiz.total_pages = reader.getNumPages() except Exception: wiz.total_pages = 0 else: wiz.total_pages = 0 @api.depends('approval_form', 'signature_page', 'signature_offset_x', 'signature_offset_y') def _compute_preview_image(self): for wiz in self: if not wiz.approval_form or not wiz.signature_page: wiz.preview_image = False continue try: wiz.preview_image = wiz._render_preview() except Exception as e: _logger.warning("Preview render failed: %s", e) wiz.preview_image = False def _get_template_coords(self, page_h=792): """Load coordinates from SA Signature Template with per-case offsets.""" tpl = self.env['fusion.sa.signature.template'].search([ ('active', '=', True), ], limit=1) if tpl: coords = tpl.get_sa_coordinates(page_h) else: coords = { 'name_x': 105, 'name_y': page_h - 97, 'date_x': 430, 'date_y': page_h - 97, 'sig_x': 72, 'sig_y': page_h - 72 - 25, 'sig_w': 190, 'sig_h': 25, } ox = self.signature_offset_x or 0 oy = self.signature_offset_y or 0 if ox or oy: for k in ('name_x', 'date_x', 'sig_x'): if k in coords: coords[k] += ox for k in ('name_y', 'date_y', 'sig_y'): if k in coords: coords[k] += oy return coords def _render_preview(self): """Render the selected page as a PNG with a red rectangle showing signature placement.""" from odoo.tools.pdf import PdfFileReader pdf_bytes = base64.b64decode(self.approval_form) reader = PdfFileReader(BytesIO(pdf_bytes)) num_pages = reader.getNumPages() page_idx = (self.signature_page or 2) - 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, cannot generate preview.") 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 scale_x = img_w / page_w_pts scale_y = img_h / page_h_pts coords = self._get_template_coords(page_h_pts) try: font_b = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14) font_sm = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10) except Exception: font_b = font_sm = ImageFont.load_default() # Signature box (red) -- sig_y is bottom-left in ReportLab # top edge of box in from-top coords = page_h - (sig_y + sig_h) sig_from_top = page_h_pts - coords['sig_y'] - coords['sig_h'] px_x = int(coords['sig_x'] * scale_x) px_y = int(sig_from_top * scale_y) px_w = int(coords['sig_w'] * scale_x) px_h = int(coords['sig_h'] * scale_y) for off in range(3): draw.rectangle( [px_x - off, px_y - off, px_x + px_w + off, px_y + px_h + off], outline='red', ) draw.text((px_x + 4, px_y + 4), "Signature", fill='red', font=font_sm) # Name (blue) -- convert ReportLab bottom-origin back to top-origin for PIL if 'name_x' in coords: name_from_top = page_h_pts - coords['name_y'] nx = int(coords['name_x'] * scale_x) ny = int(name_from_top * scale_y) draw.text((nx, ny - 16), "John Smith", fill='blue', font=font_b) draw.text((nx, ny + 2), "Name", fill='blue', font=font_sm) # Date (purple) if 'date_x' in coords: date_from_top = page_h_pts - coords['date_y'] dx = int(coords['date_x'] * scale_x) dy = int(date_from_top * scale_y) draw.text((dx, dy - 16), "2026-02-17", fill='purple', font=font_b) draw.text((dx, dy + 2), "Date", fill='purple', font=font_sm) buf = BytesIO() img.save(buf, format='PNG') return base64.b64encode(buf.getvalue()) def action_confirm(self): """Save signature settings, advance status, and open the delivery task form.""" self.ensure_one() order = self.sale_order_id if self.signature_page < 1 or (self.total_pages and self.signature_page > self.total_pages): raise UserError( "Invalid signature page. Must be between 1 and %s." % self.total_pages ) order.write({ 'x_fc_sa_signature_page': self.signature_page, 'x_fc_sa_signature_offset_x': self.signature_offset_x, 'x_fc_sa_signature_offset_y': self.signature_offset_y, }) return { 'name': 'Schedule Delivery Task', 'type': 'ir.actions.act_window', 'res_model': 'fusion.technician.task', 'view_mode': 'form', 'target': 'new', 'context': { 'default_task_type': 'delivery', 'default_sale_order_id': order.id, 'default_partner_id': order.partner_id.id, 'default_pod_required': True, 'mark_odsp_ready_for_delivery': True, }, } def action_preview_full(self): """Open the full approval PDF for preview.""" self.ensure_one() if not self.approval_form: raise UserError("No approval form available to preview.") att = self.env['ir.attachment'].search([ ('res_model', '=', 'sale.order'), ('res_id', '=', self.sale_order_id.id), ('name', '=', self.approval_form_filename), ], order='create_date desc', limit=1) if not att: att = self.env['ir.attachment'].create({ 'name': self.approval_form_filename or 'ODSP_Approval.pdf', 'type': 'binary', 'datas': self.approval_form, 'res_model': 'sale.order', 'res_id': self.sale_order_id.id, 'mimetype': 'application/pdf', }) return { 'type': 'ir.actions.client', 'tag': 'fusion_claims.preview_document', 'params': { 'attachment_id': att.id, 'title': att.name, }, }