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.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]:
|
||||||
|
@ -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):
|
||||||
|
@ -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))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user