diff --git a/.env.demo b/.env.demo index 277da1b4..fb4752f2 100644 --- a/.env.demo +++ b/.env.demo @@ -122,6 +122,14 @@ PAPERLESS_NGX_API_TOKEN= # Example: PAPERLESS_CUSTOM_FIELD_ABSENDER=Absender # PAPERLESS_CUSTOM_FIELD_ABSENDER= +# Optional: JSON mapping of metadata fields to Paperless custom field names +# This allows you to map multiple extracted metadata fields to custom fields in Paperless +# The mapping format is: {"metadata_field_name": "PaperlessCustomFieldName", ...} +# Available metadata fields: absender, empfaenger, correspondent, document_type, language, +# kommunikationsart, kommunikationskategorie, reference_number, etc. +# Example: PAPERLESS_CUSTOM_FIELDS_MAPPING={"absender": "Sender", "empfaenger": "Recipient", "language": "Language", "correspondent": "Correspondent"} +# PAPERLESS_CUSTOM_FIELDS_MAPPING= + # Dropbox DROPBOX_APP_KEY= DROPBOX_APP_SECRET= diff --git a/app/config.py b/app/config.py index b090a530..150f3697 100644 --- a/app/config.py +++ b/app/config.py @@ -32,6 +32,9 @@ class Settings(BaseSettings): paperless_ngx_api_token: Optional[str] = None paperless_host: Optional[str] = None paperless_custom_field_absender: Optional[str] = None # Name or ID of the "absender" custom field in Paperless + # JSON mapping of metadata field names to Paperless custom field names + # Example: {"absender": "Sender", "empfaenger": "Recipient", "language": "Language", "correspondent": "Correspondent"} + paperless_custom_fields_mapping: Optional[str] = None azure_ai_key: str azure_region: str diff --git a/app/tasks/upload_to_paperless.py b/app/tasks/upload_to_paperless.py index 9c2ed2d6..3c2076c2 100644 --- a/app/tasks/upload_to_paperless.py +++ b/app/tasks/upload_to_paperless.py @@ -16,6 +16,7 @@ logger = logging.getLogger(__name__) POLL_MAX_ATTEMPTS = 10 POLL_INTERVAL_SEC = 3 +UNKNOWN_VALUE = "Unknown" # Magic value used to indicate missing metadata def _get_headers(): @@ -126,7 +127,7 @@ def set_document_custom_fields(doc_id: int, custom_fields: dict, task_id: str) - # Build custom_fields array for PATCH request custom_fields_array = [] for field_name, value in custom_fields.items(): - if value and value != "Unknown": # Only set non-empty, non-Unknown values + if value and value != UNKNOWN_VALUE: # Only set non-empty, non-Unknown values try: field_id = get_custom_field_id(field_name) custom_fields_array.append({"field": field_id, "value": value}) @@ -244,22 +245,56 @@ def upload_to_paperless(self, file_path: str, file_id: int = None): ) # Set custom fields if configured and metadata available - if settings.paperless_custom_field_absender and metadata.get("absender"): - logger.info(f"[{task_id}] Setting custom fields for document {doc_id}") - log_task_progress(task_id, "set_custom_fields", "in_progress", "Setting custom fields", file_id=file_id) + custom_fields_to_set = {} - # Prepare custom fields to set - custom_fields_to_set = {} - if settings.paperless_custom_field_absender: + # First, check for the new flexible mapping configuration + if settings.paperless_custom_fields_mapping: + try: + # Parse JSON mapping: {"metadata_field": "PaperlessFieldName", ...} + import json + + field_mapping = json.loads(settings.paperless_custom_fields_mapping) + logger.info(f"[{task_id}] Using custom fields mapping: {field_mapping}") + + # Map each metadata field to its corresponding Paperless custom field + for metadata_field, paperless_field in field_mapping.items(): + if metadata_field in metadata and metadata[metadata_field]: + custom_fields_to_set[paperless_field] = metadata[metadata_field] + logger.debug( + f"[{task_id}] Mapping {metadata_field}='{metadata[metadata_field]}' to field '{paperless_field}'" + ) + except json.JSONDecodeError as e: + logger.error(f"[{task_id}] Failed to parse PAPERLESS_CUSTOM_FIELDS_MAPPING: {e}") + except Exception as e: + logger.error(f"[{task_id}] Error processing custom fields mapping: {e}") + + # Fallback to legacy single-field configuration for backward compatibility + if settings.paperless_custom_field_absender and metadata.get("absender"): + # Only add if not already set by the mapping + if settings.paperless_custom_field_absender not in custom_fields_to_set: custom_fields_to_set[settings.paperless_custom_field_absender] = metadata.get("absender") + logger.debug(f"[{task_id}] Using legacy absender field configuration") + + # Set custom fields if we have any to set + if custom_fields_to_set: + logger.info(f"[{task_id}] Setting {len(custom_fields_to_set)} custom field(s) for document {doc_id}") + log_task_progress(task_id, "set_custom_fields", "in_progress", "Setting custom fields", file_id=file_id) try: set_document_custom_fields(doc_id, custom_fields_to_set, task_id) - log_task_progress(task_id, "set_custom_fields", "success", "Custom fields set", file_id=file_id) + log_task_progress( + task_id, + "set_custom_fields", + "success", + f"Set {len(custom_fields_to_set)} custom field(s)", + file_id=file_id, + ) except Exception as e: logger.error(f"[{task_id}] Failed to set custom fields: {e}") log_task_progress(task_id, "set_custom_fields", "failure", f"Failed: {str(e)}", file_id=file_id) # Don't fail the entire upload if custom fields fail + else: + logger.debug(f"[{task_id}] No custom fields configured or no metadata available") return { "status": "Completed", diff --git a/docs/ConfigurationGuide.md b/docs/ConfigurationGuide.md index b16a2636..a68459a6 100644 --- a/docs/ConfigurationGuide.md +++ b/docs/ConfigurationGuide.md @@ -212,11 +212,45 @@ SECURITY_HEADER_CSP_VALUE="default-src 'self'; script-src 'self' https://trusted ### Paperless NGX -| **Variable** | **Description** | -|-----------------------------------|-----------------------------------------------------------------------------------------------------| -| `PAPERLESS_NGX_API_TOKEN` | API token for Paperless NGX. | -| `PAPERLESS_HOST` | Root URL for Paperless NGX (e.g. `https://paperless.example.com`). | -| `PAPERLESS_CUSTOM_FIELD_ABSENDER` | (Optional) Name of the custom field in Paperless-ngx to store the sender ("absender") information. If set, the extracted sender will be automatically set as a custom field after document upload. Example: `Absender` or `Sender` | +| **Variable** | **Description** | +|-------------------------------------|-----------------------------------------------------------------------------------------------------| +| `PAPERLESS_NGX_API_TOKEN` | API token for Paperless NGX. | +| `PAPERLESS_HOST` | Root URL for Paperless NGX (e.g. `https://paperless.example.com`). | +| `PAPERLESS_CUSTOM_FIELD_ABSENDER` | (Optional, Legacy) Name of the custom field in Paperless-ngx to store the sender ("absender") information. If set, the extracted sender will be automatically set as a custom field after document upload. Example: `Absender` or `Sender` | +| `PAPERLESS_CUSTOM_FIELDS_MAPPING` | (Optional, Recommended) JSON mapping of extracted metadata fields to Paperless custom field names. This allows you to map multiple fields at once. Format: `{"metadata_field": "CustomFieldName", ...}`. See examples below. | + +#### Custom Fields Mapping Examples + +**Single Field (Legacy Method)**: +```bash +PAPERLESS_CUSTOM_FIELD_ABSENDER=Absender +``` + +**Multiple Fields (Recommended Method)**: +```bash +# Map multiple metadata fields to custom fields in Paperless +PAPERLESS_CUSTOM_FIELDS_MAPPING='{"absender": "Sender", "empfaenger": "Recipient", "language": "Language"}' +``` + +**All Available Metadata Fields**: +DocuElevate extracts the following fields that can be mapped to Paperless custom fields: +- `absender` - Sender/author of the document +- `empfaenger` - Recipient of the document +- `correspondent` - The issuing entity/company (shortened name) +- `document_type` - Type classification (Invoice, Contract, etc.) +- `language` - Document language (ISO 639-1 code, e.g., "de", "en") +- `kommunikationsart` - Communication type (German classification) +- `kommunikationskategorie` - Communication category (German classification) +- `reference_number` - Invoice/order/reference number if found +- `title` - Human-readable document title +- `tags` - List of thematic keywords (array) + +**Complete Example**: +```bash +PAPERLESS_CUSTOM_FIELDS_MAPPING='{"absender": "Sender", "empfaenger": "Recipient", "correspondent": "Correspondent", "language": "Language", "reference_number": "ReferenceNumber"}' +``` + +**Note**: Custom fields must be created in your Paperless-ngx instance before DocuElevate can use them. The field names in the mapping (right side of the JSON) must exactly match the names in Paperless. ### Dropbox diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 6ab73be2..0d05bd47 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -150,13 +150,44 @@ Depending on the system configuration, DocuElevate can perform: If Paperless-ngx is configured, DocuElevate can automatically upload processed documents and set custom fields with extracted metadata: 1. **Automatic Upload**: After processing, documents are automatically uploaded to Paperless-ngx -2. **Custom Fields**: If configured via `PAPERLESS_CUSTOM_FIELD_ABSENDER`, the sender ("absender") information extracted by AI will be automatically set as a custom field in Paperless -3. **Setup Requirements**: - - Create a custom field in Paperless-ngx (e.g., named "Absender" or "Sender") - - Set the `PAPERLESS_CUSTOM_FIELD_ABSENDER` environment variable to match your custom field name - - Ensure the custom field exists in Paperless before processing documents +2. **Custom Fields Support**: DocuElevate can automatically populate custom fields in Paperless with extracted metadata +3. **Flexible Field Mapping**: Map any extracted metadata field to any custom field in Paperless -**Note**: The custom field feature requires that the field is already created in your Paperless-ngx instance. DocuElevate will look up the field by name and set its value automatically. +#### Setting Up Custom Fields + +**Basic Setup (Single Field)**: +```bash +# In your .env file +PAPERLESS_CUSTOM_FIELD_ABSENDER=Absender +``` + +**Advanced Setup (Multiple Fields)**: +```bash +# Map multiple metadata fields to Paperless custom fields +PAPERLESS_CUSTOM_FIELDS_MAPPING='{"absender": "Sender", "empfaenger": "Recipient", "language": "Language", "correspondent": "Correspondent"}' +``` + +**Available Metadata Fields**: +- `absender` - Sender/author +- `empfaenger` - Recipient +- `correspondent` - Issuing company (short name) +- `language` - Document language (e.g., "de", "en") +- `document_type` - Classification (Invoice, Contract, etc.) +- `reference_number` - Invoice/order/reference number +- `kommunikationsart` - Communication type +- `kommunikationskategorie` - Communication category +- And more... + +**Requirements**: +- Create the custom fields in Paperless-ngx first (Settings → Custom Fields) +- The field names in your configuration must exactly match the names in Paperless +- Ensure `PAPERLESS_HOST` and `PAPERLESS_NGX_API_TOKEN` are configured + +**How It Works**: +1. DocuElevate extracts metadata from your documents using AI +2. The document is uploaded to Paperless-ngx +3. After successful upload, custom fields are automatically populated +4. You can view the populated fields in your Paperless-ngx document details ## API Access