Files
Odoo-Modules/fusion_plating/fusion_plating_certificates/tests/test_fischerscope_parser.py
gsinghpal 8c76a16366 chore(plating): de-dash shipped code + intake-neutral customer emails
Replace em-dashes and en-dashes with hyphens across 789 shipped source
files (py/xml/js/scss) so the delivered module reads as human-written;
em-dashes had become a recognizable AI-generated tell. Internal .md dev
notes are excluded. The WO-sticker mojibake strippers keep their dash
search targets (now written — / –). No logic changes: comments
and display strings only; validated with py_compile + lxml parse.

Rewrite the 7 customer notification emails to be intake-neutral
(ship-in / drop-off / pickup) and repair-aware, and fix the Shipped
email documents line (packing slip vs bill of lading; certificate only
when issued). Subjects use a hyphen separator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 00:16:19 -04:00

187 lines
8.2 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# Unit tests for the Fischerscope thickness-report parser.
# Pure-Python tests - no Odoo DB needed. Builds synthetic .docx files
# matching the real XDAL 600 export layout and verifies extraction.
import io
from datetime import datetime
from odoo.tests.common import TransactionCase
# Lazy import inside methods to avoid the circular-import trap that
# fires during test-module discovery (the package __init__ pulls in
# `lib`; if tests/__init__ also resolves `..lib` at top-level, Python
# sees a partially-initialised parent package).
class TestFischerscopeParser(TransactionCase):
"""Round-trip tests against the parser. We build a known-shape .docx
in memory, parse it back, and assert the structure matches what the
real Fischerscope XDAL 600 produces (see screenshot 2026-05-19)."""
@classmethod
def setUpClass(cls):
super().setUpClass()
try:
import docx # python-docx - required for tests
cls.docx = docx
except ImportError:
cls.docx = None
# Resolve the parser by absolute path at first use - relative
# `from ..lib import` at module top trips the test loader's
# partially-initialised-package check.
from odoo.addons.fusion_plating_certificates.lib import (
fischerscope_parser as _fp,
)
cls.fischerscope_parser = _fp
def _make_sample_docx(self, with_image=False):
"""Build a .docx that matches the screenshot layout."""
if not self.docx:
self.skipTest('python-docx not available')
doc = self.docx.Document()
doc.add_paragraph('Fischerscope® XDAL 600')
doc.add_paragraph('Product: 2805031 / NiP/Al-alloys 2805030')
doc.add_paragraph('Directory: NiP products for flat samples')
doc.add_paragraph('Application: 16 / NiP/Al-alloys')
doc.add_paragraph('')
doc.add_paragraph('Calibr. Std. Set NiP/Al STD SET SN 100174568')
doc.add_paragraph('n= 1 NiP 1= 0.6885 mils Ni 1 = 91.323 % P 1 = 8.6771 %')
doc.add_paragraph('n= 2 NiP 1= 0.5049 mils Ni 1 = 93.179 % P 1 = 6.8209 %')
doc.add_paragraph('n= 3 NiP 1= 0.5134 mils Ni 1 = 92.273 % P 1 = 7.7266 %')
doc.add_paragraph('')
doc.add_paragraph(' NiP 1 mils Ni 1 % P 1 %')
doc.add_paragraph('Mean 0.5689 92.258 7.7415')
doc.add_paragraph('Standard Deviation 0.1037 0.9282 0.9282')
doc.add_paragraph('CoV (%) 18.22 1.01 11.99')
doc.add_paragraph('Range 0.1836 1.8562 1.8562')
doc.add_paragraph('Number of readings 3 3 3')
doc.add_paragraph('Measuring time 120 sec')
doc.add_paragraph('Operator: BK 4755 1')
doc.add_paragraph('Date: 5/15/2026 Time: 12:24:46 PM')
if with_image:
# Embed a tiny valid 1x1 PNG so the image-extraction path
# is exercised. Bytes from
# https://github.com/mathiasbynens/small/blob/master/png-transparent.png
png = bytes.fromhex(
'89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4'
'890000000a49444154789c63000100000500010d0a2db40000000049454e44ae'
'426082'
)
img_buf = io.BytesIO(png)
doc.add_picture(img_buf)
buf = io.BytesIO()
doc.save(buf)
return buf.getvalue()
# ---- happy path: full Fischerscope export ----------------------------
def test_parse_extracts_three_readings(self):
result = self.fischerscope_parser.parse_fischerscope_file(
'sample.docx', self._make_sample_docx(),
)
self.assertTrue(result['success'])
self.assertEqual(len(result['readings']), 3)
self.assertEqual(result['readings'][0], {
'reading_number': 1,
'nip_mils': 0.6885,
'ni_percent': 91.323,
'p_percent': 8.6771,
})
self.assertEqual(result['readings'][2]['reading_number'], 3)
def test_parse_extracts_metadata(self):
result = self.fischerscope_parser.parse_fischerscope_file(
'sample.docx', self._make_sample_docx(),
)
meta = result['metadata']
self.assertIn('Fischerscope', (meta.get('equipment_model') or ''))
self.assertIn('XDAL 600', (meta.get('equipment_model') or ''))
self.assertEqual(meta.get('product_ref'),
'2805031 / NiP/Al-alloys 2805030')
self.assertEqual(meta.get('calibration_std_ref'),
'NiP/Al STD SET SN 100174568')
self.assertEqual(meta.get('measuring_time_seconds'), 120)
self.assertEqual(meta.get('operator_name'), 'BK')
self.assertEqual(meta.get('reading_datetime'),
datetime(2026, 5, 15, 12, 24, 46))
def test_parse_extracts_image_when_present(self):
result = self.fischerscope_parser.parse_fischerscope_file(
'sample.docx', self._make_sample_docx(with_image=True),
)
self.assertIsNotNone(result['image'])
self.assertGreater(len(result['image']), 50)
# python-docx writes the relationship type to image; mime is content_type.
self.assertTrue((result.get('image_mime') or '').startswith('image/'))
def test_parse_handles_no_image(self):
result = self.fischerscope_parser.parse_fischerscope_file(
'sample.docx', self._make_sample_docx(with_image=False),
)
self.assertIsNone(result['image'])
# ---- fallback / error paths -----------------------------------------
def test_parse_unknown_extension(self):
result = self.fischerscope_parser.parse_fischerscope_file(
'sample.csv', b'irrelevant',
)
self.assertFalse(result['success'])
self.assertEqual(result['readings'], [])
self.assertTrue(result['errors'])
self.assertIn('Unsupported', result['errors'][0])
def test_parse_legacy_doc_extension(self):
result = self.fischerscope_parser.parse_fischerscope_file(
'sample.doc', b'%PDF',
)
self.assertFalse(result['success'])
self.assertIn('.doc', result['errors'][0])
def test_parse_corrupt_docx(self):
result = self.fischerscope_parser.parse_fischerscope_file(
'sample.docx', b'not a real docx file',
)
self.assertFalse(result['success'])
self.assertEqual(result['readings'], [])
self.assertTrue(result['errors'])
def test_parse_empty_docx_no_readings(self):
if not self.docx:
self.skipTest('python-docx not available')
doc = self.docx.Document()
doc.add_paragraph('Just a blank report')
buf = io.BytesIO()
doc.save(buf)
result = self.fischerscope_parser.parse_fischerscope_file(
'blank.docx', buf.getvalue(),
)
self.assertFalse(result['success'])
self.assertEqual(result['readings'], [])
# raw_text should still be populated for debug
self.assertIn('blank report', result['raw_text'])
# ---- robustness: variation in spacing / channel digits --------------
def test_parse_tolerates_whitespace_variation(self):
if not self.docx:
self.skipTest('python-docx not available')
doc = self.docx.Document()
doc.add_paragraph('Calibr. Std. Set TESTSTD SN 999')
# Tighter spacing, no channel digit (some exports omit "1")
doc.add_paragraph('n=1 NiP= 0.50 mils Ni = 92.0 % P = 8.0 %')
# Looser spacing, channel digit "1"
doc.add_paragraph('n = 2 NiP 1 = 0.55 mils Ni 1 = 91.5 % P 1 = 8.5 %')
buf = io.BytesIO()
doc.save(buf)
result = self.fischerscope_parser.parse_fischerscope_file(
'variant.docx', buf.getvalue(),
)
self.assertTrue(result['success'])
self.assertEqual(len(result['readings']), 2)
self.assertAlmostEqual(result['readings'][0]['nip_mils'], 0.50)
self.assertAlmostEqual(result['readings'][1]['nip_mils'], 0.55)