# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. """HTTP endpoints for the mobile QC checklist OWL client action. Kept narrow (read state + mark-pass/fail + upload PDF + finalize). The OWL component is purely a thin client over these endpoints so any future native mobile app can reuse the same API. """ import base64 import logging from odoo import http, _ from odoo.http import request _logger = logging.getLogger(__name__) class FpQcController(http.Controller): # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ @staticmethod def _check(check_id): """Resolve and access-check a QC record.""" if not check_id: return False check = request.env['fusion.plating.quality.check'].browse( int(check_id) ).exists() if not check: return False check.check_access('read') return check @staticmethod def _line_payload(line): return { 'id': line.id, 'sequence': line.sequence, 'name': line.name, 'description': line.description or '', 'check_type': line.check_type, 'required': line.required, 'requires_value': line.requires_value, 'value': line.value, 'value_min': line.value_min, 'value_max': line.value_max, 'value_uom': line.value_uom or '', 'value_in_range': line.value_in_range, 'requires_photo': line.requires_photo, 'has_photo': bool(line.photo_attachment_id), 'photo_attachment_id': line.photo_attachment_id.id or False, 'result': line.result or 'pending', 'notes': line.notes or '', 'inspector_name': ( line.inspector_id.name if line.inspector_id else '' ), } @staticmethod def _check_payload(check): return { 'id': check.id, 'name': check.name, 'state': check.state, 'overall_result': check.overall_result or '', 'job_id': check.job_id.id if check.job_id else False, 'job_name': check.job_id.name if check.job_id else '', 'partner_name': ( check.partner_id.name if check.partner_id else '' ), 'template_name': ( check.template_id.name if check.template_id else '' ), 'inspector_name': ( check.inspector_id.name if check.inspector_id else '' ), 'line_count': check.line_count, 'lines_passed': check.lines_passed, 'lines_failed': check.lines_failed, 'lines_pending': check.lines_pending, 'require_thickness_readings': check.require_thickness_readings, 'require_thickness_report_pdf': check.require_thickness_report_pdf, 'has_thickness_pdf': bool(check.thickness_report_pdf_id), 'thickness_reading_count': check.thickness_reading_count, 'notes': check.notes or '', } # ------------------------------------------------------------------ # GET state — OWL calls this on mount + after every action # ------------------------------------------------------------------ @http.route( '/fp/qc/get', type='jsonrpc', auth='user', methods=['POST'], ) def get_state(self, check_id=None, job_id=None, **kw): check = self._check(check_id) if not check and job_id: # Resolve latest active QC for this fp.job check = request.env['fusion.plating.quality.check'].search([ ('job_id', '=', int(job_id)), ], order='create_date desc', limit=1) if not check: return {'ok': False, 'error': 'no_qc'} if not check: return {'ok': False, 'error': 'not_found'} return { 'ok': True, 'check': self._check_payload(check), 'lines': [ self._line_payload(l) for l in check.line_ids.sorted('sequence') ], } # ------------------------------------------------------------------ # Line actions # ------------------------------------------------------------------ @http.route( '/fp/qc/line/mark', type='jsonrpc', auth='user', methods=['POST'], ) def line_mark(self, check_id=None, line_id=None, result=None, value=None, notes=None, **kw): check = self._check(check_id) if not check: return {'ok': False, 'error': 'not_found'} Line = request.env['fusion.plating.quality.check.line'] line = Line.browse(int(line_id)).exists() if not line or line.check_id.id != check.id: return {'ok': False, 'error': 'invalid_line'} # Start the check if it's still draft if check.state == 'draft': check.action_start() # Numeric value handling — write before action to let # _compute_value_in_range update the record. vals = {} if value is not None and line.requires_value: try: vals['value'] = float(value) except (TypeError, ValueError): return {'ok': False, 'error': 'invalid_value'} if notes is not None: vals['notes'] = notes if vals: line.write(vals) try: if result == 'pass': line.action_mark_pass() elif result == 'fail': line.action_mark_fail() elif result == 'na': line.action_mark_na() elif result == 'pending': line.write({ 'result': 'pending', 'inspector_id': False, 'completed_at': False, }) except Exception as e: return {'ok': False, 'error': str(e)} return { 'ok': True, 'line': self._line_payload(line), 'check': self._check_payload(check), } # ------------------------------------------------------------------ # Photo upload for an individual line # ------------------------------------------------------------------ @http.route( '/fp/qc/line/photo', type='http', auth='user', methods=['POST'], csrf=False, ) def line_photo(self, line_id=None, **kw): Line = request.env['fusion.plating.quality.check.line'] line = Line.browse(int(line_id)).exists() if not line: return request.make_json_response( {'ok': False, 'error': 'invalid_line'}, ) upload = request.httprequest.files.get('file') if not upload: return request.make_json_response( {'ok': False, 'error': 'no_file'}, ) data = upload.read() if not data: return request.make_json_response( {'ok': False, 'error': 'empty_file'}, ) att = request.env['ir.attachment'].create({ 'name': upload.filename or 'qc_photo.jpg', 'type': 'binary', 'datas': base64.b64encode(data), 'res_model': 'fusion.plating.quality.check.line', 'res_id': line.id, 'mimetype': upload.mimetype or 'image/jpeg', }) line.write({'photo_attachment_id': att.id}) return request.make_json_response({ 'ok': True, 'attachment_id': att.id, }) # ------------------------------------------------------------------ # Fischerscope PDF upload # ------------------------------------------------------------------ @http.route( '/fp/qc/thickness_pdf', type='http', auth='user', methods=['POST'], csrf=False, ) def thickness_pdf(self, check_id=None, **kw): check = self._check(check_id) if not check: return request.make_json_response( {'ok': False, 'error': 'not_found'}, ) upload = request.httprequest.files.get('file') if not upload: return request.make_json_response( {'ok': False, 'error': 'no_file'}, ) data = upload.read() if not data: return request.make_json_response( {'ok': False, 'error': 'empty_file'}, ) att = request.env['ir.attachment'].create({ 'name': upload.filename or 'thickness_report.pdf', 'type': 'binary', 'datas': base64.b64encode(data), 'res_model': 'fusion.plating.quality.check', 'res_id': check.id, 'mimetype': upload.mimetype or 'application/pdf', }) # Triggers _on_thickness_pdf_uploaded via write() override. check.write({'thickness_report_pdf_id': att.id}) return request.make_json_response({ 'ok': True, 'attachment_id': att.id, 'reading_count': check.thickness_reading_count, }) # ------------------------------------------------------------------ # Check-level actions # ------------------------------------------------------------------ @http.route( '/fp/qc/finalize', type='jsonrpc', auth='user', methods=['POST'], ) def finalize(self, check_id=None, result=None, notes=None, **kw): check = self._check(check_id) if not check: return {'ok': False, 'error': 'not_found'} if notes is not None: check.write({'notes': notes}) try: if result == 'pass': check.action_pass() elif result == 'fail': check.action_fail() elif result == 'rework': check.action_rework() else: return {'ok': False, 'error': 'invalid_result'} except Exception as e: return {'ok': False, 'error': str(e)} return {'ok': True, 'check': self._check_payload(check)} @http.route( '/fp/qc/start', type='jsonrpc', auth='user', methods=['POST'], ) def start(self, check_id=None, **kw): check = self._check(check_id) if not check: return {'ok': False, 'error': 'not_found'} if check.state == 'draft': check.action_start() return {'ok': True, 'check': self._check_payload(check)}