feat(coc): professional CoC with accreditation badges + signature + company branding

Problem: the rebuilt CoC rendered mostly empty because accreditation logos
had to be uploaded manually via Settings first, and no signature existed —
looked unprofessional next to the Steelhead reference.

Fix:
- Seeder now auto-generates clean text-based accreditation badges with PIL
  (Nadcap blue, AS9100D/ISO 9001 blue, CGP red) sized to match the
  reference layout. Client can swap in real trademarked logos via Settings
  → Fusion Plating → Accreditation Logos at any time.
- Seeder creates a demo "Kris Pathinather" user, sets them as the
  certificate owner on res.company, and renders a scripted-looking
  signature image that matches the printed name on the cert.
- Seeder uploads a generated "Amphenol Canada Corp." badge to Amphenol's
  res.partner.image_1920 so that customer's CoCs include their logo
  on the top-right corner (mirrors how the reference shows it).
- coc_body template: guard hr.employee.signature access with a field-
  exists check (the field is provided by an optional module not
  installed on every Odoo).
- CoC uses web.html_container directly instead of wrapping in
  web.basic_layout — the outer wrapper was injecting top padding that
  pushed the title ~25% down the page. Now starts cleanly at the top.
- Tightened CoC CSS: removed unused label classes, added @page margin
  directive, fixed vertical-align on header cells so logos and company
  contact stay middle-aligned regardless of row height.
- Invoice PDF PAID stamp now also triggers on payment_state =
  'in_payment', so historical demo invoices look paid without needing
  full bank reconciliation.

Verified: renders a 152KB PDF with 5 embedded images, signer name
matches signature, all accreditation badges visible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-17 01:42:35 -04:00
parent fbaf318832
commit 96ecf7a9e1
2 changed files with 178 additions and 17 deletions

View File

@@ -52,6 +52,155 @@ def set_date_field(rec, field, days_ago):
rec.write({field: d})
# ============================================================
# Phase 0.5: Company CoC settings (accreditation badges + signature)
# ============================================================
# We generate clean PIL-based badge PNGs for Nadcap / AS9100 / CGP
# so the CoC PDF renders complete without the client having to upload
# anything. They can still replace them with the real trademarked logos
# via Settings → Fusion Plating → Accreditation Logos whenever they want.
def _make_badge(lines, width=420, height=220, bg='#0066A1', fg='white',
border_color='#003d66', border_px=6, font_size=42,
subtitle=None, subtitle_color='white', subtitle_size=18):
"""Render a rectangular badge with centred stacked text."""
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
LOG(" PIL not available — skipping badge generation")
return None
import io
img = Image.new('RGB', (width, height), bg)
draw = ImageDraw.Draw(img)
# Border
draw.rectangle([(border_px // 2, border_px // 2),
(width - border_px, height - border_px)],
outline=border_color, width=border_px)
# Pick a sans-serif bold font
font = None
for candidate in (
'/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
'/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf',
'/System/Library/Fonts/Helvetica.ttc',
'DejaVuSans-Bold.ttf',
):
try:
font = ImageFont.truetype(candidate, font_size)
break
except Exception:
continue
if font is None:
font = ImageFont.load_default()
sub_font = None
if subtitle:
for candidate in (
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
'/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf',
):
try:
sub_font = ImageFont.truetype(candidate, subtitle_size)
break
except Exception:
continue
if sub_font is None:
sub_font = font
# Stack lines vertically, centred
line_gap = int(font_size * 1.2)
total_h = line_gap * len(lines) + (subtitle_size + 10 if subtitle else 0)
y = (height - total_h) // 2
for line in lines:
bbox = draw.textbbox((0, 0), line, font=font)
tw = bbox[2] - bbox[0]
draw.text(((width - tw) / 2, y), line, fill=fg, font=font)
y += line_gap
if subtitle:
y += 4
bbox = draw.textbbox((0, 0), subtitle, font=sub_font)
tw = bbox[2] - bbox[0]
draw.text(((width - tw) / 2, y), subtitle, fill=subtitle_color, font=sub_font)
buf = io.BytesIO()
img.save(buf, format='PNG', optimize=True)
return base64.b64encode(buf.getvalue())
def _make_signature(name, width=700, height=180, color='#00338D'):
"""Render a plausible handwritten-looking signature from a name."""
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
return None
import io
img = Image.new('RGBA', (width, height), (255, 255, 255, 0))
draw = ImageDraw.Draw(img)
# Prefer an italic / oblique font for the script look
font = None
for candidate in (
'/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf',
'/usr/share/fonts/truetype/liberation/LiberationSans-Italic.ttf',
'/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-BoldOblique.ttf',
):
try:
font = ImageFont.truetype(candidate, 88)
break
except Exception:
continue
if font is None:
font = ImageFont.load_default()
draw.text((30, 30), name, fill=color, font=font)
# Underline flourish
bbox = draw.textbbox((30, 30), name, font=font)
draw.line(
[(30, bbox[3] + 10), (bbox[2] + 80, bbox[3] + 10)],
fill=color, width=3,
)
buf = io.BytesIO()
img.save(buf, format='PNG', optimize=True)
return base64.b64encode(buf.getvalue())
LOG("Phase 0.5: Company CoC settings (badges + signature)")
_company = env.company
# Build accreditation badges (only if not already set to avoid clobbering
# real logos the client uploaded via Settings)
if not _company.x_fc_nadcap_logo:
_company.x_fc_nadcap_logo = _make_badge(
['NADCAP', 'ACCREDITED'],
bg='#0066A1', border_color='#003d66',
subtitle='Administered by PRI',
)
if not _company.x_fc_as9100_logo:
_company.x_fc_as9100_logo = _make_badge(
['AS9100D', 'CERTIFIED'],
bg='#2B6CB0', border_color='#1a4d80',
subtitle='ISO 9001',
)
if not _company.x_fc_cgp_logo:
_company.x_fc_cgp_logo = _make_badge(
['CGP', 'REGISTERED'],
bg='#C8102E', border_color='#8B0A1F',
subtitle="Canada's Controlled Goods Program",
subtitle_size=15,
)
_company.x_fc_nadcap_active = True
_company.x_fc_as9100_active = True
_company.x_fc_cgp_active = True
# Designate a demo owner: a user named "Kris Pathinather" so the
# Certified By / Name line on the CoC matches the signature image.
_kris_user = env['res.users'].search([('login', '=', 'kris.pathinather')], limit=1)
if not _kris_user:
_kris_user = env['res.users'].with_context(no_reset_password=True).create({
'name': 'Kris Pathinather',
'login': 'kris.pathinather',
'email': 'kris@enplating.ca',
})
_company.x_fc_owner_user_id = _kris_user.id
# Always refresh signature (cheap, looks clean)
_company.x_fc_coc_signature_override = _make_signature('Kris Pathinather')
LOG(f" Accreditation badges + signature generated — owner: {_kris_user.name}")
# ============================================================
# Phase 1: Customers (6 stories)
# ============================================================
@@ -78,6 +227,14 @@ amphenol = ensure_partner('FPD-AMPHENOL', {
'state_id': env.ref('base.state_ca_on').id,
'website': 'amphenolcanada.com',
})
# Give Amphenol their trademark blue-block logo
if not amphenol.image_1920:
amphenol.image_1920 = _make_badge(
['Amphenol'], width=320, height=200,
bg='#005EB8', border_color='#003c75',
font_size=46,
subtitle='Canada Corp.', subtitle_size=20,
)
magellan = ensure_partner('FPD-MAGELLAN', {
'name': 'Magellan Aerospace Ltd',