]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Merge models
authorshamoon <4887959+shamoon@users.noreply.github.com>
Tue, 8 Apr 2025 23:18:32 +0000 (16:18 -0700)
committershamoon <4887959+shamoon@users.noreply.github.com>
Tue, 8 Apr 2025 23:18:32 +0000 (16:18 -0700)
75 files changed:
src/documents/admin.py
src/documents/barcodes.py
src/documents/bulk_download.py
src/documents/bulk_edit.py
src/documents/caching.py
src/documents/checks.py
src/documents/classifier.py
src/documents/conditionals.py
src/documents/consumer.py
src/documents/context_processors.py
src/documents/file_handling.py
src/documents/filters.py
src/documents/index.py
src/documents/management/commands/convert_mariadb_uuid.py
src/documents/management/commands/decrypt_documents.py
src/documents/management/commands/document_archiver.py
src/documents/management/commands/document_consumer.py
src/documents/management/commands/document_exporter.py
src/documents/management/commands/document_fuzzy_match.py
src/documents/management/commands/document_importer.py
src/documents/management/commands/document_renamer.py
src/documents/management/commands/document_retagger.py
src/documents/management/commands/document_thumbnails.py
src/documents/matching.py
src/documents/models.py [deleted file]
src/documents/sanity_checker.py
src/documents/serialisers.py
src/documents/signals/handlers.py
src/documents/tasks.py
src/documents/templating/filepath.py
src/documents/tests/factories.py
src/documents/tests/test_admin.py
src/documents/tests/test_api_bulk_download.py
src/documents/tests/test_api_bulk_edit.py
src/documents/tests/test_api_custom_fields.py
src/documents/tests/test_api_documents.py
src/documents/tests/test_api_filter_by_custom_fields.py
src/documents/tests/test_api_objects.py
src/documents/tests/test_api_permissions.py
src/documents/tests/test_api_search.py
src/documents/tests/test_api_status.py
src/documents/tests/test_api_tasks.py
src/documents/tests/test_api_trash.py
src/documents/tests/test_api_workflows.py
src/documents/tests/test_barcodes.py
src/documents/tests/test_bulk_edit.py
src/documents/tests/test_checks.py
src/documents/tests/test_classifier.py
src/documents/tests/test_consumer.py
src/documents/tests/test_delayedquery.py
src/documents/tests/test_document_model.py
src/documents/tests/test_file_handling.py
src/documents/tests/test_index.py
src/documents/tests/test_management.py
src/documents/tests/test_management_consumer.py
src/documents/tests/test_management_exporter.py
src/documents/tests/test_management_fuzzy.py
src/documents/tests/test_management_importer.py
src/documents/tests/test_management_retagger.py
src/documents/tests/test_management_thumbnails.py
src/documents/tests/test_matchables.py
src/documents/tests/test_migration_storage_path_template.py
src/documents/tests/test_models.py
src/documents/tests/test_sanity_check.py
src/documents/tests/test_task_signals.py
src/documents/tests/test_tasks.py
src/documents/tests/test_views.py
src/documents/tests/test_workflows.py
src/documents/views.py
src/paperless/adapter.py
src/paperless/models.py
src/paperless_mail/mail.py
src/paperless_mail/models.py
src/paperless_mail/tests/test_api.py
src/paperless_mail/tests/test_mail.py

index 59cbf185334779eb43196de687f5f27b26bf3d47..2ef908d065b39ecbb3fb5bfde55f2ff382cbf05e 100644 (file)
@@ -2,18 +2,18 @@ from django.conf import settings
 from django.contrib import admin
 from guardian.admin import GuardedModelAdmin
 
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import Note
-from documents.models import PaperlessTask
-from documents.models import SavedView
-from documents.models import SavedViewFilterRule
-from documents.models import ShareLink
-from documents.models import StoragePath
-from documents.models import Tag
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import Note
+from paperless.models import PaperlessTask
+from paperless.models import SavedView
+from paperless.models import SavedViewFilterRule
+from paperless.models import ShareLink
+from paperless.models import StoragePath
+from paperless.models import Tag
 
 if settings.AUDIT_LOG_ENABLED:
     from auditlog.admin import LogEntryAdmin
index 3b0c1d33bba29f0c35c75887d684ce1833231e58..11a7dc6b33be7a4ef8e3024e95fbf90bc4937dcb 100644 (file)
@@ -15,13 +15,13 @@ from pikepdf import Pdf
 
 from documents.converters import convert_from_tiff_to_pdf
 from documents.data_models import ConsumableDocument
-from documents.models import Tag
 from documents.plugins.base import ConsumeTaskPlugin
 from documents.plugins.base import StopConsumeTaskError
 from documents.plugins.helpers import ProgressStatusOptions
 from documents.utils import copy_basic_file_stats
 from documents.utils import copy_file_with_basic_stats
 from documents.utils import maybe_override_pixel_limit
+from paperless.models import Tag
 
 if TYPE_CHECKING:
     from collections.abc import Callable
index 7e87f048808eba65d44d04590e0dd20ef19cc8af..94c3f2d637ea310a77f162d2465a1ce785a110a1 100644 (file)
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
     from collections.abc import Callable
     from zipfile import ZipFile
 
-    from documents.models import Document
+    from paperless.models import Document
 
 
 class BulkArchiveStrategy:
index b8e76f7c800910770587aee215a04a186d71c878..d95e32420ebdef688aac9cc497c22bb93ad7af0e 100644 (file)
@@ -19,17 +19,17 @@ from django.utils import timezone
 from documents.data_models import ConsumableDocument
 from documents.data_models import DocumentMetadataOverrides
 from documents.data_models import DocumentSource
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import StoragePath
 from documents.permissions import set_permissions_for_object
 from documents.plugins.helpers import DocumentsStatusManager
 from documents.tasks import bulk_update_documents
 from documents.tasks import consume_file
 from documents.tasks import update_document_content_maybe_archive_file
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import StoragePath
 
 if TYPE_CHECKING:
     from django.contrib.auth.models import User
index 1099a7a73d976176d58e54a85b5f16c38cae86e6..a8c3bf923474d682e9211e85d4bc9ceb647fd87f 100644 (file)
@@ -8,7 +8,7 @@ from typing import Final
 
 from django.core.cache import cache
 
-from documents.models import Document
+from paperless.models import Document
 
 if TYPE_CHECKING:
     from documents.classifier import DocumentClassifier
index 8f8fbf4f9f2857cfcb286f20c85e855bbdec6b30..f0a5996f7525d19495bb823da699924a539eeb90 100644 (file)
@@ -14,8 +14,8 @@ from documents.templating.utils import convert_format_str_to_template_format
 
 @register()
 def changed_password_check(app_configs, **kwargs):
-    from documents.models import Document
     from paperless.db import GnuPG
