ECM-Modell

Das ECM-Modell ist das zentrale Konzept von ecmind-blue-client. Es bietet eine typisierte, ORM-artige Schnittstelle für ECM-Objekte — Ordner, Register und Dokumente.

Jedes Objekt, das von ecm.dms.select(), ecm.dms.get(), ecm.dms.insert_and_get() oder ecm.dms.update_and_get() zurückgegeben wird, ist eine Instanz einer Modellklasse. Modellinstanzen enthalten sowohl Indexfelddaten (benutzerdefinierte Felder) als auch Systemmetadaten (immer über obj.system).

1. Modellklassen

Eine Modellklasse wird durch Ableitung von einer der drei Basisklassen definiert. Die Felder werden als typisierte Klassenannotationen deklariert:

from ecmind_blue_client.ecm.model import ECMFolderModel, ECMRegisterModel, ECMDocumentModel, ECMField, ECMTableField, ECMTableRowModel

class RechnungsZeile(ECMTableRowModel):
    Betrag: ECMField[float]
    Beschreibung: ECMField[str]

class RechnungsOrdner(ECMFolderModel):
    _internal_name_ = "InvoiceFolder"   # interner Name des Objekttyps auf dem Server
    Titel: ECMField[str]
    Jahr: ECMField[int]
    Positionen: ECMTableField[RechnungsZeile]

class RechnungsRegister(ECMRegisterModel):
    _internal_name_ = "InvoiceRegister"
    Name: ECMField[str]

class RechnungsDokument(ECMDocumentModel):
    _internal_name_ = "InvoiceDocument"
    Betreff: ECMField[str]
    Betrag: ECMField[float]
Basisklasse Verwendung

ECMFolderModel

Ordner-Objekttypen

ECMRegisterModel

Register-Objekttypen (Unterordner)

ECMDocumentModel

Dokument-Objekttypen

1.1. Dynamische Modelle

Wenn der Objekttyp erst zur Laufzeit bekannt ist, können statt einer Klassendefinition Factory-Funktionen verwendet werden:

from ecmind_blue_client.ecm.model import make_folder_model, make_register_model, make_document_model

RechnungsOrdner = make_folder_model("InvoiceFolder")

Dynamische Modelle unterstützen dieselbe Query-API. Nicht deklarierte Felder werden über obj["internalName"] abgerufen.

2. ECMField

ECMField[T] ist der Descriptor für typisierte Indexfelder. Der Typparameter T legt den Python-Typ des Feldwertes fest (str, int, float, datetime, date, time, bool).

Auf Klassenebene wirkt ECMField als Condition-Builder:

RechnungsOrdner.Jahr == 2024           # ECMCondition
RechnungsOrdner.Jahr >= 2020           # ECMCondition
RechnungsOrdner.Titel.in_("A", "B")   # ECMCondition
RechnungsOrdner.Jahr.DESC              # ECMSortOrder für order_by()

Auf Instanzebene gibt er den gespeicherten Wert zurück:

ordner = ecm.dms.get(RechnungsOrdner, 12345)
print(ordner.Jahr)   # int | None

2.1. Pflichtfelder

Felder können als Pflichtfeld deklariert werden. insert() und update() prüfen diese standardmäßig clientseitig:

class RechnungsOrdner(ECMFolderModel):
    _internal_name_ = "InvoiceFolder"
    Titel: ECMField[str] = ECMField(mandatory=True)
    Jahr: ECMField[int]

2.2. Read-only-Felder

Felder können schreibgeschützt sein. Der Parameter read_only von ECMField wird beim Speichern (insert(), update(), upsert()) clientseitig durchgesetzt — verglichen wird gegen den vom Server geladenen Wert:

Wert Bedeutung

"always"

Immer schreibgeschützt — der Server besitzt den Wert. Wird das Feld bei einer Neuanlage gesetzt oder bei einem bestehenden Objekt geändert, schlägt das Speichern fehl. Ein als "always" markiertes Feld wird vom Modellgenerator nie als mandatory ausgegeben.

"init"

Nur bei Neuanlage beschreibbar (solange das Objekt keine ID hat). Sobald das Objekt gespeichert wurde (id gesetzt), ist eine Änderung unzulässig.

"arch"

Beschreibbar, bis das Dokument archiviert ist (nicht leeres OBJECT_AVDATE). Sperrt nie Ordner- oder Registermodelle.

class Rechnung(ECMDocumentModel):
    _internal_name_ = "Invoice"
    Belegnummer: ECMField[str] = ECMField(str, mandatory=True, read_only="init")
    SystemId: ECMField[str] = ECMField(str, default=None, read_only="always")

ECMField(read_only=…​) wird vom Modellgenerator automatisch aus den Definitions-Flags (readonly / readonly_after_initialization / readonly_after_archiving) abgeleitet; Vorrang always > init > arch.

3. ECMTableField

ECMTableField[RowT] enthält mehrreihige Tabellenfelder. Die Zeilenklasse muss ECMTableRowModel ableiten und ihre Spalten als ECMField-Annotationen deklarieren.

