it('should support JSON validation for e.g. user_args', () => {
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" }' })
- expect(component.errors).toEqual({ user_args: null })
+ expect(component.errors['user_args']).toBeNull()
})
it('should upload file, show error if necessary', () => {
export const ConfigCategory = {
General: $localize`General Settings`,
OCR: $localize`OCR Settings`,
+ Barcode: $localize`Barcode Settings`,
}
export interface ConfigOption {
config_key: 'PAPERLESS_APP_TITLE',
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 {
user_args: object
app_logo: 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
}
from documents.converters import convert_from_tiff_to_pdf
from documents.data_models import ConsumableDocument
+from documents.data_models import DocumentMetadataOverrides
from documents.models import Tag
from documents.plugins.base import ConsumeTaskPlugin
from documents.plugins.base import StopConsumeTaskError
+from documents.plugins.helpers import ProgressManager
from documents.plugins.helpers import ProgressStatusOptions
from documents.utils import copy_basic_file_stats
from documents.utils import copy_file_with_basic_stats
from documents.utils import maybe_override_pixel_limit
+from paperless.config import BarcodeConfig
if TYPE_CHECKING:
from collections.abc import Callable
page: int
value: str
+ settings: BarcodeConfig
@property
def is_separator(self) -> bool:
Returns True if the barcode value equals the configured separation value,
False otherwise
"""
- return self.value == settings.CONSUMER_BARCODE_STRING
+ return self.value == self.settings.barcode_string
@property
def is_asn(self) -> bool:
Returns True if the barcode value matches the configured ASN prefix,
False otherwise
"""
- return self.value.startswith(settings.CONSUMER_ASN_BARCODE_PREFIX)
+ return self.value.startswith(self.settings.barcode_asn_prefix)
class BarcodePlugin(ConsumeTaskPlugin):
- ASN from barcode detection is enabled or
- 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"}
else:
supported_mimes = {"application/pdf"}
return (
- settings.CONSUMER_ENABLE_ASN_BARCODE
- or settings.CONSUMER_ENABLE_BARCODES
- or settings.CONSUMER_ENABLE_TAG_BARCODE
+ self.settings.barcode_enable_asn
+ or self.settings.barcodes_enabled
+ or self.settings.barcode_enable_tag
) 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:
self.temp_dir = tempfile.TemporaryDirectory(
dir=self.base_tmp_dir,
# try reading tags from barcodes
if (
- settings.CONSUMER_ENABLE_TAG_BARCODE
+ self.settings.barcode_enable_tag
and (tags := self.tags) is not None
and len(tags) > 0
):
logger.info(f"Found tags in barcode: {tags}")
# Lastly attempt to split documents
- if settings.CONSUMER_ENABLE_BARCODES and (
+ if self.settings.barcodes_enabled and (
separator_pages := self.get_separation_pages()
):
# We have pages to split against
# Update/overwrite an ASN if possible
# After splitting, as otherwise each split document gets the same ASN
- if (
- settings.CONSUMER_ENABLE_ASN_BARCODE
- and (located_asn := self.asn) is not None
- ):
+ if self.settings.barcode_enable_asn and (located_asn := self.asn) is not None:
logger.info(f"Found ASN in barcode: {located_asn}")
self.metadata.asn = located_asn
# Get limit from configuration
barcode_max_pages: int = (
num_of_pages
- if settings.CONSUMER_BARCODE_MAX_PAGES == 0
- else settings.CONSUMER_BARCODE_MAX_PAGES
+ if self.settings.barcode_max_pages == 0
+ else self.settings.barcode_max_pages
)
if barcode_max_pages < num_of_pages: # pragma: no cover
# Convert page to image
page = convert_from_path(
self.pdf_file,
- dpi=settings.CONSUMER_BARCODE_DPI,
+ dpi=self.settings.barcode_dpi,
output_folder=self.temp_dir.name,
first_page=current_page_number + 1,
last_page=current_page_number + 1,
logger.debug(f"Image is at {page_filepath}")
# Upscale image if configured
- factor = settings.CONSUMER_BARCODE_UPSCALE
+ factor = self.settings.barcode_upscale
if factor > 1.0:
logger.debug(
f"Upscaling image by {factor} for better barcode detection",
# Detect barcodes
for barcode_value in reader(page):
self.barcodes.append(
- Barcode(current_page_number, barcode_value),
+ Barcode(current_page_number, barcode_value, self.settings),
)
# Delete temporary image file
def asn(self) -> int | None:
"""
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.
Returns the detected ASN (or None)
"""
# Ensure the barcodes have been read
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(
(x.value for x in self.barcodes if x.is_asn),
None,
if asn_text:
logger.debug(f"Found ASN Barcode: {asn_text}")
# 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
asn_text = re.sub(r"\D", "", asn_text)
for raw in tag_texts.split(","):
try:
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):
- sub = settings.CONSUMER_TAG_BARCODE_MAPPING[regex]
+ sub = self.settings.barcode_tag_mapping[regex]
tag_str = (
re.sub(regex, sub, raw, flags=re.IGNORECASE)
if sub
"""
# filter all barcodes for the separator string
# get the page numbers of the separating barcodes
- retain = settings.CONSUMER_BARCODE_RETAIN_SPLIT_PAGES
+ retain = self.settings.barcode_retain_split_pages
separator_pages = {
bc.page: retain
for bc in self.barcodes
if bc.is_separator and (not retain or (retain and bc.page > 0))
} # 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
# add the page numbers of the ASN barcodes
self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(
- json.dumps(response.data[0]),
- json.dumps(
- {
- "id": 1,
- "user_args": None,
- "output_type": None,
- "pages": None,
- "language": None,
- "mode": None,
- "skip_archive_file": None,
- "image_dpi": None,
- "unpaper_clean": None,
- "deskew": None,
- "rotate_pages": None,
- "rotate_pages_threshold": None,
- "max_image_pixels": None,
- "color_conversion_strategy": None,
- "app_title": None,
- "app_logo": None,
- },
- ),
+ self.maxDiff = None
+
+ self.assertDictEqual(
+ response.data[0],
+ {
+ "id": 1,
+ "output_type": None,
+ "pages": None,
+ "language": None,
+ "mode": None,
+ "skip_archive_file": None,
+ "image_dpi": None,
+ "unpaper_clean": None,
+ "deskew": None,
+ "rotate_pages": None,
+ "rotate_pages_threshold": None,
+ "max_image_pixels": None,
+ "color_conversion_strategy": None,
+ "user_args": None,
+ "app_title": 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):
{
"user_args": "",
"language": "",
+ "barcode_tag_mapping": "",
},
),
content_type="application/json",
config = ApplicationConfiguration.objects.first()
self.assertEqual(config.user_args, None)
self.assertEqual(config.language, None)
+ self.assertEqual(config.barcode_tag_mapping, None)
def test_api_replace_app_logo(self):
"""
from documents.tests.utils import DummyProgressManager
from documents.tests.utils import FileSystemAssertsMixin
from documents.tests.utils import SampleDirMixin
+from paperless.models import ApplicationConfiguration
try:
import zxingcpp # noqa: F401
},
)
+ 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")
class TestBarcodeNewConsume(
user_args = json.loads(settings.OCR_USER_ARGS)
except json.JSONDecodeError:
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
class GeneralConfig(BaseConfig):
"""
--- /dev/null
+# 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",
+ ),
+ ),
+ ]
null=True,
)
+ """
+ Settings for the Paperless application
+ """
+
app_title = models.CharField(
verbose_name=_("Application title"),
null=True,
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:
verbose_name = _("paperless application settings")
class ApplicationConfigurationSerializer(serializers.ModelSerializer):
user_args = serializers.JSONField(binary=True, allow_null=True)
+ barcode_tag_mapping = serializers.JSONField(binary=True, allow_null=True)
def run_validation(self, data):
# Empty strings treated as None to avoid unexpected behavior
if "user_args" in data and data["user_args"] == "":
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"] == "":
data["language"] = None
return super().run_validation(data)