+    from paperless.models import Document
 
     try:
         encrypted_doc = (
index 728c8322898377c1b319ff6d6aa0d5258dd52720..042cba1a5f24bafcdc96fd9b9f3cca28722e6eee 100644 (file)
@@ -21,8 +21,8 @@ from documents.caching import CACHE_50_MINUTES
 from documents.caching import CLASSIFIER_HASH_KEY
 from documents.caching import CLASSIFIER_MODIFIED_KEY
 from documents.caching import CLASSIFIER_VERSION_KEY
-from documents.models import Document
-from documents.models import MatchingModel
+from paperless.models import Document
+from paperless.models import MatchingModel
 
 logger = logging.getLogger("paperless.classifier")
 
index 47d9bfe4b184f68d47ba576231c13f4e62e5fad2..d843391cee116fbbf23cf294abdc303c49ee2b4f 100644 (file)
@@ -11,7 +11,7 @@ from documents.caching import CLASSIFIER_MODIFIED_KEY
 from documents.caching import CLASSIFIER_VERSION_KEY
 from documents.caching import get_thumbnail_modified_key
 from documents.classifier import DocumentClassifier
-from documents.models import Document
+from paperless.models import Document
 
 
 def suggestions_etag(request, pk: int) -> str | None:
index 04ba588d4244aa2a73e1b5eb56fc77896298f788..1b0ed4459b7bb848a219cfe13c095ccec87ba1a1 100644 (file)
@@ -21,14 +21,6 @@ from documents.data_models import DocumentMetadataOverrides
 from documents.file_handling import create_source_path_directory
 from documents.file_handling import generate_unique_filename
 from documents.loggers import LoggingMixin
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import StoragePath
-from documents.models import Tag
-from documents.models import WorkflowTrigger
 from documents.parsers import DocumentParser
 from documents.parsers import ParseError
 from documents.parsers import get_parser_class_for_mime_type
@@ -47,6 +39,14 @@ from documents.templating.workflows import parse_w_workflow_placeholders
 from documents.utils import copy_basic_file_stats
 from documents.utils import copy_file_with_basic_stats
 from documents.utils import run_subprocess
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import StoragePath
+from paperless.models import Tag
+from paperless.models import WorkflowTrigger
 from paperless_mail.parsers import MailDocumentParser
 
 
index d083aaf369df7279c8818053d9f819ff295d80e1..8d2d65f5bbf3667d5daf8b1b6fee84a48c0e3c07 100644 (file)
@@ -1,8 +1,8 @@
 from django.conf import settings as django_settings
 from django.contrib.auth.models import User
 
-from documents.models import Document
 from paperless.config import GeneralConfig
+from paperless.models import Document
 
 
 def settings(request):
index 3d1a643dfebcb8c919add324282d8c48301ac368..03e451257e21b4b585ddd87e8861ce424852c49a 100644 (file)
@@ -2,9 +2,9 @@ import os
 
 from django.conf import settings
 
-from documents.models import Document
 from documents.templating.filepath import validate_filepath_template_and_render
 from documents.templating.utils import convert_format_str_to_template_format
+from paperless.models import Document
 
 
 def create_source_path_directory(source_path):
index 90161a1e61a4e35e65c72df4df630236513a2a2c..a1c9917a8862396771e1b7283c66533be2047f4e 100644 (file)
@@ -31,15 +31,15 @@ from rest_framework import serializers
 from rest_framework.filters import OrderingFilter
 from rest_framework_guardian.filters import ObjectPermissionsFilter
 
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import PaperlessTask
-from documents.models import ShareLink
-from documents.models import StoragePath
-from documents.models import Tag
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import PaperlessTask
+from paperless.models import ShareLink
+from paperless.models import StoragePath
+from paperless.models import Tag
 
 if TYPE_CHECKING:
     from collections.abc import Callable
index 9b3a1724c8fc9382b1efffc6c1c6a1118fc7be0b..32aa54b1d10cec41fb9f7544741997528c4c06e5 100644 (file)
@@ -38,10 +38,10 @@ from whoosh.scoring import TF_IDF
 from whoosh.util.times import timespan
 from whoosh.writing import AsyncWriter
 
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import Note
-from documents.models import User
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import Note
+from paperless.models import User
 
 if TYPE_CHECKING:
     from django.db.models import QuerySet
index 76ccf9e76284edb25a42411f915fb76764257a04..779494e7e1eda577deff4c8cfe8fe4ea52555491 100644 (file)
@@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand
 from django.db import connection
 from django.db import models
 
-from documents.models import Document
+from paperless.models import Document
 
 
 class Command(BaseCommand):
index 793cac4bb24f6325b21ac00331b917fbc0e65afa..906bc1176c9a9f241dea1bea49ef62081165d3de 100644 (file)
@@ -4,8 +4,8 @@ from django.conf import settings
 from django.core.management.base import BaseCommand
 from django.core.management.base import CommandError
 
-from documents.models import Document
 from paperless.db import GnuPG
+from paperless.models import Document
 
 
 class Command(BaseCommand):
index 1aa52117a3a9571ca01f6410c64f15fc6739e738..298a3a92f9fb969bea8aa341e7228046ea0d83a7 100644 (file)
@@ -8,8 +8,8 @@ from django.core.management.base import BaseCommand
 
 from documents.management.commands.mixins import MultiProcessMixin
 from documents.management.commands.mixins import ProgressBarMixin
-from documents.models import Document
 from documents.tasks import update_document_content_maybe_archive_file
+from paperless.models import Document
 
 logger = logging.getLogger("paperless.management.archiver")
 
index 1e98533f028d11b5818eb7847eed276c38993e99..3f15e6a04aabc4d2f3bb7f1e91d6eb517c7e9377 100644 (file)
@@ -19,9 +19,9 @@ from watchdog.observers.polling import PollingObserver
 from documents.data_models import ConsumableDocument
 from documents.data_models import DocumentMetadataOverrides
 from documents.data_models import DocumentSource
-from documents.models import Tag
 from documents.parsers import is_file_ext_supported
 from documents.tasks import consume_file
+from paperless.models import Tag
 
 try:
     from inotifyrecursive import INotify
index 6dc89479e1f1d9d409eebfa5510412e50427f4bf..d67b0d337bdebcaa2bcb005c081f4a08d8845581 100644 (file)
@@ -35,22 +35,6 @@ if settings.AUDIT_LOG_ENABLED:
 from documents.file_handling import delete_empty_directories
 from documents.file_handling import generate_filename
 from documents.management.commands.mixins import CryptMixin
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import Note
-from documents.models import SavedView
-from documents.models import SavedViewFilterRule
-from documents.models import StoragePath
-from documents.models import Tag
-from documents.models import UiSettings
-from documents.models import Workflow
-from documents.models import WorkflowAction
-from documents.models import WorkflowActionEmail
-from documents.models import WorkflowActionWebhook
-from documents.models import WorkflowTrigger
 from documents.settings import EXPORTER_ARCHIVE_NAME
 from documents.settings import EXPORTER_FILE_NAME
 from documents.settings import EXPORTER_THUMBNAIL_NAME
@@ -58,6 +42,22 @@ from documents.utils import copy_file_with_basic_stats
 from paperless import version
 from paperless.db import GnuPG
 from paperless.models import ApplicationConfiguration
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import Note
+from paperless.models import SavedView
+from paperless.models import SavedViewFilterRule
+from paperless.models import StoragePath
+from paperless.models import Tag
+from paperless.models import UiSettings
+from paperless.models import Workflow
+from paperless.models import WorkflowAction
+from paperless.models import WorkflowActionEmail
+from paperless.models import WorkflowActionWebhook
+from paperless.models import WorkflowTrigger
 from paperless_mail.models import MailAccount
 from paperless_mail.models import MailRule
 
index 9e01ff1b06cb850a4d86b51d3390e1fa4389b23d..1197fdececfdd0fa6fb327a40bf2cf8833ea4580 100644 (file)
@@ -9,7 +9,7 @@ from django.core.management import CommandError
 
 from documents.management.commands.mixins import MultiProcessMixin
 from documents.management.commands.mixins import ProgressBarMixin
-from documents.models import Document
+from paperless.models import Document
 
 
 @dataclasses.dataclass(frozen=True)
index 9e3af47e772af020951d1fc70fa476a2f6bf11c1..f58af53bb4c403c92e7524be9b08c3806bfe2d73 100644 (file)
@@ -23,13 +23,6 @@ from filelock import FileLock
 
 from documents.file_handling import create_source_path_directory
 from documents.management.commands.mixins import CryptMixin
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import Note
-from documents.models import Tag
 from documents.parsers import run_convert
 from documents.settings import EXPORTER_ARCHIVE_NAME
 from documents.settings import EXPORTER_CRYPTO_SETTINGS_NAME
@@ -39,6 +32,13 @@ from documents.signals.handlers import check_paths_and_prune_custom_fields
 from documents.signals.handlers import update_filename_and_move_files
 from documents.utils import copy_file_with_basic_stats
 from paperless import version
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import Note
+from paperless.models import Tag
 
 if settings.AUDIT_LOG_ENABLED:
     from auditlog.registry import auditlog
index 2dfca217e87586b1476b623cd97422f4b6ac7124..76dcb69cbe798ef189093c000b1be50ffbf5d4f0 100644 (file)
@@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand
 from django.db.models.signals import post_save
 
 from documents.management.commands.mixins import ProgressBarMixin
-from documents.models import Document
+from paperless.models import Document
 
 
 class Command(ProgressBarMixin, BaseCommand):
index 10bb54b711a3507782b090e9d65eec172047f88d..de51f2368c90c89e35ae38669eb82b014e9553dc 100644 (file)
@@ -5,11 +5,11 @@ from django.core.management.base import BaseCommand
 
 from documents.classifier import load_classifier
 from documents.management.commands.mixins import ProgressBarMixin
-from documents.models import Document
 from documents.signals.handlers import set_correspondent
 from documents.signals.handlers import set_document_type
 from documents.signals.handlers import set_storage_path
 from documents.signals.handlers import set_tags
+from paperless.models import Document
 
 logger = logging.getLogger("paperless.management.retagger")
 
index d4653f0b3639e5a5c59d072a96949a6579ed25cc..def58c1d8d40546542c55bacda6f7640d3d8f68f 100644 (file)
@@ -8,8 +8,8 @@ from django.core.management.base import BaseCommand
 
 from documents.management.commands.mixins import MultiProcessMixin
 from documents.management.commands.mixins import ProgressBarMixin
-from documents.models import Document
 from documents.parsers import get_parser_class_for_mime_type
+from paperless.models import Document
 
 
 def _process_document(doc_id):
index ab3866518e31fbf72e154a34e6e8296f85c43bcc..e01ca7eca92320dfb97c1adff163531b2fc8ef10 100644 (file)
@@ -7,15 +7,15 @@ from typing import TYPE_CHECKING
 
 from documents.data_models import ConsumableDocument
 from documents.data_models import DocumentSource
-from documents.models import Correspondent
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import MatchingModel
-from documents.models import StoragePath
-from documents.models import Tag
-from documents.models import Workflow
-from documents.models import WorkflowTrigger
 from documents.permissions import get_objects_for_user_owner_aware
+from paperless.models import Correspondent
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import MatchingModel
+from paperless.models import StoragePath
+from paperless.models import Tag
+from paperless.models import Workflow
+from paperless.models import WorkflowTrigger
 
 if TYPE_CHECKING:
     from documents.classifier import DocumentClassifier
diff --git a/src/documents/models.py b/src/documents/models.py
deleted file mode 100644 (file)
index 4b3f97e..0000000
+++ /dev/null
@@ -1,1473 +0,0 @@
-import datetime
-from pathlib import Path
-from typing import Final
-
-import pathvalidate
-from celery import states
-from django.conf import settings
-from django.contrib.auth.models import Group
-from django.contrib.auth.models import User
-from django.core.validators import MaxValueValidator
-from django.core.validators import MinValueValidator
-from django.db import models
-from django.utils import timezone
-from django.utils.translation import gettext_lazy as _
-from multiselectfield import MultiSelectField
-
-if settings.AUDIT_LOG_ENABLED:
-    from auditlog.registry import auditlog
-
-from django.db.models import Case
-from django.db.models.functions import Cast
-from django.db.models.functions import Substr
-from django_softdelete.models import SoftDeleteModel
-
-from documents.data_models import DocumentSource
-from documents.parsers import get_default_file_extension
-
-
-class ModelWithOwner(models.Model):
-    owner = models.ForeignKey(
-        User,
-        blank=True,
-        null=True,
-        default=None,
-        on_delete=models.SET_NULL,
-        verbose_name=_("owner"),
-    )
-
-    class Meta:
-        abstract = True
-
-
-class MatchingModel(ModelWithOwner):
-    MATCH_NONE = 0
-    MATCH_ANY = 1
-    MATCH_ALL = 2
-    MATCH_LITERAL = 3
-    MATCH_REGEX = 4
-    MATCH_FUZZY = 5
-    MATCH_AUTO = 6
-
-    MATCHING_ALGORITHMS = (
-        (MATCH_NONE, _("None")),
-        (MATCH_ANY, _("Any word")),
-        (MATCH_ALL, _("All words")),
-        (MATCH_LITERAL, _("Exact match")),
-        (MATCH_REGEX, _("Regular expression")),
-        (MATCH_FUZZY, _("Fuzzy word")),
-        (MATCH_AUTO, _("Automatic")),
-    )
-
-    name = models.CharField(_("name"), max_length=128)
-
-    match = models.CharField(_("match"), max_length=256, blank=True)
-
-    matching_algorithm = models.PositiveIntegerField(
-        _("matching algorithm"),
-        choices=MATCHING_ALGORITHMS,
-        default=MATCH_ANY,
-    )
-
-    is_insensitive = models.BooleanField(_("is insensitive"), default=True)
-
-    class Meta:
-        abstract = True
-        ordering = ("name",)
-        constraints = [
-            models.UniqueConstraint(
-                fields=["name", "owner"],
-                name="%(app_label)s_%(class)s_unique_name_owner",
-            ),
-            models.UniqueConstraint(
-                name="%(app_label)s_%(class)s_name_uniq",
-                fields=["name"],
-                condition=models.Q(owner__isnull=True),
-            ),
-        ]
-
-    def __str__(self):
-        return self.name
-
-
-class Correspondent(MatchingModel):
-    class Meta(MatchingModel.Meta):
-        verbose_name = _("correspondent")
-        verbose_name_plural = _("correspondents")
-
-
-class Tag(MatchingModel):
-    color = models.CharField(_("color"), max_length=7, default="#a6cee3")
-
-    is_inbox_tag = models.BooleanField(
-        _("is inbox tag"),
-        default=False,
-        help_text=_(
-            "Marks this tag as an inbox tag: All newly consumed "
-            "documents will be tagged with inbox tags.",
-        ),
-    )
-
-    class Meta(MatchingModel.Meta):
-        verbose_name = _("tag")
-        verbose_name_plural = _("tags")
-
-
-class DocumentType(MatchingModel):
-    class Meta(MatchingModel.Meta):
-        verbose_name = _("document type")
-        verbose_name_plural = _("document types")
-
-
-class StoragePath(MatchingModel):
-    path = models.TextField(
-        _("path"),
-    )
-
-    class Meta(MatchingModel.Meta):
-        verbose_name = _("storage path")
-        verbose_name_plural = _("storage paths")
-
-
-class Document(SoftDeleteModel, ModelWithOwner):
-    STORAGE_TYPE_UNENCRYPTED = "unencrypted"
-    STORAGE_TYPE_GPG = "gpg"
-    STORAGE_TYPES = (
-        (STORAGE_TYPE_UNENCRYPTED, _("Unencrypted")),
-        (STORAGE_TYPE_GPG, _("Encrypted with GNU Privacy Guard")),
-    )
-
-    correspondent = models.ForeignKey(
-        Correspondent,
-        blank=True,
-        null=True,
-        related_name="documents",
-        on_delete=models.SET_NULL,
-        verbose_name=_("correspondent"),
-    )
-
-    storage_path = models.ForeignKey(
-        StoragePath,
-        blank=True,
-        null=True,
-        related_name="documents",
-        on_delete=models.SET_NULL,
-        verbose_name=_("storage path"),
-    )
-
-    title = models.CharField(_("title"), max_length=128, blank=True, db_index=True)
-
-    document_type = models.ForeignKey(
-        DocumentType,
-        blank=True,
-        null=True,
-        related_name="documents",
-        on_delete=models.SET_NULL,
-        verbose_name=_("document type"),
-    )
-
-    content = models.TextField(
-        _("content"),
-        blank=True,
-        help_text=_(
-            "The raw, text-only data of the document. This field is "
-            "primarily used for searching.",
-        ),
-    )
-
-    mime_type = models.CharField(_("mime type"), max_length=256, editable=False)
-
-    tags = models.ManyToManyField(
-        Tag,
-        related_name="documents",
-        blank=True,
-        verbose_name=_("tags"),
-    )
-
-    checksum = models.CharField(
-        _("checksum"),
-        max_length=32,
-        editable=False,
-        unique=True,
-        help_text=_("The checksum of the original document."),
-    )
-
-    archive_checksum = models.CharField(
-        _("archive checksum"),
-        max_length=32,
-        editable=False,
-        blank=True,
-        null=True,
-        help_text=_("The checksum of the archived document."),
-    )
-
-    page_count = models.PositiveIntegerField(
-        _("page count"),
-        blank=False,
-        null=True,
-        unique=False,
-        db_index=False,
-        validators=[MinValueValidator(1)],
-        help_text=_(
-            "The number of pages of the document.",
-        ),
-    )
-
-    created = models.DateTimeField(_("created"), default=timezone.now, db_index=True)
-
-    modified = models.DateTimeField(
-        _("modified"),
-        auto_now=True,
-        editable=False,
-        db_index=True,
-    )
-
-    storage_type = models.CharField(
-        _("storage type"),
-        max_length=11,
-        choices=STORAGE_TYPES,
-        default=STORAGE_TYPE_UNENCRYPTED,
-        editable=False,
-    )
-
-    added = models.DateTimeField(
-        _("added"),
-        default=timezone.now,
-        editable=False,
-        db_index=True,
-    )
-
-    filename = models.FilePathField(
-        _("filename"),
-        max_length=1024,
-        editable=False,
-        default=None,
-        unique=True,
-        null=True,
-        help_text=_("Current filename in storage"),
-    )
-
-    archive_filename = models.FilePathField(
-        _("archive filename"),
-        max_length=1024,
-        editable=False,
-        default=None,
-        unique=True,
-        null=True,
-        help_text=_("Current archive filename in storage"),
-    )
-
-    original_filename = models.CharField(
-        _("original filename"),
-        max_length=1024,
-        editable=False,
-        default=None,
-        unique=False,
-        null=True,
-        help_text=_("The original name of the file when it was uploaded"),
-    )
-
-    ARCHIVE_SERIAL_NUMBER_MIN: Final[int] = 0
-    ARCHIVE_SERIAL_NUMBER_MAX: Final[int] = 0xFF_FF_FF_FF
-
-    archive_serial_number = models.PositiveIntegerField(
-        _("archive serial number"),
-        blank=True,
-        null=True,
-        unique=True,
-        db_index=True,
-        validators=[
-            MaxValueValidator(ARCHIVE_SERIAL_NUMBER_MAX),
-            MinValueValidator(ARCHIVE_SERIAL_NUMBER_MIN),
-        ],
-        help_text=_(
-            "The position of this document in your physical document archive.",
-        ),
-    )
-
-    class Meta:
-        ordering = ("-created",)
-        verbose_name = _("document")
-        verbose_name_plural = _("documents")
-
-    def __str__(self) -> str:
-        # Convert UTC database time to local time
-        created = datetime.date.isoformat(timezone.localdate(self.created))
-
-        res = f"{created}"
-
-        if self.correspondent:
-            res += f" {self.correspondent}"
-        if self.title:
-            res += f" {self.title}"
-        return res
-
-    @property
-    def source_path(self) -> Path:
-        if self.filename:
-            fname = str(self.filename)
-        else:
-            fname = f"{self.pk:07}{self.file_type}"
-            if self.storage_type == self.STORAGE_TYPE_GPG:
-                fname += ".gpg"  # pragma: no cover
-
-        return (settings.ORIGINALS_DIR / Path(fname)).resolve()
-
-    @property
-    def source_file(self):
-        return Path(self.source_path).open("rb")
-
-    @property
-    def has_archive_version(self) -> bool:
-        return self.archive_filename is not None
-
-    @property
-    def archive_path(self) -> Path | None:
-        if self.has_archive_version:
-            return (settings.ARCHIVE_DIR / Path(str(self.archive_filename))).resolve()
-        else:
-            return None
-
-    @property
-    def archive_file(self):
-        return Path(self.archive_path).open("rb")
-
-    def get_public_filename(self, *, archive=False, counter=0, suffix=None) -> str:
-        """
-        Returns a sanitized filename for the document, not including any paths.
-        """
-        result = str(self)
-
-        if counter:
-            result += f"_{counter:02}"
-
-        if suffix:
-            result += suffix
-
-        if archive:
-            result += ".pdf"
-        else:
-            result += self.file_type
-
-        return pathvalidate.sanitize_filename(result, replacement_text="-")
-
-    @property
-    def file_type(self):
-        return get_default_file_extension(self.mime_type)
-
-    @property
-    def thumbnail_path(self) -> Path:
-        webp_file_name = f"{self.pk:07}.webp"
-        if self.storage_type == self.STORAGE_TYPE_GPG:
-            webp_file_name += ".gpg"
-
-        webp_file_path = settings.THUMBNAIL_DIR / Path(webp_file_name)
-
-        return webp_file_path.resolve()
-
-    @property
-    def thumbnail_file(self):
-        return Path(self.thumbnail_path).open("rb")
-
-    @property
-    def created_date(self):
-        return timezone.localdate(self.created)
-
-
-class SavedView(ModelWithOwner):
-    class DisplayMode(models.TextChoices):
-        TABLE = ("table", _("Table"))
-        SMALL_CARDS = ("smallCards", _("Small Cards"))
-        LARGE_CARDS = ("largeCards", _("Large Cards"))
-
-    class DisplayFields(models.TextChoices):
-        TITLE = ("title", _("Title"))
-        CREATED = ("created", _("Created"))
-        ADDED = ("added", _("Added"))
-        TAGS = ("tag"), _("Tags")
-        CORRESPONDENT = ("correspondent", _("Correspondent"))
-        DOCUMENT_TYPE = ("documenttype", _("Document Type"))
-        STORAGE_PATH = ("storagepath", _("Storage Path"))
-        NOTES = ("note", _("Note"))
-        OWNER = ("owner", _("Owner"))
-        SHARED = ("shared", _("Shared"))
-        ASN = ("asn", _("ASN"))
-        PAGE_COUNT = ("pagecount", _("Pages"))
-        CUSTOM_FIELD = ("custom_field_%d", ("Custom Field"))
-
-    name = models.CharField(_("name"), max_length=128)
-
-    show_on_dashboard = models.BooleanField(
-        _("show on dashboard"),
-    )
-    show_in_sidebar = models.BooleanField(
-        _("show in sidebar"),
-    )
-
-    sort_field = models.CharField(
-        _("sort field"),
-        max_length=128,
-        null=True,
-        blank=True,
-    )
-    sort_reverse = models.BooleanField(_("sort reverse"), default=False)
-
-    page_size = models.PositiveIntegerField(
-        _("View page size"),
-        null=True,
-        blank=True,
-        validators=[MinValueValidator(1)],
-    )
-
-    display_mode = models.CharField(
-        max_length=128,
-        verbose_name=_("View display mode"),
-        choices=DisplayMode.choices,
-        null=True,
-        blank=True,
-    )
-
-    display_fields = models.JSONField(
-        verbose_name=_("Document display fields"),
-        null=True,
-        blank=True,
-    )
-
-    class Meta:
-        ordering = ("name",)
-        verbose_name = _("saved view")
-        verbose_name_plural = _("saved views")
-
-    def __str__(self):
-        return f"SavedView {self.name}"
-
-
-class SavedViewFilterRule(models.Model):
-    RULE_TYPES = [
-        (0, _("title contains")),
-        (1, _("content contains")),
-        (2, _("ASN is")),
-        (3, _("correspondent is")),
-        (4, _("document type is")),
-        (5, _("is in inbox")),
-        (6, _("has tag")),
-        (7, _("has any tag")),
-        (8, _("created before")),
-        (9, _("created after")),
-        (10, _("created year is")),
-        (11, _("created month is")),
-        (12, _("created day is")),
-        (13, _("added before")),
-        (14, _("added after")),
-        (15, _("modified before")),
-        (16, _("modified after")),
-        (17, _("does not have tag")),
-        (18, _("does not have ASN")),
-        (19, _("title or content contains")),
-        (20, _("fulltext query")),
-        (21, _("more like this")),
-        (22, _("has tags in")),
-        (23, _("ASN greater than")),
-        (24, _("ASN less than")),
-        (25, _("storage path is")),
-        (26, _("has correspondent in")),
-        (27, _("does not have correspondent in")),
-        (28, _("has document type in")),
-        (29, _("does not have document type in")),
-        (30, _("has storage path in")),
-        (31, _("does not have storage path in")),
-        (32, _("owner is")),
-        (33, _("has owner in")),
-        (34, _("does not have owner")),
-        (35, _("does not have owner in")),
-        (36, _("has custom field value")),
-        (37, _("is shared by me")),
-        (38, _("has custom fields")),
-        (39, _("has custom field in")),
-        (40, _("does not have custom field in")),
-        (41, _("does not have custom field")),
-        (42, _("custom fields query")),
-        (43, _("created to")),
-        (44, _("created from")),
-        (45, _("added to")),
-        (46, _("added from")),
-        (47, _("mime type is")),
-    ]
-
-    saved_view = models.ForeignKey(
-        SavedView,
-        on_delete=models.CASCADE,
-        related_name="filter_rules",
-        verbose_name=_("saved view"),
-    )
-
-    rule_type = models.PositiveIntegerField(_("rule type"), choices=RULE_TYPES)
-
-    value = models.CharField(_("value"), max_length=255, blank=True, null=True)
-
-    class Meta:
-        verbose_name = _("filter rule")
-        verbose_name_plural = _("filter rules")
-
-    def __str__(self) -> str:
-        return f"SavedViewFilterRule: {self.rule_type} : {self.value}"
-
-
-# Extending User Model Using a One-To-One Link
-class UiSettings(models.Model):
-    user = models.OneToOneField(
-        User,
-        on_delete=models.CASCADE,
-        related_name="ui_settings",
-    )
-    settings = models.JSONField(null=True)
-
-    def __str__(self):
-        return self.user.username
-
-
-class PaperlessTask(ModelWithOwner):
-    ALL_STATES = sorted(states.ALL_STATES)
-    TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))
-
-    class TaskType(models.TextChoices):
-        AUTO = ("auto_task", _("Auto Task"))
-        SCHEDULED_TASK = ("scheduled_task", _("Scheduled Task"))
-        MANUAL_TASK = ("manual_task", _("Manual Task"))
-
-    class TaskName(models.TextChoices):
-        CONSUME_FILE = ("consume_file", _("Consume File"))
-        TRAIN_CLASSIFIER = ("train_classifier", _("Train Classifier"))
-        CHECK_SANITY = ("check_sanity", _("Check Sanity"))
-        INDEX_OPTIMIZE = ("index_optimize", _("Index Optimize"))
-
-    task_id = models.CharField(
-        max_length=255,
-        unique=True,
-        verbose_name=_("Task ID"),
-        help_text=_("Celery ID for the Task that was run"),
-    )
-
-    acknowledged = models.BooleanField(
-        default=False,
-        verbose_name=_("Acknowledged"),
-        help_text=_("If the task is acknowledged via the frontend or API"),
-    )
-
-    task_file_name = models.CharField(
-        null=True,
-        max_length=255,
-        verbose_name=_("Task Filename"),
-        help_text=_("Name of the file which the Task was run for"),
-    )
-
-    task_name = models.CharField(
-        null=True,
-        max_length=255,
-        choices=TaskName.choices,
-        verbose_name=_("Task Name"),
-        help_text=_("Name of the task that was run"),
-    )
-
-    status = models.CharField(
-        max_length=30,
-        default=states.PENDING,
-        choices=TASK_STATE_CHOICES,
-        verbose_name=_("Task State"),
-        help_text=_("Current state of the task being run"),
-    )
-
-    date_created = models.DateTimeField(
-        null=True,
-        default=timezone.now,
-        verbose_name=_("Created DateTime"),
-        help_text=_("Datetime field when the task result was created in UTC"),
-    )
-
-    date_started = models.DateTimeField(
-        null=True,
-        default=None,
-        verbose_name=_("Started DateTime"),
-        help_text=_("Datetime field when the task was started in UTC"),
-    )
-
-    date_done = models.DateTimeField(
-        null=True,
-        default=None,
-        verbose_name=_("Completed DateTime"),
-        help_text=_("Datetime field when the task was completed in UTC"),
-    )
-
-    result = models.TextField(
-        null=True,
-        default=None,
-        verbose_name=_("Result Data"),
-        help_text=_(
-            "The data returned by the task",
-        ),
-    )
-
-    type = models.CharField(
-        max_length=30,
-        choices=TaskType.choices,
-        default=TaskType.AUTO,
-        verbose_name=_("Task Type"),
-        help_text=_("The type of task that was run"),
-    )
-
-    def __str__(self) -> str:
-        return f"Task {self.task_id}"
-
-
-class Note(SoftDeleteModel):
-    note = models.TextField(
-        _("content"),
-        blank=True,
-        help_text=_("Note for the document"),
-    )
-
-    created = models.DateTimeField(
-        _("created"),
-        default=timezone.now,
-        db_index=True,
-    )
-
-    document = models.ForeignKey(
-        Document,
-        blank=True,
-        null=True,
-        related_name="notes",
-        on_delete=models.CASCADE,
-        verbose_name=_("document"),
-    )
-
-    user = models.ForeignKey(
-        User,
-        blank=True,
-        null=True,
-        related_name="notes",
-        on_delete=models.SET_NULL,
-        verbose_name=_("user"),
-    )
-
-    class Meta:
-        ordering = ("created",)
-        verbose_name = _("note")
-        verbose_name_plural = _("notes")
-
-    def __str__(self):
-        return self.note
-
-
-class ShareLink(SoftDeleteModel):
-    class FileVersion(models.TextChoices):
-        ARCHIVE = ("archive", _("Archive"))
-        ORIGINAL = ("original", _("Original"))
-
-    created = models.DateTimeField(
-        _("created"),
-        default=timezone.now,
-        db_index=True,
-        blank=True,
-        editable=False,
-    )
-
-    expiration = models.DateTimeField(
-        _("expiration"),
-        blank=True,
-        null=True,
-        db_index=True,
-    )
-
-    slug = models.SlugField(
-        _("slug"),
-        db_index=True,
-        unique=True,
-        blank=True,
-        editable=False,
-    )
-
-    document = models.ForeignKey(
-        Document,
-        blank=True,
-        related_name="share_links",
-        on_delete=models.CASCADE,
-        verbose_name=_("document"),
-    )
-
-    file_version = models.CharField(
-        max_length=50,
-        choices=FileVersion.choices,
-        default=FileVersion.ARCHIVE,
-    )
-
-    owner = models.ForeignKey(
-        User,
-        blank=True,
-        null=True,
-        related_name="share_links",
-        on_delete=models.SET_NULL,
-        verbose_name=_("owner"),
-    )
-
-    class Meta:
-        ordering = ("created",)
-        verbose_name = _("share link")
-        verbose_name_plural = _("share links")
-
-    def __str__(self):
-        return f"Share Link for {self.document.title}"
-
-
-class CustomField(models.Model):
-    """
-    Defines the name and type of a custom field
-    """
-
-    class FieldDataType(models.TextChoices):
-        STRING = ("string", _("String"))
-        URL = ("url", _("URL"))
-        DATE = ("date", _("Date"))
-        BOOL = ("boolean"), _("Boolean")
-        INT = ("integer", _("Integer"))
-        FLOAT = ("float", _("Float"))
-        MONETARY = ("monetary", _("Monetary"))
-        DOCUMENTLINK = ("documentlink", _("Document Link"))
-        SELECT = ("select", _("Select"))
-
-    created = models.DateTimeField(
-        _("created"),
-        default=timezone.now,
-        db_index=True,
-        editable=False,
-    )
-
-    name = models.CharField(max_length=128)
-
-    data_type = models.CharField(
-        _("data type"),
-        max_length=50,
-        choices=FieldDataType.choices,
-        editable=False,
-    )
-
-    extra_data = models.JSONField(
-        _("extra data"),
-        null=True,
-        blank=True,
-        help_text=_(
-            "Extra data for the custom field, such as select options",
-        ),
-    )
-
-    class Meta:
-        ordering = ("created",)
-        verbose_name = _("custom field")
-        verbose_name_plural = _("custom fields")
-        constraints = [
-            models.UniqueConstraint(
-                fields=["name"],
-                name="%(app_label)s_%(class)s_unique_name",
-            ),
-        ]
-
-    def __str__(self) -> str:
-        return f"{self.name} : {self.data_type}"
-
-
-class CustomFieldInstance(SoftDeleteModel):
-    """
-    A single instance of a field, attached to a CustomField for the name and type
-    and attached to a single Document to be metadata for it
-    """
-
-    TYPE_TO_DATA_STORE_NAME_MAP = {
-        CustomField.FieldDataType.STRING: "value_text",
-        CustomField.FieldDataType.URL: "value_url",
-        CustomField.FieldDataType.DATE: "value_date",
-        CustomField.FieldDataType.BOOL: "value_bool",
-        CustomField.FieldDataType.INT: "value_int",
-        CustomField.FieldDataType.FLOAT: "value_float",
-        CustomField.FieldDataType.MONETARY: "value_monetary",
-        CustomField.FieldDataType.DOCUMENTLINK: "value_document_ids",
-        CustomField.FieldDataType.SELECT: "value_select",
-    }
-
-    created = models.DateTimeField(
-        _("created"),
-        default=timezone.now,
-        db_index=True,
-        editable=False,
-    )
-
-    document = models.ForeignKey(
-        Document,
-        blank=False,
-        null=False,
-        on_delete=models.CASCADE,
-        related_name="custom_fields",
-        editable=False,
-    )
-
-    field = models.ForeignKey(
-        CustomField,
-        blank=False,
-        null=False,
-        on_delete=models.CASCADE,
-        related_name="fields",
-        editable=False,
-    )
-
-    # Actual data storage
-    value_text = models.CharField(max_length=128, null=True)
-
-    value_bool = models.BooleanField(null=True)
-
-    value_url = models.URLField(null=True)
-
-    value_date = models.DateField(null=True)
-
-    value_int = models.IntegerField(null=True)
-
-    value_float = models.FloatField(null=True)
-
-    value_monetary = models.CharField(null=True, max_length=128)
-
-    value_monetary_amount = models.GeneratedField(
-        expression=Case(
-            # If the value starts with a number and no currency symbol, use the whole string
-            models.When(
-                value_monetary__regex=r"^\d+",
-                then=Cast(
-                    Substr("value_monetary", 1),
-                    output_field=models.DecimalField(decimal_places=2, max_digits=65),
-                ),
-            ),
-            # If the value starts with a 3-char currency symbol, use the rest of the string
-            default=Cast(
-                Substr("value_monetary", 4),
-                output_field=models.DecimalField(decimal_places=2, max_digits=65),
-            ),
-            output_field=models.DecimalField(decimal_places=2, max_digits=65),
-        ),
-        output_field=models.DecimalField(decimal_places=2, max_digits=65),
-        db_persist=True,
-    )
-
-    value_document_ids = models.JSONField(null=True)
-
-    value_select = models.CharField(null=True, max_length=16)
-
-    class Meta:
-        ordering = ("created",)
-        verbose_name = _("custom field instance")
-        verbose_name_plural = _("custom field instances")
-        constraints = [
-            models.UniqueConstraint(
-                fields=["document", "field"],
-                name="%(app_label)s_%(class)s_unique_document_field",
-            ),
-        ]
-
-    def __str__(self) -> str:
-        value = (
-            next(
-                option.get("label")
-                for option in self.field.extra_data["select_options"]
-                if option.get("id") == self.value_select
-            )
-            if (
-                self.field.data_type == CustomField.FieldDataType.SELECT
-                and self.value_select is not None
-            )
-            else self.value
-        )
-        return str(self.field.name) + f" : {value}"
-
-    @classmethod
-    def get_value_field_name(cls, data_type: CustomField.FieldDataType):
-        try:
-            return cls.TYPE_TO_DATA_STORE_NAME_MAP[data_type]
-        except KeyError:  # pragma: no cover
-            raise NotImplementedError(data_type)
-
-    @property
-    def value(self):
-        """
-        Based on the data type, access the actual value the instance stores
-        A little shorthand/quick way to get what is actually here
-        """
-        value_field_name = self.get_value_field_name(self.field.data_type)
-        return getattr(self, value_field_name)
-
-
-if settings.AUDIT_LOG_ENABLED:
-    auditlog.register(
-        Document,
-        m2m_fields={"tags"},
-        exclude_fields=["modified"],
-    )
-    auditlog.register(Correspondent)
-    auditlog.register(Tag)
-    auditlog.register(DocumentType)
-    auditlog.register(Note)
-    auditlog.register(CustomField)
-    auditlog.register(CustomFieldInstance)
-
-
-class WorkflowTrigger(models.Model):
-    class WorkflowTriggerMatching(models.IntegerChoices):
-        # No auto matching
-        NONE = MatchingModel.MATCH_NONE, _("None")
-        ANY = MatchingModel.MATCH_ANY, _("Any word")
-        ALL = MatchingModel.MATCH_ALL, _("All words")
-        LITERAL = MatchingModel.MATCH_LITERAL, _("Exact match")
-        REGEX = MatchingModel.MATCH_REGEX, _("Regular expression")
-        FUZZY = MatchingModel.MATCH_FUZZY, _("Fuzzy word")
-
-    class WorkflowTriggerType(models.IntegerChoices):
-        CONSUMPTION = 1, _("Consumption Started")
-        DOCUMENT_ADDED = 2, _("Document Added")
-        DOCUMENT_UPDATED = 3, _("Document Updated")
-        SCHEDULED = 4, _("Scheduled")
-
-    class DocumentSourceChoices(models.IntegerChoices):
-        CONSUME_FOLDER = DocumentSource.ConsumeFolder.value, _("Consume Folder")
-        API_UPLOAD = DocumentSource.ApiUpload.value, _("Api Upload")
-        MAIL_FETCH = DocumentSource.MailFetch.value, _("Mail Fetch")
-        WEB_UI = DocumentSource.WebUI.value, _("Web UI")
-
-    class ScheduleDateField(models.TextChoices):
-        ADDED = "added", _("Added")
-        CREATED = "created", _("Created")
-        MODIFIED = "modified", _("Modified")
-        CUSTOM_FIELD = "custom_field", _("Custom Field")
-
-    type = models.PositiveIntegerField(
-        _("Workflow Trigger Type"),
-        choices=WorkflowTriggerType.choices,
-        default=WorkflowTriggerType.CONSUMPTION,
-    )
-
-    sources = MultiSelectField(
-        max_length=7,
-        choices=DocumentSourceChoices.choices,
-        default=f"{DocumentSource.ConsumeFolder},{DocumentSource.ApiUpload},{DocumentSource.MailFetch},{DocumentSource.WebUI}",
-    )
-
-    filter_path = models.CharField(
-        _("filter path"),
-        max_length=256,
-        null=True,
-        blank=True,
-        help_text=_(
-            "Only consume documents with a path that matches "
-            "this if specified. Wildcards specified as * are "
-            "allowed. Case insensitive.",
-        ),
-    )
-
-    filter_filename = models.CharField(
-        _("filter filename"),
-        max_length=256,
-        null=True,
-        blank=True,
-        help_text=_(
-            "Only consume documents which entirely match this "
-            "filename if specified. Wildcards such as *.pdf or "
-            "*invoice* are allowed. Case insensitive.",
-        ),
-    )
-
-    filter_mailrule = models.ForeignKey(
-        "paperless_mail.MailRule",
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL,
-        verbose_name=_("filter documents from this mail rule"),
-    )
-
-    match = models.CharField(_("match"), max_length=256, blank=True)
-
-    matching_algorithm = models.PositiveIntegerField(
-        _("matching algorithm"),
-        choices=WorkflowTriggerMatching.choices,
-        default=WorkflowTriggerMatching.NONE,
-    )
-
-    is_insensitive = models.BooleanField(_("is insensitive"), default=True)
-
-    filter_has_tags = models.ManyToManyField(
-        Tag,
-        blank=True,
-        verbose_name=_("has these tag(s)"),
-    )
-
-    filter_has_document_type = models.ForeignKey(
-        DocumentType,
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL,
-        verbose_name=_("has this document type"),
-    )
-
-    filter_has_correspondent = models.ForeignKey(
-        Correspondent,
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL,
-        verbose_name=_("has this correspondent"),
-    )
-
-    schedule_offset_days = models.PositiveIntegerField(
-        _("schedule offset days"),
-        default=0,
-        help_text=_(
-            "The number of days to offset the schedule trigger by.",
-        ),
-    )
-
-    schedule_is_recurring = models.BooleanField(
-        _("schedule is recurring"),
-        default=False,
-        help_text=_(
-            "If the schedule should be recurring.",
-        ),
-    )
-
-    schedule_recurring_interval_days = models.PositiveIntegerField(
-        _("schedule recurring delay in days"),
-        default=1,
-        validators=[MinValueValidator(1)],
-        help_text=_(
-            "The number of days between recurring schedule triggers.",
-        ),
-    )
-
-    schedule_date_field = models.CharField(
-        _("schedule date field"),
-        max_length=20,
-        choices=ScheduleDateField.choices,
-        default=ScheduleDateField.ADDED,
-        help_text=_(
-            "The field to check for a schedule trigger.",
-        ),
-    )
-
-    schedule_date_custom_field = models.ForeignKey(
-        CustomField,
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL,
-        verbose_name=_("schedule date custom field"),
-    )
-
-    class Meta:
-        verbose_name = _("workflow trigger")
-        verbose_name_plural = _("workflow triggers")
-
-    def __str__(self):
-        return f"WorkflowTrigger {self.pk}"
-
-
-class WorkflowActionEmail(models.Model):
-    subject = models.CharField(
-        _("email subject"),
-        max_length=256,
-        null=False,
-        help_text=_(
-            "The subject of the email, can include some placeholders, "
-            "see documentation.",
-        ),
-    )
-
-    body = models.TextField(
-        _("email body"),
-        null=False,
-        help_text=_(
-            "The body (message) of the email, can include some placeholders, "
-            "see documentation.",
-        ),
-    )
-
-    to = models.TextField(
-        _("emails to"),
-        null=False,
-        help_text=_(
-            "The destination email addresses, comma separated.",
-        ),
-    )
-
-    include_document = models.BooleanField(
-        default=False,
-        verbose_name=_("include document in email"),
-    )
-
-    def __str__(self):
-        return f"Workflow Email Action {self.pk}"
-
-
-class WorkflowActionWebhook(models.Model):
-    # We dont use the built-in URLField because it is not flexible enough
-    # validation is handled in the serializer
-    url = models.CharField(
-        _("webhook url"),
-        null=False,
-        max_length=256,
-        help_text=_("The destination URL for the notification."),
-    )
-
-    use_params = models.BooleanField(
-        default=True,
-        verbose_name=_("use parameters"),
-    )
-
-    as_json = models.BooleanField(
-        default=False,
-        verbose_name=_("send as JSON"),
-    )
-
-    params = models.JSONField(
-        _("webhook parameters"),
-        null=True,
-        blank=True,
-        help_text=_("The parameters to send with the webhook URL if body not used."),
-    )
-
-    body = models.TextField(
-        _("webhook body"),
-        null=True,
-        blank=True,
-        help_text=_("The body to send with the webhook URL if parameters not used."),
-    )
-
-    headers = models.JSONField(
-        _("webhook headers"),
-        null=True,
-        blank=True,
-        help_text=_("The headers to send with the webhook URL."),
-    )
-
-    include_document = models.BooleanField(
-        default=False,
-        verbose_name=_("include document in webhook"),
-    )
-
-    def __str__(self):
-        return f"Workflow Webhook Action {self.pk}"
-
-
-class WorkflowAction(models.Model):
-    class WorkflowActionType(models.IntegerChoices):
-        ASSIGNMENT = (
-            1,
-            _("Assignment"),
-        )
-        REMOVAL = (
-            2,
-            _("Removal"),
-        )
-        EMAIL = (
-            3,
-            _("Email"),
-        )
-        WEBHOOK = (
-            4,
-            _("Webhook"),
-        )
-
-    type = models.PositiveIntegerField(
-        _("Workflow Action Type"),
-        choices=WorkflowActionType.choices,
-        default=WorkflowActionType.ASSIGNMENT,
-    )
-
-    assign_title = models.CharField(
-        _("assign title"),
-        max_length=256,
-        null=True,
-        blank=True,
-        help_text=_(
-            "Assign a document title, can include some placeholders, "
-            "see documentation.",
-        ),
-    )
-
-    assign_tags = models.ManyToManyField(
-        Tag,
-        blank=True,
-        related_name="+",
-        verbose_name=_("assign this tag"),
-    )
-
-    assign_document_type = models.ForeignKey(
-        DocumentType,
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL,
-        related_name="+",
-        verbose_name=_("assign this document type"),
-    )
-
-    assign_correspondent = models.ForeignKey(
-        Correspondent,
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL,
-        related_name="+",
-        verbose_name=_("assign this correspondent"),
-    )
-
-    assign_storage_path = models.ForeignKey(
-        StoragePath,
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL,
-        related_name="+",
-        verbose_name=_("assign this storage path"),
-    )
-
-    assign_owner = models.ForeignKey(
-        User,
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL,
-        related_name="+",
-        verbose_name=_("assign this owner"),
-    )
-
-    assign_view_users = models.ManyToManyField(
-        User,
-        blank=True,
-        related_name="+",
-        verbose_name=_("grant view permissions to these users"),
-    )
-
-    assign_view_groups = models.ManyToManyField(
-        Group,
-        blank=True,
-        related_name="+",
-        verbose_name=_("grant view permissions to these groups"),
-    )
-
-    assign_change_users = models.ManyToManyField(
-        User,
-        blank=True,
-        related_name="+",
-        verbose_name=_("grant change permissions to these users"),
-    )
-
-    assign_change_groups = models.ManyToManyField(
-        Group,
-        blank=True,
-        related_name="+",
-        verbose_name=_("grant change permissions to these groups"),
-    )
-
-    assign_custom_fields = models.ManyToManyField(
-        CustomField,
-        blank=True,
-        related_name="+",
-        verbose_name=_("assign these custom fields"),
-    )
-
-    assign_custom_fields_values = models.JSONField(
-        _("custom field values"),
-        null=True,
-        blank=True,
-        help_text=_(
-            "Optional values to assign to the custom fields.",
-        ),
-        default=dict,
-    )
-
-    remove_tags = models.ManyToManyField(
-        Tag,
-        blank=True,
-        related_name="+",
-        verbose_name=_("remove these tag(s)"),
-    )
-
-    remove_all_tags = models.BooleanField(
-        default=False,
-        verbose_name=_("remove all tags"),
-    )
-
-    remove_document_types = models.ManyToManyField(
-        DocumentType,
-        blank=True,
-        related_name="+",
-        verbose_name=_("remove these document type(s)"),
-    )
-
-    remove_all_document_types = models.BooleanField(
-        default=False,
-        verbose_name=_("remove all document types"),
-    )
-
-    remove_correspondents = models.ManyToManyField(
-        Correspondent,
-        blank=True,
-        related_name="+",
-        verbose_name=_("remove these correspondent(s)"),
-    )
-
-    remove_all_correspondents = models.BooleanField(
-        default=False,
-        verbose_name=_("remove all correspondents"),
-    )
-
-    remove_storage_paths = models.ManyToManyField(
-        StoragePath,
-        blank=True,
-        related_name="+",
-        verbose_name=_("remove these storage path(s)"),
-    )
-
-    remove_all_storage_paths = models.BooleanField(
-        default=False,
-        verbose_name=_("remove all storage paths"),
-    )
-
-    remove_owners = models.ManyToManyField(
-        User,
-        blank=True,
-        related_name="+",
-        verbose_name=_("remove these owner(s)"),
-    )
-
-    remove_all_owners = models.BooleanField(
-        default=False,
-        verbose_name=_("remove all owners"),
-    )
-
-    remove_view_users = models.ManyToManyField(
-        User,
-        blank=True,
-        related_name="+",
-        verbose_name=_("remove view permissions for these users"),
-    )
-
-    remove_view_groups = models.ManyToManyField(
-        Group,
-        blank=True,
-        related_name="+",
-        verbose_name=_("remove view permissions for these groups"),
-    )
-
-    remove_change_users = models.ManyToManyField(
-        User,
-        blank=True,
-        related_name="+",
-        verbose_name=_("remove change permissions for these users"),
-    )
-
-    remove_change_groups = models.ManyToManyField(
-        Group,
-        blank=True,
-        related_name="+",
-        verbose_name=_("remove change permissions for these groups"),
-    )
-
-    remove_all_permissions = models.BooleanField(
-        default=False,
-        verbose_name=_("remove all permissions"),
-    )
-
-    remove_custom_fields = models.ManyToManyField(
-        CustomField,
-        blank=True,
-        related_name="+",
-        verbose_name=_("remove these custom fields"),
-    )
-
-    remove_all_custom_fields = models.BooleanField(
-        default=False,
-        verbose_name=_("remove all custom fields"),
-    )
-
-    email = models.ForeignKey(
-        WorkflowActionEmail,
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL,
-        related_name="action",
-        verbose_name=_("email"),
-    )
-
-    webhook = models.ForeignKey(
-        WorkflowActionWebhook,
-        null=True,
-        blank=True,
-        on_delete=models.SET_NULL,
-        related_name="action",
-        verbose_name=_("webhook"),
-    )
-
-    class Meta:
-        verbose_name = _("workflow action")
-        verbose_name_plural = _("workflow actions")
-
-    def __str__(self):
-        return f"WorkflowAction {self.pk}"
-
-
-class Workflow(models.Model):
-    name = models.CharField(_("name"), max_length=256, unique=True)
-
-    order = models.IntegerField(_("order"), default=0)
-
-    triggers = models.ManyToManyField(
-        WorkflowTrigger,
-        related_name="workflows",
-        blank=False,
-        verbose_name=_("triggers"),
-    )
-
-    actions = models.ManyToManyField(
-        WorkflowAction,
-        related_name="workflows",
-        blank=False,
-        verbose_name=_("actions"),
-    )
-
-    enabled = models.BooleanField(_("enabled"), default=True)
-
-    def __str__(self):
-        return f"Workflow: {self.name}"
-
-
-class WorkflowRun(models.Model):
-    workflow = models.ForeignKey(
-        Workflow,
-        on_delete=models.CASCADE,
-        related_name="runs",
-        verbose_name=_("workflow"),
-    )
-
-    type = models.PositiveIntegerField(
-        _("workflow trigger type"),
-        choices=WorkflowTrigger.WorkflowTriggerType.choices,
-        null=True,
-    )
-
-    document = models.ForeignKey(
-        Document,
-        null=True,
-        on_delete=models.CASCADE,
-        related_name="workflow_runs",
-        verbose_name=_("document"),
-    )
-
-    run_at = models.DateTimeField(
-        _("date run"),
-        default=timezone.now,
-        db_index=True,
-    )
-
-    class Meta:
-        verbose_name = _("workflow run")
-        verbose_name_plural = _("workflow runs")
-
-    def __str__(self):
-        return f"WorkflowRun of {self.workflow} at {self.run_at} on {self.document}"
index 6cef98f1a3ea3a8fb001baa9105e944b0707034a..e5e34c79e7dbc88068dc947a1d4daa91d6060d74 100644 (file)
@@ -10,8 +10,8 @@ from django.conf import settings
 from django.utils import timezone
 from tqdm import tqdm
 
