Model changes and Barcode consumer logic

This commit is contained in:
Florian Huber 2023-11-02 15:31:34 +00:00
parent 55e799b833
commit 28b70e288a
3 changed files with 98 additions and 20 deletions

View File

@ -17,9 +17,19 @@ from documents.converters import convert_from_tiff_to_pdf
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
from documents.models import ASNPrefix
from documents.models import ASN
logger = logging.getLogger("paperless.barcodes") logger = logging.getLogger("paperless.barcodes")
ASNPrefixes = list(ASNPrefix.objects.all())
@receiver(post_save, sender=ASNPrefix)
@receiver(post_delete, sender=ASNPrefix)
def update_asn_prefix_list(sender, instance, **kwargs):
global ASNPrefixes
ASNPrefixes = list(ASNPrefix.objects.all())
@dataclass(frozen=True) @dataclass(frozen=True)
class Barcode: class Barcode:
@ -44,7 +54,10 @@ 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) for ASNPrefix in ASNPrefixes:
if self.value.startswith(ASNPrefix.prefix):
return True
return False
class BarcodeReader: class BarcodeReader:
@ -78,7 +91,7 @@ class BarcodeReader:
return self.mime in self.SUPPORTED_FILE_MIMES return self.mime in self.SUPPORTED_FILE_MIMES
@property @property
def asn(self) -> Optional[int]: def asn(self) -> Optional[ASN]:
""" """
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 CONSUMER_ASN_BARCODE_PREFIX
@ -99,18 +112,32 @@ class BarcodeReader:
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_prefix = None
asn_prefix_len = 0
for prefix in ASNPrefixes:
if asn_text.startswith(prefix.prefix) and len(prefix.prefix) > asn_prefix_len:
asn_prefix = prefix
asn_prefix_len = len(prefix.prefix)
if asn_prefix == None:
logger.warning("Failed to parse ASN Prefix")
return None
asn_value = asn_text[asn_prefix_len :].strip()
# remove non-numeric parts of the remaining string # remove non-numeric parts of the remaining string
asn_text = re.sub("[^0-9]", "", asn_text) asn_value = re.sub("[^0-9]", "", asn_value)
# now, try parsing the ASN number # now, try parsing the ASN number
try: try:
asn = int(asn_text) asn_number = int(asn_value)
except ValueError as e: except ValueError as e:
logger.warning(f"Failed to parse ASN number because: {e}") logger.warning(f"Failed to parse ASN number because: {e}")
return None
return asn return ASN(prefix=asn_prefix,number=asn_number)
@staticmethod @staticmethod
def read_barcodes_zxing(image: Image) -> list[str]: def read_barcodes_zxing(image: Image) -> list[str]:

View File

@ -34,6 +34,7 @@ from documents.models import DocumentType
from documents.models import FileInfo from documents.models import FileInfo
from documents.models import StoragePath from documents.models import StoragePath
from documents.models import Tag from documents.models import Tag
from documents.models import ASN
from documents.parsers import DocumentParser from documents.parsers import DocumentParser
from documents.parsers import ParseError from documents.parsers import ParseError
from documents.parsers import get_parser_class_for_mime_type from documents.parsers import get_parser_class_for_mime_type
@ -174,8 +175,8 @@ class Consumer(LoggingMixin):
# Validate the range is above zero and less than uint32_t max # Validate the range is above zero and less than uint32_t max
# otherwise, Whoosh can't handle it in the index # otherwise, Whoosh can't handle it in the index
if ( if (
self.override_asn < Document.ARCHIVE_SERIAL_NUMBER_MIN self.override_asn.number < Document.ARCHIVE_SERIAL_NUMBER_MIN
or self.override_asn > Document.ARCHIVE_SERIAL_NUMBER_MAX or self.override_asn.number > Document.ARCHIVE_SERIAL_NUMBER_MAX
): ):
self._fail( self._fail(
ConsumerStatusShortMessage.ASN_RANGE, ConsumerStatusShortMessage.ASN_RANGE,
@ -184,10 +185,11 @@ class Consumer(LoggingMixin):
f"[{Document.ARCHIVE_SERIAL_NUMBER_MIN:,}, " f"[{Document.ARCHIVE_SERIAL_NUMBER_MIN:,}, "
f"{Document.ARCHIVE_SERIAL_NUMBER_MAX:,}]", f"{Document.ARCHIVE_SERIAL_NUMBER_MAX:,}]",
) )
if Document.objects.filter(archive_serial_number=self.override_asn).exists():
if ASN.objects.filter(prefix=self.override_asn.prefix, number=self.override_asn.number).exists():
self._fail( self._fail(
ConsumerStatusShortMessage.ASN_ALREADY_EXISTS, ConsumerStatusShortMessage.ASN_ALREADY_EXISTS,
f"Not consuming {self.filename}: Given ASN already exists!", f"Not consuming {self.filename}: Given ASN already exists for the Prefix {self.override_asn.prefix.prefix}!",
) )
def run_pre_consume_script(self): def run_pre_consume_script(self):

