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 ""