diff --git a/src-ui/src/app/components/common/input/number/number.component.html b/src-ui/src/app/components/common/input/number/number.component.html index c3d435ffa..9e277277a 100644 --- a/src-ui/src/app/components/common/input/number/number.component.html +++ b/src-ui/src/app/components/common/input/number/number.component.html @@ -12,6 +12,14 @@
+ @if (prefix) { + + } @if (showAdd) { diff --git a/src-ui/src/app/components/common/input/number/number.component.ts b/src-ui/src/app/components/common/input/number/number.component.ts index 127574334..46ccbf089 100644 --- a/src-ui/src/app/components/common/input/number/number.component.ts +++ b/src-ui/src/app/components/common/input/number/number.component.ts @@ -2,11 +2,13 @@ import { Component, forwardRef, Input } from '@angular/core' import { FormsModule, NG_VALUE_ACCESSOR, - ReactiveFormsModule, + ReactiveFormsModule } from '@angular/forms' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { DocumentService } from 'src/app/services/rest/document.service' import { AbstractInputComponent } from '../abstract-input' +import { NgSelectModule } from '@ng-select/ng-select' +import { AsnPrefix } from "../../../../data/asn-prefix"; @Component({ providers: [ @@ -19,12 +21,18 @@ import { AbstractInputComponent } from '../abstract-input' selector: 'pngx-input-number', templateUrl: './number.component.html', styleUrls: ['./number.component.scss'], - imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule], + imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule, NgSelectModule], }) export class NumberComponent extends AbstractInputComponent { @Input() showAdd: boolean = true + @Input() + prefix: AsnPrefix[] + + @Input() + prefixSelect: number + @Input() step: number = 1 diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 9686ab4d0..b9ddd6dc6 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -106,7 +106,7 @@
- + (this.correspondents = result.results)) } + if ( + this.permissionsService.currentUserCan( + PermissionAction.View, + PermissionType.AsnPrefix + ) + ) { + this.asnPrefixService + .listAll() + .pipe(first(), takeUntil(this.unsubscribeNotifier)) + .subscribe((result) => (this.asnPrefix = result.results)) + } if ( this.permissionsService.currentUserCan( PermissionAction.View, @@ -403,9 +418,8 @@ export class DocumentDetailComponent this.previewText = res.toString() }, error: (err) => { - this.previewText = $localize`An error occurred loading content: ${ - err.message ?? err.toString() - }` + this.previewText = $localize`An error occurred loading content: ${err.message ?? err.toString() + }` }, }) this.thumbUrl = this.documentsService.getThumbUrl(documentId) @@ -449,7 +463,7 @@ export class DocumentDetailComponent this.documentForm.get('permissions_form').value['owner'] openDocument['permissions'] = this.documentForm.get('permissions_form').value[ - 'set_permissions' + 'set_permissions' ] delete openDocument['permissions_form'] } @@ -494,6 +508,7 @@ export class DocumentDetailComponent correspondent: doc.correspondent, document_type: doc.document_type, storage_path: doc.storage_path, + archive_serial_number_prefix: doc.archive_serial_number_prefix, archive_serial_number: doc.archive_serial_number, tags: [...doc.tags], permissions_form: { @@ -793,7 +808,7 @@ export class DocumentDetailComponent save(close: boolean = false) { this.networkActive = true - ;(document.activeElement as HTMLElement)?.dispatchEvent(new Event('change')) + ; (document.activeElement as HTMLElement)?.dispatchEvent(new Event('change')) this.documentsService .update(this.document) .pipe(first()) @@ -1046,7 +1061,7 @@ export class DocumentDetailComponent this.previewZoomScale = ZoomSetting.PageWidth this.previewZoomSetting = Object.values(ZoomSetting)[ - Math.min(Object.values(ZoomSetting).length - 1, currentIndex + 1) + Math.min(Object.values(ZoomSetting).length - 1, currentIndex + 1) ] } diff --git a/src-ui/src/app/data/asn-prefix.ts b/src-ui/src/app/data/asn-prefix.ts new file mode 100644 index 000000000..30b9eb40b --- /dev/null +++ b/src-ui/src/app/data/asn-prefix.ts @@ -0,0 +1,9 @@ +import {ObjectWithId} from "./object-with-id"; + +export interface AsnPrefix extends ObjectWithId { + name?: string + + slug?: string + + document_count?: number +} diff --git a/src-ui/src/app/data/document.ts b/src-ui/src/app/data/document.ts index 168fcff92..a637e0e33 100644 --- a/src-ui/src/app/data/document.ts +++ b/src-ui/src/app/data/document.ts @@ -1,4 +1,5 @@ import { Observable } from 'rxjs' +import { AsnPrefix } from './asn-prefix' import { Correspondent } from './correspondent' import { CustomFieldInstance } from './custom-field-instance' import { DocumentNote } from './document-note' @@ -118,6 +119,10 @@ export interface SearchHit { } export interface Document extends ObjectWithPermissions { + archive_serial_number_prefix$?: Observable + + archive_serial_number_prefix?: number + correspondent$?: Observable correspondent?: number diff --git a/src-ui/src/app/services/permissions.service.ts b/src-ui/src/app/services/permissions.service.ts index 3d88b10cc..dddb6719e 100644 --- a/src-ui/src/app/services/permissions.service.ts +++ b/src-ui/src/app/services/permissions.service.ts @@ -12,6 +12,7 @@ export enum PermissionAction { export enum PermissionType { Document = '%s_document', Tag = '%s_tag', + AsnPrefix = '%s_asnprefix', Correspondent = '%s_correspondent', DocumentType = '%s_documenttype', StoragePath = '%s_storagepath', diff --git a/src-ui/src/app/services/rest/asn-prefix.service.ts b/src-ui/src/app/services/rest/asn-prefix.service.ts new file mode 100644 index 000000000..573d2b6d0 --- /dev/null +++ b/src-ui/src/app/services/rest/asn-prefix.service.ts @@ -0,0 +1,13 @@ +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { AsnPrefix } from 'src/app/data/asn-prefix' +import { AbstractNameFilterService } from './abstract-name-filter-service' + +@Injectable({ + providedIn: 'root', +}) +export class AsnPrefixService extends AbstractNameFilterService { + constructor(http: HttpClient) { + super(http, 'asn_prefix') + } +} diff --git a/src/documents/admin.py b/src/documents/admin.py index 59cbf1853..f1107391d 100644 --- a/src/documents/admin.py +++ b/src/documents/admin.py @@ -2,6 +2,7 @@ from django.conf import settings from django.contrib import admin from guardian.admin import GuardedModelAdmin +from documents.models import ArchiveSerialNumberPrefix from documents.models import Correspondent from documents.models import CustomField from documents.models import CustomFieldInstance @@ -38,6 +39,10 @@ class DocumentTypeAdmin(GuardedModelAdmin): list_filter = ("matching_algorithm",) list_editable = ("match", "matching_algorithm") +class ArchiveSerialNumberPrefixAdmin(GuardedModelAdmin): + list_display = ("name", "owner") + list_filter = ("name",) + #list_editable = ("name",) class DocumentAdmin(GuardedModelAdmin): search_fields = ("correspondent__name", "title", "content", "tags__name") @@ -60,6 +65,7 @@ class DocumentAdmin(GuardedModelAdmin): list_filter = ( ("mime_type"), + ("archive_serial_number_prefix", admin.EmptyFieldListFilter), ("archive_serial_number", admin.EmptyFieldListFilter), ("archive_filename", admin.EmptyFieldListFilter), ) @@ -195,6 +201,7 @@ class CustomFieldInstancesAdmin(GuardedModelAdmin): admin.site.register(Correspondent, CorrespondentAdmin) admin.site.register(Tag, TagAdmin) admin.site.register(DocumentType, DocumentTypeAdmin) +admin.site.register(ArchiveSerialNumberPrefix, ArchiveSerialNumberPrefixAdmin) admin.site.register(Document, DocumentAdmin) admin.site.register(SavedView, SavedViewAdmin) admin.site.register(StoragePath, StoragePathAdmin) diff --git a/src/documents/filters.py b/src/documents/filters.py index 185ba7b6f..1aff98a15 100644 --- a/src/documents/filters.py +++ b/src/documents/filters.py @@ -28,6 +28,7 @@ from rest_framework import serializers from rest_framework.filters import OrderingFilter from rest_framework_guardian.filters import ObjectPermissionsFilter +from documents.models import ArchiveSerialNumberPrefix from documents.models import Correspondent from documents.models import CustomField from documents.models import CustomFieldInstance @@ -65,6 +66,14 @@ class TagFilterSet(FilterSet): } +class ArchiveSerialNumberPrefixFilterSet(FilterSet): + class Meta: + model = ArchiveSerialNumberPrefix + fields = { + "id": ID_KWARGS, + "name": CHAR_KWARGS, + } + class DocumentTypeFilterSet(FilterSet): class Meta: model = DocumentType diff --git a/src/documents/index.py b/src/documents/index.py index eacd1f99b..511c0bcb1 100644 --- a/src/documents/index.py +++ b/src/documents/index.py @@ -245,6 +245,7 @@ class DelayedQuery: "title": "title", "correspondent__name": "correspondent", "document_type__name": "type", + "archive_serial_number_prefix": "asn_prefix", "archive_serial_number": "asn", "num_notes": "num_notes", "owner": "owner", diff --git a/src/documents/migrations/1061_archiveserialnumberprefix_and_more.py b/src/documents/migrations/1061_archiveserialnumberprefix_and_more.py new file mode 100644 index 000000000..47748d0d8 --- /dev/null +++ b/src/documents/migrations/1061_archiveserialnumberprefix_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 5.1.4 on 2025-01-05 16:53 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '1060_alter_customfieldinstance_value_select'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ArchiveSerialNumberPrefix', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=6, verbose_name='name')), + ('owner', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='owner')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='document', + name='archive_serial_number_prefix', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documents', to='documents.archiveserialnumberprefix', verbose_name='archive serial number prefix'), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 88265a7da..9b348c9e4 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -95,6 +95,13 @@ class MatchingModel(ModelWithOwner): return self.name +class ArchiveSerialNumberPrefix(ModelWithOwner): + name = models.CharField(_("name"), max_length=6) + + def __str__(self): + return self.name + + class Correspondent(MatchingModel): class Meta(MatchingModel.Meta): verbose_name = _("correspondent") @@ -272,6 +279,15 @@ class Document(SoftDeleteModel, ModelWithOwner): help_text=_("The original name of the file when it was uploaded"), ) + archive_serial_number_prefix = models.ForeignKey( + ArchiveSerialNumberPrefix, + blank=True, + null=True, + related_name="documents", + on_delete=models.SET_NULL, + verbose_name=_("archive serial number prefix"), + ) + ARCHIVE_SERIAL_NUMBER_MIN: Final[int] = 0 ARCHIVE_SERIAL_NUMBER_MAX: Final[int] = 0xFF_FF_FF_FF diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index e051e00d6..82ba51fe3 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -34,6 +34,7 @@ if settings.AUDIT_LOG_ENABLED: from documents import bulk_edit from documents.data_models import DocumentSource +from documents.models import ArchiveSerialNumberPrefix from documents.models import Correspondent from documents.models import CustomField from documents.models import CustomFieldInstance @@ -342,6 +343,22 @@ class OwnedObjectListSerializer(serializers.ListSerializer): return super().to_representation(documents) + +class ArchiveSerialNumberPrefixSerializer(MatchingModelSerializer, OwnedObjectSerializer): + class Meta: + model = ArchiveSerialNumberPrefix + fields = ( + "id", + "slug", + "name", + "document_count", + "owner", + "permissions", + "user_can_change", + "set_permissions", + ) + + class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer): last_correspondence = serializers.DateTimeField(read_only=True, required=False) @@ -362,7 +379,6 @@ class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer): "set_permissions", ) - class DocumentTypeSerializer(MatchingModelSerializer, OwnedObjectSerializer): class Meta: model = DocumentType @@ -474,6 +490,9 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer): raise serializers.ValidationError(_("Invalid color.")) return color +class ArchiveSerialNumberPrefixField(serializers.PrimaryKeyRelatedField): + def get_queryset(self): + return ArchiveSerialNumberPrefix.objects.all() class CorrespondentField(serializers.PrimaryKeyRelatedField): def get_queryset(self): @@ -778,6 +797,7 @@ class DocumentSerializer( NestedUpdateMixin, DynamicFieldsModelSerializer, ): + archive_serial_number_prefix = ArchiveSerialNumberPrefixField(allow_null=True) correspondent = CorrespondentField(allow_null=True) tags = TagsField(many=True) document_type = DocumentTypeField(allow_null=True) @@ -832,6 +852,7 @@ class DocumentSerializer( and len(str(attrs["archive_serial_number"])) > 0 and Document.deleted_objects.filter( archive_serial_number=attrs["archive_serial_number"], + #archive_serial_number_prefix__prefix=attrs["archive_serial_number_prefix"], # TODO: asn type empty is allowed ).exists() ): raise serializers.ValidationError( @@ -934,6 +955,7 @@ class DocumentSerializer( "modified", "added", "deleted_at", + "archive_serial_number_prefix", "archive_serial_number", "original_file_name", "archived_file_name", diff --git a/src/documents/views.py b/src/documents/views.py index 4e2e4a8bf..cd0fd0757 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -93,6 +93,7 @@ from documents.conditionals import thumbnail_last_modified from documents.data_models import ConsumableDocument from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentSource +from documents.filters import ArchiveSerialNumberPrefixFilterSet from documents.filters import CorrespondentFilterSet from documents.filters import CustomFieldFilterSet from documents.filters import DocumentFilterSet @@ -107,6 +108,7 @@ from documents.matching import match_correspondents from documents.matching import match_document_types from documents.matching import match_storage_paths from documents.matching import match_tags +from documents.models import ArchiveSerialNumberPrefix from documents.models import Correspondent from documents.models import CustomField from documents.models import Document @@ -130,6 +132,7 @@ from documents.permissions import get_objects_for_user_owner_aware from documents.permissions import has_perms_owner_aware from documents.permissions import set_permissions_for_object from documents.serialisers import AcknowledgeTasksViewSerializer +from documents.serialisers import ArchiveSerialNumberPrefixSerializer from documents.serialisers import BulkDownloadSerializer from documents.serialisers import BulkEditObjectsSerializer from documents.serialisers import BulkEditSerializer @@ -258,6 +261,22 @@ class PermissionsAwareDocumentCountMixin(PassUserMixin): ) +class ArchiveSerialNumberPrefixViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin): + model = ArchiveSerialNumberPrefix + + queryset = ArchiveSerialNumberPrefix.objects.select_related("owner").order_by(Lower("name")) + + serializer_class = ArchiveSerialNumberPrefixSerializer + pagination_class = StandardPagination + permission_classes = (IsAuthenticated, PaperlessObjectPermissions) + filter_backends = ( + DjangoFilterBackend, + OrderingFilter, + ObjectOwnedOrGrantedPermissionsFilter, + ) + filterset_class = ArchiveSerialNumberPrefixFilterSet + ordering_fields = ("name") + class CorrespondentViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin): model = Correspondent diff --git a/src/paperless/urls.py b/src/paperless/urls.py index c528c5e2a..d8786005b 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -17,6 +17,7 @@ from django.views.static import serve from rest_framework.authtoken import views from rest_framework.routers import DefaultRouter +from documents.views import ArchiveSerialNumberPrefixViewSet from documents.views import BulkDownloadView from documents.views import BulkEditObjectsView from documents.views import BulkEditView @@ -59,6 +60,7 @@ from paperless_mail.views import MailRuleViewSet from paperless_mail.views import OauthCallbackView api_router = DefaultRouter() +api_router.register(r"asn_prefix", ArchiveSerialNumberPrefixViewSet) api_router.register(r"correspondents", CorrespondentViewSet) api_router.register(r"document_types", DocumentTypeViewSet) api_router.register(r"documents", UnifiedSearchViewSet)