mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-05-15 12:29:29 -05:00
Enhancement: add barcode frontend config (#9742)
This commit is contained in:
parent
bcb0ae1ee5
commit
6a5be992c0
@ -105,9 +105,9 @@ describe('ConfigComponent', () => {
|
|||||||
|
|
||||||
it('should support JSON validation for e.g. user_args', () => {
|
it('should support JSON validation for e.g. user_args', () => {
|
||||||
component.configForm.patchValue({ user_args: '{ foo bar }' })
|
component.configForm.patchValue({ user_args: '{ foo bar }' })
|
||||||
expect(component.errors).toEqual({ user_args: 'Invalid JSON' })
|
expect(component.errors['user_args']).toEqual('Invalid JSON')
|
||||||
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
|
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
|
||||||
expect(component.errors).toEqual({ user_args: null })
|
expect(component.errors['user_args']).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should upload file, show error if necessary', () => {
|
it('should upload file, show error if necessary', () => {
|
||||||
|
@ -49,6 +49,7 @@ export enum ConfigOptionType {
|
|||||||
export const ConfigCategory = {
|
export const ConfigCategory = {
|
||||||
General: $localize`General Settings`,
|
General: $localize`General Settings`,
|
||||||
OCR: $localize`OCR Settings`,
|
OCR: $localize`OCR Settings`,
|
||||||
|
Barcode: $localize`Barcode Settings`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigOption {
|
export interface ConfigOption {
|
||||||
@ -180,6 +181,83 @@ export const PaperlessConfigOptions: ConfigOption[] = [
|
|||||||
config_key: 'PAPERLESS_APP_TITLE',
|
config_key: 'PAPERLESS_APP_TITLE',
|
||||||
category: ConfigCategory.General,
|
category: ConfigCategory.General,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'barcodes_enabled',
|
||||||
|
title: $localize`Enable Barcodes`,
|
||||||
|
type: ConfigOptionType.Boolean,
|
||||||
|
config_key: 'PAPERLESS_CONSUMER_ENABLE_BARCODES',
|
||||||
|
category: ConfigCategory.Barcode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'barcode_enable_tiff_support',
|
||||||
|
title: $localize`Enable TIFF Support`,
|
||||||
|
type: ConfigOptionType.Boolean,
|
||||||
|
config_key: 'PAPERLESS_CONSUMER_BARCODE_TIFF_SUPPORT',
|
||||||
|
category: ConfigCategory.Barcode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'barcode_string',
|
||||||
|
title: $localize`Barcode String`,
|
||||||
|
type: ConfigOptionType.String,
|
||||||
|
config_key: 'PAPERLESS_CONSUMER_BARCODE_STRING',
|
||||||
|
category: ConfigCategory.Barcode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'barcode_retain_split_pages',
|
||||||
|
title: $localize`Retain Split Pages`,
|
||||||
|
type: ConfigOptionType.Boolean,
|
||||||
|
config_key: 'PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES',
|
||||||
|
category: ConfigCategory.Barcode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'barcode_enable_asn',
|
||||||
|
title: $localize`Enable ASN`,
|
||||||
|
type: ConfigOptionType.Boolean,
|
||||||
|
config_key: 'PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE',
|
||||||
|
category: ConfigCategory.Barcode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'barcode_asn_prefix',
|
||||||
|
title: $localize`ASN Prefix`,
|
||||||
|
type: ConfigOptionType.String,
|
||||||
|
config_key: 'PAPERLESS_CONSUMER_ASN_BARCODE_PREFIX',
|
||||||
|
category: ConfigCategory.Barcode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'barcode_upscale',
|
||||||
|
title: $localize`Upscale`,
|
||||||
|
type: ConfigOptionType.Number,
|
||||||
|
config_key: 'PAPERLESS_CONSUMER_BARCODE_UPSCALE',
|
||||||
|
category: ConfigCategory.Barcode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'barcode_dpi',
|
||||||
|
title: $localize`DPI`,
|
||||||
|
type: ConfigOptionType.Number,
|
||||||
|
config_key: 'PAPERLESS_CONSUMER_BARCODE_DPI',
|
||||||
|
category: ConfigCategory.Barcode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'barcode_max_pages',
|
||||||
|
title: $localize`Max Pages`,
|
||||||
|
type: ConfigOptionType.Number,
|
||||||
|
config_key: 'PAPERLESS_CONSUMER_BARCODE_MAX_PAGES',
|
||||||
|
category: ConfigCategory.Barcode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'barcode_enable_tag',
|
||||||
|
title: $localize`Enable Tag Detection`,
|
||||||
|
type: ConfigOptionType.Boolean,
|
||||||
|
config_key: 'PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE',
|
||||||
|
category: ConfigCategory.Barcode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'barcode_tag_mapping',
|
||||||
|
title: $localize`Tag Mapping`,
|
||||||
|
type: ConfigOptionType.JSON,
|
||||||
|
config_key: 'PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING',
|
||||||
|
category: ConfigCategory.Barcode,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export interface PaperlessConfig extends ObjectWithId {
|
export interface PaperlessConfig extends ObjectWithId {
|
||||||
@ -198,4 +276,15 @@ export interface PaperlessConfig extends ObjectWithId {
|
|||||||
user_args: object
|
user_args: object
|
||||||
app_logo: string
|
app_logo: string
|
||||||
app_title: string
|
app_title: string
|
||||||
|
barcodes_enabled: boolean
|
||||||
|
barcode_enable_tiff_support: boolean
|
||||||
|
barcode_string: string
|
||||||
|
barcode_retain_split_pages: boolean
|
||||||
|
barcode_enable_asn: boolean
|
||||||
|
barcode_asn_prefix: string
|
||||||
|
barcode_upscale: number
|
||||||
|
barcode_dpi: number
|
||||||
|
barcode_max_pages: number
|
||||||
|
barcode_enable_tag: boolean
|
||||||
|
barcode_tag_mapping: object
|
||||||
}
|
}
|
||||||
|
@ -15,13 +15,16 @@ from pikepdf import Pdf
|
|||||||
|
|
||||||
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 ConsumableDocument
|
||||||
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.plugins.base import ConsumeTaskPlugin
|
from documents.plugins.base import ConsumeTaskPlugin
|
||||||
from documents.plugins.base import StopConsumeTaskError
|
from documents.plugins.base import StopConsumeTaskError
|
||||||
|
from documents.plugins.helpers import ProgressManager
|
||||||
from documents.plugins.helpers import ProgressStatusOptions
|
from documents.plugins.helpers import ProgressStatusOptions
|
||||||
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
|
||||||
from documents.utils import maybe_override_pixel_limit
|
from documents.utils import maybe_override_pixel_limit
|
||||||
|
from paperless.config import BarcodeConfig
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
@ -39,6 +42,7 @@ class Barcode:
|
|||||||
|
|
||||||
page: int
|
page: int
|
||||||
value: str
|
value: str
|
||||||
|
settings: BarcodeConfig
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_separator(self) -> bool:
|
def is_separator(self) -> bool:
|
||||||
@ -46,7 +50,7 @@ class Barcode:
|
|||||||
Returns True if the barcode value equals the configured separation value,
|
Returns True if the barcode value equals the configured separation value,
|
||||||
False otherwise
|
False otherwise
|
||||||
"""
|
"""
|
||||||
return self.value == settings.CONSUMER_BARCODE_STRING
|
return self.value == self.settings.barcode_string
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_asn(self) -> bool:
|
def is_asn(self) -> bool:
|
||||||
@ -54,7 +58,7 @@ class Barcode:
|
|||||||
Returns True if the barcode value matches the configured ASN prefix,
|
Returns True if the barcode value matches the configured ASN prefix,
|
||||||
False otherwise
|
False otherwise
|
||||||
"""
|
"""
|
||||||
return self.value.startswith(settings.CONSUMER_ASN_BARCODE_PREFIX)
|
return self.value.startswith(self.settings.barcode_asn_prefix)
|
||||||
|
|
||||||
|
|
||||||
class BarcodePlugin(ConsumeTaskPlugin):
|
class BarcodePlugin(ConsumeTaskPlugin):
|
||||||
@ -67,17 +71,41 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
- ASN from barcode detection is enabled or
|
- ASN from barcode detection is enabled or
|
||||||
- Barcode support is enabled and the mime type is supported
|
- Barcode support is enabled and the mime type is supported
|
||||||
"""
|
"""
|
||||||
if settings.CONSUMER_BARCODE_TIFF_SUPPORT:
|
if self.settings.barcode_enable_tiff_support:
|
||||||
supported_mimes: set[str] = {"application/pdf", "image/tiff"}
|
supported_mimes: set[str] = {"application/pdf", "image/tiff"}
|
||||||
else:
|
else:
|
||||||
supported_mimes = {"application/pdf"}
|
supported_mimes = {"application/pdf"}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
settings.CONSUMER_ENABLE_ASN_BARCODE
|
self.settings.barcode_enable_asn
|
||||||
or settings.CONSUMER_ENABLE_BARCODES
|
or self.settings.barcodes_enabled
|
||||||
or settings.CONSUMER_ENABLE_TAG_BARCODE
|
or self.settings.barcode_enable_tag
|
||||||
) and self.input_doc.mime_type in supported_mimes
|
) and self.input_doc.mime_type in supported_mimes
|
||||||
|
|
||||||
|
def get_settings(self) -> BarcodeConfig:
|
||||||
|
"""
|
||||||
|
Returns the settings for this plugin (Django settings or app config)
|
||||||
|
"""
|
||||||
|
return BarcodeConfig()
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
input_doc: ConsumableDocument,
|
||||||
|
metadata: DocumentMetadataOverrides,
|
||||||
|
status_mgr: ProgressManager,
|
||||||
|
base_tmp_dir: Path,
|
||||||
|
task_id: str,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
input_doc,
|
||||||
|
metadata,
|
||||||
|
status_mgr,
|
||||||
|
base_tmp_dir,
|
||||||
|
task_id,
|
||||||
|
)
|
||||||
|
# need these for able_to_run
|
||||||
|
self.settings = self.get_settings()
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
self.temp_dir = tempfile.TemporaryDirectory(
|
self.temp_dir = tempfile.TemporaryDirectory(
|
||||||
dir=self.base_tmp_dir,
|
dir=self.base_tmp_dir,
|
||||||
@ -99,7 +127,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
|
|
||||||
# try reading tags from barcodes
|
# try reading tags from barcodes
|
||||||
if (
|
if (
|
||||||
settings.CONSUMER_ENABLE_TAG_BARCODE
|
self.settings.barcode_enable_tag
|
||||||
and (tags := self.tags) is not None
|
and (tags := self.tags) is not None
|
||||||
and len(tags) > 0
|
and len(tags) > 0
|
||||||
):
|
):
|
||||||
@ -110,7 +138,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
logger.info(f"Found tags in barcode: {tags}")
|
logger.info(f"Found tags in barcode: {tags}")
|
||||||
|
|
||||||
# Lastly attempt to split documents
|
# Lastly attempt to split documents
|
||||||
if settings.CONSUMER_ENABLE_BARCODES and (
|
if self.settings.barcodes_enabled and (
|
||||||
separator_pages := self.get_separation_pages()
|
separator_pages := self.get_separation_pages()
|
||||||
):
|
):
|
||||||
# We have pages to split against
|
# We have pages to split against
|
||||||
@ -155,10 +183,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
|
|
||||||
# Update/overwrite an ASN if possible
|
# Update/overwrite an ASN if possible
|
||||||
# After splitting, as otherwise each split document gets the same ASN
|
# After splitting, as otherwise each split document gets the same ASN
|
||||||
if (
|
if self.settings.barcode_enable_asn and (located_asn := self.asn) is not None:
|
||||||
settings.CONSUMER_ENABLE_ASN_BARCODE
|
|
||||||
and (located_asn := self.asn) is not None
|
|
||||||
):
|
|
||||||
logger.info(f"Found ASN in barcode: {located_asn}")
|
logger.info(f"Found ASN in barcode: {located_asn}")
|
||||||
self.metadata.asn = located_asn
|
self.metadata.asn = located_asn
|
||||||
|
|
||||||
@ -245,8 +270,8 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
# Get limit from configuration
|
# Get limit from configuration
|
||||||
barcode_max_pages: int = (
|
barcode_max_pages: int = (
|
||||||
num_of_pages
|
num_of_pages
|
||||||
if settings.CONSUMER_BARCODE_MAX_PAGES == 0
|
if self.settings.barcode_max_pages == 0
|
||||||
else settings.CONSUMER_BARCODE_MAX_PAGES
|
else self.settings.barcode_max_pages
|
||||||
)
|
)
|
||||||
|
|
||||||
if barcode_max_pages < num_of_pages: # pragma: no cover
|
if barcode_max_pages < num_of_pages: # pragma: no cover
|
||||||
@ -261,7 +286,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
# Convert page to image
|
# Convert page to image
|
||||||
page = convert_from_path(
|
page = convert_from_path(
|
||||||
self.pdf_file,
|
self.pdf_file,
|
||||||
dpi=settings.CONSUMER_BARCODE_DPI,
|
dpi=self.settings.barcode_dpi,
|
||||||
output_folder=self.temp_dir.name,
|
output_folder=self.temp_dir.name,
|
||||||
first_page=current_page_number + 1,
|
first_page=current_page_number + 1,
|
||||||
last_page=current_page_number + 1,
|
last_page=current_page_number + 1,
|
||||||
@ -272,7 +297,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
logger.debug(f"Image is at {page_filepath}")
|
logger.debug(f"Image is at {page_filepath}")
|
||||||
|
|
||||||
# Upscale image if configured
|
# Upscale image if configured
|
||||||
factor = settings.CONSUMER_BARCODE_UPSCALE
|
factor = self.settings.barcode_upscale
|
||||||
if factor > 1.0:
|
if factor > 1.0:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Upscaling image by {factor} for better barcode detection",
|
f"Upscaling image by {factor} for better barcode detection",
|
||||||
@ -285,7 +310,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
# Detect barcodes
|
# Detect barcodes
|
||||||
for barcode_value in reader(page):
|
for barcode_value in reader(page):
|
||||||
self.barcodes.append(
|
self.barcodes.append(
|
||||||
Barcode(current_page_number, barcode_value),
|
Barcode(current_page_number, barcode_value, self.settings),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete temporary image file
|
# Delete temporary image file
|
||||||
@ -308,7 +333,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
def asn(self) -> int | None:
|
def asn(self) -> int | None:
|
||||||
"""
|
"""
|
||||||
Search the parsed barcodes for any ASNs.
|
Search the parsed barcodes for any ASNs.
|
||||||
The first barcode that starts with CONSUMER_ASN_BARCODE_PREFIX
|
The first barcode that starts with barcode_asn_prefix
|
||||||
is considered the ASN to be used.
|
is considered the ASN to be used.
|
||||||
Returns the detected ASN (or None)
|
Returns the detected ASN (or None)
|
||||||
"""
|
"""
|
||||||
@ -317,7 +342,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
# Ensure the barcodes have been read
|
# Ensure the barcodes have been read
|
||||||
self.detect()
|
self.detect()
|
||||||
|
|
||||||
# get the first barcode that starts with CONSUMER_ASN_BARCODE_PREFIX
|
# get the first barcode that starts with barcode_asn_prefix
|
||||||
asn_text: str | None = next(
|
asn_text: str | None = next(
|
||||||
(x.value for x in self.barcodes if x.is_asn),
|
(x.value for x in self.barcodes if x.is_asn),
|
||||||
None,
|
None,
|
||||||
@ -326,7 +351,7 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
if asn_text:
|
if asn_text:
|
||||||
logger.debug(f"Found ASN Barcode: {asn_text}")
|
logger.debug(f"Found ASN Barcode: {asn_text}")
|
||||||
# remove the prefix and remove whitespace
|
# remove the prefix and remove whitespace
|
||||||
asn_text = asn_text[len(settings.CONSUMER_ASN_BARCODE_PREFIX) :].strip()
|
asn_text = asn_text[len(self.settings.barcode_asn_prefix) :].strip()
|
||||||
|
|
||||||
# remove non-numeric parts of the remaining string
|
# remove non-numeric parts of the remaining string
|
||||||
asn_text = re.sub(r"\D", "", asn_text)
|
asn_text = re.sub(r"\D", "", asn_text)
|
||||||
@ -356,9 +381,9 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
for raw in tag_texts.split(","):
|
for raw in tag_texts.split(","):
|
||||||
try:
|
try:
|
||||||
tag_str: str | None = None
|
tag_str: str | None = None
|
||||||
for regex in settings.CONSUMER_TAG_BARCODE_MAPPING:
|
for regex in self.settings.barcode_tag_mapping:
|
||||||
if re.match(regex, raw, flags=re.IGNORECASE):
|
if re.match(regex, raw, flags=re.IGNORECASE):
|
||||||
sub = settings.CONSUMER_TAG_BARCODE_MAPPING[regex]
|
sub = self.settings.barcode_tag_mapping[regex]
|
||||||
tag_str = (
|
tag_str = (
|
||||||
re.sub(regex, sub, raw, flags=re.IGNORECASE)
|
re.sub(regex, sub, raw, flags=re.IGNORECASE)
|
||||||
if sub
|
if sub
|
||||||
@ -394,13 +419,13 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
"""
|
"""
|
||||||
# filter all barcodes for the separator string
|
# filter all barcodes for the separator string
|
||||||
# get the page numbers of the separating barcodes
|
# get the page numbers of the separating barcodes
|
||||||
retain = settings.CONSUMER_BARCODE_RETAIN_SPLIT_PAGES
|
retain = self.settings.barcode_retain_split_pages
|
||||||
separator_pages = {
|
separator_pages = {
|
||||||
bc.page: retain
|
bc.page: retain
|
||||||
for bc in self.barcodes
|
for bc in self.barcodes
|
||||||
if bc.is_separator and (not retain or (retain and bc.page > 0))
|
if bc.is_separator and (not retain or (retain and bc.page > 0))
|
||||||
} # as below, dont include the first page if retain is enabled
|
} # as below, dont include the first page if retain is enabled
|
||||||
if not settings.CONSUMER_ENABLE_ASN_BARCODE:
|
if not self.settings.barcode_enable_asn:
|
||||||
return separator_pages
|
return separator_pages
|
||||||
|
|
||||||
# add the page numbers of the ASN barcodes
|
# add the page numbers of the ASN barcodes
|
||||||
|
@ -32,12 +32,12 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
self.assertEqual(
|
self.maxDiff = None
|
||||||
json.dumps(response.data[0]),
|
|
||||||
json.dumps(
|
self.assertDictEqual(
|
||||||
|
response.data[0],
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"user_args": None,
|
|
||||||
"output_type": None,
|
"output_type": None,
|
||||||
"pages": None,
|
"pages": None,
|
||||||
"language": None,
|
"language": None,
|
||||||
@ -50,10 +50,21 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
"rotate_pages_threshold": None,
|
"rotate_pages_threshold": None,
|
||||||
"max_image_pixels": None,
|
"max_image_pixels": None,
|
||||||
"color_conversion_strategy": None,
|
"color_conversion_strategy": None,
|
||||||
|
"user_args": None,
|
||||||
"app_title": None,
|
"app_title": None,
|
||||||
"app_logo": None,
|
"app_logo": None,
|
||||||
|
"barcodes_enabled": None,
|
||||||
|
"barcode_enable_tiff_support": None,
|
||||||
|
"barcode_string": None,
|
||||||
|
"barcode_retain_split_pages": None,
|
||||||
|
"barcode_enable_asn": None,
|
||||||
|
"barcode_asn_prefix": None,
|
||||||
|
"barcode_upscale": None,
|
||||||
|
"barcode_dpi": None,
|
||||||
|
"barcode_max_pages": None,
|
||||||
|
"barcode_enable_tag": None,
|
||||||
|
"barcode_tag_mapping": None,
|
||||||
},
|
},
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_api_get_ui_settings_with_config(self):
|
def test_api_get_ui_settings_with_config(self):
|
||||||
@ -118,6 +129,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
{
|
{
|
||||||
"user_args": "",
|
"user_args": "",
|
||||||
"language": "",
|
"language": "",
|
||||||
|
"barcode_tag_mapping": "",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
@ -126,6 +138,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
config = ApplicationConfiguration.objects.first()
|
config = ApplicationConfiguration.objects.first()
|
||||||
self.assertEqual(config.user_args, None)
|
self.assertEqual(config.user_args, None)
|
||||||
self.assertEqual(config.language, None)
|
self.assertEqual(config.language, None)
|
||||||
|
self.assertEqual(config.barcode_tag_mapping, None)
|
||||||
|
|
||||||
def test_api_replace_app_logo(self):
|
def test_api_replace_app_logo(self):
|
||||||
"""
|
"""
|
||||||
|
@ -22,6 +22,7 @@ from documents.tests.utils import DocumentConsumeDelayMixin
|
|||||||
from documents.tests.utils import DummyProgressManager
|
from documents.tests.utils import DummyProgressManager
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from documents.tests.utils import SampleDirMixin
|
from documents.tests.utils import SampleDirMixin
|
||||||
|
from paperless.models import ApplicationConfiguration
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import zxingcpp # noqa: F401
|
import zxingcpp # noqa: F401
|
||||||
@ -547,6 +548,27 @@ class TestBarcode(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_barcode_config(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Barcode app config is set (settings are not)
|
||||||
|
WHEN:
|
||||||
|
- Document with barcode is processed
|
||||||
|
THEN:
|
||||||
|
- The barcode config is used
|
||||||
|
"""
|
||||||
|
app_config = ApplicationConfiguration.objects.first()
|
||||||
|
app_config.barcodes_enabled = True
|
||||||
|
app_config.barcode_string = "CUSTOM BARCODE"
|
||||||
|
app_config.save()
|
||||||
|
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-custom.pdf"
|
||||||
|
with self.get_reader(test_file) as reader:
|
||||||
|
reader.detect()
|
||||||
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
|
self.assertEqual(reader.pdf_file, test_file)
|
||||||
|
self.assertDictEqual(separator_page_numbers, {0: False})
|
||||||
|
|
||||||
|
|
||||||
@override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
|
@override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
|
||||||
class TestBarcodeNewConsume(
|
class TestBarcodeNewConsume(
|
||||||
|
@ -96,10 +96,65 @@ class OcrConfig(OutputTypeConfig):
|
|||||||
user_args = json.loads(settings.OCR_USER_ARGS)
|
user_args = json.loads(settings.OCR_USER_ARGS)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
user_args = {}
|
user_args = {}
|
||||||
|
|
||||||
self.user_args = user_args
|
self.user_args = user_args
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class BarcodeConfig(BaseConfig):
|
||||||
|
"""
|
||||||
|
Barcodes settings
|
||||||
|
"""
|
||||||
|
|
||||||
|
barcodes_enabled: bool = dataclasses.field(init=False)
|
||||||
|
barcode_enable_tiff_support: bool = dataclasses.field(init=False)
|
||||||
|
barcode_string: str = dataclasses.field(init=False)
|
||||||
|
barcode_retain_split_pages: bool = dataclasses.field(init=False)
|
||||||
|
barcode_enable_asn: bool = dataclasses.field(init=False)
|
||||||
|
barcode_asn_prefix: str = dataclasses.field(init=False)
|
||||||
|
barcode_upscale: float = dataclasses.field(init=False)
|
||||||
|
barcode_dpi: int = dataclasses.field(init=False)
|
||||||
|
barcode_max_pages: int = dataclasses.field(init=False)
|
||||||
|
barcode_enable_tag: bool = dataclasses.field(init=False)
|
||||||
|
barcode_tag_mapping: dict[str, str] = dataclasses.field(init=False)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
app_config = self._get_config_instance()
|
||||||
|
|
||||||
|
self.barcodes_enabled = (
|
||||||
|
app_config.barcodes_enabled or settings.CONSUMER_ENABLE_BARCODES
|
||||||
|
)
|
||||||
|
self.barcode_enable_tiff_support = (
|
||||||
|
app_config.barcode_enable_tiff_support
|
||||||
|
or settings.CONSUMER_BARCODE_TIFF_SUPPORT
|
||||||
|
)
|
||||||
|
self.barcode_string = (
|
||||||
|
app_config.barcode_string or settings.CONSUMER_BARCODE_STRING
|
||||||
|
)
|
||||||
|
self.barcode_retain_split_pages = (
|
||||||
|
app_config.barcode_retain_split_pages
|
||||||
|
or settings.CONSUMER_BARCODE_RETAIN_SPLIT_PAGES
|
||||||
|
)
|
||||||
|
self.barcode_enable_asn = (
|
||||||
|
app_config.barcode_enable_asn or settings.CONSUMER_ENABLE_ASN_BARCODE
|
||||||
|
)
|
||||||
|
self.barcode_asn_prefix = (
|
||||||
|
app_config.barcode_asn_prefix or settings.CONSUMER_ASN_BARCODE_PREFIX
|
||||||
|
)
|
||||||
|
self.barcode_upscale = (
|
||||||
|
app_config.barcode_upscale or settings.CONSUMER_BARCODE_UPSCALE
|
||||||
|
)
|
||||||
|
self.barcode_dpi = app_config.barcode_dpi or settings.CONSUMER_BARCODE_DPI
|
||||||
|
self.barcode_max_pages = (
|
||||||
|
app_config.barcode_max_pages or settings.CONSUMER_BARCODE_MAX_PAGES
|
||||||
|
)
|
||||||
|
self.barcode_enable_tag = (
|
||||||
|
app_config.barcode_enable_tag or settings.CONSUMER_ENABLE_TAG_BARCODE
|
||||||
|
)
|
||||||
|
self.barcode_tag_mapping = (
|
||||||
|
app_config.barcode_tag_mapping or settings.CONSUMER_TAG_BARCODE_MAPPING
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class GeneralConfig(BaseConfig):
|
class GeneralConfig(BaseConfig):
|
||||||
"""
|
"""
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
# Generated by Django 5.1.7 on 2025-04-02 19:21
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("paperless", "0003_alter_applicationconfiguration_max_image_pixels"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="barcode_asn_prefix",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
max_length=32,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Sets the ASN barcode prefix",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="barcode_dpi",
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
null=True,
|
||||||
|
validators=[django.core.validators.MinValueValidator(1)],
|
||||||
|
verbose_name="Sets the barcode DPI",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="barcode_enable_asn",
|
||||||
|
field=models.BooleanField(null=True, verbose_name="Enables ASN barcode"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="barcode_enable_tag",
|
||||||
|
field=models.BooleanField(null=True, verbose_name="Enables tag barcode"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="barcode_enable_tiff_support",
|
||||||
|
field=models.BooleanField(
|
||||||
|
null=True,
|
||||||
|
verbose_name="Enables barcode TIFF support",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="barcode_max_pages",
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
null=True,
|
||||||
|
validators=[django.core.validators.MinValueValidator(1)],
|
||||||
|
verbose_name="Sets the maximum pages for barcode",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="barcode_retain_split_pages",
|
||||||
|
field=models.BooleanField(null=True, verbose_name="Retains split pages"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="barcode_string",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
max_length=32,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Sets the barcode string",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="barcode_tag_mapping",
|
||||||
|
field=models.JSONField(
|
||||||
|
null=True,
|
||||||
|
verbose_name="Sets the tag barcode mapping",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="barcode_upscale",
|
||||||
|
field=models.FloatField(
|
||||||
|
null=True,
|
||||||
|
validators=[django.core.validators.MinValueValidator(1.0)],
|
||||||
|
verbose_name="Sets the barcode upscale factor",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="barcodes_enabled",
|
||||||
|
field=models.BooleanField(
|
||||||
|
null=True,
|
||||||
|
verbose_name="Enables barcode scanning",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -167,6 +167,10 @@ class ApplicationConfiguration(AbstractSingletonModel):
|
|||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Settings for the Paperless application
|
||||||
|
"""
|
||||||
|
|
||||||
app_title = models.CharField(
|
app_title = models.CharField(
|
||||||
verbose_name=_("Application title"),
|
verbose_name=_("Application title"),
|
||||||
null=True,
|
null=True,
|
||||||
@ -184,6 +188,83 @@ class ApplicationConfiguration(AbstractSingletonModel):
|
|||||||
upload_to="logo/",
|
upload_to="logo/",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Settings for the barcode scanner
|
||||||
|
"""
|
||||||
|
|
||||||
|
# PAPERLESS_CONSUMER_ENABLE_BARCODES
|
||||||
|
barcodes_enabled = models.BooleanField(
|
||||||
|
verbose_name=_("Enables barcode scanning"),
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# PAPERLESS_CONSUMER_BARCODE_TIFF_SUPPORT
|
||||||
|
barcode_enable_tiff_support = models.BooleanField(
|
||||||
|
verbose_name=_("Enables barcode TIFF support"),
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# PAPERLESS_CONSUMER_BARCODE_STRING
|
||||||
|
barcode_string = models.CharField(
|
||||||
|
verbose_name=_("Sets the barcode string"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
max_length=32,
|
||||||
|
)
|
||||||
|
|
||||||
|
# PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES
|
||||||
|
barcode_retain_split_pages = models.BooleanField(
|
||||||
|
verbose_name=_("Retains split pages"),
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE
|
||||||
|
barcode_enable_asn = models.BooleanField(
|
||||||
|
verbose_name=_("Enables ASN barcode"),
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# PAPERLESS_CONSUMER_ASN_BARCODE_PREFIX
|
||||||
|
barcode_asn_prefix = models.CharField(
|
||||||
|
verbose_name=_("Sets the ASN barcode prefix"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
max_length=32,
|
||||||
|
)
|
||||||
|
|
||||||
|
# PAPERLESS_CONSUMER_BARCODE_UPSCALE
|
||||||
|
barcode_upscale = models.FloatField(
|
||||||
|
verbose_name=_("Sets the barcode upscale factor"),
|
||||||
|
null=True,
|
||||||
|
validators=[MinValueValidator(1.0)],
|
||||||
|
)
|
||||||
|
|
||||||
|
# PAPERLESS_CONSUMER_BARCODE_DPI
|
||||||
|
barcode_dpi = models.PositiveIntegerField(
|
||||||
|
verbose_name=_("Sets the barcode DPI"),
|
||||||
|
null=True,
|
||||||
|
validators=[MinValueValidator(1)],
|
||||||
|
)
|
||||||
|
|
||||||
|
# PAPERLESS_CONSUMER_BARCODE_MAX_PAGES
|
||||||
|
barcode_max_pages = models.PositiveIntegerField(
|
||||||
|
verbose_name=_("Sets the maximum pages for barcode"),
|
||||||
|
null=True,
|
||||||
|
validators=[MinValueValidator(1)],
|
||||||
|
)
|
||||||
|
|
||||||
|
# PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE
|
||||||
|
barcode_enable_tag = models.BooleanField(
|
||||||
|
verbose_name=_("Enables tag barcode"),
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING
|
||||||
|
barcode_tag_mapping = models.JSONField(
|
||||||
|
verbose_name=_("Sets the tag barcode mapping"),
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("paperless application settings")
|
verbose_name = _("paperless application settings")
|
||||||
|
|
||||||
|
@ -185,11 +185,14 @@ class ProfileSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class ApplicationConfigurationSerializer(serializers.ModelSerializer):
|
class ApplicationConfigurationSerializer(serializers.ModelSerializer):
|
||||||
user_args = serializers.JSONField(binary=True, allow_null=True)
|
user_args = serializers.JSONField(binary=True, allow_null=True)
|
||||||
|
barcode_tag_mapping = serializers.JSONField(binary=True, allow_null=True)
|
||||||
|
|
||||||
def run_validation(self, data):
|
def run_validation(self, data):
|
||||||
# Empty strings treated as None to avoid unexpected behavior
|
# Empty strings treated as None to avoid unexpected behavior
|
||||||
if "user_args" in data and data["user_args"] == "":
|
if "user_args" in data and data["user_args"] == "":
|
||||||
data["user_args"] = None
|
data["user_args"] = None
|
||||||
|
if "barcode_tag_mapping" in data and data["barcode_tag_mapping"] == "":
|
||||||
|
data["barcode_tag_mapping"] = None
|
||||||
if "language" in data and data["language"] == "":
|
if "language" in data and data["language"] == "":
|
||||||
data["language"] = None
|
data["language"] = None
|
||||||
return super().run_validation(data)
|
return super().run_validation(data)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user