for zeile in ordner.Positionen:
    print(zeile.Betrag, zeile.Beschreibung)

Jede Zeile stellt folgende Attribute bereit:

Attribut Beschreibung

row_id

Interne Zeilenkennung, die vom Server vergeben wird.

is_modified

True, wenn seit dem Laden ein Feld dieser Zeile geändert wurde.

is_field_modified(field_name)

Gibt True zurück, wenn das genannte Feld geändert wurde.

4. system-Eigenschaften

Jede Modellinstanz besitzt ein system-Attribut, das alle serverseitig befüllten Metadaten enthält. Indexfelder (eigene ECMField-Deklarationen) und Systemeigenschaften sind strikt getrennt.

4.1. Immer verfügbar

Die folgenden Eigenschaften sind immer gefüllt, unabhängig von den Flags des Abfrage- oder Abrufaufrufs:

Eigenschaft Typ Beschreibung

system.id

int | None

Numerische ID des Objekts auf dem Server. None für Instanzen, die noch nicht gespeichert wurden (z. B. vor insert()).

system.name

str | None

Anzeigename des Objekts, wie er auf dem Server gespeichert ist. Entspricht in der Regel dem Wert des Schlüsselfeldes.

system.is_modified

bool

True, wenn seit dem Laden ein Indexfeld geändert wurde. Wird intern von update() verwendet, um unnötige Serveraufrufe zu überspringen.

system.has_removed_table_rows

bool

True, wenn seit dem Laden mindestens eine Tabellenzeile aus einem ECMTableField entfernt wurde. Löst REPLACETABLEFIELDS=1 in update() aus.

system.modified_fields

set[str]

Menge der internen Feldnamen, die seit dem Laden geändert wurden.

system.modified_table_fields

set[str]

Menge der Tabellenfeldnamen, bei denen Zeilen hinzugefügt, entfernt oder geändert wurden.

system.is_field_modified(field_name)

bool

Gibt True zurück, wenn das genannte Feld seit dem Laden geändert wurde.

4.2. Je nach Objekttyp verfügbar

Einige Systemeigenschaften sind nur bei bestimmten Modelltypen vorhanden:

Eigenschaft Typ Verfügbar bei Beschreibung

system.folder_id

int | None

ECMRegisterModel, ECMDocumentModel

ID des übergeordneten Ordners. Wird nur befüllt, wenn use_result_list=True an get() übergeben wird oder das Objekt über select() zurückgegeben wird.

system.parent_register_id

int | None

ECMRegisterModel

ID des direkt übergeordneten Registers, wenn das Register in einem anderen Register verschachtelt ist.

system.register_id

int | None

ECMDocumentModel

ID des übergeordneten Registers. Wird nur befüllt, wenn use_result_list=True an get() übergeben wird oder das Objekt über select() zurückgegeben wird.

system.register_type_id

int | None

ECMDocumentModel

Typ-ID des übergeordneten Registers.

system.system_id

int | None

ECMDocumentModel

ID des externen Archivsystems (OBJECT_SYSTEMID), in dem die referenzierte Datei liegt. Bildet zusammen mit system.foreign_id die archivsystemübergreifende Referenz: system_id zeigt auf das Fremdarchiv, foreign_id auf das Dokument darin. 0/None bedeutet keine externe Archivreferenz. Sonderfall „grüner Pfeil" (Bezeichnung nach dem Symbol im enaio-Client): system_id 0/None und foreign_id gesetzt → Datei wird aus einem anderen enaio-Dokument referenziert, dessen Objekt-ID in foreign_id steht. Nicht zu verwechseln mit system.id (eigene enaio-Objekt-ID).

system.foreign_id

str | None

ECMDocumentModel

Referenz auf das Dokument im externen Archivsystem (OBJECT_FOREIGNID); im Kontext von system.system_id zu interpretieren: bei system_id > 0 ist es die Dokument-ID im durch system_id benannten Fremdarchiv; bei system_id 0/None und gesetztem foreign_id enthält es die enaio-Objekt-ID eines anderen Dokuments, dessen Datei wiederverwendet wird (grüner Pfeil).

system.lock_user_id

int | None

ECMDocumentModel

Numerische ID des Benutzers, der das Dokument aktuell gesperrt hält (OBJECT_LOCKUSER). None wenn das Dokument nicht gesperrt ist oder das Feld nicht angefordert wurde. Für den Sperrzustand (SELF/OTHERS/…) siehe system.base_params.locked.

4.3. Optional — auf Anfrage geladen

Die folgenden Eigenschaften sind standardmäßig None und müssen über Flags bei select(), get(), insert_and_get() oder update_and_get() explizit angefordert werden.

4.3.1. system.rights

Befüllt bei rights=True (get()) oder .rights() (select()). Typ: ECMModelRights.

Attribut Typ Beschreibung

insert

bool

Darf untergeordnete Objekte anlegen (Register oder Dokumente in einem Ordner; Dokumente in einem Register).

