diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py index 1097367a..2ad80667 100644 --- a/fusion_plating/fusion_plating_jobs/__manifest__.py +++ b/fusion_plating/fusion_plating_jobs/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Plating — Native Jobs', - 'version': '19.0.11.2.0', + 'version': '19.0.11.3.0', 'category': 'Manufacturing/Plating', 'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.', 'author': 'Nexa Systems Inc.', diff --git a/fusion_plating/fusion_plating_jobs/models/fp_certificate.py b/fusion_plating/fusion_plating_jobs/models/fp_certificate.py index 43f11b21..fc1d26d3 100644 --- a/fusion_plating/fusion_plating_jobs/models/fp_certificate.py +++ b/fusion_plating/fusion_plating_jobs/models/fp_certificate.py @@ -123,3 +123,160 @@ class FpCertificate(models.Model): 'view_mode': 'form', 'target': 'current', } + + # ---- Parse-on-upload for the cert-form Fischerscope field (2026-05-28) + # The Issue Certs wizard parses .doc/.docx/RTF Fischerscope exports into + # readings + metadata + microscope image. Dropping the same file straight + # onto the cert form's x_fc_local_thickness_pdf field did nothing — it + # just stored the bytes. These hooks give the form the SAME behaviour as + # the wizard: on save, a non-PDF upload is parsed and relocated to the + # evidence field (a real PDF is left in place to merge as page 2). + + @api.model_create_multi + def create(self, vals_list): + recs = super().create(vals_list) + for rec in recs: + if (rec.x_fc_local_thickness_pdf + and not self.env.context.get('fp_skip_thickness_parse')): + rec._fp_parse_local_thickness_upload() + return recs + + def write(self, vals): + res = super().write(vals) + if (vals.get('x_fc_local_thickness_pdf') + and not self.env.context.get('fp_skip_thickness_parse')): + for rec in self: + rec._fp_parse_local_thickness_upload() + return res + + def _fp_parse_local_thickness_upload(self): + """Parse a Fischerscope .doc/.docx/RTF dropped on the cert form's + x_fc_local_thickness_pdf field, exactly like the Issue Certs wizard: + extract readings → thickness_reading_ids, header metadata → + x_fc_thickness_* fields, microscope image → x_fc_thickness_image_id, + then relocate the non-PDF source to x_fc_local_thickness_evidence_id + and clear the PDF field (so the page-2 merge doesn't choke on it). + + A real PDF is left in place — it merges as page 2 of the CoC on + Issue and carries no parseable readings. Unknown non-PDF types are + left untouched. + """ + import base64 + from datetime import datetime + self.ensure_one() + if not self.x_fc_local_thickness_pdf: + return + try: + raw = base64.b64decode(self.x_fc_local_thickness_pdf) + except Exception: + return + # Real PDF → leave it (merges as page 2). XDAL 600 names RTF files + # ".doc"; detect by magic bytes, not extension (see CLAUDE.md). + if raw[:4] == b'%PDF': + return + name = (self.x_fc_local_thickness_pdf_filename or '').lower() + is_rtf = raw[:5] == b'{\\rtf' + is_docx = name.endswith('.docx') + if not (is_rtf or is_docx): + return # unknown non-PDF — don't guess + + from ..wizards.fp_cert_issue_wizard import ( + _fp_parse_fischerscope_rtf, _fp_parse_fischerscope_docx, + _fp_extract_rtf_images, _fp_pick_microscope_image, + ) + parsed = (_fp_parse_fischerscope_rtf(raw) if is_rtf + else _fp_parse_fischerscope_docx(raw)) + + vals = {} + for fname, fval in ( + ('x_fc_thickness_operator', parsed.get('operator')), + ('x_fc_thickness_product', parsed.get('product')), + ('x_fc_thickness_directory', parsed.get('directory')), + ('x_fc_thickness_application', parsed.get('application')), + ('x_fc_thickness_measuring_time_sec', + parsed.get('measuring_time_sec') or 0), + ('x_fc_thickness_equipment', + parsed.get('equipment') or 'Fischerscope XDAL 600'), + ('x_fc_thickness_source_filename', + self.x_fc_local_thickness_pdf_filename or ''), + ): + if fname in self._fields and fval: + vals[fname] = fval + + date_str = (parsed.get('date_str') or '').strip() + time_str = (parsed.get('time_str') or '').strip() + if date_str and 'x_fc_thickness_datetime' in self._fields: + combined = ('%s %s' % (date_str, time_str)).strip() + for fmt in ( + '%m/%d/%Y %I:%M:%S %p', '%m/%d/%Y %I:%M %p', + '%m/%d/%Y %H:%M:%S', '%m/%d/%Y %H:%M', '%m/%d/%Y', + ): + try: + vals['x_fc_thickness_datetime'] = datetime.strptime( + combined, fmt, + ) + break + except ValueError: + continue + + # Readings — replace any existing set with the freshly-parsed rows + # (the uploaded report is authoritative for this cert). + readings = parsed.get('readings') or [] + Reading = self.env.get('fp.thickness.reading') + if readings and Reading is not None: + calibration = parsed.get('calibration') or '' + cmds = [(5, 0, 0)] + for i, (nip, ni, p) in enumerate(readings): + rvals = {'nip_mils': nip, 'ni_percent': ni, 'p_percent': p} + if 'reading_number' in Reading._fields: + rvals['reading_number'] = i + 1 + if calibration and 'calibration_std_ref' in Reading._fields: + rvals['calibration_std_ref'] = calibration + cmds.append((0, 0, rvals)) + vals['thickness_reading_ids'] = cmds + + # Relocate the non-PDF source to the evidence slot + clear the PDF + # field (mirrors the wizard's non-PDF end state). + att = self.env['ir.attachment'].sudo().create({ + 'name': self.x_fc_local_thickness_pdf_filename or 'fischerscope-report', + 'type': 'binary', + 'datas': self.x_fc_local_thickness_pdf, + 'res_model': self._name, + 'res_id': self.id, + }) + if 'x_fc_local_thickness_evidence_id' in self._fields: + vals['x_fc_local_thickness_evidence_id'] = att.id + vals['x_fc_local_thickness_pdf'] = False + vals['x_fc_local_thickness_pdf_filename'] = False + + # Microscope image (RTF only — .docx images need a different path). + if is_rtf and 'x_fc_thickness_image_id' in self._fields: + try: + pngs = _fp_extract_rtf_images(raw) + img_bytes, img_w, img_h = _fp_pick_microscope_image(pngs) + if img_bytes: + img_att = self.env['ir.attachment'].sudo().create({ + 'name': '%s-microscope.png' % ( + (self.x_fc_local_thickness_pdf_filename + or 'fischerscope').rsplit('.', 1)[0] + ), + 'type': 'binary', + 'datas': base64.b64encode(img_bytes), + 'mimetype': 'image/png', + 'res_model': self._name, + 'res_id': self.id, + }) + vals['x_fc_thickness_image_id'] = img_att.id + except Exception: + pass + + self.with_context(fp_skip_thickness_parse=True).write(vals) + from markupsafe import Markup + from odoo import _ + self.message_post(body=Markup(_( + 'Fischerscope file %s parsed from the cert form: ' + '%d reading(s) extracted.' + )) % ( + self.x_fc_thickness_source_filename or name or 'unnamed', + len(readings), + ))