-from documents.models import Document
-from documents.models import PaperlessTask
+from paperless.models import Document
+from paperless.models import PaperlessTask
 
 
 class SanityCheckMessages:
index 782f4d6c874a77ff6c262ebb491653b215a41b04..006da390b1c2dc6554c93817588b60d2531a6ac8 100644 (file)
@@ -37,25 +37,6 @@ if settings.AUDIT_LOG_ENABLED:
 
 from documents import bulk_edit
 from documents.data_models import DocumentSource
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import MatchingModel
-from documents.models import Note
-from documents.models import PaperlessTask
-from documents.models import SavedView
-from documents.models import SavedViewFilterRule
-from documents.models import ShareLink
-from documents.models import StoragePath
-from documents.models import Tag
-from documents.models import UiSettings
-from documents.models import Workflow
-from documents.models import WorkflowAction
-from documents.models import WorkflowActionEmail
-from documents.models import WorkflowActionWebhook
-from documents.models import WorkflowTrigger
 from documents.parsers import is_mime_type_supported
 from documents.permissions import get_groups_with_only_permission
 from documents.permissions import set_permissions_for_object
@@ -63,6 +44,25 @@ from documents.templating.filepath import validate_filepath_template_and_render
 from documents.templating.utils import convert_format_str_to_template_format
 from documents.validators import uri_validator
 from documents.validators import url_validator
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import MatchingModel
+from paperless.models import Note
+from paperless.models import PaperlessTask
+from paperless.models import SavedView
+from paperless.models import SavedViewFilterRule
+from paperless.models import ShareLink
+from paperless.models import StoragePath
+from paperless.models import Tag
+from paperless.models import UiSettings
+from paperless.models import Workflow
+from paperless.models import WorkflowAction
+from paperless.models import WorkflowActionEmail
+from paperless.models import WorkflowActionWebhook
+from paperless.models import WorkflowTrigger
 
 if TYPE_CHECKING:
     from collections.abc import Iterable
