changes
This commit is contained in:
@@ -71,6 +71,13 @@ class DeviceApprovalWizard(models.TransientModel):
|
||||
help='ADP Claim Number from the approval letter',
|
||||
)
|
||||
|
||||
approval_date = fields.Date(
|
||||
string='Approval Date',
|
||||
default=fields.Date.context_today,
|
||||
help='Date ADP approved the application. Defaults to today but can be changed '
|
||||
'if ADP approved before the letter was received.',
|
||||
)
|
||||
|
||||
# Approval Documents - for Mark as Approved mode
|
||||
is_mark_approved_mode = fields.Boolean(
|
||||
string='Mark Approved Mode',
|
||||
@@ -281,6 +288,7 @@ class DeviceApprovalWizard(models.TransientModel):
|
||||
|
||||
update_vals = {
|
||||
'x_fc_adp_application_status': new_status,
|
||||
'x_fc_claim_approval_date': self.approval_date or fields.Date.context_today(self),
|
||||
}
|
||||
|
||||
# Save claim number if provided
|
||||
@@ -340,8 +348,8 @@ class DeviceApprovalWizard(models.TransientModel):
|
||||
|
||||
# Post approval to chatter with all documents in ONE message
|
||||
from markupsafe import Markup
|
||||
from datetime import date
|
||||
|
||||
approval_date_str = (self.approval_date or fields.Date.context_today(self)).strftime('%B %d, %Y')
|
||||
device_details = f'{approved_count} approved'
|
||||
if unapproved_count > 0:
|
||||
device_details += f', {unapproved_count} not approved'
|
||||
@@ -364,7 +372,7 @@ class DeviceApprovalWizard(models.TransientModel):
|
||||
body=Markup(
|
||||
'<div class="alert alert-success" role="alert">'
|
||||
'<h5 class="alert-heading"><i class="fa fa-check-circle"/> Application Approved</h5>'
|
||||
f'<p class="mb-1"><strong>Date:</strong> {date.today().strftime("%B %d, %Y")}</p>'
|
||||
f'<p class="mb-1"><strong>Date:</strong> {approval_date_str}</p>'
|
||||
f'<p class="mb-1"><strong>Devices:</strong> {device_details}</p>'
|
||||
f'{docs_html}'
|
||||
'</div>'
|
||||
@@ -379,7 +387,7 @@ class DeviceApprovalWizard(models.TransientModel):
|
||||
body=Markup(
|
||||
'<div class="alert alert-success" role="alert">'
|
||||
'<h5 class="alert-heading"><i class="fa fa-check-circle"/> Application Approved</h5>'
|
||||
f'<p class="mb-1"><strong>Date:</strong> {date.today().strftime("%B %d, %Y")}</p>'
|
||||
f'<p class="mb-1"><strong>Date:</strong> {approval_date_str}</p>'
|
||||
f'<p class="mb-1"><strong>Devices:</strong> {device_details}</p>'
|
||||
'</div>'
|
||||
),
|
||||
|
||||
@@ -37,12 +37,15 @@
|
||||
<strong>Invoices exist.</strong> Changes will automatically update existing invoices.
|
||||
</div>
|
||||
|
||||
<!-- Claim Number - Required for Mark as Approved -->
|
||||
<!-- Claim Number and Approval Date - Required for Mark as Approved -->
|
||||
<group invisible="not is_mark_approved_mode">
|
||||
<group>
|
||||
<field name="claim_number" required="is_mark_approved_mode"
|
||||
<field name="claim_number" required="is_mark_approved_mode"
|
||||
placeholder="Enter ADP Claim Number from approval letter"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="approval_date" required="is_mark_approved_mode"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Header: Order and All Approved -->
|
||||
|
||||
@@ -27,14 +27,6 @@ class OdspReadyDeliveryWizard(models.TransientModel):
|
||||
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',
|
||||
@@ -52,14 +44,14 @@ class OdspReadyDeliveryWizard(models.TransientModel):
|
||||
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),
|
||||
tpl = self.env['fusion.pdf.template'].search([
|
||||
('category', '=', 'odsp'), ('state', '=', 'active'),
|
||||
], limit=1)
|
||||
default_page = tpl.sa_default_sig_page if tpl else 2
|
||||
default_page = 2
|
||||
if tpl and tpl.field_ids:
|
||||
default_page = tpl.field_ids[0].page or 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')
|
||||
@@ -76,8 +68,7 @@ class OdspReadyDeliveryWizard(models.TransientModel):
|
||||
else:
|
||||
wiz.total_pages = 0
|
||||
|
||||
@api.depends('approval_form', 'signature_page',
|
||||
'signature_offset_x', 'signature_offset_y')
|
||||
@api.depends('approval_form', 'signature_page')
|
||||
def _compute_preview_image(self):
|
||||
for wiz in self:
|
||||
if not wiz.approval_form or not wiz.signature_page:
|
||||
@@ -89,37 +80,17 @@ class OdspReadyDeliveryWizard(models.TransientModel):
|
||||
_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),
|
||||
def _get_template_fields(self):
|
||||
"""Load field positions from the active ODSP PDF Template."""
|
||||
tpl = self.env['fusion.pdf.template'].search([
|
||||
('category', '=', 'odsp'), ('state', '=', 'active'),
|
||||
], 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
|
||||
if not tpl:
|
||||
return []
|
||||
return tpl.field_ids.filtered(lambda f: f.is_active)
|
||||
|
||||
def _render_preview(self):
|
||||
"""Render the selected page as a PNG with a red rectangle showing signature placement."""
|
||||
"""Render the selected page as a PNG with colored markers at field positions."""
|
||||
from odoo.tools.pdf import PdfFileReader
|
||||
|
||||
pdf_bytes = base64.b64decode(self.approval_form)
|
||||
@@ -145,15 +116,7 @@ class OdspReadyDeliveryWizard(models.TransientModel):
|
||||
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)
|
||||
@@ -161,42 +124,41 @@ class OdspReadyDeliveryWizard(models.TransientModel):
|
||||
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)
|
||||
colors = {
|
||||
'text': 'blue',
|
||||
'date': 'purple',
|
||||
'signature': 'red',
|
||||
}
|
||||
sample_text = {
|
||||
'text': 'John Smith',
|
||||
'date': '2026-02-17',
|
||||
}
|
||||
|
||||
# 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)
|
||||
for field in self._get_template_fields():
|
||||
color = colors.get(field.field_type, 'gray')
|
||||
px_x = int(field.pos_x * img_w)
|
||||
px_y = int(field.pos_y * img_h)
|
||||
|
||||
# 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)
|
||||
if field.field_type == 'signature':
|
||||
px_w = int(field.width * img_w)
|
||||
px_h = int(field.height * img_h)
|
||||
for off in range(3):
|
||||
draw.rectangle(
|
||||
[px_x - off, px_y - off, px_x + px_w + off, px_y + px_h + off],
|
||||
outline=color,
|
||||
)
|
||||
draw.text((px_x + 4, px_y + 4), field.label or 'Signature', fill=color, font=font_sm)
|
||||
else:
|
||||
text = sample_text.get(field.field_type, field.label or field.name)
|
||||
draw.text((px_x, px_y - 16), text, fill=color, font=font_b)
|
||||
draw.text((px_x, px_y + 2), field.label or field.name, fill=color, 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."""
|
||||
"""Save signature page, advance status, and open the delivery task form."""
|
||||
self.ensure_one()
|
||||
order = self.sale_order_id
|
||||
|
||||
@@ -207,8 +169,6 @@ class OdspReadyDeliveryWizard(models.TransientModel):
|
||||
|
||||
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 {
|
||||
|
||||
@@ -13,16 +13,14 @@
|
||||
<group>
|
||||
<group string="Signature Settings">
|
||||
<div class="text-muted mb-2" colspan="2">
|
||||
Select the page containing the signature area. Position defaults are loaded from Settings.
|
||||
Use X/Y offsets to fine-tune for this specific case if needed.
|
||||
Select the page containing the signature area. Positions are managed in
|
||||
Configuration > PDF Templates (ODSP category).
|
||||
</div>
|
||||
<label for="signature_page"/>
|
||||
<div class="d-flex align-items-center">
|
||||
<field name="signature_page" class="oe_inline" style="width: 60px;"/>
|
||||
<span class="ms-2 text-muted">of <field name="total_pages" class="oe_inline" widget="integer" readonly="1"/> pages</span>
|
||||
</div>
|
||||
<field name="signature_offset_x" string="Fine-tune X Offset"/>
|
||||
<field name="signature_offset_y" string="Fine-tune Y Offset"/>
|
||||
<div colspan="2" class="mt-2">
|
||||
<button name="action_preview_full" type="object"
|
||||
string="Preview Full PDF" class="btn-link"
|
||||
|
||||
Reference in New Issue
Block a user