Repository: cognitedata/neat
Pull request #1087: Neat 880 Selection of base model for sourcing concepts for drop down menus
Author: login: nikokaoja
Created: 2025-04-03T14:35:29Z | Merged: 2025-05-05T13:27:12Z

# Description

This enables specialization of base model for sourcing concepts for drop down menus when making template for defining data models using spreadsheets. There was a necessary refactoring of `ExcelExporter` to accommodate for this feature, which entailed reduction of complexity of the whole exporter (e.g. instead of having dedicated methods to create helper sheets / drop-downs/ data validations for each role, now these are singular methods, which behavior is configured by indicating role).

## Bump

- [ ] Patch
- [X] Minor
- [ ] Skip

## Changelog
### Added
- Support for specifying base model and max number of concepts in `NeatSession.template.conceptual_data_model()`



Diff:
diff --git a/cognite/neat/_constants.py b/cognite/neat/_constants.py
index 5ce690910d..dc1331105c 100644
--- a/cognite/neat/_constants.py
+++ b/cognite/neat/_constants.py
@@ -1,7 +1,7 @@
 import re
 from collections.abc import Mapping
 from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Literal
 
 from cognite.client import data_modeling as dm
 from cognite.client.data_classes.data_modeling.ids import DataModelId
@@ -9,6 +9,7 @@
 from rdflib.namespace import DefinedNamespace
 
 from cognite import neat
+from cognite.neat._issues.errors._general import NeatValueError
 
 if TYPE_CHECKING:
     from cognite.neat._rules.models.dms import DMSProperty
@@ -71,16 +72,16 @@ def _is_in_browser() -> bool:
     "CogniteActivity",
     "CogniteTimeSeries",
     "CogniteFile",
+    "CogniteDescribable",
+    "CogniteSourceable",
+    "CogniteSchedulable",
+    "CogniteVisualizable",
+    "CogniteSourceSystem",
     "CogniteUnit",
     "CogniteAssetClass",
     "CogniteAssetType",
     "CogniteEquipmentType",
     "CogniteFileCategory",
-    "CogniteDescribable",
-    "CogniteSourceable",
-    "CogniteSourceSystem",
-    "CogniteSchedulable",
-    "CogniteVisualizable",
     "CogniteAnnotation",
     "CogniteDiagramAnnotation",
     "CogniteCubeMap",
@@ -192,6 +193,8 @@ def get_default_prefixes_and_namespaces() -> dict[str, Namespace]:
     "view_property": "path",
 }
 
+BASE_MODEL = Literal["CogniteCore"]
+
 
 def get_asset_read_only_properties_with_connection() -> "list[DMSProperty]":
     """Gets the asset read-only properties with connection, i.e. Root and Path."""
@@ -200,6 +203,22 @@ def get_asset_read_only_properties_with_connection() -> "list[DMSProperty]":
     return [DMSProperty.model_validate(item) for item in (_ASSET_ROOT_PROPERTY, _ASSET_PATH_PROPERTY)]
 
 
