SuperAPI AI Directory

crosstech-impl-ifc-erpnext-costing

Data & analytics

View repository

Updated 10-15 22:17

About this skill

crosstech-impl-ifc-erpnext-costing

Quick Reference

IFC Quantity Types (all 5)

IFC Type Value Unit ERPNext UOM Typical Use
IfcQuantityLength m Meter Beam lengths, pipe runs, cable trays
IfcQuantityArea m2 Square Meter Wall surfaces, floor areas, roof areas
IfcQuantityVolume m3 Cubic Meter Concrete volumes, excavation volumes
IfcQuantityCount integer Nos Door count, window count, fixture count
IfcQuantityWeight kg Kg Steel tonnage, rebar weight

Standard Quantity Sets (Qto_ prefix)

Quantity Set Element Types Key Quantities
Qto_WallBaseQuantities IfcWall Length, Height, Width, GrossSideArea, NetSideArea, GrossVolume, NetVolume
Qto_SlabBaseQuantities IfcSlab Area, NominalThickness, GrossVolume, NetVolume, GrossWeight
Qto_BeamBaseQuantities IfcBeam Length, CrossSectionArea, GrossSurfaceArea, NetVolume
Qto_ColumnBaseQuantities IfcColumn Length, CrossSectionArea, GrossSurfaceArea, GrossVolume
Qto_DoorBaseQuantities IfcDoor Height, Width, Area
Qto_WindowBaseQuantities IfcWindow Height, Width, Area

Critical Warnings

NEVER map IFC quantities to ERPNext without checking the IFC file's unit system first. Use ifcopenshell.util.unit.calculate_unit_scale(model) to get the scale factor to SI units.

NEVER submit an ERPNext BOM without verifying that all referenced Items exist. The Frappe API returns a 400 error with no rollback if an Item is missing.

NEVER use IfcQuantityVolume for cost estimation of composite elements (e.g., layered walls). The gross volume includes ALL material layers — use material-specific quantities instead.

NEVER assume every IFC element has quantity sets. Many IFC files export with empty Qto_ sets or no quantity sets at all. ALWAYS validate that quantities are non-null before creating BOM Items.

NEVER create duplicate Items in ERPNext from IFC instances of the same type. Group by IfcElementType first, then aggregate quantities across instances.

ALWAYS use ifcopenshell.util.element.get_psets(element, qtos_only=True) to extract quantities — this merges occurrence-level and type-level quantity sets automatically.


Technology Boundary

Side A: IFC Model (IfcOpenShell 0.8.x)

Data format: IFC-SPF (.ifc) or IFC-XML (.ifcXML), parsed into ifcopenshell.file object.

API surface for quantity extraction:

Function Purpose
ifcopenshell.open(path) Load IFC file into memory
model.by_type("IfcWall") Get all elements of a type
ifcopenshell.util.element.get_psets(elem, qtos_only=True) Extract quantity sets
ifcopenshell.util.element.get_psets(elem, psets_only=True) Extract property sets
ifcopenshell.util.element.get_type(elem) Get the IfcElementType
ifcopenshell.util.classification.get_references(elem) Get classification codes
ifcopenshell.util.unit.calculate_unit_scale(model) Get SI unit scale factor
model.schema Check schema version (IFC2X3, IFC4, IFC4X3)

Data available: geometry, properties, quantities, classifications, materials, spatial hierarchy, relationships.

Side B: ERPNext ERP (Frappe REST API, ERPNext v15)

Data format: JSON via REST API at /api/resource/:doctype.

Core DocTypes for costing:

DocType Purpose Key Fields
Item Master record for materials/products item_code, item_name, item_group, stock_uom, description
BOM Bill of Materials (assembly recipe) item, items (child table), rm_cost_as_per
BOM Item Component row within a BOM item_code, qty, uom, rate
Project Top-level project container project_name, company
Item Group Hierarchical item categorization name, parent_item_group

Authentication: Token-based header: Authorization: token api_key:api_secret.

Data available: pricing, valuation rates, supplier info, warehouse locations, serial/batch tracking, cost centers.

The Bridge: Quantity Extraction to BOM Creation Pipeline

IFC Model (.ifc)
    │
    ▼
[1] Parse with IfcOpenShell
    │  ifcopenshell.open("project.ifc")
    │  Check units: ifcopenshell.util.unit.calculate_unit_scale(model)
    │
    ▼
[2] Extract element types + quantities
    │  Group elements by IfcElementType
    │  For each type: aggregate quantities across all instances
    │  Map classification codes to ERPNext Item Groups
    │
    ▼
[3] Create Items in ERPNext
    │  POST /api/resource/Item for each unique element type
    │  Map IFC unit → ERPNext UOM
    │  Map IFC classification → Item Group
    │
    ▼
