# -*- 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()