First Commit

This commit is contained in:
2026-05-31 10:17:09 +07:00
commit 17a9c69379
4547 changed files with 1170384 additions and 0 deletions
@@ -0,0 +1 @@
from . import ifc_jsonrpc
@@ -0,0 +1,888 @@
import base64
import datetime
import hashlib
import hmac
import json
import mimetypes
import tempfile
import time
import uuid
from pathlib import Path
from urllib.parse import urlencode
from odoo import http, _
from odoo.exceptions import AccessError, UserError
from odoo.http import request
TEMP_SUBDIR = 'odoo_ifc_temp'
TEMP_TTL_MINUTES = 60
def _json_http_response(payload, status=200):
return request.make_response(
json.dumps(payload),
headers=[('Content-Type', 'application/json')],
status=status,
)
def _jsonrpc_ok(result=None):
return {'success': True, 'result': result or {}}
def _jsonrpc_fail(message, code='ERROR', details=None):
payload = {'success': False, 'error': {'code': code, 'message': message}}
if details:
payload['error']['details'] = details
return payload
def _safe_filename(filename, fallback='document.ifc'):
name = (filename or fallback).strip().replace('\\', '_').replace('/', '_')
return name or fallback
def _to_bool(value, default=True):
if isinstance(value, bool):
return value
if value is None:
return default
return str(value).strip().lower() in ('1', 'true', 'yes', 'y', 'on')
class IfcJsonRpcController(http.Controller):
def _check_record_access(self, record, mode='read'):
record.check_access_rights(mode)
record.check_access_rule(mode)
def _serialize_project(self, project):
return {
'id': project.id,
'name': project.name,
'document_ref': project.document_ref,
'file_name': project.file_name,
'company_id': project.company_id.id if project.company_id else False,
'company_name': project.company_id.name if project.company_id else False,
'state': project.state,
'approval_state': project.approval_state,
'uploaded_date': project.uploaded_date.isoformat() if project.uploaded_date else False,
'uploaded_by': {
'id': project.uploaded_by_id.id,
'name': project.uploaded_by_id.name,
} if project.uploaded_by_id else False,
'last_update': project.last_update.isoformat() if project.last_update else False,
'last_revision_label': project.last_revision_label,
'last_revision_note': project.last_revision_note,
'last_revision_date': project.last_revision_date.isoformat() if project.last_revision_date else False,
'revised_by': {
'id': project.revised_by_id.id,
'name': project.revised_by_id.name,
} if project.revised_by_id else False,
'estimated_bom_id': project.estimated_bom_id.id if project.estimated_bom_id else False,
'permanent_bom_id': project.permanent_bom_id.id if project.permanent_bom_id else False,
'latest_mo_id': project.manufacturing_order_id.id if project.manufacturing_order_id else False,
'mo_count': len(project.manufacturing_order_ids),
'activity_count': len(project.activity_ids),
'history_count': len(project.history_ids),
'revision_count': len(project.revision_ids),
}
def _serialize_activity(self, activity):
return {
'id': activity.id,
'summary': activity.summary,
'note': activity.note,
'date_deadline': activity.date_deadline.isoformat() if activity.date_deadline else False,
'state': activity.state,
'res_id': activity.res_id,
'assigned_to': {
'id': activity.user_id.id,
'name': activity.user_id.name,
} if activity.user_id else False,
'created_at': activity.create_date.isoformat() if activity.create_date else False,
}
def _serialize_history(self, history):
return {
'id': history.id,
'activity_type': history.activity_type,
'activity_date': history.activity_date.isoformat() if history.activity_date else False,
'note': history.note,
'state_snapshot': history.state_snapshot,
'user': {
'id': history.user_id.id,
'name': history.user_id.name,
} if history.user_id else False,
}
def _serialize_bom_line(self, line):
return {
'id': line.id,
'name': line.product_id.display_name,
'product_id': line.product_id.id,
'default_code': line.product_id.default_code,
'qty': line.product_qty,
'uom': {
'id': line.product_uom_id.id,
'name': line.product_uom_id.name,
} if line.product_uom_id else False,
}
def _serialize_bom(self, bom):
return {
'id': bom.id,
'code': bom.code,
'product': {
'id': bom.product_tmpl_id.id,
'name': bom.product_tmpl_id.display_name,
'default_code': bom.product_tmpl_id.default_code,
} if bom.product_tmpl_id else False,
'quantity': bom.product_qty,
'type': bom.type,
'line_count': len(bom.line_ids),
'lines': [self._serialize_bom_line(line) for line in bom.line_ids],
}
def _serialize_purchase_order(self, purchase_order):
return {
'id': purchase_order.id,
'name': purchase_order.name,
'state': purchase_order.state,
'vendor': {
'id': purchase_order.partner_id.id,
'name': purchase_order.partner_id.name,
} if purchase_order.partner_id else False,
'origin': purchase_order.origin,
'line_count': len(purchase_order.order_line),
'lines': [
{
'id': line.id,
'product_id': line.product_id.id,
'product_name': line.product_id.display_name,
'qty': line.product_qty,
'uom_id': line.product_uom.id,
'uom_name': line.product_uom.name,
'price_unit': line.price_unit,
}
for line in purchase_order.order_line
],
}
def _serialize_manufacturing_order(self, manufacturing_order):
return {
'id': manufacturing_order.id,
'name': manufacturing_order.name,
'state': manufacturing_order.state,
'origin': manufacturing_order.origin,
'product': {
'id': manufacturing_order.product_id.id,
'name': manufacturing_order.product_id.display_name,
'default_code': manufacturing_order.product_id.default_code,
} if manufacturing_order.product_id else False,
'qty': manufacturing_order.product_qty,
'uom': {
'id': manufacturing_order.product_uom_id.id,
'name': manufacturing_order.product_uom_id.name,
} if manufacturing_order.product_uom_id else False,
'bom_id': manufacturing_order.bom_id.id if manufacturing_order.bom_id else False,
'ifc_project_id': manufacturing_order.ifc_project_id.id if manufacturing_order.ifc_project_id else False,
}
def _get_signing_secret(self):
param = request.env['ir.config_parameter'].sudo()
secret = param.get_param('database.secret') or param.get_param('database.uuid')
return secret or request.session.db or 'odoo'
def _build_temp_signature(self, file_key, document_id, user_id, expires_at):
payload = f'{file_key}|{document_id}|{user_id}|{expires_at}'
return hmac.new(
self._get_signing_secret().encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256,
).hexdigest()
def _build_signed_temp_download_url(self, file_key, document_id, user_id):
expires_at = int(time.time()) + (TEMP_TTL_MINUTES * 60)
signature = self._build_temp_signature(file_key, document_id, user_id, expires_at)
query = urlencode(
{
'doc_id': int(document_id),
'uid': int(user_id),
'exp': expires_at,
'sig': signature,
}
)
return f'/api/ifc/temp/{file_key}?{query}', expires_at
def _verify_temp_signature(self, file_key, document_id, user_id, expires_at, signature):
if not signature:
return False
try:
exp_int = int(expires_at)
except (TypeError, ValueError):
return False
if exp_int < int(time.time()):
return False
expected = self._build_temp_signature(file_key, document_id, user_id, exp_int)
return hmac.compare_digest(expected, signature)
def _get_project_for_action(self, document_id, mode='write'):
project = request.env['grt.ifc.project'].browse(int(document_id)).exists()
if not project:
return False, _jsonrpc_fail('Dokumen tidak ditemukan.', code='NOT_FOUND')
try:
self._check_record_access(project, mode=mode)
except AccessError:
return False, _jsonrpc_fail('Tidak punya akses ke dokumen.', code='ACCESS_DENIED')
return project, None
def _execute_project_action(self, document_id, method_name):
project, error = self._get_project_for_action(document_id, mode='write')
if error:
return error
try:
getattr(project, method_name)()
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='ACTION_FAILED')
except Exception as exc:
return _jsonrpc_fail('Eksekusi aksi dokumen gagal.', code='ACTION_FAILED', details=str(exc))
return _jsonrpc_ok({'document': self._serialize_project(project)})
def _get_temp_root(self):
root = Path(tempfile.gettempdir()) / TEMP_SUBDIR
root.mkdir(parents=True, exist_ok=True)
return root
def _cleanup_temp_files(self):
root = self._get_temp_root()
now = datetime.datetime.now(datetime.UTC)
for file_path in root.glob('*.ifc'):
try:
age_seconds = now.timestamp() - file_path.stat().st_mtime
except OSError:
continue
if age_seconds > TEMP_TTL_MINUTES * 60:
try:
file_path.unlink()
except OSError:
pass
@http.route('/api/session/login', type='json', auth='none', methods=['POST'], csrf=False)
def api_session_login(self, db=None, login=None, password=None, **kwargs):
database = db or request.session.db
username = login
passwd = password
if not database or not username or not passwd:
return _jsonrpc_fail('db, login, dan password wajib diisi.', code='BAD_REQUEST')
try:
uid = request.session.authenticate(database, username, passwd)
except Exception as exc:
return _jsonrpc_fail('Login gagal.', code='AUTH_FAILED', details=str(exc))
if not uid:
return _jsonrpc_fail('Login gagal. Cek kredensial.', code='AUTH_FAILED')
user = request.env['res.users'].sudo().browse(uid)
return _jsonrpc_ok(
{
'uid': uid,
'name': user.name,
'login': user.login,
'db': database,
'session_id': request.session.sid,
}
)
@http.route('/api/session/logout', type='json', auth='user', methods=['POST'], csrf=False)
def api_session_logout(self, **kwargs):
request.session.logout()
return _jsonrpc_ok({'message': 'Logout berhasil.'})
@http.route('/api/session/me', type='json', auth='user', methods=['POST'], csrf=False)
def api_session_me(self, **kwargs):
user = request.env.user
return _jsonrpc_ok(
{
'uid': user.id,
'name': user.name,
'login': user.login,
'session_id': request.session.sid,
'db': request.session.db,
}
)
@http.route('/api/ifc/documents/list', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_list(self, limit=50, offset=0, search=None, **kwargs):
domain = []
if search:
domain = ['|', ('name', 'ilike', search), ('document_ref', 'ilike', search)]
projects = request.env['grt.ifc.project'].search(domain, limit=int(limit), offset=int(offset), order='id desc')
for project in projects:
self._check_record_access(project, mode='read')
return _jsonrpc_ok(
{
'count': len(projects),
'items': [self._serialize_project(project) for project in projects],
}
)
@http.route('/api/ifc/documents/create', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_create(self, name=None, file_name=None, file_data=None, revision_note=None, company_id=None, **kwargs):
if not name or not file_data:
return _jsonrpc_fail('name dan file_data (base64) wajib diisi.', code='BAD_REQUEST')
vals = {
'name': name,
'file_name': _safe_filename(file_name or '%s.ifc' % name),
'file_data': file_data,
'revision_note_input': revision_note or _('Dokumen awal diunggah via API.'),
}
if company_id:
vals['company_id'] = int(company_id)
try:
project = request.env['grt.ifc.project'].create(vals)
except Exception as exc:
return _jsonrpc_fail('Gagal membuat dokumen IFC.', code='CREATE_FAILED', details=str(exc))
self._check_record_access(project, mode='read')
return _jsonrpc_ok({'document': self._serialize_project(project)})
@http.route('/api/ifc/documents/revise', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_revise(self, document_id=None, file_data=None, file_name=None, revision_note=None, **kwargs):
if not document_id or not file_data:
return _jsonrpc_fail('document_id dan file_data (base64) wajib diisi.', code='BAD_REQUEST')
project = request.env['grt.ifc.project'].browse(int(document_id)).exists()
if not project:
return _jsonrpc_fail('Dokumen tidak ditemukan.', code='NOT_FOUND')
try:
self._check_record_access(project, mode='write')
except AccessError:
return _jsonrpc_fail('Tidak punya akses untuk revisi dokumen.', code='ACCESS_DENIED')
vals = {
'file_data': file_data,
'revision_note_input': revision_note or _('Revisi dokumen via API.'),
}
if file_name:
vals['file_name'] = _safe_filename(file_name)
try:
project.write(vals)
except Exception as exc:
return _jsonrpc_fail('Gagal melakukan revisi dokumen.', code='REVISION_FAILED', details=str(exc))
return _jsonrpc_ok({'document': self._serialize_project(project)})
@http.route('/api/ifc/documents/parse', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_parse(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project = request.env['grt.ifc.project'].browse(int(document_id)).exists()
if not project:
return _jsonrpc_fail('Dokumen tidak ditemukan.', code='NOT_FOUND')
try:
self._check_record_access(project, mode='write')
project.action_parse_ifc()
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='PARSE_FAILED')
except Exception as exc:
return _jsonrpc_fail('Gagal parse IFC.', code='PARSE_FAILED', details=str(exc))
return _jsonrpc_ok({'document': self._serialize_project(project)})
@http.route('/api/ifc/documents/approval/request-validation', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_request_validation(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
return self._execute_project_action(document_id, 'action_request_validation')
@http.route('/api/ifc/documents/approval/submit', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_submit_approval(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
return self._execute_project_action(document_id, 'action_submit_for_approval')
@http.route('/api/ifc/documents/approval/approve-production', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_approve_production(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
return self._execute_project_action(document_id, 'action_approve_production')
@http.route('/api/ifc/documents/approval/approve-provisioning', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_approve_provisioning(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
return self._execute_project_action(document_id, 'action_approve_provisioning')
@http.route('/api/ifc/documents/approval/reject', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_reject(self, document_id=None, note=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project, error = self._get_project_for_action(document_id, mode='write')
if error:
return error
try:
if note:
project.revision_note_input = note
project.action_reject_document()
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='ACTION_FAILED')
except Exception as exc:
return _jsonrpc_fail('Reject dokumen gagal.', code='ACTION_FAILED', details=str(exc))
return _jsonrpc_ok({'document': self._serialize_project(project)})
@http.route('/api/ifc/documents/approval/back-draft', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_back_draft(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
return self._execute_project_action(document_id, 'action_back_to_draft')
@http.route('/api/ifc/documents/activities/add', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_activities_add(self, document_id=None, summary=None, note=None, user_id=None, deadline=None, **kwargs):
if not document_id or not summary:
return _jsonrpc_fail('document_id dan summary wajib diisi.', code='BAD_REQUEST')
project = request.env['grt.ifc.project'].browse(int(document_id)).exists()
if not project:
return _jsonrpc_fail('Dokumen tidak ditemukan.', code='NOT_FOUND')
try:
self._check_record_access(project, mode='write')
except AccessError:
return _jsonrpc_fail('Tidak punya akses membuat activity.', code='ACCESS_DENIED')
target_user_id = int(user_id) if user_id else request.env.user.id
date_deadline = deadline or False
try:
project.activity_schedule(
'mail.mail_activity_data_todo',
summary=summary,
note=note or '',
user_id=target_user_id,
date_deadline=date_deadline,
)
project._log_history('validation', _('Activity baru ditambahkan via API.'))
except Exception as exc:
return _jsonrpc_fail('Gagal membuat activity.', code='ACTIVITY_FAILED', details=str(exc))
return _jsonrpc_ok({'message': 'Activity berhasil dibuat.'})
@http.route('/api/ifc/documents/activities/list', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_activities_list(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project = request.env['grt.ifc.project'].browse(int(document_id)).exists()
if not project:
return _jsonrpc_fail('Dokumen tidak ditemukan.', code='NOT_FOUND')
try:
self._check_record_access(project, mode='read')
except AccessError:
return _jsonrpc_fail('Tidak punya akses membaca activity.', code='ACCESS_DENIED')
activities = request.env['mail.activity'].search(
[
('res_model', '=', 'grt.ifc.project'),
('res_id', '=', project.id),
],
order='date_deadline asc, id desc',
)
history = request.env['grt.ifc.activity.history'].search(
[('project_id', '=', project.id)],
order='activity_date desc, id desc',
)
return _jsonrpc_ok(
{
'document': self._serialize_project(project),
'activities': [self._serialize_activity(activity) for activity in activities],
'history': [self._serialize_history(item) for item in history],
}
)
@http.route('/api/ifc/documents/bom/generate', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_bom_generate(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project, error = self._get_project_for_action(document_id, mode='write')
if error:
return error
try:
project.action_create_bom_estimate()
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='BOM_FAILED')
except Exception as exc:
return _jsonrpc_fail('Gagal generate BoM estimasi.', code='BOM_FAILED', details=str(exc))
return _jsonrpc_ok(
{
'document': self._serialize_project(project),
'estimated_bom': self._serialize_bom(project.estimated_bom_id) if project.estimated_bom_id else False,
}
)
@http.route('/api/ifc/documents/bom/get', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_bom_get(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project, error = self._get_project_for_action(document_id, mode='read')
if error:
return error
parsed_bom = [
{
'id': line.id,
'ifc_key': line.ifc_key,
'name': line.name,
'global_id': line.global_id,
'ifc_type': line.ifc_type,
'relation_type': line.relation_type,
'level': line.level,
'quantity': line.quantity,
'unit_name': line.unit_name,
'parent_id': line.parent_id.id if line.parent_id else False,
}
for line in project.bom_ids
]
return _jsonrpc_ok(
{
'document': self._serialize_project(project),
'parsed_bom': parsed_bom,
'estimated_bom': self._serialize_bom(project.estimated_bom_id) if project.estimated_bom_id else False,
'permanent_bom': self._serialize_bom(project.permanent_bom_id) if project.permanent_bom_id else False,
}
)
@http.route('/api/ifc/documents/bom/create-permanent-if-available', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_bom_create_permanent_if_available(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project, error = self._get_project_for_action(document_id, mode='write')
if error:
return error
try:
permanent_bom = project.action_create_permanent_bom_if_available()
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='BOM_PERMANENT_FAILED')
except Exception as exc:
return _jsonrpc_fail('Gagal membuat Permanent BoM.', code='BOM_PERMANENT_FAILED', details=str(exc))
return _jsonrpc_ok(
{
'document': self._serialize_project(project),
'permanent_bom': self._serialize_bom(permanent_bom),
}
)
@http.route('/api/ifc/documents/inventory/check', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_inventory_check(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project, error = self._get_project_for_action(document_id, mode='read')
if error:
return error
try:
availability = project.action_check_inventory_availability()
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='INVENTORY_FAILED')
except Exception as exc:
return _jsonrpc_fail('Gagal cek ketersediaan inventory.', code='INVENTORY_FAILED', details=str(exc))
return _jsonrpc_ok(
{
'document': self._serialize_project(project),
'availability': availability,
}
)
@http.route('/api/ifc/documents/inventory/create-draft-purchase', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_inventory_create_draft_purchase(self, document_id=None, vendor_id=None, only_shortage=True, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project, error = self._get_project_for_action(document_id, mode='write')
if error:
return error
try:
purchase_orders = project.action_create_draft_purchase_for_shortage(
vendor_id=vendor_id,
only_shortage=_to_bool(only_shortage, default=True),
)
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='PURCHASE_DRAFT_FAILED')
except Exception as exc:
return _jsonrpc_fail('Gagal membuat draft purchase.', code='PURCHASE_DRAFT_FAILED', details=str(exc))
return _jsonrpc_ok(
{
'document': self._serialize_project(project),
'purchase_orders': [self._serialize_purchase_order(po) for po in purchase_orders],
}
)
@http.route('/api/ifc/documents/inventory/create-product', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_inventory_create_product(self, document_id=None, ifc_bom_line_id=None, **kwargs):
if not document_id or not ifc_bom_line_id:
return _jsonrpc_fail('document_id dan ifc_bom_line_id wajib diisi.', code='BAD_REQUEST')
project, error = self._get_project_for_action(document_id, mode='write')
if error:
return error
try:
product, created = project.action_create_missing_product_for_ifc_bom(ifc_bom_line_id)
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='PRODUCT_CREATE_FAILED')
except Exception as exc:
return _jsonrpc_fail('Gagal membuat produk dari IFC BOM.', code='PRODUCT_CREATE_FAILED', details=str(exc))
return _jsonrpc_ok(
{
'document': self._serialize_project(project),
'created': created,
'product': {
'id': product.id,
'name': product.display_name,
'default_code': product.default_code,
'qty_available': product.qty_available,
'virtual_available': product.virtual_available,
},
}
)
@http.route('/api/ifc/documents/mo/create-if-available', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_mo_create_if_available(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project, error = self._get_project_for_action(document_id, mode='write')
if error:
return error
try:
manufacturing_order = project.action_create_mo_if_available()
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='MO_CREATE_FAILED')
except Exception as exc:
return _jsonrpc_fail('Gagal membuat Manufacturing Order.', code='MO_CREATE_FAILED', details=str(exc))
return _jsonrpc_ok(
{
'document': self._serialize_project(project),
'manufacturing_order': self._serialize_manufacturing_order(manufacturing_order),
}
)
@http.route('/api/ifc/documents/mo/create-from-permanent-bom', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_mo_create_from_permanent_bom(self, document_id=None, **kwargs):
return self.api_ifc_documents_mo_create_if_available(document_id=document_id, **kwargs)
@http.route('/api/ifc/documents/mo/check-all-availability', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_mo_check_all_availability(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project, error = self._get_project_for_action(document_id, mode='read')
if error:
return error
try:
summary = project.action_check_all_mo_availability_summary()
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='MO_AVAILABILITY_FAILED')
except Exception as exc:
return _jsonrpc_fail('Gagal cek availability seluruh MO.', code='MO_AVAILABILITY_FAILED', details=str(exc))
return _jsonrpc_ok(
{
'document': self._serialize_project(project),
'summary': summary,
}
)
@http.route('/api/ifc/documents/mo/confirm-all-if-available', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_mo_confirm_all_if_available(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project, error = self._get_project_for_action(document_id, mode='write')
if error:
return error
try:
confirmed_mos = project.action_confirm_all_mo_if_available()
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='MO_CONFIRM_FAILED')
except Exception as exc:
return _jsonrpc_fail('Gagal confirm seluruh MO.', code='MO_CONFIRM_FAILED', details=str(exc))
return _jsonrpc_ok(
{
'document': self._serialize_project(project),
'confirmed_mos': [self._serialize_manufacturing_order(mo) for mo in confirmed_mos],
}
)
@http.route('/api/ifc/documents/mo/run-pipeline', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_mo_run_pipeline(self, document_id=None, auto_confirm=False, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project, error = self._get_project_for_action(document_id, mode='write')
if error:
return error
try:
pipeline_result = project.action_run_mo_pipeline(auto_confirm=_to_bool(auto_confirm, default=False))
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='MO_PIPELINE_FAILED')
except Exception as exc:
return _jsonrpc_fail('Gagal menjalankan pipeline MO.', code='MO_PIPELINE_FAILED', details=str(exc))
return _jsonrpc_ok(
{
'document': self._serialize_project(project),
'estimated_availability': pipeline_result['estimated_availability'],
'permanent_bom': self._serialize_bom(pipeline_result['permanent_bom']) if pipeline_result['permanent_bom'] else False,
'manufacturing_order': self._serialize_manufacturing_order(pipeline_result['manufacturing_order']) if pipeline_result['manufacturing_order'] else False,
'mo_summary': pipeline_result['mo_summary'],
'auto_confirm': pipeline_result['auto_confirm'],
'confirmed_mos': [self._serialize_manufacturing_order(mo) for mo in pipeline_result['confirmed_mos']],
}
)
@http.route('/api/ifc/documents/mo/pipeline-dry-run', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_mo_pipeline_dry_run(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project, error = self._get_project_for_action(document_id, mode='read')
if error:
return error
try:
preview = project.action_preview_mo_pipeline()
except (AccessError, UserError) as exc:
return _jsonrpc_fail(str(exc), code='MO_PIPELINE_DRY_RUN_FAILED')
except Exception as exc:
return _jsonrpc_fail('Gagal menjalankan dry-run pipeline MO.', code='MO_PIPELINE_DRY_RUN_FAILED', details=str(exc))
return _jsonrpc_ok(
{
'document': self._serialize_project(project),
'preview': preview,
}
)
@http.route('/api/ifc/documents/temp-file', type='json', auth='user', methods=['POST'], csrf=False)
def api_ifc_documents_temp_file(self, document_id=None, **kwargs):
if not document_id:
return _jsonrpc_fail('document_id wajib diisi.', code='BAD_REQUEST')
project = request.env['grt.ifc.project'].browse(int(document_id)).exists()
if not project:
return _jsonrpc_fail('Dokumen tidak ditemukan.', code='NOT_FOUND')
try:
self._check_record_access(project, mode='read')
except AccessError:
return _jsonrpc_fail('Tidak punya akses membaca file.', code='ACCESS_DENIED')
if not project.file_data:
return _jsonrpc_fail('Dokumen tidak memiliki file IFC.', code='NO_FILE')
self._cleanup_temp_files()
token = uuid.uuid4().hex
file_name = _safe_filename(project.file_name or ('%s.ifc' % project.document_ref))
temp_root = self._get_temp_root()
temp_path = temp_root / ('%s_%s' % (token, file_name))
try:
temp_path.write_bytes(base64.b64decode(project.file_data))
except Exception as exc:
return _jsonrpc_fail('Gagal decode file IFC ke temp folder.', code='DECODE_FAILED', details=str(exc))
download_url, expires_at = self._build_signed_temp_download_url(temp_path.name, project.id, request.env.user.id)
return _jsonrpc_ok(
{
'token': token,
'file_name': file_name,
'temp_path': str(temp_path),
'download_url': download_url,
'expires_in_minutes': TEMP_TTL_MINUTES,
'expires_at_unix': expires_at,
}
)
@http.route('/api/ifc/temp/<string:file_key>', type='http', auth='user', methods=['GET'], csrf=False)
def api_ifc_temp_get(self, file_key, **kwargs):
if not file_key or '/' in file_key or '\\' in file_key:
return _json_http_response({'error': 'Invalid file key.'}, status=400)
doc_id = kwargs.get('doc_id')
signed_uid = kwargs.get('uid')
exp = kwargs.get('exp')
sig = kwargs.get('sig')
if not doc_id or not signed_uid or not exp or not sig:
return _json_http_response({'error': 'Signed URL parameters are required.'}, status=400)
try:
doc_id_int = int(doc_id)
signed_uid_int = int(signed_uid)
except (TypeError, ValueError):
return _json_http_response({'error': 'Invalid signed URL parameters.'}, status=400)
if signed_uid_int != request.env.user.id:
return _json_http_response({'error': 'Signed URL does not match current user.'}, status=403)
if not self._verify_temp_signature(file_key, doc_id_int, signed_uid_int, exp, sig):
return _json_http_response({'error': 'Invalid or expired signature.'}, status=403)
project = request.env['grt.ifc.project'].browse(doc_id_int).exists()
if not project:
return _json_http_response({'error': 'Document not found.'}, status=404)
try:
self._check_record_access(project, mode='read')
except AccessError:
return _json_http_response({'error': 'Access denied.'}, status=403)
temp_path = self._get_temp_root() / file_key
if not temp_path.exists() or not temp_path.is_file():
return _json_http_response({'error': 'Temp file not found.'}, status=404)
mimetype, _ = mimetypes.guess_type(str(temp_path))
mimetype = mimetype or 'application/octet-stream'
content = temp_path.read_bytes()
filename = file_key.split('_', 1)[1] if '_' in file_key else file_key
headers = [
('Content-Type', mimetype),
('Content-Length', str(len(content))),
('Content-Disposition', 'inline; filename="%s"' % filename),
]
return request.make_response(content, headers=headers)