[4] Create BOM in ERPNext
    │  POST /api/resource/BOM with aggregated quantities
    │  Reference Items created in step 3
    │
    ▼
[5] Submit BOM
    PUT /api/resource/BOM/:name with docstatus=1

Directionality: IFC → ERPNext is the primary flow. ERPNext → IFC is NOT supported (ERPNext has no geometry model, no spatial relationships, no IFC schema awareness).


Critical Rules

  1. ALWAYS check the IFC unit system before extracting quantities. Multiply all quantity values by the unit scale factor to normalize to SI units.
  2. ALWAYS group IFC elements by their IfcElementType before creating ERPNext Items. One Item per type, NOT one Item per element instance.
  3. ALWAYS verify that an Item exists in ERPNext (GET request) before referencing it in a BOM. Create missing Items first.
  4. ALWAYS use the qtos_only=True parameter when extracting quantities to avoid mixing property values with quantity values.
  5. ALWAYS map IFC units to ERPNext UOM names exactly: m → Meter, m2 → Square Meter, m3 → Cubic Meter, kg → Kg.
  6. ALWAYS handle the case where quantity sets are empty or missing — skip elements with no quantities instead of creating zero-quantity BOM Items.
  7. NEVER submit a BOM (docstatus=1) until all Items and quantities are verified. Submitted BOMs are immutable in ERPNext.
  8. NEVER use hardcoded unit conversions. Use the IFC file's IfcUnitAssignment via calculate_unit_scale().
  9. NEVER flatten IFC spatial hierarchy for cost tracking without mapping storeys to ERPNext Cost Centers first.
  10. NEVER send more than 50 API requests per second to ERPNext — use batch creation or rate limiting.

Decision Tree

START: You have an IFC file and need cost estimates in ERPNext
│
├── Q1: Does the IFC file contain quantity sets (Qto_ prefixed)?
│   ├── YES → Use existing quantities. Proceed to Q2.
│   └── NO → Quantities must be calculated from geometry.
│       └── Use IfcOpenShell geometry engine to compute volumes/areas.
│           model.by_type("IfcWall") → create_shape() → compute from mesh.
│           WARNING: Geometry-derived quantities are approximations.
│
├── Q2: What unit system does the IFC file use?
│   ├── Check: ifcopenshell.util.unit.calculate_unit_scale(model)
│   ├── Returns 1.0 → File uses meters (SI). No conversion needed.
│   ├── Returns 0.001 → File uses millimeters. Multiply all lengths by 0.001.
│   └── Returns other → Apply scale factor to ALL extracted quantities.
│
├── Q3: Does the IFC file have classification references?
│   ├── YES → Map classification codes to ERPNext Item Groups.
│   │   ├── Uniclass → Use Identification field (e.g., "Ss_25_10_30")
│   │   └── OmniClass → Use Identification field (e.g., "23-13 21 00")
│   └── NO → Use IFC element type names as Item Group fallback.
│       └── Group by is_a() result: "IfcWall" → "Walls", "IfcSlab" → "Slabs"
│
├── Q4: Single-level or multi-level BOM?
│   ├── SINGLE → One BOM per building storey with all element quantities.
│   └── MULTI → Parent BOM (building) → child BOMs (storeys) → element Items.
│       └── Create hierarchical BOMs: Building BOM references Storey BOMs.
│
└── Q5: Cost source in ERPNext?
    ├── Valuation Rate → Uses average stock valuation. Set rm_cost_as_per="Valuation Rate".
    ├── Last Purchase Rate → Uses most recent purchase price.
    └── Price List → Uses a specific price list. Specify price_list field.

Essential Patterns

Pattern 1: Extract Quantities with Unit Normalization

import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.unit

model = ifcopenshell.open("project.ifc")

# ALWAYS get unit scale first
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(model)
# unit_scale converts file units to meters (e.g., 0.001 for mm files)

for wall in model.by_type("IfcWall"):
    qtos = ifcopenshell.util.element.get_psets(wall, qtos_only=True)
    qto = qtos.get("Qto_WallBaseQuantities", {})

    # Apply unit scale to length/area/volume quantities
    length = qto.get("Length", 0) * unit_scale          # now in meters
    area = qto.get("GrossSideArea", 0) * unit_scale**2  # now in m2
    volume = qto.get("GrossVolume", 0) * unit_scale**3  # now in m3
    # Count and Weight do NOT need unit_scale conversion

Pattern 2: Group by Element Type for Item Creation

from collections import defaultdict

type_quantities = defaultdict(lambda: {"count": 0, "volume": 0.0, "area": 0.0})

