= upsert()
:description: Fluenter Upsert-Builder (ECMModelUpsertSync/Async) auf Basis von dms.XMLImport. Konfigurierbar mit search(), action0/1/multiple() und files(). Gibt (id, type_id, hits, action) zurück.
:keywords: upsert, XMLImport, ECMModelUpsertSync, ECMModelUpsertAsync, search, action0, action1, insert, update
:page-tags: api, dms

`upsert()` gibt einen fluenten Builder zurück, der `dms.XMLImport` kapselt.
Der Server führt zuerst eine Suche anhand der mit `.search()` konfigurierten Bedingungen durch und
führt je nach Trefferzahl die konfigurierte Aktion aus.

== Signatur

[tabs]
====
Sync::
+
[source,python]
----
ecm.dms.upsert(
    model: T,
    folder_id: int | None = None,
    register_id: int | None = None,
    *,
    check_mandatory: bool = True,
) -> ECMModelUpsertSync[T]
----

Async::
+
[source,python]
----
ecm.dms.upsert(
    model: T,
    folder_id: int | None = None,
    register_id: int | None = None,
    *,
    check_mandatory: bool = True,
) -> ECMModelUpsertAsync[T]
----
====

== Parameter

[cols="1,1,3"]
|===
|Parameter |Standard |Beschreibung

|`model`
|—
|Befüllte Modell-Instanz (Subklasse von `ECMFolderModel`, `ECMRegisterModel` oder `ECMDocumentModel`), deren Feldwerte auf den Server geschrieben werden.

|`folder_id`
|`None`
|ID des übergeordneten Ordners. Nur für `ECMRegisterModel` und `ECMDocumentModel` relevant.

|`register_id`
|`None`
|ID des übergeordneten Registers. Bei `ECMDocumentModel`: Register, dem das Dokument zugeordnet wird. Bei `ECMRegisterModel`: übergeordnetes Register.

|`check_mandatory`
|`True`
|Prüft beim Aufruf von `.execute()`, ob alle Pflichtfelder (`mandatory=True`) einen Wert haben. Bei `False` wird die clientseitige Prüfung übersprungen und `CHECKOBLIGATION=0` an den Server gesendet.
|===

== Builder-Methoden

=== `.search(*conditions)`

Definiert die Suchbedingungen für die serverseitige Duplikatprüfung.

[IMPORTANT]
====
Nur `field_name` und `value` jeder Bedingung werden verwendet — Operatoren werden ignoriert.
Der Server führt immer einen Gleichheitsabgleich in der `<Search>`-Sektion aus.
====

[source,python]
----
.search(InvoiceFolder.Title == "Rechnung 2024", InvoiceFolder.Year == 2024)
----

=== `.action0(value)`, `.action1(value)`, `.action_multiple(value)`

Konfiguriert die Aktion abhängig von der Trefferzahl der Suche:

[cols="1,1,1,3"]
|===
|Methode |Trefferanzahl |Standard |Gültige Werte

|`.action0(value)`
|0 Treffer
|`"INSERT"`
|`"INSERT"`, `"NONE"`, `"ERROR"`

|`.action1(value)`
|genau 1 Treffer
|`"UPDATE"`
|`"UPDATE"`, `"INSERT"`, `"NONE"`

|`.action_multiple(value)`
|mehr als 1 Treffer
|`"ERROR"`
|`"ERROR"`, `"UPDATE"`, `"INSERT"`, `"NONE"`
|===

Bedeutung der Aktionswerte:

`"INSERT"`:: Legt ein neues Objekt an.
`"UPDATE"`:: Aktualisiert das gefundene Objekt mit den Feldern aus `model`.
`"NONE"`:: Führt keine Aktion aus. `object_id` und `object_type_id` sind `-1`.
`"ERROR"`:: Bricht mit einem Fehler ab.

=== `.files(value, replace=True)`

Hängt Dateien an den Upsert-Vorgang an. Nur für `ECMDocumentModel` gültig.

[cols="1,1,3"]
|===
|Parameter |Standard |Beschreibung

|`value`
|—
|`list[JobRequestFile]` — Liste der anzuhängenden Dateien.

|`replace`
|`True`
|Bei `True` ersetzen die Dateien vorhandene Dateien des Dokuments (`REPLACEFILES=1`). Bei `False` werden sie hinzugefügt.
|===

[NOTE]
====
Wird `.files()` für ein Nicht-Dokument-Modell aufgerufen, wird `TypeError` ausgelöst.
====

=== `.execute()`

Führt den Upsert aus und gibt ein 4-Tupel zurück:

[cols="1,1,3"]
|===
|Element |Typ |Beschreibung

|`object_id`
|`int`
|ID des angelegten oder aktualisierten Objekts. `-1` bei Fehler oder wenn keine Aktion ausgeführt wurde.

