Merge remote-tracking branch 'origin/dev'

This commit is contained in:
Trenton Holmes 2023-12-15 17:05:16 -08:00
commit c4b7429e99
22 changed files with 589 additions and 629 deletions

View File

@ -733,7 +733,7 @@ they use underscores instead of dashes.
Paperless has been tested to work with the OCR options provided Paperless has been tested to work with the OCR options provided
above. There are many options that are incompatible with each other, above. There are many options that are incompatible with each other,
so specifying invalid options may prevent paperless from consuming so specifying invalid options may prevent paperless from consuming
any documents. any documents. Use with caution!
Specify arguments as a JSON dictionary. Keep note of lower case Specify arguments as a JSON dictionary. Keep note of lower case
booleans and double quoted parameter names and strings. Examples: booleans and double quoted parameter names and strings. Examples:

View File

@ -17,7 +17,7 @@
<div class="col-md-4"> <div class="col-md-4">
<h5 class="border-bottom pb-2" i18n>Filters</h5> <h5 class="border-bottom pb-2" i18n>Filters</h5>
<p class="small" i18n>Process documents that match <em>all</em> filters specified below.</p> <p class="small" i18n>Process documents that match <em>all</em> 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-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply 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 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 to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text> <pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text>
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select> <pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>

View File

@ -9,7 +9,7 @@
</button> </button>
</div> </div>
<div [class.col-md-9]="horizontal"> <div [class.col-md-9]="horizontal">
<div [class.input-group]="allowCreateNew || showFilter"> <div [class.input-group]="allowCreateNew || showFilter" [class.is-invalid]="error">
<ng-select name="inputId" [(ngModel)]="value" <ng-select name="inputId" [(ngModel)]="value"
[disabled]="disabled" [disabled]="disabled"
[style.color]="textColor" [style.color]="textColor"
@ -42,6 +42,9 @@
</svg> </svg>
</button> </button>
</div> </div>
<div class="invalid-feedback">
{{error}}
</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small> <small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
<small *ngIf="getSuggestions().length > 0"> <small *ngIf="getSuggestions().length > 0">
<span i18n>Suggestions:</span>&nbsp; <span i18n>Suggestions:</span>&nbsp;

View File

@ -17,3 +17,12 @@
font-style: italic; font-style: italic;
opacity: .75; opacity: .75;
} }
::ng-deep .is-invalid ng-select .ng-select-container input {
// replicate bootstrap
padding-right: calc(1.5em + 0.75rem) !important;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") !important;
background-repeat: no-repeat !important;
background-position: right calc(0.375em + 0.1875rem) center !important;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem) !important;
}

View File