for wall in model.by_type("IfcWall"):
    wall_type = ifcopenshell.util.element.get_type(wall)
    type_name = wall_type.Name if wall_type else "UNTYPED_WALL"

    qtos = ifcopenshell.util.element.get_psets(wall, qtos_only=True)
    qto = qtos.get("Qto_WallBaseQuantities", {})

    type_quantities[type_name]["count"] += 1
    type_quantities[type_name]["volume"] += qto.get("GrossVolume", 0) * unit_scale**3
    type_quantities[type_name]["area"] += qto.get("GrossSideArea", 0) * unit_scale**2

# Result: {"Concrete Wall 200mm": {"count": 12, "volume": 45.6, "area": 228.0}, ...}

Pattern 3: Create ERPNext Items from IFC Types

import requests

ERP_URL = "https://erp.example.com"
HEADERS = {
    "Authorization": "token api_key:api_secret",
    "Content-Type": "application/json"
}

IFC_UNIT_TO_ERP_UOM = {
    "length": "Meter",
    "area": "Square Meter",
    "volume": "Cubic Meter",
    "count": "Nos",
    "weight": "Kg",
}

def create_item(item_code: str, item_name: str, group: str, uom: str) -> dict:
    """Create an ERPNext Item. Returns the created item data."""
    # ALWAYS check if item exists first
    check = requests.get(
        f"{ERP_URL}/api/resource/Item/{item_code}",
        headers=HEADERS
    )
    if check.status_code == 200:
        return check.json()["data"]  # Item already exists

    item_data = {
        "doctype": "Item",
        "item_code": item_code,
        "item_name": item_name,
        "item_group": group,
        "stock_uom": uom,
        "description": f"Auto-created from IFC model ({item_name})"
    }
    response = requests.post(
        f"{ERP_URL}/api/resource/Item",
        json=item_data,
        headers=HEADERS
    )
    response.raise_for_status()
    return response.json()["data"]

Common Operations

IFC Unit to ERPNext UOM Mapping

IFC Unit (from IfcUnitAssignment) unit_scale Result ERPNext UOM Conversion
METRE 1.0 Meter Direct
MILLIMETRE 0.001 Meter value * 0.001
SQUARE_METRE 1.0 Square Meter Direct
CUBIC_METRE 1.0 Cubic Meter Direct
KILOGRAM 1.0 Kg Direct
POUND 0.453592 Kg value * 0.453592

IFC Classification to ERPNext Item Group Mapping

IFC Source Field ERPNext Target Example
IfcClassificationReference.Identification Uniclass code Item.item_group "Ss_25_10_30" → "Concrete Wall Systems"
IfcClassificationReference.Name Human name Item.item_name "Concrete wall systems"
element.is_a() IFC entity type Fallback group "IfcWall" → "Walls"
IfcElementType.Name Type name Item.item_code "Basic Wall:200mm Concrete"

Data Loss at the Boundary

IFC data with NO ERPNext equivalent:

  • Geometric representations (IfcShapeRepresentation) — ERPNext has no geometry model
  • Spatial relationships (IfcRelContainedInSpatialStructure) — ERPNext uses flat item lists
  • Material layer composition (IfcMaterialLayerSetUsage) — ERPNext tracks single-material items
  • Connection geometry (IfcRelConnectsPathElements) — no structural analysis in ERPNext
  • Parametric definitions — ERPNext stores quantities, not parametric rules

ERPNext data with NO IFC equivalent:

  • Pricing and valuation rates — IFC has no cost schema
  • Warehouse and bin locations — IFC tracks building locations, not stock locations
  • Supplier information — IFC has IfcOrganization but not procurement workflows
  • Serial numbers and batch tracking — no equivalent in IFC
  • Workflow states (Draft, Submitted, Cancelled) — no equivalent in IFC

Frappe API: Creating and Submitting a BOM

def create_bom(building_item: str, bom_items: list[dict]) -> str:
    """Create and submit a BOM in ERPNext. Returns BOM name."""
    bom_data = {
        "doctype": "BOM",
        "item": building_item,
        "items": bom_items,  # [{"item_code": ..., "qty": ..., "uom": ...}, ...]
        "rm_cost_as_per": "Valuation Rate"
    }
    response = requests.post(
        f"{ERP_URL}/api/resource/BOM",
        json=bom_data,
        headers=HEADERS
    )
    response.raise_for_status()
    bom_name = response.json()["data"]["name"]

    # Submit the BOM (makes it active and immutable)
    requests.put(
        f"{ERP_URL}/api/resource/BOM/{bom_name}",
        json={"docstatus": 1},
        headers=HEADERS
    ).raise_for_status()

    return bom_name

Reference Links

Official Sources