From f1273798cdb78ac4de45b47575068ae32a88770c Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 29 May 2026 09:16:36 -0400 Subject: [PATCH] feat(shopfloor): tablet shipping endpoints + /load shipping payload generate_label (sudo'd FedEx machinery) and mark_shipped (as the technician), both enforcing the order-level ship-together gate. /load now returns a shipping block (carrier/service/weight + readiness + any existing label) when the job is awaiting_ship. Co-Authored-By: Claude Opus 4.7 --- .../fusion_plating_shopfloor/__manifest__.py | 2 +- .../controllers/workspace_controller.py | 105 ++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index 8e31dea4..3fa26c47 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Shop Floor', - 'version': '19.0.36.0.3', + 'version': '19.0.36.1.0', 'category': 'Manufacturing/Plating', 'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.', 'description': """ diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py b/fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py index 7b71e3a8..6357b7af 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py @@ -189,6 +189,35 @@ class FpWorkspaceController(http.Controller): } for dmg in rec.damage_ids], }) + # ---- Shipping (awaiting_ship jobs) ------------------------------ + # Spec 2026-05-29. Surfaces the order-level ship-readiness gate + + # the carrier/service/weight inputs + any label already on the + # order's single outbound shipment. job is already sudo() here, so + # the helper's reads are fine for display. ship_rec (not rec) to + # avoid shadowing the receivings loop variable above. + shipping_payload = None + if job.state == 'awaiting_ship': + info = job._fp_order_ship_state() + ship_rec = info['order_receiving'] + shipment = ship_rec.x_fc_outbound_shipment_id if ship_rec else False + carriers = env['delivery.carrier'].sudo().search([], limit=50) + shipping_payload = { + 'ready': info['ready'], + 'not_ready': info['not_ready'], + 'receiving_id': ship_rec.id if ship_rec else False, + 'carrier_id': (ship_rec.x_fc_carrier_id.id + if ship_rec and ship_rec.x_fc_carrier_id else False), + 'carrier_options': [{'id': c.id, 'name': c.name} for c in carriers], + 'service_type': (ship_rec.x_fc_outbound_service_type or '') if ship_rec else '', + 'service_options': env['fp.receiving']._fp_get_service_type_selection(), + 'weight': (ship_rec.x_fc_weight or 0.0) if ship_rec else 0.0, + 'has_label': bool(ship_rec.x_fc_has_label) if ship_rec else False, + 'tracking_number': (shipment.tracking_number or '') if shipment else '', + 'label_attachment_id': (shipment.label_attachment_id.id + if shipment and shipment.label_attachment_id + else False), + } + return { 'ok': True, 'job': { @@ -246,6 +275,7 @@ class FpWorkspaceController(http.Controller): ], 'required_certs': required_certs, 'receivings': receivings_payload, + 'shipping': shipping_payload, # 2026-05-24 — is_manager surfaces to the JS so it can offer # the manager-bypass affordance (e.g. on the required-inputs # gate dialog). Server-side endpoints re-check the group @@ -622,3 +652,78 @@ class FpWorkspaceController(http.Controller): _logger.exception("workspace/damage_delete failed") return {'ok': False, 'error': str(exc)} return {'ok': True} + + # ====================================================================== + # Shipping — generate outbound label + mark shipped (2026-05-29) + # ====================================================================== + # Spec D3/D4. mark_shipped runs as the technician (real chatter + # attribution; the method has no group gate). generate_label sudo's + # the carrier/stock/shipment machinery — technicians intentionally + # don't hold those ACLs. Both re-check the order-level "ship together" + # gate server-side via fp.job._fp_order_ship_state. + + @http.route('/fp/workspace/mark_shipped', type='jsonrpc', auth='user') + def mark_shipped(self, job_id): + env = request.env + job = env['fp.job'].browse(int(job_id)) # as the tech + if not job.exists(): + return {'ok': False, 'error': f'Job {job_id} not found'} + try: + shipped = job._fp_mark_order_shipped() + except UserError as e: + return {'ok': False, 'error': str(e.args[0]) if e.args else str(e)} + except Exception as exc: + _logger.exception("workspace/mark_shipped failed") + return {'ok': False, 'error': str(exc)} + return {'ok': True, 'shipped': shipped.mapped('display_wo_name')} + + @http.route('/fp/workspace/generate_label', type='jsonrpc', auth='user') + def generate_label(self, job_id, weight, service_type='', carrier_id=False): + env = request.env + job = env['fp.job'].browse(int(job_id)) + if not job.exists(): + return {'ok': False, 'error': f'Job {job_id} not found'} + info = job._fp_order_ship_state() + if not info['ready']: + names = ', '.join(n['wo_name'] for n in info['not_ready']) or 'none' + return {'ok': False, + 'error': 'Not all jobs on the order are finished: %s' % names, + 'not_ready': info['not_ready']} + rec = info['order_receiving'] + if not rec: + return {'ok': False, + 'error': 'No receiving record on this order to ship from.'} + try: + w = float(weight or 0) + except (TypeError, ValueError): + w = 0.0 + if w <= 0: + return {'ok': False, + 'error': 'Enter a non-zero weight before generating the label.'} + # sudo: carrier write triggers delivery.carrier read; the actual + # generate synthesizes a stock.picking + fusion.shipment + label + # attachment — all privileged. The carrier choice came from the + # sudo'd options list in /load, so this is safe. + rec = rec.sudo() + vals = {'x_fc_weight': w, + 'x_fc_outbound_service_type': service_type or False} + if carrier_id: + vals['x_fc_carrier_id'] = int(carrier_id) + try: + rec.write(vals) + rec._fp_actually_generate_outbound_label() + except UserError as e: + return {'ok': False, 'error': str(e.args[0]) if e.args else str(e)} + except Exception as exc: + _logger.exception("workspace/generate_label failed") + return {'ok': False, 'error': 'Label generation failed: %s' % exc} + # The model returns a manual-wizard action (no raise) on API + # failure — so success is "a label landed on the shipment". + shipment = rec.x_fc_outbound_shipment_id + if not (shipment and shipment.label_attachment_id): + return {'ok': False, 'error': ( + 'The carrier did not return a label. Ask the office to ' + 'generate it from the receiving record.')} + return {'ok': True, + 'tracking_number': shipment.tracking_number or '', + 'label_attachment_id': shipment.label_attachment_id.id}