index 073026b192a28350b6dc50c8775e9cf4e01702b0..f94362ed97735132c1137f138028f05d6e35d494 100644 (file)
@@ -29,22 +29,22 @@ from documents.file_handling import create_source_path_directory
 from documents.file_handling import delete_empty_directories
 from documents.file_handling import generate_unique_filename
 from documents.mail import send_email
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import MatchingModel
-from documents.models import PaperlessTask
-from documents.models import SavedView
-from documents.models import Tag
-from documents.models import Workflow
-from documents.models import WorkflowAction
-from documents.models import WorkflowRun
-from documents.models import WorkflowTrigger
 from documents.permissions import get_objects_for_user_owner_aware
 from documents.permissions import set_permissions_for_object
 from documents.templating.workflows import parse_w_workflow_placeholders
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import MatchingModel
+from paperless.models import PaperlessTask
+from paperless.models import SavedView
+from paperless.models import Tag
+from paperless.models import Workflow
+from paperless.models import WorkflowAction
+from paperless.models import WorkflowRun
+from paperless.models import WorkflowTrigger
 
 if TYPE_CHECKING:
     from pathlib import Path
index 7d71d48c95bfb3f47c0ca52279359fe624b2d231..a01782c9df8fb4ce067b0ba375b037dc186cb935 100644 (file)
@@ -32,16 +32,6 @@ from documents.data_models import DocumentMetadataOverrides
 from documents.double_sided import CollatePlugin
 from documents.file_handling import create_source_path_directory
 from documents.file_handling import generate_unique_filename
-from documents.models import Correspondent
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import PaperlessTask
-from documents.models import StoragePath
-from documents.models import Tag
-from documents.models import Workflow
-from documents.models import WorkflowRun
-from documents.models import WorkflowTrigger
 from documents.parsers import DocumentParser
 from documents.parsers import get_parser_class_for_mime_type
 from documents.plugins.base import ConsumeTaskPlugin
@@ -52,6 +42,16 @@ from documents.sanity_checker import SanityCheckFailedException
 from documents.signals import document_updated
 from documents.signals.handlers import cleanup_document_deletion
 from documents.signals.handlers import run_workflows
+from paperless.models import Correspondent
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import PaperlessTask
+from paperless.models import StoragePath
+from paperless.models import Tag
+from paperless.models import Workflow
+from paperless.models import WorkflowRun
+from paperless.models import WorkflowTrigger
 
 if settings.AUDIT_LOG_ENABLED:
     from auditlog.models import LogEntry
index 45e1cad9e2f6cd349908e873619f9f363815f270..1b9646fdc8d07a277164d7c5083a1051f41368f4 100644 (file)
@@ -17,13 +17,13 @@ from jinja2 import make_logging_undefined
 from jinja2.sandbox import SandboxedEnvironment
 from jinja2.sandbox import SecurityError
 
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import StoragePath
-from documents.models import Tag
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import StoragePath
+from paperless.models import Tag
 
 logger = logging.getLogger("paperless.templating")
 
index de41bbd02fdd62699799f3deccb5e5712a3c0a98..7be8b250fc3a67ebadcb0c17af7ae1f013409984 100644 (file)
@@ -1,8 +1,8 @@
 from factory import Faker
 from factory.django import DjangoModelFactory
 
-from documents.models import Correspondent
-from documents.models import Document
+from paperless.models import Correspondent
+from paperless.models import Document
 
 
 class CorrespondentFactory(DjangoModelFactory):
index ab32562a8e56eaac1f1b5ccc16201ec4ba2ec8e9..05e14d588c7dd8bcc62d16c89c6285774db4af0b 100644 (file)
@@ -7,9 +7,9 @@ from django.utils import timezone
 
 from documents import index
 from documents.admin import DocumentAdmin
-from documents.models import Document
 from documents.tests.utils import DirectoriesMixin
 from paperless.admin import PaperlessUserAdmin
+from paperless.models import Document
 
 
 class TestDocumentAdmin(DirectoriesMixin, TestCase):
index a7e8f5df3224e902ecdcc0fed5a34a3100e60675..0561fa9c3db101a1dc8809169e3a4c7fe9c48d27 100644 (file)
@@ -10,11 +10,11 @@ from django.utils import timezone
 from rest_framework import status
 from rest_framework.test import APITestCase
 
