From efaf16dffb4418ae783e7d66ce29728055540f1a Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 23 May 2026 00:43:44 -0400 Subject: [PATCH] feat(shopfloor): propagate tablet_tech_id to shopfloor + manager action endpoints (P6.3.3 + P6.3.4) 10 endpoints in shopfloor_controller (log_chemistry, start_bake, end_bake, start_wo, stop_wo, bump_qty_done, bump_qty_scrapped, log_thickness_reading, quality_hold, mark_gate) and 3 in manager_controller (assign_worker, assign_tank, take_over) now accept a `tablet_tech_id` kwarg. Each rebinds env via env_for_tablet_tech() so writes carry the correct uid even when the OS session belongs to the persistent tablet user. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../controllers/manager_controller.py | 22 +++-- .../controllers/shopfloor_controller.py | 84 ++++++++++++------- 2 files changed, 69 insertions(+), 37 deletions(-) diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/manager_controller.py b/fusion_plating/fusion_plating_shopfloor/controllers/manager_controller.py index 6bedec7a..5c6063c0 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/manager_controller.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/manager_controller.py @@ -25,6 +25,8 @@ from odoo import fields, http from odoo.addons.fusion_plating.models.fp_tz import fp_format from odoo.http import request +from ._tablet_audit import env_for_tablet_tech + _logger = logging.getLogger(__name__) @@ -387,7 +389,8 @@ class FpManagerDashboardController(http.Controller): # Assign a worker to a step # ------------------------------------------------------------------ @http.route('/fp/manager/assign_worker', type='jsonrpc', auth='user') - def assign_worker(self, step_id=None, user_id=None, workorder_id=None, **kwargs): + def assign_worker(self, step_id=None, user_id=None, workorder_id=None, + tablet_tech_id=None, **kwargs): """Assign an operator to a step. ``step_id`` is the canonical kwarg; ``workorder_id`` is accepted as a deprecated alias for one release so any caller we missed doesn't break. @@ -400,7 +403,8 @@ class FpManagerDashboardController(http.Controller): step_id = workorder_id if not step_id: return {'ok': False, 'error': 'step_id required'} - step = request.env['fp.job.step'].browse(int(step_id)) + env = env_for_tablet_tech(request.env, tablet_tech_id) + step = env['fp.job.step'].browse(int(step_id)) if not step.exists(): return {'ok': False, 'error': 'Step not found.'} step.assigned_user_id = int(user_id) if user_id else False @@ -415,7 +419,8 @@ class FpManagerDashboardController(http.Controller): # Reassign or swap tank on a step # ------------------------------------------------------------------ @http.route('/fp/manager/assign_tank', type='jsonrpc', auth='user') - def assign_tank(self, step_id=None, tank_id=None, workorder_id=None, **kwargs): + def assign_tank(self, step_id=None, tank_id=None, workorder_id=None, + tablet_tech_id=None, **kwargs): """Swap the tank on a step. ``step_id`` is the canonical kwarg; ``workorder_id`` is accepted as a deprecated alias. """ @@ -427,7 +432,8 @@ class FpManagerDashboardController(http.Controller): step_id = workorder_id if not step_id: return {'ok': False, 'error': 'step_id required'} - step = request.env['fp.job.step'].browse(int(step_id)) + env = env_for_tablet_tech(request.env, tablet_tech_id) + step = env['fp.job.step'].browse(int(step_id)) if not step.exists(): return {'ok': False, 'error': 'Step not found.'} step.tank_id = int(tank_id) if tank_id else False @@ -442,7 +448,8 @@ class FpManagerDashboardController(http.Controller): # Manager takes over a step (no-show coverage) # ------------------------------------------------------------------ @http.route('/fp/manager/take_over', type='jsonrpc', auth='user') - def take_over(self, step_id=None, workorder_id=None, **kwargs): + def take_over(self, step_id=None, workorder_id=None, + tablet_tech_id=None, **kwargs): """Manager takes over a step. ``step_id`` is the canonical kwarg; ``workorder_id`` is accepted as a deprecated alias. """ @@ -454,10 +461,11 @@ class FpManagerDashboardController(http.Controller): step_id = workorder_id if not step_id: return {'ok': False, 'error': 'step_id required'} - step = request.env['fp.job.step'].browse(int(step_id)) + env = env_for_tablet_tech(request.env, tablet_tech_id) + step = env['fp.job.step'].browse(int(step_id)) if not step.exists(): return {'ok': False, 'error': 'Step not found.'} - user = request.env.user + user = env.user previous = step.assigned_user_id.name or '—' step.assigned_user_id = user.id step.message_post( diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py b/fusion_plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py index 75bfa06f..f49f0020 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py @@ -20,6 +20,8 @@ from odoo.addons.fusion_plating.models.fp_tz import ( from odoo.exceptions import UserError from odoo.http import request +from ._tablet_audit import env_for_tablet_tech + _logger = logging.getLogger(__name__) @@ -255,11 +257,13 @@ class FpShopfloorController(http.Controller): # Quick chemistry log from the tablet # ---------------------------------------------------------------------- @http.route('/fp/shopfloor/log_chemistry', type='jsonrpc', auth='user') - def log_chemistry(self, bath_id, readings, shift=None, notes=None): + def log_chemistry(self, bath_id, readings, shift=None, notes=None, + tablet_tech_id=None): """Create a fusion.plating.bath.log with one line per reading.""" + env = env_for_tablet_tech(request.env, tablet_tech_id) if not bath_id: raise UserError("bath_id required") - bath = request.env['fusion.plating.bath'].browse(int(bath_id)) + bath = env['fusion.plating.bath'].browse(int(bath_id)) if not bath.exists(): raise UserError(f"Bath {bath_id} not found") @@ -274,7 +278,7 @@ class FpShopfloorController(http.Controller): 'value': float(value) if value not in (None, '') else 0.0, })) - log = request.env['fusion.plating.bath.log'].create({ + log = env['fusion.plating.bath.log'].create({ 'bath_id': bath.id, 'shift': shift or False, 'notes': notes or False, @@ -291,10 +295,11 @@ class FpShopfloorController(http.Controller): # Bake window controls # ---------------------------------------------------------------------- @http.route('/fp/shopfloor/start_bake', type='jsonrpc', auth='user') - def start_bake(self, bake_window_id, oven_id=None): + def start_bake(self, bake_window_id, oven_id=None, tablet_tech_id=None): # action_start_bake raises UserError for S6 missed_window. Wrap # the same way as start_wo so operator gets a clean flash. - bw = request.env['fusion.plating.bake.window'].browse(int(bake_window_id)) + env = env_for_tablet_tech(request.env, tablet_tech_id) + bw = env['fusion.plating.bake.window'].browse(int(bake_window_id)) if not bw.exists(): return {'ok': False, 'error': f'Bake window {bake_window_id} not found'} if oven_id: @@ -306,12 +311,13 @@ class FpShopfloorController(http.Controller): return { 'ok': True, 'state': bw.state, - 'bake_start_time': fp_format(request.env, bw.bake_start_time), + 'bake_start_time': fp_format(env, bw.bake_start_time), } @http.route('/fp/shopfloor/end_bake', type='jsonrpc', auth='user') - def end_bake(self, bake_window_id): - bw = request.env['fusion.plating.bake.window'].browse(int(bake_window_id)) + def end_bake(self, bake_window_id, tablet_tech_id=None): + env = env_for_tablet_tech(request.env, tablet_tech_id) + bw = env['fusion.plating.bake.window'].browse(int(bake_window_id)) if not bw.exists(): return {'ok': False, 'error': f'Bake window {bake_window_id} not found'} try: @@ -321,7 +327,7 @@ class FpShopfloorController(http.Controller): return { 'ok': True, 'state': bw.state, - 'bake_end_time': fp_format(request.env, bw.bake_end_time), + 'bake_end_time': fp_format(env, bw.bake_end_time), 'bake_duration_hours': bw.bake_duration_hours, } @@ -340,8 +346,15 @@ class FpShopfloorController(http.Controller): step = request.env['fp.job.step'].browse(int(sid)) return step if step.exists() else False + def _resolve_step_in_env(self, env, step_id=None, workorder_id=None): + sid = step_id if step_id else workorder_id + if not sid: + return False + step = env['fp.job.step'].browse(int(sid)) + return step if step.exists() else False + @http.route('/fp/shopfloor/start_wo', type='jsonrpc', auth='user') - def start_wo(self, workorder_id=None, step_id=None): + def start_wo(self, workorder_id=None, step_id=None, tablet_tech_id=None): """Start the timer on a fp.job.step (called from the tablet). button_start() can raise UserError for any guarded condition @@ -350,7 +363,8 @@ class FpShopfloorController(http.Controller): the explicit state check, so the tablet flashes a clean toast instead of popping a stack-trace dialog at the operator. """ - step = self._resolve_step(step_id, workorder_id) + env = env_for_tablet_tech(request.env, tablet_tech_id) + step = self._resolve_step_in_env(env, step_id, workorder_id) if not step: return {'ok': False, 'error': 'Step not found'} if not _step_can_start(step): @@ -369,7 +383,8 @@ class FpShopfloorController(http.Controller): } @http.route('/fp/shopfloor/stop_wo', type='jsonrpc', auth='user') - def stop_wo(self, workorder_id=None, step_id=None, finish=False): + def stop_wo(self, workorder_id=None, step_id=None, finish=False, + tablet_tech_id=None): """Finish the timer on a fp.job.step. finish=True calls button_finish(); other values are no-ops for @@ -380,7 +395,8 @@ class FpShopfloorController(http.Controller): not provided). Wrapped same as start_wo so the operator gets a clean flash, not a stack-trace dialog. """ - step = self._resolve_step(step_id, workorder_id) + env = env_for_tablet_tech(request.env, tablet_tech_id) + step = self._resolve_step_in_env(env, step_id, workorder_id) if not step: return {'ok': False, 'error': 'Step not found'} if finish: @@ -409,11 +425,12 @@ class FpShopfloorController(http.Controller): # both with a single tap. Scrap auto-spawns a hold via fp.job.write # (S17 hook, no extra wiring needed here). @http.route('/fp/shopfloor/bump_qty_done', type='jsonrpc', auth='user') - def bump_qty_done(self, job_id, delta=1): + def bump_qty_done(self, job_id, delta=1, tablet_tech_id=None): """Increment job.qty_done by `delta` (defaults to +1). Returns the new totals so the tablet can update without a full refresh. """ - job = request.env['fp.job'].browse(int(job_id)) + env = env_for_tablet_tech(request.env, tablet_tech_id) + job = env['fp.job'].browse(int(job_id)) if not job.exists(): return {'ok': False, 'error': 'Job not found'} try: @@ -433,13 +450,15 @@ class FpShopfloorController(http.Controller): } @http.route('/fp/shopfloor/bump_qty_scrapped', type='jsonrpc', auth='user') - def bump_qty_scrapped(self, job_id, delta=1, reason=None): + def bump_qty_scrapped(self, job_id, delta=1, reason=None, + tablet_tech_id=None): """Increment job.qty_scrapped by `delta`. The S17 write-hook on fp.job auto-spawns a fusion.plating.quality.hold for the delta; the operator can edit the description on that hold later. `reason` is optional — passed through to the hold's description. """ - job = request.env['fp.job'].browse(int(job_id)) + env = env_for_tablet_tech(request.env, tablet_tech_id) + job = env['fp.job'].browse(int(job_id)) if not job.exists(): return {'ok': False, 'error': 'Job not found'} try: @@ -470,20 +489,22 @@ class FpShopfloorController(http.Controller): position_label=None, reading_number=None, equipment_model=None, calibration_std_ref=None, microscope_image=None, - microscope_image_filename=None): + microscope_image_filename=None, + tablet_tech_id=None): """Record a single Fischerscope reading against a job. `job_id` is the canonical kwarg; `production_id` is accepted as an alias for older clients. The reading auto-links to an existing CoC certificate for the job when one exists. """ - Reading = request.env.get('fp.thickness.reading') + env = env_for_tablet_tech(request.env, tablet_tech_id) + Reading = env.get('fp.thickness.reading') if Reading is None: return {'ok': False, 'error': 'Certificates module not installed'} target_id = job_id or production_id if not target_id: return {'ok': False, 'error': 'job_id required'} - job = request.env['fp.job'].browse(int(target_id)) + job = env['fp.job'].browse(int(target_id)) if not job.exists(): return {'ok': False, 'error': f'Job {target_id} not found'} @@ -508,7 +529,7 @@ class FpShopfloorController(http.Controller): 'ni_percent': float(ni_percent or 0.0), 'p_percent': float(p_percent or 0.0), 'position_label': position_label or '', - 'operator_id': request.env.user.id, + 'operator_id': env.user.id, } if equipment_model: @@ -516,7 +537,7 @@ class FpShopfloorController(http.Controller): if calibration_std_ref: vals['calibration_std_ref'] = calibration_std_ref if microscope_image: - att = request.env['ir.attachment'].create({ + att = env['ir.attachment'].create({ 'name': microscope_image_filename or f'thickness_{reading_number}.jpg', 'datas': microscope_image, 'res_model': 'fp.thickness.reading', @@ -525,7 +546,7 @@ class FpShopfloorController(http.Controller): vals['microscope_image_id'] = att.id # Auto-link to an existing CoC if there is one for this job. - Cert = request.env.get('fp.certificate') + Cert = env.get('fp.certificate') if Cert is not None: if 'x_fc_job_id' in Cert._fields: cert_field = 'x_fc_job_id' @@ -557,7 +578,8 @@ class FpShopfloorController(http.Controller): part_ref=None, qty_on_hold=0, qty_original=0, hold_reason='other', description=None, mark_for_scrap=False, facility_id=None, - work_center_id=None, current_process_node=None): + work_center_id=None, current_process_node=None, + tablet_tech_id=None): """Create a quality hold record, splitting qty from the original lot. The hold is linked to the fp.job and (when provided) the @@ -566,7 +588,8 @@ class FpShopfloorController(http.Controller): if not qty_on_hold or int(qty_on_hold) <= 0: raise UserError("qty_on_hold must be a positive integer.") - Hold = request.env['fusion.plating.quality.hold'] + env = env_for_tablet_tech(request.env, tablet_tech_id) + Hold = env['fusion.plating.quality.hold'] vals = { 'part_ref': part_ref or '', @@ -583,7 +606,7 @@ class FpShopfloorController(http.Controller): if work_center_id: vals['work_center_id'] = int(work_center_id) if portal_job_id: - pj = request.env['fusion.plating.portal.job'].browse( + pj = env['fusion.plating.portal.job'].browse( int(portal_job_id), ) if pj.exists(): @@ -594,7 +617,7 @@ class FpShopfloorController(http.Controller): # via fusion_plating_jobs (Phase 3) as `x_fc_job_id` / `x_fc_step_id`. step_target_id = step_id or workorder_id if step_target_id: - step = request.env['fp.job.step'].browse(int(step_target_id)) + step = env['fp.job.step'].browse(int(step_target_id)) if step.exists(): if 'x_fc_step_id' in Hold._fields: vals['x_fc_step_id'] = step.id @@ -605,7 +628,7 @@ class FpShopfloorController(http.Controller): # set it through the step. if (job_id and 'x_fc_job_id' in Hold._fields and not vals.get('x_fc_job_id')): - j = request.env['fp.job'].browse(int(job_id)) + j = env['fp.job'].browse(int(job_id)) if j.exists(): vals['x_fc_job_id'] = j.id @@ -995,8 +1018,9 @@ class FpShopfloorController(http.Controller): # Mark a first-piece gate result from the tablet # ---------------------------------------------------------------------- @http.route('/fp/shopfloor/mark_gate', type='jsonrpc', auth='user') - def mark_gate(self, gate_id, result): - gate = request.env['fusion.plating.first.piece.gate'].browse(int(gate_id)) + def mark_gate(self, gate_id, result, tablet_tech_id=None): + env = env_for_tablet_tech(request.env, tablet_tech_id) + gate = env['fusion.plating.first.piece.gate'].browse(int(gate_id)) if not gate.exists(): return {'ok': False, 'error': 'Gate not found.'} try: