Source code for ferrosoft.apps.befundung.models

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] def format_report_pdf_name(self, report: Report) -> str: return self.email_pdf_attachment_name_template.format(**report.examination)
[docs] def format_image_name(self, report: Report, count: int, media_type: str) -> str: if media_type == "image/png": ext = "png" elif media_type == "image/jpeg": ext = "jpg" else: raise ValueError("unsupported media type %s" % media_type) return self.email_photo_attachment_name_template.format( **report.examination | { "count": count, "file_ext": ext, } )
[docs] @define class TransformedPhoto: media_type: str binary_data: bytes encoded_data: str
[docs] @define class Report: examination: dict """Serialized representation of Examination model. This is not the Examination model for historic reasons.""" images: Iterable[TransformedPhoto]