- Relocate plating thickness out of the cramped rail (where it shared a row with Due and wrapped) to a big 21pt "PLATING THICKNESS" banner at the top of the main panel — the team's most-watched spec, now hard to miss. Gated on a new has_thk flag (real value with a digit; skips empty/'N/A'). Due takes the full rail row. - Fix the bake-text degree mojibake: operators type 'º' (U+00BA) for "375ºF"; through this sticker's lightweight html_container path (no .article UTF-8 wrapper) it renders "375°F". Adding a .article wrapper fixes encoding but blows up the dpi=96 mm layout (tested), so _clean() now strips º/°/˚ to clean ASCII -> "375F". Verified on entech (WO-30094). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
114 lines
4.8 KiB
Python
114 lines
4.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
# Copyright 2026 Nexa Systems Inc.
|
||
"""Display helpers for the redesigned job stickers (Internal = Layout A,
|
||
one per job; External = Layout B, one per box).
|
||
|
||
Keeps the QWeb templates thin: all field resolution, the customer
|
||
short-code (shop-floor "secrecy cover"), em-dash/smart-quote cleanup for
|
||
the entech wkhtmltopdf font, and the length-tiered notes font size live
|
||
here in Python.
|
||
"""
|
||
from odoo import models
|
||
|
||
|
||
def _clean(text):
|
||
"""Strip the glyphs entech's wkhtmltopdf font mojibakes."""
|
||
if not text:
|
||
return ''
|
||
t = str(text)
|
||
for a, b in ((u'—', '-'), (u'–', '-'), (u'‘', "'"),
|
||
(u'’', "'"), (u'“', '"'), (u'”', '"'),
|
||
(u'…', '...'),
|
||
# Degree symbols: the masculine-ordinal 'º' (U+00BA) operators
|
||
# type for "375ºF", the real degree '°' (U+00B0), and the ring
|
||
# '˚' ALL mojibake to "°"/"º" through this sticker's lightweight
|
||
# html_container path (no .article UTF-8 wrapper — and adding one
|
||
# blows up the dpi=96 mm layout). Strip to clean ASCII: "375F".
|
||
(u'º', ''), (u'°', ''), (u'˚', '')):
|
||
t = t.replace(a, b)
|
||
return t.strip()
|
||
|
||
|
||
class FpJob(models.Model):
|
||
_inherit = 'fp.job'
|
||
|
||
def _fp_sticker_shortcode(self, partner):
|
||
"""ABC Manufacturing Inc -> 'ABC-MANU'. First 3 of word[0] + first 4
|
||
of word[1] (alnum-only), uppercase. Single word -> first 3."""
|
||
name = (partner.name or '') if partner else ''
|
||
words = [''.join(c for c in w if c.isalnum()) for w in name.split()]
|
||
words = [w for w in words if w]
|
||
if len(words) >= 2:
|
||
return (words[0][:3] + '-' + words[1][:4]).upper()
|
||
if words:
|
||
return words[0][:3].upper()
|
||
return name or '-'
|
||
|
||
def _fp_note_pt(self, text):
|
||
"""Length-tiered notes font (pt) so long instructions stay on one
|
||
label. Mirrors the approved mockups."""
|
||
n = len(text or '')
|
||
if n <= 180:
|
||
return 11.0
|
||
if n <= 320:
|
||
return 10.0
|
||
if n <= 520:
|
||
return 9.0
|
||
return 8.5
|
||
|
||
def _fp_sticker_data(self):
|
||
"""Resolved display values for the job sticker (both variants)."""
|
||
self.ensure_one()
|
||
job = self
|
||
line = job.sale_order_line_ids[:1] if 'sale_order_line_ids' in job._fields \
|
||
else job.env['sale.order.line']
|
||
part = (('part_catalog_id' in job._fields and job.part_catalog_id)
|
||
or (line and 'x_fc_part_catalog_id' in line._fields and line.x_fc_part_catalog_id)
|
||
or False)
|
||
so = job.sale_order_id
|
||
|
||
rev = ''
|
||
if part and getattr(part, 'revision', False):
|
||
rev = (part.revision or '').strip()
|
||
if rev.lower().startswith('rev '):
|
||
rev = rev[4:].strip()
|
||
|
||
due = job.date_deadline or (so and so.commitment_date) or False
|
||
due_s = due.strftime('%b %d %Y') if due else ''
|
||
|
||
thk = ''
|
||
if line and 'x_fc_thickness_range' in line._fields and line.x_fc_thickness_range:
|
||
thk = _clean(line.x_fc_thickness_range)
|
||
|
||
q = job.qty or 0
|
||
qty = int(q) if float(q).is_integer() else q
|
||
|
||
return {
|
||
'wo': job.name or '',
|
||
'part': ((part.part_number if part and getattr(part, 'part_number', False)
|
||
else (part.name if part else '')) or ''),
|
||
'rev': rev,
|
||
'customer': self._fp_sticker_shortcode(job.partner_id),
|
||
'customer_full': job.partner_id.name or '',
|
||
'po': (so and getattr(so, 'x_fc_po_number', False)) or '',
|
||
'qty': qty,
|
||
'due': due_s,
|
||
'thk': thk,
|
||
# Real thickness present (has a digit) — drives the prominent
|
||
# THICKNESS banner; skips empty / 'N/A' / '-' placeholders.
|
||
'has_thk': bool(thk and any(c.isdigit() for c in thk)),
|
||
'mask': bool(line and 'x_fc_masking_enabled' in line._fields and line.x_fc_masking_enabled),
|
||
'bake': _clean(line.x_fc_bake_instructions) if (line and 'x_fc_bake_instructions' in line._fields) else '',
|
||
'internal_notes': _clean(line.x_fc_internal_description) if (line and 'x_fc_internal_description' in line._fields) else '',
|
||
'customer_notes': _clean(line.name) if line else '',
|
||
}
|
||
|
||
def _fp_sticker_boxes(self):
|
||
"""The job's tracked boxes (External sticker prints one label each).
|
||
Empty recordset when none yet — the template falls back to 1/1."""
|
||
self.ensure_one()
|
||
if self.sale_order_id and 'fp.box' in self.env:
|
||
return self.env['fp.box'].sudo().search(
|
||
[('sale_order_id', '=', self.sale_order_id.id)], order='box_number')
|
||
return self.env['fp.box'] if 'fp.box' in self.env else self.browse()
|