Model changes and Barcode consumer logic
This commit is contained in:
parent
55e799b833
commit
28b70e288a
@ -17,9 +17,19 @@ from documents.converters import convert_from_tiff_to_pdf
|
||||
from documents.data_models import DocumentSource
|
||||
from documents.utils import copy_basic_file_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")
|
||||
|
||||
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)
|
||||
class Barcode:
|
||||
@ -44,7 +54,10 @@ class Barcode:
|
||||
Returns True if the barcode value matches the configured ASN prefix,
|
||||
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:
|
||||
@ -78,7 +91,7 @@ class BarcodeReader:
|
||||
return self.mime in self.SUPPORTED_FILE_MIMES
|
||||
|
||||
@property
|
||||
def asn(self) -> Optional[int]:
|
||||
def asn(self) -> Optional[ASN]:
|
||||
"""
|
||||
Search the parsed barcodes for any ASNs.
|
||||
The first barcode that starts with CONSUMER_ASN_BARCODE_PREFIX
|
||||
@ -99,18 +112,32 @@ class BarcodeReader:
|
||||
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_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
|
||||
asn_text = re.sub("[^0-9]", "", asn_text)
|
||||
asn_value = re.sub("[^0-9]", "", asn_value)
|
||||
|
||||
# now, try parsing the ASN number
|
||||
try:
|
||||
asn = int(asn_text)
|
||||
asn_number = int(asn_value)
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to parse ASN number because: {e}")
|
||||
return None
|
||||
|
||||
return asn
|
||||
return ASN(prefix=asn_prefix,number=asn_number)
|
||||
|
||||
@staticmethod
|
||||
def read_barcodes_zxing(image: Image) -> list[str]:
|
||||
|
@ -34,6 +34,7 @@ from documents.models import DocumentType
|
||||
from documents.models import FileInfo
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.models import ASN
|
||||
from documents.parsers import DocumentParser
|
||||
from documents.parsers import ParseError
|
||||
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
|
||||
# otherwise, Whoosh can't handle it in the index
|
||||
if (
|
||||
self.override_asn < Document.ARCHIVE_SERIAL_NUMBER_MIN
|
||||
or self.override_asn > Document.ARCHIVE_SERIAL_NUMBER_MAX
|
||||
self.override_asn.number < Document.ARCHIVE_SERIAL_NUMBER_MIN
|
||||
or self.override_asn.number > Document.ARCHIVE_SERIAL_NUMBER_MAX
|
||||
):
|
||||
self._fail(
|
||||
ConsumerStatusShortMessage.ASN_RANGE,
|
||||
@ -184,10 +185,11 @@ class Consumer(LoggingMixin):
|
||||
f"[{Document.ARCHIVE_SERIAL_NUMBER_MIN:,}, "
|
||||
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(
|
||||
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):
|
||||
|
@ -15,6 +15,7 @@ from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import MaxValueValidator
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -129,6 +130,58 @@ class StoragePath(MatchingModel):
|
||||
verbose_name = _("storage path")
|
||||
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):
|
||||
STORAGE_TYPE_UNENCRYPTED = "unencrypted"
|
||||
@ -256,19 +309,14 @@ class Document(ModelWithOwner):
|
||||
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,
|
||||
null=True,
|
||||
unique=True,
|
||||
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.",
|
||||
),
|
||||
@ -574,6 +622,7 @@ class UiSettings(models.Model):
|
||||
return self.user.username
|
||||
|
||||
|
||||
|
||||
class PaperlessTask(models.Model):
|
||||
ALL_STATES = sorted(states.ALL_STATES)
|
||||
TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))
|
||||
|
Loading…
x
Reference in New Issue
Block a user