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 |
|---|---|
|
Ordner-Objekttypen |
|
Register-Objekttypen (Unterordner) |
|
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 oder ein generiertes Katalog-Enum).
Felder mit Listenkatalog werden mit einer generierten (str, Enum)-Klasse typisiert (siehe Modellgenerator). Wird einem solchen Feld ein Enum-Member zugewiesen, schreibt der Client den zugrunde liegenden Katalogwert — das Enum wird beim Speichern auf seinen .value reduziert, sodass ordner.Status = InvoiceFolder_StatusEnum.PAID als Paid gesendet wird.
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 |
|---|---|
|
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 |
|
Nur bei Neuanlage beschreibbar (solange das Objekt keine ID hat). Sobald das Objekt gespeichert wurde ( |
|
Beschreibbar, bis das Dokument archiviert ist (nicht leeres |
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 |
|---|---|
|
Interne Zeilenkennung, die vom Server vergeben wird. |
|
|
|
Gibt |
3.1. Tabellenspalten abfragen
Eine Bedingung auf eine Tabellenfeld-Spalte wird über den Spaltenzugriff am Tabellenfeld auf
Klassenebene gebildet. select(…).where(Invoice.Positions.ArticleNo == "A-100") erzeugt eine
<TableCondition> / <TableColumn>, sodass der Server auf die Untertabellen-Spalte filtert, anstatt
ein unbekanntes Objektfeld abzulehnen. Mit der Server-Zeilennummer als Index lässt sich die Bedingung
auf eine Zeile beschränken (Invoice.Positions[3].Quantity == 5; expliziter Alias .row(3)). Alle
Vergleichs- und Mengenoperatoren sind anwendbar. Vollständige Beispiele und das erzeugte XML: siehe
select().
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. system-Felder in Abfragen (Klassen-Zugriff)
Das system-Attribut verhält sich je nach Zugriffsebene unterschiedlich:
-
Instanz-Zugriff (
obj.system.id) liefert den geladenen Wert (z. B.int). -
Klassen-Zugriff (
Modell.system.id) liefert einECMField, das wie ein Indexfeld inwhere()undorder_by()verwendet werden kann.
So lässt sich derselbe punktierte Pfad in einer Abfrage einsetzen. Typisches Beispiel — alle Dokumente in einem bekannten Ordner über eine schrankübergreifende (Cross-Type-)Bedingung auf die Ordner-ID:
docs = (
ecm.dms.select(PostDoc)
.where(Post.system.id == folder.system.id) # links Klasse (ECMField), rechts Instanzwert (int)
.limit(1)
.execute()
)
Die erzeugte Bedingung ist an den Objekttyp des Modells gebunden, auf dem sie aufgerufen wird (Post), und wird vom HOL-Builder in ein eigenes ConditionObject einsortiert — identisch zu Post["OBJECT_ID"], inklusive automatischem system="1"-Attribut. Es werden nur Systemfelder mit einer einzelnen abfragbaren Spalte als ECMField angeboten; aggregierte Werte ohne Backing-Feld (system.rights, system.name, system.is_modified, …) sind ausschließlich auf Instanzen verfügbar.
Abfragbar sind: die unter Immer verfügbar gelisteten Felder mit Einzelspalten-Backing (id, owner_guid, creator, creation_date, last_modifier, last_modified, creation_time, deleted_at), die typabhängigen Felder (folder_id, parent_register_id, register_id, register_type_id, system_id, foreign_id, lock_user_id, archivist, archiving_date) sowie die abfragbare Teilmenge von system.base_params (creator, creation_date, modifier, modified_date, links_count, locked_user_id) und system.file_properties (count, size). Felder ohne abfragbare Serverspalte (z. B. system.base_params.version, system.file_properties.extension) bleiben rein instanzseitig.
4.1.1. Generischer Item-Zugriff für beliebige Systemfelder
Der .system-Namespace bietet nur die kuratierte, typisierte Teilmenge an. Für jedes Server-Systemfeld — auch solche ohne .system-Eigenschaft (OBJECT_FLAGS, OBJECT_RETENTION, OBJECT_MAIN, …) — und für dynamische Modelle ohne deklarierte Felder steht der generische Item-Zugriff auf der Modellklasse zur Verfügung:
ecm.dms.select(PostDoc).where(Post["OBJECT_ID"] == folder.system.id) # gleichbedeutend mit Post.system.id
ecm.dms.select(PostDoc).where(Post["OBJECT_FLAGS"] == 0) # Systemfeld ohne .system-Eigenschaft
Modell["NAME"] liefert ein ECMField, das an den Objekttyp gebunden ist; ist NAME ein Member des SystemFields-Enums (z. B. OBJECT_ID), wird beim Bauen automatisch system="1" gesetzt. Der Item-Zugriff ist untypisiert (ECMField[str], keine IDE-Vervollständigung); Modell.system.<feld> ist typisiert und entdeckbar, aber auf die kuratierte Teilmenge beschränkt. Beide erzeugen dieselbe Bedingung. Derselbe Item-Zugriff funktioniert auch für nicht deklarierte Indexfelder (Modell["EigenesFeld"]).
4.2. Immer verfügbar
Die folgenden Eigenschaften sind immer gefüllt, unabhängig von den Flags des Abfrage- oder Abrufaufrufs:
| Eigenschaft | Typ | Beschreibung |
|---|---|---|
|
|
Numerische ID des Objekts auf dem Server. |
|
|
Anzeigename des Objekts, wie er auf dem Server gespeichert ist. Entspricht in der Regel dem Wert des Schlüsselfeldes. |
|
|
|
|
|
|
|
|
Menge der internen Feldnamen, die seit dem Laden geändert wurden. |
|
|
Menge der Tabellenfeldnamen, bei denen Zeilen hinzugefügt, entfernt oder geändert wurden. |
|
|
Gibt |
4.3. Je nach Objekttyp verfügbar
Einige Systemeigenschaften sind nur bei bestimmten Modelltypen vorhanden:
| Eigenschaft | Typ | Verfügbar bei | Beschreibung |
|---|---|---|---|
|
|
|
ID des übergeordneten Ordners. Wird nur befüllt, wenn |
|
|
|
ID des direkt übergeordneten Registers, wenn das Register in einem anderen Register verschachtelt ist. |
|
|
|
ID des übergeordneten Registers. Wird nur befüllt, wenn |
|
|
|
Typ-ID des übergeordneten Registers. |
|
|
|
ID des externen Archivsystems ( |
|
|
|
Referenz auf das Dokument im externen Archivsystem ( |
|
|
|
Numerische ID des Benutzers, der das Dokument aktuell gesperrt hält ( |
|
|
|
|
|
|
|
Roher Archivierungsstatus-Code ( |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Die Archivierungs- und Sperr-Eigenschaften (
Die Suchflag-Eigenschaften |
4.4. 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.4.1. system.rights
| Attribut | Typ | Beschreibung |
|---|---|---|
|
|
Darf untergeordnete Objekte anlegen (Register oder Dokumente in einem Ordner; Dokumente in einem Register). |
|
|
Darf die Indexfelder des Objekts ändern. |
|
|
Darf die Datei des Dokuments lesen. |
|
|
Darf die Datei des Dokuments ändern. |
|
|
Darf das Objekt löschen. |
ordner = ecm.dms.get(RechnungsOrdner, 12345, rights=True)
if ordner.system.rights.edit_metadata:
print("Bearbeitung erlaubt")
4.4.2. system.base_params
| Attribut | Typ | Beschreibung |
|---|---|---|
|
|
Benutzername des Erstellers. |
|
|
Erstellungszeitpunkt. |
|
|
Aktueller Eigentümer des Objekts. |
|
|
Benutzername des letzten Bearbeiters. |
|
|
Zeitstempel der letzten Änderung. |
|
|
Anzahl der Verknüpfungen auf dieses Objekt. |
|
|
Anzahl der Textnotizen (Bemerkungen) am Objekt. |
|
|
Sperrzustand des Objekts: |
|
|
Numerische ID des sperrenden Benutzers, oder |
|
|
Anzahl der PDF-Annotationen am Objekt. |
|
|
Versionsnummer des Objekts. |
|
|
Anzeigetext des Archivierungszustands, oder |
|
|
Numerischer Wert des Archivierungszustands (aus dem |
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.4.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 |
|---|---|---|
|
|
Anzahl der Dateien (Primär- und Sekundärdateien). |
|
|
Gesamtdateigröße in Bytes. |
|
|
Dateiendung (z. B. |
|
|
MIME-Typ (z. B. |
|
|
MIME-Gruppe (z. B. |
|
|
ID des Dateitypicons. |
|
|
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.4.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 |
|---|---|---|
|
|
Dokument-ID dieser Variante. |
|
|
Versionsbezeichnung (z. B. |
|
|
|
|
|
ID der übergeordneten Variante, oder |
|
|
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 "")
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
-
Workflow-Modell — das typisierte Modell für Workflow-Eingangsvariablen
-
select() — Query-Builder mit
.rights(),.base_params(),.file_properties(),.variants(),.remarks() -
get() — Einzelnes Objekt per ID laden mit optionalen Flag-Parametern
-
insert() / insert_and_get() — Neue Objekte anlegen
-
update() / update_and_get() — Änderungen auf dem Server speichern