@if (formGroup.get('schedule_delay_field').value === 'custom_field') {
}
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 570756604..09296250d 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
@@ -339,6 +339,7 @@ export class WorkflowEditDialogComponent
trigger.filter_has_document_type
),
schedule_delay: new FormControl(trigger.schedule_delay),
+ schedule_is_recurring: new FormControl(trigger.schedule_is_recurring),
schedule_delay_field: new FormControl(trigger.schedule_delay_field),
schedule_delay_custom_field: new FormControl(
trigger.schedule_delay_custom_field
@@ -448,6 +449,7 @@ export class WorkflowEditDialogComponent
match: '',
is_insensitive: true,
schedule_delay: null,
+ schedule_is_recurring: false,
schedule_delay_field: ScheduleDelayField.Added,
schedule_delay_custom_field: null,
}
diff --git a/src-ui/src/app/data/workflow-trigger.ts b/src-ui/src/app/data/workflow-trigger.ts
index 78d5e418b..ed160e024 100644
--- a/src-ui/src/app/data/workflow-trigger.ts
+++ b/src-ui/src/app/data/workflow-trigger.ts
@@ -45,6 +45,8 @@ export interface WorkflowTrigger extends ObjectWithId {
schedule_delay?: string
+ schedule_is_recurring?: boolean
+
schedule_delay_field?: ScheduleDelayField
schedule_delay_custom_field?: number // CustomField.id
diff --git a/src/documents/migrations/1056_workflowtrigger_schedule_delay_and_more.py b/src/documents/migrations/1056_workflowtrigger_schedule_delay_and_more.py
index 9391fd2d9..8a4b949b9 100644
--- a/src/documents/migrations/1056_workflowtrigger_schedule_delay_and_more.py
+++ b/src/documents/migrations/1056_workflowtrigger_schedule_delay_and_more.py
@@ -1,6 +1,7 @@
-# Generated by Django 5.1.1 on 2024-10-23 20:54
+# Generated by Django 5.1.1 on 2024-10-24 04:03
import django.db.models.deletion
+import django.utils.timezone
from django.db import migrations
from django.db import models
@@ -49,6 +50,15 @@ class Migration(migrations.Migration):
verbose_name="schedule delay field",
),
),
+ migrations.AddField(
+ model_name="workflowtrigger",
+ name="schedule_is_recurring",
+ field=models.BooleanField(
+ default=False,
+ help_text="If the schedule should be recurring.",
+ verbose_name="schedule is recurring",
+ ),
+ ),
migrations.AlterField(
model_name="workflowtrigger",
name="type",
@@ -63,4 +73,49 @@ class Migration(migrations.Migration):
verbose_name="Workflow Trigger Type",
),
),
+ migrations.CreateModel(
+ name="WorkflowRun",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "run_at",
+ models.DateTimeField(
+ db_index=True,
+ default=django.utils.timezone.now,
+ verbose_name="date run",
+ ),
+ ),
+ (
+ "document",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="workflow_runs",
+ to="documents.document",
+ verbose_name="document",
+ ),
+ ),
+ (
+ "workflow",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="runs",
+ to="documents.workflow",
+ verbose_name="workflow",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "workflow run",
+ "verbose_name_plural": "workflow runs",
+ },
+ ),
]
diff --git a/src/documents/models.py b/src/documents/models.py
index 006f48a56..798075d8f 100644
--- a/src/documents/models.py
+++ b/src/documents/models.py
@@ -1115,6 +1115,14 @@ class WorkflowTrigger(models.Model):
),
)
+ schedule_is_recurring = models.BooleanField(
+ _("schedule is recurring"),
+ default=False,
+ help_text=_(
+ "If the schedule should be recurring.",
+ ),
+ )
+
schedule_delay_field = models.CharField(
_("schedule delay field"),
max_length=20,
@@ -1383,3 +1391,33 @@ class Workflow(models.Model):
def __str__(self):
return f"Workflow: {self.name}"
+
+
+class WorkflowRun(models.Model):
+ workflow = models.ForeignKey(
+ Workflow,
+ on_delete=models.CASCADE,
+ related_name="runs",
+ verbose_name=_("workflow"),
+ )
+
+ document = models.ForeignKey(
+ Document,
+ null=True,
+ on_delete=models.CASCADE,
+ related_name="workflow_runs",
+ verbose_name=_("document"),
+ )
+
+ run_at = models.DateTimeField(
+ _("date run"),
+ default=timezone.now,
+ db_index=True,
+ )
+
+ class Meta:
+ verbose_name = _("workflow run")
+ verbose_name_plural = _("workflow runs")
+
+ def __str__(self):
+ return f"WorkflowRun {self.pk}"
diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py
index fdb1be8a5..764d0a8aa 100644
--- a/src/documents/serialisers.py
+++ b/src/documents/serialisers.py
@@ -1738,10 +1738,6 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer):
label="Trigger Type",
)
- schedule_delay = serializers.CharField(
- required=False,
- )
-
class Meta:
model = WorkflowTrigger
fields = [
@@ -1758,6 +1754,7 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer):
"filter_has_correspondent",
"filter_has_document_type",
"schedule_delay",
+ "schedule_is_recurring",
"schedule_delay_field",
"schedule_delay_custom_field",
]
diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py
index 73aee2936..0a8439085 100644
--- a/src/documents/signals/handlers.py
+++ b/src/documents/signals/handlers.py
@@ -37,6 +37,7 @@ from documents.models import PaperlessTask
from documents.models import Tag
from documents.models import Workflow
from documents.models import WorkflowAction
+from documents.models import WorkflowRun
from documents.models import WorkflowTrigger
from documents.permissions import get_objects_for_user_owner_aware
from documents.permissions import set_permissions_for_object
@@ -915,6 +916,11 @@ def run_workflows(
document.save()
document.tags.set(doc_tag_ids)
+ WorkflowRun.objects.create(
+ workflow=workflow,
+ document=document if not use_overrides else None,
+ )
+
if use_overrides:
return overrides, "\n".join(messages)
diff --git a/src/documents/tasks.py b/src/documents/tasks.py
index 3c9eba7c8..5ed23b1af 100644
--- a/src/documents/tasks.py
+++ b/src/documents/tasks.py
@@ -38,6 +38,7 @@ from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from documents.models import Workflow
+from documents.models import WorkflowRun
from documents.models import WorkflowTrigger
from documents.parsers import DocumentParser
from documents.parsers import get_parser_class_for_mime_type
@@ -382,6 +383,26 @@ def check_scheduled_workflows():
f"Found {documents.count()} documents for trigger {trigger}",
)
for document in documents:
+ workflow_runs = WorkflowRun.objects.filter(
+ document=document,
+ workflow=workflow,
+ )
+ if not trigger.schedule_is_recurring and workflow_runs.exists():
+ # schedule is non-recurring and the workflow has already been run
+ logger.debug(
+ f"Skipping document {document} for non-recurring workflow {workflow} as it has already been run",
+ )
+ continue
+ elif (
+ trigger.schedule_is_recurring
+ and workflow_runs.exists()
+ and workflow_runs.last().run_at > timezone.now() - delay_td
+ ):
+ # schedule is recurring but the last run was within the delay
+ logger.debug(
+ f"Skipping document {document} for recurring workflow {workflow} as the last run was within the delay",
+ )
+ continue
run_workflows(
WorkflowTrigger.WorkflowTriggerType.SCHEDULED,
document,
diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py
index c02b44a1f..a98cb4d3b 100644
--- a/src/documents/tests/test_workflows.py
+++ b/src/documents/tests/test_workflows.py
@@ -1354,7 +1354,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
def test_new_trigger_type_raises_exception(self):
trigger = WorkflowTrigger.objects.create(
- type=4,
+ type=99,
)
action = WorkflowAction.objects.create(
assign_title="Doc assign owner",