# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # # /fp/jobs/tablet/* — JSON-RPC endpoints powering the native-job # Tablet Station (Phase 6 of the native job migration). Operator- # facing touchscreen UI for starting/finishing fp.job.step rows. # # Endpoints: # POST /fp/jobs/tablet/jobs -> active jobs the operator can pick # POST /fp/jobs/tablet/job_detail -> job header + ordered step list # POST /fp/jobs/tablet/start_step -> calls fp.job.step.button_start # POST /fp/jobs/tablet/finish_step -> calls fp.job.step.button_finish # # All write paths funnel through the model's button_start / button_finish # methods so the audit / timelog / duration_actual roll-up logic from # Phase 1 still applies. from odoo import http from odoo.http import request class FpJobsTabletController(http.Controller): @http.route('/fp/jobs/tablet/jobs', type='jsonrpc', auth='user', website=False) def fp_jobs_tablet_jobs(self, facility_id=None, **kwargs): """Active jobs the operator can pick from.""" env = request.env Job = env['fp.job'] domain = [('state', 'in', ('confirmed', 'in_progress'))] if facility_id: domain.append(('facility_id', '=', int(facility_id))) jobs = Job.search( domain, order='priority desc, date_deadline asc, id desc', limit=50, ) return { 'jobs': [{ 'id': j.id, 'name': j.name, 'partner': j.partner_id.name or '', 'qty': j.qty, 'progress_pct': j.step_progress_pct, 'state': j.state, 'priority': j.priority, 'current_step': ( j.current_step_id.name if j.current_step_id else None ), 'deadline': ( j.date_deadline.isoformat() if j.date_deadline else None ), } for j in jobs], } @http.route('/fp/jobs/tablet/job_detail', type='jsonrpc', auth='user', website=False) def fp_jobs_tablet_job_detail(self, job_id, **kwargs): """Job header + ordered step list for the detail panel.""" env = request.env Job = env['fp.job'] job = Job.browse(int(job_id)).exists() if not job: return {'error': 'Job not found'} steps = [] for step in job.step_ids.sorted('sequence'): steps.append({ 'id': step.id, 'name': step.name, 'sequence': step.sequence, 'state': step.state, 'kind': step.kind, 'work_centre': ( step.work_centre_id.name if step.work_centre_id else None ), 'duration_expected': step.duration_expected, 'duration_actual': step.duration_actual, 'thickness_target': step.thickness_target, 'thickness_uom': step.thickness_uom, 'assigned_user': ( step.assigned_user_id.name if step.assigned_user_id else None ), 'date_started': ( step.date_started.isoformat() if step.date_started else None ), 'date_finished': ( step.date_finished.isoformat() if step.date_finished else None ), }) return { 'id': job.id, 'name': job.name, 'partner': job.partner_id.name or '', 'qty': job.qty, 'state': job.state, 'priority': job.priority, 'recipe': job.recipe_id.name if job.recipe_id else None, 'progress_pct': job.step_progress_pct, 'step_done': job.step_done_count, 'step_total': job.step_count, 'steps': steps, } @http.route('/fp/jobs/tablet/step_detail', type='jsonrpc', auth='user', website=False) def fp_jobs_tablet_step_detail(self, step_id, **kwargs): """Step detail panel — used to refresh after button_start / button_finish so the timelog history pulls in the new row. """ env = request.env step = env['fp.job.step'].browse(int(step_id)).exists() if not step: return {'error': 'Step not found'} timelogs = [] for log in step.time_log_ids.sorted('date_started', reverse=True): timelogs.append({ 'id': log.id, 'user': log.user_id.name or '', 'date_started': ( log.date_started.isoformat() if log.date_started else None ), 'date_finished': ( log.date_finished.isoformat() if log.date_finished else None ), 'duration_minutes': log.duration_minutes, }) return { 'id': step.id, 'name': step.name, 'sequence': step.sequence, 'state': step.state, 'kind': step.kind, 'work_centre': ( step.work_centre_id.name if step.work_centre_id else None ), 'duration_expected': step.duration_expected, 'duration_actual': step.duration_actual, 'thickness_target': step.thickness_target, 'thickness_uom': step.thickness_uom, 'assigned_user': ( step.assigned_user_id.name if step.assigned_user_id else None ), 'date_started': ( step.date_started.isoformat() if step.date_started else None ), 'date_finished': ( step.date_finished.isoformat() if step.date_finished else None ), 'instructions': step.instructions or '', 'timelogs': timelogs, } @http.route('/fp/jobs/tablet/start_step', type='jsonrpc', auth='user', website=False) def fp_jobs_tablet_start_step(self, step_id, **kwargs): env = request.env step = env['fp.job.step'].browse(int(step_id)).exists() if not step: return {'ok': False, 'error': 'Step not found'} try: step.button_start() return { 'ok': True, 'state': step.state, 'date_started': ( step.date_started.isoformat() if step.date_started else None ), } except Exception as e: return {'ok': False, 'error': str(e)} @http.route('/fp/jobs/tablet/finish_step', type='jsonrpc', auth='user', website=False) def fp_jobs_tablet_finish_step(self, step_id, **kwargs): env = request.env step = env['fp.job.step'].browse(int(step_id)).exists() if not step: return {'ok': False, 'error': 'Step not found'} try: step.button_finish() return { 'ok': True, 'state': step.state, 'duration_actual': step.duration_actual, 'date_finished': ( step.date_finished.isoformat() if step.date_finished else None ), } except Exception as e: return {'ok': False, 'error': str(e)}