First Commit

This commit is contained in:
2026-05-31 10:17:09 +07:00
commit 17a9c69379
4547 changed files with 1170384 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
from . import models
+21
View File
@@ -0,0 +1,21 @@
{
'name': 'GRT IfcOpenShell Integration',
'version': '19.0.2.0.0',
'category': 'Construction/BIM',
'summary': 'Integrasi Odoo dengan library IfcOpenShell untuk file IFC',
'description': """
Modul ini digunakan untuk memproses dan mengelola file IFC (Industry Foundation Classes)
menggunakan library IfcOpenShell di Odoo 19.
""",
'author': 'GRT',
'maintainer': 'GRT',
'depends': ['base'],
'data': [
'security/ir.model.access.csv',
'views/ifc_project_menu.xml',
'views/ifc_project_views.xml',
],
'installable': True,
'application': True,
'license': 'LGPL-3',
}
+2
View File
@@ -0,0 +1,2 @@
from . import ifc_project
from . import ifc_bom
+20
View File
@@ -0,0 +1,20 @@
from odoo import fields, models
class IfcBom(models.Model):
_name = 'grt.ifc.bom'
_description = 'IFC Bill of Materials'
_order = 'level, parent_id, sequence, id'
project_id = fields.Many2one('grt.ifc.project', string='Project', required=True, ondelete='cascade')
ifc_key = fields.Char(string='IFC Key', required=True, index=True)
name = fields.Char(string='Name', required=True)
global_id = fields.Char(string='GlobalId', index=True)
ifc_type = fields.Char(string='IFC Type', required=True, default='IfcProduct', index=True)
parent_id = fields.Many2one('grt.ifc.bom', string='Parent Item', ondelete='cascade', index=True)
child_ids = fields.One2many('grt.ifc.bom', 'parent_id', string='Child Items')
relation_type = fields.Char(string='Relation Type', readonly=True)
level = fields.Integer(string='Level', default=0, readonly=True)
sequence = fields.Integer(string='Sequence', default=10)
quantity = fields.Float(string='Quantity', digits=(16, 4), default=1.0)
unit_name = fields.Char(string='Unit', default='ea')
+337
View File
@@ -0,0 +1,337 @@
import base64
import os
import tempfile
from collections import OrderedDict
from pathlib import Path
from odoo import _, api, fields, models, Command
from odoo.exceptions import UserError
class IfcProject(models.Model):
_name = 'grt.ifc.project'
_description = 'IFC Project'
_order = 'id desc'
name = fields.Char(required=True)
company_id = fields.Many2one(
'res.company',
default=lambda self: self.env.company,
required=True,
index=True,
)
file_data = fields.Binary(string='IFC File', required=True, attachment=True)
file_name = fields.Char(string='Filename')
sample_file_key = fields.Selection(
selection='_selection_sample_files',
string='Sample IFC',
help='Pilih sample IFC dari folder samples untuk pengujian cepat.',
)
state = fields.Selection(
[
('draft', 'Draft'),
('parsed', 'Parsed'),
('failed', 'Failed'),
],
default='draft',
required=True,
)
schema_name = fields.Char(readonly=True)
entity_count = fields.Integer(readonly=True)
parse_message = fields.Text(readonly=True)
product_count = fields.Integer(string='IfcProduct Count', readonly=True)
assembly_count = fields.Integer(string='IfcAssembly Count', readonly=True)
part_count = fields.Integer(string='Part Count', readonly=True)
fastener_count = fields.Integer(string='Fastener Count', readonly=True)
manufacturing_info = fields.Text(string='Manufacturing Details', readonly=True)
bom_ids = fields.One2many('grt.ifc.bom', 'project_id', string='Bill of Materials')
parsed_at = fields.Datetime(readonly=True)
@api.model
def _get_ifcopenshell(self):
try:
import ifcopenshell # pylint: disable=import-outside-toplevel
except ImportError as exc:
raise UserError(
_(
'Python package "ifcopenshell" belum terpasang di environment Odoo.'
)
) from exc
return ifcopenshell
@api.model
def _samples_dir(self):
return Path(__file__).resolve().parents[1] / 'samples'
@api.model
def _selection_sample_files(self):
samples_dir = self._samples_dir()
if not samples_dir.exists():
return []
return [
(sample.name, sample.name)
for sample in sorted(samples_dir.glob('*.ifc'))
if sample.is_file()
]
def action_load_sample_ifc(self):
for record in self:
if not record.sample_file_key:
raise UserError(_('Pilih sample IFC terlebih dahulu.'))
sample_path = self._samples_dir() / record.sample_file_key
if not sample_path.exists() or not sample_path.is_file():
raise UserError(_('File sample tidak ditemukan: %s') % record.sample_file_key)
payload = sample_path.read_bytes()
record.write(
{
'name': record.name or sample_path.stem,
'file_name': sample_path.name,
'file_data': base64.b64encode(payload),
}
)
record.action_reset_parse()
@api.model
def _is_product_engineering_entity(self, entity):
try:
if not entity or not entity.is_a('IfcProduct'):
return False
if entity.is_a('IfcSpatialStructureElement') or entity.is_a('IfcProject'):
return False
return True
except Exception:
return False
@api.model
def _entity_key(self, entity):
return getattr(entity, 'GlobalId', None) or f"#{entity.id()}"
@api.model
def _extract_quantity(self, entity):
unit_by_value_attr = {
'CountValue': 'ea',
'LengthValue': 'm',
'AreaValue': 'm2',
'VolumeValue': 'm3',
'WeightValue': 'kg',
'TimeValue': 's',
}
try:
for definition in getattr(entity, 'IsDefinedBy', []) or []:
if not definition.is_a('IfcRelDefinesByProperties'):
continue
prop_def = getattr(definition, 'RelatingPropertyDefinition', None)
if not prop_def or not prop_def.is_a('IfcElementQuantity'):
continue
for quantity in getattr(prop_def, 'Quantities', []) or []:
for attr_name, unit_name in unit_by_value_attr.items():
if hasattr(quantity, attr_name):
value = getattr(quantity, attr_name)
if isinstance(value, (int, float)):
return float(value), unit_name
except Exception:
pass
return 1.0, 'ea'
def action_parse_ifc(self):
for record in self:
if not record.file_data:
raise UserError(_('Unggah file IFC terlebih dahulu.'))
ifcopenshell = self._get_ifcopenshell()
file_path = None
try:
with tempfile.NamedTemporaryFile(
mode='wb', suffix='.ifc', delete=False
) as temp_ifc:
temp_ifc.write(base64.b64decode(record.file_data))
file_path = temp_ifc.name
ifc_model = ifcopenshell.open(file_path)
def safe_by_type(model, entity_type):
try:
return model.by_type(entity_type) or []
except Exception:
return []
entities = safe_by_type(ifc_model, 'IfcRoot')
# --- Ekstraksi Entitas Manufaktur ---
products = safe_by_type(ifc_model, 'IfcProduct')
assemblies = safe_by_type(ifc_model, 'IfcElementAssembly')
# IFC2X3 umumnya memakai IfcBuildingElementPart, beberapa varian lain memakai IfcPart.
parts = safe_by_type(ifc_model, 'IfcBuildingElementPart')
if not parts:
parts = safe_by_type(ifc_model, 'IfcPart')
# Fastener dapat muncul sebagai IfcMechanicalFastener (IFC4) atau IfcFastener.
fasteners = safe_by_type(ifc_model, 'IfcMechanicalFastener')
if not fasteners:
fasteners = safe_by_type(ifc_model, 'IfcFastener')
mfg_details = (
f"Identifikasi Manufaktur:\n"
f"- Schema IFC: {getattr(ifc_model, 'schema', '-') or '-'}\n"
f"- Total IfcProduct: {len(products)}\n"
f"- Total Assembly (IfcElementAssembly): {len(assemblies)}\n"
f"- Total Part: {len(parts)}\n"
f"- Total Fastener: {len(fasteners)}\n"
)
if assemblies:
mfg_details += "\nDaftar Assembly Utama (Maksimal 10):\n"
for asm in assemblies[:10]:
mfg_details += f"- {asm.Name or 'Unnamed Assembly'} (GlobalId: {asm.GlobalId})\n"
if len(assemblies) > 10:
mfg_details += f"... dan {len(assemblies) - 10} assembly lainnya.\n"
# --- Membuat Data BOM (Bills of Materials) ---
# (5, 0, 0) digunakan untuk menghapus semua relasi lama sebelum mengganti dengan yang baru
bom_data = [Command.clear()]
bom_rows = OrderedDict()
def add_bom_row(entity, default_name, relation_type='Direct', parent_key=None, sequence=10):
key = self._entity_key(entity)
if key in bom_rows:
existing_parent = bom_rows[key].get('parent_key')
if parent_key and not existing_parent:
bom_rows[key]['parent_key'] = parent_key
bom_rows[key]['relation_type'] = relation_type
return
qty_value, qty_unit = self._extract_quantity(entity)
bom_rows[key] = {
'ifc_key': key,
'name': getattr(entity, 'Name', None) or default_name,
'global_id': getattr(entity, 'GlobalId', ''),
'ifc_type': entity.is_a(),
'quantity': qty_value,
'unit_name': qty_unit,
'relation_type': relation_type,
'parent_key': parent_key,
'sequence': sequence,
}
for asm in assemblies:
add_bom_row(asm, 'Unnamed Assembly')
for part in parts:
add_bom_row(part, 'Unnamed Part')
for fastener in fasteners:
add_bom_row(fastener, 'Unnamed Fastener')
relation_records = safe_by_type(ifc_model, 'IfcRelAggregates') + safe_by_type(ifc_model, 'IfcRelNests')
for relation in relation_records:
parent_entity = getattr(relation, 'RelatingObject', None)
if not self._is_product_engineering_entity(parent_entity):
continue
parent_key = self._entity_key(parent_entity)
add_bom_row(parent_entity, 'Unnamed Parent', relation_type=relation.is_a())
for idx, child_entity in enumerate(getattr(relation, 'RelatedObjects', []) or [], start=1):
if not self._is_product_engineering_entity(child_entity):
continue
add_bom_row(
child_entity,
'Unnamed Child',
relation_type=relation.is_a(),
parent_key=parent_key,
sequence=idx * 10,
)
# Fallback agar sample product engineering minimal tetap menghasilkan BOM.
if not bom_rows:
for product in products:
if not self._is_product_engineering_entity(product):
continue
add_bom_row(product, 'Unnamed Product')
level_cache = {}
def get_level(row_key):
if row_key in level_cache:
return level_cache[row_key]
row = bom_rows.get(row_key)
if not row or not row.get('parent_key'):
level_cache[row_key] = 0
return 0
parent_key = row.get('parent_key')
if parent_key == row_key or parent_key not in bom_rows:
level_cache[row_key] = 0
return 0
level_cache[row_key] = get_level(parent_key) + 1
return level_cache[row_key]
for key in bom_rows:
bom_rows[key]['level'] = get_level(key)
for values in bom_rows.values():
bom_data.append(
Command.create(
{
'ifc_key': values['ifc_key'],
'name': values['name'],
'global_id': values['global_id'],
'ifc_type': values['ifc_type'],
'relation_type': values['relation_type'],
'level': values['level'],
'sequence': values['sequence'],
'quantity': values['quantity'],
'unit_name': values['unit_name'],
}
)
)
record.write(
{
'schema_name': getattr(ifc_model, 'schema', None),
'entity_count': len(entities),
'product_count': len(products),
'assembly_count': len(assemblies),
'part_count': len(parts),
'fastener_count': len(fasteners),
'manufacturing_info': mfg_details,
'bom_ids': bom_data,
'parse_message': _('IFC berhasil diparsing.'),
'parsed_at': fields.Datetime.now(),
'state': 'parsed',
}
)
bom_by_key = {line.ifc_key: line for line in record.bom_ids}
for row in bom_rows.values():
parent_key = row.get('parent_key')
if not parent_key:
continue
child_line = bom_by_key.get(row['ifc_key'])
parent_line = bom_by_key.get(parent_key)
if child_line and parent_line and child_line.id != parent_line.id:
child_line.parent_id = parent_line.id
except Exception as exc: # pragma: no cover
record.write(
{
'state': 'failed',
'parse_message': str(exc),
}
)
raise UserError(_('Gagal memproses file IFC: %s') % exc) from exc
finally:
if file_path and os.path.exists(file_path):
os.unlink(file_path)
def action_reset_parse(self):
self.write(
{
'state': 'draft',
'schema_name': False,
'entity_count': 0,
'parse_message': False,
'product_count': 0,
'assembly_count': 0,
'part_count': 0,
'fastener_count': 0,
'manufacturing_info': False,
'bom_ids': [Command.clear()],
'parsed_at': False,
}
)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_grt_ifc_project_user,access.grt.ifc.project.user,model_grt_ifc_project,base.group_user,1,1,1,1
access_grt_ifc_bom_user,access.grt.ifc.bom.user,model_grt_ifc_bom,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_grt_ifc_project_user access.grt.ifc.project.user model_grt_ifc_project base.group_user 1 1 1 1
3 access_grt_ifc_bom_user access.grt.ifc.bom.user model_grt_ifc_bom base.group_user 1 1 1 1
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem
id="menu_grt_ifc_root"
name="IFC"
sequence="80"
groups="base.group_user"
/>
<menuitem
id="menu_grt_ifc_project"
name="IFC Projects"
parent="menu_grt_ifc_root"
action="action_grt_ifc_project"
sequence="10"
groups="base.group_user"
/>
</odoo>
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_grt_ifc_project_tree" model="ir.ui.view">
<field name="name">grt.ifc.project.tree</field>
<field name="model">grt.ifc.project</field>
<field name="arch" type="xml">
<list string="IFC Projects">
<field name="name"/>
<field name="company_id"/>
<field name="file_name"/>
<field name="schema_name"/>
<field name="entity_count"/>
<field name="product_count" optional="show"/>
<field name="assembly_count" optional="show"/>
<field name="part_count" optional="hide"/>
<field name="fastener_count" optional="hide"/>
<field name="state"/>
<field name="parsed_at"/>
</list>
</field>
</record>
<record id="view_grt_ifc_project_form" model="ir.ui.view">
<field name="name">grt.ifc.project.form</field>
<field name="model">grt.ifc.project</field>
<field name="arch" type="xml">
<form string="IFC Project">
<header>
<button name="action_parse_ifc"
string="Parse IFC"
type="object"
class="btn-primary"
invisible="state == 'parsed'"/>
<button name="action_load_sample_ifc"
string="Load Sample"
type="object"
class="btn-secondary"/>
<button name="action_reset_parse"
string="Reset"
type="object"
invisible="state == 'draft'"/>
<field name="state" widget="statusbar" statusbar_visible="draft,parsed,failed"/>
</header>
<sheet>
<group>
<group>
<field name="name"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="sample_file_key"/>
<field name="file_name"/>
<field name="file_data" filename="file_name"/>
</group>
<group>
<field name="schema_name" readonly="1"/>
<field name="entity_count" readonly="1"/>
<field name="parsed_at" readonly="1"/>
</group>
</group>
<group string="Parse Log">
<field name="parse_message" nolabel="1" readonly="1"/>
</group>
<group string="Product Engineering &amp; Manufacturing Data">
<field name="product_count"/>
<field name="assembly_count"/>
<field name="part_count"/>
<field name="fastener_count"/>
<field name="manufacturing_info" nolabel="1" readonly="1"/>
</group>
<notebook>
<page string="Bill of Materials" name="bom_page" invisible="state != 'parsed'">
<field name="bom_ids" readonly="1">
<list string="BOM">
<field name="level"/>
<field name="parent_id" optional="hide"/>
<field name="name"/>
<field name="global_id"/>
<field name="ifc_type" widget="badge"/>
<field name="relation_type" optional="show"/>
<field name="quantity" optional="show"/>
<field name="unit_name" optional="show"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_grt_ifc_project" model="ir.actions.act_window">
<field name="name">IFC Projects</field>
<field name="res_model">grt.ifc.project</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">Upload file IFC pertama Anda</p>
</field>
</record>
</odoo>