View File

@ -15,6 +15,7 @@ from django.contrib.auth.models import Group
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.validators import MaxValueValidator from django.core.validators import MaxValueValidator
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.core.validators import MinLengthValidator
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -129,6 +130,58 @@ class StoragePath(MatchingModel):
verbose_name = _("storage path") verbose_name = _("storage path")
verbose_name_plural = _("storage paths") verbose_name_plural = _("storage paths")
class ASNPrefix(models.Model):
prefix = models.CharField(_("ASN-Prefix"), max_length=16, blank=False, primary_key=True, validators=[MinLengthValidator(2)])
description = models.CharField(_("Description", max_length=512, blank=True))
def __str__(self):
return f"{self.prefix.prefix}"
class ASN(models.Model):
prefix = models.ForeignKey(
ASNPrefix,
blank = False,
on_delete = models.CASCADE # TODO: Not shure about that one
)
ARCHIVE_SERIAL_NUMBER_MIN: Final[int] = 0
ARCHIVE_SERIAL_NUMBER_MAX: Final[int] = 0xFF_FF_FF_FF
number = models.PositiveIntegerField(
_("archive serial number"),
blank=True,
null=True,
unique=False,
db_index=True,
validators=[
MaxValueValidator(ARCHIVE_SERIAL_NUMBER_MAX),
MinValueValidator(ARCHIVE_SERIAL_NUMBER_MIN),
],
help_text=_(
"The position of this document in your physical document archive.",
),
)
class Meta:
unique_together = ('prefix', 'number')
def __str__(self):
return f"{self.prefix.prefix}-{self.number:06d}"
@staticmethod
def get_max_number_for_prefix(prefix):
try:
max_number = ASN.objects.filter(prefix=prefix).aggregate(models.Max('number'))['number__max']
return max_number if max_number is not None else 0
except ASN.DoesNotExist:
return 0
@staticmethod
def get_next_ASN_for_prefix(prefix):
return ASN(prefix, ASN.get_max_number_for_prefix(prefix)+1)
class Document(ModelWithOwner): class Document(ModelWithOwner):
STORAGE_TYPE_UNENCRYPTED = "unencrypted" STORAGE_TYPE_UNENCRYPTED = "unencrypted"
@ -256,19 +309,14 @@ class Document(ModelWithOwner):
help_text=_("The original name of the file when it was uploaded"), help_text=_("The original name of the file when it was uploaded"),
) )
ARCHIVE_SERIAL_NUMBER_MIN: Final[int] = 0
ARCHIVE_SERIAL_NUMBER_MAX: Final[int] = 0xFF_FF_FF_FF
archive_serial_number = models.PositiveIntegerField(
_("archive serial number"), archive_serial_number = models.OneToOneField(
to=ASN,
on_delete=models.CASCADE,
verbose_name=_("archive serial number"),
blank=True, blank=True,
null=True, null=True,
unique=True,
db_index=True,
validators=[
MaxValueValidator(ARCHIVE_SERIAL_NUMBER_MAX),
MinValueValidator(ARCHIVE_SERIAL_NUMBER_MIN),
],
help_text=_( help_text=_(
"The position of this document in your physical document archive.", "The position of this document in your physical document archive.",
), ),
@ -574,6 +622,7 @@ class UiSettings(models.Model):
return self.user.username return self.user.username
class PaperlessTask(models.Model): class PaperlessTask(models.Model):
ALL_STATES = sorted(states.ALL_STATES) ALL_STATES = sorted(states.ALL_STATES)
TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES)) TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))