@ -5,7 +5,7 @@ export const environment = {
apiBaseUrl: document.baseURI + 'api/', apiBaseUrl: document.baseURI + 'api/',
apiVersion: '3', apiVersion: '3',
appTitle: 'Paperless-ngx', appTitle: 'Paperless-ngx',
version: '2.1.2', version: '2.1.2-dev',
webSocketHost: window.location.host, webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/', webSocketBaseUrl: base_url.pathname + 'ws/',

View File

@ -10,13 +10,13 @@
</context-group> </context-group>
<target state="final">Fermer</target> <target state="final">Fermer</target>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.HH" datatype="html"> <trans-unit id="ngb.timepicker.HH" datatype="html" approved="yes">
<source>HH</source> <source>HH</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
<target state="translated">HH</target> <target state="final">HH</target>
</trans-unit> </trans-unit>
<trans-unit id="ngb.toast.close-aria" datatype="html" approved="yes"> <trans-unit id="ngb.toast.close-aria" datatype="html" approved="yes">
<source>Close</source> <source>Close</source>
@ -100,13 +100,13 @@
</context-group> </context-group>
<target state="final">Précédent</target> <target state="final">Précédent</target>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.MM" datatype="html"> <trans-unit id="ngb.timepicker.MM" datatype="html" approved="yes">
<source>MM</source> <source>MM</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/src/ngb-config.ts</context> <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
<target state="translated">MM</target> <target state="final">MM</target>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.next" datatype="html" approved="yes"> <trans-unit id="ngb.pagination.next" datatype="html" approved="yes">
<source>»</source> <source>»</source>

View File

@ -288,7 +288,7 @@
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">90</context> <context context-type="linenumber">90</context>
</context-group> </context-group>
<target state="needs-translation">Document <x id="PH" equiv-text="status.filename"/> was added to Paperless-ngx.</target> <target state="translated">Dokument <x id="PH" equiv-text="status.filename"/> je dodan u Paperless-ngx.</target>
</trans-unit> </trans-unit>
<trans-unit id="1931214133925051574" datatype="html"> <trans-unit id="1931214133925051574" datatype="html">
<source>Open document</source> <source>Open document</source>
@ -316,7 +316,7 @@
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">120</context> <context context-type="linenumber">120</context>
</context-group> </context-group>
<target state="needs-translation">Document <x id="PH" equiv-text="status.filename"/> is being processed by Paperless-ngx.</target> <target state="translated">Dokument <x id="PH" equiv-text="status.filename"/> je u fazi obrade.</target>
</trans-unit> </trans-unit>
<trans-unit id="2501522447884928778" datatype="html"> <trans-unit id="2501522447884928778" datatype="html">
<source>Prev</source> <source>Prev</source>
@ -476,7 +476,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">15</context> <context context-type="linenumber">15</context>
</context-group> </context-group>
<target state="needs-translation">Auto refresh</target> <target state="translated">Automatsko osvježavanje</target>
</trans-unit> </trans-unit>
<trans-unit id="3894950702316166331" datatype="html"> <trans-unit id="3894950702316166331" datatype="html">
<source>Loading...</source> <source>Loading...</source>
@ -596,7 +596,7 @@
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">15</context> <context context-type="linenumber">15</context>
</context-group> </context-group>
<target state="needs-translation">General</target> <target state="translated">Općenito</target>
</trans-unit> </trans-unit>
<trans-unit id="8671234314555525900" datatype="html"> <trans-unit id="8671234314555525900" datatype="html">
<source>Appearance</source> <source>Appearance</source>
@ -916,7 +916,7 @@
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">178,180</context> <context context-type="linenumber">178,180</context>
</context-group> </context-group>
<target state="needs-translation"> Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI </target> <target state="translated"> Postavke ovog korisničkog računa za objekte (Oznake, Pravila za e-poštu, itd.) stvorene putem web sučelja </target>
</trans-unit> </trans-unit>
<trans-unit id="4292903881380648974" datatype="html"> <trans-unit id="4292903881380648974" datatype="html">
<source>Default Owner</source> <source>Default Owner</source>
@ -2074,7 +2074,7 @@
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context> <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">124</context> <context context-type="linenumber">124</context>
</context-group> </context-group>
<target state="needs-translation">Deleted user</target> <target state="translated">Izbrisani korisnik</target>
</trans-unit> </trans-unit>
<trans-unit id="1942566571910298572" datatype="html"> <trans-unit id="1942566571910298572" datatype="html">
<source>Error deleting user.</source> <source>Error deleting user.</source>
@ -2479,7 +2479,7 @@
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
<context context-type="linenumber">276</context> <context context-type="linenumber">276</context>
</context-group> </context-group>
<target state="needs-translation">An error occurred while saving update checking settings.</target> <target state="translated">Došlo je do pogreške prilikom spremanja postavki ažuriranja.</target>
</trans-unit> </trans-unit>
<trans-unit id="8700121026680200191" datatype="html"> <trans-unit id="8700121026680200191" datatype="html">
<source>Clear</source> <source>Clear</source>
@ -3039,7 +3039,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">9</context> <context context-type="linenumber">9</context>
</context-group> </context-group>
<target state="needs-translation">Data type</target> <target state="translated">Tip podataka</target>
</trans-unit> </trans-unit>
<trans-unit id="5933665691581884232" datatype="html"> <trans-unit id="5933665691581884232" datatype="html">
<source>Data type cannot be changed after a field is created</source> <source>Data type cannot be changed after a field is created</source>
@ -3047,7 +3047,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">10</context> <context context-type="linenumber">10</context>
</context-group> </context-group>
<target state="needs-translation">Data type cannot be changed after a field is created</target> <target state="translated">Tip podataka ne može se promijeniti nakon što je polje stvoreno</target>
</trans-unit> </trans-unit>
<trans-unit id="528950215505228201" datatype="html"> <trans-unit id="528950215505228201" datatype="html">
<source>Create new custom field</source> <source>Create new custom field</source>
@ -3175,7 +3175,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.html</context>
<context context-type="linenumber">19</context> <context context-type="linenumber">19</context>
</context-group> </context-group>
<target state="needs-translation">Character Set</target> <target state="translated">Skup znakova</target>
</trans-unit> </trans-unit>
<trans-unit id="6563391987554512024" datatype="html"> <trans-unit id="6563391987554512024" datatype="html">
<source>Test</source> <source>Test</source>

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,8 @@ from pikepdf import Pdf
from PIL import Image from PIL import Image
from documents.converters import convert_from_tiff_to_pdf from documents.converters import convert_from_tiff_to_pdf
from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides
from documents.data_models import DocumentSource from documents.data_models import DocumentSource
from documents.utils import copy_basic_file_stats from documents.utils import copy_basic_file_stats
from documents.utils import copy_file_with_basic_stats from documents.utils import copy_file_with_basic_stats
@ -53,6 +55,7 @@ class BarcodeReader:
self.mime: Final[str] = mime_type self.mime: Final[str] = mime_type
self.pdf_file: Path = self.file self.pdf_file: Path = self.file
self.barcodes: list[Barcode] = [] self.barcodes: list[Barcode] = []
self._tiff_conversion_done = False
self.temp_dir: Optional[tempfile.TemporaryDirectory] = None self.temp_dir: Optional[tempfile.TemporaryDirectory] = None
if settings.CONSUMER_BARCODE_TIFF_SUPPORT: if settings.CONSUMER_BARCODE_TIFF_SUPPORT:
@ -150,12 +153,14 @@ class BarcodeReader:
def convert_from_tiff_to_pdf(self): def convert_from_tiff_to_pdf(self):
""" """
May convert a TIFF image into a PDF, if the input is a TIFF May convert a TIFF image into a PDF, if the input is a TIFF and
the TIFF has not been made into a PDF
""" """
# Nothing to do, pdf_file is already assigned correctly # Nothing to do, pdf_file is already assigned correctly
if self.mime != "image/tiff": if self.mime != "image/tiff" or self._tiff_conversion_done:
return return
self._tiff_conversion_done = True
self.pdf_file = convert_from_tiff_to_pdf(self.file, Path(self.temp_dir.name)) self.pdf_file = convert_from_tiff_to_pdf(self.file, Path(self.temp_dir.name))
def detect(self) -> None: def detect(self) -> None:
@ -167,6 +172,9 @@ class BarcodeReader:
if self.barcodes: if self.barcodes:
return return
# No op if not a TIFF
self.convert_from_tiff_to_pdf()
# Choose the library for reading # Choose the library for reading
if settings.CONSUMER_BARCODE_SCANNER == "PYZBAR": if settings.CONSUMER_BARCODE_SCANNER == "PYZBAR":
reader = self.read_barcodes_pyzbar reader = self.read_barcodes_pyzbar
@ -240,7 +248,7 @@ class BarcodeReader:
""" """
document_paths = [] document_paths = []
fname = self.file.with_suffix("").name fname = self.file.stem
with Pdf.open(self.pdf_file) as input_pdf: with Pdf.open(self.pdf_file) as input_pdf:
# Start with an empty document # Start with an empty document
current_document: list[Page] = [] current_document: list[Page] = []
@ -290,7 +298,7 @@ class BarcodeReader:
def separate( def separate(
self, self,
source: DocumentSource, source: DocumentSource,
override_name: Optional[str] = None, overrides: DocumentMetadataOverrides,
) -> bool: ) -> bool:
""" """
Separates the document, based on barcodes and configuration, creating new Separates the document, based on barcodes and configuration, creating new
@ -316,27 +324,23 @@ class BarcodeReader:
logger.warning("No pages to split on!") logger.warning("No pages to split on!")
return False return False
# Create the split documents tmp_dir = Path(tempfile.mkdtemp(prefix="paperless-barcode-split-")).resolve()
doc_paths = self.separate_pages(separator_pages)
# Save the new documents to correct folder from documents import tasks
if source != DocumentSource.ConsumeFolder:
# The given file is somewhere in SCRATCH_DIR,
# and new documents must be moved to the CONSUMPTION_DIR
# for the consumer to notice them
save_to_dir = settings.CONSUMPTION_DIR
else:
# The given file is somewhere in CONSUMPTION_DIR,
# and may be some levels down for recursive tagging
# so use the file's parent to preserve any metadata
save_to_dir = self.file.parent
for idx, document_path in enumerate(doc_paths): # Create the split document tasks
if override_name is not None: for new_document in self.separate_pages(separator_pages):
newname = f"{idx}_{override_name}" copy_file_with_basic_stats(new_document, tmp_dir / new_document.name)
dest = save_to_dir / newname
else: tasks.consume_file.delay(
dest = save_to_dir ConsumableDocument(
logger.info(f"Saving {document_path} to {dest}") # Same source, for templates
copy_file_with_basic_stats(document_path, dest) source=source,
# Can't use same folder or the consume might grab it again
original_file=(tmp_dir / new_document.name).resolve(),
),
# All the same metadata
overrides,
)
logger.info("Barcode splitting complete!")
return True return True

View File

@ -3,6 +3,7 @@ import math
import os import os
from collections import Counter from collections import Counter
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime
from typing import Optional from typing import Optional
from dateutil.parser import isoparse from dateutil.parser import isoparse
@ -371,7 +372,7 @@ class LocalDateParser(English):
if isinstance(d, timespan): if isinstance(d, timespan):
d.start = self.reverse_timezone_offset(d.start) d.start = self.reverse_timezone_offset(d.start)
d.end = self.reverse_timezone_offset(d.end) d.end = self.reverse_timezone_offset(d.end)
else: elif isinstance(d, datetime):
d = self.reverse_timezone_offset(d) d = self.reverse_timezone_offset(d)
return d return d

View File

@ -238,18 +238,6 @@ class Command(BaseCommand):
serializers.serialize("json", StoragePath.objects.all()), serializers.serialize("json", StoragePath.objects.all()),
) )
notes = json.loads(
serializers.serialize("json", Note.objects.all()),
)
if not self.split_manifest:
manifest += notes
documents = Document.objects.order_by("id")
document_map = {d.pk: d for d in documents}
document_manifest = json.loads(serializers.serialize("json", documents))
if not self.split_manifest:
manifest += document_manifest
manifest += json.loads( manifest += json.loads(
serializers.serialize("json", MailAccount.objects.all()), serializers.serialize("json", MailAccount.objects.all()),
) )
@ -303,10 +291,24 @@ class Command(BaseCommand):
serializers.serialize("json", CustomField.objects.all()), serializers.serialize("json", CustomField.objects.all()),
) )
# These are treated specially and included in the per-document manifest
# if that setting is enabled. Otherwise, they are just exported to the bulk
# manifest
documents = Document.objects.order_by("id")
document_map: dict[int, Document] = {d.pk: d for d in documents}
document_manifest = json.loads(serializers.serialize("json", documents))
notes = json.loads(
serializers.serialize("json", Note.objects.all()),
)
custom_field_instances = json.loads(
serializers.serialize("json", CustomFieldInstance.objects.all()),
)
if not self.split_manifest: if not self.split_manifest:
manifest += json.loads( manifest += document_manifest
serializers.serialize("json", CustomFieldInstance.objects.all()), manifest += notes
) manifest += custom_field_instances
# 3. Export files from each document # 3. Export files from each document
for index, document_dict in tqdm.tqdm( for index, document_dict in tqdm.tqdm(
@ -412,6 +414,12 @@ class Command(BaseCommand):
notes, notes,
), ),
) )
content += list(
filter(
lambda d: d["fields"]["document"] == document_dict["pk"],
custom_field_instances,
),
)
manifest_name.write_text( manifest_name.write_text(
json.dumps(content, indent=2, ensure_ascii=False), json.dumps(content, indent=2, ensure_ascii=False),
encoding="utf-8", encoding="utf-8",

View File

@ -140,7 +140,7 @@ def consume_file(
with BarcodeReader(input_doc.original_file, input_doc.mime_type) as reader: with BarcodeReader(input_doc.original_file, input_doc.mime_type) as reader:
if settings.CONSUMER_ENABLE_BARCODES and reader.separate( if settings.CONSUMER_ENABLE_BARCODES and reader.separate(
input_doc.source, input_doc.source,
overrides.filename, overrides,
): ):
# notify the sender, otherwise the progress bar # notify the sender, otherwise the progress bar
# in the UI stays stuck # in the UI stays stuck

View File

@ -455,6 +455,31 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
# Assert subset in results # Assert subset in results
self.assertDictEqual(result, {**result, **subset}) self.assertDictEqual(result, {**result, **subset})
def test_search_added_invalid_date(self):
"""
GIVEN:
- One document added right now
WHEN:
- Query with invalid added date
THEN:
- No documents returned
"""
d1 = Document.objects.create(
title="invoice",
content="the thing i bought at a shop and paid with bank account",
checksum="A",
pk=1,
)
with index.open_index_writer() as writer:
index.update_document(writer, d1)
response = self.client.get("/api/documents/?query=added:invalid-date")
results = response.data["results"]
# Expect 0 document returned
self.assertEqual(len(results), 0)
@mock.patch("documents.index.autocomplete") @mock.patch("documents.index.autocomplete")
def test_search_autocomplete_limits(self, m): def test_search_autocomplete_limits(self, m):
""" """

View File

@ -1,5 +1,4 @@
import shutil import shutil
from pathlib import Path
from unittest import mock from unittest import mock
import pytest import pytest
@ -11,10 +10,13 @@ from documents import tasks
from documents.barcodes import BarcodeReader from documents.barcodes import BarcodeReader
from documents.consumer import ConsumerError from documents.consumer import ConsumerError
from documents.data_models import ConsumableDocument from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides
from documents.data_models import DocumentSource from documents.data_models import DocumentSource
from documents.models import Document from documents.models import Document
from documents.tests.utils import DirectoriesMixin from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import DocumentConsumeDelayMixin
from documents.tests.utils import FileSystemAssertsMixin from documents.tests.utils import FileSystemAssertsMixin
from documents.tests.utils import SampleDirMixin
try: try:
import zxingcpp # noqa: F401 import zxingcpp # noqa: F401
@ -25,11 +27,7 @@ except ImportError:
@override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR") @override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase): class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, TestCase):
SAMPLE_DIR = Path(__file__).parent / "samples"
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"
def test_scan_file_for_separating_barcodes(self): def test_scan_file_for_separating_barcodes(self):
""" """
GIVEN: GIVEN:
@ -48,6 +46,46 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertEqual(reader.pdf_file, test_file) self.assertEqual(reader.pdf_file, test_file)
self.assertDictEqual(separator_page_numbers, {0: False}) self.assertDictEqual(separator_page_numbers, {0: False})
@override_settings(
CONSUMER_BARCODE_TIFF_SUPPORT=True,
)
def test_scan_tiff_for_separating_barcodes(self):
"""
GIVEN:
- TIFF image containing barcodes
WHEN:
- Consume task returns
THEN:
- The file was split
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.tiff"
with BarcodeReader(test_file, "image/tiff") as reader:
reader.detect()
separator_page_numbers = reader.get_separation_pages()
self.assertDictEqual(separator_page_numbers, {1: False})
@override_settings(
CONSUMER_BARCODE_TIFF_SUPPORT=True,
)
def test_scan_tiff_with_alpha_for_separating_barcodes(self):
"""
GIVEN:
- TIFF image containing barcodes
WHEN:
- Consume task returns
THEN:
- The file was split
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle-alpha.tiff"
with BarcodeReader(test_file, "image/tiff") as reader:
reader.detect()
separator_page_numbers = reader.get_separation_pages()
self.assertDictEqual(separator_page_numbers, {1: False})
def test_scan_file_for_separating_barcodes_none_present(self): def test_scan_file_for_separating_barcodes_none_present(self):
""" """
GIVEN: GIVEN:
@ -285,6 +323,28 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertGreater(len(reader.barcodes), 0) self.assertGreater(len(reader.barcodes), 0)
self.assertDictEqual(separator_page_numbers, {1: False}) self.assertDictEqual(separator_page_numbers, {1: False})
def test_scan_file_for_separating_barcodes_password(self):
"""
GIVEN:
- Password protected PDF
WHEN:
- File is scanned for barcode
THEN:
- Scanning handles the exception without crashing
"""
test_file = self.SAMPLE_DIR / "password-is-test.pdf"
with self.assertLogs("paperless.barcodes", level="WARNING") as cm:
with BarcodeReader(test_file, "application/pdf") as reader:
reader.detect()
warning = cm.output[0]
expected_str = "WARNING:paperless.barcodes:File is likely password protected, not checking for barcodes"
self.assertTrue(warning.startswith(expected_str))
separator_page_numbers = reader.get_separation_pages()
self.assertEqual(reader.pdf_file, test_file)
self.assertDictEqual(separator_page_numbers, {})
def test_separate_pages(self): def test_separate_pages(self):
""" """
GIVEN: GIVEN:
@ -332,8 +392,12 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
with self.assertLogs("paperless.barcodes", level="WARNING") as cm: with self.assertLogs("paperless.barcodes", level="WARNING") as cm:
with BarcodeReader(test_file, "application/pdf") as reader: with BarcodeReader(test_file, "application/pdf") as reader:
success = reader.separate(DocumentSource.ApiUpload) self.assertFalse(
self.assertFalse(success) reader.separate(
DocumentSource.ApiUpload,
DocumentMetadataOverrides(),
),
)
self.assertEqual( self.assertEqual(
cm.output, cm.output,
[ [
@ -341,215 +405,6 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
], ],
) )
def test_save_to_dir_given_name(self):
"""
GIVEN:
- File to save to a directory
- There is a name override
WHEN:
- The file is saved
THEN:
- The file exists
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
with BarcodeReader(test_file, "application/pdf") as reader:
reader.separate(DocumentSource.ApiUpload, "newname.pdf")
self.assertEqual(reader.pdf_file, test_file)
target_file1 = settings.CONSUMPTION_DIR / "0_newname.pdf"
target_file2 = settings.CONSUMPTION_DIR / "1_newname.pdf"
self.assertIsFile(target_file1)
self.assertIsFile(target_file2)
def test_barcode_splitter_api_upload(self):
"""
GIVEN:
- Input file containing barcodes
WHEN:
- Input file is split on barcodes
THEN:
- Correct number of files produced
"""
sample_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
test_file = settings.SCRATCH_DIR / "patch-code-t-middle.pdf"
shutil.copy(sample_file, test_file)
with BarcodeReader(test_file, "application/pdf") as reader:
reader.separate(DocumentSource.ApiUpload)
self.assertEqual(reader.pdf_file, test_file)
target_file1 = (
settings.CONSUMPTION_DIR / "patch-code-t-middle_document_0.pdf"
)
target_file2 = (
settings.CONSUMPTION_DIR / "patch-code-t-middle_document_1.pdf"
)
self.assertIsFile(target_file1)
self.assertIsFile(target_file2)
def test_barcode_splitter_consume_dir(self):
"""
GIVEN:
- Input file containing barcodes
WHEN:
- Input file is split on barcodes
THEN:
- Correct number of files produced
"""
sample_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
test_file = settings.CONSUMPTION_DIR / "patch-code-t-middle.pdf"
shutil.copy(sample_file, test_file)
with BarcodeReader(test_file, "application/pdf") as reader:
reader.detect()
reader.separate(DocumentSource.ConsumeFolder)
self.assertEqual(reader.pdf_file, test_file)
target_file1 = (
settings.CONSUMPTION_DIR / "patch-code-t-middle_document_0.pdf"
)
target_file2 = (
settings.CONSUMPTION_DIR / "patch-code-t-middle_document_1.pdf"
)
self.assertIsFile(target_file1)
self.assertIsFile(target_file2)
def test_barcode_splitter_consume_dir_recursive(self):
"""
GIVEN:
- Input file containing barcodes
- Input file is within a directory structure of the consume folder
WHEN:
- Input file is split on barcodes
THEN:
- Correct number of files produced
- Output files are within the same directory structure
"""
sample_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
test_file = (
settings.CONSUMPTION_DIR / "tag1" / "tag2" / "patch-code-t-middle.pdf"
)
test_file.parent.mkdir(parents=True)
shutil.copy(sample_file, test_file)
with BarcodeReader(test_file, "application/pdf") as reader:
reader.separate(DocumentSource.ConsumeFolder)
self.assertEqual(reader.pdf_file, test_file)
target_file1 = (
settings.CONSUMPTION_DIR
/ "tag1"
/ "tag2"
/ "patch-code-t-middle_document_0.pdf"
)
target_file2 = (
settings.CONSUMPTION_DIR
/ "tag1"
/ "tag2"
/ "patch-code-t-middle_document_1.pdf"
)
self.assertIsFile(target_file1)
self.assertIsFile(target_file2)
@override_settings(CONSUMER_ENABLE_BARCODES=True)
def test_consume_barcode_file(self):
"""
GIVEN:
- Input file with barcodes given to consume task
WHEN:
- Consume task returns
THEN:
- The file was split
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
dst = settings.SCRATCH_DIR / "patch-code-t-middle.pdf"
shutil.copy(test_file, dst)
with mock.patch("documents.tasks.async_to_sync"):
self.assertEqual(
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=dst,
),
None,
),
"File successfully split",
)
@override_settings(
CONSUMER_ENABLE_BARCODES=True,
CONSUMER_BARCODE_TIFF_SUPPORT=True,
)
def test_consume_barcode_tiff_file(self):
"""
GIVEN:
- TIFF image containing barcodes
WHEN:
- Consume task returns
THEN:
- The file was split
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.tiff"
dst = settings.SCRATCH_DIR / "patch-code-t-middle.tiff"
shutil.copy(test_file, dst)
with mock.patch("documents.tasks.async_to_sync"):
self.assertEqual(
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=dst,
),
None,
),
"File successfully split",
)
self.assertIsNotFile(dst)
@override_settings(
CONSUMER_ENABLE_BARCODES=True,
CONSUMER_BARCODE_TIFF_SUPPORT=True,
)
def test_consume_barcode_tiff_file_with_alpha(self):
"""
GIVEN:
- TIFF image containing barcodes
- TIFF image has an alpha layer
WHEN:
- Consume task handles the alpha layer and returns
THEN:
- The file was split without issue
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle-alpha.tiff"
dst = settings.SCRATCH_DIR / "patch-code-t-middle.tiff"
shutil.copy(test_file, dst)
with mock.patch("documents.tasks.async_to_sync"):
self.assertEqual(
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=dst,
),
None,
),
"File successfully split",
)
self.assertIsNotFile(dst)
@override_settings( @override_settings(
CONSUMER_ENABLE_BARCODES=True, CONSUMER_ENABLE_BARCODES=True,
CONSUMER_BARCODE_TIFF_SUPPORT=True, CONSUMER_BARCODE_TIFF_SUPPORT=True,
@ -597,60 +452,6 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertIsNone(kwargs["override_document_type_id"]) self.assertIsNone(kwargs["override_document_type_id"])
self.assertIsNone(kwargs["override_tag_ids"]) self.assertIsNone(kwargs["override_tag_ids"])
@override_settings(
CONSUMER_ENABLE_BARCODES=True,
CONSUMER_BARCODE_TIFF_SUPPORT=True,
)
def test_consume_barcode_supported_no_extension_file(self):
"""
GIVEN:
- TIFF image containing barcodes
- TIFF file is given without extension
WHEN:
- Consume task returns
THEN:
- The file was split
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.tiff"
dst = settings.SCRATCH_DIR / "patch-code-t-middle"
shutil.copy(test_file, dst)
with mock.patch("documents.tasks.async_to_sync"):
self.assertEqual(
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=dst,
),
None,
),
"File successfully split",
)
self.assertIsNotFile(dst)
def test_scan_file_for_separating_barcodes_password(self):
"""
GIVEN:
- Password protected PDF
WHEN:
- File is scanned for barcode
THEN:
- Scanning handles the exception without crashing
"""
test_file = self.SAMPLE_DIR / "password-is-test.pdf"
with self.assertLogs("paperless.barcodes", level="WARNING") as cm:
with BarcodeReader(test_file, "application/pdf") as reader:
reader.detect()
warning = cm.output[0]
expected_str = "WARNING:paperless.barcodes:File is likely password protected, not checking for barcodes"
self.assertTrue(warning.startswith(expected_str))
separator_page_numbers = reader.get_separation_pages()
self.assertEqual(reader.pdf_file, test_file)
self.assertDictEqual(separator_page_numbers, {})
@override_settings( @override_settings(
CONSUMER_ENABLE_BARCODES=True, CONSUMER_ENABLE_BARCODES=True,
CONSUMER_ENABLE_ASN_BARCODE=True, CONSUMER_ENABLE_ASN_BARCODE=True,
@ -722,11 +523,64 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertEqual(len(document_list), 5) self.assertEqual(len(document_list), 5)
class TestAsnBarcode(DirectoriesMixin, TestCase): @override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
SAMPLE_DIR = Path(__file__).parent / "samples" class TestBarcodeNewConsume(
DirectoriesMixin,
FileSystemAssertsMixin,
SampleDirMixin,
DocumentConsumeDelayMixin,
TestCase,
):
@override_settings(CONSUMER_ENABLE_BARCODES=True)
def test_consume_barcode_file(self):
"""
GIVEN:
- Incoming file with at 1 barcode producing 2 documents
- Document includes metadata override information
WHEN:
- The document is split
THEN:
- Two new consume tasks are created
- Metadata overrides are preserved for the new consume
- The document source is unchanged (for consume templates)
"""
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
temp_copy = self.dirs.scratch_dir / test_file.name
shutil.copy(test_file, temp_copy)
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes" overrides = DocumentMetadataOverrides(tag_ids=[1, 2, 9])
with mock.patch("documents.tasks.async_to_sync") as progress_mocker:
self.assertEqual(
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=temp_copy,
),
overrides,
),
"File successfully split",
)
# We let the consumer know progress is done
progress_mocker.assert_called_once()
# 2 new document consume tasks created
self.assertEqual(self.consume_file_mock.call_count, 2)
self.assertIsNotFile(temp_copy)
# Check the split files exist
# Check the source is unchanged
# Check the overrides are unchanged
for (
new_input_doc,
new_doc_overrides,
) in self.get_all_consume_delay_call_args():
self.assertEqual(new_input_doc.source, DocumentSource.ConsumeFolder)
self.assertIsFile(new_input_doc.original_file)
self.assertEqual(overrides, new_doc_overrides)
class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, TestCase):
@override_settings(CONSUMER_ASN_BARCODE_PREFIX="CUSTOM-PREFIX-") @override_settings(CONSUMER_ASN_BARCODE_PREFIX="CUSTOM-PREFIX-")
def test_scan_file_for_asn_custom_prefix(self): def test_scan_file_for_asn_custom_prefix(self):
""" """

View File

@ -646,10 +646,13 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
with paperless_environment(): with paperless_environment():
self.assertEqual(Document.objects.count(), 4) self.assertEqual(Document.objects.count(), 4)
self.assertEqual(CustomFieldInstance.objects.count(), 1)
Document.objects.all().delete() Document.objects.all().delete()
CustomFieldInstance.objects.all().delete()
self.assertEqual(Document.objects.count(), 0) self.assertEqual(Document.objects.count(), 0)
call_command("document_importer", "--no-progress-bar", self.target) call_command("document_importer", "--no-progress-bar", self.target)
self.assertEqual(Document.objects.count(), 4) self.assertEqual(Document.objects.count(), 4)
self.assertEqual(CustomFieldInstance.objects.count(), 1)
def test_folder_prefix(self): def test_folder_prefix(self):
""" """

View File

@ -235,8 +235,10 @@ class DocumentConsumeDelayMixin:
""" """
Iterates over all calls to the async task and returns the arguments Iterates over all calls to the async task and returns the arguments
""" """
# Must be at least 1 call
self.consume_file_mock.assert_called()
for args, _ in self.consume_file_mock.call_args_list: for args, kwargs in self.consume_file_mock.call_args_list:
input_doc, overrides = args input_doc, overrides = args
yield (input_doc, overrides) yield (input_doc, overrides)
@ -244,7 +246,7 @@ class DocumentConsumeDelayMixin:
def get_specific_consume_delay_call_args( def get_specific_consume_delay_call_args(
self, self,
index: int, index: int,
) -> Iterator[tuple[ConsumableDocument, DocumentMetadataOverrides]]: ) -> tuple[ConsumableDocument, DocumentMetadataOverrides]:
""" """
Returns the arguments of a specific call to the async task Returns the arguments of a specific call to the async task
""" """
@ -299,3 +301,9 @@ class TestMigrations(TransactionTestCase):
def setUpBeforeMigration(self, apps): def setUpBeforeMigration(self, apps):
pass pass
class SampleDirMixin:
SAMPLE_DIR = Path(__file__).parent / "samples"
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"

View File

@ -182,10 +182,14 @@ class PassUserMixin(CreateModelMixin):
class CorrespondentViewSet(ModelViewSet, PassUserMixin): class CorrespondentViewSet(ModelViewSet, PassUserMixin):
model = Correspondent model = Correspondent
queryset = Correspondent.objects.annotate( queryset = (
document_count=Count("documents"), Correspondent.objects.annotate(
last_correspondence=Max("documents__created"), document_count=Count("documents"),
).order_by(Lower("name")) last_correspondence=Max("documents__created"),
)
.select_related("owner")
.order_by(Lower("name"))
)
serializer_class = CorrespondentSerializer serializer_class = CorrespondentSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
@ -208,8 +212,12 @@ class CorrespondentViewSet(ModelViewSet, PassUserMixin):
class TagViewSet(ModelViewSet, PassUserMixin): class TagViewSet(ModelViewSet, PassUserMixin):
model = Tag model = Tag
queryset = Tag.objects.annotate(document_count=Count("documents")).order_by( queryset = (
Lower("name"), Tag.objects.annotate(document_count=Count("documents"))
.select_related("owner")
.order_by(
Lower("name"),
)
) )
def get_serializer_class(self, *args, **kwargs): def get_serializer_class(self, *args, **kwargs):
@ -232,9 +240,13 @@ class TagViewSet(ModelViewSet, PassUserMixin):
class DocumentTypeViewSet(ModelViewSet, PassUserMixin): class DocumentTypeViewSet(ModelViewSet, PassUserMixin):
model = DocumentType model = DocumentType
queryset = DocumentType.objects.annotate( queryset = (
document_count=Count("documents"), DocumentType.objects.annotate(
).order_by(Lower("name")) document_count=Count("documents"),
)
.select_related("owner")
.order_by(Lower("name"))
)
serializer_class = DocumentTypeSerializer serializer_class = DocumentTypeSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
@ -283,7 +295,12 @@ class DocumentViewSet(
) )
def get_queryset(self): def get_queryset(self):
return Document.objects.distinct().annotate(num_notes=Count("notes")) return (
Document.objects.distinct()
.annotate(num_notes=Count("notes"))
.select_related("correspondent", "storage_path", "document_type", "owner")
.prefetch_related("tags", "custom_fields", "notes")
)
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
fields_param = self.request.query_params.get("fields", None) fields_param = self.request.query_params.get("fields", None)
@ -627,9 +644,18 @@ class DocumentViewSet(
class SearchResultSerializer(DocumentSerializer, PassUserMixin): class SearchResultSerializer(DocumentSerializer, PassUserMixin):
def to_representation(self, instance): def to_representation(self, instance):
doc = Document.objects.get(id=instance["id"]) doc = (
Document.objects.select_related(
"correspondent",
"storage_path",
"document_type",
"owner",
)
.prefetch_related("tags", "custom_fields", "notes")
.get(id=instance["id"])
)
notes = ",".join( notes = ",".join(
[str(c.note) for c in Note.objects.filter(document=instance["id"])], [str(c.note) for c in doc.notes.all()],
) )
r = super().to_representation(doc) r = super().to_representation(doc)
r["__search_hit__"] = { r["__search_hit__"] = {
@ -752,7 +778,11 @@ class SavedViewViewSet(ModelViewSet, PassUserMixin):
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user
return SavedView.objects.filter(owner=user) return (
SavedView.objects.filter(owner=user)
.select_related("owner")
.prefetch_related("filter_rules")
)
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save(owner=self.request.user) serializer.save(owner=self.request.user)
@ -1080,8 +1110,12 @@ class BulkDownloadView(GenericAPIView):
class StoragePathViewSet(ModelViewSet, PassUserMixin): class StoragePathViewSet(ModelViewSet, PassUserMixin):
model = StoragePath model = StoragePath
queryset = StoragePath.objects.annotate(document_count=Count("documents")).order_by( queryset = (
Lower("name"), StoragePath.objects.annotate(document_count=Count("documents"))
.select_related("owner")
.order_by(
Lower("name"),
)
) )
serializer_class = StoragePathSerializer serializer_class = StoragePathSerializer
@ -1347,7 +1381,18 @@ class ConsumptionTemplateViewSet(ModelViewSet):
model = ConsumptionTemplate model = ConsumptionTemplate
queryset = ConsumptionTemplate.objects.all().order_by("order") queryset = (
ConsumptionTemplate.objects.prefetch_related(
"assign_tags",
"assign_view_users",
"assign_view_groups",
"assign_change_users",
"assign_change_groups",
"assign_custom_fields",
)
.all()
.order_by("order")
)
class CustomFieldViewSet(ModelViewSet): class CustomFieldViewSet(ModelViewSet):

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-05 08:26-0800\n" "POT-Creation-Date: 2023-12-05 08:26-0800\n"
"PO-Revision-Date: 2023-12-12 00:24\n" "PO-Revision-Date: 2023-12-14 00:23\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: French\n" "Language-Team: French\n"
"Language: fr_FR\n" "Language: fr_FR\n"

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-05 08:26-0800\n" "POT-Creation-Date: 2023-12-05 08:26-0800\n"
"PO-Revision-Date: 2023-12-05 16:27\n" "PO-Revision-Date: 2023-12-13 12:09\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Croatian\n" "Language-Team: Croatian\n"
"Language: hr_HR\n" "Language: hr_HR\n"

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-05 08:26-0800\n" "POT-Creation-Date: 2023-12-05 08:26-0800\n"
"PO-Revision-Date: 2023-12-05 16:27\n" "PO-Revision-Date: 2023-12-16 00:23\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Romanian\n" "Language-Team: Romanian\n"
"Language: ro_RO\n" "Language: ro_RO\n"
@ -23,11 +23,11 @@ msgstr "Documente"
#: documents/models.py:36 documents/models.py:734 #: documents/models.py:36 documents/models.py:734
msgid "owner" msgid "owner"
msgstr "" msgstr "proprietar"
#: documents/models.py:53 #: documents/models.py:53
msgid "None" msgid "None"
msgstr "" msgstr "Nimic"
#: documents/models.py:54 #: documents/models.py:54
msgid "Any word" msgid "Any word"
@ -108,15 +108,15 @@ msgstr "tipuri de document"
#: documents/models.py:124 #: documents/models.py:124
msgid "path" msgid "path"
msgstr "" msgstr "cale"
#: documents/models.py:129 documents/models.py:156 #: documents/models.py:129 documents/models.py:156
msgid "storage path" msgid "storage path"
msgstr "" msgstr "cale de stocare"
#: documents/models.py:130 #: documents/models.py:130
msgid "storage paths" msgid "storage paths"
msgstr "" msgstr "căi de stocare"
#: documents/models.py:137 #: documents/models.py:137
msgid "Unencrypted" msgid "Unencrypted"
@ -193,11 +193,11 @@ msgstr "Numele curent al arhivei stocate"
#: documents/models.py:250 #: documents/models.py:250
msgid "original filename" msgid "original filename"
msgstr "" msgstr "numele original al fișierului"
#: documents/models.py:256 #: documents/models.py:256
msgid "The original name of the file when it was uploaded" msgid "The original name of the file when it was uploaded"
msgstr "" msgstr "Numele original al fișierului când a fost încărcat"
#: documents/models.py:263 #: documents/models.py:263
msgid "archive serial number" msgid "archive serial number"
@ -381,47 +381,47 @@ msgstr ""
#: documents/models.py:447 #: documents/models.py:447
msgid "storage path is" msgid "storage path is"
msgstr "" msgstr "calea de stocare este"
#: documents/models.py:448 #: documents/models.py:448
msgid "has correspondent in" msgid "has correspondent in"
msgstr "" msgstr "are corespondent în"
#: documents/models.py:449 #: documents/models.py:449
msgid "does not have correspondent in" msgid "does not have correspondent in"
msgstr "" msgstr "nu are corespondent în"
#: documents/models.py:450 #: documents/models.py:450
msgid "has document type in" msgid "has document type in"
msgstr "" msgstr "are tip de document în"
#: documents/models.py:451 #: documents/models.py:451
msgid "does not have document type in" msgid "does not have document type in"
msgstr "" msgstr "nu are tip document în"
#: documents/models.py:452 #: documents/models.py:452
msgid "has storage path in" msgid "has storage path in"
msgstr "" msgstr "are cale de stocare în"
#: documents/models.py:453 #: documents/models.py:453
msgid "does not have storage path in" msgid "does not have storage path in"
msgstr "" msgstr "nu are cale de stocare în"
#: documents/models.py:454 #: documents/models.py:454
msgid "owner is" msgid "owner is"
msgstr "" msgstr "proprietarul este"
#: documents/models.py:455 #: documents/models.py:455
msgid "has owner in" msgid "has owner in"
msgstr "" msgstr "are proprietar în"
#: documents/models.py:456 #: documents/models.py:456
msgid "does not have owner" msgid "does not have owner"
msgstr "" msgstr "nu are proprietar"
#: documents/models.py:457 #: documents/models.py:457
msgid "does not have owner in" msgid "does not have owner in"
msgstr "" msgstr "nu are proprietar în"
#: documents/models.py:467 #: documents/models.py:467
msgid "rule type" msgid "rule type"
@ -441,47 +441,47 @@ msgstr "reguli de filtrare"
#: documents/models.py:584 #: documents/models.py:584
msgid "Task ID" msgid "Task ID"
msgstr "" msgstr "ID Sarcină"
#: documents/models.py:585 #: documents/models.py:585
msgid "Celery ID for the Task that was run" msgid "Celery ID for the Task that was run"
msgstr "" msgstr "ID-ul sarcinii Celery care a fost rulată"
#: documents/models.py:590 #: documents/models.py:590
msgid "Acknowledged" msgid "Acknowledged"
msgstr "" msgstr "Confirmat"
#: documents/models.py:591 #: documents/models.py:591
msgid "If the task is acknowledged via the frontend or API" msgid "If the task is acknowledged via the frontend or API"
msgstr "" msgstr "Dacă sarcina este confirmată prin frontend sau API"
#: documents/models.py:597 #: documents/models.py:597
msgid "Task Filename" msgid "Task Filename"
msgstr "" msgstr "Numele fișierului sarcină"
#: documents/models.py:598 #: documents/models.py:598
msgid "Name of the file which the Task was run for" msgid "Name of the file which the Task was run for"
msgstr "" msgstr "Numele fișierului pentru care sarcina a fost executată"
#: documents/models.py:604 #: documents/models.py:604
msgid "Task Name" msgid "Task Name"
msgstr "" msgstr "Nume sarcină"
#: documents/models.py:605 #: documents/models.py:605
msgid "Name of the Task which was run" msgid "Name of the Task which was run"
msgstr "" msgstr "Numele sarcinii care a fost executată"
#: documents/models.py:612 #: documents/models.py:612
msgid "Task State" msgid "Task State"
msgstr "" msgstr "Stare sarcină"
#: documents/models.py:613 #: documents/models.py:613
msgid "Current state of the task being run" msgid "Current state of the task being run"
msgstr "" msgstr "Stadiul actual al sarcinii în curs de desfășurare"
#: documents/models.py:618 #: documents/models.py:618
msgid "Created DateTime" msgid "Created DateTime"
msgstr "" msgstr "Data creării"
#: documents/models.py:619 #: documents/models.py:619
msgid "Datetime field when the task result was created in UTC" msgid "Datetime field when the task result was created in UTC"
@ -489,7 +489,7 @@ msgstr ""
#: documents/models.py:624 #: documents/models.py:624
msgid "Started DateTime" msgid "Started DateTime"
msgstr "" msgstr "Data începerii"
#: documents/models.py:625 #: documents/models.py:625
msgid "Datetime field when the task was started in UTC" msgid "Datetime field when the task was started in UTC"
@ -497,7 +497,7 @@ msgstr ""
#: documents/models.py:630 #: documents/models.py:630
msgid "Completed DateTime" msgid "Completed DateTime"
msgstr "" msgstr "Data finalizării"
#: documents/models.py:631 #: documents/models.py:631
msgid "Datetime field when the task was completed in UTC" msgid "Datetime field when the task was completed in UTC"
@ -505,15 +505,15 @@ msgstr ""
#: documents/models.py:636 #: documents/models.py:636
msgid "Result Data" msgid "Result Data"
msgstr "" msgstr "Datele rezultatului"
#: documents/models.py:638 #: documents/models.py:638
msgid "The data returned by the task" msgid "The data returned by the task"
msgstr "" msgstr "Datele returnate de sarcină"
#: documents/models.py:650 #: documents/models.py:650
msgid "Note for the document" msgid "Note for the document"
msgstr "" msgstr "Notă pentru document"
#: documents/models.py:674 #: documents/models.py:674
msgid "user" msgid "user"
@ -521,23 +521,23 @@ msgstr "utilizator"
#: documents/models.py:679 #: documents/models.py:679
msgid "note" msgid "note"
msgstr "" msgstr "notă"
#: documents/models.py:680 #: documents/models.py:680
msgid "notes" msgid "notes"
msgstr "" msgstr "note"
#: documents/models.py:688 #: documents/models.py:688
msgid "Archive" msgid "Archive"
msgstr "" msgstr "Arhivă"
#: documents/models.py:689 #: documents/models.py:689
msgid "Original" msgid "Original"
msgstr "" msgstr "Original"
#: documents/models.py:700 #: documents/models.py:700
msgid "expiration" msgid "expiration"
msgstr "" msgstr "expirare"
#: documents/models.py:707 #: documents/models.py:707
msgid "slug" msgid "slug"
@ -545,35 +545,35 @@ msgstr ""
#: documents/models.py:739 #: documents/models.py:739
msgid "share link" msgid "share link"
msgstr "" msgstr "link de partajare"
#: documents/models.py:740 #: documents/models.py:740
msgid "share links" msgid "share links"
msgstr "" msgstr "link-uri de partajare"
#: documents/models.py:752 #: documents/models.py:752
msgid "String" msgid "String"
msgstr "" msgstr "Şir de caractere"
#: documents/models.py:753 #: documents/models.py:753
msgid "URL" msgid "URL"
msgstr "" msgstr "Adresă URL"
#: documents/models.py:754 #: documents/models.py:754
msgid "Date" msgid "Date"
msgstr "" msgstr "Dată"
#: documents/models.py:755 #: documents/models.py:755
msgid "Boolean" msgid "Boolean"
msgstr "" msgstr "Boolean"
#: documents/models.py:756 #: documents/models.py:756
msgid "Integer" msgid "Integer"
msgstr "" msgstr "Număr întreg"
#: documents/models.py:757 #: documents/models.py:757
msgid "Float" msgid "Float"
msgstr "" msgstr "Număr zecimal"
#: documents/models.py:758 #: documents/models.py:758
msgid "Monetary" msgid "Monetary"
@ -581,11 +581,11 @@ msgstr ""
#: documents/models.py:759 #: documents/models.py:759
msgid "Document Link" msgid "Document Link"
msgstr "" msgstr "Link document"
#: documents/models.py:771 #: documents/models.py:771
msgid "data type" msgid "data type"
msgstr "" msgstr "tip date"
#: documents/models.py:779 #: documents/models.py:779
msgid "custom field" msgid "custom field"

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-05 08:26-0800\n" "POT-Creation-Date: 2023-12-05 08:26-0800\n"
"PO-Revision-Date: 2023-12-05 16:27\n" "PO-Revision-Date: 2023-12-14 00:23\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Chinese Traditional\n" "Language-Team: Chinese Traditional\n"
"Language: zh_TW\n" "Language: zh_TW\n"
@ -47,7 +47,7 @@ msgstr ""
#: documents/models.py:58 #: documents/models.py:58
msgid "Fuzzy word" msgid "Fuzzy word"
msgstr "" msgstr "模糊詞"
#: documents/models.py:59 #: documents/models.py:59
msgid "Automatic" msgid "Automatic"
@ -68,15 +68,15 @@ msgstr "比對演算法"
#: documents/models.py:72 #: documents/models.py:72
msgid "is insensitive" msgid "is insensitive"
msgstr "" msgstr "不區分大小寫"
#: documents/models.py:95 documents/models.py:147 #: documents/models.py:95 documents/models.py:147
msgid "correspondent" msgid "correspondent"
msgstr "" msgstr "聯繫者"
#: documents/models.py:96 #: documents/models.py:96
msgid "correspondents" msgid "correspondents"
msgstr "" msgstr "聯繫者"
#: documents/models.py:100 #: documents/models.py:100
msgid "color" msgid "color"
@ -84,47 +84,47 @@ msgstr "顏色"
#: documents/models.py:103 #: documents/models.py:103
msgid "is inbox tag" msgid "is inbox tag"
msgstr "" msgstr "收件匣標籤"
#: documents/models.py:106 #: documents/models.py:106
msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags." msgid "Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags."
msgstr "" msgstr "標記此標籤為收件匣標籤:所有新處理的文件將會以此收件匣標籤作標記。"
#: documents/models.py:112 #: documents/models.py:112
msgid "tag" msgid "tag"
msgstr "" msgstr "標籤"
#: documents/models.py:113 documents/models.py:185 #: documents/models.py:113 documents/models.py:185
msgid "tags" msgid "tags"
msgstr "" msgstr "標籤"
#: documents/models.py:118 documents/models.py:167 #: documents/models.py:118 documents/models.py:167
msgid "document type" msgid "document type"
msgstr "" msgstr "文件類型"
#: documents/models.py:119 #: documents/models.py:119
msgid "document types" msgid "document types"
msgstr "" msgstr "文件類型"
#: documents/models.py:124 #: documents/models.py:124
msgid "path" msgid "path"
msgstr "" msgstr "位址"
#: documents/models.py:129 documents/models.py:156 #: documents/models.py:129 documents/models.py:156
msgid "storage path" msgid "storage path"
msgstr "" msgstr "儲存位址"
#: documents/models.py:130 #: documents/models.py:130
msgid "storage paths" msgid "storage paths"
msgstr "" msgstr "儲存位址"
#: documents/models.py:137 #: documents/models.py:137
msgid "Unencrypted" msgid "Unencrypted"
msgstr "" msgstr "未加密"
#: documents/models.py:138 #: documents/models.py:138
msgid "Encrypted with GNU Privacy Guard" msgid "Encrypted with GNU Privacy Guard"
msgstr "" msgstr "已使用 GNU Privacy Guard 進行加密"
#: documents/models.py:159 #: documents/models.py:159
msgid "title" msgid "title"
@ -189,27 +189,27 @@ msgstr "存檔檔案名稱"
#: documents/models.py:246 #: documents/models.py:246
msgid "Current archive filename in storage" msgid "Current archive filename in storage"
msgstr "" msgstr "現時儲存空間封存的檔案名稱"
#: documents/models.py:250 #: documents/models.py:250
msgid "original filename" msgid "original filename"
msgstr "" msgstr "原先檔案名稱"
#: documents/models.py:256 #: documents/models.py:256
msgid "The original name of the file when it was uploaded" msgid "The original name of the file when it was uploaded"
msgstr "" msgstr "檔案上傳時的檔案名稱"
#: documents/models.py:263 #: documents/models.py:263
msgid "archive serial number" msgid "archive serial number"
msgstr "" msgstr "封存編號"
#: documents/models.py:273 #: documents/models.py:273
msgid "The position of this document in your physical document archive." msgid "The position of this document in your physical document archive."
msgstr "" msgstr "此檔案在你實體儲存空間的位置。"
#: documents/models.py:279 documents/models.py:665 documents/models.py:719 #: documents/models.py:279 documents/models.py:665 documents/models.py:719
msgid "document" msgid "document"
msgstr "" msgstr "文件"
#: documents/models.py:280 #: documents/models.py:280
msgid "documents" msgid "documents"
@ -217,47 +217,47 @@ msgstr "文件"
#: documents/models.py:368 #: documents/models.py:368
msgid "debug" msgid "debug"
msgstr "" msgstr "偵錯"
#: documents/models.py:369 #: documents/models.py:369
msgid "information" msgid "information"
msgstr "" msgstr "資訊"
#: documents/models.py:370 #: documents/models.py:370
msgid "warning" msgid "warning"
msgstr "" msgstr "警告"
#: documents/models.py:371 paperless_mail/models.py:305 #: documents/models.py:371 paperless_mail/models.py:305
msgid "error" msgid "error"
msgstr "" msgstr "錯誤"
#: documents/models.py:372 #: documents/models.py:372
msgid "critical" msgid "critical"
msgstr "" msgstr "嚴重"
#: documents/models.py:375 #: documents/models.py:375
msgid "group" msgid "group"
msgstr "" msgstr "群組"
#: documents/models.py:377 #: documents/models.py:377
msgid "message" msgid "message"
msgstr "" msgstr "訊息"
#: documents/models.py:380 #: documents/models.py:380
msgid "level" msgid "level"
msgstr "" msgstr "程度"
#: documents/models.py:389 #: documents/models.py:389
msgid "log" msgid "log"
msgstr "" msgstr "記錄"
#: documents/models.py:390 #: documents/models.py:390
msgid "logs" msgid "logs"
msgstr "" msgstr "記錄"
#: documents/models.py:399 documents/models.py:464 #: documents/models.py:399 documents/models.py:464
msgid "saved view" msgid "saved view"
msgstr "" msgstr "已儲存的檢視表"
#: documents/models.py:400 #: documents/models.py:400
msgid "saved views" msgid "saved views"
@ -265,207 +265,207 @@ msgstr "保存視圖"
#: documents/models.py:405 #: documents/models.py:405
msgid "show on dashboard" msgid "show on dashboard"
msgstr "" msgstr "顯示在概覽"
#: documents/models.py:408 #: documents/models.py:408
msgid "show in sidebar" msgid "show in sidebar"
msgstr "" msgstr "顯示在側邊欄"
#: documents/models.py:412 #: documents/models.py:412
msgid "sort field" msgid "sort field"
msgstr "" msgstr "排序欄位"
#: documents/models.py:417 #: documents/models.py:417
msgid "sort reverse" msgid "sort reverse"
msgstr "" msgstr "倒轉排序"
#: documents/models.py:422 #: documents/models.py:422
msgid "title contains" msgid "title contains"
msgstr "" msgstr "標題包含"
#: documents/models.py:423 #: documents/models.py:423
msgid "content contains" msgid "content contains"
msgstr "" msgstr "內容包含"
#: documents/models.py:424 #: documents/models.py:424
msgid "ASN is" msgid "ASN is"
msgstr "" msgstr "ASN 為"
#: documents/models.py:425 #: documents/models.py:425
msgid "correspondent is" msgid "correspondent is"
msgstr "" msgstr "聯繫者為"
#: documents/models.py:426 #: documents/models.py:426
msgid "document type is" msgid "document type is"
msgstr "" msgstr "文件類型為"
#: documents/models.py:427 #: documents/models.py:427
msgid "is in inbox" msgid "is in inbox"
msgstr "" msgstr "在收件匣內"
#: documents/models.py:428 #: documents/models.py:428
msgid "has tag" msgid "has tag"
msgstr "" msgstr "包含標籤"
#: documents/models.py:429 #: documents/models.py:429
msgid "has any tag" msgid "has any tag"
msgstr "" msgstr "包含任何標籤"
#: documents/models.py:430 #: documents/models.py:430
msgid "created before" msgid "created before"
msgstr "" msgstr "建立時間之前"
#: documents/models.py:431 #: documents/models.py:431
msgid "created after" msgid "created after"
msgstr "" msgstr "建立時間之後"
#: documents/models.py:432 #: documents/models.py:432
msgid "created year is" msgid "created year is"
msgstr "" msgstr "建立年份為"
#: documents/models.py:433 #: documents/models.py:433
msgid "created month is" msgid "created month is"
msgstr "" msgstr "建立月份為"
#: documents/models.py:434 #: documents/models.py:434
msgid "created day is" msgid "created day is"
msgstr "" msgstr "建立日期為"
#: documents/models.py:435 #: documents/models.py:435
msgid "added before" msgid "added before"
msgstr "" msgstr "加入時間之前"
#: documents/models.py:436 #: documents/models.py:436
msgid "added after" msgid "added after"
msgstr "" msgstr "加入時間之後"
#: documents/models.py:437 #: documents/models.py:437
msgid "modified before" msgid "modified before"
msgstr "" msgstr "修改之前"
#: documents/models.py:438 #: documents/models.py:438
msgid "modified after" msgid "modified after"
msgstr "" msgstr "修改之後"
#: documents/models.py:439 #: documents/models.py:439
msgid "does not have tag" msgid "does not have tag"
msgstr "" msgstr "沒有包含標籤"
#: documents/models.py:440 #: documents/models.py:440
msgid "does not have ASN" msgid "does not have ASN"
msgstr "" msgstr "沒有包含 ASN"
#: documents/models.py:441 #: documents/models.py:441
msgid "title or content contains" msgid "title or content contains"
msgstr "" msgstr "標題或內容包含"
#: documents/models.py:442 #: documents/models.py:442
msgid "fulltext query" msgid "fulltext query"
msgstr "" msgstr "全文搜索"
#: documents/models.py:443 #: documents/models.py:443
msgid "more like this" msgid "more like this"
msgstr "" msgstr "其他類似內容"
#: documents/models.py:444 #: documents/models.py:444
msgid "has tags in" msgid "has tags in"
msgstr "" msgstr "含有這個標籤"
#: documents/models.py:445 #: documents/models.py:445
msgid "ASN greater than" msgid "ASN greater than"
msgstr "" msgstr "ASN 大於"
#: documents/models.py:446 #: documents/models.py:446
msgid "ASN less than" msgid "ASN less than"
msgstr "" msgstr "ASN 小於"
#: documents/models.py:447 #: documents/models.py:447
msgid "storage path is" msgid "storage path is"
msgstr "" msgstr "儲存位址為"
#: documents/models.py:448 #: documents/models.py:448
msgid "has correspondent in" msgid "has correspondent in"
msgstr "" msgstr "包含聯繫者"
#: documents/models.py:449 #: documents/models.py:449
msgid "does not have correspondent in" msgid "does not have correspondent in"
msgstr "" msgstr "沒有包含聯繫者"
#: documents/models.py:450 #: documents/models.py:450
msgid "has document type in" msgid "has document type in"
msgstr "" msgstr "文件類型包含"
#: documents/models.py:451 #: documents/models.py:451
msgid "does not have document type in" msgid "does not have document type in"
msgstr "" msgstr "沒有包含的文件類型"
#: documents/models.py:452 #: documents/models.py:452
msgid "has storage path in" msgid "has storage path in"
msgstr "" msgstr "儲存位址包含"
#: documents/models.py:453 #: documents/models.py:453
msgid "does not have storage path in" msgid "does not have storage path in"
msgstr "" msgstr "沒有包含的儲存位址"
#: documents/models.py:454 #: documents/models.py:454
msgid "owner is" msgid "owner is"
msgstr "" msgstr "擁有者為"
#: documents/models.py:455 #: documents/models.py:455
msgid "has owner in" msgid "has owner in"
msgstr "" msgstr "擁有者包含"
#: documents/models.py:456 #: documents/models.py:456
msgid "does not have owner" msgid "does not have owner"
msgstr "" msgstr "沒有包含的擁有者"
#: documents/models.py:457 #: documents/models.py:457
msgid "does not have owner in" msgid "does not have owner in"
msgstr "" msgstr "沒有包含的擁有者"
#: documents/models.py:467 #: documents/models.py:467
msgid "rule type" msgid "rule type"
msgstr "" msgstr "規則類型"
#: documents/models.py:469 #: documents/models.py:469
msgid "value" msgid "value"
msgstr "" msgstr "數值"
#: documents/models.py:472 #: documents/models.py:472
msgid "filter rule" msgid "filter rule"
msgstr "" msgstr "過濾規則"
#: documents/models.py:473 #: documents/models.py:473
msgid "filter rules" msgid "filter rules"
msgstr "" msgstr "過濾規則"
#: documents/models.py:584 #: documents/models.py:584
msgid "Task ID" msgid "Task ID"
msgstr "" msgstr "任務 ID"
#: documents/models.py:585 #: documents/models.py:585
msgid "Celery ID for the Task that was run" msgid "Celery ID for the Task that was run"
msgstr "" msgstr "已執行任務的 Celery ID"
#: documents/models.py:590 #: documents/models.py:590
msgid "Acknowledged" msgid "Acknowledged"
msgstr "" msgstr "已確認"
#: documents/models.py:591 #: documents/models.py:591
msgid "If the task is acknowledged via the frontend or API" msgid "If the task is acknowledged via the frontend or API"
msgstr "" msgstr "如果任務已由前端 / API 確認"
#: documents/models.py:597 #: documents/models.py:597
msgid "Task Filename" msgid "Task Filename"
msgstr "" msgstr "任務檔案名稱"
#: documents/models.py:598 #: documents/models.py:598
msgid "Name of the file which the Task was run for" msgid "Name of the file which the Task was run for"
msgstr "" msgstr "執行任務的目標檔案名稱"
#: documents/models.py:604 #: documents/models.py:604
msgid "Task Name" msgid "Task Name"
msgstr "" msgstr "任務名稱"
#: documents/models.py:605 #: documents/models.py:605
msgid "Name of the Task which was run" msgid "Name of the Task which was run"
@ -473,7 +473,7 @@ msgstr ""
#: documents/models.py:612 #: documents/models.py:612
msgid "Task State" msgid "Task State"
msgstr "" msgstr "任務狀態"
#: documents/models.py:613 #: documents/models.py:613
msgid "Current state of the task being run" msgid "Current state of the task being run"
@ -657,7 +657,7 @@ msgstr ""
#: documents/models.py:967 paperless_mail/models.py:238 #: documents/models.py:967 paperless_mail/models.py:238
msgid "assign this correspondent" msgid "assign this correspondent"
msgstr "" msgstr "指派這個聯繫者"
#: documents/models.py:975 #: documents/models.py:975
msgid "assign this storage path" msgid "assign this storage path"
@ -1128,7 +1128,7 @@ msgstr ""
#: paperless_mail/models.py:88 #: paperless_mail/models.py:88
msgid "Do not assign a correspondent" msgid "Do not assign a correspondent"
msgstr "" msgstr "不要指派聯繫者"
#: paperless_mail/models.py:89 #: paperless_mail/models.py:89
msgid "Use mail address" msgid "Use mail address"
@ -1140,7 +1140,7 @@ msgstr ""
#: paperless_mail/models.py:91 #: paperless_mail/models.py:91
msgid "Use correspondent selected below" msgid "Use correspondent selected below"
msgstr "" msgstr "使用以下已選擇的聯繫者"
#: paperless_mail/models.py:101 #: paperless_mail/models.py:101
msgid "account" msgid "account"
@ -1220,7 +1220,7 @@ msgstr ""
#: paperless_mail/models.py:228 #: paperless_mail/models.py:228
msgid "assign correspondent from" msgid "assign correspondent from"
msgstr "" msgstr "指派聯繫者從"
#: paperless_mail/models.py:242 #: paperless_mail/models.py:242
msgid "Assign the rule owner to documents" msgid "Assign the rule owner to documents"

View File

@ -254,7 +254,7 @@ class RasterisedDocumentParser(DocumentParser):
f"Image DPI of {ocrmypdf_args['image_dpi']} is low, OCR may fail", f"Image DPI of {ocrmypdf_args['image_dpi']} is low, OCR may fail",
) )
if settings.OCR_USER_ARGS and not safe_fallback: if settings.OCR_USER_ARGS:
try: try:
user_args = json.loads(settings.OCR_USER_ARGS) user_args = json.loads(settings.OCR_USER_ARGS)
ocrmypdf_args = {**ocrmypdf_args, **user_args} ocrmypdf_args = {**ocrmypdf_args, **user_args}