Support consumption template source
This commit is contained in:
parent
ed37f82c0f
commit
5a32b6fb9a
2
Pipfile
2
Pipfile
@ -3,7 +3,6 @@ url = "https://pypi.python.org/simple"
|
|||||||
verify_ssl = true
|
verify_ssl = true
|
||||||
name = "pypi"
|
name = "pypi"
|
||||||
|
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
dateparser = "~=1.1"
|
dateparser = "~=1.1"
|
||||||
# WARNING: django does not use semver.
|
# WARNING: django does not use semver.
|
||||||
@ -51,6 +50,7 @@ pdf2image = "*"
|
|||||||
flower = "*"
|
flower = "*"
|
||||||
bleach = "*"
|
bleach = "*"
|
||||||
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
||||||
|
django-multiselectfield = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
# Linting
|
# Linting
|
||||||
|
939
Pipfile.lock
generated
939
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@
|
|||||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
|
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
|
||||||
<pngx-input-number i18n-title title="Rule order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
|
<pngx-input-number i18n-title title="Rule order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
|
||||||
<p class="small" i18n>Paperless-ngx will process mails that match <em>any</em> of the filters specified below.</p>
|
<p class="small" i18n>Paperless-ngx will process mails that match <em>any</em> of the filters specified below.</p>
|
||||||
|
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.filter_filename"></pngx-input-select>
|
||||||
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply template to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply template to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
||||||
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply template to documents that match this path. Wildcards specified as * are allowed. Case insensitive." [error]="error?.filter_path"></pngx-input-text>
|
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply template to documents that match this path. Wildcards specified as * are allowed. Case insensitive." [error]="error?.filter_path"></pngx-input-text>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,16 +2,12 @@ import { Component } from '@angular/core'
|
|||||||
import { FormGroup, FormControl } from '@angular/forms'
|
import { FormGroup, FormControl } from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { first } from 'rxjs'
|
import { first } from 'rxjs'
|
||||||
import { PaperlessConsumptionTemplate } from 'src/app/data/paperless-consumption-template'
|
import {
|
||||||
|
DocumentSource,
|
||||||
|
PaperlessConsumptionTemplate,
|
||||||
|
} from 'src/app/data/paperless-consumption-template'
|
||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||||
import {
|
|
||||||
MailFilterAttachmentType,
|
|
||||||
MailRuleConsumptionScope,
|
|
||||||
MailAction,
|
|
||||||
MailMetadataTitleOption,
|
|
||||||
MailMetadataCorrespondentOption,
|
|
||||||
} from 'src/app/data/paperless-mail-rule'
|
|
||||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||||
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
|
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
@ -21,6 +17,21 @@ import { UserService } from 'src/app/services/rest/user.service'
|
|||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { EditDialogComponent } from '../edit-dialog.component'
|
import { EditDialogComponent } from '../edit-dialog.component'
|
||||||
|
|
||||||
|
const SOURCE_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: DocumentSource.ConsumeFolder,
|
||||||
|
name: $localize`Documents uploaded via consume folder`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DocumentSource.ApiUpload,
|
||||||
|
name: $localize`Documents uploaded via api upload`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: DocumentSource.MailFetch,
|
||||||
|
name: $localize`Documents uploaded via mail fetch`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-consumption-template-edit-dialog',
|
selector: 'pngx-consumption-template-edit-dialog',
|
||||||
templateUrl: './consumption-template-edit-dialog.component.html',
|
templateUrl: './consumption-template-edit-dialog.component.html',
|
||||||
@ -74,6 +85,7 @@ export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<
|
|||||||
filter_filename: new FormControl(null),
|
filter_filename: new FormControl(null),
|
||||||
filter_path: new FormControl(null),
|
filter_path: new FormControl(null),
|
||||||
order: new FormControl(null),
|
order: new FormControl(null),
|
||||||
|
sources: new FormControl([]),
|
||||||
assign_tags: new FormControl([]),
|
assign_tags: new FormControl([]),
|
||||||
assign_owner: new FormControl(null),
|
assign_owner: new FormControl(null),
|
||||||
assign_document_type: new FormControl(null),
|
assign_document_type: new FormControl(null),
|
||||||
@ -85,4 +97,8 @@ export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<
|
|||||||
assign_change_groups: new FormControl(null),
|
assign_change_groups: new FormControl(null),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get sourceOptions() {
|
||||||
|
return SOURCE_OPTIONS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,10 @@ import {
|
|||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { PaperlessConsumptionTemplate } from 'src/app/data/paperless-consumption-template'
|
import {
|
||||||
|
DocumentSource,
|
||||||
|
PaperlessConsumptionTemplate,
|
||||||
|
} from 'src/app/data/paperless-consumption-template'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
|
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
@ -24,6 +27,11 @@ const templates: PaperlessConsumptionTemplate[] = [
|
|||||||
id: 0,
|
id: 0,
|
||||||
name: 'Template 1',
|
name: 'Template 1',
|
||||||
order: 0,
|
order: 0,
|
||||||
|
sources: [
|
||||||
|
DocumentSource.ConsumeFolder,
|
||||||
|
DocumentSource.ApiUpload,
|
||||||
|
DocumentSource.MailFetch,
|
||||||
|
],
|
||||||
filter_filename: 'foo',
|
filter_filename: 'foo',
|
||||||
filter_path: 'bar',
|
filter_path: 'bar',
|
||||||
assign_tags: [1, 2, 3],
|
assign_tags: [1, 2, 3],
|
||||||
@ -32,6 +40,7 @@ const templates: PaperlessConsumptionTemplate[] = [
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'Template 2',
|
name: 'Template 2',
|
||||||
order: 1,
|
order: 1,
|
||||||
|
sources: [DocumentSource.MailFetch],
|
||||||
filter_filename: null,
|
filter_filename: null,
|
||||||
filter_path: 'foo/bar',
|
filter_path: 'foo/bar',
|
||||||
assign_owner: 1,
|
assign_owner: 1,
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import { ObjectWithPermissions } from './object-with-permissions'
|
import { ObjectWithPermissions } from './object-with-permissions'
|
||||||
|
|
||||||
|
export enum DocumentSource {
|
||||||
|
ConsumeFolder = 1,
|
||||||
|
ApiUpload = 2,
|
||||||
|
MailFetch = 3,
|
||||||
|
}
|
||||||
|
|
||||||
export interface PaperlessConsumptionTemplate extends ObjectWithPermissions {
|
export interface PaperlessConsumptionTemplate extends ObjectWithPermissions {
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
order: number
|
order: number
|
||||||
|
|
||||||
|
sources: DocumentSource[]
|
||||||
|
|
||||||
filter_filename: string
|
filter_filename: string
|
||||||
|
|
||||||
filter_path: string
|
filter_path: string
|
||||||
|
@ -21,6 +21,7 @@ from django.utils import timezone
|
|||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.permissions import set_permissions_for_object
|
from documents.permissions import set_permissions_for_object
|
||||||
from documents.utils import copy_basic_file_stats
|
from documents.utils import copy_basic_file_stats
|
||||||
@ -595,16 +596,25 @@ class Consumer(LoggingMixin):
|
|||||||
|
|
||||||
def get_template_overrides(
|
def get_template_overrides(
|
||||||
self,
|
self,
|
||||||
input_doc: Path,
|
input_doc: ConsumableDocument,
|
||||||
) -> DocumentMetadataOverrides:
|
) -> DocumentMetadataOverrides:
|
||||||
overrides = DocumentMetadataOverrides()
|
overrides = DocumentMetadataOverrides()
|
||||||
for template in ConsumptionTemplate.objects.all():
|
for template in ConsumptionTemplate.objects.all():
|
||||||
template_overrides = DocumentMetadataOverrides()
|
template_overrides = DocumentMetadataOverrides()
|
||||||
|
|
||||||
if fnmatch(
|
if int(input_doc.source) in list(template.sources) and (
|
||||||
input_doc.name.lower(),
|
(
|
||||||
template.filter_filename.lower(),
|
template.filter_filename is not None
|
||||||
) or input_doc.match(template.filter_path):
|
and fnmatch(
|
||||||
|
input_doc.original_file.name.lower(),
|
||||||
|
template.filter_filename.lower(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
template.filter_path is not None
|
||||||
|
and input_doc.original_file.match(template.filter_path)
|
||||||
|
)
|
||||||
|
):
|
||||||
self.log.info(f"Document matched consumption template {template.name}")
|
self.log.info(f"Document matched consumption template {template.name}")
|
||||||
if template.assign_tags is not None:
|
if template.assign_tags is not None:
|
||||||
template_overrides.tag_ids = [
|
template_overrides.tag_ids = [
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
# Generated by Django 4.1.11 on 2023-09-16 05:01
|
# Generated by Django 4.1.11 on 2023-09-16 18:04
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
import multiselectfield.db.fields
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
# TODO: migrate permissions, upgrade mail rules?
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -31,6 +34,18 @@ class Migration(migrations.Migration):
|
|||||||
models.CharField(max_length=256, unique=True, verbose_name="name"),
|
models.CharField(max_length=256, unique=True, verbose_name="name"),
|
||||||
),
|
),
|
||||||
("order", models.IntegerField(default=0, verbose_name="order")),
|
("order", models.IntegerField(default=0, verbose_name="order")),
|
||||||
|
(
|
||||||
|
"sources",
|
||||||
|
multiselectfield.db.fields.MultiSelectField(
|
||||||
|
choices=[
|
||||||
|
(1, "Consume Folder"),
|
||||||
|
(2, "Api Upload"),
|
||||||
|
(3, "Mail Fetch"),
|
||||||
|
],
|
||||||
|
default="1,2,3",
|
||||||
|
max_length=3,
|
||||||
|
),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"filter_path",
|
"filter_path",
|
||||||
models.CharField(
|
models.CharField(
|
||||||
|
@ -18,7 +18,9 @@ from django.core.validators import MinValueValidator
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from multiselectfield import MultiSelectField
|
||||||
|
|
||||||
|
from documents.data_models import DocumentSource
|
||||||
from documents.parsers import get_default_file_extension
|
from documents.parsers import get_default_file_extension
|
||||||
|
|
||||||
ALL_STATES = sorted(states.ALL_STATES)
|
ALL_STATES = sorted(states.ALL_STATES)
|
||||||
@ -738,6 +740,13 @@ class ShareLink(models.Model):
|
|||||||
return f"Share Link for {self.document.title}"
|
return f"Share Link for {self.document.title}"
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENT_SOURCE = (
|
||||||
|
(int(DocumentSource.ConsumeFolder), _("Consume Folder")),
|
||||||
|
(int(DocumentSource.ApiUpload), _("Api Upload")),
|
||||||
|
(int(DocumentSource.MailFetch), _("Mail Fetch")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConsumptionTemplate(ModelWithOwner):
|
class ConsumptionTemplate(ModelWithOwner):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("consumption template")
|
verbose_name = _("consumption template")
|
||||||
@ -747,6 +756,12 @@ class ConsumptionTemplate(ModelWithOwner):
|
|||||||
|
|
||||||
order = models.IntegerField(_("order"), default=0)
|
order = models.IntegerField(_("order"), default=0)
|
||||||
|
|
||||||
|
sources = MultiSelectField(
|
||||||
|
max_length=3,
|
||||||
|
choices=DOCUMENT_SOURCE,
|
||||||
|
default=f"{DocumentSource.ConsumeFolder},{DocumentSource.ApiUpload},{DocumentSource.MailFetch}",
|
||||||
|
)
|
||||||
|
|
||||||
filter_path = models.CharField(
|
filter_path = models.CharField(
|
||||||
_("filter path"),
|
_("filter path"),
|
||||||
max_length=256,
|
max_length=256,
|
||||||
|
@ -13,13 +13,16 @@ from django.utils.text import slugify
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from guardian.core import ObjectPermissionChecker
|
from guardian.core import ObjectPermissionChecker
|
||||||
from guardian.shortcuts import get_users_with_perms
|
from guardian.shortcuts import get_users_with_perms
|
||||||
|
from rest_framework import fields
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
|
||||||
|
from documents.data_models import DocumentSource
|
||||||
from documents.permissions import get_groups_with_only_permission
|
from documents.permissions import get_groups_with_only_permission
|
||||||
from documents.permissions import set_permissions_for_object
|
from documents.permissions import set_permissions_for_object
|
||||||
|
|
||||||
from . import bulk_edit
|
from . import bulk_edit
|
||||||
|
from .models import DOCUMENT_SOURCE
|
||||||
from .models import ConsumptionTemplate
|
from .models import ConsumptionTemplate
|
||||||
from .models import Correspondent
|
from .models import Correspondent
|
||||||
from .models import Document
|
from .models import Document
|
||||||
@ -1039,17 +1042,28 @@ class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissions
|
|||||||
|
|
||||||
|
|
||||||
class ConsumptionTemplateSerializer(OwnedObjectSerializer):
|
class ConsumptionTemplateSerializer(OwnedObjectSerializer):
|
||||||
|
order = serializers.IntegerField(required=False)
|
||||||
|
sources = fields.MultipleChoiceField(
|
||||||
|
choices=DOCUMENT_SOURCE,
|
||||||
|
allow_empty=False,
|
||||||
|
default={
|
||||||
|
DocumentSource.ConsumeFolder,
|
||||||
|
DocumentSource.ApiUpload,
|
||||||
|
DocumentSource.MailFetch,
|
||||||
|
},
|
||||||
|
)
|
||||||
assign_correspondent = CorrespondentField(allow_null=True, required=False)
|
assign_correspondent = CorrespondentField(allow_null=True, required=False)
|
||||||
assign_tags = TagsField(many=True, allow_null=True, required=False)
|
assign_tags = TagsField(many=True, allow_null=True, required=False)
|
||||||
assign_document_type = DocumentTypeField(allow_null=True, required=False)
|
assign_document_type = DocumentTypeField(allow_null=True, required=False)
|
||||||
assign_storage_path = StoragePathField(allow_null=True, required=False)
|
assign_storage_path = StoragePathField(allow_null=True, required=False)
|
||||||
order = serializers.IntegerField(required=False)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsumptionTemplate
|
model = ConsumptionTemplate
|
||||||
fields = [
|
fields = [
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
|
"order",
|
||||||
|
"sources",
|
||||||
"filter_path",
|
"filter_path",
|
||||||
"filter_filename",
|
"filter_filename",
|
||||||
"assign_tags",
|
"assign_tags",
|
||||||
@ -1061,7 +1075,6 @@ class ConsumptionTemplateSerializer(OwnedObjectSerializer):
|
|||||||
"assign_view_groups",
|
"assign_view_groups",
|
||||||
"assign_change_users",
|
"assign_change_users",
|
||||||
"assign_change_groups",
|
"assign_change_groups",
|
||||||
"order",
|
|
||||||
"owner",
|
"owner",
|
||||||
"user_can_change",
|
"user_can_change",
|
||||||
"permissions",
|
"permissions",
|
||||||
@ -1081,6 +1094,7 @@ class ConsumptionTemplateSerializer(OwnedObjectSerializer):
|
|||||||
# return mail_rule
|
# return mail_rule
|
||||||
|
|
||||||
# def validate(self, attrs):
|
# def validate(self, attrs):
|
||||||
|
# TODO: require path or filename filter
|
||||||
# if (
|
# if (
|
||||||
# attrs["action"] == ConsumptionTemplate.MailAction.TAG
|
# attrs["action"] == ConsumptionTemplate.MailAction.TAG
|
||||||
# or attrs["action"] == ConsumptionTemplate.MailAction.MOVE
|
# or attrs["action"] == ConsumptionTemplate.MailAction.MOVE
|
||||||
|
@ -155,7 +155,7 @@ def consume_file(
|
|||||||
logger.info(f"Found ASN in barcode: {overrides.asn}")
|
logger.info(f"Found ASN in barcode: {overrides.asn}")
|
||||||
|
|
||||||
template_overrides = Consumer().get_template_overrides(
|
template_overrides = Consumer().get_template_overrides(
|
||||||
input_doc=input_doc.original_file,
|
input_doc=input_doc,
|
||||||
)
|
)
|
||||||
|
|
||||||
overrides = merge_overrides(overridesA=overrides, overridesB=template_overrides)
|
overrides = merge_overrides(overridesA=overrides, overridesB=template_overrides)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user