-from documents.models import Correspondent
-from documents.models import Document
-from documents.models import DocumentType
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import SampleDirMixin
+from paperless.models import Correspondent
+from paperless.models import Document
+from paperless.models import DocumentType
 
 
 class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
index bcbe5922d09fecfc48360a34b4529c3b50ab03db..701e459fbc7c5dc33e6c5b77d1d76d32c14c23ff 100644 (file)
@@ -9,13 +9,13 @@ from guardian.shortcuts import assign_perm
 from rest_framework import status
 from rest_framework.test import APITestCase
 
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import StoragePath
-from documents.models import Tag
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import StoragePath
+from paperless.models import Tag
 
 
 class TestBulkEditAPI(DirectoriesMixin, APITestCase):
index 8e24226dcd4935feae4a6b87e68946f6f38a89e8..d452f0a4f19544609fb4828649f9fe7ed59a3442 100644 (file)
@@ -7,10 +7,10 @@ from django.contrib.auth.models import User
 from rest_framework import status
 from rest_framework.test import APITestCase
 
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
 
 
 class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
index a0a380e41fe2fc9b94b64903529045d746897728..c27c1f05070d5387a929346a22ce57247e87616c 100644 (file)
@@ -29,23 +29,23 @@ from documents.caching import CLASSIFIER_HASH_KEY
 from documents.caching import CLASSIFIER_MODIFIED_KEY
 from documents.caching import CLASSIFIER_VERSION_KEY
 from documents.data_models import DocumentSource
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import MatchingModel
-from documents.models import Note
-from documents.models import SavedView
-from documents.models import ShareLink
-from documents.models import StoragePath
-from documents.models import Tag
-from documents.models import Workflow
-from documents.models import WorkflowAction
-from documents.models import WorkflowTrigger
 from documents.signals.handlers import run_workflows
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import DocumentConsumeDelayMixin
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import MatchingModel
+from paperless.models import Note
+from paperless.models import SavedView
+from paperless.models import ShareLink
+from paperless.models import StoragePath
+from paperless.models import Tag
+from paperless.models import Workflow
+from paperless.models import WorkflowAction
+from paperless.models import WorkflowTrigger
 
 
 class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
index 70d43dfde3c92f5bd5ca4317f0ee9a1060d1c9c0..d6737dcd024393d19d32726b3469ee6399a83816 100644 (file)
@@ -7,10 +7,10 @@ from urllib.parse import quote
 from django.contrib.auth.models import User
 from rest_framework.test import APITestCase
 
-from documents.models import CustomField
-from documents.models import Document
 from documents.serialisers import DocumentSerializer
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import CustomField
+from paperless.models import Document
 
 
 class DocumentWrapper:
index d4d3c729ec9d1b2a959e7e9c2d43c9ffceb32ac4..98ed794c8acf0b65c2804fd34d1bad4fe1341ed6 100644 (file)
@@ -8,12 +8,12 @@ from django.utils import timezone
 from rest_framework import status
 from rest_framework.test import APITestCase
 
-from documents.models import Correspondent
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import StoragePath
-from documents.models import Tag
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import Correspondent
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import StoragePath
+from paperless.models import Tag
 
 
 class TestApiObjects(DirectoriesMixin, APITestCase):
index 692c2241754178c4fa98b627d11e450f48ea94f8..4cfd06c38655814a88fb011378197e4f00c09636 100644 (file)
@@ -13,13 +13,13 @@ from guardian.shortcuts import get_users_with_perms
 from rest_framework import status
 from rest_framework.test import APITestCase
 
-from documents.models import Correspondent
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import MatchingModel
-from documents.models import StoragePath
-from documents.models import Tag
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import Correspondent
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import MatchingModel
+from paperless.models import StoragePath
+from paperless.models import Tag
 
 
 class TestApiAuth(DirectoriesMixin, APITestCase):
index 118862979bb7a11af3167bd126e482550573e210..da2fe2b7bac64158230806911b77aadd1d7e9abb 100644 (file)
@@ -16,17 +16,17 @@ from whoosh.writing import AsyncWriter
 
 from documents import index
 from documents.bulk_edit import set_permissions
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import Note
-from documents.models import SavedView
-from documents.models import StoragePath
-from documents.models import Tag
-from documents.models import Workflow
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import Note
+from paperless.models import SavedView
+from paperless.models import StoragePath
+from paperless.models import Tag
+from paperless.models import Workflow
 from paperless_mail.models import MailAccount
 from paperless_mail.models import MailRule
 
index 9b7bf37adabb880201a60f146f833e220f7f4fd3..39c385ceb23bc0bd5bbec661950a2114661949e4 100644 (file)
@@ -8,8 +8,8 @@ from django.test import override_settings
 from rest_framework import status
 from rest_framework.test import APITestCase
 
-from documents.models import PaperlessTask
 from paperless import version
+from paperless.models import PaperlessTask
 
 
 class TestSystemStatus(APITestCase):
index c139d05dacc0f9cdada9cbddf684cfe710f24f76..99a9e911e1b5941cd89834c0e4eed6deb65f85bb 100644 (file)
@@ -7,9 +7,9 @@ from django.contrib.auth.models import User
 from rest_framework import status
 from rest_framework.test import APITestCase
 
-from documents.models import PaperlessTask
 from documents.tests.utils import DirectoriesMixin
 from documents.views import TasksViewSet
+from paperless.models import PaperlessTask
 
 
 class TestTasks(DirectoriesMixin, APITestCase):
index ab4e967737bb44c09ed61aad2dc56be87379f6b0..ec9b189aab1fd6bb654554f09b2d137a21d9951b 100644 (file)
@@ -4,7 +4,7 @@ from django.core.cache import cache
 from rest_framework import status
 from rest_framework.test import APITestCase
 
-from documents.models import Document
+from paperless.models import Document
 
 
 class TestTrashAPI(APITestCase):
index 4aa3a81a650734809d3043b3483960b2e2ac129e..d414eae770246a7377cb2a531b3095462d945529 100644 (file)
@@ -6,15 +6,15 @@ from rest_framework import status
 from rest_framework.test import APITestCase
 
 from documents.data_models import DocumentSource
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import DocumentType
-from documents.models import StoragePath
-from documents.models import Tag
-from documents.models import Workflow
-from documents.models import WorkflowAction
-from documents.models import WorkflowTrigger
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import DocumentType
+from paperless.models import StoragePath
+from paperless.models import Tag
+from paperless.models import Workflow
+from paperless.models import WorkflowAction
+from paperless.models import WorkflowTrigger
 
 
 class TestApiWorkflows(DirectoriesMixin, APITestCase):
index 03b0903dd992d96da7138c42e1ca4fd32feabbca..218588dbc52621229f32a56002daa47523193b37 100644 (file)
@@ -14,14 +14,14 @@ from documents.barcodes import BarcodePlugin
 from documents.data_models import ConsumableDocument
 from documents.data_models import DocumentMetadataOverrides
 from documents.data_models import DocumentSource
-from documents.models import Document
-from documents.models import Tag
 from documents.plugins.base import StopConsumeTaskError
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import DocumentConsumeDelayMixin
 from documents.tests.utils import DummyProgressManager
 from documents.tests.utils import FileSystemAssertsMixin
 from documents.tests.utils import SampleDirMixin
+from paperless.models import Document
+from paperless.models import Tag
 
 try:
     import zxingcpp  # noqa: F401
index dd59a6217d4c41b4a0e051621eb3a6274e737424..691b80e4668a927e6b55cdf3ae401fcd2f4121a4 100644 (file)
@@ -10,14 +10,14 @@ from guardian.shortcuts import get_groups_with_perms
 from guardian.shortcuts import get_users_with_perms
 
 from documents import bulk_edit
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import StoragePath
-from documents.models import Tag
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import StoragePath
+from paperless.models import Tag
 
 
 class TestBulkEdit(DirectoriesMixin, TestCase):
index 4af05746fde4a332da3203c78a251f21190b7449..c10eaa0c262277d97f213e769fda7048214557d9 100644 (file)
@@ -9,8 +9,8 @@ from django.test import override_settings
 from documents.checks import changed_password_check
 from documents.checks import filename_format_check
 from documents.checks import parser_check
-from documents.models import Document
 from documents.tests.factories import DocumentFactory
+from paperless.models import Document
 
 
 class TestDocumentChecks(TestCase):
index d1bc8e04fc65f8f790cc7f56fee14e535e118101..bb349611bfe46bee3862948eece76370a37bd461 100644 (file)
@@ -12,13 +12,13 @@ from documents.classifier import ClassifierModelCorruptError
 from documents.classifier import DocumentClassifier
 from documents.classifier import IncompatibleClassifierVersionError
 from documents.classifier import load_classifier
-from documents.models import Correspondent
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import MatchingModel
-from documents.models import StoragePath
-from documents.models import Tag
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import Correspondent
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import MatchingModel
+from paperless.models import StoragePath
+from paperless.models import Tag
 
 
 def dummy_preprocess(content: str):
index 96afa61d3f99e9ee2a73ff9bedd062995bf5409f..6155f4841fc17244621e30e7a83f535fac77a17c 100644 (file)
@@ -20,12 +20,6 @@ from guardian.core import ObjectPermissionChecker
 from documents.consumer import ConsumerError
 from documents.data_models import DocumentMetadataOverrides
 from documents.data_models import DocumentSource
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import StoragePath
-from documents.models import Tag
 from documents.parsers import DocumentParser
 from documents.parsers import ParseError
 from documents.plugins.helpers import ProgressStatusOptions
@@ -33,6 +27,12 @@ from documents.tasks import sanity_check
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import FileSystemAssertsMixin
 from documents.tests.utils import GetConsumerMixin
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import StoragePath
+from paperless.models import Tag
 from paperless_mail.models import MailRule
 from paperless_mail.parsers import MailDocumentParser
 
index 3ee4fb15d9b14004013c7dd632a0498420ce174d..a8cc12c1e686235c56c4a38b9b254c30fdeee8b8 100644 (file)
@@ -2,7 +2,7 @@ from django.test import TestCase
 from whoosh import query
 
 from documents.index import get_permissions_criterias
-from documents.models import User
+from paperless.models import User
 
 
 class TestDelayedQuery(TestCase):
index eca08f82a3bf653de987d32147a94e12ca6ea8f2..9a83602a35411fe1bba355efe5e52f5c60de4667 100644 (file)
@@ -8,9 +8,9 @@ from django.test import TestCase
 from django.test import override_settings
 from django.utils import timezone
 
-from documents.models import Correspondent
-from documents.models import Document
 from documents.tasks import empty_trash
+from paperless.models import Correspondent
+from paperless.models import Document
 
 
 class TestDocument(TestCase):
index 6d2d396fc71700ed8062aa003dd6e7a5e5292451..3ba06ed64bfaca4f643ef625e293459271bbcc5a 100644 (file)
@@ -16,15 +16,15 @@ from django.utils import timezone
 from documents.file_handling import create_source_path_directory
 from documents.file_handling import delete_empty_directories
 from documents.file_handling import generate_filename
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import StoragePath
 from documents.tasks import empty_trash
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import FileSystemAssertsMixin
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import StoragePath
 
 
 class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
index 24bc26d4c5848d0daad4fdb3aed9e7fa702cae81..459ac596e6efcee6a5ae1adc61da04e1cb97f92f 100644 (file)
@@ -3,8 +3,8 @@ from unittest import mock
 from django.test import TestCase
 
 from documents import index
-from documents.models import Document
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import Document
 
 
 class TestAutoComplete(DirectoriesMixin, TestCase):
index 2726fd02e2e80c8c0aad66b7689bdc574a949527..fede4318be647d70cd85dce7dd82b706af9da79b 100644 (file)
@@ -14,10 +14,10 @@ from django.test import TestCase
 from django.test import override_settings
 
 from documents.file_handling import generate_filename
-from documents.models import Document
 from documents.tasks import update_document_content_maybe_archive_file
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import FileSystemAssertsMixin
+from paperless.models import Document
 
 sample_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
 
index 808216d3d608f83fc5a11628ad01e9e0261f06a7..a741fe5aa7a7408fe87bb96f4c69f411d4bac471 100644 (file)
@@ -15,9 +15,9 @@ from django.test import override_settings
 from documents.consumer import ConsumerError
 from documents.data_models import ConsumableDocument
 from documents.management.commands import document_consumer
-from documents.models import Tag
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import DocumentConsumeDelayMixin
+from paperless.models import Tag
 
 
 class ConsumerThread(Thread):
index eec2fcd4b5ae3f74590a472b224467a4ac849c18..944e2942e9b9d8589d1be6d3450924e4ac9b07bc 100644 (file)
@@ -25,24 +25,24 @@ from guardian.models import UserObjectPermission
 from guardian.shortcuts import assign_perm
 
 from documents.management.commands import document_exporter
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import Note
-from documents.models import StoragePath
-from documents.models import Tag
-from documents.models import User
-from documents.models import Workflow
-from documents.models import WorkflowAction
-from documents.models import WorkflowTrigger
 from documents.sanity_checker import check_sanity
 from documents.settings import EXPORTER_FILE_NAME
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import FileSystemAssertsMixin
 from documents.tests.utils import SampleDirMixin
 from documents.tests.utils import paperless_environment
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import Note
+from paperless.models import StoragePath
+from paperless.models import Tag
+from paperless.models import User
+from paperless.models import Workflow
+from paperless.models import WorkflowAction
+from paperless.models import WorkflowTrigger
 from paperless_mail.models import MailAccount
 
 
index 7cc1f265ea9a6a9c98f56d2ac89dbc9a48b580cd..9ef35d69dfae87dbbab0b4c9c31993f5a1e70e88 100644 (file)
@@ -4,7 +4,7 @@ from django.core.management import CommandError
 from django.core.management import call_command
 from django.test import TestCase
 
-from documents.models import Document
+from paperless.models import Document
 
 
 class TestFuzzyMatchCommand(TestCase):
index 5cee9ae478b56dae2920eb90005f92a4fea9a68b..29daa5bf6d01269b66f958e682494e97b5afa4d6 100644 (file)
@@ -9,12 +9,12 @@ from django.core.management.base import CommandError
 from django.test import TestCase
 
 from documents.management.commands.document_importer import Command
-from documents.models import Document
 from documents.settings import EXPORTER_ARCHIVE_NAME
 from documents.settings import EXPORTER_FILE_NAME
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import FileSystemAssertsMixin
 from documents.tests.utils import SampleDirMixin
+from paperless.models import Document
 
 
 class TestCommandImport(
index eb65afb4212c07da0e7c1465fe3b5356fb6a0917..0b650aec5f01d270b0ead77d05e5c0ae72047545 100644 (file)
@@ -2,12 +2,12 @@ from django.core.management import call_command
 from django.core.management.base import CommandError
 from django.test import TestCase
 
-from documents.models import Correspondent
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import StoragePath
-from documents.models import Tag
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import Correspondent
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import StoragePath
+from paperless.models import Tag
 
 
 class TestRetagger(DirectoriesMixin, TestCase):
index cb80e6c709c0a7542eb8ae0317b518f241f62236..3f4452da51976db41fc22e0a329adf524d6ce109 100644 (file)
@@ -6,10 +6,10 @@ from django.core.management import call_command
 from django.test import TestCase
 
 from documents.management.commands.document_thumbnails import _process_document
-from documents.models import Document
 from documents.parsers import get_default_thumbnail
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import FileSystemAssertsMixin
+from paperless.models import Document
 
 
 class TestMakeThumbnails(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
index 180cf77ed3a59b8dee4062744e3ef4eb5e514474..a3b5ec52ded570672a64ccb35c6b3e8430fbf132 100644 (file)
@@ -9,11 +9,11 @@ from django.test import TestCase
 from django.test import override_settings
 
 from documents import matching
-from documents.models import Correspondent
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import Tag
 from documents.signals import document_consumption_finished
+from paperless.models import Correspondent
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import Tag
 
 
 class _TestMatchingBase(TestCase):
index 37b87a115183386dc4ef6c7ae6c448920046c58d..9f50eaea56d0cfe680ad5809c72ec1169e7ba194 100644 (file)
@@ -1,5 +1,5 @@
-from documents.models import StoragePath
 from documents.tests.utils import TestMigrations
+from paperless.models import StoragePath
 
 
 class TestMigrateStoragePathToTemplate(TestMigrations):
index 1c99be3f769ecfc91a51780a10ec0f8f651fb554..c530c9f8006f1c97672cdd1a62a4b9c4e0cb61b1 100644 (file)
@@ -1,9 +1,9 @@
 from django.test import TestCase
 
-from documents.models import Correspondent
-from documents.models import Document
 from documents.tests.factories import CorrespondentFactory
 from documents.tests.factories import DocumentFactory
+from paperless.models import Correspondent
+from paperless.models import Document
 
 
 class CorrespondentTestCase(TestCase):
index 2f40247621aad95e62ec44f0bca06553f936c9ea..d8a1b1814cf237972fd0964dfad5da3d8cd360ad 100644 (file)
@@ -7,9 +7,9 @@ import filelock
 from django.conf import settings
 from django.test import TestCase
 
-from documents.models import Document
 from documents.sanity_checker import check_sanity
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import Document
 
 
 class TestSanityCheck(DirectoriesMixin, TestCase):
index d94eb38480a3431e33ac90612a8329ff95e5db27..a5ab8bd07de99d7a6bf0835cfc7e1ad9af20cb5a 100644 (file)
@@ -7,13 +7,13 @@ from django.test import TestCase
 from documents.data_models import ConsumableDocument
 from documents.data_models import DocumentMetadataOverrides
 from documents.data_models import DocumentSource
-from documents.models import PaperlessTask
 from documents.signals.handlers import before_task_publish_handler
 from documents.signals.handlers import task_failure_handler
 from documents.signals.handlers import task_postrun_handler
 from documents.signals.handlers import task_prerun_handler
 from documents.tests.test_consumer import fake_magic_from_file
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import PaperlessTask
 
 
 @mock.patch("documents.consumer.magic.from_file", fake_magic_from_file)
index 11712549a4bdda3a5eed21ba83b21f21e6a7cf0c..8b9a389e21efa0faebaa8174ff75ddde90c22416 100644 (file)
@@ -8,15 +8,15 @@ from django.test import TestCase
 from django.utils import timezone
 
 from documents import tasks
-from documents.models import Correspondent
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import Tag
 from documents.sanity_checker import SanityCheckFailedException
 from documents.sanity_checker import SanityCheckMessages
 from documents.tests.test_classifier import dummy_preprocess
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import FileSystemAssertsMixin
+from paperless.models import Correspondent
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import Tag
 
 
 class TestIndexReindex(DirectoriesMixin, TestCase):
index 4c987e3af361dbdf2c4e7a2cc2ea6aabb544f558..2f804fde44a0f13eb0b5cac0ee9516241dd79d12 100644 (file)
@@ -10,10 +10,10 @@ from django.test import override_settings
 from django.utils import timezone
 from rest_framework import status
 
-from documents.models import Document
-from documents.models import ShareLink
 from documents.tests.utils import DirectoriesMixin
 from paperless.models import ApplicationConfiguration
+from paperless.models import Document
+from paperless.models import ShareLink
 
 
 class TestViews(DirectoriesMixin, TestCase):
index 3006594cc4d7ecaf325d1c23f85ad8aae320902c..8deebe4820c966be2d8147b2d007d96e540c1d69 100644 (file)
@@ -25,25 +25,25 @@ from documents import tasks
 from documents.data_models import ConsumableDocument
 from documents.data_models import DocumentSource
 from documents.matching import document_matches_workflow
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import CustomFieldInstance
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import MatchingModel
-from documents.models import StoragePath
-from documents.models import Tag
-from documents.models import Workflow
-from documents.models import WorkflowAction
-from documents.models import WorkflowActionEmail
-from documents.models import WorkflowActionWebhook
-from documents.models import WorkflowRun
-from documents.models import WorkflowTrigger
 from documents.signals import document_consumption_finished
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import DummyProgressManager
 from documents.tests.utils import FileSystemAssertsMixin
 from documents.tests.utils import SampleDirMixin
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import CustomFieldInstance
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import MatchingModel
+from paperless.models import StoragePath
+from paperless.models import Tag
+from paperless.models import Workflow
+from paperless.models import WorkflowAction
+from paperless.models import WorkflowActionEmail
+from paperless.models import WorkflowActionWebhook
+from paperless.models import WorkflowRun
+from paperless.models import WorkflowTrigger
 from paperless_mail.models import MailAccount
 from paperless_mail.models import MailRule
 
index 7298391f28416bc75099dfdfa9e845e9ea859fb4..fcf176909be9f540e6feaf90da06acf76018f6a2 100644 (file)
@@ -113,20 +113,6 @@ from documents.matching import match_correspondents
 from documents.matching import match_document_types
 from documents.matching import match_storage_paths
 from documents.matching import match_tags
-from documents.models import Correspondent
-from documents.models import CustomField
-from documents.models import Document
-from documents.models import DocumentType
-from documents.models import Note
-from documents.models import PaperlessTask
-from documents.models import SavedView
-from documents.models import ShareLink
-from documents.models import StoragePath
-from documents.models import Tag
-from documents.models import UiSettings
-from documents.models import Workflow
-from documents.models import WorkflowAction
-from documents.models import WorkflowTrigger
 from documents.parsers import get_parser_class_for_mime_type
 from documents.parsers import parse_date_generator
 from documents.permissions import PaperlessAdminPermissions
@@ -171,6 +157,20 @@ from paperless import version
 from paperless.celery import app as celery_app
 from paperless.config import GeneralConfig
 from paperless.db import GnuPG
+from paperless.models import Correspondent
+from paperless.models import CustomField
+from paperless.models import Document
+from paperless.models import DocumentType
+from paperless.models import Note
+from paperless.models import PaperlessTask
+from paperless.models import SavedView
+from paperless.models import ShareLink
+from paperless.models import StoragePath
+from paperless.models import Tag
+from paperless.models import UiSettings
+from paperless.models import Workflow
+from paperless.models import WorkflowAction
+from paperless.models import WorkflowTrigger
 from paperless.serialisers import GroupSerializer
 from paperless.serialisers import UserSerializer
 from paperless.views import StandardPagination
index f8517a3aa1ba444494ad24ff4541484df638e677..17cf7ec827ff8a006340f757cdffaa6c5aef7365 100644 (file)
@@ -10,7 +10,7 @@ from django.contrib.auth.models import User
 from django.forms import ValidationError
 from django.urls import reverse
 
-from documents.models import Document
+from paperless.models import Document
 from paperless.signals import handle_social_account_updated
 
 logger = logging.getLogger("paperless.auth")
index 1f6cfbcedd7fdd238b25d74aff312d48de4cd87a..78d46b036b219c8146c6d9cb7378ee91d9e58394 100644 (file)
-from django.core.validators import FileExtensionValidator
+import datetime
+from pathlib import Path
+from typing import Final
+
+import pathvalidate
+from celery import states
+from django.conf import settings
+from django.contrib.auth.models import Group
+from django.contrib.auth.models import User
+from django.core.validators import MaxValueValidator
 from django.core.validators import MinValueValidator
 from django.db import models
+from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
+from multiselectfield import MultiSelectField
+
+if settings.AUDIT_LOG_ENABLED:
+    from auditlog.registry import auditlog
+
+from django.core.validators import FileExtensionValidator
+from django.db.models import Case
+from django.db.models.functions import Cast
+from django.db.models.functions import Substr
+from django_softdelete.models import SoftDeleteModel
+
+from documents.data_models import DocumentSource
+from documents.parsers import get_default_file_extension
 
 DEFAULT_SINGLETON_INSTANCE_ID = 1
 
 
+class ModelWithOwner(models.Model):
+    owner = models.ForeignKey(
+        User,
+        blank=True,
+        null=True,
+        default=None,
+        on_delete=models.SET_NULL,
+        verbose_name=_("owner"),
+    )
+
+    class Meta:
+        abstract = True
+
+
+class MatchingModel(ModelWithOwner):
+    MATCH_NONE = 0
+    MATCH_ANY = 1
+    MATCH_ALL = 2
+    MATCH_LITERAL = 3
+    MATCH_REGEX = 4
+    MATCH_FUZZY = 5
+    MATCH_AUTO = 6
+
+    MATCHING_ALGORITHMS = (
+        (MATCH_NONE, _("None")),
+        (MATCH_ANY, _("Any word")),
+        (MATCH_ALL, _("All words")),
+        (MATCH_LITERAL, _("Exact match")),
+        (MATCH_REGEX, _("Regular expression")),
+        (MATCH_FUZZY, _("Fuzzy word")),
+        (MATCH_AUTO, _("Automatic")),
+    )
+
+    name = models.CharField(_("name"), max_length=128)
+
+    match = models.CharField(_("match"), max_length=256, blank=True)
+
+    matching_algorithm = models.PositiveIntegerField(
+        _("matching algorithm"),
+        choices=MATCHING_ALGORITHMS,
+        default=MATCH_ANY,
+    )
+
+    is_insensitive = models.BooleanField(_("is insensitive"), default=True)
+
+    class Meta:
+        abstract = True
+        ordering = ("name",)
+        constraints = [
+            models.UniqueConstraint(
+                fields=["name", "owner"],
+                name="%(app_label)s_%(class)s_unique_name_owner",
+            ),
+            models.UniqueConstraint(
+                name="%(app_label)s_%(class)s_name_uniq",
+                fields=["name"],
+                condition=models.Q(owner__isnull=True),
+            ),
+        ]
+
+    def __str__(self):
+        return self.name
+
+
+class Correspondent(MatchingModel):
+    class Meta(MatchingModel.Meta):
+        app_label = "documents"
+        verbose_name = _("correspondent")
+        verbose_name_plural = _("correspondents")
+
+
+class Tag(MatchingModel):
+    color = models.CharField(_("color"), max_length=7, default="#a6cee3")
+
+    is_inbox_tag = models.BooleanField(
+        _("is inbox tag"),
+        default=False,
+        help_text=_(
+            "Marks this tag as an inbox tag: All newly consumed "
+            "documents will be tagged with inbox tags.",
+        ),
+    )
+
+    class Meta(MatchingModel.Meta):
+        app_label = "documents"
+        verbose_name = _("tag")
+        verbose_name_plural = _("tags")
+
+
+class DocumentType(MatchingModel):
+    class Meta(MatchingModel.Meta):
+        app_label = "documents"
+        verbose_name = _("document type")
+        verbose_name_plural = _("document types")
+
+
+class StoragePath(MatchingModel):
+    path = models.TextField(
+        _("path"),
+    )
+
+    class Meta(MatchingModel.Meta):
+        app_label = "documents"
+        verbose_name = _("storage path")
+        verbose_name_plural = _("storage paths")
+
+
+class Document(SoftDeleteModel, ModelWithOwner):
+    STORAGE_TYPE_UNENCRYPTED = "unencrypted"
+    STORAGE_TYPE_GPG = "gpg"
+    STORAGE_TYPES = (
+        (STORAGE_TYPE_UNENCRYPTED, _("Unencrypted")),
+        (STORAGE_TYPE_GPG, _("Encrypted with GNU Privacy Guard")),
+    )
+
+    correspondent = models.ForeignKey(
+        Correspondent,
+        blank=True,
+        null=True,
+        related_name="documents",
+        on_delete=models.SET_NULL,
+        verbose_name=_("correspondent"),
+    )
+
+    storage_path = models.ForeignKey(
+        StoragePath,
+        blank=True,
+        null=True,
+        related_name="documents",
+        on_delete=models.SET_NULL,
+        verbose_name=_("storage path"),
+    )
+
+    title = models.CharField(_("title"), max_length=128, blank=True, db_index=True)
+
+    document_type = models.ForeignKey(
+        DocumentType,
+        blank=True,
+        null=True,
+        related_name="documents",
+        on_delete=models.SET_NULL,
+        verbose_name=_("document type"),
+    )
+
+    content = models.TextField(
+        _("content"),
+        blank=True,
+        help_text=_(
+            "The raw, text-only data of the document. This field is "
+            "primarily used for searching.",
+        ),
+    )
+
+    mime_type = models.CharField(_("mime type"), max_length=256, editable=False)
+
+    tags = models.ManyToManyField(
+        Tag,
+        related_name="documents",
+        blank=True,
+        verbose_name=_("tags"),
+    )
+
+    checksum = models.CharField(
+        _("checksum"),
+        max_length=32,
+        editable=False,
+        unique=True,
+        help_text=_("The checksum of the original document."),
+    )
+
+    archive_checksum = models.CharField(
+        _("archive checksum"),
+        max_length=32,
+        editable=False,
+        blank=True,
+        null=True,
+        help_text=_("The checksum of the archived document."),
+    )
+
+    page_count = models.PositiveIntegerField(
+        _("page count"),
+        blank=False,
+        null=True,
+        unique=False,
+        db_index=False,
+        validators=[MinValueValidator(1)],
+        help_text=_(
+            "The number of pages of the document.",
+        ),
+    )
+
+    created = models.DateTimeField(_("created"), default=timezone.now, db_index=True)
+
+    modified = models.DateTimeField(
+        _("modified"),
+        auto_now=True,
+        editable=False,
+        db_index=True,
+    )
+
+    storage_type = models.CharField(
+        _("storage type"),
+        max_length=11,
+        choices=STORAGE_TYPES,
+        default=STORAGE_TYPE_UNENCRYPTED,
+        editable=False,
+    )
+
+    added = models.DateTimeField(
+        _("added"),
+        default=timezone.now,
+        editable=False,
+        db_index=True,
+    )
+
+    filename = models.FilePathField(
+        _("filename"),
+        max_length=1024,
+        editable=False,
+        default=None,
+        unique=True,
+        null=True,
+        help_text=_("Current filename in storage"),
+    )
+
+    archive_filename = models.FilePathField(
+        _("archive filename"),
+        max_length=1024,
+        editable=False,
+        default=None,
+        unique=True,
+        null=True,
+        help_text=_("Current archive filename in storage"),
+    )
+
+    original_filename = models.CharField(
+        _("original filename"),
+        max_length=1024,
+        editable=False,
+        default=None,
+        unique=False,
+        null=True,
+        help_text=_("The original name of the file when it was uploaded"),
+    )
+
+    ARCHIVE_SERIAL_NUMBER_MIN: Final[int] = 0
+    ARCHIVE_SERIAL_NUMBER_MAX: Final[int] = 0xFF_FF_FF_FF
+
+    archive_serial_number = models.PositiveIntegerField(
+        _("archive serial number"),
+        blank=True,
+        null=True,
+        unique=True,
+        db_index=True,
+        validators=[
+            MaxValueValidator(ARCHIVE_SERIAL_NUMBER_MAX),
+            MinValueValidator(ARCHIVE_SERIAL_NUMBER_MIN),
+        ],
+        help_text=_(
+            "The position of this document in your physical document archive.",
+        ),
+    )
+
+    class Meta:
+        app_label = "documents"
+        ordering = ("-created",)
+        verbose_name = _("document")
+        verbose_name_plural = _("documents")
+
+    def __str__(self) -> str:
+        # Convert UTC database time to local time
+        created = datetime.date.isoformat(timezone.localdate(self.created))
+
+        res = f"{created}"
+
+        if self.correspondent:
+            res += f" {self.correspondent}"
+        if self.title:
+            res += f" {self.title}"
+        return res
+
+    @property
+    def source_path(self) -> Path:
+        if self.filename:
+            fname = str(self.filename)
+        else:
+            fname = f"{self.pk:07}{self.file_type}"
+            if self.storage_type == self.STORAGE_TYPE_GPG:
+                fname += ".gpg"  # pragma: no cover
+
+        return (settings.ORIGINALS_DIR / Path(fname)).resolve()
+
+    @property
+    def source_file(self):
+        return Path(self.source_path).open("rb")
+
+    @property
+    def has_archive_version(self) -> bool:
+        return self.archive_filename is not None
+
+    @property
+    def archive_path(self) -> Path | None:
+        if self.has_archive_version:
+            return (settings.ARCHIVE_DIR / Path(str(self.archive_filename))).resolve()
+        else:
+            return None
+
+    @property
+    def archive_file(self):
+        return Path(self.archive_path).open("rb")
+
+    def get_public_filename(self, *, archive=False, counter=0, suffix=None) -> str:
+        """
+        Returns a sanitized filename for the document, not including any paths.
+        """
+        result = str(self)
+
+        if counter:
+            result += f"_{counter:02}"
+
+        if suffix:
+            result += suffix
+
+        if archive:
+            result += ".pdf"
+        else:
+            result += self.file_type
+
+        return pathvalidate.sanitize_filename(result, replacement_text="-")
+
+    @property
+    def file_type(self):
+        return get_default_file_extension(self.mime_type)
+
+    @property
+    def thumbnail_path(self) -> Path:
+        webp_file_name = f"{self.pk:07}.webp"
+        if self.storage_type == self.STORAGE_TYPE_GPG:
+            webp_file_name += ".gpg"
+
+        webp_file_path = settings.THUMBNAIL_DIR / Path(webp_file_name)
+
+        return webp_file_path.resolve()
+
+    @property
+    def thumbnail_file(self):
+        return Path(self.thumbnail_path).open("rb")
+
+    @property
+    def created_date(self):
+        return timezone.localdate(self.created)
+
+
+class SavedView(ModelWithOwner):
+    class DisplayMode(models.TextChoices):
+        TABLE = ("table", _("Table"))
+        SMALL_CARDS = ("smallCards", _("Small Cards"))
+        LARGE_CARDS = ("largeCards", _("Large Cards"))
+
+    class DisplayFields(models.TextChoices):
+        TITLE = ("title", _("Title"))
+        CREATED = ("created", _("Created"))
+        ADDED = ("added", _("Added"))
+        TAGS = ("tag"), _("Tags")
+        CORRESPONDENT = ("correspondent", _("Correspondent"))
+        DOCUMENT_TYPE = ("documenttype", _("Document Type"))
+        STORAGE_PATH = ("storagepath", _("Storage Path"))
+        NOTES = ("note", _("Note"))
+        OWNER = ("owner", _("Owner"))
+        SHARED = ("shared", _("Shared"))
+        ASN = ("asn", _("ASN"))
+        PAGE_COUNT = ("pagecount", _("Pages"))
+        CUSTOM_FIELD = ("custom_field_%d", ("Custom Field"))
+
+    name = models.CharField(_("name"), max_length=128)
+
+    show_on_dashboard = models.BooleanField(
+        _("show on dashboard"),
+    )
+    show_in_sidebar = models.BooleanField(
+        _("show in sidebar"),
+    )
+
+    sort_field = models.CharField(
+        _("sort field"),
+        max_length=128,
+        null=True,
+        blank=True,
+    )
+    sort_reverse = models.BooleanField(_("sort reverse"), default=False)
+
+    page_size = models.PositiveIntegerField(
+        _("View page size"),
+        null=True,
+        blank=True,
+        validators=[MinValueValidator(1)],
+    )
+
+    display_mode = models.CharField(
+        max_length=128,
+        verbose_name=_("View display mode"),
+        choices=DisplayMode.choices,
+        null=True,
+        blank=True,
+    )
+
+    display_fields = models.JSONField(
+        verbose_name=_("Document display fields"),
+        null=True,
+        blank=True,
+    )
+
+    class Meta:
+        app_label = "documents"
+        ordering = ("name",)
+        verbose_name = _("saved view")
+        verbose_name_plural = _("saved views")
+
+    def __str__(self):
+        return f"SavedView {self.name}"
+
+
+class SavedViewFilterRule(models.Model):
+    RULE_TYPES = [
+        (0, _("title contains")),
+        (1, _("content contains")),
+        (2, _("ASN is")),
+        (3, _("correspondent is")),
+        (4, _("document type is")),
+        (5, _("is in inbox")),
+        (6, _("has tag")),
+        (7, _("has any tag")),
+        (8, _("created before")),
+        (9, _("created after")),
+        (10, _("created year is")),
+        (11, _("created month is")),
+        (12, _("created day is")),
+        (13, _("added before")),
+        (14, _("added after")),
+        (15, _("modified before")),
+        (16, _("modified after")),
+        (17, _("does not have tag")),
+        (18, _("does not have ASN")),
+        (19, _("title or content contains")),
+        (20, _("fulltext query")),
+        (21, _("more like this")),
+        (22, _("has tags in")),
+        (23, _("ASN greater than")),
+        (24, _("ASN less than")),
+        (25, _("storage path is")),
+        (26, _("has correspondent in")),
+        (27, _("does not have correspondent in")),
+        (28, _("has document type in")),
+        (29, _("does not have document type in")),
+        (30, _("has storage path in")),
+        (31, _("does not have storage path in")),
+        (32, _("owner is")),
+        (33, _("has owner in")),
+        (34, _("does not have owner")),
+        (35, _("does not have owner in")),
+        (36, _("has custom field value")),
+        (37, _("is shared by me")),
+        (38, _("has custom fields")),
+        (39, _("has custom field in")),
+        (40, _("does not have custom field in")),
+        (41, _("does not have custom field")),
+        (42, _("custom fields query")),
+        (43, _("created to")),
+        (44, _("created from")),
+        (45, _("added to")),
+        (46, _("added from")),
+        (47, _("mime type is")),
+    ]
+
+    saved_view = models.ForeignKey(
+        SavedView,
+        on_delete=models.CASCADE,
+        related_name="filter_rules",
+        verbose_name=_("saved view"),
+    )
+
+    rule_type = models.PositiveIntegerField(_("rule type"), choices=RULE_TYPES)
+
+    value = models.CharField(_("value"), max_length=255, blank=True, null=True)
+
+    class Meta:
+        app_label = "documents"
+        verbose_name = _("filter rule")
+        verbose_name_plural = _("filter rules")
+
+    def __str__(self) -> str:
+        return f"SavedViewFilterRule: {self.rule_type} : {self.value}"
+
+
+# Extending User Model Using a One-To-One Link
+class UiSettings(models.Model):
+    user = models.OneToOneField(
+        User,
+        on_delete=models.CASCADE,
+        related_name="ui_settings",
+    )
+    settings = models.JSONField(null=True)
+
+    class Meta:
+        app_label = "documents"
+
+    def __str__(self):
+        return self.user.username
+
+
+class PaperlessTask(ModelWithOwner):
+    ALL_STATES = sorted(states.ALL_STATES)
+    TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))
+
+    class TaskType(models.TextChoices):
+        AUTO = ("auto_task", _("Auto Task"))
+        SCHEDULED_TASK = ("scheduled_task", _("Scheduled Task"))
+        MANUAL_TASK = ("manual_task", _("Manual Task"))
+
+    class TaskName(models.TextChoices):
+        CONSUME_FILE = ("consume_file", _("Consume File"))
+        TRAIN_CLASSIFIER = ("train_classifier", _("Train Classifier"))
+        CHECK_SANITY = ("check_sanity", _("Check Sanity"))
+        INDEX_OPTIMIZE = ("index_optimize", _("Index Optimize"))
+
+    task_id = models.CharField(
+        max_length=255,
+        unique=True,
+        verbose_name=_("Task ID"),
+        help_text=_("Celery ID for the Task that was run"),
+    )
+
+    acknowledged = models.BooleanField(
+        default=False,
+        verbose_name=_("Acknowledged"),
+        help_text=_("If the task is acknowledged via the frontend or API"),
+    )
+
+    task_file_name = models.CharField(
+        null=True,
+        max_length=255,
+        verbose_name=_("Task Filename"),
+        help_text=_("Name of the file which the Task was run for"),
+    )
+
+    task_name = models.CharField(
+        null=True,
+        max_length=255,
+        choices=TaskName.choices,
+        verbose_name=_("Task Name"),
+        help_text=_("Name of the task that was run"),
+    )
+
+    status = models.CharField(
+        max_length=30,
+        default=states.PENDING,
+        choices=TASK_STATE_CHOICES,
+        verbose_name=_("Task State"),
+        help_text=_("Current state of the task being run"),
+    )
+
+    date_created = models.DateTimeField(
+        null=True,
+        default=timezone.now,
+        verbose_name=_("Created DateTime"),
+        help_text=_("Datetime field when the task result was created in UTC"),
+    )
+
+    date_started = models.DateTimeField(
+        null=True,
+        default=None,
+        verbose_name=_("Started DateTime"),
+        help_text=_("Datetime field when the task was started in UTC"),
+    )
+
+    date_done = models.DateTimeField(
+        null=True,
+        default=None,
+        verbose_name=_("Completed DateTime"),
+        help_text=_("Datetime field when the task was completed in UTC"),
+    )
+
+    result = models.TextField(
+        null=True,
+        default=None,
+        verbose_name=_("Result Data"),
+        help_text=_(
+            "The data returned by the task",
+        ),
+    )
+
+    type = models.CharField(
+        max_length=30,
+        choices=TaskType.choices,
+        default=TaskType.AUTO,
+        verbose_name=_("Task Type"),
+        help_text=_("The type of task that was run"),
+    )
+
+    class Meta:
+        app_label = "documents"
+
+    def __str__(self) -> str:
+        return f"Task {self.task_id}"
+
+
+class Note(SoftDeleteModel):
+    note = models.TextField(
+        _("content"),
+        blank=True,
+        help_text=_("Note for the document"),
+    )
+
+    created = models.DateTimeField(
+        _("created"),
+        default=timezone.now,
+        db_index=True,
+    )
+
+    document = models.ForeignKey(
+        Document,
+        blank=True,
+        null=True,
+        related_name="notes",
+        on_delete=models.CASCADE,
+        verbose_name=_("document"),
+    )
+
+    user = models.ForeignKey(
+        User,
+        blank=True,
+        null=True,
+        related_name="notes",
+        on_delete=models.SET_NULL,
+        verbose_name=_("user"),
+    )
+
+    class Meta:
+        app_label = "documents"
+        ordering = ("created",)
+        verbose_name = _("note")
+        verbose_name_plural = _("notes")
+
+    def __str__(self):
+        return self.note
+
+
+class ShareLink(SoftDeleteModel):
+    class FileVersion(models.TextChoices):
+        ARCHIVE = ("archive", _("Archive"))
+        ORIGINAL = ("original", _("Original"))
+
+    created = models.DateTimeField(
+        _("created"),
+        default=timezone.now,
+        db_index=True,
+        blank=True,
+        editable=False,
+    )
+
+    expiration = models.DateTimeField(
+        _("expiration"),
+        blank=True,
+        null=True,
+        db_index=True,
+    )
+
+    slug = models.SlugField(
+        _("slug"),
+        db_index=True,
+        unique=True,
+        blank=True,
+        editable=False,
+    )
+
+    document = models.ForeignKey(
+        Document,
+        blank=True,
+        related_name="share_links",
+        on_delete=models.CASCADE,
+        verbose_name=_("document"),
+    )
+
+    file_version = models.CharField(
+        max_length=50,
+        choices=FileVersion.choices,
+        default=FileVersion.ARCHIVE,
+    )
+
+    owner = models.ForeignKey(
+        User,
+        blank=True,
+        null=True,
+        related_name="share_links",
+        on_delete=models.SET_NULL,
+        verbose_name=_("owner"),
+    )
+
+    class Meta:
+        app_label = "documents"
+        ordering = ("created",)
+        verbose_name = _("share link")
+        verbose_name_plural = _("share links")
+
+    def __str__(self):
+        return f"Share Link for {self.document.title}"
+
+
+class CustomField(models.Model):
+    """
+    Defines the name and type of a custom field
+    """
+
+    class FieldDataType(models.TextChoices):
+        STRING = ("string", _("String"))
+        URL = ("url", _("URL"))
+        DATE = ("date", _("Date"))
+        BOOL = ("boolean"), _("Boolean")
+        INT = ("integer", _("Integer"))
+        FLOAT = ("float", _("Float"))
+        MONETARY = ("monetary", _("Monetary"))
+        DOCUMENTLINK = ("documentlink", _("Document Link"))
+        SELECT = ("select", _("Select"))
+
+    created = models.DateTimeField(
+        _("created"),
+        default=timezone.now,
+        db_index=True,
+        editable=False,
+    )
+
+    name = models.CharField(max_length=128)
+
+    data_type = models.CharField(
+        _("data type"),
+        max_length=50,
+        choices=FieldDataType.choices,
+        editable=False,
+    )
+
+    extra_data = models.JSONField(
+        _("extra data"),
+        null=True,
+        blank=True,
+        help_text=_(
+            "Extra data for the custom field, such as select options",
+        ),
+    )
+
+    class Meta:
+        app_label = "documents"
+        ordering = ("created",)
+        verbose_name = _("custom field")
+        verbose_name_plural = _("custom fields")
+        constraints = [
+            models.UniqueConstraint(
+                fields=["name"],
+                name="%(app_label)s_%(class)s_unique_name",
+            ),
+        ]
+
+    def __str__(self) -> str:
+        return f"{self.name} : {self.data_type}"
+
+
+class CustomFieldInstance(SoftDeleteModel):
+    """
+    A single instance of a field, attached to a CustomField for the name and type
+    and attached to a single Document to be metadata for it
+    """
+
+    TYPE_TO_DATA_STORE_NAME_MAP = {
+        CustomField.FieldDataType.STRING: "value_text",
+        CustomField.FieldDataType.URL: "value_url",
+        CustomField.FieldDataType.DATE: "value_date",
+        CustomField.FieldDataType.BOOL: "value_bool",
+        CustomField.FieldDataType.INT: "value_int",
+        CustomField.FieldDataType.FLOAT: "value_float",
+        CustomField.FieldDataType.MONETARY: "value_monetary",
+        CustomField.FieldDataType.DOCUMENTLINK: "value_document_ids",
+        CustomField.FieldDataType.SELECT: "value_select",
+    }
+
+    created = models.DateTimeField(
+        _("created"),
+        default=timezone.now,
+        db_index=True,
+        editable=False,
+    )
+
+    document = models.ForeignKey(
+        Document,
+        blank=False,
+        null=False,
+        on_delete=models.CASCADE,
+        related_name="custom_fields",
+        editable=False,
+    )
+
+    field = models.ForeignKey(
+        CustomField,
+        blank=False,
+        null=False,
+        on_delete=models.CASCADE,
+        related_name="fields",
+        editable=False,
+    )
+
+    # Actual data storage
+    value_text = models.CharField(max_length=128, null=True)
+
+    value_bool = models.BooleanField(null=True)
+
+    value_url = models.URLField(null=True)
+
+    value_date = models.DateField(null=True)
+
+    value_int = models.IntegerField(null=True)
+
+    value_float = models.FloatField(null=True)
+
+    value_monetary = models.CharField(null=True, max_length=128)
+
+    value_monetary_amount = models.GeneratedField(
+        expression=Case(
+            # If the value starts with a number and no currency symbol, use the whole string
+            models.When(
+                value_monetary__regex=r"^\d+",
+                then=Cast(
+                    Substr("value_monetary", 1),
+                    output_field=models.DecimalField(decimal_places=2, max_digits=65),
+                ),
+            ),
+            # If the value starts with a 3-char currency symbol, use the rest of the string
+            default=Cast(
+                Substr("value_monetary", 4),
+                output_field=models.DecimalField(decimal_places=2, max_digits=65),
+            ),
+            output_field=models.DecimalField(decimal_places=2, max_digits=65),
+        ),
+        output_field=models.DecimalField(decimal_places=2, max_digits=65),
+        db_persist=True,
+    )
+
+    value_document_ids = models.JSONField(null=True)
+
+    value_select = models.CharField(null=True, max_length=16)
+
+    class Meta:
+        app_label = "documents"
+        ordering = ("created",)
+        verbose_name = _("custom field instance")
+        verbose_name_plural = _("custom field instances")
+        constraints = [
+            models.UniqueConstraint(
+                fields=["document", "field"],
+                name="%(app_label)s_%(class)s_unique_document_field",
+            ),
+        ]
+
+    def __str__(self) -> str:
+        value = (
+            next(
+                option.get("label")
+                for option in self.field.extra_data["select_options"]
+                if option.get("id") == self.value_select
+            )
+            if (
+                self.field.data_type == CustomField.FieldDataType.SELECT
+                and self.value_select is not None
+            )
+            else self.value
+        )
+        return str(self.field.name) + f" : {value}"
+
+    @classmethod
+    def get_value_field_name(cls, data_type: CustomField.FieldDataType):
+        try:
+            return cls.TYPE_TO_DATA_STORE_NAME_MAP[data_type]
+        except KeyError:  # pragma: no cover
+            raise NotImplementedError(data_type)
+
+    @property
+    def value(self):
+        """
+        Based on the data type, access the actual value the instance stores
+        A little shorthand/quick way to get what is actually here
+        """
+        value_field_name = self.get_value_field_name(self.field.data_type)
+        return getattr(self, value_field_name)
+
+
+if settings.AUDIT_LOG_ENABLED:
+    auditlog.register(
+        Document,
+        m2m_fields={"tags"},
+        exclude_fields=["modified"],
+    )
+    auditlog.register(Correspondent)
+    auditlog.register(Tag)
+    auditlog.register(DocumentType)
+    auditlog.register(Note)
+    auditlog.register(CustomField)
+    auditlog.register(CustomFieldInstance)
+
+
+class WorkflowTrigger(models.Model):
+    class WorkflowTriggerMatching(models.IntegerChoices):
+        # No auto matching
+        NONE = MatchingModel.MATCH_NONE, _("None")
+        ANY = MatchingModel.MATCH_ANY, _("Any word")
+        ALL = MatchingModel.MATCH_ALL, _("All words")
+        LITERAL = MatchingModel.MATCH_LITERAL, _("Exact match")
+        REGEX = MatchingModel.MATCH_REGEX, _("Regular expression")
+        FUZZY = MatchingModel.MATCH_FUZZY, _("Fuzzy word")
+
+    class WorkflowTriggerType(models.IntegerChoices):
+        CONSUMPTION = 1, _("Consumption Started")
+        DOCUMENT_ADDED = 2, _("Document Added")
+        DOCUMENT_UPDATED = 3, _("Document Updated")
+        SCHEDULED = 4, _("Scheduled")
+
+    class DocumentSourceChoices(models.IntegerChoices):
+        CONSUME_FOLDER = DocumentSource.ConsumeFolder.value, _("Consume Folder")
+        API_UPLOAD = DocumentSource.ApiUpload.value, _("Api Upload")
+        MAIL_FETCH = DocumentSource.MailFetch.value, _("Mail Fetch")
+        WEB_UI = DocumentSource.WebUI.value, _("Web UI")
+
+    class ScheduleDateField(models.TextChoices):
+        ADDED = "added", _("Added")
+        CREATED = "created", _("Created")
+        MODIFIED = "modified", _("Modified")
+        CUSTOM_FIELD = "custom_field", _("Custom Field")
+
+    type = models.PositiveIntegerField(
+        _("Workflow Trigger Type"),
+        choices=WorkflowTriggerType.choices,
+        default=WorkflowTriggerType.CONSUMPTION,
+    )
+
+    sources = MultiSelectField(
+        max_length=7,
+        choices=DocumentSourceChoices.choices,
+        default=f"{DocumentSource.ConsumeFolder},{DocumentSource.ApiUpload},{DocumentSource.MailFetch},{DocumentSource.WebUI}",
+    )
+
+    filter_path = models.CharField(
+        _("filter path"),
+        max_length=256,
+        null=True,
+        blank=True,
+        help_text=_(
+            "Only consume documents with a path that matches "
+            "this if specified. Wildcards specified as * are "
+            "allowed. Case insensitive.",
+        ),
+    )
+
+    filter_filename = models.CharField(
+        _("filter filename"),
+        max_length=256,
+        null=True,
+        blank=True,
+        help_text=_(
+            "Only consume documents which entirely match this "
+            "filename if specified. Wildcards such as *.pdf or "
+            "*invoice* are allowed. Case insensitive.",
+        ),
+    )
+
+    filter_mailrule = models.ForeignKey(
+        "paperless_mail.MailRule",
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        verbose_name=_("filter documents from this mail rule"),
+    )
+
+    match = models.CharField(_("match"), max_length=256, blank=True)
+
+    matching_algorithm = models.PositiveIntegerField(
+        _("matching algorithm"),
+        choices=WorkflowTriggerMatching.choices,
+        default=WorkflowTriggerMatching.NONE,
+    )
+
+    is_insensitive = models.BooleanField(_("is insensitive"), default=True)
+
+    filter_has_tags = models.ManyToManyField(
+        Tag,
+        blank=True,
+        verbose_name=_("has these tag(s)"),
+    )
+
+    filter_has_document_type = models.ForeignKey(
+        DocumentType,
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        verbose_name=_("has this document type"),
+    )
+
+    filter_has_correspondent = models.ForeignKey(
+        Correspondent,
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        verbose_name=_("has this correspondent"),
+    )
+
+    schedule_offset_days = models.PositiveIntegerField(
+        _("schedule offset days"),
+        default=0,
+        help_text=_(
+            "The number of days to offset the schedule trigger by.",
+        ),
+    )
+
+    schedule_is_recurring = models.BooleanField(
+        _("schedule is recurring"),
+        default=False,
+        help_text=_(
+            "If the schedule should be recurring.",
+        ),
+    )
+
+    schedule_recurring_interval_days = models.PositiveIntegerField(
+        _("schedule recurring delay in days"),
+        default=1,
+        validators=[MinValueValidator(1)],
+        help_text=_(
+            "The number of days between recurring schedule triggers.",
+        ),
+    )
+
+    schedule_date_field = models.CharField(
+        _("schedule date field"),
+        max_length=20,
+        choices=ScheduleDateField.choices,
+        default=ScheduleDateField.ADDED,
+        help_text=_(
+            "The field to check for a schedule trigger.",
+        ),
+    )
+
+    schedule_date_custom_field = models.ForeignKey(
+        CustomField,
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        verbose_name=_("schedule date custom field"),
+    )
+
+    class Meta:
+        app_label = "documents"
+        verbose_name = _("workflow trigger")
+        verbose_name_plural = _("workflow triggers")
+
+    def __str__(self):
+        return f"WorkflowTrigger {self.pk}"
+
+
+class WorkflowActionEmail(models.Model):
+    subject = models.CharField(
+        _("email subject"),
+        max_length=256,
+        null=False,
+        help_text=_(
+            "The subject of the email, can include some placeholders, "
+            "see documentation.",
+        ),
+    )
+
+    body = models.TextField(
+        _("email body"),
+        null=False,
+        help_text=_(
+            "The body (message) of the email, can include some placeholders, "
+            "see documentation.",
+        ),
+    )
+
+    to = models.TextField(
+        _("emails to"),
+        null=False,
+        help_text=_(
+            "The destination email addresses, comma separated.",
+        ),
+    )
+
+    include_document = models.BooleanField(
+        default=False,
+        verbose_name=_("include document in email"),
+    )
+
+    class Meta:
+        app_label = "documents"
+
+    def __str__(self):
+        return f"Workflow Email Action {self.pk}"
+
+
+class WorkflowActionWebhook(models.Model):
+    # We dont use the built-in URLField because it is not flexible enough
+    # validation is handled in the serializer
+    url = models.CharField(
+        _("webhook url"),
+        null=False,
+        max_length=256,
+        help_text=_("The destination URL for the notification."),
+    )
+
+    use_params = models.BooleanField(
+        default=True,
+        verbose_name=_("use parameters"),
+    )
+
+    as_json = models.BooleanField(
+        default=False,
+        verbose_name=_("send as JSON"),
+    )
+
+    params = models.JSONField(
+        _("webhook parameters"),
+        null=True,
+        blank=True,
+        help_text=_("The parameters to send with the webhook URL if body not used."),
+    )
+
+    body = models.TextField(
+        _("webhook body"),
+        null=True,
+        blank=True,
+        help_text=_("The body to send with the webhook URL if parameters not used."),
+    )
+
+    headers = models.JSONField(
+        _("webhook headers"),
+        null=True,
+        blank=True,
+        help_text=_("The headers to send with the webhook URL."),
+    )
+
+    include_document = models.BooleanField(
+        default=False,
+        verbose_name=_("include document in webhook"),
+    )
+
+    class Meta:
+        app_label = "documents"
+
+    def __str__(self):
+        return f"Workflow Webhook Action {self.pk}"
+
+
+class WorkflowAction(models.Model):
+    class WorkflowActionType(models.IntegerChoices):
+        ASSIGNMENT = (
+            1,
+            _("Assignment"),
+        )
+        REMOVAL = (
+            2,
+            _("Removal"),
+        )
+        EMAIL = (
+            3,
+            _("Email"),
+        )
+        WEBHOOK = (
+            4,
+            _("Webhook"),
+        )
+
+    type = models.PositiveIntegerField(
+        _("Workflow Action Type"),
+        choices=WorkflowActionType.choices,
+        default=WorkflowActionType.ASSIGNMENT,
+    )
+
+    assign_title = models.CharField(
+        _("assign title"),
+        max_length=256,
+        null=True,
+        blank=True,
+        help_text=_(
+            "Assign a document title, can include some placeholders, "
+            "see documentation.",
+        ),
+    )
+
+    assign_tags = models.ManyToManyField(
+        Tag,
+        blank=True,
+        related_name="+",
+        verbose_name=_("assign this tag"),
+    )
+
+    assign_document_type = models.ForeignKey(
+        DocumentType,
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name="+",
+        verbose_name=_("assign this document type"),
+    )
+
+    assign_correspondent = models.ForeignKey(
+        Correspondent,
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name="+",
+        verbose_name=_("assign this correspondent"),
+    )
+
+    assign_storage_path = models.ForeignKey(
+        StoragePath,
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name="+",
+        verbose_name=_("assign this storage path"),
+    )
+
+    assign_owner = models.ForeignKey(
+        User,
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name="+",
+        verbose_name=_("assign this owner"),
+    )
+
+    assign_view_users = models.ManyToManyField(
+        User,
+        blank=True,
+        related_name="+",
+        verbose_name=_("grant view permissions to these users"),
+    )
+
+    assign_view_groups = models.ManyToManyField(
+        Group,
+        blank=True,
+        related_name="+",
+        verbose_name=_("grant view permissions to these groups"),
+    )
+
+    assign_change_users = models.ManyToManyField(
+        User,
+        blank=True,
+        related_name="+",
+        verbose_name=_("grant change permissions to these users"),
+    )
+
+    assign_change_groups = models.ManyToManyField(
+        Group,
+        blank=True,
+        related_name="+",
+        verbose_name=_("grant change permissions to these groups"),
+    )
+
+    assign_custom_fields = models.ManyToManyField(
+        CustomField,
+        blank=True,
+        related_name="+",
+        verbose_name=_("assign these custom fields"),
+    )
+
+    assign_custom_fields_values = models.JSONField(
+        _("custom field values"),
+        null=True,
+        blank=True,
+        help_text=_(
+            "Optional values to assign to the custom fields.",
+        ),
+        default=dict,
+    )
+
+    remove_tags = models.ManyToManyField(
+        Tag,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove these tag(s)"),
+    )
+
+    remove_all_tags = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all tags"),
+    )
+
+    remove_document_types = models.ManyToManyField(
+        DocumentType,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove these document type(s)"),
+    )
+
+    remove_all_document_types = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all document types"),
+    )
+
+    remove_correspondents = models.ManyToManyField(
+        Correspondent,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove these correspondent(s)"),
+    )
+
+    remove_all_correspondents = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all correspondents"),
+    )
+
+    remove_storage_paths = models.ManyToManyField(
+        StoragePath,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove these storage path(s)"),
+    )
+
+    remove_all_storage_paths = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all storage paths"),
+    )
+
+    remove_owners = models.ManyToManyField(
+        User,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove these owner(s)"),
+    )
+
+    remove_all_owners = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all owners"),
+    )
+
+    remove_view_users = models.ManyToManyField(
+        User,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove view permissions for these users"),
+    )
+
+    remove_view_groups = models.ManyToManyField(
+        Group,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove view permissions for these groups"),
+    )
+
+    remove_change_users = models.ManyToManyField(
+        User,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove change permissions for these users"),
+    )
+
+    remove_change_groups = models.ManyToManyField(
+        Group,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove change permissions for these groups"),
+    )
+
+    remove_all_permissions = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all permissions"),
+    )
+
+    remove_custom_fields = models.ManyToManyField(
+        CustomField,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove these custom fields"),
+    )
+
+    remove_all_custom_fields = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all custom fields"),
+    )
+
+    email = models.ForeignKey(
+        WorkflowActionEmail,
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name="action",
+        verbose_name=_("email"),
+    )
+
+    webhook = models.ForeignKey(
+        WorkflowActionWebhook,
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL,
+        related_name="action",
+        verbose_name=_("webhook"),
+    )
+
+    class Meta:
+        app_label = "documents"
+        verbose_name = _("workflow action")
+        verbose_name_plural = _("workflow actions")
+
+    def __str__(self):
+        return f"WorkflowAction {self.pk}"
+
+
+class Workflow(models.Model):
+    name = models.CharField(_("name"), max_length=256, unique=True)
+
+    order = models.IntegerField(_("order"), default=0)
+
+    triggers = models.ManyToManyField(
+        WorkflowTrigger,
+        related_name="workflows",
+        blank=False,
+        verbose_name=_("triggers"),
+    )
+
+    actions = models.ManyToManyField(
+        WorkflowAction,
+        related_name="workflows",
+        blank=False,
+        verbose_name=_("actions"),
+    )
+
+    enabled = models.BooleanField(_("enabled"), default=True)
+
+    class Meta:
+        app_label = "documents"
+
+    def __str__(self):
+        return f"Workflow: {self.name}"
+
+
+class WorkflowRun(models.Model):
+    workflow = models.ForeignKey(
+        Workflow,
+        on_delete=models.CASCADE,
+        related_name="runs",
+        verbose_name=_("workflow"),
+    )
+
+    type = models.PositiveIntegerField(
+        _("workflow trigger type"),
+        choices=WorkflowTrigger.WorkflowTriggerType.choices,
+        null=True,
+    )
+
+    document = models.ForeignKey(
+        Document,
+        null=True,
+        on_delete=models.CASCADE,
+        related_name="workflow_runs",
+        verbose_name=_("document"),
+    )
+
+    run_at = models.DateTimeField(
+        _("date run"),
+        default=timezone.now,
+        db_index=True,
+    )
+
+    class Meta:
+        app_label = "documents"
+        verbose_name = _("workflow run")
+        verbose_name_plural = _("workflow runs")
+
+    def __str__(self):
+        return f"WorkflowRun of {self.workflow} at {self.run_at} on {self.document}"
+
+
 class AbstractSingletonModel(models.Model):
     class Meta:
         abstract = True
index cf35ea6cbbf031c3bd4cb4aa2f5b7d6a42416b08..906804aa3271119ca1753229d1b1ffc21b9da819 100644 (file)
@@ -37,9 +37,9 @@ from documents.data_models import ConsumableDocument
 from documents.data_models import DocumentMetadataOverrides
 from documents.data_models import DocumentSource
 from documents.loggers import LoggingMixin
-from documents.models import Correspondent
 from documents.parsers import is_mime_type_supported
 from documents.tasks import consume_file
+from paperless.models import Correspondent
 from paperless_mail.models import MailAccount
 from paperless_mail.models import MailRule
 from paperless_mail.models import ProcessedMail
index cf33a056b6508cf02841cd427651a37c604edb7c..138385e3f3262b98a2c014adefcbcc97510eab56 100644 (file)
@@ -2,7 +2,7 @@ from django.db import models
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
 
-import documents.models as document_models
+import paperless.models as document_models
 
 
 class MailAccount(document_models.ModelWithOwner):
index 985ed006b08c8672cb3426eb2fd3b397b74eaeae..9171395ec6a20b12a555bd300e4f3b1c214b3058 100644 (file)
@@ -7,10 +7,10 @@ from guardian.shortcuts import assign_perm
 from rest_framework import status
 from rest_framework.test import APITestCase
 
-from documents.models import Correspondent
-from documents.models import DocumentType
-from documents.models import Tag
 from documents.tests.utils import DirectoriesMixin
+from paperless.models import Correspondent
+from paperless.models import DocumentType
+from paperless.models import Tag
 from paperless_mail.models import MailAccount
 from paperless_mail.models import MailRule
 from paperless_mail.tests.test_mail import BogusMailBox
index a73f9cf34e532600004c55c04d26a852cfc68c1a..97ff5373d06f8782c5eb637c38dc29cf2891b21c 100644 (file)
@@ -24,9 +24,9 @@ from imap_tools import errors
 from rest_framework import status
 from rest_framework.test import APITestCase
 
-from documents.models import Correspondent
 from documents.tests.utils import DirectoriesMixin
 from documents.tests.utils import FileSystemAssertsMixin
+from paperless.models import Correspondent
 from paperless_mail import tasks
 from paperless_mail.mail import MailAccountHandler
 from paperless_mail.mail import MailError