From 4da123c2d3aa1bce4630c5a4e87a9a54ef316c2b Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 17 May 2026 02:49:18 -0400 Subject: [PATCH] feat(portal): _fp_group_documents helper for detail-page doc panel V1 surfaces only the fields directly on fp.portal.job (CoC + packing list). Other 2 groups (From You, Specifications) render placeholder rows. V2 will wire in sale.order linking for full doc surfacing. Also adds _fp_size_label helper for friendly file-size strings. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../controllers/portal.py | 87 +++++++++++++++++++ .../tests/test_portal_dashboard.py | 27 ++++++ 2 files changed, 114 insertions(+) diff --git a/fusion_plating/fusion_plating_portal/controllers/portal.py b/fusion_plating/fusion_plating_portal/controllers/portal.py index cfb064e2..a24690a8 100644 --- a/fusion_plating/fusion_plating_portal/controllers/portal.py +++ b/fusion_plating/fusion_plating_portal/controllers/portal.py @@ -170,6 +170,93 @@ class FpCustomerPortal(CustomerPortal): }) return out + def _fp_group_documents(self, job): + """Build the 4-group document panel for the job detail page. + + V1: surfaces only the directly-attached fields on fp.portal.job + (coc_attachment_id, packing_list_attachment_id). Other 3 groups + render placeholder/pending rows. V2 (separate change) will wire in + sale_order_id, quote_request_id, part_catalog_id sources. + + Returns a list of 4 dicts: {key, label, docs}, where docs is a list + of {label, sub, url, icon_class, icon, pending}. + """ + groups = [ + {'key': 'from_you', 'label': 'From You', 'docs': []}, + {'key': 'specs', 'label': 'Specifications', 'docs': []}, + {'key': 'quality', 'label': 'Quality', 'docs': []}, + {'key': 'shipping', 'label': 'Shipping', 'docs': []}, + ] + + # FROM YOU โ€” V1: placeholder (V2 will pull PO + drawings via SO link) + groups[0]['docs'].append({ + 'label': 'Customer documents', + 'sub': 'Upload your PO and drawings via your sales contact for now', + 'pending': True, + 'icon': '๐Ÿ“„', + }) + + # SPECIFICATIONS โ€” V1: placeholder (V2 will pull customer spec) + groups[1]['docs'].append({ + 'label': 'Customer Specification', + 'sub': 'Will appear when EN Plating links the spec', + 'pending': True, + 'icon': '๐Ÿ“‹', + }) + + # QUALITY โ€” CoC from coc_attachment_id (the legacy direct field) + if job.coc_attachment_id: + groups[2]['docs'].append({ + 'label': 'Certificate of Conformance', + 'sub': 'EN Plating ยท %s ยท %s' % ( + job.actual_ship_date and job.actual_ship_date.strftime('%b %d') or '', + self._fp_size_label(job.coc_attachment_id), + ), + 'url': '/my/jobs/%s/coc' % job.id, + 'icon_class': 'o_fp_doc_icon_quality', + 'icon': '๐Ÿ“‘', + }) + else: + groups[2]['docs'].append({ + 'label': 'Certificate of Conformance', + 'sub': 'Will appear after QC completes', + 'pending': True, + 'icon': '๐Ÿ“‘', + }) + + # SHIPPING โ€” packing list + tracking + if job.packing_list_attachment_id: + groups[3]['docs'].append({ + 'label': 'Packing Slip', + 'sub': 'EN Plating ยท %s ยท %s' % ( + job.actual_ship_date and job.actual_ship_date.strftime('%b %d') or '', + self._fp_size_label(job.packing_list_attachment_id), + ), + 'url': '/web/content/%s?download=true' % job.packing_list_attachment_id.id, + 'icon_class': 'o_fp_doc_icon_shipping', + 'icon': '๐Ÿ“ฆ', + }) + else: + groups[3]['docs'].append({ + 'label': 'Packing Slip ยท Tracking #', + 'sub': 'Available when shipped' + (' โ€” ' + job.tracking_ref if job.tracking_ref else ''), + 'pending': not job.tracking_ref, + 'icon': '๐Ÿ“ฆ', + }) + + return groups + + def _fp_size_label(self, attachment): + """Render attachment.file_size as a friendly KB/MB string.""" + if not attachment or not attachment.file_size: + return '' + size = attachment.file_size + if size < 1024: + return '%d B' % size + if size < 1024 * 1024: + return '%.0f KB' % (size / 1024) + return '%.1f MB' % (size / (1024 * 1024)) + # ========================================================================== # DASHBOARD # ========================================================================== diff --git a/fusion_plating/fusion_plating_portal/tests/test_portal_dashboard.py b/fusion_plating/fusion_plating_portal/tests/test_portal_dashboard.py index 7d8f68cf..275b6bc1 100644 --- a/fusion_plating/fusion_plating_portal/tests/test_portal_dashboard.py +++ b/fusion_plating/fusion_plating_portal/tests/test_portal_dashboard.py @@ -116,3 +116,30 @@ class TestPortalDashboard(TransactionCase): timeline = FpCustomerPortal()._fp_get_stage_timeline(job) statuses = [t['status'] for t in timeline] self.assertEqual(statuses, ['done', 'done', 'done', 'done', 'done']) + + def test_group_documents_v1_returns_4_groups(self): + """V1 doc grouping returns 4 groups; quality populated when CoC set.""" + from odoo.addons.fusion_plating_portal.controllers.portal import FpCustomerPortal + Job = self.env['fusion.plating.portal.job'] + att = self.env['ir.attachment'].create({ + 'name': 'CoC_WO-TEST.pdf', + 'datas': b'', + 'res_model': 'fusion.plating.portal.job', + }) + job = Job.create({ + 'name': 'WO-DOC-001', + 'partner_id': self.partner.id, + 'state': 'shipped', + 'coc_attachment_id': att.id, + }) + groups = FpCustomerPortal()._fp_group_documents(job) + self.assertEqual(len(groups), 4) + keys = [g['key'] for g in groups] + self.assertEqual(keys, ['from_you', 'specs', 'quality', 'shipping']) + # Quality group has the CoC populated (not pending) + quality = next(g for g in groups if g['key'] == 'quality') + self.assertTrue(any(d['label'] == 'Certificate of Conformance' and not d.get('pending') + for d in quality['docs'])) + # From You + Specifications are placeholders in V1 + from_you = next(g for g in groups if g['key'] == 'from_you') + self.assertTrue(all(d.get('pending') for d in from_you['docs']))