diff --git a/docs/usage.md b/docs/usage.md index 6da6c4d77..8bbb32715 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -457,7 +457,7 @@ fields and permissions, which will be merged. #### Types {#workflow-trigger-types} -Currently, there are four events that correspond to workflow trigger 'types': +Currently, there are five events that correspond to workflow trigger 'types': 1. **Consumption Started**: _before_ a document is consumed, so events can include filters by source (mail, consumption folder or API), file path, file name, mail rule @@ -469,8 +469,10 @@ Currently, there are four events that correspond to workflow trigger 'types': 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). +5. **Version Added**: when a new version is added for an existing document. This trigger evaluates filters against the root document + and applies actions to the root document. -The following flow diagram illustrates the four document trigger types: +The following flow diagram illustrates the document trigger types: ```mermaid flowchart TD diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html index 51b8a2a5d..9fd4b398c 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -164,7 +164,7 @@ } - @if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated || formGroup.get('type').value === WorkflowTriggerType.Scheduled) { + @if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated || formGroup.get('type').value === WorkflowTriggerType.Scheduled || formGroup.get('type').value === WorkflowTriggerType.VersionAdded) { @if (matchingPatternRequired(formGroup)) { @@ -175,7 +175,7 @@ } - @if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated || formGroup.get('type').value === WorkflowTriggerType.Scheduled) { + @if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated || formGroup.get('type').value === WorkflowTriggerType.Scheduled || formGroup.get('type').value === WorkflowTriggerType.VersionAdded) {
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 83e7a40f9..b5666ee0f 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 @@ -120,6 +120,10 @@ export const WORKFLOW_TYPE_OPTIONS = [ id: WorkflowTriggerType.Scheduled, name: $localize`Scheduled`, }, + { + id: WorkflowTriggerType.VersionAdded, + name: $localize`Version Added`, + }, ] export const WORKFLOW_ACTION_OPTIONS = [ diff --git a/src-ui/src/app/data/workflow-trigger.ts b/src-ui/src/app/data/workflow-trigger.ts index 2bc89f188..b959457e3 100644 --- a/src-ui/src/app/data/workflow-trigger.ts +++ b/src-ui/src/app/data/workflow-trigger.ts @@ -12,6 +12,7 @@ export enum WorkflowTriggerType { DocumentAdded = 2, DocumentUpdated = 3, Scheduled = 4, + VersionAdded = 5, } export enum ScheduleDateField { diff --git a/src/documents/apps.py b/src/documents/apps.py index c14f56ee4..e3ea991e3 100644 --- a/src/documents/apps.py +++ b/src/documents/apps.py @@ -15,6 +15,7 @@ class DocumentsConfig(AppConfig): from documents.signals.handlers import add_to_index from documents.signals.handlers import run_workflows_added from documents.signals.handlers import run_workflows_updated + from documents.signals.handlers import run_workflows_version_added from documents.signals.handlers import send_websocket_document_updated from documents.signals.handlers import set_correspondent from documents.signals.handlers import set_document_type @@ -28,6 +29,7 @@ class DocumentsConfig(AppConfig): document_consumption_finished.connect(set_storage_path) document_consumption_finished.connect(add_to_index) document_consumption_finished.connect(run_workflows_added) + document_consumption_finished.connect(run_workflows_version_added) document_consumption_finished.connect(add_or_update_document_in_llm_index) document_updated.connect(run_workflows_updated) document_updated.connect(send_websocket_document_updated) diff --git a/src/documents/matching.py b/src/documents/matching.py index e023adae7..b1c5c7231 100644 --- a/src/documents/matching.py +++ b/src/documents/matching.py @@ -689,6 +689,7 @@ def document_matches_workflow( trigger_type == WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED or trigger_type == WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED or trigger_type == WorkflowTrigger.WorkflowTriggerType.SCHEDULED + or trigger_type == WorkflowTrigger.WorkflowTriggerType.VERSION_ADDED ): trigger_matched, reason = existing_document_matches_workflow( document, diff --git a/src/documents/migrations/0014_alter_workflowtrigger_type.py b/src/documents/migrations/0014_alter_workflowtrigger_type.py new file mode 100644 index 000000000..6304cac54 --- /dev/null +++ b/src/documents/migrations/0014_alter_workflowtrigger_type.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2026-03-02 00:00 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "0013_document_root_document"), + ] + + operations = [ + migrations.AlterField( + model_name="workflowtrigger", + name="type", + field=models.PositiveSmallIntegerField( + choices=[ + (1, "Consumption Started"), + (2, "Document Added"), + (3, "Document Updated"), + (4, "Scheduled"), + (5, "Version Added"), + ], + default=1, + verbose_name="Workflow Trigger Type", + ), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 6147ac001..ba97e2690 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -1174,6 +1174,7 @@ class WorkflowTrigger(models.Model): DOCUMENT_ADDED = 2, _("Document Added") DOCUMENT_UPDATED = 3, _("Document Updated") SCHEDULED = 4, _("Scheduled") + VERSION_ADDED = 5, _("Version Added") class DocumentSourceChoices(models.IntegerChoices): CONSUME_FOLDER = DocumentSource.ConsumeFolder.value, _("Consume Folder") diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 81dd3ddd9..7f5f9cd35 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -783,6 +783,25 @@ def run_workflows_added( ) +def run_workflows_version_added( + sender, + document: Document, + logging_group: uuid.UUID | None = None, + original_file=None, + **kwargs, +) -> None: + if document.root_document is None: + return + + run_workflows( + trigger_type=WorkflowTrigger.WorkflowTriggerType.VERSION_ADDED, + document=document.root_document, + logging_group=logging_group, + overrides=None, + original_file=original_file, + ) + + def run_workflows_updated( sender, document: Document, diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index 78d42437d..b3d2ead8c 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -1786,6 +1786,89 @@ class TestWorkflows( ).exists(), ) + def test_version_added_workflow_runs_on_root_document(self) -> None: + trigger = WorkflowTrigger.objects.create( + type=WorkflowTrigger.WorkflowTriggerType.VERSION_ADDED, + ) + action = WorkflowAction.objects.create( + assign_title="Updated by version", + assign_owner=self.user2, + ) + workflow = Workflow.objects.create( + name="Version workflow", + order=0, + ) + workflow.triggers.add(trigger) + workflow.actions.add(action) + + root_doc = Document.objects.create( + title="root", + correspondent=self.c, + original_filename="root.pdf", + ) + version_doc = Document.objects.create( + title="version", + correspondent=self.c, + original_filename="version.pdf", + root_document=root_doc, + ) + + document_consumption_finished.send( + sender=self.__class__, + document=version_doc, + ) + + root_doc.refresh_from_db() + version_doc.refresh_from_db() + + self.assertEqual(root_doc.title, "Updated by version") + self.assertEqual(root_doc.owner, self.user2) + self.assertIsNone(version_doc.owner) + self.assertEqual( + WorkflowRun.objects.filter( + workflow=workflow, + type=WorkflowTrigger.WorkflowTriggerType.VERSION_ADDED, + document=root_doc, + ).count(), + 1, + ) + + def test_version_added_workflow_ignored_for_root_documents(self) -> None: + trigger = WorkflowTrigger.objects.create( + type=WorkflowTrigger.WorkflowTriggerType.VERSION_ADDED, + ) + action = WorkflowAction.objects.create( + assign_title="Should not run", + ) + workflow = Workflow.objects.create( + name="Version workflow", + order=0, + ) + workflow.triggers.add(trigger) + workflow.actions.add(action) + + root_doc = Document.objects.create( + title="root", + correspondent=self.c, + original_filename="root.pdf", + ) + + document_consumption_finished.send( + sender=self.__class__, + document=root_doc, + ) + + root_doc.refresh_from_db() + + self.assertEqual(root_doc.title, "root") + self.assertFalse( + WorkflowRun.objects.filter( + workflow=workflow, + type=WorkflowTrigger.WorkflowTriggerType.VERSION_ADDED, + document=root_doc, + ).exists(), + ) + def test_document_updated_workflow(self) -> None: trigger = WorkflowTrigger.objects.create( type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,