From 2c329255a90d58d9b71cded48db899f364509c29 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:07:00 -0700 Subject: [PATCH] email and webhooks too --- docs/usage.md | 2 +- src/documents/tests/test_workflows.py | 66 +++++++++++++++++++++++++++ src/documents/workflows/actions.py | 22 +++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/docs/usage.md b/docs/usage.md index c4c407414..12d2fcb2b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -726,7 +726,7 @@ applied. You can use the following placeholders in the template with any trigger - `{{added_time}}`: added time in HH:MM format - `{{original_filename}}`: original file name without extension - `{{filename}}`: current file name without extension (for "added" workflows this may not be final yet, you can use `{{original_filename}}`) -- `{{custom_fields}}` (title assignments only): Custom field names mapped to their type and value, as in [file name templates](advanced_usage.md#additional-variables). For example, `{{ custom_fields.Invoice.value }}` or `{{ custom_fields | get_cf_value("Invoice", "") }}` for a fallback when the field is missing. In title assignments, fields assigned by the same action are available. +- `{{custom_fields}}`: Custom field names mapped to their type and value, as in [file name templates](advanced_usage.md#additional-variables). For example, `{{ custom_fields.Invoice.value }}` or `{{ custom_fields | get_cf_value("Invoice", "") }}` for a fallback when the field is missing. In title assignments, fields assigned by the same action are available. - `{{doc_title}}`: current document title (cannot be used in title assignment) The following placeholders are only available for "added" or "updated" triggers diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index 265429d8f..506fa6872 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -5164,6 +5164,72 @@ class TestWorkflows( self.assertEqual(Document.objects.count(), 0) +@pytest.mark.django_db +@pytest.mark.parametrize("use_overrides", [False, True]) +def test_custom_fields_in_email_and_webhook_templates( + use_overrides, + settings, + tmp_path, +): + from documents.workflows.actions import build_workflow_action_context + from documents.workflows.actions import execute_email_action + from documents.workflows.actions import execute_webhook_action + + settings.EMAIL_ENABLED = True + field = CustomField.objects.create( + name="Invoice", + data_type=CustomField.FieldDataType.STRING, + ) + if use_overrides: + invoice = tmp_path / "invoice.pdf" + invoice.write_bytes(b"%PDF-1.7") + document = ConsumableDocument( + source=DocumentSource.ConsumeFolder, + original_file=invoice, + ) + overrides = DocumentMetadataOverrides(custom_fields={field.pk: "INV-123"}) + else: + document = Document.objects.create(title="Invoice") + CustomFieldInstance.objects.create( + document=document, + field=field, + value_text="INV-123", + ) + overrides = None + context = build_workflow_action_context(document, overrides) + template = '{{ custom_fields.Invoice.value }} {{ custom_fields | get_cf_value("Missing", "fallback") }}' + action = WorkflowAction( + email=WorkflowActionEmail( + subject=template, + body=template, + to="test@example.com", + ), + webhook=WorkflowActionWebhook( + url="https://example.com", + body=template, + ), + ) + with mock.patch("documents.workflows.actions.send_email") as send_email: + execute_email_action( + action, + document, + context, + None, + Path("invoice.pdf"), + WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED, + ) + assert send_email.call_args.kwargs["subject"] == "INV-123 fallback" + assert send_email.call_args.kwargs["body"] == "INV-123 fallback" + with mock.patch("documents.workflows.actions.send_webhook.apply_async") as send: + for use_params in (False, True): + action.webhook.use_params = use_params + action.webhook.params = {"invoice": template} + execute_webhook_action(action, document, context, None, Path("invoice.pdf")) + assert send.call_args.kwargs["kwargs"]["data"] == ( + {"invoice": "INV-123 fallback"} if use_params else "INV-123 fallback" + ) + + class TestWebhookSend: def test_send_webhook_data_or_json( self, diff --git a/src/documents/workflows/actions.py b/src/documents/workflows/actions.py index 5a2e0c6be..f6011126f 100644 --- a/src/documents/workflows/actions.py +++ b/src/documents/workflows/actions.py @@ -11,6 +11,8 @@ from documents.data_models import DocumentMetadataOverrides from documents.mail import EmailAttachment from documents.mail import send_email from documents.models import Correspondent +from documents.models import CustomField +from documents.models import CustomFieldInstance from documents.models import Document from documents.models import DocumentType from documents.models import WorkflowAction @@ -48,6 +50,9 @@ def build_workflow_action_context( "added": timezone.localtime(document.added), "created": document.created, "id": document.pk, + "custom_fields": CustomFieldInstance.global_objects.filter( + document=document, + ).select_related("field"), } correspondent_obj = ( @@ -80,6 +85,19 @@ def build_workflow_action_context( "added": timezone.localtime(timezone.now()), "created": overrides.created if overrides else None, "id": "", + "custom_fields": [ + CustomFieldInstance( + field=field, + **{ + CustomFieldInstance.get_value_field_name( + field.data_type, + ): overrides.custom_fields[field.pk], + }, + ) + for field in CustomField.objects.filter( + pk__in=overrides.custom_fields or {}, + ) + ], } @@ -116,6 +134,7 @@ def execute_email_action( context["title"], context["doc_url"], context["id"], + custom_fields=context["custom_fields"], ) if action.email.subject else "" @@ -133,6 +152,7 @@ def execute_email_action( context["title"], context["doc_url"], context["id"], + custom_fields=context["custom_fields"], ) if action.email.body else "" @@ -211,6 +231,7 @@ def execute_webhook_action( context["title"], context["doc_url"], context["id"], + custom_fields=context["custom_fields"], ) except Exception as e: logger.error( @@ -230,6 +251,7 @@ def execute_webhook_action( context["title"], context["doc_url"], context["id"], + custom_fields=context["custom_fields"], ) headers = {} if action.webhook.headers: