diff --git a/docker/management_script.sh b/docker/management_script.sh
index 1fa31c372..91a6336d0 100755
--- a/docker/management_script.sh
+++ b/docker/management_script.sh
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py management_command "$@"
-elif [[ $(id -un) == "paperless" ]]; then
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
python3 manage.py management_command "$@"
-else
- echo "Unknown user."
+elif [[ $(id -un) == "paperless" ]]; then
+ s6-setuidgid paperless python3 manage.py management_command "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/convert_mariadb_uuid b/docker/rootfs/usr/local/bin/convert_mariadb_uuid
index 806a98f3b..019c558f1 100755
--- a/docker/rootfs/usr/local/bin/convert_mariadb_uuid
+++ b/docker/rootfs/usr/local/bin/convert_mariadb_uuid
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py convert_mariadb_uuid "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py convert_mariadb_uuid "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py convert_mariadb_uuid "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py convert_mariadb_uuid "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/createsuperuser b/docker/rootfs/usr/local/bin/createsuperuser
index f931952ba..2b56869f6 100755
--- a/docker/rootfs/usr/local/bin/createsuperuser
+++ b/docker/rootfs/usr/local/bin/createsuperuser
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py createsuperuser "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py createsuperuser "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py createsuperuser "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py createsuperuser "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/document_archiver b/docker/rootfs/usr/local/bin/document_archiver
index 383acfcc6..8d7771d26 100755
--- a/docker/rootfs/usr/local/bin/document_archiver
+++ b/docker/rootfs/usr/local/bin/document_archiver
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py document_archiver "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py document_archiver "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py document_archiver "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py document_archiver "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/document_create_classifier b/docker/rootfs/usr/local/bin/document_create_classifier
index 72dc33d6f..23acc6741 100755
--- a/docker/rootfs/usr/local/bin/document_create_classifier
+++ b/docker/rootfs/usr/local/bin/document_create_classifier
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py document_create_classifier "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py document_create_classifier "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py document_create_classifier "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py document_create_classifier "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/document_exporter b/docker/rootfs/usr/local/bin/document_exporter
index 7f48215d7..d55f01d48 100755
--- a/docker/rootfs/usr/local/bin/document_exporter
+++ b/docker/rootfs/usr/local/bin/document_exporter
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py document_exporter "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py document_exporter "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py document_exporter "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py document_exporter "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/document_fuzzy_match b/docker/rootfs/usr/local/bin/document_fuzzy_match
index 5b9548557..c6e4edadc 100755
--- a/docker/rootfs/usr/local/bin/document_fuzzy_match
+++ b/docker/rootfs/usr/local/bin/document_fuzzy_match
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py document_fuzzy_match "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py document_fuzzy_match "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py document_fuzzy_match "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py document_fuzzy_match "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/document_importer b/docker/rootfs/usr/local/bin/document_importer
index 2286e89f7..07c92bb04 100755
--- a/docker/rootfs/usr/local/bin/document_importer
+++ b/docker/rootfs/usr/local/bin/document_importer
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py document_importer "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py document_importer "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py document_importer "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py document_importer "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/document_index b/docker/rootfs/usr/local/bin/document_index
index 2d518b5c5..47c893c10 100755
--- a/docker/rootfs/usr/local/bin/document_index
+++ b/docker/rootfs/usr/local/bin/document_index
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py document_index "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py document_index "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py document_index "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py document_index "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/document_renamer b/docker/rootfs/usr/local/bin/document_renamer
index 326317a73..3406182ee 100755
--- a/docker/rootfs/usr/local/bin/document_renamer
+++ b/docker/rootfs/usr/local/bin/document_renamer
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py document_renamer "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py document_renamer "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py document_renamer "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py document_renamer "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/document_retagger b/docker/rootfs/usr/local/bin/document_retagger
index 3bab3e790..b0d1047ff 100755
--- a/docker/rootfs/usr/local/bin/document_retagger
+++ b/docker/rootfs/usr/local/bin/document_retagger
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py document_retagger "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py document_retagger "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py document_retagger "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py document_retagger "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/document_sanity_checker b/docker/rootfs/usr/local/bin/document_sanity_checker
index 5c0c29ef2..d792124fc 100755
--- a/docker/rootfs/usr/local/bin/document_sanity_checker
+++ b/docker/rootfs/usr/local/bin/document_sanity_checker
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py document_sanity_checker "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py document_sanity_checker "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py document_sanity_checker "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py document_sanity_checker "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/document_thumbnails b/docker/rootfs/usr/local/bin/document_thumbnails
index c1000c31a..71d80e00d 100755
--- a/docker/rootfs/usr/local/bin/document_thumbnails
+++ b/docker/rootfs/usr/local/bin/document_thumbnails
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py document_thumbnails "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py document_thumbnails "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py document_thumbnails "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py document_thumbnails "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/mail_fetcher b/docker/rootfs/usr/local/bin/mail_fetcher
index 2ae1d1dfb..654c07389 100755
--- a/docker/rootfs/usr/local/bin/mail_fetcher
+++ b/docker/rootfs/usr/local/bin/mail_fetcher
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py mail_fetcher "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py mail_fetcher "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py mail_fetcher "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py mail_fetcher "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/manage_superuser b/docker/rootfs/usr/local/bin/manage_superuser
index 9f7f37ecf..a6e41168c 100755
--- a/docker/rootfs/usr/local/bin/manage_superuser
+++ b/docker/rootfs/usr/local/bin/manage_superuser
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py manage_superuser "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py manage_superuser "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py manage_superuser "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py manage_superuser "$@"
fi
diff --git a/docker/rootfs/usr/local/bin/prune_audit_logs b/docker/rootfs/usr/local/bin/prune_audit_logs
index b9142e98e..04446df17 100755
--- a/docker/rootfs/usr/local/bin/prune_audit_logs
+++ b/docker/rootfs/usr/local/bin/prune_audit_logs
@@ -5,10 +5,8 @@ set -e
cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]]; then
- s6-setuidgid paperless python3 manage.py prune_audit_logs "$@"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py prune_audit_logs "$@"
elif [[ $(id -un) == "paperless" ]]; then
- python3 manage.py prune_audit_logs "$@"
-else
- echo "Unknown user."
+ s6-setuidgid paperless python3 manage.py prune_audit_logs "$@"
fi
diff --git a/pyproject.toml b/pyproject.toml
index 097e2c19b..500461199 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -257,7 +257,7 @@ lint.isort.force-single-line = true
[tool.codespell]
write-changes = true
-ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober"
+ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober,commitish"
skip = "src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless_mail/tests/samples/*,src/documents/tests/samples/*,*.po,*.json"
[tool.pytest.ini_options]
diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf
index 5cab6203c..2af2fcbf3 100644
--- a/src-ui/messages.xlf
+++ b/src-ui/messages.xlf
@@ -5257,84 +5257,105 @@
Has any of these tags
src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 203
+ 209
Has all of these tags
src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 210
+ 216
Does not have these tags
src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 217
+ 223
+
+
+
+ Has any of these correspondents
+
+ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
+ 230
Has correspondent
src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 224
+ 238
Does not have correspondents
src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 232
+ 246
Has document type
src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 240
+ 254
+
+
+
+ Has any of these document types
+
+ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
+ 262
Does not have document types
src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 248
+ 270
Has storage path
src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 256
+ 278
+
+
+
+ Has any of these storage paths
+
+ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
+ 286
Does not have storage paths
src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 264
+ 294
Matches custom field query
src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 272
+ 302
Create new workflow
src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 474
+ 531
Edit workflow
src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
- 478
+ 535
diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts
index fafc9e876..ac8a5d2c7 100644
--- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts
+++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts
@@ -412,6 +412,9 @@ describe('WorkflowEditDialogComponent', () => {
return newFilter
}
+ const correspondentAny = addFilterOfType(TriggerFilterType.CorrespondentAny)
+ correspondentAny.get('values').setValue([11])
+
const correspondentIs = addFilterOfType(TriggerFilterType.CorrespondentIs)
correspondentIs.get('values').setValue(1)
@@ -421,12 +424,18 @@ describe('WorkflowEditDialogComponent', () => {
const documentTypeIs = addFilterOfType(TriggerFilterType.DocumentTypeIs)
documentTypeIs.get('values').setValue(1)
+ const documentTypeAny = addFilterOfType(TriggerFilterType.DocumentTypeAny)
+ documentTypeAny.get('values').setValue([12])
+
const documentTypeNot = addFilterOfType(TriggerFilterType.DocumentTypeNot)
documentTypeNot.get('values').setValue([1])
const storagePathIs = addFilterOfType(TriggerFilterType.StoragePathIs)
storagePathIs.get('values').setValue(1)
+ const storagePathAny = addFilterOfType(TriggerFilterType.StoragePathAny)
+ storagePathAny.get('values').setValue([13])
+
const storagePathNot = addFilterOfType(TriggerFilterType.StoragePathNot)
storagePathNot.get('values').setValue([1])
@@ -441,10 +450,13 @@ describe('WorkflowEditDialogComponent', () => {
expect(formValues.triggers[0].filter_has_tags).toEqual([1])
expect(formValues.triggers[0].filter_has_all_tags).toEqual([2, 3])
expect(formValues.triggers[0].filter_has_not_tags).toEqual([4])
+ expect(formValues.triggers[0].filter_has_any_correspondents).toEqual([11])
expect(formValues.triggers[0].filter_has_correspondent).toEqual(1)
expect(formValues.triggers[0].filter_has_not_correspondents).toEqual([1])
+ expect(formValues.triggers[0].filter_has_any_document_types).toEqual([12])
expect(formValues.triggers[0].filter_has_document_type).toEqual(1)
expect(formValues.triggers[0].filter_has_not_document_types).toEqual([1])
+ expect(formValues.triggers[0].filter_has_any_storage_paths).toEqual([13])
expect(formValues.triggers[0].filter_has_storage_path).toEqual(1)
expect(formValues.triggers[0].filter_has_not_storage_paths).toEqual([1])
expect(formValues.triggers[0].filter_custom_field_query).toEqual(
@@ -507,16 +519,22 @@ describe('WorkflowEditDialogComponent', () => {
setFilter(TriggerFilterType.TagsAll, 11)
setFilter(TriggerFilterType.TagsNone, 12)
+ setFilter(TriggerFilterType.CorrespondentAny, 16)
setFilter(TriggerFilterType.CorrespondentNot, 13)
+ setFilter(TriggerFilterType.DocumentTypeAny, 17)
setFilter(TriggerFilterType.DocumentTypeNot, 14)
+ setFilter(TriggerFilterType.StoragePathAny, 18)
setFilter(TriggerFilterType.StoragePathNot, 15)
const formValues = component['getFormValues']()
expect(formValues.triggers[0].filter_has_all_tags).toEqual([11])
expect(formValues.triggers[0].filter_has_not_tags).toEqual([12])
+ expect(formValues.triggers[0].filter_has_any_correspondents).toEqual([16])
expect(formValues.triggers[0].filter_has_not_correspondents).toEqual([13])
+ expect(formValues.triggers[0].filter_has_any_document_types).toEqual([17])
expect(formValues.triggers[0].filter_has_not_document_types).toEqual([14])
+ expect(formValues.triggers[0].filter_has_any_storage_paths).toEqual([18])
expect(formValues.triggers[0].filter_has_not_storage_paths).toEqual([15])
})
@@ -640,8 +658,11 @@ describe('WorkflowEditDialogComponent', () => {
filter_has_tags: [],
filter_has_all_tags: [],
filter_has_not_tags: [],
+ filter_has_any_correspondents: [],
filter_has_not_correspondents: [],
+ filter_has_any_document_types: [],
filter_has_not_document_types: [],
+ filter_has_any_storage_paths: [],
filter_has_not_storage_paths: [],
filter_has_correspondent: null,
filter_has_document_type: null,
@@ -699,11 +720,14 @@ describe('WorkflowEditDialogComponent', () => {
trigger.filter_has_tags = [1]
trigger.filter_has_all_tags = [2, 3]
trigger.filter_has_not_tags = [4]
+ trigger.filter_has_any_correspondents = [10] as any
trigger.filter_has_correspondent = 5 as any
trigger.filter_has_not_correspondents = [6] as any
trigger.filter_has_document_type = 7 as any
+ trigger.filter_has_any_document_types = [11] as any
trigger.filter_has_not_document_types = [8] as any
trigger.filter_has_storage_path = 9 as any
+ trigger.filter_has_any_storage_paths = [12] as any
trigger.filter_has_not_storage_paths = [10] as any
trigger.filter_custom_field_query = JSON.stringify([
'AND',
@@ -714,8 +738,8 @@ describe('WorkflowEditDialogComponent', () => {
component.ngOnInit()
const triggerGroup = component.triggerFields.at(0) as FormGroup
const filters = component.getFiltersFormArray(triggerGroup)
- expect(filters.length).toBe(10)
- const customFieldFilter = filters.at(9) as FormGroup
+ expect(filters.length).toBe(13)
+ const customFieldFilter = filters.at(12) as FormGroup
expect(customFieldFilter.get('type').value).toBe(
TriggerFilterType.CustomFieldQuery
)
@@ -724,12 +748,27 @@ describe('WorkflowEditDialogComponent', () => {
})
it('should expose select metadata helpers', () => {
+ expect(component.isSelectMultiple(TriggerFilterType.CorrespondentAny)).toBe(
+ true
+ )
expect(component.isSelectMultiple(TriggerFilterType.CorrespondentNot)).toBe(
true
)
expect(component.isSelectMultiple(TriggerFilterType.CorrespondentIs)).toBe(
false
)
+ expect(component.isSelectMultiple(TriggerFilterType.DocumentTypeAny)).toBe(
+ true
+ )
+ expect(component.isSelectMultiple(TriggerFilterType.DocumentTypeIs)).toBe(
+ false
+ )
+ expect(component.isSelectMultiple(TriggerFilterType.StoragePathAny)).toBe(
+ true
+ )
+ expect(component.isSelectMultiple(TriggerFilterType.StoragePathIs)).toBe(
+ false
+ )
component.correspondents = [{ id: 1, name: 'C1' } as any]
component.documentTypes = [{ id: 2, name: 'DT' } as any]
@@ -741,9 +780,15 @@ describe('WorkflowEditDialogComponent', () => {
expect(
component.getFilterSelectItems(TriggerFilterType.DocumentTypeIs)
).toEqual(component.documentTypes)
+ expect(
+ component.getFilterSelectItems(TriggerFilterType.DocumentTypeAny)
+ ).toEqual(component.documentTypes)
expect(
component.getFilterSelectItems(TriggerFilterType.StoragePathIs)
).toEqual(component.storagePaths)
+ expect(
+ component.getFilterSelectItems(TriggerFilterType.StoragePathAny)
+ ).toEqual(component.storagePaths)
expect(component.getFilterSelectItems(TriggerFilterType.TagsAll)).toEqual(
[]
)
diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
index 74221e3f0..94d8318e0 100644
--- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
+++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
@@ -145,10 +145,13 @@ export enum TriggerFilterType {
TagsAny = 'tags_any',
TagsAll = 'tags_all',
TagsNone = 'tags_none',
+ CorrespondentAny = 'correspondent_any',
CorrespondentIs = 'correspondent_is',
CorrespondentNot = 'correspondent_not',
+ DocumentTypeAny = 'document_type_any',
DocumentTypeIs = 'document_type_is',
DocumentTypeNot = 'document_type_not',
+ StoragePathAny = 'storage_path_any',
StoragePathIs = 'storage_path_is',
StoragePathNot = 'storage_path_not',
CustomFieldQuery = 'custom_field_query',
@@ -172,8 +175,11 @@ type TriggerFilterAggregate = {
filter_has_tags: number[]
filter_has_all_tags: number[]
filter_has_not_tags: number[]
+ filter_has_any_correspondents: number[]
filter_has_not_correspondents: number[]
+ filter_has_any_document_types: number[]
filter_has_not_document_types: number[]
+ filter_has_any_storage_paths: number[]
filter_has_not_storage_paths: number[]
filter_has_correspondent: number | null
filter_has_document_type: number | null
@@ -219,6 +225,14 @@ const TRIGGER_FILTER_DEFINITIONS: TriggerFilterDefinition[] = [
allowMultipleEntries: false,
allowMultipleValues: true,
},
+ {
+ id: TriggerFilterType.CorrespondentAny,
+ name: $localize`Has any of these correspondents`,
+ inputType: 'select',
+ allowMultipleEntries: false,
+ allowMultipleValues: true,
+ selectItems: 'correspondents',
+ },
{
id: TriggerFilterType.CorrespondentIs,
name: $localize`Has correspondent`,
@@ -243,6 +257,14 @@ const TRIGGER_FILTER_DEFINITIONS: TriggerFilterDefinition[] = [
allowMultipleValues: false,
selectItems: 'documentTypes',
},
+ {
+ id: TriggerFilterType.DocumentTypeAny,
+ name: $localize`Has any of these document types`,
+ inputType: 'select',
+ allowMultipleEntries: false,
+ allowMultipleValues: true,
+ selectItems: 'documentTypes',
+ },
{
id: TriggerFilterType.DocumentTypeNot,
name: $localize`Does not have document types`,
@@ -259,6 +281,14 @@ const TRIGGER_FILTER_DEFINITIONS: TriggerFilterDefinition[] = [
allowMultipleValues: false,
selectItems: 'storagePaths',
},
+ {
+ id: TriggerFilterType.StoragePathAny,
+ name: $localize`Has any of these storage paths`,
+ inputType: 'select',
+ allowMultipleEntries: false,
+ allowMultipleValues: true,
+ selectItems: 'storagePaths',
+ },
{
id: TriggerFilterType.StoragePathNot,
name: $localize`Does not have storage paths`,
@@ -306,6 +336,15 @@ const FILTER_HANDLERS: Record = {
extract: (trigger) => trigger.filter_has_not_tags,
hasValue: (value) => Array.isArray(value) && value.length > 0,
},
+ [TriggerFilterType.CorrespondentAny]: {
+ apply: (aggregate, values) => {
+ aggregate.filter_has_any_correspondents = Array.isArray(values)
+ ? [...values]
+ : [values]
+ },
+ extract: (trigger) => trigger.filter_has_any_correspondents,
+ hasValue: (value) => Array.isArray(value) && value.length > 0,
+ },
[TriggerFilterType.CorrespondentIs]: {
apply: (aggregate, values) => {
aggregate.filter_has_correspondent = Array.isArray(values)
@@ -333,6 +372,15 @@ const FILTER_HANDLERS: Record = {
extract: (trigger) => trigger.filter_has_document_type,
hasValue: (value) => value !== null && value !== undefined,
},
+ [TriggerFilterType.DocumentTypeAny]: {
+ apply: (aggregate, values) => {
+ aggregate.filter_has_any_document_types = Array.isArray(values)
+ ? [...values]
+ : [values]
+ },
+ extract: (trigger) => trigger.filter_has_any_document_types,
+ hasValue: (value) => Array.isArray(value) && value.length > 0,
+ },
[TriggerFilterType.DocumentTypeNot]: {
apply: (aggregate, values) => {
aggregate.filter_has_not_document_types = Array.isArray(values)
@@ -351,6 +399,15 @@ const FILTER_HANDLERS: Record = {
extract: (trigger) => trigger.filter_has_storage_path,
hasValue: (value) => value !== null && value !== undefined,
},
+ [TriggerFilterType.StoragePathAny]: {
+ apply: (aggregate, values) => {
+ aggregate.filter_has_any_storage_paths = Array.isArray(values)
+ ? [...values]
+ : [values]
+ },
+ extract: (trigger) => trigger.filter_has_any_storage_paths,
+ hasValue: (value) => Array.isArray(value) && value.length > 0,
+ },
[TriggerFilterType.StoragePathNot]: {
apply: (aggregate, values) => {
aggregate.filter_has_not_storage_paths = Array.isArray(values)
@@ -642,8 +699,11 @@ export class WorkflowEditDialogComponent
filter_has_tags: [],
filter_has_all_tags: [],
filter_has_not_tags: [],
+ filter_has_any_correspondents: [],
filter_has_not_correspondents: [],
+ filter_has_any_document_types: [],
filter_has_not_document_types: [],
+ filter_has_any_storage_paths: [],
filter_has_not_storage_paths: [],
filter_has_correspondent: null,
filter_has_document_type: null,
@@ -670,10 +730,16 @@ export class WorkflowEditDialogComponent
trigger.filter_has_tags = aggregate.filter_has_tags
trigger.filter_has_all_tags = aggregate.filter_has_all_tags
trigger.filter_has_not_tags = aggregate.filter_has_not_tags
+ trigger.filter_has_any_correspondents =
+ aggregate.filter_has_any_correspondents
trigger.filter_has_not_correspondents =
aggregate.filter_has_not_correspondents
+ trigger.filter_has_any_document_types =
+ aggregate.filter_has_any_document_types
trigger.filter_has_not_document_types =
aggregate.filter_has_not_document_types
+ trigger.filter_has_any_storage_paths =
+ aggregate.filter_has_any_storage_paths
trigger.filter_has_not_storage_paths =
aggregate.filter_has_not_storage_paths
trigger.filter_has_correspondent =
@@ -856,8 +922,11 @@ export class WorkflowEditDialogComponent
case TriggerFilterType.TagsAny:
case TriggerFilterType.TagsAll:
case TriggerFilterType.TagsNone:
+ case TriggerFilterType.CorrespondentAny:
case TriggerFilterType.CorrespondentNot:
+ case TriggerFilterType.DocumentTypeAny:
case TriggerFilterType.DocumentTypeNot:
+ case TriggerFilterType.StoragePathAny:
case TriggerFilterType.StoragePathNot:
return true
default:
@@ -1179,8 +1248,11 @@ export class WorkflowEditDialogComponent
filter_has_tags: [],
filter_has_all_tags: [],
filter_has_not_tags: [],
+ filter_has_any_correspondents: [],
filter_has_not_correspondents: [],
+ filter_has_any_document_types: [],
filter_has_not_document_types: [],
+ filter_has_any_storage_paths: [],
filter_has_not_storage_paths: [],
filter_custom_field_query: null,
filter_has_correspondent: null,
diff --git a/src-ui/src/app/data/workflow-trigger.ts b/src-ui/src/app/data/workflow-trigger.ts
index 888b18cc3..2bc89f188 100644
--- a/src-ui/src/app/data/workflow-trigger.ts
+++ b/src-ui/src/app/data/workflow-trigger.ts
@@ -44,10 +44,16 @@ export interface WorkflowTrigger extends ObjectWithId {
filter_has_not_tags?: number[] // Tag.id[]
+ filter_has_any_correspondents?: number[] // Correspondent.id[]
+
filter_has_not_correspondents?: number[] // Correspondent.id[]
+ filter_has_any_document_types?: number[] // DocumentType.id[]
+
filter_has_not_document_types?: number[] // DocumentType.id[]
+ filter_has_any_storage_paths?: number[] // StoragePath.id[]
+
filter_has_not_storage_paths?: number[] // StoragePath.id[]
filter_custom_field_query?: string
diff --git a/src/documents/data_models.py b/src/documents/data_models.py
index 3acc3f51b..a4b1150dd 100644
--- a/src/documents/data_models.py
+++ b/src/documents/data_models.py
@@ -118,7 +118,7 @@ class DocumentMetadataOverrides:
).values_list("id", flat=True),
)
overrides.custom_fields = {
- custom_field.id: custom_field.value
+ custom_field.field.id: custom_field.value
for custom_field in doc.custom_fields.all()
}
diff --git a/src/documents/matching.py b/src/documents/matching.py
index 198ead64c..9276ad583 100644
--- a/src/documents/matching.py
+++ b/src/documents/matching.py
@@ -403,6 +403,18 @@ def existing_document_matches_workflow(
f"Document tags {list(document.tags.all())} include excluded tags {list(trigger_has_not_tags_qs)}",
)
+ allowed_correspondent_ids = set(
+ trigger.filter_has_any_correspondents.values_list("id", flat=True),
+ )
+ if (
+ allowed_correspondent_ids
+ and document.correspondent_id not in allowed_correspondent_ids
+ ):
+ return (
+ False,
+ f"Document correspondent {document.correspondent} is not one of {list(trigger.filter_has_any_correspondents.all())}",
+ )
+
# Document correspondent vs trigger has_correspondent
if (
trigger.filter_has_correspondent_id is not None
@@ -424,6 +436,17 @@ def existing_document_matches_workflow(
f"Document correspondent {document.correspondent} is excluded by {list(trigger.filter_has_not_correspondents.all())}",
)
+ allowed_document_type_ids = set(
+ trigger.filter_has_any_document_types.values_list("id", flat=True),
+ )
+ if allowed_document_type_ids and (
+ document.document_type_id not in allowed_document_type_ids
+ ):
+ return (
+ False,
+ f"Document doc type {document.document_type} is not one of {list(trigger.filter_has_any_document_types.all())}",
+ )
+
# Document document_type vs trigger has_document_type
if (
trigger.filter_has_document_type_id is not None
@@ -445,6 +468,17 @@ def existing_document_matches_workflow(
f"Document doc type {document.document_type} is excluded by {list(trigger.filter_has_not_document_types.all())}",
)
+ allowed_storage_path_ids = set(
+ trigger.filter_has_any_storage_paths.values_list("id", flat=True),
+ )
+ if allowed_storage_path_ids and (
+ document.storage_path_id not in allowed_storage_path_ids
+ ):
+ return (
+ False,
+ f"Document storage path {document.storage_path} is not one of {list(trigger.filter_has_any_storage_paths.all())}",
+ )
+
# Document storage_path vs trigger has_storage_path
if (
trigger.filter_has_storage_path_id is not None
@@ -532,6 +566,10 @@ def prefilter_documents_by_workflowtrigger(
# Correspondent, DocumentType, etc. filtering
+ if trigger.filter_has_any_correspondents.exists():
+ documents = documents.filter(
+ correspondent__in=trigger.filter_has_any_correspondents.all(),
+ )
if trigger.filter_has_correspondent is not None:
documents = documents.filter(
correspondent=trigger.filter_has_correspondent,
@@ -541,6 +579,10 @@ def prefilter_documents_by_workflowtrigger(
correspondent__in=trigger.filter_has_not_correspondents.all(),
)
+ if trigger.filter_has_any_document_types.exists():
+ documents = documents.filter(
+ document_type__in=trigger.filter_has_any_document_types.all(),
+ )
if trigger.filter_has_document_type is not None:
documents = documents.filter(
document_type=trigger.filter_has_document_type,
@@ -550,6 +592,10 @@ def prefilter_documents_by_workflowtrigger(
document_type__in=trigger.filter_has_not_document_types.all(),
)
+ if trigger.filter_has_any_storage_paths.exists():
+ documents = documents.filter(
+ storage_path__in=trigger.filter_has_any_storage_paths.all(),
+ )
if trigger.filter_has_storage_path is not None:
documents = documents.filter(
storage_path=trigger.filter_has_storage_path,
@@ -604,8 +650,11 @@ def document_matches_workflow(
"filter_has_tags",
"filter_has_all_tags",
"filter_has_not_tags",
+ "filter_has_any_document_types",
"filter_has_not_document_types",
+ "filter_has_any_correspondents",
"filter_has_not_correspondents",
+ "filter_has_any_storage_paths",
"filter_has_not_storage_paths",
)
)
diff --git a/src/documents/migrations/0005_workflowtrigger_filter_has_any_correspondents_and_more.py b/src/documents/migrations/0005_workflowtrigger_filter_has_any_correspondents_and_more.py
new file mode 100644
index 000000000..db5ef5754
--- /dev/null
+++ b/src/documents/migrations/0005_workflowtrigger_filter_has_any_correspondents_and_more.py
@@ -0,0 +1,43 @@
+# Generated by Django 5.2.7 on 2025-12-17 22:25
+
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("documents", "0004_remove_document_storage_type"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="workflowtrigger",
+ name="filter_has_any_correspondents",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="workflowtriggers_has_any_correspondent",
+ to="documents.correspondent",
+ verbose_name="has one of these correspondents",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowtrigger",
+ name="filter_has_any_document_types",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="workflowtriggers_has_any_document_type",
+ to="documents.documenttype",
+ verbose_name="has one of these document types",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowtrigger",
+ name="filter_has_any_storage_paths",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="workflowtriggers_has_any_storage_path",
+ to="documents.storagepath",
+ verbose_name="has one of these storage paths",
+ ),
+ ),
+ ]
diff --git a/src/documents/models.py b/src/documents/models.py
index 496d998f1..d30963c99 100644
--- a/src/documents/models.py
+++ b/src/documents/models.py
@@ -1180,6 +1180,13 @@ class WorkflowTrigger(models.Model):
verbose_name=_("has this document type"),
)
+ filter_has_any_document_types = models.ManyToManyField(
+ DocumentType,
+ blank=True,
+ related_name="workflowtriggers_has_any_document_type",
+ verbose_name=_("has one of these document types"),
+ )
+
filter_has_not_document_types = models.ManyToManyField(
DocumentType,
blank=True,
@@ -1202,6 +1209,13 @@ class WorkflowTrigger(models.Model):
verbose_name=_("does not have these correspondent(s)"),
)
+ filter_has_any_correspondents = models.ManyToManyField(
+ Correspondent,
+ blank=True,
+ related_name="workflowtriggers_has_any_correspondent",
+ verbose_name=_("has one of these correspondents"),
+ )
+
filter_has_storage_path = models.ForeignKey(
StoragePath,
null=True,
@@ -1210,6 +1224,13 @@ class WorkflowTrigger(models.Model):
verbose_name=_("has this storage path"),
)
+ filter_has_any_storage_paths = models.ManyToManyField(
+ StoragePath,
+ blank=True,
+ related_name="workflowtriggers_has_any_storage_path",
+ verbose_name=_("has one of these storage paths"),
+ )
+
filter_has_not_storage_paths = models.ManyToManyField(
StoragePath,
blank=True,
diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py
index 27479a849..030b14807 100644
--- a/src/documents/serialisers.py
+++ b/src/documents/serialisers.py
@@ -2405,8 +2405,11 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer):
"filter_has_all_tags",
"filter_has_not_tags",
"filter_custom_field_query",
+ "filter_has_any_correspondents",
"filter_has_not_correspondents",
+ "filter_has_any_document_types",
"filter_has_not_document_types",
+ "filter_has_any_storage_paths",
"filter_has_not_storage_paths",
"filter_has_correspondent",
"filter_has_document_type",
@@ -2644,14 +2647,26 @@ class WorkflowSerializer(serializers.ModelSerializer):
filter_has_tags = trigger.pop("filter_has_tags", None)
filter_has_all_tags = trigger.pop("filter_has_all_tags", None)
filter_has_not_tags = trigger.pop("filter_has_not_tags", None)
+ filter_has_any_correspondents = trigger.pop(
+ "filter_has_any_correspondents",
+ None,
+ )
filter_has_not_correspondents = trigger.pop(
"filter_has_not_correspondents",
None,
)
+ filter_has_any_document_types = trigger.pop(
+ "filter_has_any_document_types",
+ None,
+ )
filter_has_not_document_types = trigger.pop(
"filter_has_not_document_types",
None,
)
+ filter_has_any_storage_paths = trigger.pop(
+ "filter_has_any_storage_paths",
+ None,
+ )
filter_has_not_storage_paths = trigger.pop(
"filter_has_not_storage_paths",
None,
@@ -2668,14 +2683,26 @@ class WorkflowSerializer(serializers.ModelSerializer):
trigger_instance.filter_has_all_tags.set(filter_has_all_tags)
if filter_has_not_tags is not None:
trigger_instance.filter_has_not_tags.set(filter_has_not_tags)
+ if filter_has_any_correspondents is not None:
+ trigger_instance.filter_has_any_correspondents.set(
+ filter_has_any_correspondents,
+ )
if filter_has_not_correspondents is not None:
trigger_instance.filter_has_not_correspondents.set(
filter_has_not_correspondents,
)
+ if filter_has_any_document_types is not None:
+ trigger_instance.filter_has_any_document_types.set(
+ filter_has_any_document_types,
+ )
if filter_has_not_document_types is not None:
trigger_instance.filter_has_not_document_types.set(
filter_has_not_document_types,
)
+ if filter_has_any_storage_paths is not None:
+ trigger_instance.filter_has_any_storage_paths.set(
+ filter_has_any_storage_paths,
+ )
if filter_has_not_storage_paths is not None:
trigger_instance.filter_has_not_storage_paths.set(
filter_has_not_storage_paths,
diff --git a/src/documents/tests/test_api_workflows.py b/src/documents/tests/test_api_workflows.py
index 9efdb8451..1d3efd457 100644
--- a/src/documents/tests/test_api_workflows.py
+++ b/src/documents/tests/test_api_workflows.py
@@ -186,8 +186,11 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
"filter_has_tags": [self.t1.id],
"filter_has_all_tags": [self.t2.id],
"filter_has_not_tags": [self.t3.id],
+ "filter_has_any_correspondents": [self.c.id],
"filter_has_not_correspondents": [self.c2.id],
+ "filter_has_any_document_types": [self.dt.id],
"filter_has_not_document_types": [self.dt2.id],
+ "filter_has_any_storage_paths": [self.sp.id],
"filter_has_not_storage_paths": [self.sp2.id],
"filter_custom_field_query": json.dumps(
[
@@ -248,14 +251,26 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
set(trigger.filter_has_not_tags.values_list("id", flat=True)),
{self.t3.id},
)
+ self.assertSetEqual(
+ set(trigger.filter_has_any_correspondents.values_list("id", flat=True)),
+ {self.c.id},
+ )
self.assertSetEqual(
set(trigger.filter_has_not_correspondents.values_list("id", flat=True)),
{self.c2.id},
)
+ self.assertSetEqual(
+ set(trigger.filter_has_any_document_types.values_list("id", flat=True)),
+ {self.dt.id},
+ )
self.assertSetEqual(
set(trigger.filter_has_not_document_types.values_list("id", flat=True)),
{self.dt2.id},
)
+ self.assertSetEqual(
+ set(trigger.filter_has_any_storage_paths.values_list("id", flat=True)),
+ {self.sp.id},
+ )
self.assertSetEqual(
set(trigger.filter_has_not_storage_paths.values_list("id", flat=True)),
{self.sp2.id},
@@ -419,8 +434,11 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
"filter_has_tags": [self.t1.id],
"filter_has_all_tags": [self.t2.id],
"filter_has_not_tags": [self.t3.id],
+ "filter_has_any_correspondents": [self.c.id],
"filter_has_not_correspondents": [self.c2.id],
+ "filter_has_any_document_types": [self.dt.id],
"filter_has_not_document_types": [self.dt2.id],
+ "filter_has_any_storage_paths": [self.sp.id],
"filter_has_not_storage_paths": [self.sp2.id],
"filter_custom_field_query": json.dumps(
["AND", [[self.cf1.id, "exact", "value"]]],
@@ -450,14 +468,26 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
workflow.triggers.first().filter_has_not_tags.first(),
self.t3,
)
+ self.assertEqual(
+ workflow.triggers.first().filter_has_any_correspondents.first(),
+ self.c,
+ )
self.assertEqual(
workflow.triggers.first().filter_has_not_correspondents.first(),
self.c2,
)
+ self.assertEqual(
+ workflow.triggers.first().filter_has_any_document_types.first(),
+ self.dt,
+ )
self.assertEqual(
workflow.triggers.first().filter_has_not_document_types.first(),
self.dt2,
)
+ self.assertEqual(
+ workflow.triggers.first().filter_has_any_storage_paths.first(),
+ self.sp,
+ )
self.assertEqual(
workflow.triggers.first().filter_has_not_storage_paths.first(),
self.sp2,
diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py
index d2f843a68..75f9d5fe6 100644
--- a/src/documents/tests/test_workflows.py
+++ b/src/documents/tests/test_workflows.py
@@ -1276,6 +1276,76 @@ class TestWorkflows(
)
self.assertIn(expected_str, cm.output[1])
+ def test_document_added_any_filters(self):
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
+ )
+ trigger.filter_has_any_correspondents.set([self.c])
+ trigger.filter_has_any_document_types.set([self.dt])
+ trigger.filter_has_any_storage_paths.set([self.sp])
+
+ matching_doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ document_type=self.dt,
+ storage_path=self.sp,
+ original_filename="sample.pdf",
+ checksum="checksum-any-match",
+ )
+
+ matched, reason = existing_document_matches_workflow(matching_doc, trigger)
+ self.assertTrue(matched)
+ self.assertIsNone(reason)
+
+ wrong_correspondent = Document.objects.create(
+ title="wrong correspondent",
+ correspondent=self.c2,
+ document_type=self.dt,
+ storage_path=self.sp,
+ original_filename="sample2.pdf",
+ )
+ matched, reason = existing_document_matches_workflow(
+ wrong_correspondent,
+ trigger,
+ )
+ self.assertFalse(matched)
+ self.assertIn("correspondent", reason)
+
+ other_document_type = DocumentType.objects.create(name="Other")
+ wrong_document_type = Document.objects.create(
+ title="wrong doc type",
+ correspondent=self.c,
+ document_type=other_document_type,
+ storage_path=self.sp,
+ original_filename="sample3.pdf",
+ checksum="checksum-wrong-doc-type",
+ )
+ matched, reason = existing_document_matches_workflow(
+ wrong_document_type,
+ trigger,
+ )
+ self.assertFalse(matched)
+ self.assertIn("doc type", reason)
+
+ other_storage_path = StoragePath.objects.create(
+ name="Other path",
+ path="/other/",
+ )
+ wrong_storage_path = Document.objects.create(
+ title="wrong storage",
+ correspondent=self.c,
+ document_type=self.dt,
+ storage_path=other_storage_path,
+ original_filename="sample4.pdf",
+ checksum="checksum-wrong-storage-path",
+ )
+ matched, reason = existing_document_matches_workflow(
+ wrong_storage_path,
+ trigger,
+ )
+ self.assertFalse(matched)
+ self.assertIn("storage path", reason)
+
def test_document_added_custom_field_query_no_match(self):
trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
@@ -1384,6 +1454,39 @@ class TestWorkflows(
self.assertIn(doc1, filtered)
self.assertNotIn(doc2, filtered)
+ def test_prefilter_documents_any_filters(self):
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
+ )
+ trigger.filter_has_any_correspondents.set([self.c])
+ trigger.filter_has_any_document_types.set([self.dt])
+ trigger.filter_has_any_storage_paths.set([self.sp])
+
+ allowed_document = Document.objects.create(
+ title="allowed",
+ correspondent=self.c,
+ document_type=self.dt,
+ storage_path=self.sp,
+ original_filename="doc-allowed.pdf",
+ checksum="checksum-any-allowed",
+ )
+ blocked_document = Document.objects.create(
+ title="blocked",
+ correspondent=self.c2,
+ document_type=self.dt,
+ storage_path=self.sp,
+ original_filename="doc-blocked.pdf",
+ checksum="checksum-any-blocked",
+ )
+
+ filtered = prefilter_documents_by_workflowtrigger(
+ Document.objects.all(),
+ trigger,
+ )
+
+ self.assertIn(allowed_document, filtered)
+ self.assertNotIn(blocked_document, filtered)
+
def test_consumption_trigger_requires_filter_configuration(self):
serializer = WorkflowTriggerSerializer(
data={
diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po
index 7e4bf0abf..7bc9a9801 100644
--- a/src/locale/en_US/LC_MESSAGES/django.po
+++ b/src/locale/en_US/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-01-25 03:30+0000\n"
+"POT-Creation-Date: 2026-01-25 21:46+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@@ -89,7 +89,7 @@ msgstr ""
msgid "Automatic"
msgstr ""
-#: documents/models.py:64 documents/models.py:434 documents/models.py:1507
+#: documents/models.py:64 documents/models.py:434 documents/models.py:1528
#: paperless_mail/models.py:23 paperless_mail/models.py:143
msgid "name"
msgstr ""
@@ -252,7 +252,7 @@ msgid "The position of this document in your physical document archive."
msgstr ""
#: documents/models.py:303 documents/models.py:678 documents/models.py:732
-#: documents/models.py:1550
+#: documents/models.py:1571
msgid "document"
msgstr ""
@@ -869,346 +869,358 @@ msgid "has this document type"
msgstr ""
#: documents/models.py:1073
+msgid "has one of these document types"
+msgstr ""
+
+#: documents/models.py:1080
msgid "does not have these document type(s)"
msgstr ""
-#: documents/models.py:1081
+#: documents/models.py:1088
msgid "has this correspondent"
msgstr ""
-#: documents/models.py:1088
+#: documents/models.py:1095
msgid "does not have these correspondent(s)"
msgstr ""
-#: documents/models.py:1096
-msgid "has this storage path"
-msgstr ""
-
-#: documents/models.py:1103
-msgid "does not have these storage path(s)"
-msgstr ""
-
-#: documents/models.py:1107
-msgid "filter custom field query"
+#: documents/models.py:1102
+msgid "has one of these correspondents"
msgstr ""
#: documents/models.py:1110
-msgid "JSON-encoded custom field query expression."
-msgstr ""
-
-#: documents/models.py:1114
-msgid "schedule offset days"
+msgid "has this storage path"
msgstr ""
#: documents/models.py:1117
+msgid "has one of these storage paths"
+msgstr ""
+
+#: documents/models.py:1124
+msgid "does not have these storage path(s)"
+msgstr ""
+
+#: documents/models.py:1128
+msgid "filter custom field query"
+msgstr ""
+
+#: documents/models.py:1131
+msgid "JSON-encoded custom field query expression."
+msgstr ""
+
+#: documents/models.py:1135
+msgid "schedule offset days"
+msgstr ""
+
+#: documents/models.py:1138
msgid "The number of days to offset the schedule trigger by."
msgstr ""
-#: documents/models.py:1122
+#: documents/models.py:1143
msgid "schedule is recurring"
msgstr ""
-#: documents/models.py:1125
+#: documents/models.py:1146
msgid "If the schedule should be recurring."
msgstr ""
-#: documents/models.py:1130
+#: documents/models.py:1151
msgid "schedule recurring delay in days"
msgstr ""
-#: documents/models.py:1134
+#: documents/models.py:1155
msgid "The number of days between recurring schedule triggers."
msgstr ""
-#: documents/models.py:1139
+#: documents/models.py:1160
msgid "schedule date field"
msgstr ""
-#: documents/models.py:1144
+#: documents/models.py:1165
msgid "The field to check for a schedule trigger."
msgstr ""
-#: documents/models.py:1153
+#: documents/models.py:1174
msgid "schedule date custom field"
msgstr ""
-#: documents/models.py:1157
+#: documents/models.py:1178
msgid "workflow trigger"
msgstr ""
-#: documents/models.py:1158
+#: documents/models.py:1179
msgid "workflow triggers"
msgstr ""
-#: documents/models.py:1166
+#: documents/models.py:1187
msgid "email subject"
msgstr ""
-#: documents/models.py:1170
+#: documents/models.py:1191
msgid ""
"The subject of the email, can include some placeholders, see documentation."
msgstr ""
-#: documents/models.py:1176
+#: documents/models.py:1197
msgid "email body"
msgstr ""
-#: documents/models.py:1179
+#: documents/models.py:1200
msgid ""
"The body (message) of the email, can include some placeholders, see "
"documentation."
msgstr ""
-#: documents/models.py:1185
+#: documents/models.py:1206
msgid "emails to"
msgstr ""
-#: documents/models.py:1188
+#: documents/models.py:1209
msgid "The destination email addresses, comma separated."
msgstr ""
-#: documents/models.py:1194
+#: documents/models.py:1215
msgid "include document in email"
msgstr ""
-#: documents/models.py:1205
+#: documents/models.py:1226
msgid "webhook url"
msgstr ""
-#: documents/models.py:1208
+#: documents/models.py:1229
msgid "The destination URL for the notification."
msgstr ""
-#: documents/models.py:1213
+#: documents/models.py:1234
msgid "use parameters"
msgstr ""
-#: documents/models.py:1218
+#: documents/models.py:1239
msgid "send as JSON"
msgstr ""
-#: documents/models.py:1222
+#: documents/models.py:1243
msgid "webhook parameters"
msgstr ""
-#: documents/models.py:1225
+#: documents/models.py:1246
msgid "The parameters to send with the webhook URL if body not used."
msgstr ""
-#: documents/models.py:1229
+#: documents/models.py:1250
msgid "webhook body"
msgstr ""
-#: documents/models.py:1232
+#: documents/models.py:1253
msgid "The body to send with the webhook URL if parameters not used."
msgstr ""
-#: documents/models.py:1236
+#: documents/models.py:1257
msgid "webhook headers"
msgstr ""
-#: documents/models.py:1239
+#: documents/models.py:1260
msgid "The headers to send with the webhook URL."
msgstr ""
-#: documents/models.py:1244
+#: documents/models.py:1265
msgid "include document in webhook"
msgstr ""
-#: documents/models.py:1255
+#: documents/models.py:1276
msgid "Assignment"
msgstr ""
-#: documents/models.py:1259
+#: documents/models.py:1280
msgid "Removal"
msgstr ""
-#: documents/models.py:1263 documents/templates/account/password_reset.html:15
+#: documents/models.py:1284 documents/templates/account/password_reset.html:15
msgid "Email"
msgstr ""
-#: documents/models.py:1267
+#: documents/models.py:1288
msgid "Webhook"
msgstr ""
-#: documents/models.py:1271
+#: documents/models.py:1292
msgid "Workflow Action Type"
msgstr ""
-#: documents/models.py:1276 documents/models.py:1509
+#: documents/models.py:1297 documents/models.py:1530
#: paperless_mail/models.py:145
msgid "order"
msgstr ""
-#: documents/models.py:1279
+#: documents/models.py:1300
msgid "assign title"
msgstr ""
-#: documents/models.py:1283
+#: documents/models.py:1304
msgid "Assign a document title, must be a Jinja2 template, see documentation."
msgstr ""
-#: documents/models.py:1291 paperless_mail/models.py:274
+#: documents/models.py:1312 paperless_mail/models.py:274
msgid "assign this tag"
msgstr ""
-#: documents/models.py:1300 paperless_mail/models.py:282
+#: documents/models.py:1321 paperless_mail/models.py:282
msgid "assign this document type"
msgstr ""
-#: documents/models.py:1309 paperless_mail/models.py:296
+#: documents/models.py:1330 paperless_mail/models.py:296
msgid "assign this correspondent"
msgstr ""
-#: documents/models.py:1318
+#: documents/models.py:1339
msgid "assign this storage path"
msgstr ""
-#: documents/models.py:1327
+#: documents/models.py:1348
msgid "assign this owner"
msgstr ""
-#: documents/models.py:1334
+#: documents/models.py:1355
msgid "grant view permissions to these users"
msgstr ""
-#: documents/models.py:1341
+#: documents/models.py:1362
msgid "grant view permissions to these groups"
msgstr ""
-#: documents/models.py:1348
+#: documents/models.py:1369
msgid "grant change permissions to these users"
msgstr ""
-#: documents/models.py:1355
+#: documents/models.py:1376
msgid "grant change permissions to these groups"
msgstr ""
-#: documents/models.py:1362
+#: documents/models.py:1383
msgid "assign these custom fields"
msgstr ""
-#: documents/models.py:1366
+#: documents/models.py:1387
msgid "custom field values"
msgstr ""
-#: documents/models.py:1370
+#: documents/models.py:1391
msgid "Optional values to assign to the custom fields."
msgstr ""
-#: documents/models.py:1379
+#: documents/models.py:1400
msgid "remove these tag(s)"
msgstr ""
-#: documents/models.py:1384
+#: documents/models.py:1405
msgid "remove all tags"
msgstr ""
-#: documents/models.py:1391
+#: documents/models.py:1412
msgid "remove these document type(s)"
msgstr ""
-#: documents/models.py:1396
+#: documents/models.py:1417
msgid "remove all document types"
msgstr ""
-#: documents/models.py:1403
+#: documents/models.py:1424
msgid "remove these correspondent(s)"
msgstr ""
-#: documents/models.py:1408
+#: documents/models.py:1429
msgid "remove all correspondents"
msgstr ""
-#: documents/models.py:1415
+#: documents/models.py:1436
msgid "remove these storage path(s)"
msgstr ""
-#: documents/models.py:1420
+#: documents/models.py:1441
msgid "remove all storage paths"
msgstr ""
-#: documents/models.py:1427
+#: documents/models.py:1448
msgid "remove these owner(s)"
msgstr ""
-#: documents/models.py:1432
+#: documents/models.py:1453
msgid "remove all owners"
msgstr ""
-#: documents/models.py:1439
+#: documents/models.py:1460
msgid "remove view permissions for these users"
msgstr ""
-#: documents/models.py:1446
+#: documents/models.py:1467
msgid "remove view permissions for these groups"
msgstr ""
-#: documents/models.py:1453
+#: documents/models.py:1474
msgid "remove change permissions for these users"
msgstr ""
-#: documents/models.py:1460
+#: documents/models.py:1481
msgid "remove change permissions for these groups"
msgstr ""
-#: documents/models.py:1465
+#: documents/models.py:1486
msgid "remove all permissions"
msgstr ""
-#: documents/models.py:1472
+#: documents/models.py:1493
msgid "remove these custom fields"
msgstr ""
-#: documents/models.py:1477
+#: documents/models.py:1498
msgid "remove all custom fields"
msgstr ""
-#: documents/models.py:1486
+#: documents/models.py:1507
msgid "email"
msgstr ""
-#: documents/models.py:1495
+#: documents/models.py:1516
msgid "webhook"
msgstr ""
-#: documents/models.py:1499
+#: documents/models.py:1520
msgid "workflow action"
msgstr ""
-#: documents/models.py:1500
+#: documents/models.py:1521
msgid "workflow actions"
msgstr ""
-#: documents/models.py:1515
+#: documents/models.py:1536
msgid "triggers"
msgstr ""
-#: documents/models.py:1522
+#: documents/models.py:1543
msgid "actions"
msgstr ""
-#: documents/models.py:1525 paperless_mail/models.py:154
+#: documents/models.py:1546 paperless_mail/models.py:154
msgid "enabled"
msgstr ""
-#: documents/models.py:1536
+#: documents/models.py:1557
msgid "workflow"
msgstr ""
-#: documents/models.py:1540
+#: documents/models.py:1561
msgid "workflow trigger type"
msgstr ""
-#: documents/models.py:1554
+#: documents/models.py:1575
msgid "date run"
msgstr ""
-#: documents/models.py:1560
+#: documents/models.py:1581
msgid "workflow run"
msgstr ""
-#: documents/models.py:1561
+#: documents/models.py:1582
msgid "workflow runs"
msgstr ""