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(),order_by()and (forselect_lol())fields(), exactly like an index field. Sort and result field entries automatically receive thesystem="1"attribute.
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. |
|
|
|
|
|
|
|
|
Names of the table fields the caller emptied on purpose — assigned, passed, or mutated ( |
|
|
Internal field names that have been modified since loading, mapped to their current value. |
|
|
Table field names whose rows have been added, removed, or changed since loading, mapped to their current row list. |
|
|
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 ( |
|
|
|
ID of the directly enclosing register ( |
|
|
|
ID of the parent register ( |
|
|
|
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. |
Each attribute is True exactly when the server grants the right; a right that is not granted is False. The insert quotas are requested along with the rights so that insert carries a real value.
When the server returns a <Rights> element it never evaluated (all attributes -1, which happens when the query did not request rights), system.rights stays unpopulated and accessing it raises a ValueError as usual.
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 |
|
|
The child variants branched from this one. |
|
|
Nesting depth in the variant tree: |
walk() returns a node and, depth first, every node below it — handy for searching the tree without recursing manually:
for root in doc.system.variants:
for variant in root.walk():
print(" " * variant.level, variant.doc_ver)
Without a typed load, variants() returns the same nodes from an object ID alone.
4.4.5. system.active_variant
The active variant from the loaded variant tree, searched at any level. Derived from system.variants, so no further server call is involved — in a query with .variants() the access costs nothing per hit. Type: ECMModelDocumentVariant | None; None when the document has no variants.
for doc in ecm.dms.select(InvoiceDocument).variants().limit(100).stream():
active = doc.system.active_variant
print(doc.system.id, active.doc_ver if active else "no variants")
When variants was not requested, the property raises ValueError just like system.variants — the model cannot reload it, as it holds no connection. For a document that is not loaded, use active_variant() on the ecm.dms namespace.
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": "Updated 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.
A table field’s row list is an ECMTableList that also records that the caller touched it. Assigning a list, passing one to the constructor, and every mutating call (append, clear, del, …) mark it; lists built from a server response or from the class defaults stay unmarked. A marked list that ends up empty is a deliberate clear, which is how upsert() tells Positions=[] from a table field nobody named:
folder = InvoiceFolder(Title="Invoice 2024")
print(folder.Positions.touched) # False — the implicit default
print(folder.system.explicitly_emptied_table_fields) # []
folder.Positions = [] # naming it is intent
print(folder.system.explicitly_emptied_table_fields) # ["Positions"]
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