First Commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import controllers
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
'name': 'GRT Product Engineering',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Manufacturing',
|
||||
'summary': 'Manajemen dokumen IFC, approval berlapis, dan estimasi BoM',
|
||||
'description': """
|
||||
Modul Product Engineering untuk Odoo 19:
|
||||
- Manajemen dokumen desain IFC beserta riwayat revisi
|
||||
- Validasi dan approval 2 layer (Manager Produksi, Manager Provisioning)
|
||||
- Aktivitas pemeriksaan dan approval berbasis mail.activity
|
||||
- Estimasi BoM dari hasil parsing IFC ke MRP BoM
|
||||
""",
|
||||
'author': 'GRT',
|
||||
'maintainer': 'GRT',
|
||||
'license': 'LGPL-3',
|
||||
'depends': ['grt_ifcopenshell', 'mail', 'mrp', 'purchase', 'web'],
|
||||
'data': [
|
||||
'security/security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/ir_sequence_data.xml',
|
||||
'views/ifc_project_views.xml',
|
||||
'views/ifc_project_menu.xml',
|
||||
'views/ifc_kpi_dashboard_views.xml',
|
||||
'views/mrp_bom_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
@@ -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)
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="seq_grt_ifc_project_document" model="ir.sequence">
|
||||
<field name="name">IFC Engineering Document</field>
|
||||
<field name="code">grt.ifc.project.document</field>
|
||||
<field name="prefix">IFC/%(year)s/</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="implementation">standard</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,318 @@
|
||||
# API Quick Reference
|
||||
|
||||
Dokumen ini adalah cheat sheet endpoint Product Engineering IFC untuk kebutuhan implementasi cepat.
|
||||
Dokumen ini bersifat rekomendasi implementasi dan dapat direvisi mengikuti perubahan backend.
|
||||
|
||||
Lihat detail lengkap di [BACKEND_TECHNICAL_GUIDE.md](BACKEND_TECHNICAL_GUIDE.md) dan [FRONTEND_TECHNICAL_GUIDE.md](FRONTEND_TECHNICAL_GUIDE.md).
|
||||
|
||||
## State Model Singkat
|
||||
|
||||
- Parse state: draft -> parsed | failed
|
||||
- Approval state: draft -> in_validation -> waiting_production -> waiting_provisioning -> approved (dapat rejected pada tahap validasi atau approval)
|
||||
|
||||
## 1) JSON-RPC Envelope
|
||||
|
||||
Gunakan payload berikut untuk semua endpoint type json:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {},
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
|
||||
## 2) Session Endpoints
|
||||
|
||||
Authentication login endpoint:
|
||||
|
||||
- Primary: /api/session/login
|
||||
- Fallback standar Odoo: /web/session/authenticate
|
||||
|
||||
### Login
|
||||
|
||||
- Route: /api/session/login
|
||||
- Auth: none
|
||||
- Params: db, login, password
|
||||
|
||||
Contoh params:
|
||||
|
||||
```json
|
||||
{
|
||||
"db": "odoo19",
|
||||
"login": "user_backend",
|
||||
"password": "******"
|
||||
}
|
||||
```
|
||||
|
||||
### Logout
|
||||
|
||||
- Route: /api/session/logout
|
||||
- Auth: user
|
||||
- Params: none
|
||||
|
||||
### Me
|
||||
|
||||
- Route: /api/session/me
|
||||
- Auth: user
|
||||
- Params: none
|
||||
|
||||
## 3) Document Endpoints
|
||||
|
||||
### List
|
||||
|
||||
- Route: /api/ifc/documents/list
|
||||
- Auth: user
|
||||
- Params opsional: limit, offset, search
|
||||
|
||||
### Create
|
||||
|
||||
- Route: /api/ifc/documents/create
|
||||
- Auth: user
|
||||
- Params wajib: name, file_data
|
||||
- Params opsional: file_name, revision_note, company_id
|
||||
|
||||
Contoh params:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "IFC Docking Station",
|
||||
"file_name": "docking_station.ifc",
|
||||
"file_data": "<BASE64_IFC>",
|
||||
"revision_note": "Initial upload"
|
||||
}
|
||||
```
|
||||
|
||||
### Revise
|
||||
|
||||
- Route: /api/ifc/documents/revise
|
||||
- Auth: user
|
||||
- Params wajib: document_id, file_data
|
||||
- Params opsional: file_name, revision_note
|
||||
|
||||
### Parse
|
||||
|
||||
- Route: /api/ifc/documents/parse
|
||||
- Auth: user
|
||||
- Params wajib: document_id
|
||||
|
||||
## 4) Approval Endpoints
|
||||
|
||||
### Request Validation
|
||||
|
||||
- Route: /api/ifc/documents/approval/request-validation
|
||||
- Params wajib: document_id
|
||||
|
||||
### Submit Approval
|
||||
|
||||
- Route: /api/ifc/documents/approval/submit
|
||||
- Params wajib: document_id
|
||||
|
||||
### Approve Production
|
||||
|
||||
- Route: /api/ifc/documents/approval/approve-production
|
||||
- Params wajib: document_id
|
||||
- Role: manager produksi
|
||||
|
||||
### Approve Provisioning
|
||||
|
||||
- Route: /api/ifc/documents/approval/approve-provisioning
|
||||
- Params wajib: document_id
|
||||
- Role: manager provisioning
|
||||
|
||||
### Reject
|
||||
|
||||
- Route: /api/ifc/documents/approval/reject
|
||||
- Params wajib: document_id
|
||||
- Params opsional: note
|
||||
|
||||
### Back to Draft
|
||||
|
||||
- Route: /api/ifc/documents/approval/back-draft
|
||||
- Params wajib: document_id
|
||||
|
||||
## 5) Activity Endpoints
|
||||
|
||||
### Add Activity
|
||||
|
||||
- Route: /api/ifc/documents/activities/add
|
||||
- Params wajib: document_id, summary
|
||||
- Params opsional: note, user_id, deadline
|
||||
|
||||
### List Activity
|
||||
|
||||
- Route: /api/ifc/documents/activities/list
|
||||
- Params wajib: document_id
|
||||
- Response: activities aktif dan history internal
|
||||
|
||||
## 6) BoM Endpoints
|
||||
|
||||
### Generate Estimated BoM
|
||||
|
||||
- Route: /api/ifc/documents/bom/generate
|
||||
- Params wajib: document_id
|
||||
|
||||
### Get BoM Data
|
||||
|
||||
- Route: /api/ifc/documents/bom/get
|
||||
- Params wajib: document_id
|
||||
- Response: parsed_bom dan estimated_bom
|
||||
|
||||
### Create Permanent BoM If Availability Fulfilled
|
||||
|
||||
- Route: /api/ifc/documents/bom/create-permanent-if-available
|
||||
- Params wajib: document_id
|
||||
- Rule: hanya berhasil jika availability Estimated BoM terpenuhi
|
||||
- Response: data permanent_bom yang terhubung ke IFC document
|
||||
|
||||
## 7) Temp IFC File Endpoints
|
||||
|
||||
### Generate Signed Temp URL
|
||||
|
||||
- Route: /api/ifc/documents/temp-file
|
||||
- Params wajib: document_id
|
||||
- Response penting: download_url, expires_in_minutes, expires_at_unix
|
||||
|
||||
### Download Temp File
|
||||
|
||||
- Route: /api/ifc/temp/<file_key>?doc_id=...&uid=...&exp=...&sig=...
|
||||
- Method: GET
|
||||
- Auth: user (session)
|
||||
- Catatan: URL signed, ada expiry, terikat user dan dokumen
|
||||
|
||||
## 8) Inventory and Procurement Endpoints
|
||||
|
||||
### Check Availability vs Estimated BoM
|
||||
|
||||
- Route: /api/ifc/documents/inventory/check
|
||||
- Params wajib: document_id
|
||||
- Response: ringkasan all_available, total_shortage_qty, dan detail item per komponen
|
||||
|
||||
### Create Draft Purchase for Shortage
|
||||
|
||||
- Route: /api/ifc/documents/inventory/create-draft-purchase
|
||||
- Params wajib: document_id
|
||||
- Params opsional: vendor_id, only_shortage (default true)
|
||||
- Response: daftar draft purchase.order yang dibuat
|
||||
|
||||
### Create Product from IFC BOM Line
|
||||
|
||||
- Route: /api/ifc/documents/inventory/create-product
|
||||
- Params wajib: document_id, ifc_bom_line_id
|
||||
- Response: data product existing/new + flag created
|
||||
|
||||
## 9) Manufacturing Order Endpoints
|
||||
|
||||
### Create MO If Availability Fulfilled
|
||||
|
||||
- Route: /api/ifc/documents/mo/create-if-available
|
||||
- Params wajib: document_id
|
||||
- Rule: auto memastikan Permanent BoM tersedia, lalu create MO dari Permanent BoM
|
||||
- Response: data manufacturing order yang baru dibuat
|
||||
|
||||
### Create MO from Permanent BoM
|
||||
|
||||
- Route: /api/ifc/documents/mo/create-from-permanent-bom
|
||||
- Params wajib: document_id
|
||||
|
||||
### Check All MOs Availability Summary
|
||||
|
||||
- Route: /api/ifc/documents/mo/check-all-availability
|
||||
- Params wajib: document_id
|
||||
- Response: summary all MOs + summary product requirement vs on hand
|
||||
|
||||
### Confirm All MOs If Available
|
||||
|
||||
- Route: /api/ifc/documents/mo/confirm-all-if-available
|
||||
- Params wajib: document_id
|
||||
- Rule: hanya confirm jika summary all MOs menunjukkan all_available = true
|
||||
- Response: daftar MO yang dikonfirmasi
|
||||
|
||||
### Run End-to-End MO Pipeline
|
||||
|
||||
- Route: /api/ifc/documents/mo/run-pipeline
|
||||
- Params wajib: document_id
|
||||
- Params opsional: auto_confirm (default false)
|
||||
- Alur: check estimated availability -> create permanent bom -> create mo from permanent bom -> check all mo summary -> optional confirm all mo
|
||||
- Response: ringkasan lengkap tiap tahap pipeline
|
||||
|
||||
### Dry Run MO Pipeline (No Data Mutation)
|
||||
|
||||
- Route: /api/ifc/documents/mo/pipeline-dry-run
|
||||
- Params wajib: document_id
|
||||
- Alur: simulasi readiness tiap tahap pipeline tanpa create/update data
|
||||
- Response: preview readiness step, estimated availability, existing MO summary
|
||||
|
||||
### Urutan Rekomendasi Eksekusi MO
|
||||
|
||||
1. /api/ifc/documents/inventory/check
|
||||
2. /api/ifc/documents/bom/create-permanent-if-available
|
||||
3. /api/ifc/documents/mo/create-from-permanent-bom
|
||||
4. /api/ifc/documents/mo/check-all-availability
|
||||
5. /api/ifc/documents/mo/confirm-all-if-available (hanya bila all_available = true)
|
||||
|
||||
## 10) Standard Success and Error
|
||||
|
||||
### Success
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Error
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "...",
|
||||
"details": "optional"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 11) Error Code Quick List
|
||||
|
||||
- BAD_REQUEST
|
||||
- AUTH_FAILED
|
||||
- NOT_FOUND
|
||||
- ACCESS_DENIED
|
||||
- CREATE_FAILED
|
||||
- REVISION_FAILED
|
||||
- PARSE_FAILED
|
||||
- ACTION_FAILED
|
||||
- ACTIVITY_FAILED
|
||||
- BOM_FAILED
|
||||
- DECODE_FAILED
|
||||
- INVENTORY_FAILED
|
||||
- PURCHASE_DRAFT_FAILED
|
||||
- PRODUCT_CREATE_FAILED
|
||||
- MO_CREATE_FAILED
|
||||
- BOM_PERMANENT_FAILED
|
||||
- MO_AVAILABILITY_FAILED
|
||||
- MO_CONFIRM_FAILED
|
||||
- MO_PIPELINE_FAILED
|
||||
- MO_PIPELINE_DRY_RUN_FAILED
|
||||
|
||||
## 12) Integration Checklist
|
||||
|
||||
- Login session berhasil dan cookie terkirim otomatis.
|
||||
- Upload create atau revise dokumen IFC sukses.
|
||||
- Parse IFC sukses dan state berubah.
|
||||
- Approval flow berjalan sesuai role.
|
||||
- Activity add atau list berjalan per dokumen.
|
||||
- Generate atau get BoM sukses.
|
||||
- Create permanent BoM dari estimated BoM sukses.
|
||||
- Cek inventory availability dan trigger draft purchase sukses.
|
||||
- Endpoint create product dari IFC BOM line berjalan.
|
||||
- Endpoint create MO dari permanent BoM berjalan.
|
||||
- Endpoint check-all-MOs availability summary dan confirm-all-MOs berjalan.
|
||||
- Endpoint run-pipeline berjalan untuk eksekusi flow end-to-end.
|
||||
- Endpoint pipeline-dry-run berjalan untuk preview sebelum eksekusi real.
|
||||
- Temp file URL bisa dipakai viewer.
|
||||
- Jika URL expired, frontend meminta URL baru.
|
||||
@@ -0,0 +1,330 @@
|
||||
# Backend Technical Guide
|
||||
|
||||
Dokumen ini ditujukan untuk developer backend Odoo 19 yang akan memelihara dan mengembangkan modul Product Engineering IFC.
|
||||
|
||||
Dokumen ini adalah panduan rekomendasi implementasi, bukan kontrak teknis yang kaku.
|
||||
Isi dokumen dapat direview dan direvisi agar selalu selaras dengan implementasi aktual.
|
||||
|
||||
Lihat juga indeks dokumentasi utama di [README.md](README.md).
|
||||
|
||||
## 1) Scope Modul
|
||||
|
||||
Modul utama:
|
||||
|
||||
- grt_ifcopenshell: parsing IFC, ekstraksi struktur, dan BOM parse awal.
|
||||
- grt_product_engineering: document management, revision tracking, approval workflow, KPI, dan JSON-RPC API untuk integrasi frontend.
|
||||
|
||||
Tujuan backend:
|
||||
|
||||
- Menyediakan endpoint berbasis JSON-RPC dengan session authentication.
|
||||
- Menangani upload dan revisi dokumen IFC.
|
||||
- Menangani activity check/approval dan riwayat aktivitas.
|
||||
- Menyediakan endpoint decode file IFC dari binary Odoo ke temp file yang aman.
|
||||
|
||||
## 2) Struktur Teknis Penting
|
||||
|
||||
Controller API:
|
||||
|
||||
- grt_product_engineering/controllers/ifc_jsonrpc.py
|
||||
|
||||
Model inti:
|
||||
|
||||
- grt.ifc.project
|
||||
- grt.ifc.document.revision
|
||||
- grt.ifc.activity.history
|
||||
- mrp.bom (inherit: ifc_project_id)
|
||||
- mrp.production (inherit: ifc_project_id)
|
||||
|
||||
Keterangan:
|
||||
|
||||
- Session login/logout/me via endpoint custom.
|
||||
- Semua endpoint bisnis memakai auth user.
|
||||
- Kontrol akses memanfaatkan check_access_rights dan check_access_rule.
|
||||
|
||||
## 2.1) State Model Aktual
|
||||
|
||||
State parsing dokumen IFC:
|
||||
|
||||
- draft -> parsed
|
||||
- draft -> failed
|
||||
|
||||
State approval dokumen:
|
||||
|
||||
- draft -> in_validation -> waiting_production -> waiting_provisioning -> approved
|
||||
- reject dapat terjadi pada in_validation, waiting_production, waiting_provisioning
|
||||
|
||||
Catatan:
|
||||
|
||||
- Gunakan state backend ini sebagai sumber kebenaran untuk backend dan frontend.
|
||||
- Jika frontend butuh label status bisnis tambahan, gunakan derived status tanpa mengubah state backend inti.
|
||||
|
||||
## 3) Kontrak Authentication Session
|
||||
|
||||
Endpoint login authentication yang direkomendasikan:
|
||||
|
||||
- Primary: /api/session/login
|
||||
- Fallback standar Odoo: /web/session/authenticate
|
||||
|
||||
Endpoint login:
|
||||
|
||||
- URL: /api/session/login
|
||||
- Type: json
|
||||
- Auth: none
|
||||
- Method: POST
|
||||
- Params wajib: db, login, password
|
||||
|
||||
Contoh payload JSON-RPC untuk endpoint primary:
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {
|
||||
"db": "odoo19",
|
||||
"login": "user_backend",
|
||||
"password": "******"
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
|
||||
Contoh payload JSON-RPC untuk fallback /web/session/authenticate:
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {
|
||||
"db": "odoo19",
|
||||
"login": "user_backend",
|
||||
"password": "******"
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
|
||||
Response sukses:
|
||||
|
||||
{
|
||||
"success": true,
|
||||
"result": {
|
||||
"uid": 7,
|
||||
"name": "User A",
|
||||
"login": "user_backend",
|
||||
"db": "odoo19",
|
||||
"session_id": "..."
|
||||
}
|
||||
}
|
||||
|
||||
Endpoint logout:
|
||||
|
||||
- URL: /api/session/logout
|
||||
- Type: json
|
||||
- Auth: user
|
||||
|
||||
Endpoint me:
|
||||
|
||||
- URL: /api/session/me
|
||||
- Type: json
|
||||
- Auth: user
|
||||
|
||||
## 4) Daftar Endpoint API Dokumen
|
||||
|
||||
### 4.1 Dokumen IFC
|
||||
|
||||
- /api/ifc/documents/list
|
||||
- /api/ifc/documents/create
|
||||
- /api/ifc/documents/revise
|
||||
- /api/ifc/documents/parse
|
||||
|
||||
Ketentuan create:
|
||||
|
||||
- file_data harus base64 string.
|
||||
- revision_note_input otomatis mengisi catatan revisi awal.
|
||||
|
||||
Ketentuan revise:
|
||||
|
||||
- membentuk revisi baru otomatis melalui method write model.
|
||||
|
||||
### 4.2 Approval Workflow
|
||||
|
||||
- /api/ifc/documents/approval/request-validation
|
||||
- /api/ifc/documents/approval/submit
|
||||
- /api/ifc/documents/approval/approve-production
|
||||
- /api/ifc/documents/approval/approve-provisioning
|
||||
- /api/ifc/documents/approval/reject
|
||||
- /api/ifc/documents/approval/back-draft
|
||||
|
||||
Workflow state:
|
||||
|
||||
- draft -> in_validation -> waiting_production -> waiting_provisioning -> approved
|
||||
- reject dapat terjadi pada in_validation, waiting_production, waiting_provisioning
|
||||
|
||||
### 4.3 Activity
|
||||
|
||||
- /api/ifc/documents/activities/add
|
||||
- /api/ifc/documents/activities/list
|
||||
|
||||
Activity list mengembalikan:
|
||||
|
||||
- data mail.activity yang aktif
|
||||
- history internal grt.ifc.activity.history
|
||||
|
||||
### 4.4 BoM
|
||||
|
||||
- /api/ifc/documents/bom/generate
|
||||
- /api/ifc/documents/bom/get
|
||||
- /api/ifc/documents/bom/create-permanent-if-available
|
||||
|
||||
BoM generate:
|
||||
|
||||
- memanggil action_create_bom_estimate di model dokumen
|
||||
- hasil mencakup estimated_bom
|
||||
|
||||
### 4.5 Temp IFC File
|
||||
|
||||
- /api/ifc/documents/temp-file
|
||||
- /api/ifc/temp/<file_key>
|
||||
|
||||
Flow:
|
||||
|
||||
1. frontend minta temp-file via JSON-RPC
|
||||
2. backend decode binary ke folder temp
|
||||
3. backend kirim signed download_url
|
||||
4. frontend GET download_url dengan session cookie
|
||||
|
||||
### 4.6 Inventory and Procurement
|
||||
|
||||
- /api/ifc/documents/inventory/check
|
||||
- /api/ifc/documents/inventory/create-draft-purchase
|
||||
- /api/ifc/documents/inventory/create-product
|
||||
|
||||
### 4.7 Manufacturing Order
|
||||
|
||||
- /api/ifc/documents/mo/create-if-available
|
||||
- /api/ifc/documents/mo/create-from-permanent-bom
|
||||
- /api/ifc/documents/mo/check-all-availability
|
||||
- /api/ifc/documents/mo/confirm-all-if-available
|
||||
- /api/ifc/documents/mo/run-pipeline
|
||||
- /api/ifc/documents/mo/pipeline-dry-run
|
||||
|
||||
Aturan endpoint MO:
|
||||
|
||||
- Estimated BoM harus availability terpenuhi dulu untuk membuat Permanent BoM.
|
||||
- MO dibuat dari Permanent BoM yang terhubung ke dokumen IFC.
|
||||
- Confirm all MO hanya boleh saat summary availability seluruh MO terpenuhi.
|
||||
- Gunakan pipeline-dry-run untuk simulasi readiness tanpa perubahan data.
|
||||
- Jika ada shortage, endpoint mengembalikan error yang sesuai (MO_CREATE_FAILED/MO_AVAILABILITY_FAILED/MO_CONFIRM_FAILED).
|
||||
|
||||
Urutan pipeline rekomendasi:
|
||||
|
||||
1. inventory check terhadap Estimated BoM
|
||||
2. create Permanent BoM jika availability terpenuhi
|
||||
3. create MO dari Permanent BoM
|
||||
4. check all MO availability summary
|
||||
5. confirm all MO hanya jika all_available bernilai true
|
||||
|
||||
## 5) Signed URL Security
|
||||
|
||||
Implementasi signed URL:
|
||||
|
||||
- Signature: HMAC SHA-256
|
||||
- Payload signature: file_key | doc_id | uid | exp
|
||||
- Secret source: ir.config_parameter database.secret (fallback database.uuid, fallback db name)
|
||||
|
||||
Validasi saat GET file:
|
||||
|
||||
- doc_id, uid, exp, sig wajib ada
|
||||
- exp belum lewat
|
||||
- uid di URL harus sama dengan user session
|
||||
- signature harus valid
|
||||
- user harus punya akses read ke dokumen
|
||||
|
||||
Keuntungan:
|
||||
|
||||
- mencegah penggunaan URL temp lintas user
|
||||
- membatasi masa berlaku URL
|
||||
|
||||
## 6) Temp File Management
|
||||
|
||||
Lokasi decode:
|
||||
|
||||
- system temp dir / odoo_ifc_temp
|
||||
|
||||
Masa berlaku:
|
||||
|
||||
- TEMP_TTL_MINUTES = 60
|
||||
|
||||
Cleanup:
|
||||
|
||||
- dilakukan saat endpoint temp-file dipanggil
|
||||
- file lama dihapus berdasarkan modified time
|
||||
|
||||
Catatan operasi:
|
||||
|
||||
- jika traffic tinggi, pertimbangkan scheduler cron cleanup periodik
|
||||
- jika storage temp shared, pastikan izin file aman
|
||||
|
||||
## 7) JSON-RPC Error Contract
|
||||
|
||||
Format error backend:
|
||||
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "ACCESS_DENIED",
|
||||
"message": "Tidak punya akses ke dokumen.",
|
||||
"details": "optional"
|
||||
}
|
||||
}
|
||||
|
||||
Contoh code yang dipakai:
|
||||
|
||||
- BAD_REQUEST
|
||||
- AUTH_FAILED
|
||||
- NOT_FOUND
|
||||
- ACCESS_DENIED
|
||||
- CREATE_FAILED
|
||||
- REVISION_FAILED
|
||||
- PARSE_FAILED
|
||||
- ACTION_FAILED
|
||||
- ACTIVITY_FAILED
|
||||
- BOM_FAILED
|
||||
- BOM_PERMANENT_FAILED
|
||||
- DECODE_FAILED
|
||||
- INVENTORY_FAILED
|
||||
- PURCHASE_DRAFT_FAILED
|
||||
- PRODUCT_CREATE_FAILED
|
||||
- MO_CREATE_FAILED
|
||||
- MO_AVAILABILITY_FAILED
|
||||
- MO_CONFIRM_FAILED
|
||||
- MO_PIPELINE_FAILED
|
||||
- MO_PIPELINE_DRY_RUN_FAILED
|
||||
|
||||
## 8) Checklist Backend Developer
|
||||
|
||||
Checklist implementasi:
|
||||
|
||||
- Pastikan dependency module: grt_ifcopenshell, mail, mrp, purchase, web.
|
||||
- Pastikan route controller ter-load dari __init__.
|
||||
- Verifikasi ACL dan record rule untuk model terkait.
|
||||
- Uji semua endpoint dengan user role berbeda.
|
||||
- Uji approval workflow role produksi dan provisioning.
|
||||
- Uji signed URL expiry dan mismatch user.
|
||||
- Uji beban upload file IFC berukuran besar.
|
||||
- Uji alur permanent BoM dan MO pipeline (termasuk dry-run).
|
||||
|
||||
Checklist release:
|
||||
|
||||
- Uji modul install dan upgrade.
|
||||
- Uji parse IFC sample dan IFC produksi.
|
||||
- Uji generate estimated BoM.
|
||||
- Uji inventory check dan draft purchase.
|
||||
- Uji run-pipeline dan confirm-all-if-available.
|
||||
- Uji rollback saat error parse.
|
||||
|
||||
## 9) Saran Pengembangan Lanjutan
|
||||
|
||||
Prioritas teknis:
|
||||
|
||||
- Tambah endpoint complete activity dan comment activity.
|
||||
- Tambah audit log request API untuk tracing.
|
||||
- Tambah rate limit pada endpoint login dan temp-file.
|
||||
- Tambah pagination metadata standard untuk endpoint list.
|
||||
- Tambah endpoint health-check API terpisah.
|
||||
@@ -0,0 +1,260 @@
|
||||
# Frontend Technical Guide
|
||||
|
||||
Dokumen ini ditujukan untuk developer frontend (Vue.js + three.js + ThatOpen) yang akan mengintegrasikan aplikasi dengan backend Odoo 19 Product Engineering.
|
||||
|
||||
Lihat juga indeks dokumentasi utama di [README.md](README.md).
|
||||
|
||||
## 1) Prinsip Integrasi
|
||||
|
||||
Prinsip utama:
|
||||
|
||||
- Gunakan JSON-RPC endpoint Odoo type json untuk operasi bisnis.
|
||||
- Gunakan session cookie untuk auth setelah login.
|
||||
- Jangan simpan password user di local storage.
|
||||
- Untuk file IFC viewer, gunakan signed URL dari endpoint temp-file.
|
||||
|
||||
## 2) Alur Integrasi End-to-End
|
||||
|
||||
Urutan umum:
|
||||
|
||||
1. Login session
|
||||
2. List atau create dokumen IFC
|
||||
3. Parse IFC
|
||||
4. Submit approval sesuai alur bisnis
|
||||
5. Tambah atau tampilkan activity per dokumen
|
||||
6. Generate BoM saat dibutuhkan
|
||||
7. Minta signed temp URL
|
||||
8. Load IFC file ke viewer menggunakan URL temp
|
||||
|
||||
## 3) Format Request JSON-RPC
|
||||
|
||||
Gunakan body seperti berikut untuk endpoint type json Odoo:
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "call",
|
||||
"params": {
|
||||
"document_id": 12
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
|
||||
Catatan:
|
||||
|
||||
- Header yang umum: Content-Type: application/json
|
||||
- Session cookie harus otomatis dikirim browser
|
||||
- Jika frontend beda domain, pastikan konfigurasi CORS/proxy sesuai
|
||||
|
||||
## 4) Session Authentication
|
||||
|
||||
Endpoint login authentication yang digunakan frontend:
|
||||
|
||||
- Primary: /api/session/login
|
||||
- Fallback standar Odoo: /web/session/authenticate
|
||||
|
||||
### 4.1 Login
|
||||
|
||||
Endpoint:
|
||||
|
||||
- /api/session/login
|
||||
|
||||
Params wajib:
|
||||
|
||||
- db
|
||||
- login
|
||||
- password
|
||||
|
||||
Contoh params:
|
||||
|
||||
{
|
||||
"db": "odoo19",
|
||||
"login": "user_at_company",
|
||||
"password": "******"
|
||||
}
|
||||
|
||||
### 4.2 Cek sesi aktif
|
||||
|
||||
Endpoint:
|
||||
|
||||
- /api/session/me
|
||||
|
||||
Gunakan untuk:
|
||||
|
||||
- restore session saat reload page
|
||||
- validasi user sebelum load modul viewer
|
||||
|
||||
### 4.3 Logout
|
||||
|
||||
Endpoint:
|
||||
|
||||
- /api/session/logout
|
||||
|
||||
## 5) Endpoint Operasional Frontend
|
||||
|
||||
### 5.1 Dokumen
|
||||
|
||||
- /api/ifc/documents/list
|
||||
- /api/ifc/documents/create
|
||||
- /api/ifc/documents/revise
|
||||
- /api/ifc/documents/parse
|
||||
|
||||
Saran UI:
|
||||
|
||||
- Setelah create atau revise, tampilkan status revisi terbaru.
|
||||
- Setelah parse, refresh detail dokumen agar metrik parse terlihat.
|
||||
|
||||
### 5.2 Approval
|
||||
|
||||
- /api/ifc/documents/approval/request-validation
|
||||
- /api/ifc/documents/approval/submit
|
||||
- /api/ifc/documents/approval/approve-production
|
||||
- /api/ifc/documents/approval/approve-provisioning
|
||||
- /api/ifc/documents/approval/reject
|
||||
- /api/ifc/documents/approval/back-draft
|
||||
|
||||
Saran UI:
|
||||
|
||||
- Tombol aksi ditampilkan berdasarkan role user dan approval_state.
|
||||
- Tampilkan reason reject di form revisi berikutnya.
|
||||
|
||||
### 5.3 Activity
|
||||
|
||||
- /api/ifc/documents/activities/add
|
||||
- /api/ifc/documents/activities/list
|
||||
|
||||
Saran UI:
|
||||
|
||||
- Tampilkan panel Activity (mail.activity) dan panel History terpisah.
|
||||
- Sort activity berdasarkan deadline terdekat.
|
||||
|
||||
### 5.4 BoM
|
||||
|
||||
- /api/ifc/documents/bom/generate
|
||||
- /api/ifc/documents/bom/get
|
||||
- /api/ifc/documents/bom/create-permanent-if-available
|
||||
|
||||
Saran UI:
|
||||
|
||||
- Gunakan parsed_bom untuk tree part IFC.
|
||||
- Gunakan estimated_bom untuk ringkasan manufacturing.
|
||||
- Setelah estimated BoM siap dan available, trigger create-permanent-if-available.
|
||||
|
||||
### 5.5 Inventory and Procurement
|
||||
|
||||
- /api/ifc/documents/inventory/check
|
||||
- /api/ifc/documents/inventory/create-draft-purchase
|
||||
- /api/ifc/documents/inventory/create-product
|
||||
|
||||
Saran UI:
|
||||
|
||||
- Jalankan inventory/check sebelum tombol Create MO.
|
||||
- Jika shortage > 0, tampilkan opsi create draft purchase.
|
||||
- Jika komponen belum punya product, tampilkan opsi create product.
|
||||
|
||||
### 5.6 Manufacturing Order
|
||||
|
||||
- /api/ifc/documents/mo/create-if-available
|
||||
- /api/ifc/documents/mo/create-from-permanent-bom
|
||||
- /api/ifc/documents/mo/check-all-availability
|
||||
- /api/ifc/documents/mo/confirm-all-if-available
|
||||
- /api/ifc/documents/mo/run-pipeline
|
||||
- /api/ifc/documents/mo/pipeline-dry-run
|
||||
|
||||
Saran UI:
|
||||
|
||||
- Aktifkan tombol Create MO hanya jika permanent BoM sudah ada dan availability terpenuhi.
|
||||
- Setelah ada beberapa MO, tampilkan summary kebutuhan produk dari check-all-availability.
|
||||
- Aktifkan tombol Confirm All MOs hanya jika all_available = true pada summary all MOs.
|
||||
- Untuk mode otomatis, gunakan run-pipeline dengan auto_confirm=true.
|
||||
- Untuk preview tanpa perubahan data, gunakan pipeline-dry-run sebelum eksekusi real.
|
||||
- Jika endpoint gagal karena shortage, arahkan user ke workflow procurement.
|
||||
|
||||
## 6) Integrasi IFC Viewer (ThatOpen)
|
||||
|
||||
Flow render IFC:
|
||||
|
||||
1. Panggil /api/ifc/documents/temp-file dengan document_id
|
||||
2. Backend mengembalikan download_url signed
|
||||
3. Frontend melakukan HTTP GET ke download_url
|
||||
4. Berikan URL atau blob ke pipeline loader IFC viewer
|
||||
|
||||
Penting:
|
||||
|
||||
- download_url memiliki expiry
|
||||
- jika 403 atau expired, minta ulang endpoint temp-file
|
||||
- jangan cache signed URL terlalu lama
|
||||
|
||||
## 7) Error Handling Strategy
|
||||
|
||||
Kontrak error:
|
||||
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "PARSE_FAILED",
|
||||
"message": "...",
|
||||
"details": "optional"
|
||||
}
|
||||
}
|
||||
|
||||
Saran mapping UI:
|
||||
|
||||
- BAD_REQUEST: validasi input form
|
||||
- AUTH_FAILED: redirect login
|
||||
- ACCESS_DENIED: tampilkan notifikasi izin
|
||||
- NOT_FOUND: refresh list lalu tampilkan warning
|
||||
- DECODE_FAILED: minta user upload ulang
|
||||
|
||||
Retry policy:
|
||||
|
||||
- endpoint temp-file: retry 1 kali jika timeout
|
||||
- endpoint parse: jangan auto-retry, butuh konfirmasi user
|
||||
- endpoint approval: jangan auto-retry untuk hindari duplikasi aksi
|
||||
|
||||
## 8) State Management Recommendation
|
||||
|
||||
Store minimal:
|
||||
|
||||
- auth: uid, name, login, isAuthenticated
|
||||
- documents: list, selectedDocument, paging
|
||||
- activities: listByDocument
|
||||
- approval: availableActions
|
||||
- viewer: currentIfcUrl, loadState, error
|
||||
|
||||
Event yang perlu memicu refresh data:
|
||||
|
||||
- setelah revise
|
||||
- setelah parse
|
||||
- setelah approval action
|
||||
- setelah activity add
|
||||
- setelah bom generate
|
||||
|
||||
## 9) Security Recommendation untuk Frontend
|
||||
|
||||
Checklist keamanan:
|
||||
|
||||
- Hindari menyimpan kredensial user secara permanen.
|
||||
- Pastikan semua request menggunakan HTTPS.
|
||||
- Jangan expose signed URL ke log publik.
|
||||
- Bersihkan state viewer saat logout.
|
||||
- Handle 401 dan 403 secara terpusat.
|
||||
|
||||
## 10) Checklist Integrasi Frontend
|
||||
|
||||
Checklist tahap awal:
|
||||
|
||||
- Login dan persist session berhasil.
|
||||
- List dokumen dan detail dokumen berhasil.
|
||||
- Upload create dan revise berhasil untuk file IFC nyata.
|
||||
- Parse IFC sukses dan state berubah.
|
||||
- Activity add/list tampil di UI per dokumen.
|
||||
- Approval flow berjalan sesuai role.
|
||||
- BoM generate/get tampil di panel BOM.
|
||||
- Viewer berhasil load file dari signed temp URL.
|
||||
|
||||
Checklist UAT:
|
||||
|
||||
- Uji dokumen besar.
|
||||
- Uji expiry signed URL saat viewer masih terbuka.
|
||||
- Uji pergantian role manager produksi/provisioning.
|
||||
- Uji sesi kadaluarsa lalu login ulang.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Product Engineering IFC Blueprint
|
||||
## Odoo 19 + IfcOpenShell + Vue.js 3D Viewer
|
||||
|
||||
## Document Positioning
|
||||
|
||||
Dokumen ini adalah dokumen rekomendasi arsitektur dan implementasi.
|
||||
Dokumen ini bukan kontrak teknis kaku.
|
||||
|
||||
Dokumen ini boleh direview, ditingkatkan, dan direvisi agar selalu selaras dengan implementasi aktual di:
|
||||
|
||||
- grt_ifcopenshell
|
||||
- grt_product_engineering
|
||||
|
||||
## Objective
|
||||
|
||||
Menyediakan panduan praktis pengembangan Product Engineering berbasis IFC yang:
|
||||
|
||||
- realistis terhadap implementasi backend saat ini
|
||||
- tetap memberi arah pengembangan lanjutan
|
||||
- memisahkan dengan jelas fitur yang sudah berjalan dan fitur roadmap
|
||||
|
||||
## Current Implementation Scope
|
||||
|
||||
### Modul dan Peran
|
||||
|
||||
- grt_ifcopenshell
|
||||
- upload/parse IFC
|
||||
- ekstraksi entitas IFC
|
||||
- pembentukan struktur IFC BOM awal
|
||||
- grt_product_engineering
|
||||
- document management dan revision tracking
|
||||
- approval workflow 2 layer
|
||||
- activity history
|
||||
- estimated BoM, permanent BoM, MO pipeline
|
||||
- inventory check, draft purchase, create missing product
|
||||
- JSON-RPC API untuk integrasi frontend
|
||||
|
||||
### Status Model Aktual
|
||||
|
||||
State parsing dokumen IFC:
|
||||
|
||||
```text
|
||||
draft -> parsed | failed
|
||||
```
|
||||
|
||||
State approval dokumen:
|
||||
|
||||
```text
|
||||
draft -> in_validation -> waiting_production -> waiting_provisioning -> approved
|
||||
\-> rejected
|
||||
```
|
||||
|
||||
Catatan:
|
||||
|
||||
- Status di atas adalah status backend aktual yang harus dijadikan acuan implementasi UI/workflow.
|
||||
- Status bisnis tambahan dapat ditampilkan di frontend sebagai derived status, bukan mengganti state backend.
|
||||
|
||||
## Aligned End-to-End Workflow (Recommended)
|
||||
|
||||
Urutan berikut diselaraskan dengan implementasi backend terbaru:
|
||||
|
||||
```text
|
||||
Create Engineering Document
|
||||
-> Upload IFC
|
||||
-> Parse IFC (IfcOpenShell in-process di Odoo)
|
||||
-> Review hasil parse dan IFC BOM
|
||||
-> Create/Map Product bila diperlukan
|
||||
-> Generate Estimated BoM
|
||||
-> Inventory Availability Check (Estimated BoM)
|
||||
-> Create Draft Purchase untuk shortage bila perlu
|
||||
-> Approval Workflow (Production lalu Provisioning)
|
||||
-> Create Permanent BoM (hanya jika availability memenuhi)
|
||||
-> Create MO from Permanent BoM
|
||||
-> Check All MO Availability Summary
|
||||
-> Confirm All MO jika seluruh requirement tersedia
|
||||
-> Start Production (proses MRP standar)
|
||||
```
|
||||
|
||||
Catatan penting:
|
||||
|
||||
- Permanent BoM tidak dibuat sebelum Estimated BoM dinyatakan available.
|
||||
- MO dibuat dari Permanent BoM.
|
||||
- Tersedia dry-run pipeline untuk simulasi tanpa perubahan data.
|
||||
|
||||
## Recommended Architecture (Aligned)
|
||||
|
||||
Arsitektur saat ini:
|
||||
|
||||
```text
|
||||
Vue.js 3D Viewer
|
||||
|
|
||||
v
|
||||
Odoo 19 (JSON-RPC API)
|
||||
|
|
||||
v
|
||||
grt_ifcopenshell (in-process Python library)
|
||||
|
|
||||
v
|
||||
PostgreSQL
|
||||
```
|
||||
|
||||
Catatan roadmap:
|
||||
|
||||
- IfcOpenShell Service terpisah dapat dipertimbangkan di masa depan untuk kebutuhan scaling khusus.
|
||||
- Saat ini implementasi berjalan in-process di Odoo, bukan service terpisah.
|
||||
|
||||
## Key Principles
|
||||
|
||||
1. IFC tidak langsung menjadi BoM resmi.
|
||||
2. Semua revisi dokumen harus tersimpan dan dapat diaudit.
|
||||
3. Approval wajib sebelum pembentukan Permanent BoM dan proses MO.
|
||||
4. Permanent BoM dan MO harus tertelusur ke dokumen IFC sumber.
|
||||
5. Alur inventory/procurement harus mencegah konfirmasi MO saat requirement belum terpenuhi.
|
||||
6. Fitur roadmap tidak boleh diasumsikan sebagai fitur produksi sebelum tersedia di modul.
|
||||
|
||||
## Feature Alignment Matrix
|
||||
|
||||
### Implemented
|
||||
|
||||
- Upload IFC
|
||||
- Revision control dan revision history
|
||||
- IFC parsing dan IFC BOM extraction
|
||||
- Approval workflow 2 tahap
|
||||
- Estimated BoM generation
|
||||
- Inventory availability check
|
||||
- Draft purchase creation untuk shortage
|
||||
- Missing product creation dari IFC BOM line
|
||||
- Permanent BoM creation (gated by availability)
|
||||
- MO creation from permanent BoM
|
||||
- MO availability summary + confirm all if available
|
||||
- MO pipeline run + pipeline dry-run
|
||||
- Session login/logout/me API
|
||||
- Temp IFC file decode + signed URL download
|
||||
|
||||
### Partially Implemented / Integration-Dependent
|
||||
|
||||
- IFC 3D Viewer end-to-end UX (bergantung implementasi frontend)
|
||||
- Product mapping UX yang lebih kaya (saat ini dominan backend/API)
|
||||
|
||||
### Roadmap
|
||||
|
||||
- ECO Integration
|
||||
- Visual Revision Comparison
|
||||
- Clash Detection
|
||||
- Cost Simulation
|
||||
- AI Mapping Recommendation
|
||||
- AI Quantity Validation
|
||||
- AI Procurement Recommendation
|
||||
- Quality domain model khusus dan QC workflow khusus Product Engineering
|
||||
|
||||
## API Capability Snapshot (High Level)
|
||||
|
||||
- Session: login, logout, me
|
||||
- Document: list, create, revise, parse
|
||||
- Approval: request-validation, submit, approve-production, approve-provisioning, reject, back-draft
|
||||
- Activity: add, list
|
||||
- BoM: generate, get, create-permanent-if-available
|
||||
- Inventory: check, create-draft-purchase, create-product
|
||||
- MO: create-from-permanent-bom, check-all-availability, confirm-all-if-available, run-pipeline, pipeline-dry-run
|
||||
- Temp IFC: temp-file request dan signed download
|
||||
|
||||
Lihat detail endpoint teknis pada dokumen backend API guide.
|
||||
|
||||
## Revision Policy for This Blueprint
|
||||
|
||||
Setiap perubahan signifikan pada backend harus diikuti pembaruan blueprint ini, minimal pada:
|
||||
|
||||
- status model dan alur state
|
||||
- urutan workflow inti
|
||||
- matriks fitur implemented vs roadmap
|
||||
- catatan arsitektur aktual
|
||||
|
||||
Dengan pendekatan ini, dokumen tetap menjadi panduan rekomendasi yang berguna, namun tetap konsisten dengan implementasi terbaik yang sedang berjalan.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Product Engineering API Docs Index
|
||||
|
||||
Dokumentasi ini menjadi pintu masuk utama untuk tim backend dan frontend yang mengintegrasikan Odoo 19 Product Engineering dengan IFC Viewer.
|
||||
|
||||
## Dokumen Utama
|
||||
|
||||
1. Backend technical developer guide:
|
||||
[BACKEND_TECHNICAL_GUIDE.md](BACKEND_TECHNICAL_GUIDE.md)
|
||||
|
||||
2. Frontend technical developer guide:
|
||||
[FRONTEND_TECHNICAL_GUIDE.md](FRONTEND_TECHNICAL_GUIDE.md)
|
||||
|
||||
3. API quick reference (cheat sheet):
|
||||
[API_QUICK_REFERENCE.md](API_QUICK_REFERENCE.md)
|
||||
|
||||
## Peta Endpoint
|
||||
|
||||
### Session
|
||||
|
||||
- Primary login authentication: /api/session/login
|
||||
- Standard Odoo login fallback: /web/session/authenticate
|
||||
- /api/session/login
|
||||
- /api/session/logout
|
||||
- /api/session/me
|
||||
|
||||
### Document IFC
|
||||
|
||||
- /api/ifc/documents/list
|
||||
- /api/ifc/documents/create
|
||||
- /api/ifc/documents/revise
|
||||
- /api/ifc/documents/parse
|
||||
|
||||
### Approval Workflow
|
||||
|
||||
- /api/ifc/documents/approval/request-validation
|
||||
- /api/ifc/documents/approval/submit
|
||||
- /api/ifc/documents/approval/approve-production
|
||||
- /api/ifc/documents/approval/approve-provisioning
|
||||
- /api/ifc/documents/approval/reject
|
||||
- /api/ifc/documents/approval/back-draft
|
||||
|
||||
### Activity
|
||||
|
||||
- /api/ifc/documents/activities/add
|
||||
- /api/ifc/documents/activities/list
|
||||
|
||||
### BoM
|
||||
|
||||
- /api/ifc/documents/bom/generate
|
||||
- /api/ifc/documents/bom/get
|
||||
- /api/ifc/documents/bom/create-permanent-if-available
|
||||
|
||||
### Inventory and Procurement
|
||||
|
||||
- /api/ifc/documents/inventory/check
|
||||
- /api/ifc/documents/inventory/create-draft-purchase
|
||||
- /api/ifc/documents/inventory/create-product
|
||||
|
||||
### Manufacturing Order
|
||||
|
||||
- /api/ifc/documents/mo/create-if-available
|
||||
- /api/ifc/documents/mo/create-from-permanent-bom
|
||||
- /api/ifc/documents/mo/check-all-availability
|
||||
- /api/ifc/documents/mo/confirm-all-if-available
|
||||
- /api/ifc/documents/mo/run-pipeline
|
||||
- /api/ifc/documents/mo/pipeline-dry-run
|
||||
|
||||
### Temp IFC File
|
||||
|
||||
- /api/ifc/documents/temp-file
|
||||
- /api/ifc/temp/<file_key>
|
||||
|
||||
## Alur Integrasi Cepat
|
||||
|
||||
1. Login sesi dengan endpoint session login.
|
||||
2. Buat dokumen IFC dengan documents create.
|
||||
3. Parse file IFC dengan documents parse.
|
||||
4. Jika perlu, lakukan revisi dengan documents revise.
|
||||
5. Jalankan approval bertahap sesuai role.
|
||||
6. Buat activity dan monitor activity list.
|
||||
7. Generate estimated BoM.
|
||||
8. Create Permanent BoM jika availability Estimated BoM terpenuhi.
|
||||
9. Create MO dari Permanent BoM.
|
||||
10. Cek availability all MOs dan summary material.
|
||||
11. Confirm all MOs jika availability summary terpenuhi.
|
||||
12. Minta signed URL temp-file dan load IFC ke viewer.
|
||||
|
||||
## Catatan Keamanan Penting
|
||||
|
||||
- Semua endpoint bisnis membutuhkan session user aktif.
|
||||
- Temp file IFC hanya diakses melalui signed URL ber-expiry.
|
||||
- Signed URL terikat ke document id dan user id.
|
||||
- Frontend harus re-request temp-file jika URL expired.
|
||||
|
||||
## Rekomendasi Operasional
|
||||
|
||||
- Gunakan HTTPS di semua environment.
|
||||
- Tambahkan monitoring error per endpoint.
|
||||
- Audit role akses manager produksi dan provisioning secara periodik.
|
||||
- Uji dokumen IFC ukuran besar saat UAT.
|
||||
@@ -0,0 +1,6 @@
|
||||
from . import ifc_project_engineering
|
||||
from . import ifc_document_revision
|
||||
from . import ifc_activity_history
|
||||
from . import mrp_bom
|
||||
from . import mrp_production
|
||||
from . import ifc_kpi_dashboard
|
||||
@@ -0,0 +1,40 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class IfcActivityHistory(models.Model):
|
||||
_name = 'grt.ifc.activity.history'
|
||||
_description = 'IFC Activity History'
|
||||
_order = 'activity_date desc, id desc'
|
||||
|
||||
project_id = fields.Many2one(
|
||||
'grt.ifc.project',
|
||||
string='IFC Document',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
index=True,
|
||||
)
|
||||
activity_type = fields.Selection(
|
||||
[
|
||||
('upload', 'Upload'),
|
||||
('parse', 'Parse IFC'),
|
||||
('revision', 'Revision'),
|
||||
('validation', 'Validation'),
|
||||
('approval_production', 'Approval Produksi'),
|
||||
('approval_provisioning', 'Approval Provisioning'),
|
||||
('rejected', 'Rejected'),
|
||||
('bom_estimate', 'BOM Estimate'),
|
||||
('manufacturing_order', 'Manufacturing Order'),
|
||||
],
|
||||
required=True,
|
||||
index=True,
|
||||
)
|
||||
activity_date = fields.Datetime(string='Date', default=fields.Datetime.now, required=True, index=True)
|
||||
user_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='By',
|
||||
default=lambda self: self.env.user,
|
||||
required=True,
|
||||
index=True,
|
||||
)
|
||||
note = fields.Text(string='Notes')
|
||||
state_snapshot = fields.Char(string='State Snapshot')
|
||||
@@ -0,0 +1,33 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class IfcDocumentRevision(models.Model):
|
||||
_name = 'grt.ifc.document.revision'
|
||||
_description = 'IFC Document Revision'
|
||||
_order = 'revision_number desc, id desc'
|
||||
|
||||
project_id = fields.Many2one(
|
||||
'grt.ifc.project',
|
||||
string='IFC Document',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
index=True,
|
||||
)
|
||||
revision_number = fields.Integer(string='Revision', required=True, default=1, index=True)
|
||||
revision_label = fields.Char(string='Revision Label', compute='_compute_revision_label', store=True)
|
||||
revision_date = fields.Datetime(string='Revision Date', default=fields.Datetime.now, required=True)
|
||||
revised_by_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Revised By',
|
||||
default=lambda self: self.env.user,
|
||||
required=True,
|
||||
index=True,
|
||||
)
|
||||
revision_note = fields.Text(string='Revision Notes')
|
||||
file_data = fields.Binary(string='IFC File Snapshot', attachment=True)
|
||||
file_name = fields.Char(string='File Name')
|
||||
is_current = fields.Boolean(string='Current Revision', default=False, index=True)
|
||||
|
||||
def _compute_revision_label(self):
|
||||
for record in self:
|
||||
record.revision_label = 'R%02d' % (record.revision_number or 0)
|
||||
@@ -0,0 +1,172 @@
|
||||
from odoo import _, fields, models, tools
|
||||
|
||||
|
||||
class IfcKpiDashboard(models.Model):
|
||||
_name = 'grt.ifc.kpi.dashboard'
|
||||
_description = 'IFC KPI Dashboard'
|
||||
_auto = False
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(string='Company', readonly=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True)
|
||||
|
||||
total_count = fields.Integer(string='Total Documents', readonly=True)
|
||||
draft_count = fields.Integer(string='Draft', readonly=True)
|
||||
in_validation_count = fields.Integer(string='In Validation', readonly=True)
|
||||
waiting_production_count = fields.Integer(string='Waiting Produksi', readonly=True)
|
||||
waiting_provisioning_count = fields.Integer(string='Waiting Provisioning', readonly=True)
|
||||
approved_count = fields.Integer(string='Approved', readonly=True)
|
||||
rejected_count = fields.Integer(string='Rejected', readonly=True)
|
||||
approved_month_count = fields.Integer(string='Approved This Month', readonly=True)
|
||||
my_todo_count = fields.Integer(string='My To Do', compute='_compute_my_activity_cards')
|
||||
my_overdue_count = fields.Integer(string='My Overdue', compute='_compute_my_activity_cards')
|
||||
my_validation_count = fields.Integer(string='My Validation', compute='_compute_my_activity_cards')
|
||||
my_production_count = fields.Integer(string='My Pending Produksi', compute='_compute_my_activity_cards')
|
||||
my_provisioning_count = fields.Integer(string='My Pending Provisioning', compute='_compute_my_activity_cards')
|
||||
|
||||
def _count_my_activities(self, company_id, approval_state=None, overdue=False):
|
||||
self.ensure_one()
|
||||
project_domain = [('company_id', '=', company_id)]
|
||||
if approval_state:
|
||||
project_domain.append(('approval_state', '=', approval_state))
|
||||
project_ids = self.env['grt.ifc.project'].search(project_domain).ids
|
||||
if not project_ids:
|
||||
return 0
|
||||
|
||||
domain = [
|
||||
('res_model', '=', 'grt.ifc.project'),
|
||||
('res_id', 'in', project_ids),
|
||||
('user_id', '=', self.env.user.id),
|
||||
]
|
||||
if overdue:
|
||||
domain.append(('date_deadline', '<', fields.Date.today()))
|
||||
return self.env['mail.activity'].search_count(domain)
|
||||
|
||||
def _get_my_activity_project_ids(self, company_id, approval_state=None, overdue=False):
|
||||
self.ensure_one()
|
||||
project_domain = [('company_id', '=', company_id)]
|
||||
if approval_state:
|
||||
project_domain.append(('approval_state', '=', approval_state))
|
||||
project_ids = self.env['grt.ifc.project'].search(project_domain).ids
|
||||
if not project_ids:
|
||||
return []
|
||||
|
||||
domain = [
|
||||
('res_model', '=', 'grt.ifc.project'),
|
||||
('res_id', 'in', project_ids),
|
||||
('user_id', '=', self.env.user.id),
|
||||
]
|
||||
if overdue:
|
||||
domain.append(('date_deadline', '<', fields.Date.today()))
|
||||
activities = self.env['mail.activity'].search(domain)
|
||||
return list(set(activities.mapped('res_id')))
|
||||
|
||||
def _compute_my_activity_cards(self):
|
||||
for record in self:
|
||||
company_id = record.company_id.id
|
||||
record.my_todo_count = record._count_my_activities(company_id)
|
||||
record.my_overdue_count = record._count_my_activities(company_id, overdue=True)
|
||||
record.my_validation_count = record._count_my_activities(company_id, approval_state='in_validation')
|
||||
record.my_production_count = record._count_my_activities(company_id, approval_state='waiting_production')
|
||||
record.my_provisioning_count = record._count_my_activities(company_id, approval_state='waiting_provisioning')
|
||||
|
||||
def _open_documents(self, approval_state=None):
|
||||
self.ensure_one()
|
||||
domain = [('company_id', '=', self.company_id.id)]
|
||||
if approval_state:
|
||||
domain.append(('approval_state', '=', approval_state))
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Engineering IFC Documents'),
|
||||
'res_model': 'grt.ifc.project',
|
||||
'view_mode': 'list,form',
|
||||
'domain': domain,
|
||||
'context': {'search_default_filter_my_documents': 0},
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_open_all_documents(self):
|
||||
return self._open_documents()
|
||||
|
||||
def action_open_waiting_production(self):
|
||||
return self._open_documents('waiting_production')
|
||||
|
||||
def action_open_waiting_provisioning(self):
|
||||
return self._open_documents('waiting_provisioning')
|
||||
|
||||
def action_open_validation(self):
|
||||
return self._open_documents('in_validation')
|
||||
|
||||
def action_open_approved(self):
|
||||
return self._open_documents('approved')
|
||||
|
||||
def action_open_rejected(self):
|
||||
return self._open_documents('rejected')
|
||||
|
||||
def _open_my_activity_documents(self, approval_state=None, overdue=False):
|
||||
self.ensure_one()
|
||||
project_ids = self._get_my_activity_project_ids(
|
||||
self.company_id.id,
|
||||
approval_state=approval_state,
|
||||
overdue=overdue,
|
||||
)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('My Activity Documents'),
|
||||
'res_model': 'grt.ifc.project',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('id', 'in', project_ids)],
|
||||
'context': {'search_default_filter_my_documents': 0},
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_open_my_todo(self):
|
||||
return self._open_my_activity_documents()
|
||||
|
||||
def action_open_my_overdue(self):
|
||||
return self._open_my_activity_documents(overdue=True)
|
||||
|
||||
def action_open_my_validation(self):
|
||||
return self._open_my_activity_documents(approval_state='in_validation')
|
||||
|
||||
def action_open_my_production(self):
|
||||
return self._open_my_activity_documents(approval_state='waiting_production')
|
||||
|
||||
def action_open_my_provisioning(self):
|
||||
return self._open_my_activity_documents(approval_state='waiting_provisioning')
|
||||
|
||||
def init(self):
|
||||
tools.drop_view_if_exists(self.env.cr, self._table)
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
CREATE OR REPLACE VIEW %(table)s AS (
|
||||
SELECT
|
||||
c.id AS id,
|
||||
c.id AS company_id,
|
||||
c.name AS name,
|
||||
COALESCE(COUNT(p.id), 0)::integer AS total_count,
|
||||
COALESCE(SUM(CASE WHEN p.approval_state = 'draft' THEN 1 ELSE 0 END), 0)::integer AS draft_count,
|
||||
COALESCE(SUM(CASE WHEN p.approval_state = 'in_validation' THEN 1 ELSE 0 END), 0)::integer AS in_validation_count,
|
||||
COALESCE(SUM(CASE WHEN p.approval_state = 'waiting_production' THEN 1 ELSE 0 END), 0)::integer AS waiting_production_count,
|
||||
COALESCE(SUM(CASE WHEN p.approval_state = 'waiting_provisioning' THEN 1 ELSE 0 END), 0)::integer AS waiting_provisioning_count,
|
||||
COALESCE(SUM(CASE WHEN p.approval_state = 'approved' THEN 1 ELSE 0 END), 0)::integer AS approved_count,
|
||||
COALESCE(SUM(CASE WHEN p.approval_state = 'rejected' THEN 1 ELSE 0 END), 0)::integer AS rejected_count,
|
||||
COALESCE(
|
||||
SUM(
|
||||
CASE
|
||||
WHEN p.approval_state = 'approved'
|
||||
AND p.approved_provisioning_at >= date_trunc('month', now())
|
||||
AND p.approved_provisioning_at < date_trunc('month', now()) + interval '1 month'
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
),
|
||||
0
|
||||
)::integer AS approved_month_count
|
||||
FROM res_company c
|
||||
LEFT JOIN grt_ifc_project p ON p.company_id = c.id
|
||||
GROUP BY c.id, c.name
|
||||
)
|
||||
""" % {'table': self._table}
|
||||
)
|
||||
@@ -0,0 +1,870 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import _, api, fields, models, Command
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
|
||||
|
||||
class IfcProject(models.Model):
|
||||
_inherit = ['grt.ifc.project', 'mail.thread', 'mail.activity.mixin']
|
||||
|
||||
document_ref = fields.Char(
|
||||
string='Document No',
|
||||
required=True,
|
||||
copy=False,
|
||||
default=lambda self: _('New'),
|
||||
readonly=True,
|
||||
tracking=True,
|
||||
)
|
||||
uploaded_date = fields.Datetime(
|
||||
string='Uploaded On',
|
||||
default=fields.Datetime.now,
|
||||
readonly=True,
|
||||
tracking=True,
|
||||
)
|
||||
uploaded_by_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Uploaded By',
|
||||
default=lambda self: self.env.user,
|
||||
readonly=True,
|
||||
tracking=True,
|
||||
)
|
||||
last_update = fields.Datetime(string='Last Update', related='write_date', store=True, readonly=True)
|
||||
|
||||
revision_note_input = fields.Text(string='Revision Notes')
|
||||
revision_ids = fields.One2many(
|
||||
'grt.ifc.document.revision',
|
||||
'project_id',
|
||||
string='Document Revisions',
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
last_revision_id = fields.Many2one(
|
||||
'grt.ifc.document.revision',
|
||||
string='Last Revision',
|
||||
compute='_compute_last_revision',
|
||||
store=True,
|
||||
)
|
||||
last_revision_label = fields.Char(string='Last Revision Label', related='last_revision_id.revision_label', store=True)
|
||||
last_revision_note = fields.Text(string='Last Revision Notes', related='last_revision_id.revision_note', store=True)
|
||||
revised_by_id = fields.Many2one('res.users', string='Revised By', related='last_revision_id.revised_by_id', store=True)
|
||||
last_revision_date = fields.Datetime(string='Last Revision Date', related='last_revision_id.revision_date', store=True)
|
||||
|
||||
approval_state = fields.Selection(
|
||||
[
|
||||
('draft', 'Draft'),
|
||||
('in_validation', 'In Validation'),
|
||||
('waiting_production', 'Waiting Production Approval'),
|
||||
('waiting_provisioning', 'Waiting Provisioning Approval'),
|
||||
('approved', 'Approved'),
|
||||
('rejected', 'Rejected'),
|
||||
],
|
||||
default='draft',
|
||||
required=True,
|
||||
tracking=True,
|
||||
index=True,
|
||||
string='Approval Status',
|
||||
)
|
||||
approved_production_by_id = fields.Many2one('res.users', string='Approved Production By', readonly=True)
|
||||
approved_production_at = fields.Datetime(string='Production Approved At', readonly=True)
|
||||
approved_provisioning_by_id = fields.Many2one('res.users', string='Approved Provisioning By', readonly=True)
|
||||
approved_provisioning_at = fields.Datetime(string='Provisioning Approved At', readonly=True)
|
||||
rejected_by_id = fields.Many2one('res.users', string='Rejected By', readonly=True)
|
||||
rejected_at = fields.Datetime(string='Rejected At', readonly=True)
|
||||
|
||||
history_ids = fields.One2many(
|
||||
'grt.ifc.activity.history',
|
||||
'project_id',
|
||||
string='Activity History',
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
|
||||
estimated_product_tmpl_id = fields.Many2one('product.template', string='Estimated Main Product')
|
||||
estimated_bom_id = fields.Many2one('mrp.bom', string='Estimated BoM', copy=False, readonly=True)
|
||||
permanent_bom_id = fields.Many2one('mrp.bom', string='Permanent BoM', copy=False, readonly=True)
|
||||
estimated_bom_count = fields.Integer(compute='_compute_estimated_bom_count', string='Estimated BoM Count')
|
||||
manufacturing_order_id = fields.Many2one('mrp.production', string='Latest Manufacturing Order', copy=False, readonly=True)
|
||||
manufacturing_order_ids = fields.One2many('mrp.production', 'ifc_project_id', string='Manufacturing Orders', readonly=True)
|
||||
|
||||
@api.depends('revision_ids.revision_number', 'revision_ids.is_current')
|
||||
def _compute_last_revision(self):
|
||||
for record in self:
|
||||
current = record.revision_ids.filtered(lambda rev: rev.is_current)[:1]
|
||||
if current:
|
||||
record.last_revision_id = current.id
|
||||
continue
|
||||
ordered = record.revision_ids.sorted(key=lambda rev: rev.revision_number, reverse=True)
|
||||
record.last_revision_id = ordered[:1].id if ordered else False
|
||||
|
||||
@api.depends('estimated_bom_id')
|
||||
def _compute_estimated_bom_count(self):
|
||||
for record in self:
|
||||
record.estimated_bom_count = 1 if record.estimated_bom_id else 0
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('document_ref') or vals['document_ref'] == _('New'):
|
||||
vals['document_ref'] = self.env['ir.sequence'].next_by_code('grt.ifc.project.document') or _('New')
|
||||
vals.setdefault('uploaded_date', fields.Datetime.now())
|
||||
vals.setdefault('uploaded_by_id', self.env.user.id)
|
||||
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
if record.file_data:
|
||||
record._create_revision_entry(_('Dokumen awal diunggah.'))
|
||||
record._log_history('upload', _('Upload dokumen IFC awal.'))
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
update_file = 'file_data' in vals and not self.env.context.get('skip_revision_auto')
|
||||
revision_note = vals.get('revision_note_input') or _('File IFC diperbarui.')
|
||||
result = super().write(vals)
|
||||
|
||||
if update_file:
|
||||
for record in self:
|
||||
if record.file_data:
|
||||
record._create_revision_entry(revision_note)
|
||||
record._log_history('revision', revision_note)
|
||||
return result
|
||||
|
||||
def action_parse_ifc(self):
|
||||
result = super().action_parse_ifc()
|
||||
for record in self:
|
||||
record._log_history('parse', _('IFC parsing dijalankan.'))
|
||||
return result
|
||||
|
||||
def action_request_validation(self):
|
||||
for record in self:
|
||||
if record.approval_state not in ('draft', 'rejected'):
|
||||
raise UserError(_('Dokumen hanya dapat divalidasi dari status Draft atau Rejected.'))
|
||||
if record.state != 'parsed':
|
||||
raise UserError(_('Dokumen harus diparse dahulu sebelum validasi.'))
|
||||
|
||||
record.write({'approval_state': 'in_validation'})
|
||||
record._schedule_activity(
|
||||
record.uploaded_by_id,
|
||||
_('Lakukan validasi dokumen IFC %s') % (record.document_ref,),
|
||||
_('Validasi teknis dokumen dan pastikan data IFC siap untuk approval.'),
|
||||
)
|
||||
record._log_history('validation', _('Permintaan validasi dikirim.'))
|
||||
|
||||
def action_submit_for_approval(self):
|
||||
for record in self:
|
||||
if record.approval_state not in ('in_validation', 'draft', 'rejected'):
|
||||
raise UserError(_('Dokumen tidak dapat diajukan dari status saat ini.'))
|
||||
if record.state != 'parsed':
|
||||
raise UserError(_('Dokumen harus diparse sebelum diajukan approval.'))
|
||||
|
||||
record.write({'approval_state': 'waiting_production'})
|
||||
production_user = record._get_first_user_of_group('grt_product_engineering.group_ifc_manager_production')
|
||||
if production_user:
|
||||
record._schedule_activity(
|
||||
production_user,
|
||||
_('Approval Produksi untuk %s') % (record.document_ref,),
|
||||
_('Periksa dokumen IFC dan berikan approval tahap Produksi.'),
|
||||
)
|
||||
record._log_history('validation', _('Dokumen diajukan ke approval Manager Produksi.'))
|
||||
|
||||
def action_approve_production(self):
|
||||
self._ensure_group('grt_product_engineering.group_ifc_manager_production', _('Hanya Manager Produksi yang dapat melakukan approval tahap ini.'))
|
||||
for record in self:
|
||||
if record.approval_state != 'waiting_production':
|
||||
raise UserError(_('Dokumen tidak berada pada tahap approval Produksi.'))
|
||||
|
||||
record._mark_user_activities_done(_('Approved by Production Manager.'))
|
||||
record.write(
|
||||
{
|
||||
'approval_state': 'waiting_provisioning',
|
||||
'approved_production_by_id': self.env.user.id,
|
||||
'approved_production_at': fields.Datetime.now(),
|
||||
}
|
||||
)
|
||||
provisioning_user = record._get_first_user_of_group('grt_product_engineering.group_ifc_manager_provisioning')
|
||||
if provisioning_user:
|
||||
record._schedule_activity(
|
||||
provisioning_user,
|
||||
_('Approval Provisioning untuk %s') % (record.document_ref,),
|
||||
_('Dokumen telah lolos approval Produksi. Lanjutkan approval Provisioning.'),
|
||||
)
|
||||
record._log_history('approval_production', _('Dokumen disetujui Manager Produksi.'))
|
||||
|
||||
def action_approve_provisioning(self):
|
||||
self._ensure_group('grt_product_engineering.group_ifc_manager_provisioning', _('Hanya Manager Provisioning yang dapat melakukan approval tahap ini.'))
|
||||
for record in self:
|
||||
if record.approval_state != 'waiting_provisioning':
|
||||
raise UserError(_('Dokumen tidak berada pada tahap approval Provisioning.'))
|
||||
|
||||
record._mark_user_activities_done(_('Approved by Provisioning Manager.'))
|
||||
record.write(
|
||||
{
|
||||
'approval_state': 'approved',
|
||||
'approved_provisioning_by_id': self.env.user.id,
|
||||
'approved_provisioning_at': fields.Datetime.now(),
|
||||
}
|
||||
)
|
||||
record._log_history('approval_provisioning', _('Dokumen disetujui Manager Provisioning.'))
|
||||
|
||||
def action_reject_document(self):
|
||||
self._ensure_group_any(
|
||||
[
|
||||
'grt_product_engineering.group_ifc_manager_production',
|
||||
'grt_product_engineering.group_ifc_manager_provisioning',
|
||||
],
|
||||
_('Hanya Manager Produksi atau Manager Provisioning yang dapat menolak dokumen.'),
|
||||
)
|
||||
for record in self:
|
||||
if record.approval_state not in ('waiting_production', 'waiting_provisioning', 'in_validation'):
|
||||
raise UserError(_('Dokumen tidak berada pada tahap yang dapat ditolak.'))
|
||||
|
||||
record._mark_user_activities_done(_('Dokumen ditolak dan perlu revisi.'))
|
||||
record.write(
|
||||
{
|
||||
'approval_state': 'rejected',
|
||||
'rejected_by_id': self.env.user.id,
|
||||
'rejected_at': fields.Datetime.now(),
|
||||
}
|
||||
)
|
||||
note = record.revision_note_input or _('Dokumen ditolak. Harap lakukan revisi.')
|
||||
record._log_history('rejected', note)
|
||||
if record.uploaded_by_id:
|
||||
record._schedule_activity(
|
||||
record.uploaded_by_id,
|
||||
_('Dokumen ditolak: %s') % (record.document_ref,),
|
||||
_('Lakukan revisi dokumen sesuai catatan penolakan.'),
|
||||
)
|
||||
|
||||
def action_back_to_draft(self):
|
||||
for record in self:
|
||||
record.write({'approval_state': 'draft'})
|
||||
record._log_history('validation', _('Dokumen dikembalikan ke Draft untuk perbaikan.'))
|
||||
|
||||
def action_create_bom_estimate(self):
|
||||
unit_uom = self.env.ref('uom.product_uom_unit')
|
||||
|
||||
for record in self:
|
||||
if record.state != 'parsed':
|
||||
raise UserError(_('BoM estimasi hanya bisa dibuat untuk dokumen yang sudah parsed.'))
|
||||
if not record.bom_ids:
|
||||
raise UserError(_('Data komponen IFC belum ada. Jalankan parse IFC terlebih dahulu.'))
|
||||
|
||||
main_template = record.estimated_product_tmpl_id or record._get_or_create_main_product_template(unit_uom)
|
||||
if not main_template:
|
||||
raise UserError(_('Produk utama estimasi tidak dapat dibuat.'))
|
||||
|
||||
component_map = defaultdict(float)
|
||||
for bom_line in record.bom_ids:
|
||||
if bom_line.level == 0 and bom_line.parent_id:
|
||||
continue
|
||||
component_product = record._get_or_create_component_product(bom_line, unit_uom)
|
||||
if component_product:
|
||||
qty = bom_line.quantity if bom_line.quantity and bom_line.quantity > 0 else 1.0
|
||||
component_map[component_product.id] += qty
|
||||
|
||||
if not component_map:
|
||||
raise UserError(_('Komponen untuk BoM estimasi tidak ditemukan.'))
|
||||
|
||||
line_commands = [Command.clear()]
|
||||
for product_id, quantity in component_map.items():
|
||||
component = self.env['product.product'].browse(product_id)
|
||||
line_commands.append(
|
||||
Command.create(
|
||||
{
|
||||
'product_id': component.id,
|
||||
'product_qty': quantity,
|
||||
'product_uom_id': component.uom_id.id,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
bom_vals = {
|
||||
'product_tmpl_id': main_template.id,
|
||||
'product_id': main_template.product_variant_id.id,
|
||||
'product_qty': 1.0,
|
||||
'code': '%s-%s' % (record.document_ref, record.last_revision_label or 'R00'),
|
||||
'type': 'normal',
|
||||
'company_id': record.company_id.id,
|
||||
'ifc_project_id': record.id,
|
||||
'line_ids': line_commands,
|
||||
}
|
||||
|
||||
if record.estimated_bom_id:
|
||||
record.estimated_bom_id.write(bom_vals)
|
||||
estimated_bom = record.estimated_bom_id
|
||||
else:
|
||||
estimated_bom = self.env['mrp.bom'].create(bom_vals)
|
||||
|
||||
record.estimated_product_tmpl_id = main_template.id
|
||||
record.estimated_bom_id = estimated_bom.id
|
||||
record.permanent_bom_id = False
|
||||
record._log_history('bom_estimate', _('BoM estimasi dibuat/diperbarui.'))
|
||||
|
||||
def action_create_permanent_bom_if_available(self):
|
||||
self.ensure_one()
|
||||
if not self.estimated_bom_id:
|
||||
raise UserError(_('Belum ada Estimated BoM. Jalankan generate BoM terlebih dahulu.'))
|
||||
|
||||
availability = self._compute_inventory_availability()
|
||||
if not availability.get('all_available'):
|
||||
raise UserError(_('Estimated BoM belum terpenuhi availability-nya. Permanent BoM belum dapat dibuat.'))
|
||||
|
||||
estimated_bom = self.estimated_bom_id
|
||||
product = estimated_bom.product_id or estimated_bom.product_tmpl_id.product_variant_id
|
||||
if not product:
|
||||
raise UserError(_('Produk utama pada Estimated BoM tidak ditemukan.'))
|
||||
|
||||
line_commands = [Command.clear()]
|
||||
for line in estimated_bom.line_ids:
|
||||
line_commands.append(
|
||||
Command.create(
|
||||
{
|
||||
'product_id': line.product_id.id,
|
||||
'product_qty': line.product_qty,
|
||||
'product_uom_id': line.product_uom_id.id,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
permanent_vals = {
|
||||
'product_tmpl_id': estimated_bom.product_tmpl_id.id,
|
||||
'product_id': product.id,
|
||||
'product_qty': estimated_bom.product_qty or 1.0,
|
||||
'code': '%s-%s-PERM' % (self.document_ref, self.last_revision_label or 'R00'),
|
||||
'type': estimated_bom.type,
|
||||
'company_id': self.company_id.id,
|
||||
'ifc_project_id': self.id,
|
||||
'is_ifc_permanent': True,
|
||||
'source_estimated_bom_id': estimated_bom.id,
|
||||
'line_ids': line_commands,
|
||||
}
|
||||
|
||||
if self.permanent_bom_id:
|
||||
self.permanent_bom_id.write(permanent_vals)
|
||||
permanent_bom = self.permanent_bom_id
|
||||
else:
|
||||
permanent_bom = self.env['mrp.bom'].create(permanent_vals)
|
||||
|
||||
self.permanent_bom_id = permanent_bom.id
|
||||
self._log_history('bom_estimate', _('Permanent BoM dibuat/diperbarui dari Estimated BoM.'))
|
||||
return permanent_bom
|
||||
|
||||
def action_view_estimated_bom(self):
|
||||
self.ensure_one()
|
||||
if not self.estimated_bom_id:
|
||||
raise UserError(_('Belum ada BoM estimasi.'))
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Estimated BoM'),
|
||||
'res_model': 'mrp.bom',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.estimated_bom_id.id,
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def _compute_inventory_availability(self):
|
||||
self.ensure_one()
|
||||
if not self.estimated_bom_id:
|
||||
raise UserError(_('Belum ada Estimated BoM. Jalankan generate BoM terlebih dahulu.'))
|
||||
|
||||
items = []
|
||||
total_required = 0.0
|
||||
total_on_hand = 0.0
|
||||
total_shortage = 0.0
|
||||
|
||||
for line in self.estimated_bom_id.line_ids:
|
||||
product = line.product_id
|
||||
required_qty = float(line.product_qty or 0.0)
|
||||
on_hand_qty = float(product.qty_available or 0.0) if product else 0.0
|
||||
forecast_qty = float(product.virtual_available or 0.0) if product else 0.0
|
||||
shortage_qty = max(required_qty - on_hand_qty, 0.0)
|
||||
|
||||
items.append(
|
||||
{
|
||||
'bom_line_id': line.id,
|
||||
'product_id': product.id if product else False,
|
||||
'product_name': product.display_name if product else line.name,
|
||||
'default_code': product.default_code if product else False,
|
||||
'required_qty': required_qty,
|
||||
'on_hand_qty': on_hand_qty,
|
||||
'forecast_qty': forecast_qty,
|
||||
'shortage_qty': shortage_qty,
|
||||
'is_available': shortage_qty <= 0,
|
||||
'uom_id': line.product_uom_id.id if line.product_uom_id else False,
|
||||
'uom_name': line.product_uom_id.name if line.product_uom_id else False,
|
||||
}
|
||||
)
|
||||
total_required += required_qty
|
||||
total_on_hand += on_hand_qty
|
||||
total_shortage += shortage_qty
|
||||
|
||||
return {
|
||||
'document_id': self.id,
|
||||
'document_ref': self.document_ref,
|
||||
'estimated_bom_id': self.estimated_bom_id.id,
|
||||
'total_required_qty': total_required,
|
||||
'total_on_hand_qty': total_on_hand,
|
||||
'total_shortage_qty': total_shortage,
|
||||
'all_available': total_shortage <= 0,
|
||||
'items': items,
|
||||
}
|
||||
|
||||
def action_check_inventory_availability(self):
|
||||
self.ensure_one()
|
||||
availability = self._compute_inventory_availability()
|
||||
self._log_history('validation', _('Cek ketersediaan inventory untuk Estimated BoM dilakukan.'))
|
||||
return availability
|
||||
|
||||
def _select_purchase_vendor(self, product, forced_vendor=False):
|
||||
self.ensure_one()
|
||||
if forced_vendor:
|
||||
return forced_vendor
|
||||
seller = product.seller_ids[:1]
|
||||
return seller.partner_id if seller else False
|
||||
|
||||
def action_create_draft_purchase_for_shortage(self, vendor_id=False, only_shortage=True):
|
||||
self.ensure_one()
|
||||
availability = self._compute_inventory_availability()
|
||||
purchase_orders_by_vendor = {}
|
||||
no_vendor_items = []
|
||||
|
||||
forced_vendor = self.env['res.partner'].browse(int(vendor_id)).exists() if vendor_id else False
|
||||
PurchaseOrder = self.env['purchase.order']
|
||||
|
||||
for item in availability['items']:
|
||||
shortage_qty = item['shortage_qty']
|
||||
required_qty = item['required_qty']
|
||||
if only_shortage and shortage_qty <= 0:
|
||||
continue
|
||||
|
||||
order_qty = shortage_qty if only_shortage else required_qty
|
||||
if order_qty <= 0:
|
||||
continue
|
||||
|
||||
product = self.env['product.product'].browse(item['product_id']).exists()
|
||||
if not product:
|
||||
continue
|
||||
|
||||
vendor = self._select_purchase_vendor(product, forced_vendor=forced_vendor)
|
||||
if not vendor:
|
||||
no_vendor_items.append(product.display_name)
|
||||
continue
|
||||
|
||||
po = purchase_orders_by_vendor.get(vendor.id)
|
||||
if not po:
|
||||
po = PurchaseOrder.create(
|
||||
{
|
||||
'partner_id': vendor.id,
|
||||
'company_id': self.company_id.id,
|
||||
'origin': '%s - %s' % (self.document_ref, _('IFC Estimated BoM Shortage')),
|
||||
'notes': _('Auto-generated dari Product Engineering IFC.'),
|
||||
}
|
||||
)
|
||||
purchase_orders_by_vendor[vendor.id] = po
|
||||
|
||||
seller = product.seller_ids.filtered(lambda s: s.partner_id.id == vendor.id)[:1]
|
||||
price_unit = seller.price if seller else (product.standard_price or 0.0)
|
||||
self.env['purchase.order.line'].create(
|
||||
{
|
||||
'order_id': po.id,
|
||||
'product_id': product.id,
|
||||
'name': product.display_name,
|
||||
'product_qty': order_qty,
|
||||
'product_uom': product.uom_po_id.id or product.uom_id.id,
|
||||
'price_unit': price_unit,
|
||||
'date_planned': fields.Datetime.now(),
|
||||
}
|
||||
)
|
||||
|
||||
if no_vendor_items:
|
||||
raise UserError(
|
||||
_('Vendor belum ditentukan untuk beberapa produk: %s') % ', '.join(no_vendor_items)
|
||||
)
|
||||
|
||||
purchase_orders = list(purchase_orders_by_vendor.values())
|
||||
if not purchase_orders:
|
||||
raise UserError(_('Tidak ada item yang perlu dibuatkan draft purchase.'))
|
||||
|
||||
self._log_history('validation', _('Draft purchase dibuat dari shortage Estimated BoM.'))
|
||||
return purchase_orders
|
||||
|
||||
def action_create_missing_product_for_ifc_bom(self, ifc_bom_line_id):
|
||||
self.ensure_one()
|
||||
if not ifc_bom_line_id:
|
||||
raise UserError(_('ifc_bom_line_id wajib diisi.'))
|
||||
|
||||
bom_line = self.env['grt.ifc.bom'].browse(int(ifc_bom_line_id)).exists()
|
||||
if not bom_line or bom_line.project_id.id != self.id:
|
||||
raise UserError(_('Baris IFC BOM tidak valid untuk dokumen ini.'))
|
||||
|
||||
default_code = bom_line.global_id or bom_line.ifc_key
|
||||
if not default_code:
|
||||
raise UserError(_('Baris IFC tidak memiliki global_id atau ifc_key sebagai kode produk.'))
|
||||
|
||||
product = self.env['product.product'].search([('default_code', '=', default_code)], limit=1)
|
||||
if product:
|
||||
return product, False
|
||||
|
||||
unit_uom = self.env.ref('uom.product_uom_unit')
|
||||
template = self.env['product.template'].create(
|
||||
{
|
||||
'name': bom_line.name,
|
||||
'default_code': default_code,
|
||||
'detailed_type': 'consu',
|
||||
'uom_id': unit_uom.id,
|
||||
'uom_po_id': unit_uom.id,
|
||||
}
|
||||
)
|
||||
self._log_history('validation', _('Produk baru dibuat dari IFC BOM line: %s') % bom_line.name)
|
||||
return template.product_variant_id, True
|
||||
|
||||
def _get_default_mrp_picking_type(self):
|
||||
self.ensure_one()
|
||||
return self.env['stock.picking.type'].search(
|
||||
[
|
||||
('code', '=', 'mrp_operation'),
|
||||
('company_id', 'in', [self.company_id.id, False]),
|
||||
],
|
||||
order='company_id desc, id',
|
||||
limit=1,
|
||||
)
|
||||
|
||||
def action_create_mo_if_available(self):
|
||||
self.ensure_one()
|
||||
if not self.permanent_bom_id:
|
||||
self.action_create_permanent_bom_if_available()
|
||||
|
||||
availability = self._compute_inventory_availability()
|
||||
if not availability.get('all_available'):
|
||||
raise UserError(_('Stok belum terpenuhi untuk semua komponen. MO belum dapat dibuat.'))
|
||||
|
||||
bom = self.permanent_bom_id
|
||||
product = bom.product_id or bom.product_tmpl_id.product_variant_id
|
||||
if not product:
|
||||
raise UserError(_('Produk utama pada Estimated BoM tidak ditemukan.'))
|
||||
|
||||
mo_vals = {
|
||||
'product_id': product.id,
|
||||
'product_uom_id': product.uom_id.id,
|
||||
'product_qty': bom.product_qty or 1.0,
|
||||
'bom_id': bom.id,
|
||||
'company_id': self.company_id.id,
|
||||
'origin': '%s - %s' % (self.document_ref, _('IFC Engineering')),
|
||||
'ifc_project_id': self.id,
|
||||
}
|
||||
picking_type = self._get_default_mrp_picking_type()
|
||||
if picking_type:
|
||||
mo_vals['picking_type_id'] = picking_type.id
|
||||
|
||||
manufacturing_order = self.env['mrp.production'].create(mo_vals)
|
||||
self.manufacturing_order_id = manufacturing_order.id
|
||||
self._log_history('manufacturing_order', _('Manufacturing Order dibuat: %s') % manufacturing_order.name)
|
||||
return manufacturing_order
|
||||
|
||||
def _compute_mo_requirements(self, manufacturing_order):
|
||||
requirements = []
|
||||
bom = manufacturing_order.bom_id
|
||||
if not bom:
|
||||
return requirements
|
||||
|
||||
factor = (manufacturing_order.product_qty or 0.0) / (bom.product_qty or 1.0)
|
||||
for line in bom.line_ids:
|
||||
required_qty = (line.product_qty or 0.0) * factor
|
||||
product = line.product_id
|
||||
on_hand_qty = float(product.qty_available or 0.0) if product else 0.0
|
||||
requirements.append(
|
||||
{
|
||||
'product_id': product.id if product else False,
|
||||
'product_name': product.display_name if product else False,
|
||||
'default_code': product.default_code if product else False,
|
||||
'required_qty': required_qty,
|
||||
'on_hand_qty': on_hand_qty,
|
||||
'shortage_qty': max(required_qty - on_hand_qty, 0.0),
|
||||
'uom_id': line.product_uom_id.id if line.product_uom_id else False,
|
||||
'uom_name': line.product_uom_id.name if line.product_uom_id else False,
|
||||
}
|
||||
)
|
||||
return requirements
|
||||
|
||||
def _build_all_mo_availability_summary(self, raise_if_empty=True, log=False):
|
||||
self.ensure_one()
|
||||
if not self.manufacturing_order_ids:
|
||||
if raise_if_empty:
|
||||
raise UserError(_('Belum ada Manufacturing Order untuk dokumen ini.'))
|
||||
return {
|
||||
'document_id': self.id,
|
||||
'document_ref': self.document_ref,
|
||||
'mo_count': 0,
|
||||
'all_available': False,
|
||||
'total_shortage_qty': 0.0,
|
||||
'mo_items': [],
|
||||
'product_summary': [],
|
||||
}
|
||||
|
||||
active_mos = self.manufacturing_order_ids.filtered(lambda mo: mo.state not in ('cancel',))
|
||||
if not active_mos:
|
||||
if raise_if_empty:
|
||||
raise UserError(_('Tidak ada Manufacturing Order aktif untuk dicek.'))
|
||||
return {
|
||||
'document_id': self.id,
|
||||
'document_ref': self.document_ref,
|
||||
'mo_count': 0,
|
||||
'all_available': False,
|
||||
'total_shortage_qty': 0.0,
|
||||
'mo_items': [],
|
||||
'product_summary': [],
|
||||
}
|
||||
|
||||
by_product = {}
|
||||
mo_summaries = []
|
||||
|
||||
for mo in active_mos:
|
||||
mo_items = self._compute_mo_requirements(mo)
|
||||
mo_shortage = 0.0
|
||||
for item in mo_items:
|
||||
product_id = item['product_id']
|
||||
if not product_id:
|
||||
continue
|
||||
agg = by_product.setdefault(
|
||||
product_id,
|
||||
{
|
||||
'product_id': product_id,
|
||||
'product_name': item['product_name'],
|
||||
'default_code': item['default_code'],
|
||||
'required_qty': 0.0,
|
||||
'on_hand_qty': item['on_hand_qty'],
|
||||
'uom_id': item['uom_id'],
|
||||
'uom_name': item['uom_name'],
|
||||
},
|
||||
)
|
||||
agg['required_qty'] += item['required_qty']
|
||||
mo_shortage += item['shortage_qty']
|
||||
|
||||
mo_summaries.append(
|
||||
{
|
||||
'mo_id': mo.id,
|
||||
'mo_name': mo.name,
|
||||
'state': mo.state,
|
||||
'product_id': mo.product_id.id,
|
||||
'product_name': mo.product_id.display_name,
|
||||
'required_items': mo_items,
|
||||
'is_available': mo_shortage <= 0,
|
||||
}
|
||||
)
|
||||
|
||||
total_shortage = 0.0
|
||||
product_summary = []
|
||||
for data in by_product.values():
|
||||
shortage_qty = max(data['required_qty'] - data['on_hand_qty'], 0.0)
|
||||
total_shortage += shortage_qty
|
||||
product_summary.append(
|
||||
{
|
||||
**data,
|
||||
'shortage_qty': shortage_qty,
|
||||
'is_available': shortage_qty <= 0,
|
||||
}
|
||||
)
|
||||
|
||||
result = {
|
||||
'document_id': self.id,
|
||||
'document_ref': self.document_ref,
|
||||
'mo_count': len(active_mos),
|
||||
'all_available': total_shortage <= 0,
|
||||
'total_shortage_qty': total_shortage,
|
||||
'mo_items': mo_summaries,
|
||||
'product_summary': product_summary,
|
||||
}
|
||||
if log:
|
||||
self._log_history('manufacturing_order', _('Cek summary availability seluruh MO dijalankan.'))
|
||||
return result
|
||||
|
||||
def action_check_all_mo_availability_summary(self):
|
||||
self.ensure_one()
|
||||
return self._build_all_mo_availability_summary(raise_if_empty=True, log=True)
|
||||
|
||||
def action_confirm_all_mo_if_available(self):
|
||||
self.ensure_one()
|
||||
summary = self.action_check_all_mo_availability_summary()
|
||||
if not summary.get('all_available'):
|
||||
raise UserError(_('Tidak semua kebutuhan material MO terpenuhi. Confirm MO dibatalkan.'))
|
||||
|
||||
confirmed = []
|
||||
for mo in self.manufacturing_order_ids.filtered(lambda m: m.state in ('draft', 'confirmed')):
|
||||
if mo.state == 'draft':
|
||||
mo.action_confirm()
|
||||
confirmed.append(mo)
|
||||
|
||||
if not confirmed:
|
||||
raise UserError(_('Tidak ada MO pada status yang dapat dikonfirmasi.'))
|
||||
|
||||
self._log_history('manufacturing_order', _('Confirm seluruh MO berhasil dijalankan.'))
|
||||
return confirmed
|
||||
|
||||
def action_run_mo_pipeline(self, auto_confirm=False):
|
||||
self.ensure_one()
|
||||
|
||||
estimated_availability = self.action_check_inventory_availability()
|
||||
if not estimated_availability.get('all_available'):
|
||||
raise UserError(
|
||||
_('Estimated BoM belum available sepenuhnya. Selesaikan shortage sebelum jalankan pipeline MO.')
|
||||
)
|
||||
|
||||
permanent_bom = self.action_create_permanent_bom_if_available()
|
||||
manufacturing_order = self.action_create_mo_if_available()
|
||||
mo_summary = self.action_check_all_mo_availability_summary()
|
||||
|
||||
confirmed_mos = []
|
||||
if auto_confirm:
|
||||
confirmed_mos = self.action_confirm_all_mo_if_available()
|
||||
|
||||
self._log_history('manufacturing_order', _('Pipeline MO dari Estimated BoM berhasil dijalankan.'))
|
||||
return {
|
||||
'estimated_availability': estimated_availability,
|
||||
'permanent_bom': permanent_bom,
|
||||
'manufacturing_order': manufacturing_order,
|
||||
'mo_summary': mo_summary,
|
||||
'confirmed_mos': confirmed_mos,
|
||||
'auto_confirm': bool(auto_confirm),
|
||||
}
|
||||
|
||||
def action_preview_mo_pipeline(self):
|
||||
self.ensure_one()
|
||||
if not self.estimated_bom_id:
|
||||
raise UserError(_('Belum ada Estimated BoM. Jalankan generate BoM terlebih dahulu.'))
|
||||
|
||||
estimated_availability = self._compute_inventory_availability()
|
||||
can_create_permanent_bom = bool(estimated_availability.get('all_available'))
|
||||
permanent_bom_exists = bool(self.permanent_bom_id)
|
||||
can_create_mo = can_create_permanent_bom or permanent_bom_exists
|
||||
|
||||
existing_mo_summary = self._build_all_mo_availability_summary(raise_if_empty=False, log=False)
|
||||
can_confirm_all_existing_mos = bool(existing_mo_summary.get('mo_count')) and bool(existing_mo_summary.get('all_available'))
|
||||
|
||||
return {
|
||||
'document_id': self.id,
|
||||
'document_ref': self.document_ref,
|
||||
'estimated_bom_id': self.estimated_bom_id.id,
|
||||
'permanent_bom_id': self.permanent_bom_id.id if self.permanent_bom_id else False,
|
||||
'estimated_availability': estimated_availability,
|
||||
'can_create_permanent_bom': can_create_permanent_bom,
|
||||
'can_create_mo': can_create_mo,
|
||||
'existing_mo_summary': existing_mo_summary,
|
||||
'can_confirm_all_existing_mos': can_confirm_all_existing_mos,
|
||||
'pipeline_steps': [
|
||||
{
|
||||
'step': 'check_estimated_availability',
|
||||
'ready': True,
|
||||
},
|
||||
{
|
||||
'step': 'create_permanent_bom',
|
||||
'ready': can_create_permanent_bom,
|
||||
},
|
||||
{
|
||||
'step': 'create_mo_from_permanent_bom',
|
||||
'ready': can_create_mo,
|
||||
},
|
||||
{
|
||||
'step': 'check_all_mo_availability',
|
||||
'ready': True,
|
||||
},
|
||||
{
|
||||
'step': 'confirm_all_mo_if_available',
|
||||
'ready': can_confirm_all_existing_mos,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def _get_or_create_main_product_template(self, unit_uom):
|
||||
self.ensure_one()
|
||||
default_code = self.document_ref
|
||||
template = self.env['product.template'].search([('default_code', '=', default_code)], limit=1)
|
||||
if template:
|
||||
return template
|
||||
return self.env['product.template'].create(
|
||||
{
|
||||
'name': self.name,
|
||||
'default_code': default_code,
|
||||
'detailed_type': 'product',
|
||||
'uom_id': unit_uom.id,
|
||||
'uom_po_id': unit_uom.id,
|
||||
}
|
||||
)
|
||||
|
||||
def _get_or_create_component_product(self, bom_line, unit_uom):
|
||||
default_code = bom_line.global_id or bom_line.ifc_key
|
||||
if not default_code:
|
||||
return False
|
||||
|
||||
product = self.env['product.product'].search([('default_code', '=', default_code)], limit=1)
|
||||
if product:
|
||||
return product
|
||||
|
||||
template = self.env['product.template'].create(
|
||||
{
|
||||
'name': bom_line.name,
|
||||
'default_code': default_code,
|
||||
'detailed_type': 'consu',
|
||||
'uom_id': unit_uom.id,
|
||||
'uom_po_id': unit_uom.id,
|
||||
}
|
||||
)
|
||||
return template.product_variant_id
|
||||
|
||||
def _create_revision_entry(self, note):
|
||||
self.ensure_one()
|
||||
self.revision_ids.filtered(lambda rev: rev.is_current).write({'is_current': False})
|
||||
next_revision = (max(self.revision_ids.mapped('revision_number')) + 1) if self.revision_ids else 1
|
||||
self.env['grt.ifc.document.revision'].create(
|
||||
{
|
||||
'project_id': self.id,
|
||||
'revision_number': next_revision,
|
||||
'revision_date': fields.Datetime.now(),
|
||||
'revised_by_id': self.env.user.id,
|
||||
'revision_note': note,
|
||||
'file_data': self.file_data,
|
||||
'file_name': self.file_name,
|
||||
'is_current': True,
|
||||
}
|
||||
)
|
||||
|
||||
def _log_history(self, activity_type, note):
|
||||
self.ensure_one()
|
||||
self.env['grt.ifc.activity.history'].create(
|
||||
{
|
||||
'project_id': self.id,
|
||||
'activity_type': activity_type,
|
||||
'user_id': self.env.user.id,
|
||||
'note': note,
|
||||
'state_snapshot': self.approval_state,
|
||||
}
|
||||
)
|
||||
|
||||
def _schedule_activity(self, user, summary, note):
|
||||
self.ensure_one()
|
||||
if not user:
|
||||
return
|
||||
self.activity_schedule(
|
||||
'mail.mail_activity_data_todo',
|
||||
user_id=user.id,
|
||||
summary=summary,
|
||||
note=note,
|
||||
)
|
||||
|
||||
def _mark_user_activities_done(self, feedback):
|
||||
self.ensure_one()
|
||||
open_activities = self.activity_ids.filtered(lambda act: act.user_id == self.env.user)
|
||||
for activity in open_activities:
|
||||
activity.action_feedback(feedback=feedback)
|
||||
|
||||
def _get_first_user_of_group(self, group_xmlid):
|
||||
group = self.env.ref(group_xmlid, raise_if_not_found=False)
|
||||
if not group or not group.users:
|
||||
return False
|
||||
return group.users[0]
|
||||
|
||||
def _ensure_group(self, group_xmlid, error_message):
|
||||
if not self.env.user.has_group(group_xmlid):
|
||||
raise AccessError(error_message)
|
||||
|
||||
def _ensure_group_any(self, group_xmlids, error_message):
|
||||
if not any(self.env.user.has_group(group_xmlid) for group_xmlid in group_xmlids):
|
||||
raise AccessError(error_message)
|
||||
@@ -0,0 +1,9 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MrpBom(models.Model):
|
||||
_inherit = 'mrp.bom'
|
||||
|
||||
ifc_project_id = fields.Many2one('grt.ifc.project', string='IFC Engineering Document', index=True)
|
||||
is_ifc_permanent = fields.Boolean(string='IFC Permanent BoM', default=False, index=True)
|
||||
source_estimated_bom_id = fields.Many2one('mrp.bom', string='Source Estimated BoM', copy=False)
|
||||
@@ -0,0 +1,7 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_inherit = 'mrp.production'
|
||||
|
||||
ifc_project_id = fields.Many2one('grt.ifc.project', string='IFC Engineering Document', index=True)
|
||||
@@ -0,0 +1,6 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_grt_ifc_document_revision_user,access.grt.ifc.document.revision.user,model_grt_ifc_document_revision,base.group_user,1,0,0,0
|
||||
access_grt_ifc_document_revision_manager_production,access.grt.ifc.document.revision.manager.production,model_grt_ifc_document_revision,grt_product_engineering.group_ifc_manager_production,1,1,1,0
|
||||
access_grt_ifc_document_revision_manager_provisioning,access.grt.ifc.document.revision.manager.provisioning,model_grt_ifc_document_revision,grt_product_engineering.group_ifc_manager_provisioning,1,1,1,0
|
||||
access_grt_ifc_activity_history_user,access.grt.ifc.activity.history.user,model_grt_ifc_activity_history,base.group_user,1,0,0,0
|
||||
access_grt_ifc_kpi_dashboard_user,access.grt.ifc.kpi.dashboard.user,model_grt_ifc_kpi_dashboard,base.group_user,1,0,0,0
|
||||
|
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="group_ifc_manager_production" model="res.groups">
|
||||
<field name="name">IFC Manager Produksi</field>
|
||||
<field name="category_id" ref="base.module_category_manufacturing"/>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_ifc_manager_provisioning" model="res.groups">
|
||||
<field name="name">IFC Manager Provisioning</field>
|
||||
<field name="category_id" ref="base.module_category_manufacturing"/>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_grt_ifc_kpi_dashboard_list" model="ir.ui.view">
|
||||
<field name="name">grt.ifc.kpi.dashboard.list</field>
|
||||
<field name="model">grt.ifc.kpi.dashboard</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="IFC KPI Dashboard" create="0" edit="0" delete="0">
|
||||
<field name="name"/>
|
||||
<field name="total_count"/>
|
||||
<field name="in_validation_count"/>
|
||||
<field name="waiting_production_count"/>
|
||||
<field name="waiting_provisioning_count"/>
|
||||
<field name="approved_count"/>
|
||||
<field name="approved_month_count"/>
|
||||
<field name="rejected_count"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_grt_ifc_kpi_dashboard_form" model="ir.ui.view">
|
||||
<field name="name">grt.ifc.kpi.dashboard.form</field>
|
||||
<field name="model">grt.ifc.kpi.dashboard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="IFC KPI Dashboard" create="0" edit="0" delete="0">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name" readonly="1"/>
|
||||
<field name="company_id" readonly="1"/>
|
||||
</group>
|
||||
<group string="My KPI Cards">
|
||||
<button name="action_open_my_todo" type="object" class="oe_stat_button" icon="fa-tasks">
|
||||
<field name="my_todo_count" widget="statinfo" string="My To Do"/>
|
||||
</button>
|
||||
<button name="action_open_my_overdue" type="object" class="oe_stat_button" icon="fa-exclamation-triangle">
|
||||
<field name="my_overdue_count" widget="statinfo" string="My Overdue"/>
|
||||
</button>
|
||||
<button name="action_open_my_validation" type="object" class="oe_stat_button" icon="fa-check-square">
|
||||
<field name="my_validation_count" widget="statinfo" string="My Validation"/>
|
||||
</button>
|
||||
<button name="action_open_my_production" type="object" class="oe_stat_button" icon="fa-industry">
|
||||
<field name="my_production_count" widget="statinfo" string="My Produksi"/>
|
||||
</button>
|
||||
<button name="action_open_my_provisioning" type="object" class="oe_stat_button" icon="fa-cubes">
|
||||
<field name="my_provisioning_count" widget="statinfo" string="My Provisioning"/>
|
||||
</button>
|
||||
</group>
|
||||
<group string="Approval Workload">
|
||||
<button name="action_open_waiting_production" type="object" class="oe_stat_button" icon="fa-clock-o">
|
||||
<field name="waiting_production_count" widget="statinfo" string="Pending Produksi"/>
|
||||
</button>
|
||||
<button name="action_open_waiting_provisioning" type="object" class="oe_stat_button" icon="fa-hourglass-half">
|
||||
<field name="waiting_provisioning_count" widget="statinfo" string="Pending Provisioning"/>
|
||||
</button>
|
||||
<button name="action_open_validation" type="object" class="oe_stat_button" icon="fa-check-square-o">
|
||||
<field name="in_validation_count" widget="statinfo" string="In Validation"/>
|
||||
</button>
|
||||
</group>
|
||||
<group string="Outcome">
|
||||
<button name="action_open_approved" type="object" class="oe_stat_button" icon="fa-thumbs-up">
|
||||
<field name="approved_count" widget="statinfo" string="Approved"/>
|
||||
</button>
|
||||
<button name="action_open_rejected" type="object" class="oe_stat_button" icon="fa-thumbs-down">
|
||||
<field name="rejected_count" widget="statinfo" string="Rejected"/>
|
||||
</button>
|
||||
<button name="action_open_all_documents" type="object" class="oe_stat_button" icon="fa-folder-open">
|
||||
<field name="total_count" widget="statinfo" string="Total Documents"/>
|
||||
</button>
|
||||
</group>
|
||||
<group>
|
||||
<field name="approved_month_count" readonly="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_grt_ifc_kpi_dashboard_graph" model="ir.ui.view">
|
||||
<field name="name">grt.ifc.kpi.dashboard.graph</field>
|
||||
<field name="model">grt.ifc.kpi.dashboard</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="IFC KPI Dashboard" type="bar">
|
||||
<field name="name" type="row"/>
|
||||
<field name="in_validation_count" type="measure"/>
|
||||
<field name="waiting_production_count" type="measure"/>
|
||||
<field name="waiting_provisioning_count" type="measure"/>
|
||||
<field name="approved_count" type="measure"/>
|
||||
<field name="rejected_count" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_grt_ifc_kpi_dashboard_pivot" model="ir.ui.view">
|
||||
<field name="name">grt.ifc.kpi.dashboard.pivot</field>
|
||||
<field name="model">grt.ifc.kpi.dashboard</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="IFC KPI Dashboard">
|
||||
<field name="name" type="row"/>
|
||||
<field name="in_validation_count" type="measure"/>
|
||||
<field name="waiting_production_count" type="measure"/>
|
||||
<field name="waiting_provisioning_count" type="measure"/>
|
||||
<field name="approved_count" type="measure"/>
|
||||
<field name="rejected_count" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_grt_ifc_kpi_dashboard" model="ir.actions.act_window">
|
||||
<field name="name">Engineering KPI Dashboard</field>
|
||||
<field name="res_model">grt.ifc.kpi.dashboard</field>
|
||||
<field name="view_mode">list,form,graph,pivot</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">Pantau KPI approval dokumen IFC per perusahaan.</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="action_grt_ifc_engineering_document" model="ir.actions.act_window">
|
||||
<field name="name">Engineering IFC Documents</field>
|
||||
<field name="res_model">grt.ifc.project</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_grt_ifc_project_search_engineering"/>
|
||||
<field name="context">{'search_default_filter_my_documents': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">Buat dokumen IFC engineering pertama Anda</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_grt_ifc_project_search_engineering" model="ir.ui.view">
|
||||
<field name="name">grt.ifc.project.search.engineering</field>
|
||||
<field name="model">grt.ifc.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Engineering IFC Documents">
|
||||
<field name="name"/>
|
||||
<field name="document_ref"/>
|
||||
<field name="uploaded_by_id"/>
|
||||
<filter name="filter_my_documents" string="My Documents" domain="[('uploaded_by_id', '=', uid)]"/>
|
||||
<filter name="filter_waiting_production" string="Waiting Produksi" domain="[('approval_state', '=', 'waiting_production')]"/>
|
||||
<filter name="filter_waiting_provisioning" string="Waiting Provisioning" domain="[('approval_state', '=', 'waiting_provisioning')]"/>
|
||||
<filter name="filter_approved" string="Approved" domain="[('approval_state', '=', 'approved')]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="group_uploaded_by" string="Uploaded By" context="{'group_by': 'uploaded_by_id'}"/>
|
||||
<filter name="group_approval_state" string="Approval State" context="{'group_by': 'approval_state'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="menu_grt_ifc_document_management"
|
||||
name="Document Management"
|
||||
parent="grt_ifcopenshell.menu_grt_ifc_root"
|
||||
action="action_grt_ifc_engineering_document"
|
||||
sequence="20"
|
||||
groups="base.group_user"
|
||||
/>
|
||||
|
||||
<menuitem
|
||||
id="menu_grt_ifc_kpi_dashboard"
|
||||
name="KPI Dashboard"
|
||||
parent="menu_grt_ifc_document_management"
|
||||
action="action_grt_ifc_kpi_dashboard"
|
||||
sequence="30"
|
||||
groups="base.group_user"
|
||||
/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_grt_ifc_project_tree_inherit_engineering" model="ir.ui.view">
|
||||
<field name="name">grt.ifc.project.tree.inherit.engineering</field>
|
||||
<field name="model">grt.ifc.project</field>
|
||||
<field name="inherit_id" ref="grt_ifcopenshell.view_grt_ifc_project_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list/field[@name='name']" position="before">
|
||||
<field name="document_ref"/>
|
||||
</xpath>
|
||||
<xpath expr="//list/field[@name='state']" position="before">
|
||||
<field name="approval_state"/>
|
||||
<field name="last_revision_label" optional="show"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_grt_ifc_project_form_inherit_engineering" model="ir.ui.view">
|
||||
<field name="name">grt.ifc.project.form.inherit.engineering</field>
|
||||
<field name="model">grt.ifc.project</field>
|
||||
<field name="inherit_id" ref="grt_ifcopenshell.view_grt_ifc_project_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form/header" position="inside">
|
||||
<button name="action_request_validation"
|
||||
string="Request Validation"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
invisible="state != 'parsed' or approval_state not in ('draft', 'rejected')"/>
|
||||
<button name="action_submit_for_approval"
|
||||
string="Submit Approval"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="state != 'parsed' or approval_state not in ('draft', 'in_validation', 'rejected')"/>
|
||||
<button name="action_approve_production"
|
||||
string="Approve Produksi"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
groups="grt_product_engineering.group_ifc_manager_production"
|
||||
invisible="approval_state != 'waiting_production'"/>
|
||||
<button name="action_approve_provisioning"
|
||||
string="Approve Provisioning"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
groups="grt_product_engineering.group_ifc_manager_provisioning"
|
||||
invisible="approval_state != 'waiting_provisioning'"/>
|
||||
<button name="action_reject_document"
|
||||
string="Reject"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
groups="grt_product_engineering.group_ifc_manager_production,grt_product_engineering.group_ifc_manager_provisioning"
|
||||
invisible="approval_state not in ('in_validation', 'waiting_production', 'waiting_provisioning')"/>
|
||||
<button name="action_back_to_draft"
|
||||
string="Back to Draft"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
invisible="approval_state == 'draft'"/>
|
||||
<field name="approval_state" widget="statusbar" statusbar_visible="draft,in_validation,waiting_production,waiting_provisioning,approved,rejected"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//form/sheet" position="inside">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_estimated_bom"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-sitemap"
|
||||
invisible="estimated_bom_count == 0">
|
||||
<field name="estimated_bom_count" widget="statinfo" string="Estimated BoM"/>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//form/sheet/group[1]" position="before">
|
||||
<group string="Document Management">
|
||||
<group>
|
||||
<field name="document_ref"/>
|
||||
<field name="uploaded_date"/>
|
||||
<field name="uploaded_by_id"/>
|
||||
<field name="last_update"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="last_revision_label"/>
|
||||
<field name="last_revision_date"/>
|
||||
<field name="revised_by_id"/>
|
||||
<field name="last_revision_note"/>
|
||||
</group>
|
||||
</group>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//group[field[@name='sample_file_key']]" position="inside">
|
||||
<field name="revision_note_input" placeholder="Catatan perubahan revisi..."/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//form/sheet/notebook" position="inside">
|
||||
<page string="Revisions">
|
||||
<field name="revision_ids" readonly="1">
|
||||
<list string="Revision History" create="0" delete="0">
|
||||
<field name="revision_label"/>
|
||||
<field name="revision_date"/>
|
||||
<field name="revised_by_id"/>
|
||||
<field name="revision_note"/>
|
||||
<field name="is_current"/>
|
||||
<field name="file_name" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Approval & Activity History">
|
||||
<field name="history_ids" readonly="1">
|
||||
<list string="Activity History" create="0" delete="0">
|
||||
<field name="activity_date"/>
|
||||
<field name="activity_type"/>
|
||||
<field name="user_id"/>
|
||||
<field name="state_snapshot"/>
|
||||
<field name="note"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="BoM Estimation">
|
||||
<group>
|
||||
<field name="estimated_product_tmpl_id"/>
|
||||
<field name="estimated_bom_id" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<button name="action_create_bom_estimate" type="object" class="btn-primary" string="Generate / Update Estimated BoM"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//form" position="inside">
|
||||
<chatter/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_mrp_bom_form_inherit_ifc_project" model="ir.ui.view">
|
||||
<field name="name">mrp.bom.form.inherit.ifc.project</field>
|
||||
<field name="model">mrp.bom</field>
|
||||
<field name="inherit_id" ref="mrp.mrp_bom_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='code']" position="after">
|
||||
<field name="ifc_project_id" readonly="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user