744 lines
30 KiB
Python
744 lines
30 KiB
Python
"""
|
|
paperless_addon.py - Einstiegspunkt der LibreOffice-Erweiterung.
|
|
|
|
Registriert einen Protokoll-Handler für die URLs paperless.Command:*.
|
|
Die Symbolleiste in Addons.xcu ruft diese URLs auf.
|
|
|
|
Warum kein Eingriff in den normalen Speichern-Knopf: Paperless als echtes
|
|
Speicherziel in die Datei-Dialoge einzuhaengen würde einen UCB Content
|
|
Provider erfordern - C++-UNO-Arbeit in einer sehr dünn dokumentierten
|
|
Ecke. Eigene Schaltflaechen sind außerdem die ehrlichere Lösung, weil
|
|
sichtbar bleibt, wann eine neue Version im Archiv entsteht.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import traceback
|
|
|
|
import uno
|
|
import unohelper
|
|
from com.sun.star.frame import XDispatchProvider, XDispatch
|
|
from com.sun.star.lang import XInitialization, XServiceInfo
|
|
from com.sun.star.beans import PropertyValue
|
|
|
|
import paperless_config as cfg
|
|
import paperless_dialogs as dlg
|
|
import paperless_formats as fmt
|
|
from paperless_i18n import t
|
|
from paperless_client import PaperlessClient, PaperlessError
|
|
|
|
IMPL_NAME = "de.mmorath.paperless.AddonImpl"
|
|
SERVICE_NAME = "com.sun.star.frame.ProtocolHandler"
|
|
PROTOCOL = "paperless.Command:"
|
|
|
|
# Ladeeinstellungen für heruntergeladene Dateien.
|
|
#
|
|
# SICHERHEITSRELEVANT: Ein Dokument aus dem Archiv ist fremder Inhalt -
|
|
# auch dann, wenn es aus dem eigenen Bestand stammt, denn dort landen
|
|
# Dateien aus E-Mail-Anhängen, Scannerordnern und Freigaben. Ein ODT oder
|
|
# DOCX kann Makros enthalten, die beim Öffnen ausgeführt werden, und
|
|
# Verweise auf externe Inhalte, die beim Öffnen nachgeladen werden.
|
|
#
|
|
# MacroExecMode 1 = NEVER_EXECUTE: Makros werden nicht ausgeführt, auch
|
|
# nicht nach Rückfrage. Der Anwender kann sie nicht aus
|
|
# Versehen freigeben.
|
|
# UpdateDocMode 0 = NO_UPDATE: keine Aktualisierung verknüpfter Inhalte,
|
|
# also kein Nachladen externer Adressen beim Öffnen.
|
|
MACRO_NEVER_EXECUTE = 1
|
|
UPDATE_NO_UPDATE = 0
|
|
|
|
|
|
def _prop(name, value):
|
|
p = PropertyValue()
|
|
p.Name, p.Value = name, value
|
|
return p
|
|
|
|
|
|
class PaperlessAddon(unohelper.Base, XDispatchProvider, XDispatch,
|
|
XInitialization, XServiceInfo):
|
|
|
|
def __init__(self, ctx):
|
|
self.ctx = ctx
|
|
self.frame = None
|
|
self._desktop = None
|
|
|
|
# ------------------------------------------------------ UNO-Gerüst
|
|
|
|
def initialize(self, args):
|
|
if args:
|
|
self.frame = args[0]
|
|
|
|
def queryDispatch(self, url, _target, _flags):
|
|
return self if url.Protocol == PROTOCOL else None
|
|
|
|
def queryDispatches(self, requests):
|
|
return tuple(self.queryDispatch(r.FeatureURL, r.FrameName, r.SearchFlags)
|
|
for r in requests)
|
|
|
|
def addStatusListener(self, listener, url):
|
|
pass
|
|
|
|
def removeStatusListener(self, listener, url):
|
|
pass
|
|
|
|
def getImplementationName(self):
|
|
return IMPL_NAME
|
|
|
|
def supportsService(self, name):
|
|
return name == SERVICE_NAME
|
|
|
|
def getSupportedServiceNames(self):
|
|
return (SERVICE_NAME,)
|
|
|
|
# ---------------------------------------------------------- Dispatch
|
|
|
|
def dispatch(self, url, _args):
|
|
command = url.Path
|
|
handler = {
|
|
"Open": self.cmd_open,
|
|
"SaveVersion": self.cmd_save_version,
|
|
"SaveAs": self.cmd_save_as,
|
|
"Classify": self.cmd_classify,
|
|
"Share": self.cmd_share,
|
|
"Settings": self.cmd_settings,
|
|
"Info": self.cmd_info,
|
|
}.get(command)
|
|
if handler is None:
|
|
return
|
|
try:
|
|
handler()
|
|
except PaperlessError as exc:
|
|
dlg.error(self.ctx, self.frame, str(exc))
|
|
except Exception as exc: # noqa: BLE001
|
|
# Der Ablaufbericht steht auf der Konsole, nicht im Dialog. Er
|
|
# kann Pfade und Werte enthalten, die niemanden etwas angehen,
|
|
# der zufällig auf den Bildschirm sieht.
|
|
sys.stderr.write("[paperless] %s\n%s\n"
|
|
% (command, traceback.format_exc()))
|
|
dlg.error(self.ctx, self.frame,
|
|
"Unerwarteter Fehler bei „%s“:\n\n%s\n\n"
|
|
"Einzelheiten stehen auf der Konsole "
|
|
"(LibreOffice aus dem Terminal starten)."
|
|
% (command, str(exc)[:300]))
|
|
|
|
# ---------------------------------------------------------- Helfer
|
|
|
|
def _desktop_obj(self):
|
|
if self._desktop is None:
|
|
self._desktop = self.ctx.ServiceManager.createInstanceWithContext(
|
|
"com.sun.star.frame.Desktop", self.ctx)
|
|
return self._desktop
|
|
|
|
def _client(self, require=True):
|
|
s = cfg.load_settings()
|
|
if not cfg.settings_complete(s):
|
|
if require:
|
|
dlg.message(self.ctx, self.frame,
|
|
"Bitte zuerst Basisadresse und Token hinterlegen.")
|
|
self.cmd_settings()
|
|
s = cfg.load_settings()
|
|
if not cfg.settings_complete(s):
|
|
return None, s
|
|
return PaperlessClient(s["base_url"], s["token"],
|
|
s.get("verify_tls", True),
|
|
user_agent=s.get("user_agent") or None), s
|
|
|
|
def _current_doc(self):
|
|
try:
|
|
return self._desktop_obj().getCurrentComponent()
|
|
except Exception:
|
|
return None
|
|
|
|
def _current_doc_id(self, quiet=False):
|
|
doc = self._current_doc()
|
|
if doc is None:
|
|
if not quiet:
|
|
dlg.message(self.ctx, self.frame, "Kein Dokument geöffnet.")
|
|
return None, None
|
|
doc_id = cfg.get_document_id(doc)
|
|
if doc_id is None and not quiet:
|
|
dlg.message(self.ctx, self.frame,
|
|
"Dieses Dokument stammt nicht aus Paperless.\n\n"
|
|
"Es wurde keine Paperless-Kennung gefunden. Öffne es "
|
|
"über „Aus Paperless öffnen“, damit die "
|
|
"Zuordnung erhalten bleibt.")
|
|
return doc, doc_id
|
|
|
|
# ---------------------------------------------------- Befehl: Öffnen
|
|
|
|
def cmd_open(self):
|
|
client, settings = self._client()
|
|
if client is None:
|
|
return
|
|
|
|
picked = dlg.open_dialog(self.ctx, self.frame, client)
|
|
if not picked:
|
|
return
|
|
full, version, readonly = picked
|
|
root_id = full["id"]
|
|
|
|
# Heruntergeladen wird die gewählte Version. Jede Version ist ein
|
|
# eigener Dokumentdatensatz mit eigener Kennung.
|
|
data, remote_name = client.download(version["id"], original=True)
|
|
|
|
# Der Name stammt aus einer Kopfzeile der Gegenstelle und ist damit
|
|
# nicht vertrauenswürdig. Ein Wert wie "../../.bashrc" darf nicht
|
|
# aus dem Arbeitsverzeichnis herausführen - deshalb vollständige
|
|
# Bereinigung, plattformunabhängig für beide Trennzeichen.
|
|
name = cfg.safe_filename(
|
|
remote_name or version.get("filename")
|
|
or full.get("original_file_name")
|
|
or ("paperless-%d.bin" % root_id),
|
|
vorgabe="paperless-%d.bin" % root_id)
|
|
|
|
# Ablage im Benutzerprofil statt im allgemeinen Temp-Verzeichnis.
|
|
# Letzteres ist auf Mehrbenutzersystemen für alle beschreibbar; ein
|
|
# anderer Benutzer könnte dort unter dem erwarteten Namen einen
|
|
# symbolischen Verweis anlegen und das Schreiben umlenken.
|
|
path = os.path.join(cfg.work_dir(str(root_id)), name)
|
|
# Datei mit eingeschränkten Rechten anlegen. O_EXCL ist hier
|
|
# bewusst nicht gesetzt, weil dieselbe Version erneut geöffnet
|
|
# werden können soll - das Verzeichnis liegt aber im Benutzer-
|
|
# profil und ist für andere nicht beschreibbar.
|
|
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
try:
|
|
with os.fdopen(fd, "wb") as fh:
|
|
fh.write(data)
|
|
except Exception: # noqa: BLE001
|
|
raise
|
|
|
|
url = unohelper.systemPathToFileUrl(path)
|
|
doc = self._desktop_obj().loadComponentFromURL(
|
|
url, "_blank", 0,
|
|
(_prop("ReadOnly", readonly),
|
|
_prop("MacroExecutionMode", MACRO_NEVER_EXECUTE),
|
|
_prop("UpdateDocMode", UPDATE_NO_UPDATE)))
|
|
|
|
# Gemerkt wird immer die WURZEL-Kennung, nicht die der geöffneten
|
|
# Version. Eine neue Version wird ja an die Wurzel angehängt.
|
|
if doc is not None:
|
|
cfg.set_document_id(doc, root_id, settings["base_url"])
|
|
cfg.remember(url, root_id, settings["base_url"])
|
|
|
|
if readonly:
|
|
dlg.message(self.ctx, self.frame,
|
|
"Schreibgeschützt geöffnet.\n\nZum Ablegen einer "
|
|
"neuen Version muss das Dokument bearbeitbar "
|
|
"geöffnet werden.")
|
|
|
|
# -------------------------------------------- Befehl: Neue Version
|
|
|
|
def cmd_save_version(self):
|
|
client, settings = self._client()
|
|
if client is None:
|
|
return
|
|
doc, doc_id = self._current_doc_id()
|
|
if doc_id is None:
|
|
return
|
|
|
|
remote = client.document(doc_id)
|
|
title = remote.get("title") or str(doc_id)
|
|
|
|
if not dlg.confirm(
|
|
self.ctx, self.frame,
|
|
"Als neue Version im Archiv ablegen?\n\n"
|
|
"Dokument: %s\nPaperless-ID: %d\n\n"
|
|
"Die bisherige Fassung bleibt als Version erhalten."
|
|
% (title[:70], doc_id)):
|
|
return
|
|
|
|
path = self._store_local(doc)
|
|
if path is None:
|
|
return
|
|
|
|
with open(path, "rb") as fh:
|
|
data = fh.read()
|
|
|
|
from datetime import date
|
|
label = (settings.get("version_label") or "LibreOffice {datum}") \
|
|
.replace("{datum}", date.today().isoformat())
|
|
|
|
task = client.update_version(doc_id, os.path.basename(path), data,
|
|
version_label=label)
|
|
if not isinstance(task, str) or not task:
|
|
dlg.error(self.ctx, self.frame,
|
|
t("Unerwartete Antwort beim Hochladen:\n%r") % (task,))
|
|
return
|
|
|
|
new_id = client.wait_for_task(task)
|
|
cfg.set_document_id(doc, doc_id, settings["base_url"])
|
|
dlg.message(self.ctx, self.frame,
|
|
"Neue Version abgelegt.\n\n"
|
|
"Dokument: %s\nWurzel-ID: %d\nVersions-ID: %s\n"
|
|
"Bezeichnung: %s" % (title[:70], doc_id, new_id, label))
|
|
|
|
def _store_local(self, doc, vorschlag=None):
|
|
"""
|
|
Speichert das Dokument und liefert den Pfad auf der Platte.
|
|
|
|
Drei Fälle:
|
|
|
|
* über „Aus Paperless öffnen“ geladen - getURL() zeigt auf die
|
|
temporäre Datei, es genügt ein Speichern im Originalformat
|
|
* lokal vorhandene Datei - ebenso
|
|
* noch nie gespeichert (Unbenannt 1) - dann gibt es keinen URL.
|
|
Statt abzubrechen wird in dasselbe temporäre Verzeichnis
|
|
geschrieben, das auch beim Öffnen benutzt wird.
|
|
"""
|
|
url = doc.getURL()
|
|
|
|
if not url:
|
|
ext, filt = fmt.default_for_document(doc)
|
|
name = cfg.safe_filename(
|
|
(vorschlag or self._doc_title(doc)), maxlen=72) + ext
|
|
path = os.path.join(cfg.work_dir("neu"), name)
|
|
try:
|
|
doc.storeToURL(unohelper.systemPathToFileUrl(path),
|
|
(_prop("FilterName", filt),
|
|
_prop("Overwrite", True)))
|
|
cfg.lock_down(path, 0o600)
|
|
except Exception as exc: # noqa: BLE001
|
|
dlg.error(self.ctx, self.frame,
|
|
t("Das Dokument konnte nicht zwischengespeichert werden:\n%s") % exc)
|
|
return None
|
|
return path
|
|
|
|
path = unohelper.fileUrlToSystemPath(url)
|
|
|
|
# Bei unbekanntem Format gibt es keinen passenden Filter. Dann
|
|
# speichert LibreOffice im eigenen Format weiter - der Anwender
|
|
# sollte wissen, dass die Endung danach nicht mehr zum Inhalt passt.
|
|
if not fmt.is_known(path) and doc.isModified():
|
|
if not dlg.confirm(
|
|
self.ctx, self.frame,
|
|
"Für dieses Format ist kein Speicherfilter hinterlegt "
|
|
"(%s).\n\nLibreOffice speichert im eigenen Format. Die "
|
|
"Dateiendung passt danach möglicherweise nicht mehr zum "
|
|
"Inhalt.\n\nTrotzdem fortfahren?"
|
|
% os.path.basename(path)):
|
|
return None
|
|
|
|
try:
|
|
if doc.isModified():
|
|
filt = fmt.store_filter(path)
|
|
if filt:
|
|
doc.storeToURL(url, (_prop("FilterName", filt),
|
|
_prop("Overwrite", True)))
|
|
else:
|
|
doc.store()
|
|
except Exception as exc: # noqa: BLE001
|
|
dlg.error(self.ctx, self.frame,
|
|
t("Speichern fehlgeschlagen:\n%s") % exc)
|
|
return None
|
|
return path
|
|
|
|
@staticmethod
|
|
def _doc_title(doc):
|
|
"""Titel aus den Dokumenteigenschaften, sonst der Fenstertitel."""
|
|
try:
|
|
t = doc.getDocumentProperties().Title
|
|
if t and t.strip():
|
|
return t.strip()
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
try:
|
|
return (doc.getTitle() or "Dokument").strip()
|
|
except Exception: # noqa: BLE001
|
|
return "Dokument"
|
|
|
|
# ------------------------------------- Befehl: Als neues Dokument
|
|
|
|
def cmd_save_as(self):
|
|
"""
|
|
Legt die geöffnete Datei in Paperless ab - wahlweise als neues
|
|
Dokument oder als neue Version eines ANDEREN vorhandenen Dokuments.
|
|
|
|
Der zweite Fall deckt eine Lücke, die Dokumentenverwaltungen gern
|
|
offenlassen: Die Rechnung kommt als PDF, die bearbeitbare Fassung
|
|
entsteht später. Beides gehört zusammen, aber das PDF war zuerst da
|
|
- mit „Als neue Version ablegen“ allein kommt man da nicht hin,
|
|
weil das immer auf das eigene Wurzeldokument zielt.
|
|
"""
|
|
client, settings = self._client()
|
|
if client is None:
|
|
return
|
|
doc, doc_id = self._current_doc_id(quiet=True)
|
|
if doc is None:
|
|
dlg.message(self.ctx, self.frame, "Kein Dokument geöffnet.")
|
|
return
|
|
|
|
# Die Herkunft aus Paperless ist optional - auch eine fremde Datei
|
|
# kann so ins Archiv wandern.
|
|
source = {}
|
|
if doc_id is not None:
|
|
try:
|
|
source = client.document(doc_id)
|
|
except PaperlessError:
|
|
source = {}
|
|
if not source.get("title"):
|
|
url = doc.getURL() or ""
|
|
base = os.path.splitext(os.path.basename(
|
|
unohelper.fileUrlToSystemPath(url)))[0] if url else "Dokument"
|
|
source = {"title": base}
|
|
|
|
choice = dlg.store_target_dialog(self.ctx, self.frame, client, source)
|
|
if choice is None:
|
|
return
|
|
|
|
# Der gewählte Titel bestimmt bei einem noch nie gespeicherten
|
|
# Dokument auch den Namen der temporären Datei - und damit den
|
|
# Dateinamen, unter dem Paperless das Original ablegt.
|
|
vorschlag = choice.get("title") if choice["mode"] == "new" else \
|
|
(choice.get("target") or {}).get("title")
|
|
path = self._store_local(doc, vorschlag)
|
|
if path is None:
|
|
return
|
|
with open(path, "rb") as fh:
|
|
data = fh.read()
|
|
|
|
if choice["mode"] == "existing":
|
|
self._append_to_existing(client, settings, doc, choice, path, data)
|
|
else:
|
|
self._create_new(client, settings, doc, doc_id, source, choice,
|
|
path, data)
|
|
|
|
# -- Ziel: bestehendes Dokument -----------------------------------------
|
|
|
|
def _append_to_existing(self, client, settings, doc, choice, path, data):
|
|
target = choice["target"]
|
|
target_id = target["id"]
|
|
|
|
if not dlg.confirm(
|
|
self.ctx, self.frame,
|
|
"Die geöffnete Datei als neue Version anhängen?\n\n"
|
|
"Zieldokument: %s\nPaperless-ID: %d\n\n"
|
|
"Die bisherige Fassung des Ziels bleibt als Version "
|
|
"erhalten." % ((target.get("title") or "")[:70], target_id)):
|
|
return
|
|
|
|
from datetime import date
|
|
label = (settings.get("version_label") or "LibreOffice {datum}") \
|
|
.replace("{datum}", date.today().isoformat())
|
|
|
|
task = client.update_version(target_id, os.path.basename(path), data,
|
|
version_label=label)
|
|
if not isinstance(task, str) or not task:
|
|
dlg.error(self.ctx, self.frame,
|
|
t("Unerwartete Antwort beim Hochladen:\n%r") % (task,))
|
|
return
|
|
new_id = client.wait_for_task(task)
|
|
|
|
if choice.get("switch"):
|
|
# Ab jetzt zeigt das Fenster auf das Zieldokument. Weitere
|
|
# Versionen landen dort, nicht mehr beim Ursprung.
|
|
cfg.set_document_id(doc, target_id, settings["base_url"])
|
|
url = doc.getURL()
|
|
if url:
|
|
cfg.remember(url, target_id, settings["base_url"])
|
|
|
|
hinweis = ("Das geöffnete Fenster ist jetzt dem Zieldokument "
|
|
"zugeordnet." if choice.get("switch") else
|
|
"Das geöffnete Fenster bleibt seiner bisherigen "
|
|
"Zuordnung erhalten.")
|
|
dlg.message(self.ctx, self.frame,
|
|
"Als neue Version angehängt.\n\n"
|
|
"Zieldokument: %s\nWurzel-ID: %d\nVersions-ID: %s\n"
|
|
"Bezeichnung: %s\n\n%s"
|
|
% ((target.get("title") or "")[:70], target_id, new_id,
|
|
label, hinweis))
|
|
|
|
# -- Ziel: neues Dokument ------------------------------------------------
|
|
|
|
def _create_new(self, client, settings, doc, doc_id, source, choice,
|
|
path, data):
|
|
title = choice["title"]
|
|
take_meta = choice.get("take_meta")
|
|
|
|
fields = {"title": title[:127]}
|
|
if take_meta and doc_id is not None:
|
|
for key in ("correspondent", "document_type", "storage_path"):
|
|
if source.get(key):
|
|
fields[key] = source[key]
|
|
if source.get("created"):
|
|
fields["created"] = str(source["created"])[:10]
|
|
|
|
task = client.post_document(os.path.basename(path), data, fields)
|
|
if not isinstance(task, str) or not task:
|
|
dlg.error(self.ctx, self.frame,
|
|
t("Unerwartete Antwort beim Hochladen:\n%r") % (task,))
|
|
return
|
|
new_id = client.wait_for_task(task)
|
|
|
|
# Tags und Zusatzfelder gehen nicht über post_document, sondern
|
|
# nachträglich per PATCH. Die Herkunftskennung wird dabei bewusst
|
|
# NICHT kopiert - sie gehört zum Ursprungsdokument.
|
|
if take_meta and doc_id is not None and new_id:
|
|
payload = {}
|
|
if source.get("tags"):
|
|
payload["tags"] = source["tags"]
|
|
if source.get("owner") is not None:
|
|
payload["owner"] = source["owner"]
|
|
cf = [f for f in (source.get("custom_fields") or [])
|
|
if not self._ist_herkunftsfeld(client, settings, f)]
|
|
if cf:
|
|
payload["custom_fields"] = cf
|
|
if payload:
|
|
try:
|
|
client.patch_document(new_id, payload)
|
|
except PaperlessError as exc:
|
|
dlg.message(self.ctx, self.frame,
|
|
"Dokument angelegt, aber die Klassifizierung "
|
|
"konnte nicht vollständig übernommen "
|
|
"werden:\n\n%s" % exc)
|
|
|
|
if choice.get("switch") and new_id:
|
|
cfg.set_document_id(doc, new_id, settings["base_url"])
|
|
url = doc.getURL()
|
|
if url:
|
|
cfg.remember(url, new_id, settings["base_url"])
|
|
|
|
hinweis = ("Das geöffnete Fenster ist jetzt diesem Dokument "
|
|
"zugeordnet." if choice.get("switch") else
|
|
"Das geöffnete Fenster bleibt dem Ursprungsdokument "
|
|
"zugeordnet.")
|
|
dlg.message(self.ctx, self.frame,
|
|
"Neues Dokument angelegt.\n\n"
|
|
"Titel: %s\nPaperless-ID: %s\n\n%s"
|
|
% (title[:70], new_id, hinweis))
|
|
|
|
# Ein frisch angelegtes Dokument trägt sonst nur den Titel. Ohne
|
|
# Klassifizierung findet es später niemand wieder, und nachträglich
|
|
# macht man es erfahrungsgemäß nicht mehr. Deshalb direkt anbieten.
|
|
if new_id:
|
|
self._classify(client, new_id)
|
|
|
|
def _ist_herkunftsfeld(self, client, settings, field_entry):
|
|
"""
|
|
Zusatzfelder mit Herkunftsbezug nicht in Kopien übernehmen.
|
|
|
|
Wer aus einem anderen System migriert hat, führt dort oft ein Feld
|
|
mit der Fremdkennung. Eine Kopie mit derselben Kennung wäre falsch -
|
|
es gäbe zwei Dokumente, die vorgeben, dasselbe Original zu sein.
|
|
|
|
Welche Feldnamen das betrifft, steht in den Einstellungen unter
|
|
"herkunftsfelder".
|
|
"""
|
|
muster = [str(m).lower() for m in
|
|
(settings.get("herkunftsfelder") or [])]
|
|
if not muster:
|
|
return False
|
|
try:
|
|
if not hasattr(self, "_cf_names"):
|
|
self._cf_names = {f["id"]: f["name"]
|
|
for f in client.custom_fields()}
|
|
name = (self._cf_names.get(field_entry.get("field")) or "").lower()
|
|
except Exception: # noqa: BLE001
|
|
return False
|
|
return any(m in name for m in muster)
|
|
|
|
# ------------------------------------------- Befehl: Klassifizierung
|
|
|
|
def cmd_classify(self):
|
|
client, _settings = self._client()
|
|
if client is None:
|
|
return
|
|
_doc, doc_id = self._current_doc_id()
|
|
if doc_id is None:
|
|
return
|
|
self._classify(client, doc_id)
|
|
|
|
def _classify(self, client, doc_id):
|
|
"""Klassifizierungsdialog für ein bestimmtes Dokument."""
|
|
try:
|
|
remote = client.document(doc_id)
|
|
tags = client.tags()
|
|
corrs = client.correspondents()
|
|
dtypes = client.document_types()
|
|
except PaperlessError as exc:
|
|
dlg.error(self.ctx, self.frame,
|
|
t("Stammdaten konnten nicht geladen werden:\n%s") % exc)
|
|
return
|
|
|
|
payload = dlg.classify_dialog(self.ctx, self.frame, remote,
|
|
tags, corrs, dtypes)
|
|
if payload is None:
|
|
return
|
|
|
|
# Korrespondent und Dokumenttyp können als Freitext eingegeben
|
|
# werden. Unbekannte Werte werden nach Rückfrage angelegt.
|
|
payload["correspondent"] = self._resolve(
|
|
client, "correspondents/", corrs,
|
|
payload.pop("_correspondent_name", ""), "Korrespondent")
|
|
payload["document_type"] = self._resolve(
|
|
client, "document_types/", dtypes,
|
|
payload.pop("_document_type_name", ""), "Dokumenttyp")
|
|
|
|
client.patch_document(doc_id, payload)
|
|
dlg.message(self.ctx, self.frame, "Klassifizierung gespeichert.")
|
|
|
|
def _resolve(self, client, path, known, name, label):
|
|
name = (name or "").strip()
|
|
if not name:
|
|
return None
|
|
for row in known:
|
|
if row["name"].strip().casefold() == name.casefold():
|
|
return row["id"]
|
|
if not dlg.confirm(self.ctx, self.frame,
|
|
"%s „%s“ existiert nicht.\n\nAnlegen?"
|
|
% (label, name)):
|
|
return None
|
|
return client.create(path, name)["id"]
|
|
|
|
# ---------------------------------------------------- Befehl: Teilen
|
|
|
|
def cmd_share(self):
|
|
"""
|
|
Erzeugt einen zeitlich begrenzten, unauthentifizierten Link.
|
|
|
|
Vorher wird geprüft, ob der geteilte Stand überhaupt im Archiv
|
|
liegt. Ein Link auf eine Fassung, die nur lokal existiert, wäre
|
|
irreführend - der Empfänger bekäme etwas anderes zu sehen, als der
|
|
Absender vor sich hat.
|
|
"""
|
|
client, settings = self._client()
|
|
if client is None:
|
|
return
|
|
doc, doc_id = self._current_doc_id(quiet=True)
|
|
if doc is None:
|
|
dlg.message(self.ctx, self.frame, "Kein Dokument geöffnet.")
|
|
return
|
|
|
|
# 1. Ist das Dokument überhaupt im Archiv?
|
|
if doc_id is None:
|
|
if dlg.confirm(
|
|
self.ctx, self.frame,
|
|
"Dieses Dokument liegt noch nicht in Paperless.\n\n"
|
|
"Ein Freigabelink kann erst erzeugt werden, wenn es "
|
|
"abgelegt ist.\n\nJetzt ablegen?"):
|
|
self.cmd_save_as()
|
|
return
|
|
|
|
# 2. Gibt es lokale Änderungen, die noch nicht abgelegt sind?
|
|
try:
|
|
geaendert = bool(doc.isModified())
|
|
except Exception: # noqa: BLE001
|
|
geaendert = False
|
|
if geaendert:
|
|
antwort = dlg.choice(
|
|
self.ctx, self.frame, "Nicht abgelegte Änderungen",
|
|
"Das geöffnete Dokument enthält Änderungen, die noch nicht "
|
|
"in Paperless abgelegt sind.\n\nDer Link würde auf die "
|
|
"zuletzt abgelegte Fassung zeigen, nicht auf das, was du "
|
|
"gerade vor dir hast.",
|
|
["Abbrechen", "Trotzdem teilen", "Erst neue Version ablegen"])
|
|
if antwort in (None, 0):
|
|
return
|
|
if antwort == 2:
|
|
self.cmd_save_version()
|
|
try:
|
|
if doc.isModified():
|
|
return # Ablegen wurde abgebrochen
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
bekannte = []
|
|
try:
|
|
bekannte = sorted(t["name"] for t in client.tags())
|
|
except PaperlessError:
|
|
pass
|
|
|
|
auswahl = dlg.share_dialog(self.ctx, self.frame,
|
|
settings.get("share_days", 7),
|
|
settings.get("share_tag", "Sharelink"),
|
|
bekannte)
|
|
if auswahl is None:
|
|
return
|
|
days, variant, tagname = auswahl
|
|
|
|
url, roh = client.share_link(doc_id, days, variant)
|
|
if not url:
|
|
dlg.error(self.ctx, self.frame,
|
|
"Der Link konnte nicht ermittelt werden.\n\n"
|
|
"Antwort: %r" % (roh,))
|
|
return
|
|
|
|
# 3. Kennzeichnung anfügen, ohne vorhandene Tags zu verlieren
|
|
tag_hinweis = ""
|
|
if tagname:
|
|
try:
|
|
tag_id = self._resolve(client, "tags/", client.tags(),
|
|
tagname, "Tag")
|
|
if tag_id:
|
|
aktuell = client.document(doc_id).get("tags") or []
|
|
if tag_id not in aktuell:
|
|
client.patch_document(
|
|
doc_id, {"tags": list(aktuell) + [tag_id]})
|
|
tag_hinweis = "\nTag „%s“ gesetzt." % tagname
|
|
# Für den nächsten Aufruf merken
|
|
settings["share_tag"] = tagname
|
|
cfg.save_settings(settings)
|
|
except PaperlessError as exc:
|
|
tag_hinweis = "\nTag konnte nicht gesetzt werden: %s" % exc
|
|
|
|
gueltig = ("%d Tage" % days) if days else "unbegrenzt"
|
|
dlg.result_dialog(
|
|
self.ctx, self.frame, "Freigabelink erzeugt",
|
|
"Der Link ist ohne Anmeldung abrufbar.\nGültigkeit: %s%s"
|
|
% (gueltig, tag_hinweis), url)
|
|
|
|
# ------------------------------------------ Befehl: Einstellungen
|
|
|
|
def cmd_settings(self):
|
|
current = cfg.load_settings()
|
|
updated = dlg.settings_dialog(self.ctx, self.frame, current)
|
|
if updated is None:
|
|
return
|
|
cfg.save_settings(updated)
|
|
if not cfg.settings_complete(updated):
|
|
return
|
|
try:
|
|
PaperlessClient(updated["base_url"], updated["token"],
|
|
updated.get("verify_tls", True),
|
|
user_agent=updated.get("user_agent") or None).ping()
|
|
dlg.message(self.ctx, self.frame,
|
|
"Verbindung erfolgreich hergestellt.")
|
|
except PaperlessError as exc:
|
|
dlg.error(self.ctx, self.frame,
|
|
t("Gespeichert, aber die Verbindung schlug fehl:\n\n%s") % exc)
|
|
|
|
# ------------------------------------------------------ Befehl: Info
|
|
|
|
def cmd_info(self):
|
|
client, settings = self._client(require=False)
|
|
doc, doc_id = self._current_doc_id(quiet=True)
|
|
|
|
lines = ["Basisadresse: %s" % (settings.get("base_url") or "—")]
|
|
if doc is None:
|
|
lines.append("Kein Dokument geöffnet.")
|
|
elif doc_id is None:
|
|
lines.append("Dieses Dokument stammt nicht aus Paperless.")
|
|
elif client is None:
|
|
lines.append("Paperless-ID: %d" % doc_id)
|
|
else:
|
|
try:
|
|
r = client.document(doc_id)
|
|
versions = r.get("versions") or []
|
|
lines += [
|
|
"Paperless-ID: %d" % doc_id,
|
|
"Titel: %s" % (r.get("title") or "—"),
|
|
"Archiv-Seriennummer: %s"
|
|
% (r.get("archive_serial_number") or "—"),
|
|
"Versionen: %d" % (len(versions) or 1),
|
|
"Erstellt: %s" % ((r.get("created") or "")[:10] or "—"),
|
|
]
|
|
except PaperlessError as exc:
|
|
lines.append("Abruf fehlgeschlagen: %s" % exc)
|
|
|
|
dlg.result_dialog(self.ctx, self.frame, "Paperless — Status",
|
|
"\n".join(lines))
|
|
|
|
|
|
g_ImplementationHelper = unohelper.ImplementationHelper()
|
|
g_ImplementationHelper.addImplementation(PaperlessAddon, IMPL_NAME,
|
|
(SERVICE_NAME,))
|