but the document content has been extracted and metadata such as document type, tags, etc. have been set, so these can now
be used for filtering.
3. **Document Updated**: when a document is updated. Similar to 'added' events, triggers can include filtering by content matching,
- tags, doc type, or correspondent.
+ tags, doc type, correspondent or storage path.
4. **Scheduled**: a scheduled trigger that can be used to run workflows at a specific time. The date used can be either the document
added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date (positive
offsets will trigger after the date, negative offsets will trigger before).
- File path, including wildcards. Note that enabling `PAPERLESS_CONSUMER_RECURSIVE` would allow, for
example, automatically assigning documents to different owners based on the upload directory.
- Mail rule. Choosing this option will force 'mail fetch' to be the workflow source.
-- Content matching (`Added` and `Updated` triggers only). Filter document content using the matching settings.
-- Tags (`Added` and `Updated` triggers only). Filter for documents with any of the specified tags
-- Document type (`Added` and `Updated` triggers only). Filter documents with this doc type
-- Correspondent (`Added` and `Updated` triggers only). Filter documents with this correspondent
+- Content matching (`Added`, `Updated` and `Scheduled` triggers only). Filter document content using the matching settings.
+- Tags (`Added`, `Updated` and `Scheduled` triggers only). Filter for documents with any of the specified tags
+- Document type (`Added`, `Updated` and `Scheduled` triggers only). Filter documents with this doc type
+- Correspondent (`Added`, `Updated` and `Scheduled` triggers only). Filter documents with this correspondent
+- Storage path (`Added`, `Updated` and `Scheduled` triggers only). Filter documents with this storage path
### Workflow Actions
<pngx-input-tags [allowCreate]="false" i18n-title title="Has any of tags" formControlName="filter_has_tags"></pngx-input-tags>
<pngx-input-select i18n-title title="Has correspondent" [items]="correspondents" [allowNull]="true" formControlName="filter_has_correspondent"></pngx-input-select>
<pngx-input-select i18n-title title="Has document type" [items]="documentTypes" [allowNull]="true" formControlName="filter_has_document_type"></pngx-input-select>
+ <pngx-input-select i18n-title title="Has storage path" [items]="storagePaths" [allowNull]="true" formControlName="filter_has_storage_path"></pngx-input-select>
</div>
}
</div>
filter_has_document_type: new FormControl(
trigger.filter_has_document_type
),
+ filter_has_storage_path: new FormControl(
+ trigger.filter_has_storage_path
+ ),
schedule_offset_days: new FormControl(trigger.schedule_offset_days),
schedule_is_recurring: new FormControl(trigger.schedule_is_recurring),
schedule_recurring_interval_days: new FormControl(
filter_has_tags: [],
filter_has_correspondent: null,
filter_has_document_type: null,
+ filter_has_storage_path: null,
matching_algorithm: MATCH_NONE,
match: '',
is_insensitive: true,
filter_has_document_type?: number // DocumentType.id
+ filter_has_storage_path?: number // StoragePath.id
+
schedule_offset_days?: number
schedule_is_recurring?: boolean
)
trigger_matched = False
+ # Document storage_path vs trigger has_storage_path
+ if (
+ trigger.filter_has_storage_path is not None
+ and document.storage_path != trigger.filter_has_storage_path
+ ):
+ reason = (
+ f"Document storage path {document.storage_path} does not match {trigger.filter_has_storage_path}",
+ )
+ trigger_matched = False
+
# Document original_filename vs trigger filename
if (
trigger.filter_filename is not None
document_type=trigger.filter_has_document_type,
)
+ if trigger.filter_has_storage_path is not None:
+ documents = documents.filter(
+ storage_path=trigger.filter_has_storage_path,
+ )
+
if trigger.filter_filename is not None and len(trigger.filter_filename) > 0:
# the true fnmatch will actually run later so we just want a loose filter here
regex = fnmatch_translate(trigger.filter_filename).lstrip("^").rstrip("$")
+++ /dev/null
-# Generated by Django 5.2.5 on 2025-08-27 22:02
-import logging
-
-from django.db import migrations
-from django.db import models
-from django.db import transaction
-
-from documents.templating.utils import convert_format_str_to_template_format
-
-logger = logging.getLogger("paperless.migrations")
-
-
-def convert_from_format_to_template(apps, schema_editor):
- WorkflowActions = apps.get_model("documents", "WorkflowAction")
-
- with transaction.atomic():
- for WorkflowAction in WorkflowActions.objects.all():
- WorkflowAction.assign_title = convert_format_str_to_template_format(
- WorkflowAction.assign_title,
- )
- logger.debug(
- "Converted WorkflowAction id %d title to template format: %s",
- WorkflowAction.id,
- WorkflowAction.assign_title,
- )
- WorkflowAction.save()
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("documents", "1068_alter_document_created"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="WorkflowAction",
- name="assign_title",
- field=models.TextField(
- null=True,
- blank=True,
- help_text=(
- "Assign a document title, can be a JINJA2 template, "
- "see documentation.",
- ),
- ),
- ),
- migrations.RunPython(
- convert_from_format_to_template,
- migrations.RunPython.noop,
- ),
- ]
--- /dev/null
+# Generated by Django 5.2.6 on 2025-09-11 17:29
+
+import django.db.models.deletion
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("documents", "1068_alter_document_created"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="workflowtrigger",
+ name="filter_has_storage_path",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="documents.storagepath",
+ verbose_name="has this storage path",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="workflowaction",
+ name="assign_title",
+ field=models.TextField(
+ blank=True,
+ help_text="Assign a document title, must be a Jinja2 template, see documentation.",
+ null=True,
+ verbose_name="assign title",
+ ),
+ ),
+ ]
verbose_name=_("has this correspondent"),
)
+ filter_has_storage_path = models.ForeignKey(
+ StoragePath,
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ verbose_name=_("has this storage path"),
+ )
+
schedule_offset_days = models.IntegerField(
_("schedule offset days"),
default=0,
"filter_has_tags",
"filter_has_correspondent",
"filter_has_document_type",
+ "filter_has_storage_path",
"schedule_offset_days",
"schedule_is_recurring",
"schedule_recurring_interval_days",
"filter_has_tags": [self.t1.id],
"filter_has_document_type": self.dt.id,
"filter_has_correspondent": self.c.id,
+ "filter_has_storage_path": self.sp.id,
},
],
"actions": [
expected_str = f"Document correspondent {doc.correspondent} does not match {trigger.filter_has_correspondent}"
self.assertIn(expected_str, cm.output[1])
+ def test_document_added_no_match_storage_path(self):
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
+ filter_has_storage_path=self.sp,
+ )
+ action = WorkflowAction.objects.create(
+ assign_title="Doc assign owner",
+ assign_owner=self.user2,
+ )
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.save()
+
+ doc = Document.objects.create(
+ title="sample test",
+ original_filename="sample.pdf",
+ )
+
+ with self.assertLogs("paperless.matching", level="DEBUG") as cm:
+ document_consumption_finished.send(
+ sender=self.__class__,
+ document=doc,
+ )
+ expected_str = f"Document did not match {w}"
+ self.assertIn(expected_str, cm.output[0])
+ expected_str = f"Document storage path {doc.storage_path} does not match {trigger.filter_has_storage_path}"
+ self.assertIn(expected_str, cm.output[1])
+
def test_document_added_invalid_title_placeholders(self):
"""
GIVEN:
filter_filename="*sample*",
filter_has_document_type=self.dt,
filter_has_correspondent=self.c,
+ filter_has_storage_path=self.sp,
)
trigger.filter_has_tags.set([self.t1])
trigger.save()
title=f"sample test {i}",
checksum=f"checksum{i}",
correspondent=self.c,
+ storage_path=self.sp,
original_filename=f"sample_{i}.pdf",
document_type=self.dt if i % 2 == 0 else None,
)