|`object_type_id`
|`int`
|Typ-ID des Objekts. `-1` bei Fehler.

|`hits`
|`int`
|Anzahl der vom Server gefundenen Treffer.

|`action`
|`str`
|Tatsächlich ausgeführte Aktion: `"INSERT"`, `"UPDATE"`, `"NONE"` oder `"ERROR"`.
|===

== Ausnahmen

`ValueError`:: Ein Pflichtfeld hat keinen Wert und `check_mandatory=True`.
`TypeError`:: `.files()` wurde für ein Nicht-`ECMDocumentModel` aufgerufen.

== Beispiele

=== Einfacher Folder-Upsert

Legt einen Ordner an oder aktualisiert ihn, falls er bereits existiert:

[tabs]
====
Sync::
+
[source,python]
----
object_id, type_id, hits, action = (
    ecm.dms.upsert(InvoiceFolder(Title="Rechnung 2024", Year=2024))
    .search(InvoiceFolder.Title == "Rechnung 2024")
    .execute()
)
print(action)  # "INSERT" oder "UPDATE"
----

Async::
+
[source,python]
----
object_id, type_id, hits, action = await (
    ecm.dms.upsert(InvoiceFolder(Title="Rechnung 2024", Year=2024))
    .search(InvoiceFolder.Title == "Rechnung 2024")
    .execute()
)
print(action)  # "INSERT" oder "UPDATE"
----
====

=== Nur anlegen, nicht aktualisieren

Mit `action1("NONE")` wird bei einem vorhandenen Treffer keine Aktion ausgeführt:

[tabs]
====
Sync::
+
[source,python]
----
object_id, type_id, hits, action = (
    ecm.dms.upsert(InvoiceFolder(Title="Rechnung 2024", Year=2024))
    .search(InvoiceFolder.Title == "Rechnung 2024")
    .action1("NONE")
    .execute()
)
----

Async::
+
[source,python]
----
object_id, type_id, hits, action = await (
    ecm.dms.upsert(InvoiceFolder(Title="Rechnung 2024", Year=2024))
    .search(InvoiceFolder.Title == "Rechnung 2024")
    .action1("NONE")
    .execute()
)
----
====

=== Register mit Standortangabe

[tabs]
====
Sync::
+
[source,python]
----
object_id, type_id, hits, action = (
    ecm.dms.upsert(
        InvoiceRegister(Name="2024"),
        folder_id=42,
    )
    .search(InvoiceRegister.Name == "2024")
    .execute()
)
----

Async::
+
[source,python]
----
object_id, type_id, hits, action = await (
    ecm.dms.upsert(
        InvoiceRegister(Name="2024"),
        folder_id=42,
    )
    .search(InvoiceRegister.Name == "2024")
    .execute()
)
----
====

=== Dokument mit Datei-Anhang

[tabs]
====
Sync::
+
[source,python]
----
from datetime import date
from ecmind_blue_client.rpc import JobRequestFile

object_id, type_id, hits, action = (
    ecm.dms.upsert(
        InvoiceDocument(Title="Rechnung 2024", InvoiceDate=date(2024, 3, 1)),
        folder_id=42,
    )
    .search(InvoiceDocument.Title == "Rechnung 2024")
    .files([JobRequestFile("rechnung.pdf", pdf_bytes)], replace=True)
    .execute()
)
----

Async::
+
[source,python]
----
from datetime import date
from ecmind_blue_client.rpc import JobRequestFile

object_id, type_id, hits, action = await (
    ecm.dms.upsert(
        InvoiceDocument(Title="Rechnung 2024", InvoiceDate=date(2024, 3, 1)),
        folder_id=42,
    )
    .search(InvoiceDocument.Title == "Rechnung 2024")
    .files([JobRequestFile("rechnung.pdf", pdf_bytes)], replace=True)
    .execute()
)
----
====

=== Pflichtfeldprüfung deaktivieren

[tabs]
====
Sync::
+
[source,python]
----
object_id, type_id, hits, action = (
    ecm.dms.upsert(
        InvoiceFolder(Year=2024),
        check_mandatory=False,
    )
    .search(InvoiceFolder.Year == 2024)
    .execute()
)
----

Async::
+
[source,python]
----
object_id, type_id, hits, action = await (
    ecm.dms.upsert(
        InvoiceFolder(Year=2024),
        check_mandatory=False,
    )
    .search(InvoiceFolder.Year == 2024)
    .execute()
)
----
====

== Siehe auch

* xref:api/dms/insert.adoc[insert()] — Einfaches Anlegen ohne Duplikatprüfung
* xref:api/dms/update.adoc[update()] — Einfaches Aktualisieren ohne Duplikatprüfung
* xref:api/dms/select.adoc[select()] — Objekte mit typisiertem Modell abfragen
