285 lines
10 KiB
Python
285 lines
10 KiB
Python
# -*- 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)}
|