edit_metadata

bool

Darf die Indexfelder des Objekts ändern.

read_file

bool

Darf die Datei des Dokuments lesen.

edit_file

bool

Darf die Datei des Dokuments ändern.

delete

bool

Darf das Objekt löschen.

ordner = ecm.dms.get(RechnungsOrdner, 12345, rights=True)
if ordner.system.rights.edit_metadata:
    print("Bearbeitung erlaubt")

4.3.2. system.base_params

Befüllt bei base_params=True (get()) oder .base_params() (select()). Typ: ECMModelBaseParams.

Attribut Typ Beschreibung

creator

str | None

Benutzername des Erstellers.

creation_date

datetime | None

Erstellungszeitpunkt.

owner

str | None

Aktueller Eigentümer des Objekts.

modifier

str | None

Benutzername des letzten Bearbeiters.

modified_date

datetime | None

Zeitstempel der letzten Änderung.

links_count

int | None

Anzahl der Verknüpfungen auf dieses Objekt.

text_notice_count

int | None

Anzahl der Textnotizen (Bemerkungen) am Objekt.

locked

str | None

Sperrzustand des Objekts: "UNLOCKED", "SELF", "OTHERS" oder "EXTERNAL".

locked_user_id

int | None

Numerische ID des sperrenden Benutzers, oder None wenn das Objekt nicht gesperrt ist.

pdf_annotation_count

int | None

Anzahl der PDF-Annotationen am Objekt.

version

int | None

Versionsnummer des Objekts.

archive_state

str | None

Anzeigetext des Archivierungszustands, oder None wenn nicht gesetzt.

archive_state_value

int | None

Numerischer Wert des Archivierungszustands (aus dem value-Attribut), oder -1 wenn nicht gesetzt.

ordner = ecm.dms.get(RechnungsOrdner, 12345, base_params=True)
print(f"Erstellt von {ordner.system.base_params.creator} am {ordner.system.base_params.creation_date}")

4.3.3. system.file_properties

Befüllt bei file_properties=True (get()) oder .file_properties() (select()). Nur für ECMDocumentModel relevant. Typ: ECMModelFileProperties.

Attribut Typ Beschreibung

count

int | None

Anzahl der Dateien (Primär- und Sekundärdateien).

size

int | None

Gesamtdateigröße in Bytes.

extension

str | None

Dateiendung (z. B. pdf, docx).

mimetype

str | None

MIME-Typ (z. B. application/pdf).

mimetypegroup

str | None

MIME-Gruppe (z. B. application).

iconid

int | None

ID des Dateitypicons.

documentpagecount

int | None

Seitenanzahl, sofern bekannt.

dok = ecm.dms.get(RechnungsDokument, 42, file_properties=True)
fp = dok.system.file_properties
print(f"{fp.extension}, {fp.size} Bytes, {fp.documentpagecount} Seiten")

4.3.4. system.variants

Befüllt bei variants=True (get()) oder .variants() (select()). Nur für ECMDocumentModel mit aktiviertem W-Modul relevant. Typ: list[ECMModelDocumentVariant].

Jeder Eintrag enthält:

Attribut Typ Beschreibung

doc_id

int

Dokument-ID dieser Variante.

doc_ver

str

Versionsbezeichnung (z. B. "1.0", "1.1").

is_active

bool

True für die aktuell aktive Variante.

doc_parent

int | None

ID der übergeordneten Variante, oder None für die Wurzel.

children

list[int]

IDs der aus dieser Variante abgezweigten Kindvarianten.

dok = ecm.dms.get(RechnungsDokument, 42, variants=True)
for v in dok.system.variants:
    print(v.doc_ver, "✓" if v.is_active else "")

4.3.5. system.remarks

Befüllt bei remarks=True (get()) oder .remarks() (select()). Typ: list[str]. Enthält die Textnotizen des Objekts.

for ordner in ecm.dms.select(RechnungsOrdner).remarks().stream():
    for bemerkung in ordner.system.remarks:
        print(bemerkung)

5. Änderungsverfolgung

Modellinstanzen verfolgen Feldänderungen automatisch. Das Zuweisen eines neuen Wertes an ein ECMField markiert das Feld als geändert:

ordner = ecm.dms.get(RechnungsOrdner, 12345)
ordner.Titel = "Aktualisierter Titel"

print(ordner.system.is_modified)               # True
print(ordner.system.modified_fields)           # {"Titel"}
print(ordner.system.is_field_modified("Jahr")) # False

update() liest system.is_modified, um zu entscheiden, ob der Serveraufruf übersprungen werden kann. Wurden keine Felder geändert und werden keine Dateien übergeben, wird der Aufruf stillschweigend ausgelassen (außer bei force=True).

Das Entfernen einer Zeile aus einem ECMTableField setzt system.has_removed_table_rows = True, wodurch update() REPLACETABLEFIELDS=1 zur Anfrage hinzufügt.

6. Siehe auch