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) <noreply@anthropic.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user