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

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.

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. 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.2. 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.

4.3. 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.3.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.3.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.3.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.3.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.3.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