Compare commits
5 Commits
phase6_2-l
...
phase6_3-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27e12dd544 | ||
|
|
5f03080374 | ||
|
|
efaf16dffb | ||
|
|
e4000374ca | ||
|
|
fee4219703 |
@@ -166,6 +166,19 @@ These modules have **source code in this repo** but are **intentionally NOT inst
|
|||||||
| `fusion_plating_culture` | `state=uninstalled`, dir removed from entech disk | Soft people-ops feature (peer kudos / "Fundamental of the Week"); zero data entered; not a client priority. Top-level "Culture" menu confused operators. | Ask the client whether they want it before reinstalling. If yes: re-sync folder + `-i fusion_plating_culture` + seed a value set. |
|
| `fusion_plating_culture` | `state=uninstalled`, dir removed from entech disk | Soft people-ops feature (peer kudos / "Fundamental of the Week"); zero data entered; not a client priority. Top-level "Culture" menu confused operators. | Ask the client whether they want it before reinstalling. If yes: re-sync folder + `-i fusion_plating_culture` + seed a value set. |
|
||||||
| `fusion_plating_sensors` | deleted entirely (not in repo anymore) | Duplicated `fusion_plating_iot`'s scope but with no working alerting logic. Its valuables (sensor_type taxonomy, dashboard, location flexibility) were ported into `fusion_iot/fusion_plating_iot/`. | N/A — gone. Any new sensor work goes in `fusion_iot/fusion_plating_iot/`. |
|
| `fusion_plating_sensors` | deleted entirely (not in repo anymore) | Duplicated `fusion_plating_iot`'s scope but with no working alerting logic. Its valuables (sensor_type taxonomy, dashboard, location flexibility) were ported into `fusion_iot/fusion_plating_iot/`. | N/A — gone. Any new sensor work goes in `fusion_iot/fusion_plating_iot/`. |
|
||||||
|
|
||||||
|
## Shop-floor action endpoints — credit the correct tech via `tablet_tech_id`
|
||||||
|
The tablet sits on a long-lived "shopfloor service" Odoo session shared by many techs. The actual tech-of-record is established via the PIN unlock (Phase 6); their id lives in the OWL `fp_shopfloor_tech_store` service and is sent as `tablet_tech_id` on every action RPC.
|
||||||
|
|
||||||
|
When writing a NEW shop-floor controller endpoint that **writes** (creates a record, calls a `button_*` method, posts to chatter):
|
||||||
|
1. Add `tablet_tech_id=None` as a kwarg on the route handler.
|
||||||
|
2. At the top, call: `env = env_for_tablet_tech(request.env, tablet_tech_id)` (from `fusion_plating_shopfloor/controllers/_tablet_audit.py`).
|
||||||
|
3. Use `env` (not `request.env`) for all subsequent writes. `env.with_user(...)` is applied internally so `create_uid` / `write_uid` / chatter authorship carry the right uid.
|
||||||
|
4. Read-only endpoints (load / kanban / funnel / overview) don't need this — leave them as `request.env`.
|
||||||
|
|
||||||
|
On the client side: use `fpRpc()` from `services/fp_rpc.js` (drop-in for `rpc()`) for action calls. It auto-injects `tablet_tech_id`. Read calls can keep using plain `rpc()`.
|
||||||
|
|
||||||
|
If `tablet_tech_id` is missing or invalid, `env_for_tablet_tech` falls back to the session uid — old callers and pre-Phase-6.3 endpoints continue working.
|
||||||
|
|
||||||
## Removing menus/records — Odoo does NOT auto-delete orphans
|
## Removing menus/records — Odoo does NOT auto-delete orphans
|
||||||
Deleting a `<menuitem>` (or any `<record>`) from a data XML file does NOT remove the corresponding database row. The XML loader only updates records it sees; orphans persist in `ir.ui.menu` / `ir.model.data` until you delete them explicitly. Symptom: the menu still appears in the UI after `-u`. Fix — add a `<delete>` directive in a data file with `noupdate="0"`:
|
Deleting a `<menuitem>` (or any `<record>`) from a data XML file does NOT remove the corresponding database row. The XML loader only updates records it sees; orphans persist in `ir.ui.menu` / `ir.model.data` until you delete them explicitly. Symptom: the menu still appears in the UI after `-u`. Fix — add a `<delete>` directive in a data file with `noupdate="0"`:
|
||||||
```xml
|
```xml
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Shop Floor',
|
'name': 'Fusion Plating — Shop Floor',
|
||||||
'version': '19.0.30.1.0',
|
'version': '19.0.30.2.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, '
|
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, '
|
||||||
'first-piece inspection gates.',
|
'first-piece inspection gates.',
|
||||||
@@ -85,6 +85,10 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
# ---- Phase 6.2 tablet PIN gate ----
|
# ---- Phase 6.2 tablet PIN gate ----
|
||||||
'fusion_plating_shopfloor/static/src/js/services/tech_store.js',
|
'fusion_plating_shopfloor/static/src/js/services/tech_store.js',
|
||||||
'fusion_plating_shopfloor/static/src/js/services/activity_tracker.js',
|
'fusion_plating_shopfloor/static/src/js/services/activity_tracker.js',
|
||||||
|
# Phase 6.3 — fpRpc wrapper. MUST load before any consumer
|
||||||
|
# (job_workspace, shopfloor_landing, manager_dashboard,
|
||||||
|
# hold_composer) so `import { fpRpc }` resolves.
|
||||||
|
'fusion_plating_shopfloor/static/src/js/services/fp_rpc.js',
|
||||||
'fusion_plating_shopfloor/static/src/scss/components/_pin_pad.scss',
|
'fusion_plating_shopfloor/static/src/scss/components/_pin_pad.scss',
|
||||||
'fusion_plating_shopfloor/static/src/xml/components/pin_pad.xml',
|
'fusion_plating_shopfloor/static/src/xml/components/pin_pad.xml',
|
||||||
'fusion_plating_shopfloor/static/src/js/components/pin_pad.js',
|
'fusion_plating_shopfloor/static/src/js/components/pin_pad.js',
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
"""Helper for audit-credit propagation (Phase 6.3 tablet redesign).
|
||||||
|
|
||||||
|
Controllers that accept an optional `tablet_tech_id` kwarg use this
|
||||||
|
helper to switch their `env` to the tech-of-record before performing
|
||||||
|
writes. The result: chatter posts + create_uid/write_uid carry the
|
||||||
|
unlocked tech's identity, not the tablet's persistent session user.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def env_for_tablet_tech(env, tablet_tech_id):
|
||||||
|
"""Return an env scoped to `tablet_tech_id` if it's a valid user;
|
||||||
|
otherwise return the original env unchanged.
|
||||||
|
|
||||||
|
Validation: the user must exist and be active. We deliberately do
|
||||||
|
NOT cross-check that they actually unlocked recently — the OWL
|
||||||
|
component is the source of truth for "who's at the tablet right
|
||||||
|
now", and the only path that produces a tablet_tech_id is a
|
||||||
|
successful /fp/tablet/unlock followed by an active session in the
|
||||||
|
OWL tech_store.
|
||||||
|
"""
|
||||||
|
if not tablet_tech_id:
|
||||||
|
return env
|
||||||
|
try:
|
||||||
|
tech_id = int(tablet_tech_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return env
|
||||||
|
User = env['res.users'].sudo()
|
||||||
|
tech = User.browse(tech_id)
|
||||||
|
if not tech.exists() or not tech.active:
|
||||||
|
_logger.warning(
|
||||||
|
"tablet_tech_id %s invalid (not found or inactive); "
|
||||||
|
"falling back to session uid %s",
|
||||||
|
tablet_tech_id, env.uid,
|
||||||
|
)
|
||||||
|
return env
|
||||||
|
return env(user=tech_id)
|
||||||
@@ -25,6 +25,8 @@ from odoo import fields, http
|
|||||||
from odoo.addons.fusion_plating.models.fp_tz import fp_format
|
from odoo.addons.fusion_plating.models.fp_tz import fp_format
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
|
|
||||||
|
from ._tablet_audit import env_for_tablet_tech
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -387,7 +389,8 @@ class FpManagerDashboardController(http.Controller):
|
|||||||
# Assign a worker to a step
|
# Assign a worker to a step
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@http.route('/fp/manager/assign_worker', type='jsonrpc', auth='user')
|
@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
|
"""Assign an operator to a step. ``step_id`` is the canonical
|
||||||
kwarg; ``workorder_id`` is accepted as a deprecated alias for
|
kwarg; ``workorder_id`` is accepted as a deprecated alias for
|
||||||
one release so any caller we missed doesn't break.
|
one release so any caller we missed doesn't break.
|
||||||
@@ -400,7 +403,8 @@ class FpManagerDashboardController(http.Controller):
|
|||||||
step_id = workorder_id
|
step_id = workorder_id
|
||||||
if not step_id:
|
if not step_id:
|
||||||
return {'ok': False, 'error': 'step_id required'}
|
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():
|
if not step.exists():
|
||||||
return {'ok': False, 'error': 'Step not found.'}
|
return {'ok': False, 'error': 'Step not found.'}
|
||||||
step.assigned_user_id = int(user_id) if user_id else False
|
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
|
# Reassign or swap tank on a step
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@http.route('/fp/manager/assign_tank', type='jsonrpc', auth='user')
|
@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;
|
"""Swap the tank on a step. ``step_id`` is the canonical kwarg;
|
||||||
``workorder_id`` is accepted as a deprecated alias.
|
``workorder_id`` is accepted as a deprecated alias.
|
||||||
"""
|
"""
|
||||||
@@ -427,7 +432,8 @@ class FpManagerDashboardController(http.Controller):
|
|||||||
step_id = workorder_id
|
step_id = workorder_id
|
||||||
if not step_id:
|
if not step_id:
|
||||||
return {'ok': False, 'error': 'step_id required'}
|
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():
|
if not step.exists():
|
||||||
return {'ok': False, 'error': 'Step not found.'}
|
return {'ok': False, 'error': 'Step not found.'}
|
||||||
step.tank_id = int(tank_id) if tank_id else False
|
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)
|
# Manager takes over a step (no-show coverage)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@http.route('/fp/manager/take_over', type='jsonrpc', auth='user')
|
@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;
|
"""Manager takes over a step. ``step_id`` is the canonical kwarg;
|
||||||
``workorder_id`` is accepted as a deprecated alias.
|
``workorder_id`` is accepted as a deprecated alias.
|
||||||
"""
|
"""
|
||||||
@@ -454,10 +461,11 @@ class FpManagerDashboardController(http.Controller):
|
|||||||
step_id = workorder_id
|
step_id = workorder_id
|
||||||
if not step_id:
|
if not step_id:
|
||||||
return {'ok': False, 'error': 'step_id required'}
|
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():
|
if not step.exists():
|
||||||
return {'ok': False, 'error': 'Step not found.'}
|
return {'ok': False, 'error': 'Step not found.'}
|
||||||
user = request.env.user
|
user = env.user
|
||||||
previous = step.assigned_user_id.name or '—'
|
previous = step.assigned_user_id.name or '—'
|
||||||
step.assigned_user_id = user.id
|
step.assigned_user_id = user.id
|
||||||
step.message_post(
|
step.message_post(
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ from odoo.addons.fusion_plating.models.fp_tz import (
|
|||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
|
|
||||||
|
from ._tablet_audit import env_for_tablet_tech
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -255,11 +257,13 @@ class FpShopfloorController(http.Controller):
|
|||||||
# Quick chemistry log from the tablet
|
# Quick chemistry log from the tablet
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@http.route('/fp/shopfloor/log_chemistry', type='jsonrpc', auth='user')
|
@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."""
|
"""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:
|
if not bath_id:
|
||||||
raise UserError("bath_id required")
|
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():
|
if not bath.exists():
|
||||||
raise UserError(f"Bath {bath_id} not found")
|
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,
|
'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,
|
'bath_id': bath.id,
|
||||||
'shift': shift or False,
|
'shift': shift or False,
|
||||||
'notes': notes or False,
|
'notes': notes or False,
|
||||||
@@ -291,10 +295,11 @@ class FpShopfloorController(http.Controller):
|
|||||||
# Bake window controls
|
# Bake window controls
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@http.route('/fp/shopfloor/start_bake', type='jsonrpc', auth='user')
|
@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
|
# action_start_bake raises UserError for S6 missed_window. Wrap
|
||||||
# the same way as start_wo so operator gets a clean flash.
|
# 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():
|
if not bw.exists():
|
||||||
return {'ok': False, 'error': f'Bake window {bake_window_id} not found'}
|
return {'ok': False, 'error': f'Bake window {bake_window_id} not found'}
|
||||||
if oven_id:
|
if oven_id:
|
||||||
@@ -306,12 +311,13 @@ class FpShopfloorController(http.Controller):
|
|||||||
return {
|
return {
|
||||||
'ok': True,
|
'ok': True,
|
||||||
'state': bw.state,
|
'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')
|
@http.route('/fp/shopfloor/end_bake', type='jsonrpc', auth='user')
|
||||||
def end_bake(self, bake_window_id):
|
def end_bake(self, bake_window_id, tablet_tech_id=None):
|
||||||
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():
|
if not bw.exists():
|
||||||
return {'ok': False, 'error': f'Bake window {bake_window_id} not found'}
|
return {'ok': False, 'error': f'Bake window {bake_window_id} not found'}
|
||||||
try:
|
try:
|
||||||
@@ -321,7 +327,7 @@ class FpShopfloorController(http.Controller):
|
|||||||
return {
|
return {
|
||||||
'ok': True,
|
'ok': True,
|
||||||
'state': bw.state,
|
'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,
|
'bake_duration_hours': bw.bake_duration_hours,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,8 +346,15 @@ class FpShopfloorController(http.Controller):
|
|||||||
step = request.env['fp.job.step'].browse(int(sid))
|
step = request.env['fp.job.step'].browse(int(sid))
|
||||||
return step if step.exists() else False
|
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')
|
@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).
|
"""Start the timer on a fp.job.step (called from the tablet).
|
||||||
|
|
||||||
button_start() can raise UserError for any guarded condition
|
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
|
the explicit state check, so the tablet flashes a clean toast
|
||||||
instead of popping a stack-trace dialog at the operator.
|
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:
|
if not step:
|
||||||
return {'ok': False, 'error': 'Step not found'}
|
return {'ok': False, 'error': 'Step not found'}
|
||||||
if not _step_can_start(step):
|
if not _step_can_start(step):
|
||||||
@@ -369,7 +383,8 @@ class FpShopfloorController(http.Controller):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@http.route('/fp/shopfloor/stop_wo', type='jsonrpc', auth='user')
|
@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 the timer on a fp.job.step.
|
||||||
|
|
||||||
finish=True calls button_finish(); other values are no-ops for
|
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
|
not provided). Wrapped same as start_wo so the operator gets a
|
||||||
clean flash, not a stack-trace dialog.
|
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:
|
if not step:
|
||||||
return {'ok': False, 'error': 'Step not found'}
|
return {'ok': False, 'error': 'Step not found'}
|
||||||
if finish:
|
if finish:
|
||||||
@@ -409,11 +425,12 @@ class FpShopfloorController(http.Controller):
|
|||||||
# both with a single tap. Scrap auto-spawns a hold via fp.job.write
|
# both with a single tap. Scrap auto-spawns a hold via fp.job.write
|
||||||
# (S17 hook, no extra wiring needed here).
|
# (S17 hook, no extra wiring needed here).
|
||||||
@http.route('/fp/shopfloor/bump_qty_done', type='jsonrpc', auth='user')
|
@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).
|
"""Increment job.qty_done by `delta` (defaults to +1).
|
||||||
Returns the new totals so the tablet can update without a full refresh.
|
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():
|
if not job.exists():
|
||||||
return {'ok': False, 'error': 'Job not found'}
|
return {'ok': False, 'error': 'Job not found'}
|
||||||
try:
|
try:
|
||||||
@@ -433,13 +450,15 @@ class FpShopfloorController(http.Controller):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@http.route('/fp/shopfloor/bump_qty_scrapped', type='jsonrpc', auth='user')
|
@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
|
"""Increment job.qty_scrapped by `delta`. The S17 write-hook on
|
||||||
fp.job auto-spawns a fusion.plating.quality.hold for the delta;
|
fp.job auto-spawns a fusion.plating.quality.hold for the delta;
|
||||||
the operator can edit the description on that hold later.
|
the operator can edit the description on that hold later.
|
||||||
`reason` is optional — passed through to the hold's description.
|
`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():
|
if not job.exists():
|
||||||
return {'ok': False, 'error': 'Job not found'}
|
return {'ok': False, 'error': 'Job not found'}
|
||||||
try:
|
try:
|
||||||
@@ -470,20 +489,22 @@ class FpShopfloorController(http.Controller):
|
|||||||
position_label=None, reading_number=None,
|
position_label=None, reading_number=None,
|
||||||
equipment_model=None, calibration_std_ref=None,
|
equipment_model=None, calibration_std_ref=None,
|
||||||
microscope_image=None,
|
microscope_image=None,
|
||||||
microscope_image_filename=None):
|
microscope_image_filename=None,
|
||||||
|
tablet_tech_id=None):
|
||||||
"""Record a single Fischerscope reading against a job.
|
"""Record a single Fischerscope reading against a job.
|
||||||
|
|
||||||
`job_id` is the canonical kwarg; `production_id` is accepted as an
|
`job_id` is the canonical kwarg; `production_id` is accepted as an
|
||||||
alias for older clients. The reading auto-links to an existing
|
alias for older clients. The reading auto-links to an existing
|
||||||
CoC certificate for the job when one exists.
|
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:
|
if Reading is None:
|
||||||
return {'ok': False, 'error': 'Certificates module not installed'}
|
return {'ok': False, 'error': 'Certificates module not installed'}
|
||||||
target_id = job_id or production_id
|
target_id = job_id or production_id
|
||||||
if not target_id:
|
if not target_id:
|
||||||
return {'ok': False, 'error': 'job_id required'}
|
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():
|
if not job.exists():
|
||||||
return {'ok': False, 'error': f'Job {target_id} not found'}
|
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),
|
'ni_percent': float(ni_percent or 0.0),
|
||||||
'p_percent': float(p_percent or 0.0),
|
'p_percent': float(p_percent or 0.0),
|
||||||
'position_label': position_label or '',
|
'position_label': position_label or '',
|
||||||
'operator_id': request.env.user.id,
|
'operator_id': env.user.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
if equipment_model:
|
if equipment_model:
|
||||||
@@ -516,7 +537,7 @@ class FpShopfloorController(http.Controller):
|
|||||||
if calibration_std_ref:
|
if calibration_std_ref:
|
||||||
vals['calibration_std_ref'] = calibration_std_ref
|
vals['calibration_std_ref'] = calibration_std_ref
|
||||||
if microscope_image:
|
if microscope_image:
|
||||||
att = request.env['ir.attachment'].create({
|
att = env['ir.attachment'].create({
|
||||||
'name': microscope_image_filename or f'thickness_{reading_number}.jpg',
|
'name': microscope_image_filename or f'thickness_{reading_number}.jpg',
|
||||||
'datas': microscope_image,
|
'datas': microscope_image,
|
||||||
'res_model': 'fp.thickness.reading',
|
'res_model': 'fp.thickness.reading',
|
||||||
@@ -525,7 +546,7 @@ class FpShopfloorController(http.Controller):
|
|||||||
vals['microscope_image_id'] = att.id
|
vals['microscope_image_id'] = att.id
|
||||||
|
|
||||||
# Auto-link to an existing CoC if there is one for this job.
|
# 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 Cert is not None:
|
||||||
if 'x_fc_job_id' in Cert._fields:
|
if 'x_fc_job_id' in Cert._fields:
|
||||||
cert_field = 'x_fc_job_id'
|
cert_field = 'x_fc_job_id'
|
||||||
@@ -557,7 +578,8 @@ class FpShopfloorController(http.Controller):
|
|||||||
part_ref=None, qty_on_hold=0, qty_original=0,
|
part_ref=None, qty_on_hold=0, qty_original=0,
|
||||||
hold_reason='other', description=None,
|
hold_reason='other', description=None,
|
||||||
mark_for_scrap=False, facility_id=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.
|
"""Create a quality hold record, splitting qty from the original lot.
|
||||||
|
|
||||||
The hold is linked to the fp.job and (when provided) the
|
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:
|
if not qty_on_hold or int(qty_on_hold) <= 0:
|
||||||
raise UserError("qty_on_hold must be a positive integer.")
|
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 = {
|
vals = {
|
||||||
'part_ref': part_ref or '',
|
'part_ref': part_ref or '',
|
||||||
@@ -583,7 +606,7 @@ class FpShopfloorController(http.Controller):
|
|||||||
if work_center_id:
|
if work_center_id:
|
||||||
vals['work_center_id'] = int(work_center_id)
|
vals['work_center_id'] = int(work_center_id)
|
||||||
if portal_job_id:
|
if portal_job_id:
|
||||||
pj = request.env['fusion.plating.portal.job'].browse(
|
pj = env['fusion.plating.portal.job'].browse(
|
||||||
int(portal_job_id),
|
int(portal_job_id),
|
||||||
)
|
)
|
||||||
if pj.exists():
|
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`.
|
# via fusion_plating_jobs (Phase 3) as `x_fc_job_id` / `x_fc_step_id`.
|
||||||
step_target_id = step_id or workorder_id
|
step_target_id = step_id or workorder_id
|
||||||
if step_target_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 step.exists():
|
||||||
if 'x_fc_step_id' in Hold._fields:
|
if 'x_fc_step_id' in Hold._fields:
|
||||||
vals['x_fc_step_id'] = step.id
|
vals['x_fc_step_id'] = step.id
|
||||||
@@ -605,7 +628,7 @@ class FpShopfloorController(http.Controller):
|
|||||||
# set it through the step.
|
# set it through the step.
|
||||||
if (job_id and 'x_fc_job_id' in Hold._fields
|
if (job_id and 'x_fc_job_id' in Hold._fields
|
||||||
and not vals.get('x_fc_job_id')):
|
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():
|
if j.exists():
|
||||||
vals['x_fc_job_id'] = j.id
|
vals['x_fc_job_id'] = j.id
|
||||||
|
|
||||||
@@ -995,8 +1018,9 @@ class FpShopfloorController(http.Controller):
|
|||||||
# Mark a first-piece gate result from the tablet
|
# Mark a first-piece gate result from the tablet
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@http.route('/fp/shopfloor/mark_gate', type='jsonrpc', auth='user')
|
@http.route('/fp/shopfloor/mark_gate', type='jsonrpc', auth='user')
|
||||||
def mark_gate(self, gate_id, result):
|
def mark_gate(self, gate_id, result, tablet_tech_id=None):
|
||||||
gate = request.env['fusion.plating.first.piece.gate'].browse(int(gate_id))
|
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():
|
if not gate.exists():
|
||||||
return {'ok': False, 'error': 'Gate not found.'}
|
return {'ok': False, 'error': 'Gate not found.'}
|
||||||
try:
|
try:
|
||||||
@@ -1084,21 +1108,23 @@ class FpShopfloorController(http.Controller):
|
|||||||
@http.route('/fp/shopfloor/plant_overview/move_card',
|
@http.route('/fp/shopfloor/plant_overview/move_card',
|
||||||
type='jsonrpc', auth='user')
|
type='jsonrpc', auth='user')
|
||||||
def plant_overview_move_card(self, card_id, source_model=None,
|
def plant_overview_move_card(self, card_id, source_model=None,
|
||||||
target_workcenter_id=None):
|
target_workcenter_id=None,
|
||||||
|
tablet_tech_id=None):
|
||||||
"""Move a step card to a different work centre (drag & drop).
|
"""Move a step card to a different work centre (drag & drop).
|
||||||
|
|
||||||
`source_model` is accepted for backward compatibility but ignored —
|
`source_model` is accepted for backward compatibility but ignored —
|
||||||
Plant Overview now only ever serves fp.job.step cards. A target
|
Plant Overview now only ever serves fp.job.step cards. A target
|
||||||
of 0 / falsy clears the work centre.
|
of 0 / falsy clears the work centre.
|
||||||
"""
|
"""
|
||||||
Step = request.env['fp.job.step']
|
env = env_for_tablet_tech(request.env, tablet_tech_id)
|
||||||
|
Step = env['fp.job.step']
|
||||||
step = Step.browse(int(card_id))
|
step = Step.browse(int(card_id))
|
||||||
if not step.exists():
|
if not step.exists():
|
||||||
return {'ok': False, 'error': f'Step {card_id} not found.'}
|
return {'ok': False, 'error': f'Step {card_id} not found.'}
|
||||||
|
|
||||||
wc_id = int(target_workcenter_id) if target_workcenter_id else False
|
wc_id = int(target_workcenter_id) if target_workcenter_id else False
|
||||||
if wc_id:
|
if wc_id:
|
||||||
wc = request.env['fp.work.centre'].browse(wc_id)
|
wc = env['fp.work.centre'].browse(wc_id)
|
||||||
if not wc.exists():
|
if not wc.exists():
|
||||||
return {'ok': False,
|
return {'ok': False,
|
||||||
'error': f'Work centre {target_workcenter_id} not found.'}
|
'error': f'Work centre {target_workcenter_id} not found.'}
|
||||||
@@ -1108,7 +1134,7 @@ class FpShopfloorController(http.Controller):
|
|||||||
_logger.info(
|
_logger.info(
|
||||||
'Plant Overview: moved step %s (%s) → WC %s by uid %s',
|
'Plant Overview: moved step %s (%s) → WC %s by uid %s',
|
||||||
step.id, step.name, wc_id or 'unassigned',
|
step.id, step.name, wc_id or 'unassigned',
|
||||||
request.env.uid,
|
env.uid,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_logger.exception('Plant Overview move_card failed')
|
_logger.exception('Plant Overview move_card failed')
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ from odoo import fields, http
|
|||||||
from odoo.addons.fusion_plating.models.fp_tz import fp_format
|
from odoo.addons.fusion_plating.models.fp_tz import fp_format
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
|
|
||||||
|
from ._tablet_audit import env_for_tablet_tech
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -190,8 +192,8 @@ class FpWorkspaceController(http.Controller):
|
|||||||
@http.route('/fp/workspace/hold', type='jsonrpc', auth='user')
|
@http.route('/fp/workspace/hold', type='jsonrpc', auth='user')
|
||||||
def hold(self, job_id, reason='other', qty_on_hold=1, description='',
|
def hold(self, job_id, reason='other', qty_on_hold=1, description='',
|
||||||
part_ref='', step_id=None, mark_for_scrap=False,
|
part_ref='', step_id=None, mark_for_scrap=False,
|
||||||
photo_data=None, photo_filename=None):
|
photo_data=None, photo_filename=None, tablet_tech_id=None):
|
||||||
env = request.env
|
env = env_for_tablet_tech(request.env, tablet_tech_id)
|
||||||
job = env['fp.job'].browse(int(job_id))
|
job = env['fp.job'].browse(int(job_id))
|
||||||
if not job.exists():
|
if not job.exists():
|
||||||
return {'ok': False, 'error': f'Job {job_id} not found'}
|
return {'ok': False, 'error': f'Job {job_id} not found'}
|
||||||
@@ -253,8 +255,8 @@ class FpWorkspaceController(http.Controller):
|
|||||||
# /fp/workspace/sign_off — capture signature + finish step atomically
|
# /fp/workspace/sign_off — capture signature + finish step atomically
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
@http.route('/fp/workspace/sign_off', type='jsonrpc', auth='user')
|
@http.route('/fp/workspace/sign_off', type='jsonrpc', auth='user')
|
||||||
def sign_off(self, step_id, signature_data_uri):
|
def sign_off(self, step_id, signature_data_uri, tablet_tech_id=None):
|
||||||
env = request.env
|
env = env_for_tablet_tech(request.env, tablet_tech_id)
|
||||||
sig = (signature_data_uri or '').strip()
|
sig = (signature_data_uri or '').strip()
|
||||||
if not sig:
|
if not sig:
|
||||||
_logger.warning("workspace/sign_off: empty signature for step %s", step_id)
|
_logger.warning("workspace/sign_off: empty signature for step %s", step_id)
|
||||||
@@ -302,8 +304,8 @@ class FpWorkspaceController(http.Controller):
|
|||||||
# /fp/workspace/advance_milestone — fire next_milestone_action
|
# /fp/workspace/advance_milestone — fire next_milestone_action
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
@http.route('/fp/workspace/advance_milestone', type='jsonrpc', auth='user')
|
@http.route('/fp/workspace/advance_milestone', type='jsonrpc', auth='user')
|
||||||
def advance_milestone(self, job_id):
|
def advance_milestone(self, job_id, tablet_tech_id=None):
|
||||||
env = request.env
|
env = env_for_tablet_tech(request.env, tablet_tech_id)
|
||||||
job = env['fp.job'].browse(int(job_id))
|
job = env['fp.job'].browse(int(job_id))
|
||||||
if not job.exists():
|
if not job.exists():
|
||||||
return {'ok': False, 'error': f'Job {job_id} not found'}
|
return {'ok': False, 'error': f'Job {job_id} not found'}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import { Component, useState } from "@odoo/owl";
|
import { Component, useState } from "@odoo/owl";
|
||||||
import { Dialog } from "@web/core/dialog/dialog";
|
import { Dialog } from "@web/core/dialog/dialog";
|
||||||
import { rpc } from "@web/core/network/rpc";
|
import { fpRpc } from "../services/fp_rpc";
|
||||||
import { useService } from "@web/core/utils/hooks";
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
// Hold reasons kept here so the picker doesn't need a server roundtrip.
|
// Hold reasons kept here so the picker doesn't need a server roundtrip.
|
||||||
@@ -75,7 +75,7 @@ export class FpHoldComposer extends Component {
|
|||||||
}
|
}
|
||||||
this.state.submitting = true;
|
this.state.submitting = true;
|
||||||
try {
|
try {
|
||||||
const res = await rpc("/fp/workspace/hold", {
|
const res = await fpRpc("/fp/workspace/hold", {
|
||||||
job_id: this.props.jobId,
|
job_id: this.props.jobId,
|
||||||
step_id: this.props.stepId || null,
|
step_id: this.props.stepId || null,
|
||||||
part_ref: this.props.partRef || "",
|
part_ref: this.props.partRef || "",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl";
|
import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl";
|
||||||
import { registry } from "@web/core/registry";
|
import { registry } from "@web/core/registry";
|
||||||
import { rpc } from "@web/core/network/rpc";
|
import { rpc } from "@web/core/network/rpc";
|
||||||
|
import { fpRpc } from "./services/fp_rpc";
|
||||||
import { useService } from "@web/core/utils/hooks";
|
import { useService } from "@web/core/utils/hooks";
|
||||||
import { WorkflowChip } from "./components/workflow_chip";
|
import { WorkflowChip } from "./components/workflow_chip";
|
||||||
import { GateViz } from "./components/gate_viz";
|
import { GateViz } from "./components/gate_viz";
|
||||||
@@ -116,7 +117,7 @@ export class FpJobWorkspace extends Component {
|
|||||||
// ---- Step actions ------------------------------------------------------
|
// ---- Step actions ------------------------------------------------------
|
||||||
async onStartStep(stepId) {
|
async onStartStep(stepId) {
|
||||||
try {
|
try {
|
||||||
const res = await rpc("/fp/shopfloor/start_wo", { workorder_id: stepId });
|
const res = await fpRpc("/fp/shopfloor/start_wo", { workorder_id: stepId });
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
this.notification.add("Step started.", { type: "success" });
|
this.notification.add("Step started.", { type: "success" });
|
||||||
await this.refresh();
|
await this.refresh();
|
||||||
@@ -135,7 +136,7 @@ export class FpJobWorkspace extends Component {
|
|||||||
contextLabel: `${this.state.data.job.display_wo_name} · Step ${step.sequence_display}: ${step.name}`,
|
contextLabel: `${this.state.data.job.display_wo_name} · Step ${step.sequence_display}: ${step.name}`,
|
||||||
onSubmit: async (dataUri) => {
|
onSubmit: async (dataUri) => {
|
||||||
try {
|
try {
|
||||||
const res = await rpc("/fp/workspace/sign_off", {
|
const res = await fpRpc("/fp/workspace/sign_off", {
|
||||||
step_id: step.id,
|
step_id: step.id,
|
||||||
signature_data_uri: dataUri,
|
signature_data_uri: dataUri,
|
||||||
});
|
});
|
||||||
@@ -154,7 +155,7 @@ export class FpJobWorkspace extends Component {
|
|||||||
}
|
}
|
||||||
// Plain finish — no signature required
|
// Plain finish — no signature required
|
||||||
try {
|
try {
|
||||||
const res = await rpc("/fp/shopfloor/stop_wo", {
|
const res = await fpRpc("/fp/shopfloor/stop_wo", {
|
||||||
workorder_id: step.id, finish: true,
|
workorder_id: step.id, finish: true,
|
||||||
});
|
});
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
@@ -201,7 +202,7 @@ export class FpJobWorkspace extends Component {
|
|||||||
|
|
||||||
async onAdvanceMilestone() {
|
async onAdvanceMilestone() {
|
||||||
try {
|
try {
|
||||||
const res = await rpc("/fp/workspace/advance_milestone", {
|
const res = await fpRpc("/fp/workspace/advance_milestone", {
|
||||||
job_id: this.state.jobId,
|
job_id: this.state.jobId,
|
||||||
});
|
});
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl";
|
import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl";
|
||||||
import { registry } from "@web/core/registry";
|
import { registry } from "@web/core/registry";
|
||||||
import { rpc } from "@web/core/network/rpc";
|
import { rpc } from "@web/core/network/rpc";
|
||||||
|
import { fpRpc } from "./services/fp_rpc";
|
||||||
import { useService } from "@web/core/utils/hooks";
|
import { useService } from "@web/core/utils/hooks";
|
||||||
import { QrScanner } from "./qr_scanner";
|
import { QrScanner } from "./qr_scanner";
|
||||||
import { FpTabletLock } from "./tablet_lock";
|
import { FpTabletLock } from "./tablet_lock";
|
||||||
@@ -208,7 +209,7 @@ export class ManagerDashboard extends Component {
|
|||||||
async onAssignWorker(step, userIdRaw) {
|
async onAssignWorker(step, userIdRaw) {
|
||||||
const userId = parseInt(userIdRaw) || null;
|
const userId = parseInt(userIdRaw) || null;
|
||||||
try {
|
try {
|
||||||
const res = await rpc("/fp/manager/assign_worker", {
|
const res = await fpRpc("/fp/manager/assign_worker", {
|
||||||
step_id: step.id, user_id: userId,
|
step_id: step.id, user_id: userId,
|
||||||
});
|
});
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
@@ -226,7 +227,7 @@ export class ManagerDashboard extends Component {
|
|||||||
async onAssignTank(step, tankIdRaw) {
|
async onAssignTank(step, tankIdRaw) {
|
||||||
const tankId = parseInt(tankIdRaw) || null;
|
const tankId = parseInt(tankIdRaw) || null;
|
||||||
try {
|
try {
|
||||||
const res = await rpc("/fp/manager/assign_tank", {
|
const res = await fpRpc("/fp/manager/assign_tank", {
|
||||||
step_id: step.id, tank_id: tankId,
|
step_id: step.id, tank_id: tankId,
|
||||||
});
|
});
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
@@ -243,7 +244,7 @@ export class ManagerDashboard extends Component {
|
|||||||
|
|
||||||
async onTakeOver(step) {
|
async onTakeOver(step) {
|
||||||
try {
|
try {
|
||||||
const res = await rpc("/fp/manager/take_over", {
|
const res = await fpRpc("/fp/manager/take_over", {
|
||||||
step_id: step.id,
|
step_id: step.id,
|
||||||
});
|
});
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
// =============================================================================
|
||||||
|
// Fusion Plating — fpRpc() wrapper
|
||||||
|
//
|
||||||
|
// Drop-in replacement for the standard `rpc()` import. Automatically
|
||||||
|
// injects the current tablet_tech_id from the tech_store into every
|
||||||
|
// call, so server-side endpoints can attribute the action to the right
|
||||||
|
// user via env.with_user() (see env_for_tablet_tech in
|
||||||
|
// controllers/_tablet_audit.py).
|
||||||
|
//
|
||||||
|
// USE for any RPC that WRITES (start step, finish step, hold create,
|
||||||
|
// sign-off, milestone advance). For read-only loads (kanban, workspace
|
||||||
|
// load, manager funnel), plain rpc() is fine.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// import { fpRpc } from "../services/fp_rpc";
|
||||||
|
// await fpRpc("/fp/shopfloor/start_wo", { workorder_id: stepId });
|
||||||
|
//
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
import { rpc as baseRpc } from "@web/core/network/rpc";
|
||||||
|
|
||||||
|
function _getTechStore() {
|
||||||
|
// Lazy-resolve via the global debug API — avoids circular service init
|
||||||
|
try {
|
||||||
|
const env = odoo.__WOWL_DEBUG__?.root?.env;
|
||||||
|
if (env && env.services && env.services.fp_shopfloor_tech_store) {
|
||||||
|
return env.services.fp_shopfloor_tech_store;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fpRpc(url, params = {}) {
|
||||||
|
const techStore = _getTechStore();
|
||||||
|
if (techStore && techStore.currentTechId) {
|
||||||
|
params = { ...params, tablet_tech_id: techStore.currentTechId };
|
||||||
|
}
|
||||||
|
return baseRpc(url, params);
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl";
|
import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl";
|
||||||
import { registry } from "@web/core/registry";
|
import { registry } from "@web/core/registry";
|
||||||
import { rpc } from "@web/core/network/rpc";
|
import { rpc } from "@web/core/network/rpc";
|
||||||
|
import { fpRpc } from "./services/fp_rpc";
|
||||||
import { useService } from "@web/core/utils/hooks";
|
import { useService } from "@web/core/utils/hooks";
|
||||||
import { QrScanner } from "./qr_scanner";
|
import { QrScanner } from "./qr_scanner";
|
||||||
import { FpKanbanCard } from "./components/kanban_card";
|
import { FpKanbanCard } from "./components/kanban_card";
|
||||||
@@ -254,7 +255,7 @@ export class FpShopfloorLanding extends Component {
|
|||||||
this._movesInFlight += 1;
|
this._movesInFlight += 1;
|
||||||
this._lastDropAt = Date.now();
|
this._lastDropAt = Date.now();
|
||||||
try {
|
try {
|
||||||
const res = await rpc("/fp/shopfloor/plant_overview/move_card", {
|
const res = await fpRpc("/fp/shopfloor/plant_overview/move_card", {
|
||||||
card_id: dragged.id,
|
card_id: dragged.id,
|
||||||
target_workcenter_id: col.work_center_id,
|
target_workcenter_id: col.work_center_id,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user