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

ECMFolderModel

Folder object types

ECMRegisterModel

Register (sub-folder) object types

ECMDocumentModel

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

Fields can be declared as mandatory. insert() and update() validate them client-side by default:

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"

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 "always" is never emitted as mandatory by the model generator.

"init"

Writable only at creation (while the object has no ID). Once the object has been persisted (id set), changing it is rejected.

"arch"

Writable until the document is archived (non-empty OBJECT_AVDATE). Never locks folder or register models.

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

row_id

Internal row identifier assigned by the server.

is_modified

True if any field in this row has been changed since loading.

is_field_modified(field_name)

Returns True if the named field has been changed.

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 an ECMField usable in where() and order_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

system.id

int | None

Numeric ID of the object on the server. None for instances that have not yet been written to the server (e.g. before insert()).

system.name

str | None

Display name of the object as stored on the server. Usually the value of the key field.

system.is_modified

bool

True if any index field has been changed since the instance was loaded. Used internally by update() to skip unnecessary server calls.

system.has_removed_table_rows

bool

True if at least one table row has been removed from a ECMTableField since loading. Triggers REPLACETABLEFIELDS=1 in update().

system.modified_fields

set[str]

Set of internal field names that have been modified since loading.

system.modified_table_fields

set[str]

Set of table field names that have been modified (row added, removed, or changed) since loading.

system.is_field_modified(field_name)

bool

Returns True if the named field has been changed since loading.

4.3. Available per object type

Some system properties are only present on specific model types:

Property Type Available on Description

system.folder_id

int | None

ECMRegisterModel, ECMDocumentModel

ID of the parent folder. Only populated when use_result_list=True is passed to get(), or when returned by select().

system.parent_register_id

int | None

ECMRegisterModel

ID of the directly enclosing register, if the register is nested inside another register.

system.register_id

int | None

ECMDocumentModel

ID of the parent register. Only populated when use_result_list=True is passed to get(), or when returned by select().

system.register_type_id

int | None

ECMDocumentModel

Type ID of the parent register.

system.system_id

int | None

ECMDocumentModel

ID of the external archive system (OBJECT_SYSTEMID) that holds the referenced file. Together with system.foreign_id this forms the cross-archive reference: system_id identifies the foreign archive, foreign_id the document within it. 0/None means no external archive reference. Special case "green arrow" (named after its icon in the enaio client): system_id 0/None together with a set foreign_id means the file is sourced from another enaio document whose object ID is given by foreign_id. Not to be confused with system.id (this object’s own enaio object ID).

system.foreign_id

str | None

ECMDocumentModel

Reference to the document in the external archive system (OBJECT_FOREIGNID); to be interpreted in the context of system.system_id: with system_id > 0 it is the document ID inside the foreign archive identified by system_id; with system_id 0/None and a set foreign_id it contains the enaio object ID of another document whose file content is re-used (green arrow).

system.lock_user_id

int | None

ECMDocumentModel

Numeric ID of the user who currently holds a lock on this document (OBJECT_LOCKUSER). None if the document is not locked or the field was not requested. For the lock state (SELF/OTHERS/…) see system.base_params.locked.

system.locked

bool

ECMDocumentModel

True if the document is currently checked out / locked (OBJECT_LOCKUSER ≠ 0). Reports only whether a lock exists, not by whom. For the SELF/OTHERS distinction request system.base_params.locked via .base_params().

system.flags

int | None

ECMDocumentModel

Raw archive-status code (OBJECT_FLAGS). Prefer the decoded boolean properties below. Values per the enaio Client Programmierreferenz: 0 = archived, 1 = archivable, 2 = not archivable, 4 = page error, 8 = no pages, 16 = archived (but not the preview/dia), 32 = not archived and invisible to the archivist, 64 and system_id=0 = reference document, 64 and system_id!=0 = archived on an external system.

system.archived

bool

ECMDocumentModel

True if the document is archived in the own enaio archive (OBJECT_FLAGS 0 or 16). Documents archived on an external system are reported by system.external_archived instead.

system.archivable

bool

ECMDocumentModel

True if the document is released for archiving (OBJECT_FLAGS 1).

system.not_archivable

bool

ECMDocumentModel

True if the document is not released for archiving (OBJECT_FLAGS 2).

system.page_error

bool

ECMDocumentModel

True on a page error (OBJECT_FLAGS 4).

system.without_pages

bool

ECMDocumentModel

True if the document has no pages / files (OBJECT_FLAGS 8).

system.reference

bool

ECMDocumentModel

True if the document is a reference document (green arrow) (OBJECT_FLAGS 64 without system_id).

system.external_archived

bool

ECMDocumentModel

True if the document is archived on an external system (OBJECT_FLAGS 64 with a system_id).

The archive and lock properties (flags, archived, archivable, not_archivable, page_error, without_pages, reference, external_archived, locked) are available on every loaded document instance — OBJECT_FLAGS and OBJECT_LOCKUSER are part of the default system fields and are loaded automatically by get() and select():

doc = ecm.dms.get(Invoice, 11968)
if doc.system.archivable:
    print("Released for archiving")
elif doc.system.archived:
    print("Already archived")
elif doc.system.without_pages:
    print("Document without pages")
if doc.system.locked:
    print("Document is checked out")

The search-flag properties system.multi_location, system.has_variants, system.signed_current and system.signed_former, in contrast, work only in queries (where(…​)); there is currently no instance property for them.

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

Populated when rights=True (get()) or .rights() (select()) is passed. Type: ECMModelRights.

Attribute Type Description

insert

bool

May create child objects (registers or documents in a folder; documents in a register).

edit_metadata

bool

May modify the index fields of the object.

read_file

bool

May read the file of the document.

edit_file

bool

May modify the file of the document.

delete

bool

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

creator

str | None

Username of the user who created the object.

creation_date

datetime | None

Timestamp of object creation.

owner

str | None

Current owner of the object.

modifier

str | None

Username of the user who last modified the object.

modified_date

datetime | None

Timestamp of the last modification.

links_count

int | None

Number of links to this object.

text_notice_count

int | None

Number of text notices (remarks) attached to the object.

locked

str | None

Lock state of the object: "UNLOCKED", "SELF", "OTHERS", or "EXTERNAL".

locked_user_id

int | None

Numeric ID of the user holding the lock, or None if the object is not locked.

pdf_annotation_count

int | None

Number of PDF annotations attached to the object.

version

int | None

Version number of the object.

archive_state

str | None

Display text of the archive state, or None if not set.

archive_state_value

int | None

Numeric value of the archive state (from the value attribute), or -1 if not set.

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

count

int | None

Number of files (primary and secondary files).

size

int | None

Total file size in bytes.

extension

str | None

File extension (e.g. pdf, docx).

mimetype

str | None

MIME type (e.g. application/pdf).

mimetypegroup

str | None

MIME group (e.g. application).

iconid

int | None

ID of the file type icon.

documentpagecount

int | None

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

doc_id

int

Document ID of this variant.

doc_ver

str

Version label (e.g. "1.0", "1.1").

is_active

bool

True for the currently active variant.

doc_parent

int | None

ID of the parent variant, or None for the root.

children

list[int]

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 "")

4.4.5. system.remarks

Populated when remarks=True (get()) or .remarks() (select()) is passed. Type: list[str]. Contains the text notices attached to the object.

for folder in ecm.dms.select(InvoiceFolder).remarks().stream():
    for remark in folder.system.remarks:
        print(remark)

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