+def get_base_concepts(
+    base_model: BASE_MODEL = "CogniteCore",
+    total_concepts: int | None = None,
+) -> list[str]:
+    """Gets the base concepts for a given base model represented in the short form.
+    Args:
+        base_model: The base model to get the concepts for.
+        total_concepts: The number of concepts to get. If None, all concepts are returned.
+    """
+
+    if base_model == "CogniteCore":
+        return [f"cdf_cdm:{concept}(version=v1)" for concept in COGNITE_CONCEPTS][:total_concepts]
+    else:
+        raise NeatValueError(f"Base model <{base_model}> is not supported")
+
+
 READONLY_PROPERTIES_BY_CONTAINER: Mapping[dm.ContainerId, frozenset[str]] = {
     dm.ContainerId("cdf_cdm", "CogniteAsset"): frozenset(
         {"assetHierarchy_root", "assetHierarchy_path", "assetHierarchy_path_last_updated_time"}
diff --git a/cognite/neat/_rules/exporters/_rules2excel.py b/cognite/neat/_rules/exporters/_rules2excel.py
index b7d1b010fd..660ce52995 100644
--- a/cognite/neat/_rules/exporters/_rules2excel.py
+++ b/cognite/neat/_rules/exporters/_rules2excel.py
@@ -4,15 +4,17 @@
 from datetime import datetime
 from pathlib import Path
 from types import GenericAlias
-from typing import Any, ClassVar, Literal, cast, get_args
+from typing import Any, ClassVar, Literal, cast, get_args, overload
 
 from openpyxl import Workbook
 from openpyxl.cell import MergedCell
 from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
+from openpyxl.utils import get_column_letter
+from openpyxl.worksheet.datavalidation import DataValidation
 from openpyxl.worksheet.worksheet import Worksheet
 from rdflib import Namespace
 
-from cognite.neat._constants import COGNITE_CONCEPTS
+from cognite.neat._constants import BASE_MODEL, get_base_concepts
 from cognite.neat._rules._constants import get_internal_properties
 from cognite.neat._rules._shared import VerifiedRules
 from cognite.neat._rules.models import (
@@ -67,6 +69,16 @@ class ExcelExporter(BaseExporter[VerifiedRules, Workbook]):
         "Nodes": "Definition of Nodes",
         "Enum": "Definition of Enum Collections",
     }
+    _helper_sheet_column_indexes_by_names: ClassVar[dict[str, int]] = {
+        "Class": 1,
+        "View": 1,
+        "Implements": 2,
+        "Value Type": 3,
+        "Container": 4,
+        "In Model": 5,
+        "Immutable": 5,
+        "Used For": 6,
+    }
     style_options = get_args(Style)
     dump_options = get_args(DumpOptions)
 
@@ -80,6 +92,8 @@ def __init__(
         hide_internal_columns: bool = True,
         include_properties: Literal["same-space", "all"] = "all",
         add_drop_downs: bool = True,
+        base_model: BASE_MODEL | None = None,
+        total_concepts: int | None = None,
     ):
         self.sheet_prefix = sheet_prefix or ""
         if styling not in self.style_options:
@@ -92,6 +106,8 @@ def __init__(
         self.hide_internal_columns = hide_internal_columns
         self.include_properties = include_properties
         self.add_drop_downs = add_drop_downs
+        self.base_model = base_model
+        self.total_concepts = total_concepts
 
     @property
     def description(self) -> str:
@@ -106,6 +122,12 @@ def export_to_file(self, rules: VerifiedRules, filepath: Path) -> None:
             data.close()
         return None
 
+    @overload
+    def template(self, role: RoleTypes, filepath: Path) -> None: ...
+
+    @overload
+    def template(self, role: RoleTypes, filepath: Path | None = None) -> None: ...
+
     def template(self, role: RoleTypes, filepath: Path | None = None) -> None | Workbook:
         """This method will create an spreadsheet template for data modeling depending on the role.
 
@@ -137,10 +159,7 @@ def template(self, role: RoleTypes, filepath: Path | None = None) -> None | Work
         self._adjust_column_widths(workbook)
         self._hide_internal_columns(workbook)
 
-        if role == RoleTypes.dms:
-            self._add_dms_drop_downs(workbook)
-        else:
-            self._add_info_drop_downs(workbook)
+        self._add_drop_downs(role, workbook)
 
         if filepath:
             try:
@@ -176,10 +195,8 @@ def export(self, rules: VerifiedRules) -> Workbook:
             self._hide_internal_columns(workbook)
 
         # Only add drop downs if the rules are DMSRules
-        if self.add_drop_downs and isinstance(rules, DMSRules):
-            self._add_dms_drop_downs(workbook)
-        elif self.add_drop_downs and isinstance(rules, InformationRules):
-            self._add_info_drop_downs(workbook)
+        if self.add_drop_downs:
+            self._add_drop_downs(rules.metadata.role, workbook)
 
         return workbook
 
@@ -199,199 +216,238 @@ def _hide_internal_columns(self, workbook: Workbook) -> None:
                 if column_letter:
                     ws.column_dimensions[column_letter].hidden = True
 
-    def _add_info_drop_downs(self, workbook: Workbook, no_rows: int = 100) -> None:
+    def _add_drop_downs(
+        self,
+        role: RoleTypes,
+        workbook: Workbook,
+    ) -> None:
         """Adds drop down menus to specific columns for fast and accurate data entry
-        in the Information rules.
+        for both DMS and Information rules.
 
         Args:
+            role: The role for which the drop downs are created. Can be either "dms" or "information".
             workbook: Workbook representation of the Excel file.
-            no_rows: number of rows to add drop down menus. Defaults to 100*100.
-
-        !!! note "Why no_rows=100?"
-            Maximum number of views per data model is 100, thus this value is set accordingly
 
         !!! note "Why defining individual data validation per desired column?
             This is due to the internal working of openpyxl. Adding same validation to
             different column leads to unexpected behavior when the openpyxl workbook is exported
             as and Excel file. Probably, the validation is not copied to the new column,
             but instead reference to the data validation object is added.
+
+        !!! note "Why 100*100 rows?"
+            This is due to the fact that we need to add drop down menus for all properties
+            in the sheet. The maximum number of properties per view/class is 100. So considering
+            maximum number of views/classes is 100, we need to add 100*100 rows.
         """
-        self._make_helper_info_sheet(workbook, no_rows)
+
+        self._make_helper_sheet(role, workbook, 100)
+
+        data_validators: dict[str, DataValidation] = {}
 
         # We need create individual data validation and cannot re-use the same one due
         # the internals of openpyxl
-        dv_classes = generate_data_validation(self._helper_sheet_name, "A", no_header_rows=0, no_rows=no_rows)
-        dv_value_types = generate_data_validation(self._helper_sheet_name, "B", no_header_rows=0, no_rows=no_rows)
-        dv_implements = generate_data_validation(
-            self._helper_sheet_name,
-            "C",
-            no_header_rows=0,
-            no_rows=no_rows + len(COGNITE_CONCEPTS),
+
+        self._add_data_validation(
+            workbook,
+            sheet_name="Views" if role == RoleTypes.dms else "Classes",
+            column_name="Implements",
+            data_validator_name="implements",
+            data_validators=data_validators,
+            validation_range=100 + 100,  # base + user concepts (max)
+            total_rows=100,
+        )
+
+        self._add_data_validation(
+            workbook,
+            sheet_name="Properties",
+            column_name="Value Type",
+            data_validator_name="value_type",
+            data_validators=data_validators,
+            validation_range=150,  # primitive types + classes
+            total_rows=100 * 100,  # 100 views/classes * 100 properties (max properties per view/class)
         )
 
-        workbook["Properties"].add_data_validation(dv_classes)
-        workbook["Properties"].add_data_validation(dv_value_types)
-        workbook["Classes"].add_data_validation(dv_implements)
+        self._add_data_validation(
+            workbook,
+            sheet_name="Properties",
+            column_name="View" if role == RoleTypes.dms else "Class",
+            data_validator_name="views_or_classes",
+            data_validators=data_validators,
+            validation_range=100,
+            total_rows=100 * 100,
+        )
 
-        # we multiply no_rows with 100 since a view can have max 100 properties per view
-        if column := find_column_with_value(workbook["Properties"], "Class"):
-            dv_classes.add(f"{column}{3}:{column}{no_rows * 100}")
+        if role == RoleTypes.dms:
+            self._add_data_validation(
+                workbook,
+                sheet_name="Properties",
+                column_name="Container",
+                data_validator_name="container",
+                data_validators=data_validators,
+                validation_range=100,
+                total_rows=100 * 100,
+            )
 
-        if column := find_column_with_value(workbook["Properties"], "Value Type"):
-            dv_value_types.add(f"{column}{3}:{column}{no_rows * 100}")
+            self._add_data_validation(
+                workbook,
+                sheet_name="Properties",
+                column_name="Immutable",
+                data_validator_name="immutable",
+                data_validators=data_validators,
+                validation_range=2,
+                total_rows=100 * 100,
+            )
 
-        if column := find_column_with_value(workbook["Classes"], "Implements"):
-            dv_implements.add(f"{column}{3}:{column}{no_rows}")
+            self._add_data_validation(
+                workbook,
+                sheet_name="Views",
+                column_name="In Model",
+                data_validator_name="in_model",
+                data_validators=data_validators,
+                validation_range=2,
+                total_rows=100,  # 100 views
+            )
+
+            self._add_data_validation(
+                workbook,
+                sheet_name="Containers",
+                column_name="Used For",
+                data_validator_name="used_for",
+                data_validators=data_validators,
+                validation_range=3,
+                total_rows=100,  # 100 views
+            )
+
+    def _make_helper_sheet(
+        self,
+        role: RoleTypes,
+        workbook: Workbook,
+        no_rows: int,
+    ) -> None:
+        """This helper sheet is used as source of data for drop down menus creation
+
+        Args:
+            role: The role for which the helper sheet is created. Can be either "dms" or "information".
+            workbook: Workbook representation of the Excel file.
+            no_rows: number of rows to add data too that will form base for drop down menus.
+        """
 
-    def _make_helper_info_sheet(self, workbook: Workbook, no_rows: int) -> None:
-        """This helper Information sheet is used as source of data for drop down menus creation"""
         workbook.create_sheet(title=self._helper_sheet_name)
 
-        for dtype_counter, dtype in enumerate(_DATA_TYPE_BY_DMS_TYPE.values()):
+        value_type_counter = 0
+
+        for value_type_counter, value_type in enumerate(_DATA_TYPE_BY_DMS_TYPE.values()):
+            value_type_as_str = value_type.dms._type.casefold() if role == RoleTypes.dms else value_type.xsd
             # skip types which require special handling or are surpassed by CDM
-            if dtype.xsd in ["enum", "timeseries", "sequence", "file", "json"]:
+            if value_type_as_str in ["enum", "timeseries", "sequence", "file", "json"]:
                 continue
-            workbook[self._helper_sheet_name].cell(row=dtype_counter + 1, column=2, value=dtype.xsd)
-
-        # Add Cognite Core Data Views:
-        for concept_counter, concept in enumerate(COGNITE_CONCEPTS):
             workbook[self._helper_sheet_name].cell(
-                row=concept_counter + 1,
-                column=3,
-                value=f"cdf_cdm:{concept}(version=v1)",
+                row=value_type_counter + 1,
+                column=self._helper_sheet_column_indexes_by_names["Value Type"],
+                value=value_type_as_str,
             )
 
+        value_type_counter += 1
+
+        concept_counter = 0
+        if self.base_model and (concepts := get_base_concepts(self.base_model, self.total_concepts)):
+            for concept_counter, concept in enumerate(concepts):
+                workbook[self._helper_sheet_name].cell(
+                    row=concept_counter + 1,
+                    column=self._helper_sheet_column_indexes_by_names["Implements"],
+                    value=concept,
+                )
+            concept_counter += 1
+
+        views_or_classes_sheet = "Views" if role == RoleTypes.dms else "Classes"
+        view_or_class_column = "View" if role == RoleTypes.dms else "Class"
+
         for i in range(no_rows):
             workbook[self._helper_sheet_name].cell(
                 row=i + 1,
-                column=1,
-                value=f'=IF(ISBLANK(Classes!A{i + 3}), "", Classes!A{i + 3})',
+                column=self._helper_sheet_column_indexes_by_names[view_or_class_column],
+                value=f'=IF(ISBLANK({views_or_classes_sheet}!A{i + 3}), "", {views_or_classes_sheet}!A{i + 3})',
             )
+
             workbook[self._helper_sheet_name].cell(
-                row=dtype_counter + i + 2,
-                column=2,
-                value=f'=IF(ISBLANK(Classes!A{i + 3}), "", Classes!A{i + 3})',
+                row=concept_counter + i + 1,
+                column=self._helper_sheet_column_indexes_by_names["Implements"],
+                value=f'=IF(ISBLANK({views_or_classes_sheet}!A{i + 3}), "", {views_or_classes_sheet}!A{i + 3})',
             )
+
             workbook[self._helper_sheet_name].cell(
-                row=concept_counter + i + 2,
-                column=3,
-                value=f'=IF(ISBLANK(Classes!A{i + 3}), "", Classes!A{i + 3})',
+                row=value_type_counter + i + 1,
+                column=self._helper_sheet_column_indexes_by_names["Value Type"],
+                value=f'=IF(ISBLANK({views_or_classes_sheet}!A{i + 3}), "", {views_or_classes_sheet}!A{i + 3})',
             )
 
+            if role == RoleTypes.dms:
+                workbook[self._helper_sheet_name].cell(
+                    row=i + 1,
+                    column=self._helper_sheet_column_indexes_by_names["Container"],
+                    value=f'=IF(ISBLANK(Containers!A{i + 3}), "", Containers!A{i + 3})',
+                )
+
+        if role == RoleTypes.dms:
+            for i, value in enumerate([True, False, ""]):
+                workbook[self._helper_sheet_name].cell(
+                    row=i + 1,
+                    column=self._helper_sheet_column_indexes_by_names["In Model"],
+                    value=cast(bool | str, value),
+                )
+
+            for i, value in enumerate(["node", "edge", "all"]):
+                workbook[self._helper_sheet_name].cell(
+                    row=i + 1,
+                    column=self._helper_sheet_column_indexes_by_names["Used For"],
+                    value=value,
+                )
+
         workbook[self._helper_sheet_name].sheet_state = "hidden"
 
-    def _add_dms_drop_downs(self, workbook: Workbook, no_rows: int = 100) -> None:
-        """Adds drop down menus to specific columns for fast and accurate data entry
-        in the DMS rules.
+    def _add_data_validation(
+        self,
+        workbook: Workbook,
+        sheet_name: str,
+        column_name: str,
+        data_validator_name: str,
+        data_validators: dict,
+        validation_range: int,
+        total_rows: int,
+    ) -> None:
+        """Adds data validation to a column in a sheet.
 
         Args:
             workbook: Workbook representation of the Excel file.
-            no_rows: number of rows to add drop down menus. Defaults to 100*100.
-
-        !!! note "Why no_rows=100?"
-            Maximum number of views per data model is 100, thus this value is set accordingly
-
-        !!! note "Why defining individual data validation per desired column?
+            sheet_name: The name of the sheet to add the data validation to.
+            column_name: The name of the column to add the data validation to.
+            data_validator_name: The name of the data validation to add.
+            data_validators: A dictionary to store the data validators.
+            validation_range: The total number of validation values to add.
+            total_rows: The number of rows to add the data validation to.
+
+        !!! note "Why defining individual data validation per desired column?"
             This is due to the internal working of openpyxl. Adding same validation to
             different column leads to unexpected behavior when the openpyxl workbook is exported
-            as and Excel file. Probably, the validation is not copied to the new column,
-            but instead reference to the data validation object is added.
-        """
+            as and Excel file.
 
-        self._make_helper_dms_sheet(workbook, no_rows)
-
-        # We need create individual data validation and cannot re-use the same one due
-        # the internals of openpyxl
-        dv_views = generate_data_validation(self._helper_sheet_name, "A", no_header_rows=0, no_rows=no_rows)
-        dv_containers = generate_data_validation(self._helper_sheet_name, "B", no_header_rows=0, no_rows=no_rows)
-        dv_value_types = generate_data_validation(self._helper_sheet_name, "C", no_header_rows=0, no_rows=no_rows)
-        dv_implements = generate_data_validation(
+        !!! note "Why starting at row 3?"
+            This is due to the header rows in the sheet. The first two rows are reserved for the header.
+        """
+        # CREATE VALIDATOR
+        data_validators[data_validator_name] = generate_data_validation(
             self._helper_sheet_name,
-            "F",
-            no_header_rows=0,
-            no_rows=no_rows + len(COGNITE_CONCEPTS),
+            get_column_letter(self._helper_sheet_column_indexes_by_names[column_name]),
+            total_header_rows=0,
+            validation_range=validation_range,
         )
 
-        dv_immutable = generate_data_validation(self._helper_sheet_name, "D", no_header_rows=0, no_rows=3)
-        dv_in_model = generate_data_validation(self._helper_sheet_name, "D", no_header_rows=0, no_rows=3)
-        dv_used_for = generate_data_validation(self._helper_sheet_name, "E", no_header_rows=0, no_rows=3)
-
-        workbook["Properties"].add_data_validation(dv_views)
-        workbook["Properties"].add_data_validation(dv_containers)
-        workbook["Properties"].add_data_validation(dv_value_types)
-        workbook["Properties"].add_data_validation(dv_immutable)
-        workbook["Views"].add_data_validation(dv_in_model)
-        workbook["Views"].add_data_validation(dv_implements)
-        workbook["Containers"].add_data_validation(dv_used_for)
-
-        # we multiply no_rows with 100 since a view can have max 100 properties per view
-        if column := find_column_with_value(workbook["Properties"], "View"):
-            dv_views.add(f"{column}{3}:{column}{no_rows * 100}")
-
-        if column := find_column_with_value(workbook["Properties"], "Container"):
-            dv_containers.add(f"{column}{3}:{column}{no_rows * 100}")
-
-        if column := find_column_with_value(workbook["Properties"], "Value Type"):
-            dv_value_types.add(f"{column}{3}:{column}{no_rows * 100}")
-
-        if column := find_column_with_value(workbook["Properties"], "Immutable"):
-            dv_immutable.add(f"{column}{3}:{column}{no_rows * 100}")
-
-        if column := find_column_with_value(workbook["Views"], "In Model"):
-            dv_in_model.add(f"{column}{3}:{column}{no_rows}")
-
-        if column := find_column_with_value(workbook["Views"], "Implements"):
-            dv_implements.add(f"{column}{3}:{column}{no_rows}")
+        # REGISTER VALIDATOR TO SPECIFIC WORKBOOK SHEET
+        workbook[sheet_name].add_data_validation(data_validators[data_validator_name])
 
-        if column := find_column_with_value(workbook["Containers"], "Used For"):
-            dv_used_for.add(f"{column}{3}:{column}{no_rows}")
-
-    def _make_helper_dms_sheet(self, workbook: Workbook, no_rows: int) -> None:
-        """This helper DMS sheet is used as source of data for drop down menus creation"""
-        workbook.create_sheet(title=self._helper_sheet_name)
-
-        for dtype_counter, dtype in enumerate(_DATA_TYPE_BY_DMS_TYPE):
-            if dtype in ["enum", "timeseries", "sequence", "file", "json"]:
-                continue
-            workbook[self._helper_sheet_name].cell(row=dtype_counter + 1, column=3, value=dtype)
-
-        # Add Cognite Core Data Views:
-        for concept_counter, concept in enumerate(COGNITE_CONCEPTS):
-            workbook[self._helper_sheet_name].cell(
-                row=concept_counter + 1,
-                column=6,
-                value=f"cdf_cdm:{concept}(version=v1)",
-            )
-
-        for i in range(no_rows):
-            workbook[self._helper_sheet_name].cell(
-                row=i + 1,
-                column=1,
-                value=f'=IF(ISBLANK(Views!A{i + 3}), "", Views!A{i + 3})',
-            )
-            workbook[self._helper_sheet_name].cell(
-                row=i + 1,
-                column=2,
-                value=f'=IF(ISBLANK(Containers!A{i + 3}), "", Containers!A{i + 3})',
-            )
-            workbook[self._helper_sheet_name].cell(
-                row=dtype_counter + i + 2,
-                column=3,
-                value=f'=IF(ISBLANK(Views!A{i + 3}), "", Views!A{i + 3})',
-            )
-            workbook[self._helper_sheet_name].cell(
-                row=concept_counter + i + 2,
-                column=6,
-                value=f'=IF(ISBLANK(Views!A{i + 3}), "", Views!A{i + 3})',
-            )
-
-        for i, value in enumerate([True, False, ""]):
-            workbook[self._helper_sheet_name].cell(row=i + 1, column=4, value=cast(bool | str, value))
-
-        for i, value in enumerate(["node", "edge", "all"]):
-            workbook[self._helper_sheet_name].cell(row=i + 1, column=5, value=value)
-
-        workbook[self._helper_sheet_name].sheet_state = "hidden"
+        # APPLY VALIDATOR TO SPECIFIC COLUMN
+        if column_letter := find_column_with_value(workbook[sheet_name], column_name):
+            data_validators[data_validator_name].add(f"{column_letter}{3}:{column_letter}{3 + total_rows}")
 
     def _create_sheet_with_header(
         self,
diff --git a/cognite/neat/_rules/models/_base_rules.py b/cognite/neat/_rules/models/_base_rules.py
index 6426dac38c..835102e867 100644
--- a/cognite/neat/_rules/models/_base_rules.py
+++ b/cognite/neat/_rules/models/_base_rules.py
@@ -102,7 +102,6 @@ class DataModelAspect(StrEnum):
 
 
 class RoleTypes(StrEnum):
-    domain_expert = "domain expert"
     information = "information architect"
     dms = "DMS Architect"
 
diff --git a/cognite/neat/_session/_template.py b/cognite/neat/_session/_template.py
index 66b36331d6..c498b773cf 100644
--- a/cognite/neat/_session/_template.py
+++ b/cognite/neat/_session/_template.py
@@ -4,6 +4,7 @@
 from cognite.client.data_classes.data_modeling import DataModelIdentifier
 
 from cognite.neat._alpha import ExperimentalFlags
+from cognite.neat._constants import BASE_MODEL
 from cognite.neat._issues import IssueList, catch_issues
 from cognite.neat._rules._shared import ReadRules
 from cognite.neat._rules.exporters import ExcelExporter
@@ -126,19 +127,26 @@ def data_product_model(
             self._state.last_reference = last_rules
         return issues
 
-    def conceptual_model(self, io: Any) -> None:
+    def conceptual_model(
+        self,
+        io: Any,
+        base_model: BASE_MODEL = "CogniteCore",
+        total_concepts: int | None = None,
+    ) -> None:
         """This method will create a template for a conceptual data modeling
 
         Args:
             io: file path to the Excel sheet
+            base_model: The base model to use for implements in the conceptual data model.
+                        Currently only supporting CogniteCore.
+            total_concepts: The total number of concepts to provide in implements for selection.
+                         Default is None, meaning all concepts will be provided.
 
         """
         reader = NeatReader.create(io)
         path = reader.materialize_path()
 
-        ExcelExporter().template(RoleTypes.information, path)
-
-        return None
+        ExcelExporter(base_model=base_model, total_concepts=total_concepts).template(RoleTypes.information, path)
 
     def expand(self, io: Any, output: str | Path | None = None, dummy_property: str = "GUID") -> IssueList:
         """Creates a template for an extension of a Cognite model by expanding properties from CDM.
diff --git a/cognite/neat/_utils/spreadsheet.py b/cognite/neat/_utils/spreadsheet.py
index 6a5a7bfd19..62ec9f2fb2 100644
--- a/cognite/neat/_utils/spreadsheet.py
+++ b/cognite/neat/_utils/spreadsheet.py
@@ -148,10 +148,17 @@ def find_column_with_value(sheet: Worksheet, value: Any) -> str | None:
     return None
 
 
-def generate_data_validation(sheet: str, column: str, no_header_rows: int, no_rows: int) -> DataValidation:
-    "Creates openpyxl data validation object for a cell in a sheet"
+def generate_data_validation(sheet: str, column: str, total_header_rows: int, validation_range: int) -> DataValidation:
+    """Creates openpyxl data validation object for a cell in a sheet
+
+    Args:
+        sheet: The name of the sheet where the data validation is applied.
+        column: The column letter where the data validation is applied.
+        total_header_rows: The number of header rows in the sheet.
+        validation_range: The total number of validation values in the column.
+    """
 
     return DataValidation(
         type="list",
-        formula1=f"={sheet}!{column}${no_header_rows + 1}:{column}${no_rows}",
+        formula1=f"={sheet}!{column}${total_header_rows + 1}:{column}${validation_range + total_header_rows + 1}",
     )
diff --git a/tests/tests_unit/test_rules/test_exporters/test_rules2excel.py b/tests/tests_unit/test_rules/test_exporters/test_rules2excel.py
index 6901046ef2..691ab0a95d 100644
--- a/tests/tests_unit/test_rules/test_exporters/test_rules2excel.py
+++ b/tests/tests_unit/test_rules/test_exporters/test_rules2excel.py
@@ -1,8 +1,10 @@
 from pathlib import Path
 
+import pytest
 from lxml import etree
 from openpyxl import Workbook, load_workbook
 
+from cognite.neat._issues.errors._general import NeatValueError
 from cognite.neat._rules.exporters import ExcelExporter
 from cognite.neat._rules.models import (
     DMSRules,
@@ -56,7 +58,7 @@ def test_export_information_rules(self, david_rules: InformationRules):
         assert "Properties" in workbook.sheetnames
 
     def test_conceptual_data_model_template(self, tmp_path: Path):
-        exporter = ExcelExporter()
+        exporter = ExcelExporter(total_concepts=10, base_model="CogniteCore")
 
         expected = exporter.template(RoleTypes.information)
 
@@ -75,17 +77,43 @@ def test_conceptual_data_model_template(self, tmp_path: Path):
         assert expected["Properties"].data_validations.count == 2
         assert resulted["Properties"].data_validations.count == 2
 
-        assert f"<formula1>={exporter._helper_sheet_name}!A$1:A$100</formula1>" in etree.tostring(
+        # Validation should be applied to 10000 rows for Class and Value Type columns
+        assert '<dataValidation sqref="E3:E10003"' in etree.tostring(
+            resulted["Properties"].data_validations.to_tree(), pretty_print=True
+        ).decode("utf-8")
+
+        assert '<dataValidation sqref="A3:A10003"' in etree.tostring(
+            resulted["Properties"].data_validations.to_tree(), pretty_print=True
+        ).decode("utf-8")
+
+        assert f"<formula1>={exporter._helper_sheet_name}!A$1:A$101</formula1>" in etree.tostring(
             resulted["Properties"].data_validations.to_tree(), pretty_print=True
         ).decode("utf-8")
 
-        assert f"<formula1>={exporter._helper_sheet_name}!B$1:B$100</formula1>" in etree.tostring(
+        assert f"<formula1>={exporter._helper_sheet_name}!C$1:C$151</formula1>" in etree.tostring(
             resulted["Properties"].data_validations.to_tree(), pretty_print=True
         ).decode("utf-8")
 
         assert expected["Classes"].data_validations.count == 1
         assert resulted["Classes"].data_validations.count == 1
 
-        assert f"<formula1>={exporter._helper_sheet_name}!C$1:C$133</formula1>" in etree.tostring(
+        assert f"<formula1>={exporter._helper_sheet_name}!B$1:B$201</formula1>" in etree.tostring(
             resulted["Classes"].data_validations.to_tree(), pretty_print=True
         ).decode("utf-8")
+
+        assert 'dataValidation sqref="D3:D103"' in etree.tostring(
+            resulted["Classes"].data_validations.to_tree(), pretty_print=True
+        ).decode("utf-8")
+
+        # check if only 10 Cognite concepts are present in the dropdown
+        # by checking if 11th row contains a formula
+        assert expected["_helper"]["B11"].value == '=IF(ISBLANK(Classes!A3), "", Classes!A3)'
+        assert resulted["_helper"]["B11"].value == '=IF(ISBLANK(Classes!A3), "", Classes!A3)'
+
+    def test_conceptual_data_model_template_fail(self):
+        exporter = ExcelExporter(base_model="NeatModel")
+
+        with pytest.raises(NeatValueError) as e:
+            _ = exporter.template(RoleTypes.information)
+
+        assert e.value == NeatValueError(raw_message="Base model <NeatModel> is not supported")


Reviews:
user: login: rsjr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T08:19:18Z
user: login: doctrino
body: Some clarification and suggestions.

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T08:24:25Z
user: login: nikokaoja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:31:54Z
user: login: nikokaoja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:32:32Z
user: login: nikokaoja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:33:32Z
user: login: nikokaoja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:35:15Z
user: login: nikokaoja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:35:44Z
user: login: nikokaoja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:40:39Z
user: login: nikokaoja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:43:06Z
user: login: nikokaoja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:43:16Z
user: login: nikokaoja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:45:14Z
user: login: rsjr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T15:07:27Z
user: login: rsjr
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T07:42:19Z
user: login: doctrino
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T11:06:28Z
user: login: frxstrem
body: I didn't get through the entire PR before EOD, and I'm out until Monday, so if you want to get it reviewed by someone else you can remove the `risk-review-ongoing` label to put it back in the queue (if so then I think it'll keep its position in the queue and not be put in the back).

I left my risk review comments for the parts that I did get through below:
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-30T19:59:59Z
user: login: nikokaoja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T09:46:49Z
user: login: nikokaoja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:00:02Z
user: login: nikokaoja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:00:58Z
user: login: rsjr
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:12:59Z
user: login: frxstrem
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-05T13:23:32Z

Review comments:
diff_hunk: @@ -200,6 +200,23 @@ def get_asset_read_only_properties_with_connection() -> "list[DMSProperty]":
     return [DMSProperty.model_validate(item) for item in (_ASSET_ROOT_PROPERTY, _ASSET_PATH_PROPERTY)]
 
 
+def get_base_concepts(
+    base_model: Literal["CogniteCore"] = "CogniteCore",
+    no_concepts: int | None = None,
path: cognite/neat/_constants.py
user: login: doctrino
body: I read this as **No** concepts. What about?
```suggestion
    concept_count: int | None = None,
```
created_at: 2025-04-04T08:03:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -175,17 +175,26 @@ def data_product_model(
             self._state.last_reference = last_rules
         return issues
 
-    def conceptual_model(self, io: Any) -> None:
+    def conceptual_model(
+        self,
+        io: Any,
+        base_model: Literal["CogniteCore"] = "CogniteCore",
+        no_concepts: int | None = None,
path: cognite/neat/_session/_template.py
user: login: doctrino
body: I read this as No concepts. What about?
```suggestion
        concept_count: int | None = None,
```
created_at: 2025-04-04T08:08:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,6 +200,23 @@ def get_asset_read_only_properties_with_connection() -> "list[DMSProperty]":
     return [DMSProperty.model_validate(item) for item in (_ASSET_ROOT_PROPERTY, _ASSET_PATH_PROPERTY)]
 
 
+def get_base_concepts(
+    base_model: Literal["CogniteCore"] = "CogniteCore",
+    no_concepts: int | None = None,
+) -> list[str]:
+    """Gets the base concepts for a given base model represented in the short form.
+    Args:
+        base_model: The base model to get the concepts for.
+        no_concepts: The number of concepts to get. If None, all concepts are returned.
+    """
+    if base_model == "CogniteCore":
+        concepts = [f"cdf_cdm:{concept}(version=v1)" for concept in COGNITE_CONCEPTS]
path: cognite/neat/_constants.py
user: login: doctrino
body: In the `COGNITE_CONCEPTS` 
the `CogniteSourceSystem` is inbetween all the interfaces (Describable, Sourceabl). Suggest moving it either before or after.
created_at: 2025-04-04T08:10:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,6 +200,23 @@ def get_asset_read_only_properties_with_connection() -> "list[DMSProperty]":
     return [DMSProperty.model_validate(item) for item in (_ASSET_ROOT_PROPERTY, _ASSET_PATH_PROPERTY)]
 
 
+def get_base_concepts(
+    base_model: Literal["CogniteCore"] = "CogniteCore",
+    no_concepts: int | None = None,
+) -> list[str]:
+    """Gets the base concepts for a given base model represented in the short form.
+    Args:
+        base_model: The base model to get the concepts for.
+        no_concepts: The number of concepts to get. If None, all concepts are returned.
+    """
+    if base_model == "CogniteCore":
+        concepts = [f"cdf_cdm:{concept}(version=v1)" for concept in COGNITE_CONCEPTS]
+    else:
+        concepts = []
path: cognite/neat/_constants.py
user: login: doctrino
body: Raise error instead. With the Literal the developer should never pass in anything other than `CogniteCore`.
created_at: 2025-04-04T08:10:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,10 +148,10 @@ def find_column_with_value(sheet: Worksheet, value: Any) -> str | None:
     return None
 
 
-def generate_data_validation(sheet: str, column: str, no_header_rows: int, no_rows: int) -> DataValidation:
+def generate_data_validation(sheet: str, column: str, no_header_rows: int, no_validation_values: int) -> DataValidation:
path: cognite/neat/_utils/spreadsheet.py
user: login: doctrino
body: I read this as **no** validation values and expects a bool.
```suggestion
def generate_data_validation(sheet: str, column: str, no_header_rows: int, validation_value_count: int) -> DataValidation:
```
created_at: 2025-04-04T08:17:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -263,6 +263,12 @@ properties:
   neatId: http://purl.org/cognite/neat/data-model/verified/logical/my_space/MyModel/v1/PowerGeneratingUnit/pathLastUpdatedTime
   property_: pathLastUpdatedTime
   value_type: dateTime
+- class_: PowerGeneratingUnit
+  max_count: 1.0
+  min_count: 0
+  neatId: http://purl.org/cognite/neat/data-model/verified/logical/my_space/MyModel/v1/PowerGeneratingUnit/powerGeneratingUnitGUID
+  property_: powerGeneratingUnitGUID
+  value_type: string
path: tests/tests_integration/test_session/test_graph_flow/test_create_extension_template.yml
user: login: doctrino
body: Why are these showing up? 
created_at: 2025-04-04T08:17:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,6 +200,23 @@ def get_asset_read_only_properties_with_connection() -> "list[DMSProperty]":
     return [DMSProperty.model_validate(item) for item in (_ASSET_ROOT_PROPERTY, _ASSET_PATH_PROPERTY)]
 
 
+def get_base_concepts(
+    base_model: Literal["CogniteCore"] = "CogniteCore",
+    no_concepts: int | None = None,
+) -> list[str]:
+    """Gets the base concepts for a given base model represented in the short form.
+    Args:
+        base_model: The base model to get the concepts for.
+        no_concepts: The number of concepts to get. If None, all concepts are returned.
+    """
+    if base_model == "CogniteCore":
+        concepts = [f"cdf_cdm:{concept}(version=v1)" for concept in COGNITE_CONCEPTS]
+    else:
+        concepts = []
+
+    return concepts[:no_concepts]
path: cognite/neat/_constants.py
user: login: rsjr
body: ```suggestion
    concepts: List[str] = []
    
    if base_model == "CogniteCore":
        concepts = [f"cdf_cdm:{concept}(version=v1)" for concept in COGNITE_CONCEPTS]

    return concepts[:no_concepts]
```
created_at: 2025-04-04T08:18:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,10 +148,10 @@ def find_column_with_value(sheet: Worksheet, value: Any) -> str | None:
     return None
 
 
-def generate_data_validation(sheet: str, column: str, no_header_rows: int, no_rows: int) -> DataValidation:
+def generate_data_validation(sheet: str, column: str, no_header_rows: int, no_validation_values: int) -> DataValidation:
path: cognite/neat/_utils/spreadsheet.py
user: login: rsjr
body: what does `no_validation_values` means here?
created_at: 2025-04-04T08:18:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,199 +211,238 @@ def _hide_internal_columns(self, workbook: Workbook) -> None:
                 if column_letter:
                     ws.column_dimensions[column_letter].hidden = True
 
-    def _add_info_drop_downs(self, workbook: Workbook, no_rows: int = 100) -> None:
+    def _add_drop_downs(
+        self,
+        role: RoleTypes,
+        workbook: Workbook,
+    ) -> None:
         """Adds drop down menus to specific columns for fast and accurate data entry
-        in the Information rules.
+        for both DMS and Information rules.
 
         Args:
+            role: The role for which the drop downs are created. Can be either "dms" or "information".
             workbook: Workbook representation of the Excel file.
-            no_rows: number of rows to add drop down menus. Defaults to 100*100.
-
-        !!! note "Why no_rows=100?"
-            Maximum number of views per data model is 100, thus this value is set accordingly
 
         !!! note "Why defining individual data validation per desired column?
             This is due to the internal working of openpyxl. Adding same validation to
             different column leads to unexpected behavior when the openpyxl workbook is exported
             as and Excel file. Probably, the validation is not copied to the new column,
             but instead reference to the data validation object is added.
+
+        !!! note "Why 100*100 rows?"
+            This is due to the fact that we need to add drop down menus for all properties
+            in the sheet. The maximum number of properties per view/class is 100. So considering
+            maximum number of views/classes is 100, we need to add 100*100 rows.
         """
-        self._make_helper_info_sheet(workbook, no_rows)
+
+        self._make_helper_sheet(role, workbook, 100)
+
+        data_validators: dict[str, DataValidation] = {}
 
         # We need create individual data validation and cannot re-use the same one due
         # the internals of openpyxl
-        dv_classes = generate_data_validation(self._helper_sheet_name, "A", no_header_rows=0, no_rows=no_rows)
-        dv_value_types = generate_data_validation(self._helper_sheet_name, "B", no_header_rows=0, no_rows=no_rows)
-        dv_implements = generate_data_validation(
-            self._helper_sheet_name,
-            "C",
-            no_header_rows=0,
-            no_rows=no_rows + len(COGNITE_CONCEPTS),
+
+        self._add_data_validation(
+            workbook,
+            sheet_name="Views" if role == RoleTypes.dms else "Classes",
+            column_name="Implements",
+            data_validator_name="implements",
+            data_validators=data_validators,
+            no_validation_values=100,
+            no_rows=100,
         )
 
-        workbook["Properties"].add_data_validation(dv_classes)
-        workbook["Properties"].add_data_validation(dv_value_types)
-        workbook["Classes"].add_data_validation(dv_implements)
+        self._add_data_validation(
+            workbook,
+            sheet_name="Properties",
+            column_name="Value Type",
+            data_validator_name="value_type",
+            data_validators=data_validators,
+            no_validation_values=100,
+            no_rows=100 * 100,  # 100 views/classes * 100 properties (max properties per view/class)
+        )
 
-        # we multiply no_rows with 100 since a view can have max 100 properties per view
-        if column := find_column_with_value(workbook["Properties"], "Class"):
-            dv_classes.add(f"{column}{3}:{column}{no_rows * 100}")
+        self._add_data_validation(
+            workbook,
+            sheet_name="Properties",
+            column_name="View" if role == RoleTypes.dms else "Class",
+            data_validator_name="views_or_classes",
+            data_validators=data_validators,
+            no_validation_values=100 * 100,
+            no_rows=100,
+        )
 
-        if column := find_column_with_value(workbook["Properties"], "Value Type"):
-            dv_value_types.add(f"{column}{3}:{column}{no_rows * 100}")
+        if role == RoleTypes.dms:
+            self._add_data_validation(
+                workbook,
+                sheet_name="Properties",
+                column_name="Container",
+                data_validator_name="container",
+                data_validators=data_validators,
+                no_validation_values=100,
+                no_rows=100 * 100,
+            )
 
-        if column := find_column_with_value(workbook["Classes"], "Implements"):
-            dv_implements.add(f"{column}{3}:{column}{no_rows}")
+            self._add_data_validation(
+                workbook,
+                sheet_name="Properties",
+                column_name="Immutable",
+                data_validator_name="immutable",
+                data_validators=data_validators,
+                no_validation_values=2,
+                no_rows=100 * 100,
+            )
+
+            self._add_data_validation(
+                workbook,
+                sheet_name="Views",
+                column_name="In Model",
+                data_validator_name="in_model",
+                data_validators=data_validators,
+                no_validation_values=2,
+                no_rows=100,  # 100 views
+            )
+
+            self._add_data_validation(
+                workbook,
+                sheet_name="Containers",
+                column_name="Used For",
+                data_validator_name="used_for",
+                data_validators=data_validators,
+                no_validation_values=3,
+                no_rows=100,  # 100 views
+            )
+
+    def _make_helper_sheet(
+        self,
+        role: RoleTypes,
+        workbook: Workbook,
+        no_rows: int,
+    ) -> None:
+        """This helper sheet is used as source of data for drop down menus creation
+
+        Args:
+            role: The role for which the helper sheet is created. Can be either "dms" or "information".
+            workbook: Workbook representation of the Excel file.
+            no_rows: number of rows to add data too that will form base for drop down menus.
+        """
 
-    def _make_helper_info_sheet(self, workbook: Workbook, no_rows: int) -> None:
-        """This helper Information sheet is used as source of data for drop down menus creation"""
         workbook.create_sheet(title=self._helper_sheet_name)
 
-        for dtype_counter, dtype in enumerate(_DATA_TYPE_BY_DMS_TYPE.values()):
+        for value_type_counter, value_type in enumerate(_DATA_TYPE_BY_DMS_TYPE.values()):
path: cognite/neat/_rules/exporters/_rules2excel.py
user: login: doctrino
body: Nitpick. If the loop does not execute below, then you will get an error on line 343 `value_type_counter += 1` unless you have this line here.
```suggestion
        value_type_counter = 0

        for value_type_counter, value_type in enumerate(_DATA_TYPE_BY_DMS_TYPE.values()):
```
created_at: 2025-04-04T08:23:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,199 +211,238 @@ def _hide_internal_columns(self, workbook: Workbook) -> None:
                 if column_letter:
                     ws.column_dimensions[column_letter].hidden = True
 
-    def _add_info_drop_downs(self, workbook: Workbook, no_rows: int = 100) -> None:
+    def _add_drop_downs(
+        self,
+        role: RoleTypes,
+        workbook: Workbook,
+    ) -> None:
         """Adds drop down menus to specific columns for fast and accurate data entry
-        in the Information rules.
+        for both DMS and Information rules.
 
         Args:
+            role: The role for which the drop downs are created. Can be either "dms" or "information".
             workbook: Workbook representation of the Excel file.
-            no_rows: number of rows to add drop down menus. Defaults to 100*100.
-
-        !!! note "Why no_rows=100?"
-            Maximum number of views per data model is 100, thus this value is set accordingly
 
         !!! note "Why defining individual data validation per desired column?
             This is due to the internal working of openpyxl. Adding same validation to
             different column leads to unexpected behavior when the openpyxl workbook is exported
             as and Excel file. Probably, the validation is not copied to the new column,
             but instead reference to the data validation object is added.
+
+        !!! note "Why 100*100 rows?"
+            This is due to the fact that we need to add drop down menus for all properties
+            in the sheet. The maximum number of properties per view/class is 100. So considering
+            maximum number of views/classes is 100, we need to add 100*100 rows.
         """
-        self._make_helper_info_sheet(workbook, no_rows)
+
+        self._make_helper_sheet(role, workbook, 100)
+
+        data_validators: dict[str, DataValidation] = {}
 
         # We need create individual data validation and cannot re-use the same one due
         # the internals of openpyxl
-        dv_classes = generate_data_validation(self._helper_sheet_name, "A", no_header_rows=0, no_rows=no_rows)
-        dv_value_types = generate_data_validation(self._helper_sheet_name, "B", no_header_rows=0, no_rows=no_rows)
-        dv_implements = generate_data_validation(
-            self._helper_sheet_name,
-            "C",
-            no_header_rows=0,
-            no_rows=no_rows + len(COGNITE_CONCEPTS),
+
+        self._add_data_validation(
+            workbook,
+            sheet_name="Views" if role == RoleTypes.dms else "Classes",
+            column_name="Implements",
+            data_validator_name="implements",
+            data_validators=data_validators,
+            no_validation_values=100,
+            no_rows=100,
         )
 
-        workbook["Properties"].add_data_validation(dv_classes)
-        workbook["Properties"].add_data_validation(dv_value_types)
-        workbook["Classes"].add_data_validation(dv_implements)
+        self._add_data_validation(
+            workbook,
+            sheet_name="Properties",
+            column_name="Value Type",
+            data_validator_name="value_type",
+            data_validators=data_validators,
+            no_validation_values=100,
+            no_rows=100 * 100,  # 100 views/classes * 100 properties (max properties per view/class)
+        )
 
-        # we multiply no_rows with 100 since a view can have max 100 properties per view
-        if column := find_column_with_value(workbook["Properties"], "Class"):
-            dv_classes.add(f"{column}{3}:{column}{no_rows * 100}")
+        self._add_data_validation(
+            workbook,
+            sheet_name="Properties",
+            column_name="View" if role == RoleTypes.dms else "Class",
+            data_validator_name="views_or_classes",
+            data_validators=data_validators,
+            no_validation_values=100 * 100,
+            no_rows=100,
+        )
 
-        if column := find_column_with_value(workbook["Properties"], "Value Type"):
-            dv_value_types.add(f"{column}{3}:{column}{no_rows * 100}")
+        if role == RoleTypes.dms:
+            self._add_data_validation(
+                workbook,
+                sheet_name="Properties",
+                column_name="Container",
+                data_validator_name="container",
+                data_validators=data_validators,
+                no_validation_values=100,
+                no_rows=100 * 100,
+            )
 
-        if column := find_column_with_value(workbook["Classes"], "Implements"):
-            dv_implements.add(f"{column}{3}:{column}{no_rows}")
+            self._add_data_validation(
+                workbook,
+                sheet_name="Properties",
+                column_name="Immutable",
+                data_validator_name="immutable",
+                data_validators=data_validators,
+                no_validation_values=2,
+                no_rows=100 * 100,
+            )
+
+            self._add_data_validation(
+                workbook,
+                sheet_name="Views",
+                column_name="In Model",
+                data_validator_name="in_model",
+                data_validators=data_validators,
+                no_validation_values=2,
+                no_rows=100,  # 100 views
+            )
+
+            self._add_data_validation(
+                workbook,
+                sheet_name="Containers",
+                column_name="Used For",
+                data_validator_name="used_for",
+                data_validators=data_validators,
+                no_validation_values=3,
+                no_rows=100,  # 100 views
+            )
+
+    def _make_helper_sheet(
+        self,
+        role: RoleTypes,
+        workbook: Workbook,
+        no_rows: int,
+    ) -> None:
+        """This helper sheet is used as source of data for drop down menus creation
+
+        Args:
+            role: The role for which the helper sheet is created. Can be either "dms" or "information".
+            workbook: Workbook representation of the Excel file.
+            no_rows: number of rows to add data too that will form base for drop down menus.
+        """
 
-    def _make_helper_info_sheet(self, workbook: Workbook, no_rows: int) -> None:
-        """This helper Information sheet is used as source of data for drop down menus creation"""
         workbook.create_sheet(title=self._helper_sheet_name)
 
-        for dtype_counter, dtype in enumerate(_DATA_TYPE_BY_DMS_TYPE.values()):
+        for value_type_counter, value_type in enumerate(_DATA_TYPE_BY_DMS_TYPE.values()):
+            value_type_as_str = value_type.dms._type.casefold() if role == RoleTypes.dms else value_type.xsd
             # skip types which require special handling or are surpassed by CDM
-            if dtype.xsd in ["enum", "timeseries", "sequence", "file", "json"]:
+            if value_type_as_str in ["enum", "timeseries", "sequence", "file", "json"]:
                 continue
-            workbook[self._helper_sheet_name].cell(row=dtype_counter + 1, column=2, value=dtype.xsd)
-
-        # Add Cognite Core Data Views:
-        for concept_counter, concept in enumerate(COGNITE_CONCEPTS):
             workbook[self._helper_sheet_name].cell(
-                row=concept_counter + 1,
-                column=3,
-                value=f"cdf_cdm:{concept}(version=v1)",
+                row=value_type_counter + 1,
+                column=self._helper_sheet_column_indexes_by_names["Value Type"],
+                value=value_type_as_str,
             )
 
+        value_type_counter += 1
+
+        if self.base_model and (concepts := get_base_concepts(self.base_model, self.no_concepts)):
+            for concept_counter, concept in enumerate(concepts):
+                workbook[self._helper_sheet_name].cell(
+                    row=concept_counter + 1,
+                    column=self._helper_sheet_column_indexes_by_names["Implements"],
+                    value=concept,
+                )
+            concept_counter += 1
+        else:
+            concept_counter = 0
path: cognite/neat/_rules/exporters/_rules2excel.py
user: login: doctrino
body: Same issue here
```suggestion
        concept_counter = 0
        if self.base_model and (concepts := get_base_concepts(self.base_model, self.no_concepts)):
            for concept_counter, concept in enumerate(concepts):
                workbook[self._helper_sheet_name].cell(
                    row=concept_counter + 1,
                    column=self._helper_sheet_column_indexes_by_names["Implements"],
                    value=concept,
                )
            concept_counter += 1
```
created_at: 2025-04-04T08:23:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,10 +148,10 @@ def find_column_with_value(sheet: Worksheet, value: Any) -> str | None:
     return None
 
 
-def generate_data_validation(sheet: str, column: str, no_header_rows: int, no_rows: int) -> DataValidation:
+def generate_data_validation(sheet: str, column: str, no_header_rows: int, no_validation_values: int) -> DataValidation:
path: cognite/neat/_utils/spreadsheet.py
user: login: nikokaoja
body: total number of validation values, made update of docstring and renamed args
created_at: 2025-04-22T10:31:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,6 +200,23 @@ def get_asset_read_only_properties_with_connection() -> "list[DMSProperty]":
     return [DMSProperty.model_validate(item) for item in (_ASSET_ROOT_PROPERTY, _ASSET_PATH_PROPERTY)]
 
 
+def get_base_concepts(
+    base_model: Literal["CogniteCore"] = "CogniteCore",
+    no_concepts: int | None = None,
path: cognite/neat/_constants.py
user: login: nikokaoja
body: renamed to total_concepts
created_at: 2025-04-22T10:32:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -175,17 +175,26 @@ def data_product_model(
             self._state.last_reference = last_rules
         return issues
 
-    def conceptual_model(self, io: Any) -> None:
+    def conceptual_model(
+        self,
+        io: Any,
+        base_model: Literal["CogniteCore"] = "CogniteCore",
+        no_concepts: int | None = None,
path: cognite/neat/_session/_template.py
user: login: nikokaoja
body: renamed to total_concepts
created_at: 2025-04-22T10:33:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -263,6 +263,12 @@ properties:
   neatId: http://purl.org/cognite/neat/data-model/verified/logical/my_space/MyModel/v1/PowerGeneratingUnit/pathLastUpdatedTime
   property_: pathLastUpdatedTime
   value_type: dateTime
+- class_: PowerGeneratingUnit
+  max_count: 1.0
+  min_count: 0
+  neatId: http://purl.org/cognite/neat/data-model/verified/logical/my_space/MyModel/v1/PowerGeneratingUnit/powerGeneratingUnitGUID
+  property_: powerGeneratingUnitGUID
+  value_type: string
path: tests/tests_integration/test_session/test_graph_flow/test_create_extension_template.yml
user: login: nikokaoja
body: This follows our convention to add one dummy property per each user defined concept, similar to what we are doing at the physical data model level
created_at: 2025-04-22T10:35:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,10 +148,10 @@ def find_column_with_value(sheet: Worksheet, value: Any) -> str | None:
     return None
 
 
-def generate_data_validation(sheet: str, column: str, no_header_rows: int, no_rows: int) -> DataValidation:
+def generate_data_validation(sheet: str, column: str, no_header_rows: int, no_validation_values: int) -> DataValidation:
path: cognite/neat/_utils/spreadsheet.py
user: login: nikokaoja
body: renamed to total_validation_values
created_at: 2025-04-22T10:35:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,6 +200,23 @@ def get_asset_read_only_properties_with_connection() -> "list[DMSProperty]":
     return [DMSProperty.model_validate(item) for item in (_ASSET_ROOT_PROPERTY, _ASSET_PATH_PROPERTY)]
 
 
+def get_base_concepts(
+    base_model: Literal["CogniteCore"] = "CogniteCore",
+    no_concepts: int | None = None,
+) -> list[str]:
+    """Gets the base concepts for a given base model represented in the short form.
+    Args:
+        base_model: The base model to get the concepts for.
+        no_concepts: The number of concepts to get. If None, all concepts are returned.
+    """
+    if base_model == "CogniteCore":
+        concepts = [f"cdf_cdm:{concept}(version=v1)" for concept in COGNITE_CONCEPTS]
+    else:
+        concepts = []
path: cognite/neat/_constants.py
user: login: nikokaoja
body: Raising error implemented
created_at: 2025-04-22T10:40:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,199 +211,238 @@ def _hide_internal_columns(self, workbook: Workbook) -> None:
                 if column_letter:
                     ws.column_dimensions[column_letter].hidden = True
 
-    def _add_info_drop_downs(self, workbook: Workbook, no_rows: int = 100) -> None:
+    def _add_drop_downs(
+        self,
+        role: RoleTypes,
+        workbook: Workbook,
+    ) -> None:
         """Adds drop down menus to specific columns for fast and accurate data entry
-        in the Information rules.
+        for both DMS and Information rules.
 
         Args:
+            role: The role for which the drop downs are created. Can be either "dms" or "information".
             workbook: Workbook representation of the Excel file.
-            no_rows: number of rows to add drop down menus. Defaults to 100*100.
-
-        !!! note "Why no_rows=100?"
-            Maximum number of views per data model is 100, thus this value is set accordingly
 
         !!! note "Why defining individual data validation per desired column?
             This is due to the internal working of openpyxl. Adding same validation to
             different column leads to unexpected behavior when the openpyxl workbook is exported
             as and Excel file. Probably, the validation is not copied to the new column,
             but instead reference to the data validation object is added.
+
+        !!! note "Why 100*100 rows?"
+            This is due to the fact that we need to add drop down menus for all properties
+            in the sheet. The maximum number of properties per view/class is 100. So considering
+            maximum number of views/classes is 100, we need to add 100*100 rows.
         """
-        self._make_helper_info_sheet(workbook, no_rows)
+
+        self._make_helper_sheet(role, workbook, 100)
+
+        data_validators: dict[str, DataValidation] = {}
 
         # We need create individual data validation and cannot re-use the same one due
         # the internals of openpyxl
-        dv_classes = generate_data_validation(self._helper_sheet_name, "A", no_header_rows=0, no_rows=no_rows)
-        dv_value_types = generate_data_validation(self._helper_sheet_name, "B", no_header_rows=0, no_rows=no_rows)
-        dv_implements = generate_data_validation(
-            self._helper_sheet_name,
-            "C",
-            no_header_rows=0,
-            no_rows=no_rows + len(COGNITE_CONCEPTS),
+
+        self._add_data_validation(
+            workbook,
+            sheet_name="Views" if role == RoleTypes.dms else "Classes",
+            column_name="Implements",
+            data_validator_name="implements",
+            data_validators=data_validators,
+            no_validation_values=100,
+            no_rows=100,
         )
 
-        workbook["Properties"].add_data_validation(dv_classes)
-        workbook["Properties"].add_data_validation(dv_value_types)
-        workbook["Classes"].add_data_validation(dv_implements)
+        self._add_data_validation(
+            workbook,
+            sheet_name="Properties",
+            column_name="Value Type",
+            data_validator_name="value_type",
+            data_validators=data_validators,
+            no_validation_values=100,
+            no_rows=100 * 100,  # 100 views/classes * 100 properties (max properties per view/class)
+        )
 
-        # we multiply no_rows with 100 since a view can have max 100 properties per view
-        if column := find_column_with_value(workbook["Properties"], "Class"):
-            dv_classes.add(f"{column}{3}:{column}{no_rows * 100}")
+        self._add_data_validation(
+            workbook,
+            sheet_name="Properties",
+            column_name="View" if role == RoleTypes.dms else "Class",
+            data_validator_name="views_or_classes",
+            data_validators=data_validators,
+            no_validation_values=100 * 100,
+            no_rows=100,
+        )
 
-        if column := find_column_with_value(workbook["Properties"], "Value Type"):
-            dv_value_types.add(f"{column}{3}:{column}{no_rows * 100}")
+        if role == RoleTypes.dms:
+            self._add_data_validation(
+                workbook,
+                sheet_name="Properties",
+                column_name="Container",
+                data_validator_name="container",
+                data_validators=data_validators,
+                no_validation_values=100,
+                no_rows=100 * 100,
+            )
 
-        if column := find_column_with_value(workbook["Classes"], "Implements"):
-            dv_implements.add(f"{column}{3}:{column}{no_rows}")
+            self._add_data_validation(
+                workbook,
+                sheet_name="Properties",
+                column_name="Immutable",
+                data_validator_name="immutable",
+                data_validators=data_validators,
+                no_validation_values=2,
+                no_rows=100 * 100,
+            )
+
+            self._add_data_validation(
+                workbook,
+                sheet_name="Views",
+                column_name="In Model",
+                data_validator_name="in_model",
+                data_validators=data_validators,
+                no_validation_values=2,
+                no_rows=100,  # 100 views
+            )
+
+            self._add_data_validation(
+                workbook,
+                sheet_name="Containers",
+                column_name="Used For",
+                data_validator_name="used_for",
+                data_validators=data_validators,
+                no_validation_values=3,
+                no_rows=100,  # 100 views
+            )
+
+    def _make_helper_sheet(
+        self,
+        role: RoleTypes,
+        workbook: Workbook,
+        no_rows: int,
+    ) -> None:
+        """This helper sheet is used as source of data for drop down menus creation
+
+        Args:
+            role: The role for which the helper sheet is created. Can be either "dms" or "information".
+            workbook: Workbook representation of the Excel file.
+            no_rows: number of rows to add data too that will form base for drop down menus.
+        """
 
-    def _make_helper_info_sheet(self, workbook: Workbook, no_rows: int) -> None:
-        """This helper Information sheet is used as source of data for drop down menus creation"""
         workbook.create_sheet(title=self._helper_sheet_name)
 
-        for dtype_counter, dtype in enumerate(_DATA_TYPE_BY_DMS_TYPE.values()):
+        for value_type_counter, value_type in enumerate(_DATA_TYPE_BY_DMS_TYPE.values()):
+            value_type_as_str = value_type.dms._type.casefold() if role == RoleTypes.dms else value_type.xsd
             # skip types which require special handling or are surpassed by CDM
-            if dtype.xsd in ["enum", "timeseries", "sequence", "file", "json"]:
+            if value_type_as_str in ["enum", "timeseries", "sequence", "file", "json"]:
                 continue
-            workbook[self._helper_sheet_name].cell(row=dtype_counter + 1, column=2, value=dtype.xsd)
-
-        # Add Cognite Core Data Views:
-        for concept_counter, concept in enumerate(COGNITE_CONCEPTS):
             workbook[self._helper_sheet_name].cell(
-                row=concept_counter + 1,
-                column=3,
-                value=f"cdf_cdm:{concept}(version=v1)",
+                row=value_type_counter + 1,
+                column=self._helper_sheet_column_indexes_by_names["Value Type"],
+                value=value_type_as_str,
             )
 
+        value_type_counter += 1
+
+        if self.base_model and (concepts := get_base_concepts(self.base_model, self.no_concepts)):
+            for concept_counter, concept in enumerate(concepts):
+                workbook[self._helper_sheet_name].cell(
+                    row=concept_counter + 1,
+                    column=self._helper_sheet_column_indexes_by_names["Implements"],
+                    value=concept,
+                )
+            concept_counter += 1
+        else:
+            concept_counter = 0
path: cognite/neat/_rules/exporters/_rules2excel.py
user: login: nikokaoja
body: implemeted
created_at: 2025-04-22T10:43:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,199 +211,238 @@ def _hide_internal_columns(self, workbook: Workbook) -> None:
                 if column_letter:
                     ws.column_dimensions[column_letter].hidden = True
 
-    def _add_info_drop_downs(self, workbook: Workbook, no_rows: int = 100) -> None:
+    def _add_drop_downs(
+        self,
+        role: RoleTypes,
+        workbook: Workbook,
+    ) -> None:
         """Adds drop down menus to specific columns for fast and accurate data entry
-        in the Information rules.
+        for both DMS and Information rules.
 
         Args:
+            role: The role for which the drop downs are created. Can be either "dms" or "information".
             workbook: Workbook representation of the Excel file.
-            no_rows: number of rows to add drop down menus. Defaults to 100*100.
-
-        !!! note "Why no_rows=100?"
-            Maximum number of views per data model is 100, thus this value is set accordingly
 
         !!! note "Why defining individual data validation per desired column?
             This is due to the internal working of openpyxl. Adding same validation to
             different column leads to unexpected behavior when the openpyxl workbook is exported
             as and Excel file. Probably, the validation is not copied to the new column,
             but instead reference to the data validation object is added.
+
+        !!! note "Why 100*100 rows?"
+            This is due to the fact that we need to add drop down menus for all properties
+            in the sheet. The maximum number of properties per view/class is 100. So considering
+            maximum number of views/classes is 100, we need to add 100*100 rows.
         """
-        self._make_helper_info_sheet(workbook, no_rows)
+
+        self._make_helper_sheet(role, workbook, 100)
+
+        data_validators: dict[str, DataValidation] = {}
 
         # We need create individual data validation and cannot re-use the same one due
         # the internals of openpyxl
-        dv_classes = generate_data_validation(self._helper_sheet_name, "A", no_header_rows=0, no_rows=no_rows)
-        dv_value_types = generate_data_validation(self._helper_sheet_name, "B", no_header_rows=0, no_rows=no_rows)
-        dv_implements = generate_data_validation(
-            self._helper_sheet_name,
-            "C",
-            no_header_rows=0,
-            no_rows=no_rows + len(COGNITE_CONCEPTS),
+
+        self._add_data_validation(
+            workbook,
+            sheet_name="Views" if role == RoleTypes.dms else "Classes",
+            column_name="Implements",
+            data_validator_name="implements",
+            data_validators=data_validators,
+            no_validation_values=100,
+            no_rows=100,
         )
 
-        workbook["Properties"].add_data_validation(dv_classes)
-        workbook["Properties"].add_data_validation(dv_value_types)
-        workbook["Classes"].add_data_validation(dv_implements)
+        self._add_data_validation(
+            workbook,
+            sheet_name="Properties",
+            column_name="Value Type",
+            data_validator_name="value_type",
+            data_validators=data_validators,
+            no_validation_values=100,
+            no_rows=100 * 100,  # 100 views/classes * 100 properties (max properties per view/class)
+        )
 
-        # we multiply no_rows with 100 since a view can have max 100 properties per view
-        if column := find_column_with_value(workbook["Properties"], "Class"):
-            dv_classes.add(f"{column}{3}:{column}{no_rows * 100}")
+        self._add_data_validation(
+            workbook,
+            sheet_name="Properties",
+            column_name="View" if role == RoleTypes.dms else "Class",
+            data_validator_name="views_or_classes",
+            data_validators=data_validators,
+            no_validation_values=100 * 100,
+            no_rows=100,
+        )
 
-        if column := find_column_with_value(workbook["Properties"], "Value Type"):
-            dv_value_types.add(f"{column}{3}:{column}{no_rows * 100}")
+        if role == RoleTypes.dms:
+            self._add_data_validation(
+                workbook,
+                sheet_name="Properties",
+                column_name="Container",
+                data_validator_name="container",
+                data_validators=data_validators,
+                no_validation_values=100,
+                no_rows=100 * 100,
+            )
 
-        if column := find_column_with_value(workbook["Classes"], "Implements"):
-            dv_implements.add(f"{column}{3}:{column}{no_rows}")
+            self._add_data_validation(
+                workbook,
+                sheet_name="Properties",
+                column_name="Immutable",
+                data_validator_name="immutable",
+                data_validators=data_validators,
+                no_validation_values=2,
+                no_rows=100 * 100,
+            )
+
+            self._add_data_validation(
+                workbook,
+                sheet_name="Views",
+                column_name="In Model",
+                data_validator_name="in_model",
+                data_validators=data_validators,
+                no_validation_values=2,
+                no_rows=100,  # 100 views
+            )
+
+            self._add_data_validation(
+                workbook,
+                sheet_name="Containers",
+                column_name="Used For",
+                data_validator_name="used_for",
+                data_validators=data_validators,
+                no_validation_values=3,
+                no_rows=100,  # 100 views
+            )
+
+    def _make_helper_sheet(
+        self,
+        role: RoleTypes,
+        workbook: Workbook,
+        no_rows: int,
+    ) -> None:
+        """This helper sheet is used as source of data for drop down menus creation
+
+        Args:
+            role: The role for which the helper sheet is created. Can be either "dms" or "information".
+            workbook: Workbook representation of the Excel file.
+            no_rows: number of rows to add data too that will form base for drop down menus.
+        """
 
-    def _make_helper_info_sheet(self, workbook: Workbook, no_rows: int) -> None:
-        """This helper Information sheet is used as source of data for drop down menus creation"""
         workbook.create_sheet(title=self._helper_sheet_name)
 
-        for dtype_counter, dtype in enumerate(_DATA_TYPE_BY_DMS_TYPE.values()):
+        for value_type_counter, value_type in enumerate(_DATA_TYPE_BY_DMS_TYPE.values()):
path: cognite/neat/_rules/exporters/_rules2excel.py
user: login: nikokaoja
body: implemented
created_at: 2025-04-22T10:43:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,6 +200,23 @@ def get_asset_read_only_properties_with_connection() -> "list[DMSProperty]":
     return [DMSProperty.model_validate(item) for item in (_ASSET_ROOT_PROPERTY, _ASSET_PATH_PROPERTY)]
 
 
+def get_base_concepts(
+    base_model: Literal["CogniteCore"] = "CogniteCore",
+    no_concepts: int | None = None,
+) -> list[str]:
+    """Gets the base concepts for a given base model represented in the short form.
+    Args:
+        base_model: The base model to get the concepts for.
+        no_concepts: The number of concepts to get. If None, all concepts are returned.
+    """
+    if base_model == "CogniteCore":
+        concepts = [f"cdf_cdm:{concept}(version=v1)" for concept in COGNITE_CONCEPTS]
path: cognite/neat/_constants.py
user: login: nikokaoja
body: I made re-order.
created_at: 2025-04-22T10:45:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,6 +201,22 @@ def get_asset_read_only_properties_with_connection() -> "list[DMSProperty]":
     return [DMSProperty.model_validate(item) for item in (_ASSET_ROOT_PROPERTY, _ASSET_PATH_PROPERTY)]
 
 
+def get_base_concepts(
+    base_model: Literal["CogniteCore"] = "CogniteCore",
+    total_concepts: int | None = None,
+) -> list[str]:
+    """Gets the base concepts for a given base model represented in the short form.
+    Args:
+        base_model: The base model to get the concepts for.
+        total_concepts: The number of concepts to get. If None, all concepts are returned.
+    """
+
+    if base_model == "CogniteCore":
+        return [f"cdf_cdm:{concept}(version=v1)" for concept in COGNITE_CONCEPTS][:total_concepts]
path: cognite/neat/_constants.py
user: login: rsjr
body: `COGNITE_CONCEPT` constant should be sorted somehow, so the list slicing here to be a bit more predictable, wdyt?
created_at: 2025-04-22T15:04:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,10 +148,19 @@ def find_column_with_value(sheet: Worksheet, value: Any) -> str | None:
     return None
 
 
-def generate_data_validation(sheet: str, column: str, no_header_rows: int, no_rows: int) -> DataValidation:
-    "Creates openpyxl data validation object for a cell in a sheet"
+def generate_data_validation(
+    sheet: str, column: str, total_header_rows: int, total_validation_values: int
+) -> DataValidation:
+    """Creates openpyxl data validation object for a cell in a sheet
+
+    Args:
+        sheet: The name of the sheet where the data validation is applied.
+        column: The column letter where the data validation is applied.
+        total_header_rows: The number of header rows in the sheet.
+        total_validation_values: The total number of validation values in the column.
+    """
 
     return DataValidation(
         type="list",
-        formula1=f"={sheet}!{column}${no_header_rows + 1}:{column}${no_rows}",
+        formula1=f"={sheet}!{column}${total_header_rows + 1}:{column}${total_validation_values}",
path: cognite/neat/_utils/spreadsheet.py
user: login: rsjr
body: What is this "total_validation_values" doing? Not sure I got the whole thing, but it's 5PM and I need more coffee =P 
created_at: 2025-04-22T15:07:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,6 +201,22 @@ def get_asset_read_only_properties_with_connection() -> "list[DMSProperty]":
     return [DMSProperty.model_validate(item) for item in (_ASSET_ROOT_PROPERTY, _ASSET_PATH_PROPERTY)]
 
 
+def get_base_concepts(
+    base_model: Literal["CogniteCore"] = "CogniteCore",
path: cognite/neat/_constants.py
user: login: frxstrem
body: Pull type out as a type alias? This is also used other places in this PR.
created_at: 2025-04-30T10:55:13Z
author_association: NONE
diff_hunk: @@ -126,19 +126,26 @@ def data_product_model(
             self._state.last_reference = last_rules
         return issues
 
-    def conceptual_model(self, io: Any) -> None:
+    def conceptual_model(
+        self,
+        io: Any,
+        base_model: Literal["CogniteCore"] = "CogniteCore",
+        total_concepts: int | None = None,
+    ) -> None:
         """This method will create a template for a conceptual data modeling
 
         Args:
             io: file path to the Excel sheet
+            base_model: The base model to use for implements in the conceptual data model.
+                        Currently only supporting CogniteCore.
+            total_concepts: The total number of concepts to provide in implements for selection.
+                         Default is None, meaning all concepts will be provided.
 
         """
         reader = NeatReader.create(io)
         path = reader.materialize_path()
 
-        ExcelExporter().template(RoleTypes.information, path)
-
-        return None
+        return ExcelExporter(base_model=base_model, total_concepts=total_concepts).template(RoleTypes.information, path)
path: cognite/neat/_session/_template.py
user: login: frxstrem
body: Why return if the return type is `None`?
created_at: 2025-04-30T11:13:51Z
author_association: NONE
diff_hunk: @@ -67,6 +69,16 @@ class ExcelExporter(BaseExporter[VerifiedRules, Workbook]):
         "Nodes": "Definition of Nodes",
         "Enum": "Definition of Enum Collections",
     }
+    _helper_sheet_column_indexes_by_names: ClassVar[dict[str, int]] = {
+        "Class": 1,
+        "View": 1,
path: cognite/neat/_rules/exporters/_rules2excel.py
user: login: frxstrem
body: Just to check: these should both be `1`?
created_at: 2025-04-30T11:21:00Z
author_association: NONE
diff_hunk: @@ -67,6 +69,16 @@ class ExcelExporter(BaseExporter[VerifiedRules, Workbook]):
         "Nodes": "Definition of Nodes",
         "Enum": "Definition of Enum Collections",
     }
+    _helper_sheet_column_indexes_by_names: ClassVar[dict[str, int]] = {
+        "Class": 1,
+        "View": 1,
path: cognite/neat/_rules/exporters/_rules2excel.py
user: login: nikokaoja
body: Correct, as Class is representation of View in the conceptual data model, therefore use same column to store drop down value
created_at: 2025-05-05T09:46:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,6 +201,22 @@ def get_asset_read_only_properties_with_connection() -> "list[DMSProperty]":
     return [DMSProperty.model_validate(item) for item in (_ASSET_ROOT_PROPERTY, _ASSET_PATH_PROPERTY)]
 
 
+def get_base_concepts(
+    base_model: Literal["CogniteCore"] = "CogniteCore",
path: cognite/neat/_constants.py
user: login: nikokaoja
body: Good idea! Done !
created_at: 2025-05-05T13:00:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -126,19 +126,26 @@ def data_product_model(
             self._state.last_reference = last_rules
         return issues
 
-    def conceptual_model(self, io: Any) -> None:
+    def conceptual_model(
+        self,
+        io: Any,
+        base_model: Literal["CogniteCore"] = "CogniteCore",
+        total_concepts: int | None = None,
+    ) -> None:
         """This method will create a template for a conceptual data modeling
 
         Args:
             io: file path to the Excel sheet
+            base_model: The base model to use for implements in the conceptual data model.
+                        Currently only supporting CogniteCore.
+            total_concepts: The total number of concepts to provide in implements for selection.
+                         Default is None, meaning all concepts will be provided.
 
         """
         reader = NeatReader.create(io)
         path = reader.materialize_path()
 
-        ExcelExporter().template(RoleTypes.information, path)
-
-        return None
+        return ExcelExporter(base_model=base_model, total_concepts=total_concepts).template(RoleTypes.information, path)
path: cognite/neat/_session/_template.py
user: login: nikokaoja
body: Good point
created_at: 2025-05-05T13:00:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -193,6 +193,8 @@ def get_default_prefixes_and_namespaces() -> dict[str, Namespace]:
     "view_property": "path",
 }
 
+BASE_MODEL = Literal["CogniteCore"]
path: cognite/neat/_constants.py
user: login: frxstrem
body: Nit: I believe Python naming convention states that user-defined types (including aliases) should be like `BaseModel`?
created_at: 2025-05-05T13:23:32Z
author_association: NONE

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-22T11:47:05Z
body: # ☂️ Python Coverage
> current status: ✅
## Overall Coverage
| Lines | Covered | Coverage | Threshold | Status |
| :---: | :-----: | :------: | :-------: | :----: |
| 15268 |  12295  |    81%   |    80%    |   🟢   |
## New Files
No new covered files...
## Modified Files
| File                                          | Coverage | Status |
| :-------------------------------------------- | :------: | :----: |
| cognite/neat/_constants.py                    |    92%   |   🟢   |
| cognite/neat/_rules/exporters/_rules2excel.py |    94%   |   🟢   |
| cognite/neat/_rules/models/_base_rules.py     |    88%   |   🟢   |
| cognite/neat/_session/_template.py            |    87%   |   🟢   |
| cognite/neat/_utils/spreadsheet.py            |    98%   |   🟢   |
| **TOTAL**                                     |  **92%** |   🟢   |


> **updated for commit: `c122a1b` by [action](https://github.com/marketplace/actions/python-coverage)🐍**
author_association: NONE
user: login: nikokaoja
created_at: 2025-05-01T08:58:48Z
body: > I didn't get through the entire PR before EOD, and I'm out until Monday, so if you want to get it reviewed by someone else you can remove the `risk-review-ongoing` label to put it back in the queue (if so then I think it'll keep its position in the queue and not be put in the back).
> 
> I left my risk review comments for the parts that I did get through below:

@frxstrem np, can wait, will not update code base until you finish up review

author_association: CONTRIBUTOR
user: login: frxstrem
created_at: 2025-05-05T12:29:29Z
body: I reviewed the rest of the PR (and got some help with the context, it's a bit hard to understand the codebase as an outside with no familiarity with the service), the rest seems good. If you could address the comments I already made, that would be great.
author_association: NONE
user: login: frxstrem
created_at: 2025-05-05T13:25:42Z
body: 🦄 with non-blocking comment
author_association: NONE