ECM Model
The ECM model is the central concept of ecmind-blue-client. It provides a typed, ORM-style interface for ECM objects — folders, registers, and documents.
Every object returned by ecm.dms.select(), ecm.dms.get(), ecm.dms.insert_and_get(), or ecm.dms.update_and_get() is an instance of a model class. Model instances carry both index field values (your custom fields) and system metadata (always via obj.system).
1. Model classes
Define a model class by subclassing one of the three base classes and declaring typed fields as class annotations:
from ecmind_blue_client.ecm.model import ECMFolderModel, ECMRegisterModel, ECMDocumentModel, ECMField, ECMTableField, ECMTableRowModel
class InvoiceRow(ECMTableRowModel):
Amount: ECMField[float]
Description: ECMField[str]
class InvoiceFolder(ECMFolderModel):
_internal_name_ = "InvoiceFolder" # server-side internal name of the object type
Title: ECMField[str]
Year: ECMField[int]
Positions: ECMTableField[InvoiceRow]
class InvoiceRegister(ECMRegisterModel):
_internal_name_ = "InvoiceRegister"
Name: ECMField[str]
class InvoiceDocument(ECMDocumentModel):
_internal_name_ = "InvoiceDocument"
Subject: ECMField[str]
Amount: ECMField[float]
| Base class | Use for |
|---|---|
|
Folder object types |
|
Register (sub-folder) object types |
|
Document object types |
1.1. Dynamic models
When the object type is only known at runtime, use the factory functions instead of a class statement:
from ecmind_blue_client.ecm.model import make_folder_model, make_register_model, make_document_model
InvoiceFolder = make_folder_model("InvoiceFolder")
Dynamic models support the same query API. Undeclared fields are accessed via obj["internalName"].
2. ECMField
ECMField[T] is the descriptor for typed index fields. The type parameter T determines the Python type of the field value (str, int, float, datetime, date, time, bool, or a generated catalog enum).
Fields backed by a list catalog are typed with a generated (str, Enum) class (see the model generator). Assigning a member writes its underlying catalog value — the enum is unwrapped to its .value on save, so folder.Status = InvoiceFolder_StatusEnum.PAID is sent as Paid.
At class level, ECMField acts as a condition builder:
InvoiceFolder.Year == 2024 # ECMCondition
InvoiceFolder.Year >= 2020 # ECMCondition
InvoiceFolder.Title.in_("A", "B") # ECMCondition
InvoiceFolder.Year.DESC # ECMSortOrder for order_by()
At instance level, it returns the stored value:
folder = ecm.dms.get(InvoiceFolder, 12345)
print(folder.Year) # int | None
2.1. Mandatory fields
class InvoiceFolder(ECMFolderModel):
_internal_name_ = "InvoiceFolder"
Title: ECMField[str] = ECMField(mandatory=True)
Year: ECMField[int]
2.2. Read-only fields
Fields can be write-protected. The read_only parameter of ECMField is enforced client-side on save (insert(), update(), upsert()) by comparing against the value loaded from the server:
| Value | Meaning |
|---|---|
|
Always read-only — the server owns the value. Setting it on a new object or changing it on an existing one fails. A field marked |
|
Writable only at creation (while the object has no ID). Once the object has been persisted ( |
|
Writable until the document is archived (non-empty |
class Invoice(ECMDocumentModel):
_internal_name_ = "Invoice"
InvoiceNumber: ECMField[str] = ECMField(str, mandatory=True, read_only="init")
SystemId: ECMField[str] = ECMField(str, default=None, read_only="always")
ECMField(read_only=…) is derived automatically by the model generator from the definition flags (readonly / readonly_after_initialization / readonly_after_archiving); precedence always > init > arch.
3. ECMTableField
ECMTableField[RowT] holds multi-row table fields. The row class must subclass ECMTableRowModel and declare its columns as ECMField annotations.
for row in folder.Positions:
print(row.Amount, row.Description)
Each row exposes:
| Attribute | Description |
|---|---|
|
Internal row identifier assigned by the server. |
|
|
|
Returns |
3.1. Querying table columns
A condition on a table-field column is built by accessing the column on the table field at class
level. select(…).where(Invoice.Positions.ArticleNo == "A-100") emits a <TableCondition> /
<TableColumn> so the server filters on the sub-table column instead of rejecting an unknown object
field. Restrict it to a server row number by subscripting the table field
(Invoice.Positions[3].Quantity == 5; explicit alias .row(3)). All comparison and collection
operators apply. See select() for full examples and the emitted XML.
4. system properties
Every model instance exposes a system attribute that holds all server-populated metadata. Index fields (your ECMField declarations) and system properties are kept strictly separate.
4.1. system fields in queries (class access)
The system attribute behaves differently depending on the access level:
-
Instance access (
obj.system.id) returns the loaded value (e.g.int). -
Class access (
Model.system.id) returns anECMFieldusable inwhere()andorder_by(), exactly like an index field.
This lets you use the same dotted path inside a query. Typical example — all documents inside a known folder via a cross-type condition on the folder ID:
docs = (
ecm.dms.select(PostDoc)
.where(Post.system.id == folder.system.id) # left: class (ECMField), right: instance value (int)
.limit(1)
.execute()
)
The produced condition is bound to the object type of the model it is called on (Post) and is placed into its own ConditionObject by the HOL builder — identical to Post["OBJECT_ID"], including the automatic system="1" attribute. Only system fields backed by a single queryable column are offered as an ECMField; aggregate values without a backing field (system.rights, system.name, system.is_modified, …) remain instance-only.
Queryable fields are: the Always available fields with single-column backing (id, owner_guid, creator, creation_date, last_modifier, last_modified, creation_time, deleted_at), the type-dependent fields (folder_id, parent_register_id, register_id, register_type_id, system_id, foreign_id, lock_user_id, archivist, archiving_date), and the queryable subset of system.base_params (creator, creation_date, modifier, modified_date, links_count, locked_user_id) and system.file_properties (count, size). Fields without a queryable server column (e.g. system.base_params.version, system.file_properties.extension) stay instance-only.
4.1.1. Generic item access for arbitrary system fields
The .system namespace only offers the curated, typed subset. For any server system field — including ones without a .system property (OBJECT_FLAGS, OBJECT_RETENTION, OBJECT_MAIN, …) — and for dynamic models without declared fields, generic item access on the model class is available:
ecm.dms.select(PostDoc).where(Post["OBJECT_ID"] == folder.system.id) # same as Post.system.id
ecm.dms.select(PostDoc).where(Post["OBJECT_FLAGS"] == 0) # system field without a .system property
Model["NAME"] returns an ECMField bound to the object type; if NAME is a member of the SystemFields enum (e.g. OBJECT_ID), system="1" is set automatically at build time. Item access is untyped (ECMField[str], no IDE completion); Model.system.<field> is typed and discoverable but limited to the curated subset. Both produce the same condition. The same item access also works for undeclared index fields (Model["MyField"]).
4.2. Always available
The following properties are always populated, regardless of which flags were passed to the query or get call:
| Property | Type | Description |
|---|---|---|
|
|
Numeric ID of the object on the server. |
|
|
Display name of the object as stored on the server. Usually the value of the key field. |
|
|
|
|
|
|
|
|
Set of internal field names that have been modified since loading. |
|
|
Set of table field names that have been modified (row added, removed, or changed) since loading. |
|
|
Returns |
4.3. Available per object type
Some system properties are only present on specific model types:
| Property | Type | Available on | Description |
|---|---|---|---|
|
|
|
ID of the parent folder. Only populated when |
|
|
|
ID of the directly enclosing register, if the register is nested inside another register. |
|
|
|
ID of the parent register. Only populated when |
|
|
|
Type ID of the parent register. |
|
|
|
ID of the external archive system ( |
|
|
|
Reference to the document in the external archive system ( |
|
|
|
Numeric ID of the user who currently holds a lock on this document ( |
|
|
|
|
|
|
|
Raw archive-status code ( |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The archive and lock properties (
The search-flag properties |
4.4. Optional — loaded on request
The following properties are None by default and must be explicitly requested via flags on select(), get(), insert_and_get(), or update_and_get().
4.4.1. system.rights
| Attribute | Type | Description |
|---|---|---|
|
|
May create child objects (registers or documents in a folder; documents in a register). |
|
|
May modify the index fields of the object. |
|
|
May read the file of the document. |
|
|
May modify the file of the document. |
|
|
May delete the object. |
folder = ecm.dms.get(InvoiceFolder, 12345, rights=True)
if folder.system.rights.edit_metadata:
print("editing allowed")
4.4.2. system.base_params
Populated when base_params=True (get()) or .base_params() (select()) is passed. Type: ECMModelBaseParams.
| Attribute | Type | Description |
|---|---|---|
|
|
Username of the user who created the object. |
|
|
Timestamp of object creation. |
|
|
Current owner of the object. |
|
|
Username of the user who last modified the object. |
|
|
Timestamp of the last modification. |
|
|
Number of links to this object. |
|
|
Number of text notices (remarks) attached to the object. |
|
|
Lock state of the object: |
|
|
Numeric ID of the user holding the lock, or |
|
|
Number of PDF annotations attached to the object. |
|
|
Version number of the object. |
|
|
Display text of the archive state, or |
|
|
Numeric value of the archive state (from the |
folder = ecm.dms.get(InvoiceFolder, 12345, base_params=True)
print(f"Created by {folder.system.base_params.creator} on {folder.system.base_params.creation_date}")
4.4.3. system.file_properties
Populated when file_properties=True (get()) or .file_properties() (select()) is passed. Only meaningful for ECMDocumentModel. Type: ECMModelFileProperties.
| Attribute | Type | Description |
|---|---|---|
|
|
Number of files (primary and secondary files). |
|
|
Total file size in bytes. |
|
|
File extension (e.g. |
|
|
MIME type (e.g. |
|
|
MIME group (e.g. |
|
|
ID of the file type icon. |
|
|
Number of pages, if known. |
doc = ecm.dms.get(InvoiceDocument, 42, file_properties=True)
fp = doc.system.file_properties
print(f"{fp.extension}, {fp.size} bytes, {fp.documentpagecount} pages")
4.4.4. system.variants
Populated when variants=True (get()) or .variants() (select()) is passed. Only meaningful for ECMDocumentModel with the W-module enabled. Type: list[ECMModelDocumentVariant].
Each entry in the list has:
| Attribute | Type | Description |
|---|---|---|
|
|
Document ID of this variant. |
|
|
Version label (e.g. |
|
|
|
|
|
ID of the parent variant, or |
|
|
IDs of child variants branched from this one. |
doc = ecm.dms.get(InvoiceDocument, 42, variants=True)
for v in doc.system.variants:
print(v.doc_ver, "✓" if v.is_active else "")
5. Change tracking
Model instances track field changes automatically. Assigning a new value to an ECMField marks that field as modified:
folder = ecm.dms.get(InvoiceFolder, 12345)
folder.Title = "Updated Title"
print(folder.system.is_modified) # True
print(folder.system.modified_fields) # {"Title"}
print(folder.system.is_field_modified("Year")) # False
update() reads system.is_modified to decide whether to skip the server call. When no fields have changed and no files are provided, the call is omitted silently (unless force=True).
Removing a row from a ECMTableField sets system.has_removed_table_rows = True, which causes update() to add REPLACETABLEFIELDS=1 to the request.
6. See also
-
Workflow Model — the typed model for workflow input variables
-
select() — query builder with
.rights(),.base_params(),.file_properties(),.variants(),.remarks() -
get() — load a single object by ID with optional flag parameters
-
insert() / insert_and_get() — create new objects
-
update() / update_and_get() — write changes back to the server