from typing import Iterable
from attrs import define
from django.db import models
from django.template import Template, loader
from django.templatetags.l10n import localize
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from ferrosoft.apps.ferrobase.models import UUIDModel, CommonLimit
from ferrosoft.apps.date_time import now_utc
from ferrosoft.apps.ferrobase.models.fields import NormalDecimalField
from ferrosoft.apps.ferrobase.templating import template_from_string
[docs]
class ExaminationRequest(UUIDModel):
"""
Unvalidated examination request for debugging purposes.
"""
creation_date = models.DateTimeField(
verbose_name=_("Creation date"),
db_index=True,
)
weighing_slip_id = models.CharField(
verbose_name=_("Weighing slip ID"),
db_index=True,
)
payload = models.JSONField(
verbose_name=_("Payload"),
)
def __str__(self):
return _("Examination request %(weighing_slip_id)s at %(date)s") % {
"weighing_slip_id": self.weighing_slip_id,
"date": localize(self.creation_date),
}
[docs]
def get_absolute_url(self):
return reverse("befundung:replay_request", kwargs={"pk": self.pk})
[docs]
class ExaminationPhotoRequest(UUIDModel):
"""Unvalidated examination photo request for debugging purposes."""
request = models.ForeignKey(
ExaminationRequest,
verbose_name=_("Request"),
related_name="photos",
on_delete=models.CASCADE,
)
payload = models.JSONField(
verbose_name=_("Payload"),
)
[docs]
class ExaminationManager(models.Manager):
[docs]
def get_queryset(self):
return (
super().get_queryset().prefetch_related("refusals", "reductions", "photos")
)
[docs]
class Examination(UUIDModel):
objects = ExaminationManager()
weighing_slip_id = models.CharField(
max_length=int(CommonLimit.TINY_CHAR),
verbose_name=_("Weighing slip ID"),
)
trader_approval = models.CharField(
max_length=int(CommonLimit.TINY_CHAR),
verbose_name=_("Token of approving trader"),
blank=True,
default="",
)
comment = models.TextField(
verbose_name=_("Comment"),
default="",
blank=True,
)
exam_date = models.CharField(
max_length=int(CommonLimit.TINY_CHAR),
verbose_name=_("Exam date (localized format)"),
)
material = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("Material name"),
)
license_plate_no = models.CharField(
max_length=int(CommonLimit.TINY_CHAR),
verbose_name=_("License plate number"),
blank=True,
default="",
)
auditor_token = models.CharField(
max_length=int(CommonLimit.TINY_CHAR),
verbose_name=_("Token of auditor"),
)
customer_name = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("Customer name"),
)
refused_material = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("Refused material"),
blank=True,
default="",
)
[docs]
class Reduction(UUIDModel):
[docs]
class Type(models.TextChoices):
CUSTOM = "custom", _("custom")
WATER = "water", _("water")
DIRT = "dirt", _("dirt")
JUNK = "junk", _("junk")
GARBAGE = "garbage", _("garbage")
RUBBER = "rubber", _("rubber")
examination = models.ForeignKey(
Examination,
on_delete=models.CASCADE,
related_name="reductions",
verbose_name=_("Examination"),
)
reduction_type = models.CharField(
max_length=10,
choices=Type.choices,
verbose_name=_("Reduction type"),
)
label = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
default="",
blank=True,
verbose_name=_("Reduction label"),
)
value = NormalDecimalField(
verbose_name=_("Reduction value"),
)
[docs]
class Refusal(UUIDModel):
class Meta:
constraints = [
models.UniqueConstraint(
fields=["examination", "material_name"], name="uniq_refusal"
)
]
examination = models.ForeignKey(
Examination,
on_delete=models.CASCADE,
related_name="refusals",
verbose_name=_("Examination"),
)
material_name = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("Material name"),
)
value = NormalDecimalField(verbose_name=_("Value"))
[docs]
class Photo(UUIDModel):
examination = models.ForeignKey(
Examination,
on_delete=models.CASCADE,
related_name="photos",
verbose_name=_("Photos"),
)
image_file = models.FileField(
upload_to="examination/%Y/%m/%d/", verbose_name=_("Photo image file")
)
[docs]
class Log(UUIDModel):
date = models.DateTimeField(
verbose_name=_("Date"),
)
message = models.TextField(
verbose_name=_("Message"),
)
[docs]
def create_log(message: str):
Log.objects.create(
date=now_utc(),
message=message,
)
[docs]
class Settings(UUIDModel):
email_subject_template = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("Email subject template"),
help_text=_("Subject template of examination report email"),
default="Examination report from {exam_date} regarding {weighing_slip_id} of {customer_name}",
)
email_body_template = models.TextField(
verbose_name=_("Email body template"),
help_text=_("Content template of examination report email"),
default="",
blank=True,
)
email_sender_address = models.CharField(
max_length=int(CommonLimit.TINY_CHAR),
verbose_name=_("Email sender address"),
help_text=_("Sender address of examination report email"),
default="noreply@localhost",
)
email_recipient_address = models.CharField(
max_length=int(CommonLimit.TINY_CHAR),
verbose_name=_("Email recipient address"),
help_text=_("Recipient address of examination report email"),
default="root@localhost",
)
email_pdf_attachment_name_template = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("PDF attachment name template"),
help_text=_("PDF attachment name template of examination report email"),
default="{weighing_slip_id},03.pdf",
)
email_photo_attachment_name_template = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("Photo attachment name template"),
help_text=_(
"File name template of picture attachments. special variables: count (counter, z.B. 1); file_ext (file extension, e.g. png)"
),
default="{weighing_slip_id}_{count}.{file_ext}",
)
template_content = models.FileField(
verbose_name=_("Template content"),
upload_to="examination_template/",
default=None,
null=True,
blank=True,
)
template_logo_url = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("Template logo URL"),
help_text=_("Picture URL of company logo"),
default="http://localhost:8001/befundung/logo.webp",
)
template_logo_height = models.CharField(
max_length=int(CommonLimit.TINY_CHAR),
verbose_name=_("Template logo height"),
help_text=_("Height of company logo (width scales proportionally)"),
default="140px",
)
template_show_images = models.BooleanField(
verbose_name=_("Show images"),
help_text=_("Enable or disable image page"),
default=True,
)
template_show_reductions = models.BooleanField(
verbose_name=_("Show reductions"),
help_text=_("Show or hide reductions block"),
default=True,
)
template_reduction_label_water = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("Reduction label water"),
help_text=_('Name of reduction value "water"'),
default="water, snow, moisture",
)
template_reduction_label_dirt = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("Reduction label dirt"),
help_text=_('Name of reduction value "dirt"'),
default="wood, stones, earth, dirt",
)
template_reduction_label_junk = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("Reduction label junk"),
help_text=_('Name of reduction value "junk"'),
default="junk",
)
template_reduction_label_garbage = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("Reduction label garbage"),
help_text=_('Name of reduction value "garbage"'),
default="rubble, dirt, garbage, plastic, sinter, cinder",
)
template_reduction_label_rubber = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("Reduction label rubber"),
help_text=_('Name of reduction value "rubber"'),
default="tires, rubber",
)
sftp_enable_image_upload = models.BooleanField(
verbose_name=_("Enable image upload"),
help_text=_("Upload images to SFTP server?"),
default=False,
)
sftp_enable_document_upload = models.BooleanField(
verbose_name=_("Enable document upload"),
help_text=_("Upload documents to SFTP server?"),
default=False,
)
sftp_hostname = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("SFTP hostname"),
help_text=_("Hostname of SFTP server"),
default="",
blank=True,
)
sftp_username = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("SFTP username"),
help_text=_("Username for SFTP connection"),
default="",
blank=True,
)
sftp_port = models.IntegerField(
verbose_name=_("SFTP port"),
help_text=_("SFTP port number"),
default=22,
)
sftp_remote_dir = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("SFTP remote directory"),
help_text=_("SFTP remote directory where files are uploaded to"),
default="/share/befundung/current",
)
sftp_client_key_password = models.CharField(
max_length=int(CommonLimit.NORMAL_CHAR),
verbose_name=_("SFTP private key password"),
help_text=_("Password for encrypted client private key"),
default=None,
null=True,
blank=True,
)
sftp_client_key = models.TextField(
verbose_name=_("Client private key"),
help_text=_("RSA client private key for SFTP connection"),
default="",
blank=True,
)
sftp_host_key = models.TextField(
verbose_name=_("Host public key"),
help_text=_(
"RSA host public key for host authentication (can be obtained with e.g. ssh-keyscan)"
),
default="",
blank=True,
)
[docs]
@classmethod
def get_instance(cls) -> "Settings":
it = cls.objects.first()
if it is None:
return cls.objects.create()
return it
[docs]
def get_template(self) -> Template:
if self.template_content:
content = self.template_content.read()
return template_from_string(content.decode("utf-8"))
else:
return loader.get_template("befundung/index.html")
[docs]
@define
class Report:
examination: dict
"""Serialized representation of Examination model.
This is not the Examination model for historic reasons."""
images: Iterable[TransformedPhoto]