From 117a4437fea490e88e3a2370a9c5d4b9881992b1 Mon Sep 17 00:00:00 2001
From: mpeltriaux <Michel.Peltriaux@sgdnord.rlp.de>
Date: Mon, 15 Aug 2022 08:08:15 +0200
Subject: [PATCH 1/4] Model

* adds new model and mixin
* adds new functionality for Mailer class for sending resubmission mails
---
 compensation/models/compensation.py           |  7 ++-
 intervention/models/intervention.py           | 11 +++--
 konova/models/__init__.py                     |  1 +
 konova/models/object.py                       | 22 ++++++++-
 konova/models/resubmission.py                 | 46 +++++++++++++++++++
 konova/utils/mailer.py                        | 23 ++++++++++
 .../email/resubmission/resubmission.html      | 29 ++++++++++++
 7 files changed, 133 insertions(+), 6 deletions(-)
 create mode 100644 konova/models/resubmission.py
 create mode 100644 templates/email/resubmission/resubmission.html

diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py
index e513c95c..b65d259e 100644
--- a/compensation/models/compensation.py
+++ b/compensation/models/compensation.py
@@ -19,14 +19,17 @@ from compensation.managers import CompensationManager
 from compensation.models import CompensationState, CompensationAction
 from compensation.utils.quality import CompensationQualityChecker
 from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \
-    GeoReferencedMixin, DeadlineType
+    GeoReferencedMixin, DeadlineType, ResubmitableObjectMixin
 from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \
     DOCUMENT_REMOVED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \
     COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
 from user.models import UserActionLogEntry
 
 
-class AbstractCompensation(BaseObject, GeoReferencedMixin):
+class AbstractCompensation(BaseObject,
+                           GeoReferencedMixin,
+                           ResubmitableObjectMixin
+                           ):
     """
     Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
     EMA or EcoAccount.
diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py
index dd15beb1..ea561c5b 100644
--- a/intervention/models/intervention.py
+++ b/intervention/models/intervention.py
@@ -26,14 +26,19 @@ from intervention.models.revocation import RevocationDocument, Revocation
 from intervention.utils.quality import InterventionQualityChecker
 from konova.models import generate_document_file_upload_path, AbstractDocument, BaseObject, \
     ShareableObjectMixin, \
-    RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin
-from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP
+    RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin, ResubmitableObjectMixin
 from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE, \
     PAYMENT_REMOVED, PAYMENT_ADDED, REVOCATION_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
 from user.models import UserActionLogEntry
 
 
-class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin):
+class Intervention(BaseObject,
+                   ShareableObjectMixin,
+                   RecordableObjectMixin,
+                   CheckableObjectMixin,
+                   GeoReferencedMixin,
+                   ResubmitableObjectMixin
+                   ):
     """
     Interventions are e.g. construction sites where nature used to be.
     """
diff --git a/konova/models/__init__.py b/konova/models/__init__.py
index c9156061..ba9de1d5 100644
--- a/konova/models/__init__.py
+++ b/konova/models/__init__.py
@@ -10,3 +10,4 @@ from .deadline import *
 from .document import *
 from .geometry import *
 from .parcel import *
+from .resubmission import *
diff --git a/konova/models/object.py b/konova/models/object.py
index b468932a..0fbd6e8b 100644
--- a/konova/models/object.py
+++ b/konova/models/object.py
@@ -743,4 +743,24 @@ class GeoReferencedMixin(models.Model):
             zoom_lvl,
             x,
             y,
-        )
\ No newline at end of file
+        )
+
+
+class ResubmitableObjectMixin(models.Model):
+    resubmissions = models.ManyToManyField(
+        "konova.Resubmission",
+        null=True,
+        blank=True,
+        related_name="+",
+    )
+
+    class Meta:
+        abstract = True
+
+    def resubmit(self):
+        """ Run resubmit check and run for all related resubmissions
+
+        """
+        resubmissions = self.resubmissions.all()
+        for resubmission in resubmissions:
+            resubmission.send_resubmission_mail(self.identifier)
diff --git a/konova/models/resubmission.py b/konova/models/resubmission.py
new file mode 100644
index 00000000..ca97ebbf
--- /dev/null
+++ b/konova/models/resubmission.py
@@ -0,0 +1,46 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+from dateutil.utils import today
+from django.db import models
+
+from konova.models import BaseResource
+from konova.utils.mailer import Mailer
+
+
+class Resubmission(BaseResource):
+    user = models.ForeignKey(
+        "user.User",
+        on_delete=models.CASCADE,
+        help_text="The user who wants to be notifed"
+    )
+    resubmit_on = models.DateField(
+        help_text="On which date the resubmission should be performed"
+    )
+    resubmission_sent = models.BooleanField(
+        default=False,
+        help_text="Whether a resubmission has been sent or not"
+    )
+    comment = models.TextField(
+        null=True,
+        blank=True,
+        help_text="Optional comment for the user itself"
+    )
+
+    def send_resubmission_mail(self, obj_identifier):
+        """ Sends a resubmission mail
+
+        """
+        _today = today()
+        resubmission_handled = _today.__ge__(self.resubmit_on) and self.resubmission_sent
+        if resubmission_handled:
+            return
+
+        mailer = Mailer()
+        mailer.send_mail_resubmission(obj_identifier, self)
+        self.resubmission_sent = True
+        self.save()
diff --git a/konova/utils/mailer.py b/konova/utils/mailer.py
index 92bd2b60..8de91198 100644
--- a/konova/utils/mailer.py
+++ b/konova/utils/mailer.py
@@ -398,3 +398,26 @@ class Mailer:
             msg
         )
 
+    def send_mail_resubmission(self, obj_identifier, resubmission):
+        """ Send a resubmission mail for a user
+
+        Args:
+            obj_identifier (str): The (resubmitted) object's identifier
+            resubmission (Resubmission): The resubmission
+
+        Returns:
+
+        """
+        context = {
+            "obj_identifier": obj_identifier,
+            "resubmission": resubmission,
+            "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
+        }
+        msg = render_to_string("email/resubmission/resubmission.html", context)
+        user_mail_address = [SUPPORT_MAIL_RECIPIENT]
+        self.send(
+            user_mail_address,
+            _("Resubmission - {}").format(obj_identifier),
+            msg
+        )
+
diff --git a/templates/email/resubmission/resubmission.html b/templates/email/resubmission/resubmission.html
new file mode 100644
index 00000000..25848f55
--- /dev/null
+++ b/templates/email/resubmission/resubmission.html
@@ -0,0 +1,29 @@
+{% load i18n %}
+
+<div>
+    <h2>{% trans 'Resubmission' %}</h2>
+    <h4>{{obj_identifier}}</h4>
+    <hr>
+    <article>
+        {% trans 'Hello ' %} {{resubmission.user.username}},
+        <br>
+        <br>
+        {% trans 'you wanted to be reminded on this entry.' %}
+        <br>
+        {% if resubmission.comment %}
+            <br>
+            {% trans 'Your personal comment:' %}
+            <br>
+            <article style="font: italic">"{{resubmission.comment}}"</article>
+        {% endif %}
+        <br>
+        <br>
+        {% trans 'Best regards' %}
+        <br>
+        KSP
+        <br>
+        <br>
+        {% include 'email/signature.html' %}
+    </article>
+</div>
+

From 4f02e8ee1b336efcb99eb08a7aca87d537ed7611 Mon Sep 17 00:00:00 2001
From: mpeltriaux <Michel.Peltriaux@sgdnord.rlp.de>
Date: Mon, 15 Aug 2022 09:38:51 +0200
Subject: [PATCH 2/4] Templates + Routes

* adds control button for Intervention, Compensation, Ema and EcoAccount for setting a resubmission on an entry
---
 .../compensation/includes/controls.html       |  3 +
 .../detail/eco_account/includes/controls.html |  3 +
 compensation/urls/compensation.py             |  1 +
 compensation/urls/eco_account.py              |  1 +
 compensation/views/compensation.py            | 26 ++++++-
 compensation/views/eco_account.py             | 25 ++++++-
 .../ema/detail/includes/controls.html         |  3 +
 ema/urls.py                                   |  1 +
 ema/views.py                                  | 27 ++++++-
 .../detail/includes/controls.html             |  3 +
 intervention/urls.py                          |  4 +-
 intervention/views.py                         | 25 ++++++-
 konova/admin.py                               | 12 ++-
 konova/forms.py                               | 74 ++++++++++++++++++-
 14 files changed, 200 insertions(+), 8 deletions(-)

diff --git a/compensation/templates/compensation/detail/compensation/includes/controls.html b/compensation/templates/compensation/detail/compensation/includes/controls.html
index 5be0b3e8..4119480e 100644
--- a/compensation/templates/compensation/detail/compensation/includes/controls.html
+++ b/compensation/templates/compensation/detail/compensation/includes/controls.html
@@ -12,6 +12,9 @@
         </button>
     </a>
     {% if has_access %}
+        <button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:resubmission-create' obj.id %}">
+            {% fa5_icon 'bell' %}
+        </button>
         {% if is_default_member %}
         <a href="{% url 'compensation:edit' obj.id %}" class="mr-2">
             <button class="btn btn-default" title="{% trans 'Edit' %}">
diff --git a/compensation/templates/compensation/detail/eco_account/includes/controls.html b/compensation/templates/compensation/detail/eco_account/includes/controls.html
index fcc70dc2..42ce6067 100644
--- a/compensation/templates/compensation/detail/eco_account/includes/controls.html
+++ b/compensation/templates/compensation/detail/eco_account/includes/controls.html
@@ -12,6 +12,9 @@
         </button>
     </a>
     {% if has_access %}
+        <button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:acc:resubmission-create' obj.id %}">
+            {% fa5_icon 'bell' %}
+        </button>
         <button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'compensation:acc:share-create' obj.id %}">
             {% fa5_icon 'share-alt' %}
         </button>
diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py
index e1a41ff2..66020055 100644
--- a/compensation/urls/compensation.py
+++ b/compensation/urls/compensation.py
@@ -31,6 +31,7 @@ urlpatterns = [
     path('<id>/deadline/<deadline_id>/edit', deadline_edit_view, name='deadline-edit'),
     path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'),
     path('<id>/report', report_view, name='report'),
+    path('<id>/resub', create_resubmission_view, name='resubmission-create'),
 
     # Documents
     path('<id>/document/new/', new_document_view, name='new-doc'),
diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py
index a3d1aa38..5a84e8ca 100644
--- a/compensation/urls/eco_account.py
+++ b/compensation/urls/eco_account.py
@@ -19,6 +19,7 @@ urlpatterns = [
     path('<id>/report', report_view, name='report'),
     path('<id>/edit', edit_view, name='edit'),
     path('<id>/remove', remove_view, name='remove'),
+    path('<id>/resub', create_resubmission_view, name='resubmission-create'),
 
     path('<id>/state/new', state_new_view, name='new-state'),
     path('<id>/state/<state_id>/edit', state_edit_view, name='state-edit'),
diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py
index efe51ce3..016f8ea9 100644
--- a/compensation/views/compensation.py
+++ b/compensation/views/compensation.py
@@ -14,7 +14,8 @@ from compensation.tables import CompensationTable
 from intervention.models import Intervention
 from konova.contexts import BaseContext
 from konova.decorators import *
-from konova.forms import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm
+from konova.forms import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm, \
+    ResubmissionModalForm
 from konova.models import Deadline
 from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 from konova.utils.documents import get_document, remove_document
@@ -656,3 +657,26 @@ def report_view(request: HttpRequest, id: str):
     }
     context = BaseContext(request, context).context
     return render(request, template, context)
+
+
+@login_required
+@default_group_required
+@shared_access_required(Compensation, "id")
+def create_resubmission_view(request: HttpRequest, id: str):
+    """ Renders resubmission form for a compensation
+
+    Args:
+        request (HttpRequest): The incoming request
+        id (str): Compensation's id
+
+    Returns:
+
+    """
+    com = get_object_or_404(Compensation, id=id)
+    form = ResubmissionModalForm(request.POST or None, instance=com, request=request)
+    form.action_url = reverse("compensation:resubmission-create", args=(id,))
+    return form.process_request(
+        request,
+        msg_success=_("Resubmission set"),
+        redirect_url=reverse("compensation:detail", args=(id,))
+    )
diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py
index ebface8d..03109a8c 100644
--- a/compensation/views/eco_account.py
+++ b/compensation/views/eco_account.py
@@ -26,7 +26,7 @@ from konova.contexts import BaseContext
 from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \
     shared_access_required
 from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentModalForm, RecordModalForm, \
-    RemoveDeadlineModalForm, EditDocumentModalForm
+    RemoveDeadlineModalForm, EditDocumentModalForm, ResubmissionModalForm
 from konova.models import Deadline
 from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
 from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
@@ -838,4 +838,27 @@ def create_share_view(request: HttpRequest, id: str):
     return form.process_request(
         request,
         msg_success=_("Share settings updated")
+    )
+
+
+@login_required
+@default_group_required
+@shared_access_required(EcoAccount, "id")
+def create_resubmission_view(request: HttpRequest, id: str):
+    """ Renders resubmission form for an eco account
+
+    Args:
+        request (HttpRequest): The incoming request
+        id (str): EcoAccount's id
+
+    Returns:
+
+    """
+    acc = get_object_or_404(EcoAccount, id=id)
+    form = ResubmissionModalForm(request.POST or None, instance=acc, request=request)
+    form.action_url = reverse("compensation:acc:resubmission-create", args=(id,))
+    return form.process_request(
+        request,
+        msg_success=_("Resubmission set"),
+        redirect_url=reverse("compensation:acc:detail", args=(id,))
     )
\ No newline at end of file
diff --git a/ema/templates/ema/detail/includes/controls.html b/ema/templates/ema/detail/includes/controls.html
index 6a4f7062..a16071bf 100644
--- a/ema/templates/ema/detail/includes/controls.html
+++ b/ema/templates/ema/detail/includes/controls.html
@@ -12,6 +12,9 @@
         </button>
     </a>
     {% if has_access %}
+        <button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
+            {% fa5_icon 'bell' %}
+        </button>
         <button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-create' obj.id %}">
             {% fa5_icon 'share-alt' %}
         </button>
diff --git a/ema/urls.py b/ema/urls.py
index 90cafb66..63073d6e 100644
--- a/ema/urls.py
+++ b/ema/urls.py
@@ -19,6 +19,7 @@ urlpatterns = [
     path('<id>/remove', remove_view, name='remove'),
     path('<id>/record', record_view, name='record'),
     path('<id>/report', report_view, name='report'),
+    path('<id>/resub', create_resubmission_view, name='resubmission-create'),
 
     path('<id>/state/new', state_new_view, name='new-state'),
     path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'),
diff --git a/ema/views.py b/ema/views.py
index 589165f5..9cd6dd9d 100644
--- a/ema/views.py
+++ b/ema/views.py
@@ -17,7 +17,7 @@ from konova.contexts import BaseContext
 from konova.decorators import conservation_office_group_required, shared_access_required
 from ema.models import Ema, EmaDocument
 from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm, RemoveDeadlineModalForm, \
-    EditDocumentModalForm
+    EditDocumentModalForm, ResubmissionModalForm
 from konova.models import Deadline
 from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
 from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
@@ -710,4 +710,27 @@ def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
         request,
         msg_success=DEADLINE_REMOVED,
         redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
-    )
\ No newline at end of file
+    )
+
+
+@login_required
+@conservation_office_group_required
+@shared_access_required(Ema, "id")
+def create_resubmission_view(request: HttpRequest, id: str):
+    """ Renders resubmission form for an EMA
+
+    Args:
+        request (HttpRequest): The incoming request
+        id (str): EMA's id
+
+    Returns:
+
+    """
+    ema = get_object_or_404(Ema, id=id)
+    form = ResubmissionModalForm(request.POST or None, instance=ema, request=request)
+    form.action_url = reverse("ema:resubmission-create", args=(id,))
+    return form.process_request(
+        request,
+        msg_success=_("Resubmission set"),
+        redirect_url=reverse("ema:detail", args=(id,))
+    )
diff --git a/intervention/templates/intervention/detail/includes/controls.html b/intervention/templates/intervention/detail/includes/controls.html
index f41c8b85..7af2165b 100644
--- a/intervention/templates/intervention/detail/includes/controls.html
+++ b/intervention/templates/intervention/detail/includes/controls.html
@@ -12,6 +12,9 @@
         </button>
     </a>
     {% if has_access %}
+        <button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'intervention:resubmission-create' obj.id %}">
+            {% fa5_icon 'bell' %}
+        </button>
         <button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'intervention:share-create' obj.id %}">
             {% fa5_icon 'share-alt' %}
         </button>
diff --git a/intervention/urls.py b/intervention/urls.py
index 2a5e6d38..c7c43837 100644
--- a/intervention/urls.py
+++ b/intervention/urls.py
@@ -10,7 +10,8 @@ from django.urls import path
 from intervention.views import index_view, new_view, detail_view, edit_view, remove_view, new_document_view, share_view, \
     create_share_view, remove_revocation_view, new_revocation_view, check_view, log_view, new_deduction_view, \
     record_view, remove_document_view, get_document_view, get_revocation_view, new_id_view, report_view, \
-    remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view
+    remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view, \
+    create_resubmission_view
 
 app_name = "intervention"
 urlpatterns = [
@@ -26,6 +27,7 @@ urlpatterns = [
     path('<id>/check', check_view, name='check'),
     path('<id>/record', record_view, name='record'),
     path('<id>/report', report_view, name='report'),
+    path('<id>/resub', create_resubmission_view, name='resubmission-create'),
 
     # Compensations
     path('<id>/compensation/<comp_id>/remove', remove_compensation_view, name='remove-compensation'),
diff --git a/intervention/views.py b/intervention/views.py
index 36577202..c55fe722 100644
--- a/intervention/views.py
+++ b/intervention/views.py
@@ -12,7 +12,7 @@ from intervention.models import Intervention, Revocation, InterventionDocument,
 from intervention.tables import InterventionTable
 from konova.contexts import BaseContext
 from konova.decorators import *
-from konova.forms import SimpleGeomForm, RemoveModalForm, RecordModalForm, EditDocumentModalForm
+from konova.forms import SimpleGeomForm, RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm
 from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 from konova.utils.documents import remove_document, get_document
 from konova.utils.generators import generate_qr_code
@@ -475,6 +475,29 @@ def create_share_view(request: HttpRequest, id: str):
     )
 
 
+@login_required
+@default_group_required
+@shared_access_required(Intervention, "id")
+def create_resubmission_view(request: HttpRequest, id: str):
+    """ Renders resubmission form for an intervention
+
+    Args:
+        request (HttpRequest): The incoming request
+        id (str): Intervention's id
+
+    Returns:
+
+    """
+    intervention = get_object_or_404(Intervention, id=id)
+    form = ResubmissionModalForm(request.POST or None, instance=intervention, request=request)
+    form.action_url = reverse("intervention:resubmission-create", args=(id,))
+    return form.process_request(
+        request,
+        msg_success=_("Resubmission set"),
+        redirect_url=reverse("intervention:detail", args=(id,))
+    )
+
+
 @login_required
 @registration_office_group_required
 @shared_access_required(Intervention, "id")
diff --git a/konova/admin.py b/konova/admin.py
index b30f4b14..b40a44c8 100644
--- a/konova/admin.py
+++ b/konova/admin.py
@@ -7,7 +7,7 @@ Created on: 22.07.21
 """
 from django.contrib import admin
 
-from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup
+from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup, Resubmission
 from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
 from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
 from user.models import UserAction
@@ -139,6 +139,15 @@ class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
         ]
 
 
+class ResubmissionAdmin(BaseResourceAdmin):
+    list_display = [
+        "resubmit_on"
+    ]
+    fields = [
+        "comment",
+        "resubmit_on"
+    ]
+
 
 # Outcommented for a cleaner admin backend on production
 #admin.site.register(Geometry, GeometryAdmin)
@@ -148,3 +157,4 @@ class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
 #admin.site.register(ParcelGroup, ParcelGroupAdmin)
 #admin.site.register(GeometryConflict, GeometryConflictAdmin)
 #admin.site.register(Deadline, DeadlineAdmin)
+#admin.site.register(Resubmission, ResubmissionAdmin)
diff --git a/konova/forms.py b/konova/forms.py
index 67014260..5d8f38e2 100644
--- a/konova/forms.py
+++ b/konova/forms.py
@@ -13,7 +13,9 @@ from bootstrap_modal_forms.utils import is_ajax
 from django import forms
 from django.contrib import messages
 from django.contrib.gis import gdal
+from django.core.exceptions import ObjectDoesNotExist
 from django.db.models.fields.files import FieldFile
+from django.utils.timezone import now
 
 from compensation.models import EcoAccount
 from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
@@ -26,7 +28,7 @@ from django.shortcuts import render
 from django.utils.translation import gettext_lazy as _
 
 from konova.contexts import BaseContext
-from konova.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument
+from konova.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument, Resubmission
 from konova.settings import DEFAULT_SRID
 from konova.tasks import celery_update_parcels
 from konova.utils.message_templates import FORM_INVALID, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE, DOCUMENT_EDITED
@@ -161,6 +163,7 @@ class BaseForm(forms.Form):
             self,
             (
                 NewDeductionModalForm,
+                ResubmissionModalForm,
                 EditEcoAccountDeductionModalForm,
                 RemoveEcoAccountDeductionModalForm,
             )
@@ -686,3 +689,72 @@ class RecordModalForm(BaseModalForm):
 
         """
         pass
+
+
+class ResubmissionModalForm(BaseModalForm):
+    date = forms.DateField(
+        label_suffix=_(""),
+        label=_("Date"),
+        help_text=_("When do you want to be reminded?"),
+        widget=forms.DateInput(
+            attrs={
+                "type": "date",
+                "data-provide": "datepicker",
+                "class": "form-control",
+            },
+            format="%d.%m.%Y"
+        )
+    )
+    comment = forms.CharField(
+        required=False,
+        label=_("Comment"),
+        label_suffix=_(""),
+        help_text=_("Additional comment"),
+        widget=forms.Textarea(
+            attrs={
+                "cols": 30,
+                "rows": 5,
+                "class": "form-control",
+            }
+        )
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.form_title = _("Resubmission")
+        self.form_caption = _("Set your resubmission for this entry.")
+        self.action_url = None
+
+        try:
+            self.resubmission = self.instance.resubmissions.get(
+                user=self.user
+            )
+            self.initialize_form_field("date", str(self.resubmission.resubmit_on))
+            self.initialize_form_field("comment", self.resubmission.comment)
+        except ObjectDoesNotExist:
+            self.resubmission = Resubmission()
+
+    def is_valid(self):
+        super_valid = super().is_valid()
+        self_valid = True
+
+        date = self.cleaned_data.get("date")
+        today = now().today().date()
+        if date <= today:
+            self.add_error(
+                "date",
+                _("The date should be in the future")
+            )
+            self_valid = False
+
+        return super_valid and self_valid
+
+    def save(self):
+        with transaction.atomic():
+            self.resubmission.user = self.user
+            self.resubmission.resubmit_on = self.cleaned_data.get("date")
+            self.resubmission.comment = self.cleaned_data.get("comment")
+            self.resubmission.save()
+            self.instance.resubmissions.add(self.resubmission)
+        return self.resubmission
+

From 8bce8b8e75c14b5ff1f7d84c8a417aac287ff947 Mon Sep 17 00:00:00 2001
From: mpeltriaux <Michel.Peltriaux@sgdnord.rlp.de>
Date: Mon, 15 Aug 2022 10:02:07 +0200
Subject: [PATCH 3/4] Command

* adds new command to be used with cron for periodic checkin of resubmissions
* updates translations
---
 konova/admin.py                               |   3 +-
 .../commands/handle_resubmissions.py          |  46 +++
 konova/models/resubmission.py                 |   2 +-
 locale/de/LC_MESSAGES/django.mo               | Bin 44517 -> 45149 bytes
 locale/de/LC_MESSAGES/django.po               | 302 ++++++++++--------
 5 files changed, 226 insertions(+), 127 deletions(-)
 create mode 100644 konova/management/commands/handle_resubmissions.py

diff --git a/konova/admin.py b/konova/admin.py
index b40a44c8..07d692d7 100644
--- a/konova/admin.py
+++ b/konova/admin.py
@@ -145,7 +145,8 @@ class ResubmissionAdmin(BaseResourceAdmin):
     ]
     fields = [
         "comment",
-        "resubmit_on"
+        "resubmit_on",
+        "resubmission_sent",
     ]
 
 
diff --git a/konova/management/commands/handle_resubmissions.py b/konova/management/commands/handle_resubmissions.py
new file mode 100644
index 00000000..047dbd5c
--- /dev/null
+++ b/konova/management/commands/handle_resubmissions.py
@@ -0,0 +1,46 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+import datetime
+
+from compensation.models import Compensation, EcoAccount
+from ema.models import Ema
+from intervention.models import Intervention
+from konova.management.commands.setup import BaseKonovaCommand
+from konova.models import Resubmission
+
+
+class Command(BaseKonovaCommand):
+    help = "Checks for resubmissions due now"
+
+    def handle(self, *args, **options):
+        try:
+            resubmitable_models = [
+                Intervention,
+                Compensation,
+                Ema,
+                EcoAccount,
+            ]
+            today = datetime.date.today()
+            resubmissions = Resubmission.objects.filter(
+                resubmit_on__lte=today,
+                resubmission_sent=False,
+            )
+            self._write_warning(f"Found {resubmissions.count()} resubmission. Process now...")
+            for model in resubmitable_models:
+                all_objs = model.objects.filter(
+                    resubmissions__in=resubmissions
+                )
+                self._write_warning(f"Process resubmissions for {all_objs.count()} {model.__name__} entries")
+                for obj in all_objs:
+                    obj.resubmit()
+            self._write_success("Mails have been sent.")
+            resubmissions.delete()
+            self._write_success("Resubmissions have been deleted.")
+        except KeyboardInterrupt:
+            self._break_line()
+            exit(-1)
\ No newline at end of file
diff --git a/konova/models/resubmission.py b/konova/models/resubmission.py
index ca97ebbf..be5fe842 100644
--- a/konova/models/resubmission.py
+++ b/konova/models/resubmission.py
@@ -35,7 +35,7 @@ class Resubmission(BaseResource):
         """ Sends a resubmission mail
 
         """
-        _today = today()
+        _today = today().date()
         resubmission_handled = _today.__ge__(self.resubmit_on) and self.resubmission_sent
         if resubmission_handled:
             return
diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo
index feda2678e509fe8e667237f2618882f674d2581e..03853f485e18ce219bb50fbd9b452d147282bbb3 100644
GIT binary patch
delta 12882
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
XPIV>F@;^r4a{sfL=<I{LqP+hPT27y5

delta 12287
zcmZA72YgT0|Htv0DI$`Hgb*YWu@W;jHB!V3p+;@BXKSzUHDlIj?7dsm45~)0s-LZ;
zHEOiAOKa4o^?$u{P96`B|9$l1`8ns_d(OG%oO_e}9>1%<x!?NL-E}R<bB@DN&fRhH
z;K^)`^Nc(>O0|yDxP;>bxH(P|rc&-$(s8m<euif$dz5yZ=6D|aVoV&@;zCTs2RI%p
zmvNlDcmT`cHS}{FmlIOfaVAm`i9xs?v*JEmK7ro40khyOTmKko!11KD!I%wo-z4<M
zxz;tP`?jN=>42@jiNW;m{6iAR37_&bg1J!T7!1HFm>nBnFm^z7JP6g%Sj>U5tY2X+
z%KI@CFQKk`Vs)!v9yAaO(!Uc$QXK1{o^%-MhSBJOGmzPLQtkP}xQ_C1)W8N-Gy|H1
znt?f}0j<K!xEpig_qKcm^`N)Wr6+kwBE2e^27}O-azRvmDO3YhZMivWpq;Q7_QXh>
zkD9SPm<dl{Av}k<@C|AyLMoen@>gd5^#o<9@WHyM8=Ii^LMzlv^g#_I8MOqHQTHuH
zZK`dkCpwEE_y=kRUZMsbP{qtlAq=Hl7S&H;73N<ZG^avq-UZe0V9bqEFdWyQIy!>7
z@dj$Z_igzp>b|$AfdqbFW+)OhBSlc{S47=k4>iM0TqJo&+M?EOIBH5Kp(pM{&BPvi
z{xoX9S5OV#M>YHgwbp)B&A@VCeahj;zH@rvR9uC+uS7L7P**h)HIQU0K0%FaAgUu5
zY9^+mM!pWU_FHXv7ivupU>Kf6&CFfYl0CNN|4>Vlsk&)5FES996HB6zS3xyg8#RRu
zQ6p`S>flpUgCkHgH3@ZoE^4jUqR#I_&FFE|!0({e+`Wd`J7K7Slte$h|FucfK~q#i
z?XeIJ##mg6TH6!Y2(P1Ns6@P3qDrWls)l;<MAVG7K(*T)^*{qr15dHeRz3YYD@nA*
z+fh$&6!m21Q61ex&Co;C61+ln<o2PNF<;aGvmyUE(fp%b-vjmJW3Ub`L=EHydf_v4
zY0BS_s6qFdW(~8TC*?w@Cx}HoVFlE638(=#Mi1<SYNs1|VqesN2B02jqIEXv!B(IK
zxV0wpuLh4%5s5#c8hVDBs<)^a@U3MU4zcD&K7LL(CSgnD=ZUi(b^UeJ+W(24p;vA5
zLuW9yq`VRPqIUxGpO0ivg5yNsY|M*0P*Zgd^#r$2BYuMV0QoYX+U?<}FI#2QrfP=T
zL%pyajzvAl@2I7Ch?+T{y5>Q{TqK&RNX(Dr?1`4Bj@n};9EhHnY#oCdU<zi&`KXRo
zqLyTft>2Ei?>p4Y9JckRZP|5^L^s?(P5mA8!M{-hdx4sPEcMK$2}NC>4>hnz)b&Me
zIS#e^D_a|&mZB|c4-G{Pd?Lo`{a-**jEWyn4L?N<<Rxn4?@&|iS>JI!#t76~GaUQl
zYs`#&8kiXvg6ePrM&NQ(y9e$0BdCF&!vMAPD~T@^_faE%j((V_p=mf6RiDq6OQ4>l
zB9_HEsDX|`cN~Y>11YFIvjl764qJYW+ABec+@~igOrj2pp`N4?s>7OC4x6EFn1)$#
zC2DVMK`qe{^v6r6C%cP!;zy{deu`?>o3EGd3q%bp8eQ6zACPFo&8>ax1>=#yI*U<H
z`X?5_h{opitA`rUI1Ip9sPijO137{GKy=O_tLOwKnb);7>iote=HH8C2Nima_Mq1O
zXY|J_s1EL<X6O|LqIVNhpA#!lj<)3vsD}I6@?cau<57Dk74@@WGitymn=t>{y;rEn
zg14-XQ3LrG^;&v1<!grpaR3g%kMKV7pHrQJ1~?lv;1#yK8P(2SRQrcf{hhJpi!Kt~
zcoT#1F=j*WW@c?eF`RNNF2x3@2JfS0;t8rlkLKq3Y^W!Tww6WBKrK|eAEEl|iW-n>
z0ErrOp*BehY6@qfmSjGvqixp1sJG=js-x?+{voOZr-k{#1)(~Kw&hZ&_G_XxXCg8a
zE~g2JUZ2*efpkE9lDlDcOu-zu(3ZENIy!<H;04qser)yS1F!p|P~U-gEQ@VXOPPuq
z*bdCA_x~VCPAaZrD89gI7|7Q~KMNWozg;<fQBV9emc+f7fPbOZv}`N$g{+F2p_&+k
zjW7g1K|RP&Tb_a0>EBtS1iwY?fm5gp|3HoO6{=&e)@G(cP&1Gl)p0E9zOtyjP!F}{
zEp53Q>a`q%x^Ee34{Sx3I{40397i>9!Ip2?@)J~pP8;3;2tci2In<I>LhYFvsHLon
zYBv$J=B-d2c1D_V`e9K#)rR^1gyba^Be8W`^LxN0Orrc2)nOt(OXVOej*Bq@PgozI
zrZ{VRvy@R-fO2IlgB`F6&Oyz@Pso4H+x9N=ByBpFsqBCnaSzmoX#nb*J`4+CGUmh8
zsPl(WGjR+x1E)|k@H48N%eH(Ib^ab|$sVI-`n8M1m&B)|S>x=eCy7EeR1r1xRZ(l#
z5Vfh=qZ;UsYH&1a$|u|UIj9a-p$51CHK5H{9>2wk=z2^NMH0h0l*2}-8&WVYPQm>6
z6>2JvV^MsEu~@VdznowTRQV)ofUm56olQ9$waH7P9;CLZcR4NXiC$KhIpL(DcKurG
zPSl6!2h@yQKy`E*-SHJ_N#3GfWA`rRLBdco5pK(ctTE~JtbYj-?auP325Z_2TA&(k
zjhfna7=t~nb5T!x2;=Y}YD%NJnoZgkb^kQfrkjCv@LNno_inuA^zS5*XvEzyH;%F8
z#nv5|i~7?ThIdhW#<ROwx&SOeISzHc6KcRN)OAx)du1-hyRmukD&;#pxaLceBR%;g
z0sHha|C`Pyz0C;Y`tS!R^<$AY(W%%s{qye3!+n$=qV~qdPaUT^o<Xf`;AgxM7>R}P
z0c!0-`kC`Z(1&v6e!Tx0SxqWr6V#Wl1J1`G7>t4a&5hBhB`S})z8>nyTcT!e5^B?~
zLe1zwTYuU5%+?1EFxM3s!2Ih85~xteJy1UjMx!>{d{o2h?D_9e1Ns%UB#%(9t1qvB
z?u$U}kqW4{sWS%PA`HR}=!xH9K|Jgt@gn&Xx8oyJLu&?^8~33GatbxTYpD158R`k&
zq1HHbu$kg0)XbE}0$2;R1U+pz6?OkE%!aN*w&J|?8m4bD)CfI?n2uvmYZi~uSRZp@
zKh)bZ8S~&=)Bw^j6COl$cntL*=TIHrMDBAr&q?$I9z)Gc_@kya7<EH&RKwL!OVI{H
zaR~b3Ow@pvq1xGlIq?VUHB^Vsu?YGMGoSF%7)t+65{X9I6TPuN>WPM7E=)mnyc|7n
z4{9y<V-EZYH8c0DPcVw|8`Ro|4>!L%mOw4_Xw-cv=%e?42}v$oiyHX>d%;D_PWd6K
z1CJ5rmrg%xN1RXnPE>=9lFjv9F_`jj)IevTz6UE&Gr9?L<9>9h;maiQ25PMypl0AX
zY7=>kG$SvJdZOZ}>#L#$oPZ(N9yQ=WsCFi!mLwImXO^NDAE571?H?b-{Og7bqs$uJ
zxBiQ2Fo5Y%1JS4_EQyh*Kil;LJyA0^0X2|isO!H$4d9TiKZ#oVn^+3{N1K_cKAH-?
zrcP5Tbi*E014mF(cowyrZ`txcR-Z9uKzUIemq9&918WD&OL-t_3BEu*z#3cLi<-$Z
zE)q?}b<`R?Kn>s}hM~t;^Ft*Z^~4{eo_HX(!s)0E?x7ywt<`&++0=oknG8j3zT&9<
z%AnpJR}B(9VPn+hXov3jDeA50j~+N2%i}22bvscF97PS_7gR_0Py=~~<uLPj^Hx<y
z^^<^lz-GvRTuuiPPb#`%CG3SwaV^%z*QkNio?w3QNW^KBhhaQ=r?CE57kA+q)F*h-
zMDr`#c2xNaYQR2|%;wFBfqMTVNVL|aFdA#4)~+{(;uO@{t-|)W8})>_C!6{LsI{+*
z`p`78cEkwE18jM=Ew4p=XO3d}-~XqTP;m~`(M8mhKD4?|F*k&uI?98Zk&>t<s$fk(
zElqRuz;5V+y=?tJ)PqhyFI<Q&CCf>4!&>yk^{CCb19ko&rmr<>^ZbGu=nY$bjoQtb
zrkW+niNz_$qxMoi)DsUxJ?IG3%#NPQ{P!kVK!w)Sn}5E*RMf}|er{%>1nO;w$2e?<
z8t@mWfz3rd*&5V9cH8>ns3p0CzIY$A;&aq}p3_->Rb-!T-sAkJU0xBhU_aD|N1{3y
zi+cU$VIpp|WoL%jjJeT``nIU0XpjE*8EVE{SO@2#miW3VlsrLgI<J}LhA0f6TpG33
z@u(-RhnX=EHNcjr0e8k&T#Uu=7#7DDSON>qGGD&t*qZVrtc|XlB&A7Wn0Gx<I}F5)
z7=+(pb^H<4aZsxH>o*TpqudbH(C4TnJ8!*;`e5Bg&7j+CvlQN_c0!FVCz2$Xida;~
zHLx)@K|SFr)C_FKVEhI(u#>1y?{(ClWG^rq=9pt<t}trqE1(9DfZBu&un0EAWWE0*
zNfuM_8pq+hx#o}0uz6;0^uZA7N1!^Mg&NpuREJw^`4FnZ)7GmPPWb^AL_dC3>;5=Y
zeQi8N|4vsDO<kD<=KcP_nuOXkT~G}tqdJ^wU4k0eW?Mdjy6ysM#%`l_{okneUtu0}
zUuZsb;pkF>wMg_tEl``R6NX|p)Br|fFwR50EnnO7yHQVc4)vbjMs<7-wZ@N8Q~n0i
zJ6>cSARN_xsYT3xZjyRbXzjb8-h#oXC!3ABaXo5{kD{jXHde>1tXnQjL=CJv>b}9K
z2b+d!cPYl>X4C^c!BUuc3G=Vltnw1`)2cpJr#u$b;9=C%{fK^e8P&iY)PVlOD9pRm
zlxtuZ<qnt!lWqM1>n_xQE}&-gu8Tw+{EM1O?`5W=T&RW$qMj%oHPDu}zB}qYACDT?
zHtT*=!zWSg{f?T6r>G_NUT)e6!U)Q)ND?*lA!-f#q1JGqH5v8#j7JS%GHPuXpf=ko
z9E#hpG{&qj|5HvoWM4a1P_NsdmHd5)%djwJU6roODMwP7iuM?Ri!cWFqNe&TYUJ-w
zH)dJQyNqS90VZQjJchl|e~o#N5vbii3N_%#sCH7(1D9bLz5lC7!l}53y6`1xCfwJW
zrSU@D;E#IZAk+-yM_pe8)o>hYs;k*@5^4$CVm=&(-Z&q%2bQ59{X1Jov{w62o9--X
z<kxfo-b3B+9My5AFU<f0P<tl`%VHQ-z$Db}o`RKdFY19_U<AI#C=C0G_1BbEBheCc
z$AY*TN8t%nL-p2~J&<f2V@<J6Ma|GmTb_+RlowzoT#4Se-k#rT-MNnW_u|AJD%8M1
z)D1^$`4nm<enjo+E4KbI>bhs>iT~Pq_x0u%7H?FCeyIDhquMWwx;_@Ql%>`)|Jwc4
zsVI(pu_P|Rig+5UqR$3XpMYv;D5^dgo8TU7f&O2cFI!iu3-$WW#|Yen`SA=!;bUE(
zBxIx6L=`cbaxK))?Ov$Oxd=7lJ(vZL+ww2yM)?x|JXgXogGeD?ihBBB^6JDl<QWHV
zq2o!Nt117~B?=Mw2|axxh7+0DHFZ$$b;ePQx?|LJCw$3ca0a2LFGG15Hl#d~Xh^ON
zu45+AmY7ZKCG6pHa&nW7?A)L=izl=vCK9`;`-#{}K8QF(=vYr(fW1!}HshE{ea7)I
z$>+p*>I&dr*cyvsjJ@|)4bPLpw^V9c|HeNE9sG<)KNj)te?(8B0a1@QNPNO|rEEjl
zDf<yR%2EFTCKF4^pQ1M1`^V*sLdNv;<-!Y8cD5(tDAy$ai}-;&<0wy}jakpu{YdW4
zAoORoUg5KZj;|@-L4NW&{fJkD4u9(3;!)g2xbpB%EOC+em*~RDFK{HzBk~izM0Y}8
zkusdCOS%kojKBzDFY&_GmBuOrZ>2LEzfZrCH;ud+(Ss;w>iGMgM%Pd=fr?RtFZl@~
z*4`w3CqAa`CE-K-M+~OU9S0B>h$Dm!FWS=&sv(ry*t*7eg*yGxF^N1c@h|y1t^Z^S
z%ZZZQc$`Wd*~l|fR{(YVLG+|tIK9MvvG&JeHou2ixaL05%ht6cuS(RQ+=sYL-rk;@
zgX>&W3?aEjJR+ZBPYNH~`Df%)IH#jH{%-Tklv@#Nh(scZxNFZ%#h)qb(04>fR@<KP
zo%)BvwZz_Fiu}G$l0;n?7Y`)=lDwC$Av!@gQJ&n5^B<AFCNd6v$aHidJh)~o5vK}!
zw4|&f8jG62xSXpLCR6zkYZD8|>*F^>De^3K)Q`zCj$rEAQK(}pg6uULDOV+Kaqky+
z6C;U?;~$d2l%u$QAZ~VJ{U6&4E^)FD;cXk!O?^0*oAQS$a2&MPMv~tmHd3yQZ!j8v
z#Ad`IA}8gHqZQ={TbNEhfe5GFuj$_@WIL=#rH-}4268`Q91%!tp)MEZ<yu{yLg*+<
zT&F%8v7Km2d9F@y%rZD%;zOd6EsNJ&JBo1apm6HF6Pc*=pqv$TG{7JtnOIFZza8vF
z@}atbV?41ZJu|<kC_QEG|B1W=_nf7?oIDS))1I4_@#CQ^7o6n8OCl3_Eo{z3{q2Pv
zuqAamdXpa~{wCi=?6cSCoQ~_nuhfl6FY)IW`5xjN@&At-TK~43_=+>1V`0LB0W^Q_
z%2hW14MRBh#Fp1v?^@enan5HPM@T|w?+Q_a@aJ3t?k7%jE-R74_1+EBsrZx_%>_;H
z29ZV_qO2pxkp6qXGx7t}hZ1?-YhxSr!|e54tUlE3xA$JfRH7v3#uKh#6#gWsVlNOU
zDL2Mz*bT#o9pnS>e}s+*I?YGj52l#@{}EnDxiIxNZQX6F_>ObCh>RoB3|jl2AEmbT
zgtaI>B7Wh97dYSEIQ+c^bm{*nH&Vj!%AUJyErqE>Jn<RPgvenoP5-w7<q&=Uk5L>-
z!HtL^|I=PH4r@}^h$uo{nh_T<qfWo#Q|g!4YkOP!TFY_WAkOcm{0k9Hxdi3TL>Qst
ziPZCtA!%<bH&CujeuwBsd`~>kb(qg~UY0z8x&wrce<<tNY;fYZE;n`Eh!5;JoeH$~
zzqNJMs7s&!;v|0dWOedcTyTacK<uM@51$e`wpu%yq7%z~d2OCSC9Ww*Orqi={F%_P
z37g@0;xu^<H~ODOl4dX1&IxauKPO*H{uObUTt^G?t|oQvV+CR&b@?zGv5H*BXrhzN
z3o^i|lvmsGCh`x-=VNQV|K~{FQBfar5od@{ZW@6)x^ixTz4<Ucvw2S3LLB9qS{OyF
zB*v!S<v72Q>v%(SB%fo;xhWSRpG)*8MqwVwVtX<d9j_oCPQ8vY24@9ka?<`QF|1J9
zmo55vq*ZNQ$RjPK-80X$#hr$_rNwl4<epZu`{cMZ&&8uW(snG%?V0v`)h(~Ix9cXk
brKN7@=k8Uvd*3d7Q)g~UOiS5x!R`M5bw%+`

diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po
index 59a752d0..531241e3 100644
--- a/locale/de/LC_MESSAGES/django.po
+++ b/locale/de/LC_MESSAGES/django.po
@@ -18,15 +18,16 @@
 #: konova/filters/mixins.py:277 konova/filters/mixins.py:323
 #: konova/filters/mixins.py:361 konova/filters/mixins.py:362
 #: konova/filters/mixins.py:393 konova/filters/mixins.py:394
-#: konova/forms.py:179 konova/forms.py:281 konova/forms.py:395
-#: konova/forms.py:439 konova/forms.py:449 konova/forms.py:462
-#: konova/forms.py:474 konova/forms.py:492 user/forms.py:42
+#: konova/forms.py:183 konova/forms.py:285 konova/forms.py:399
+#: konova/forms.py:443 konova/forms.py:453 konova/forms.py:466
+#: konova/forms.py:478 konova/forms.py:496 konova/forms.py:696
+#: konova/forms.py:711 user/forms.py:42
 #, fuzzy
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-08-10 08:37+0200\n"
+"POT-Creation-Date: 2022-08-15 09:39+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -85,7 +86,7 @@ msgstr "Bericht generieren"
 msgid "Select a timespan and the desired conservation office"
 msgstr "Wählen Sie die Zeitspanne und die gewünschte Eintragungsstelle"
 
-#: analysis/forms.py:71 konova/forms.py:227
+#: analysis/forms.py:71 konova/forms.py:231
 msgid "Continue"
 msgstr "Weiter"
 
@@ -241,7 +242,8 @@ msgstr ""
 #: ema/templates/ema/detail/includes/states-after.html:36
 #: ema/templates/ema/detail/includes/states-before.html:36
 #: intervention/forms/modalForms.py:364
-#: templates/email/other/deduction_changed.html:29
+#: templates/email/other/deduction_changed.html:31
+#: templates/email/other/deduction_changed_team.html:31
 msgid "Surface"
 msgstr "Fläche"
 
@@ -308,7 +310,8 @@ msgstr "Typ"
 #: intervention/forms/modalForms.py:382 intervention/tables.py:87
 #: intervention/templates/intervention/detail/view.html:19
 #: konova/templates/konova/includes/quickstart/interventions.html:4
-#: templates/email/other/deduction_changed.html:24
+#: templates/email/other/deduction_changed.html:26
+#: templates/email/other/deduction_changed_team.html:26
 #: templates/navbars/navbar.html:22
 msgid "Intervention"
 msgstr "Eingriff"
@@ -362,7 +365,7 @@ msgstr "Automatisch generiert"
 #: intervention/templates/intervention/detail/includes/documents.html:28
 #: intervention/templates/intervention/detail/view.html:31
 #: intervention/templates/intervention/report/report.html:12
-#: konova/forms.py:438
+#: konova/forms.py:442
 msgid "Title"
 msgstr "Bezeichnung"
 
@@ -389,12 +392,13 @@ msgstr "Kompensation XY; Flur ABC"
 #: intervention/templates/intervention/detail/includes/documents.html:34
 #: intervention/templates/intervention/detail/includes/payments.html:34
 #: intervention/templates/intervention/detail/includes/revocation.html:38
-#: konova/forms.py:473 konova/templates/konova/includes/comment_card.html:16
+#: konova/forms.py:477 konova/forms.py:710
+#: konova/templates/konova/includes/comment_card.html:16
 msgid "Comment"
 msgstr "Kommentar"
 
 #: compensation/forms/forms.py:59 compensation/forms/modalForms.py:471
-#: intervention/forms/forms.py:200
+#: intervention/forms/forms.py:200 konova/forms.py:712
 msgid "Additional comment"
 msgstr "Zusätzlicher Kommentar"
 
@@ -479,7 +483,7 @@ msgstr "kompensiert Eingriff"
 msgid "Select the intervention for which this compensation compensates"
 msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist"
 
-#: compensation/forms/forms.py:219 compensation/views/compensation.py:110
+#: compensation/forms/forms.py:219 compensation/views/compensation.py:111
 msgid "New compensation"
 msgstr "Neue Kompensation"
 
@@ -531,7 +535,7 @@ msgid "Due on which date"
 msgstr "Zahlung wird an diesem Datum erwartet"
 
 #: compensation/forms/modalForms.py:65 compensation/forms/modalForms.py:363
-#: intervention/forms/modalForms.py:177 konova/forms.py:475
+#: intervention/forms/modalForms.py:177 konova/forms.py:479
 msgid "Additional comment, maximum {} letters"
 msgstr "Zusätzlicher Kommentar, maximal {} Zeichen"
 
@@ -576,7 +580,7 @@ msgstr "Neuer Zustand"
 msgid "Insert data for the new state"
 msgstr "Geben Sie die Daten des neuen Zustandes ein"
 
-#: compensation/forms/modalForms.py:219 konova/forms.py:229
+#: compensation/forms/modalForms.py:219 konova/forms.py:233
 msgid "Object removed"
 msgstr "Objekt entfernt"
 
@@ -602,7 +606,7 @@ msgstr "Fristart wählen"
 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:36
 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:36
 #: ema/templates/ema/detail/includes/deadlines.html:36
-#: intervention/forms/modalForms.py:149
+#: intervention/forms/modalForms.py:149 konova/forms.py:697
 msgid "Date"
 msgstr "Datum"
 
@@ -850,24 +854,32 @@ msgstr "In LANIS öffnen"
 msgid "Public report"
 msgstr "Öffentlicher Bericht"
 
-#: compensation/templates/compensation/detail/compensation/includes/controls.html:17
-#: compensation/templates/compensation/detail/eco_account/includes/controls.html:31
-#: ema/templates/ema/detail/includes/controls.html:31
-#: intervention/templates/intervention/detail/includes/controls.html:36
+#: compensation/templates/compensation/detail/compensation/includes/controls.html:15
+#: compensation/templates/compensation/detail/eco_account/includes/controls.html:15
+#: ema/templates/ema/detail/includes/controls.html:15
+#: intervention/templates/intervention/detail/includes/controls.html:15
+#: konova/forms.py:724 templates/email/resubmission/resubmission.html:4
+msgid "Resubmission"
+msgstr "Wiedervorlage"
+
+#: compensation/templates/compensation/detail/compensation/includes/controls.html:20
+#: compensation/templates/compensation/detail/eco_account/includes/controls.html:34
+#: ema/templates/ema/detail/includes/controls.html:34
+#: intervention/templates/intervention/detail/includes/controls.html:39
 msgid "Edit"
 msgstr "Bearbeiten"
 
-#: compensation/templates/compensation/detail/compensation/includes/controls.html:21
-#: compensation/templates/compensation/detail/eco_account/includes/controls.html:35
-#: ema/templates/ema/detail/includes/controls.html:35
-#: intervention/templates/intervention/detail/includes/controls.html:40
-msgid "Show log"
-msgstr "Log anzeigen"
-
 #: compensation/templates/compensation/detail/compensation/includes/controls.html:24
 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:38
 #: ema/templates/ema/detail/includes/controls.html:38
 #: intervention/templates/intervention/detail/includes/controls.html:43
+msgid "Show log"
+msgstr "Log anzeigen"
+
+#: compensation/templates/compensation/detail/compensation/includes/controls.html:27
+#: compensation/templates/compensation/detail/eco_account/includes/controls.html:41
+#: ema/templates/ema/detail/includes/controls.html:41
+#: intervention/templates/intervention/detail/includes/controls.html:46
 #: venv/lib/python3.7/site-packages/django/forms/formsets.py:391
 msgid "Delete"
 msgstr "Löschen"
@@ -907,7 +919,7 @@ msgstr "Dokumente"
 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:14
 #: ema/templates/ema/detail/includes/documents.html:14
 #: intervention/templates/intervention/detail/includes/documents.html:14
-#: konova/forms.py:491
+#: konova/forms.py:495
 msgid "Add new document"
 msgstr "Neues Dokument hinzufügen"
 
@@ -915,7 +927,7 @@ msgstr "Neues Dokument hinzufügen"
 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:31
 #: ema/templates/ema/detail/includes/documents.html:31
 #: intervention/templates/intervention/detail/includes/documents.html:31
-#: konova/forms.py:448
+#: konova/forms.py:452
 msgid "Created on"
 msgstr "Erstellt"
 
@@ -923,7 +935,7 @@ msgstr "Erstellt"
 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:61
 #: ema/templates/ema/detail/includes/documents.html:61
 #: intervention/templates/intervention/detail/includes/documents.html:65
-#: konova/forms.py:553
+#: konova/forms.py:557
 msgid "Edit document"
 msgstr "Dokument bearbeiten"
 
@@ -1093,22 +1105,22 @@ msgstr ""
 msgid "other users"
 msgstr "weitere Nutzer"
 
-#: compensation/templates/compensation/detail/eco_account/includes/controls.html:15
-#: ema/templates/ema/detail/includes/controls.html:15
+#: compensation/templates/compensation/detail/eco_account/includes/controls.html:18
+#: ema/templates/ema/detail/includes/controls.html:18
 #: intervention/forms/modalForms.py:71
-#: intervention/templates/intervention/detail/includes/controls.html:15
+#: intervention/templates/intervention/detail/includes/controls.html:18
 msgid "Share"
 msgstr "Freigabe"
 
-#: compensation/templates/compensation/detail/eco_account/includes/controls.html:20
-#: ema/templates/ema/detail/includes/controls.html:20
-#: intervention/templates/intervention/detail/includes/controls.html:25
+#: compensation/templates/compensation/detail/eco_account/includes/controls.html:23
+#: ema/templates/ema/detail/includes/controls.html:23
+#: intervention/templates/intervention/detail/includes/controls.html:28
 msgid "Unrecord"
 msgstr "Entzeichnen"
 
-#: compensation/templates/compensation/detail/eco_account/includes/controls.html:24
-#: ema/templates/ema/detail/includes/controls.html:24
-#: intervention/templates/intervention/detail/includes/controls.html:29
+#: compensation/templates/compensation/detail/eco_account/includes/controls.html:27
+#: ema/templates/ema/detail/includes/controls.html:27
+#: intervention/templates/intervention/detail/includes/controls.html:32
 msgid "Record"
 msgstr "Verzeichnen"
 
@@ -1215,29 +1227,34 @@ msgstr ""
 msgid "Responsible data"
 msgstr "Daten zu den verantwortlichen Stellen"
 
-#: compensation/views/compensation.py:53
+#: compensation/views/compensation.py:54
 msgid "Compensations - Overview"
 msgstr "Kompensationen - Übersicht"
 
-#: compensation/views/compensation.py:172 konova/utils/message_templates.py:36
+#: compensation/views/compensation.py:173 konova/utils/message_templates.py:36
 msgid "Compensation {} edited"
 msgstr "Kompensation {} bearbeitet"
 
-#: compensation/views/compensation.py:182 compensation/views/eco_account.py:173
+#: compensation/views/compensation.py:183 compensation/views/eco_account.py:173
 #: ema/views.py:241 intervention/views.py:338
 msgid "Edit {}"
 msgstr "Bearbeite {}"
 
-#: compensation/views/compensation.py:269 compensation/views/eco_account.py:360
-#: ema/views.py:195 intervention/views.py:542
+#: compensation/views/compensation.py:270 compensation/views/eco_account.py:360
+#: ema/views.py:195 intervention/views.py:565
 msgid "Log"
 msgstr "Log"
 
-#: compensation/views/compensation.py:613 compensation/views/eco_account.py:728
-#: ema/views.py:559 intervention/views.py:688
+#: compensation/views/compensation.py:614 compensation/views/eco_account.py:728
+#: ema/views.py:559 intervention/views.py:711
 msgid "Report {}"
 msgstr "Bericht {}"
 
+#: compensation/views/compensation.py:680 compensation/views/eco_account.py:862
+#: ema/views.py:734 intervention/views.py:496
+msgid "Resubmission set"
+msgstr "Wiedervorlage gesetzt"
+
 #: compensation/views/eco_account.py:65
 msgid "Eco-account - Overview"
 msgstr "Ökokonten - Übersicht"
@@ -1255,12 +1272,12 @@ msgid "Eco-account removed"
 msgstr "Ökokonto entfernt"
 
 #: compensation/views/eco_account.py:381 ema/views.py:283
-#: intervention/views.py:641
+#: intervention/views.py:664
 msgid "{} unrecorded"
 msgstr "{} entzeichnet"
 
 #: compensation/views/eco_account.py:381 ema/views.py:283
-#: intervention/views.py:641
+#: intervention/views.py:664
 msgid "{} recorded"
 msgstr "{} verzeichnet"
 
@@ -1462,11 +1479,11 @@ msgid "Checked compensations data and payments"
 msgstr "Kompensationen und Zahlungen geprüft"
 
 #: intervention/forms/modalForms.py:263
-#: intervention/templates/intervention/detail/includes/controls.html:19
+#: intervention/templates/intervention/detail/includes/controls.html:22
 msgid "Run check"
 msgstr "Prüfung vornehmen"
 
-#: intervention/forms/modalForms.py:264 konova/forms.py:594
+#: intervention/forms/modalForms.py:264 konova/forms.py:598
 msgid ""
 "I, {} {}, confirm that all necessary control steps have been performed by "
 "myself."
@@ -1622,11 +1639,11 @@ msgstr "Eingriff {} bearbeitet"
 msgid "{} removed"
 msgstr "{} entfernt"
 
-#: intervention/views.py:495
+#: intervention/views.py:518
 msgid "Check performed"
 msgstr "Prüfung durchgeführt"
 
-#: intervention/views.py:646
+#: intervention/views.py:669
 msgid "There are errors on this intervention:"
 msgstr "Es liegen Fehler in diesem Eingriff vor:"
 
@@ -1711,78 +1728,90 @@ msgstr "Nach Zulassungsbehörde suchen"
 msgid "Search for conservation office"
 msgstr "Nch Eintragungsstelle suchen"
 
-#: konova/forms.py:41 templates/form/collapsable/form.html:62
+#: konova/forms.py:44 templates/form/collapsable/form.html:62
 msgid "Save"
 msgstr "Speichern"
 
-#: konova/forms.py:75
+#: konova/forms.py:78
 msgid "Not editable"
 msgstr "Nicht editierbar"
 
-#: konova/forms.py:178 konova/forms.py:394
+#: konova/forms.py:182 konova/forms.py:398
 msgid "Confirm"
 msgstr "Bestätige"
 
-#: konova/forms.py:190 konova/forms.py:403
+#: konova/forms.py:194 konova/forms.py:407
 msgid "Remove"
 msgstr "Löschen"
 
-#: konova/forms.py:192
+#: konova/forms.py:196
 msgid "You are about to remove {} {}"
 msgstr "Sie sind dabei {} {} zu löschen"
 
-#: konova/forms.py:280 konova/utils/quality.py:44 konova/utils/quality.py:46
+#: konova/forms.py:284 konova/utils/quality.py:44 konova/utils/quality.py:46
 #: templates/form/collapsable/form.html:45
 msgid "Geometry"
 msgstr "Geometrie"
 
-#: konova/forms.py:331
+#: konova/forms.py:335
 msgid "Only surfaces allowed. Points or lines must be buffered."
 msgstr ""
 "Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
 
-#: konova/forms.py:404
+#: konova/forms.py:408
 msgid "Are you sure?"
 msgstr "Sind Sie sicher?"
 
-#: konova/forms.py:450
+#: konova/forms.py:454
 msgid "When has this file been created? Important for photos."
 msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?"
 
-#: konova/forms.py:461
+#: konova/forms.py:465
 #: venv/lib/python3.7/site-packages/django/db/models/fields/files.py:231
 msgid "File"
 msgstr "Datei"
 
-#: konova/forms.py:463
+#: konova/forms.py:467
 msgid "Allowed formats: pdf, jpg, png. Max size 15 MB."
 msgstr "Formate: pdf, jpg, png. Maximal 15 MB."
 
-#: konova/forms.py:528
+#: konova/forms.py:532
 msgid "Added document"
 msgstr "Dokument hinzugefügt"
 
-#: konova/forms.py:585
+#: konova/forms.py:589
 msgid "Confirm record"
 msgstr "Verzeichnen bestätigen"
 
-#: konova/forms.py:593
+#: konova/forms.py:597
 msgid "Record data"
 msgstr "Daten verzeichnen"
 
-#: konova/forms.py:600
+#: konova/forms.py:604
 msgid "Confirm unrecord"
 msgstr "Entzeichnen bestätigen"
 
-#: konova/forms.py:601
+#: konova/forms.py:605
 msgid "Unrecord data"
 msgstr "Daten entzeichnen"
 
-#: konova/forms.py:602
+#: konova/forms.py:606
 msgid "I, {} {}, confirm that this data must be unrecorded."
 msgstr ""
 "Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen."
 
+#: konova/forms.py:698
+msgid "When do you want to be reminded?"
+msgstr "Wann wollen Sie erinnert werden?"
+
+#: konova/forms.py:725
+msgid "Set your resubmission for this entry."
+msgstr "Setzen Sie eine Wiedervorlage für diesen Eintrag."
+
+#: konova/forms.py:746
+msgid "The date should be in the future"
+msgstr "Das Datum sollte in der Zukunft liegen"
+
 #: konova/management/commands/setup_data.py:26
 msgid "On shared access gained"
 msgstr "Wenn mir eine Freigabe zu Daten erteilt wird"
@@ -1929,7 +1958,7 @@ msgstr "{} - Freigegebene Daten verzeichnet"
 msgid "{} - Shared data checked"
 msgstr "{} - Freigegebene Daten geprüft"
 
-#: konova/utils/mailer.py:233 konova/utils/mailer.py:372
+#: konova/utils/mailer.py:233 konova/utils/mailer.py:376
 msgid "{} - Deduction changed"
 msgstr "{} - Abbuchung geändert"
 
@@ -1937,10 +1966,14 @@ msgstr "{} - Abbuchung geändert"
 msgid "{} - Shared data deleted"
 msgstr "{} - Freigegebene Daten gelöscht"
 
-#: konova/utils/mailer.py:393 templates/email/api/verify_token.html:4
+#: konova/utils/mailer.py:397 templates/email/api/verify_token.html:4
 msgid "Request for new API token"
 msgstr "Anfrage für neuen API Token"
 
+#: konova/utils/mailer.py:420
+msgid "Resubmission - {}"
+msgstr "Wiedervorlage - {}"
+
 #: konova/utils/message_templates.py:10
 msgid "no further details"
 msgstr "keine weitere Angabe"
@@ -2223,11 +2256,11 @@ msgstr "Irgendetwas ist passiert. Wir arbeiten daran!"
 msgid "Hello support"
 msgstr "Hallo Support"
 
-#: templates/email/api/verify_token.html:9
+#: templates/email/api/verify_token.html:10
 msgid "you need to verify the API token for user"
 msgstr "Sie müssen einen API Token für folgenden Nutzer freischalten"
 
-#: templates/email/api/verify_token.html:15
+#: templates/email/api/verify_token.html:16
 msgid ""
 "If unsure, please contact the user. The API token can not be used until you "
 "activated it in the admin backend."
@@ -2236,20 +2269,22 @@ msgstr ""
 "Token kann so lange nicht verwendet werden, wie er noch nicht von Ihnen im "
 "Admin Backend aktiviert worden ist."
 
-#: templates/email/api/verify_token.html:18
-#: templates/email/checking/shared_data_checked.html:19
-#: templates/email/checking/shared_data_checked_team.html:19
-#: templates/email/deleting/shared_data_deleted.html:19
-#: templates/email/deleting/shared_data_deleted_team.html:19
-#: templates/email/other/deduction_changed.html:38
-#: templates/email/recording/shared_data_recorded.html:19
-#: templates/email/recording/shared_data_recorded_team.html:19
-#: templates/email/recording/shared_data_unrecorded.html:19
-#: templates/email/recording/shared_data_unrecorded_team.html:19
-#: templates/email/sharing/shared_access_given.html:20
-#: templates/email/sharing/shared_access_given_team.html:20
-#: templates/email/sharing/shared_access_removed.html:20
-#: templates/email/sharing/shared_access_removed_team.html:20
+#: templates/email/api/verify_token.html:19
+#: templates/email/checking/shared_data_checked.html:20
+#: templates/email/checking/shared_data_checked_team.html:20
+#: templates/email/deleting/shared_data_deleted.html:20
+#: templates/email/deleting/shared_data_deleted_team.html:20
+#: templates/email/other/deduction_changed.html:41
+#: templates/email/other/deduction_changed_team.html:41
+#: templates/email/recording/shared_data_recorded.html:20
+#: templates/email/recording/shared_data_recorded_team.html:20
+#: templates/email/recording/shared_data_unrecorded.html:20
+#: templates/email/recording/shared_data_unrecorded_team.html:20
+#: templates/email/resubmission/resubmission.html:21
+#: templates/email/sharing/shared_access_given.html:21
+#: templates/email/sharing/shared_access_given_team.html:21
+#: templates/email/sharing/shared_access_removed.html:21
+#: templates/email/sharing/shared_access_removed_team.html:21
 msgid "Best regards"
 msgstr "Beste Grüße"
 
@@ -2263,18 +2298,19 @@ msgstr "Freigegebene Daten geprüft"
 #: templates/email/other/deduction_changed.html:8
 #: templates/email/recording/shared_data_recorded.html:8
 #: templates/email/recording/shared_data_unrecorded.html:8
+#: templates/email/resubmission/resubmission.html:8
 #: templates/email/sharing/shared_access_given.html:8
 #: templates/email/sharing/shared_access_removed.html:8
 msgid "Hello "
 msgstr "Hallo "
 
-#: templates/email/checking/shared_data_checked.html:10
-#: templates/email/checking/shared_data_checked_team.html:10
+#: templates/email/checking/shared_data_checked.html:11
+#: templates/email/checking/shared_data_checked_team.html:11
 msgid "the following dataset has just been checked"
 msgstr "der folgende Datensatz wurde soeben geprüft "
 
-#: templates/email/checking/shared_data_checked.html:16
-#: templates/email/checking/shared_data_checked_team.html:16
+#: templates/email/checking/shared_data_checked.html:17
+#: templates/email/checking/shared_data_checked_team.html:17
 msgid ""
 "This means, the responsible registration office just confirmed the "
 "correctness of this dataset."
@@ -2284,6 +2320,7 @@ msgstr ""
 
 #: templates/email/checking/shared_data_checked_team.html:8
 #: templates/email/deleting/shared_data_deleted_team.html:8
+#: templates/email/other/deduction_changed_team.html:8
 #: templates/email/recording/shared_data_recorded_team.html:8
 #: templates/email/recording/shared_data_unrecorded_team.html:8
 #: templates/email/sharing/shared_access_given_team.html:8
@@ -2296,14 +2333,15 @@ msgstr "Hallo Team"
 msgid "Shared data deleted"
 msgstr "Freigegebene Daten gelöscht"
 
-#: templates/email/deleting/shared_data_deleted.html:10
-#: templates/email/deleting/shared_data_deleted_team.html:10
+#: templates/email/deleting/shared_data_deleted.html:11
+#: templates/email/deleting/shared_data_deleted_team.html:11
 msgid "the following dataset has just been deleted"
 msgstr "der folgende Datensatz wurde soeben gelöscht "
 
-#: templates/email/deleting/shared_data_deleted.html:16
-#: templates/email/deleting/shared_data_deleted_team.html:16
-#: templates/email/other/deduction_changed.html:35
+#: templates/email/deleting/shared_data_deleted.html:17
+#: templates/email/deleting/shared_data_deleted_team.html:17
+#: templates/email/other/deduction_changed.html:38
+#: templates/email/other/deduction_changed_team.html:38
 msgid ""
 "If this should not have been happened, please contact us. See the signature "
 "for details."
@@ -2312,27 +2350,33 @@ msgstr ""
 "mail Signatur finden Sie weitere Kontaktinformationen."
 
 #: templates/email/other/deduction_changed.html:4
+#: templates/email/other/deduction_changed_team.html:4
 msgid "Deduction changed"
 msgstr "Abbuchung geändert"
 
-#: templates/email/other/deduction_changed.html:10
+#: templates/email/other/deduction_changed.html:11
+#: templates/email/other/deduction_changed_team.html:11
 msgid "a deduction of this eco account has changed:"
 msgstr "eine Abbuchung des Ökokontos hat sich geändert:"
 
-#: templates/email/other/deduction_changed.html:14
+#: templates/email/other/deduction_changed.html:16
+#: templates/email/other/deduction_changed_team.html:16
 msgid "Attribute"
 msgstr "Attribute"
 
-#: templates/email/other/deduction_changed.html:15
+#: templates/email/other/deduction_changed.html:17
+#: templates/email/other/deduction_changed_team.html:17
 msgid "Old"
 msgstr "Alt"
 
-#: templates/email/other/deduction_changed.html:16
+#: templates/email/other/deduction_changed.html:18
+#: templates/email/other/deduction_changed_team.html:18
 #: templates/generic_index.html:43 user/templates/user/team/index.html:22
 msgid "New"
 msgstr "Neu"
 
-#: templates/email/other/deduction_changed.html:19
+#: templates/email/other/deduction_changed.html:21
+#: templates/email/other/deduction_changed_team.html:21
 msgid "EcoAccount"
 msgstr "Ökokonto"
 
@@ -2341,19 +2385,19 @@ msgstr "Ökokonto"
 msgid "Shared data recorded"
 msgstr "Freigegebene Daten verzeichnet"
 
-#: templates/email/recording/shared_data_recorded.html:10
-#: templates/email/recording/shared_data_recorded_team.html:10
+#: templates/email/recording/shared_data_recorded.html:11
+#: templates/email/recording/shared_data_recorded_team.html:11
 msgid "the following dataset has just been recorded"
 msgstr "der folgende Datensatz wurde soeben verzeichnet "
 
-#: templates/email/recording/shared_data_recorded.html:16
-#: templates/email/recording/shared_data_recorded_team.html:16
+#: templates/email/recording/shared_data_recorded.html:17
+#: templates/email/recording/shared_data_recorded_team.html:17
 msgid "This means the data is now publicly available, e.g. in LANIS"
 msgstr ""
 "Das bedeutet, dass die Daten nun öffentlich verfügbar sind, z.B. im LANIS."
 
-#: templates/email/recording/shared_data_recorded.html:26
-#: templates/email/recording/shared_data_recorded_team.html:26
+#: templates/email/recording/shared_data_recorded.html:27
+#: templates/email/recording/shared_data_recorded_team.html:27
 msgid ""
 "Please note: Recorded intervention means the compensations are recorded as "
 "well."
@@ -2366,18 +2410,18 @@ msgstr ""
 msgid "Shared data unrecorded"
 msgstr "Freigegebene Daten entzeichnet"
 
-#: templates/email/recording/shared_data_unrecorded.html:10
-#: templates/email/recording/shared_data_unrecorded_team.html:10
+#: templates/email/recording/shared_data_unrecorded.html:11
+#: templates/email/recording/shared_data_unrecorded_team.html:11
 msgid "the following dataset has just been unrecorded"
 msgstr "der folgende Datensatz wurde soeben entzeichnet "
 
-#: templates/email/recording/shared_data_unrecorded.html:16
-#: templates/email/recording/shared_data_unrecorded_team.html:16
+#: templates/email/recording/shared_data_unrecorded.html:17
+#: templates/email/recording/shared_data_unrecorded_team.html:17
 msgid "This means the data is no longer publicly available."
 msgstr "Das bedeutet, dass die Daten nicht länger öffentlich verfügbar sind."
 
-#: templates/email/recording/shared_data_unrecorded.html:26
-#: templates/email/recording/shared_data_unrecorded_team.html:26
+#: templates/email/recording/shared_data_unrecorded.html:27
+#: templates/email/recording/shared_data_unrecorded_team.html:27
 msgid ""
 "Please note: Unrecorded intervention means the compensations are unrecorded "
 "as well."
@@ -2385,22 +2429,30 @@ msgstr ""
 "Bitte beachten Sie: Entzeichnete Eingriffe bedeuten, dass auch die "
 "zugehörigen Kompensationen automatisch entzeichnet worden sind."
 
+#: templates/email/resubmission/resubmission.html:11
+msgid "you wanted to be reminded on this entry."
+msgstr "Sie wollten an diesen Eintrag erinnert werden."
+
+#: templates/email/resubmission/resubmission.html:15
+msgid "Your personal comment:"
+msgstr "Ihr Kommentar:"
+
 #: templates/email/sharing/shared_access_given.html:4
 #: templates/email/sharing/shared_access_given_team.html:4
 msgid "Access shared"
 msgstr "Zugriff freigegeben"
 
-#: templates/email/sharing/shared_access_given.html:10
+#: templates/email/sharing/shared_access_given.html:11
 msgid "the following dataset has just been shared with you"
 msgstr "der folgende Datensatz wurde soeben für Sie freigegeben "
 
-#: templates/email/sharing/shared_access_given.html:16
-#: templates/email/sharing/shared_access_given_team.html:16
+#: templates/email/sharing/shared_access_given.html:17
+#: templates/email/sharing/shared_access_given_team.html:17
 msgid "This means you can now edit this dataset."
 msgstr "Das bedeutet, dass Sie diesen Datensatz nun auch bearbeiten können."
 
-#: templates/email/sharing/shared_access_given.html:17
-#: templates/email/sharing/shared_access_given_team.html:17
+#: templates/email/sharing/shared_access_given.html:18
+#: templates/email/sharing/shared_access_given_team.html:18
 msgid ""
 "The shared dataset appears now by default on your overview for this dataset "
 "type."
@@ -2408,8 +2460,8 @@ msgstr ""
 "Der freigegebene Datensatz ist nun standardmäßig in Ihrer Übersicht für den "
 "Datensatztyp im KSP gelistet."
 
-#: templates/email/sharing/shared_access_given.html:27
-#: templates/email/sharing/shared_access_given_team.html:27
+#: templates/email/sharing/shared_access_given.html:28
+#: templates/email/sharing/shared_access_given_team.html:28
 msgid ""
 "Please note: Shared access on an intervention means you automatically have "
 "editing access to related compensations."
@@ -2418,7 +2470,7 @@ msgstr ""
 "Sie automatisch auch Zugriff auf die zugehörigen Kompensationen erhalten "
 "haben."
 
-#: templates/email/sharing/shared_access_given_team.html:10
+#: templates/email/sharing/shared_access_given_team.html:11
 msgid "the following dataset has just been shared with your team"
 msgstr "der folgende Datensatz wurde soeben für Ihr Team freigegeben "
 
@@ -2427,20 +2479,20 @@ msgstr "der folgende Datensatz wurde soeben für Ihr Team freigegeben "
 msgid "Shared access removed"
 msgstr "Freigegebener Zugriff entzogen"
 
-#: templates/email/sharing/shared_access_removed.html:10
+#: templates/email/sharing/shared_access_removed.html:11
 msgid ""
 "your shared access, including editing, has been revoked for the dataset "
 msgstr ""
 "Ihnen wurde soeben der bearbeitende Zugriff auf den folgenden Datensatz "
 "entzogen: "
 
-#: templates/email/sharing/shared_access_removed.html:16
-#: templates/email/sharing/shared_access_removed_team.html:16
+#: templates/email/sharing/shared_access_removed.html:17
+#: templates/email/sharing/shared_access_removed_team.html:17
 msgid "However, you are still able to view the dataset content."
 msgstr "Sie können den Datensatz aber immer noch im KSP einsehen."
 
-#: templates/email/sharing/shared_access_removed.html:17
-#: templates/email/sharing/shared_access_removed_team.html:17
+#: templates/email/sharing/shared_access_removed.html:18
+#: templates/email/sharing/shared_access_removed_team.html:18
 msgid ""
 "Please use the provided search filter on the dataset`s overview pages to "
 "find them."
@@ -2448,7 +2500,7 @@ msgstr ""
 "Nutzen Sie hierzu einfach die entsprechenden Suchfilter auf den "
 "Übersichtsseiten"
 
-#: templates/email/sharing/shared_access_removed_team.html:10
+#: templates/email/sharing/shared_access_removed_team.html:11
 msgid ""
 "your teams shared access, including editing, has been revoked for the "
 "dataset "

From a6f7e605e6366c6c69d69c52ccd2adb145315cff Mon Sep 17 00:00:00 2001
From: mpeltriaux <Michel.Peltriaux@sgdnord.rlp.de>
Date: Mon, 15 Aug 2022 10:50:01 +0200
Subject: [PATCH 4/4] Migrations + Cleanup

* adds needed migrations
* refactors forms.py (700+ lines) in main konova app
    * splits into forms/ and forms/modals and single class/topic-files for better maintainability and overview
* fixes bug in main konova app migration which could occur if a certain compensation migration did not run before
---
 compensation/forms/modalForms.py              |   2 +-
 .../migrations/0008_auto_20220815_0803.py     |  24 +
 .../migrations/0009_auto_20220815_0803.py     |  32 +
 .../migrations/0010_auto_20220815_1030.py     |  24 +
 compensation/views/compensation.py            |   3 +-
 compensation/views/eco_account.py             |   5 +-
 compensation/views/payment.py                 |   1 -
 ema/forms.py                                  |   5 +-
 ema/migrations/0005_ema_resubmission.py       |  19 +
 ema/migrations/0006_auto_20220815_0803.py     |  23 +
 ema/migrations/0007_auto_20220815_1030.py     |  19 +
 ema/views.py                                  |   3 +-
 intervention/forms/forms.py                   |   3 +-
 intervention/forms/modalForms.py              |   3 +-
 .../0005_intervention_resubmission.py         |  19 +
 .../migrations/0006_auto_20220815_0803.py     |  23 +
 .../migrations/0007_auto_20220815_1030.py     |  19 +
 intervention/views.py                         |   3 +-
 konova/forms.py                               | 760 ------------------
 konova/forms/__init__.py                      |  11 +
 konova/forms/base_form.py                     | 157 ++++
 konova/forms/geometry_form.py                 | 133 +++
 konova/forms/modals/__init__.py               |  12 +
 konova/forms/modals/base_form.py              |  73 ++
 konova/forms/modals/document_form.py          | 163 ++++
 konova/forms/modals/record_form.py            | 123 +++
 konova/forms/modals/remove_form.py            |  58 ++
 konova/forms/modals/resubmission_form.py      |  85 ++
 konova/forms/remove_form.py                   |  54 ++
 konova/migrations/0005_auto_20220216_0856.py  |   1 +
 konova/migrations/0014_resubmission.py        |  33 +
 konova/models/object.py                       |   1 -
 konova/utils/documents.py                     |   5 +-
 user/forms.py                                 |   3 +-
 user/migrations/0006_auto_20220815_0759.py    |  18 +
 35 files changed, 1143 insertions(+), 777 deletions(-)
 create mode 100644 compensation/migrations/0008_auto_20220815_0803.py
 create mode 100644 compensation/migrations/0009_auto_20220815_0803.py
 create mode 100644 compensation/migrations/0010_auto_20220815_1030.py
 create mode 100644 ema/migrations/0005_ema_resubmission.py
 create mode 100644 ema/migrations/0006_auto_20220815_0803.py
 create mode 100644 ema/migrations/0007_auto_20220815_1030.py
 create mode 100644 intervention/migrations/0005_intervention_resubmission.py
 create mode 100644 intervention/migrations/0006_auto_20220815_0803.py
 create mode 100644 intervention/migrations/0007_auto_20220815_1030.py
 delete mode 100644 konova/forms.py
 create mode 100644 konova/forms/__init__.py
 create mode 100644 konova/forms/base_form.py
 create mode 100644 konova/forms/geometry_form.py
 create mode 100644 konova/forms/modals/__init__.py
 create mode 100644 konova/forms/modals/base_form.py
 create mode 100644 konova/forms/modals/document_form.py
 create mode 100644 konova/forms/modals/record_form.py
 create mode 100644 konova/forms/modals/remove_form.py
 create mode 100644 konova/forms/modals/resubmission_form.py
 create mode 100644 konova/forms/remove_form.py
 create mode 100644 konova/migrations/0014_resubmission.py
 create mode 100644 user/migrations/0006_auto_20220815_0759.py

diff --git a/compensation/forms/modalForms.py b/compensation/forms/modalForms.py
index 581a7b3a..ef21aa3f 100644
--- a/compensation/forms/modalForms.py
+++ b/compensation/forms/modalForms.py
@@ -20,7 +20,7 @@ from compensation.models import CompensationDocument, EcoAccountDocument
 from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \
     CompensationStateTreeRadioSelect
 from konova.contexts import BaseContext
-from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
+from konova.forms.modals import BaseModalForm, NewDocumentModalForm, RemoveModalForm
 from konova.models import DeadlineType
 from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, \
     ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
diff --git a/compensation/migrations/0008_auto_20220815_0803.py b/compensation/migrations/0008_auto_20220815_0803.py
new file mode 100644
index 00000000..a4d63132
--- /dev/null
+++ b/compensation/migrations/0008_auto_20220815_0803.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.1.3 on 2022-08-15 06:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('konova', '0014_resubmission'),
+        ('compensation', '0007_auto_20220531_1245'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='compensation',
+            name='resubmission',
+            field=models.ManyToManyField(blank=True, null=True, related_name='_compensation_resubmission_+', to='konova.Resubmission'),
+        ),
+        migrations.AddField(
+            model_name='ecoaccount',
+            name='resubmission',
+            field=models.ManyToManyField(blank=True, null=True, related_name='_ecoaccount_resubmission_+', to='konova.Resubmission'),
+        ),
+    ]
diff --git a/compensation/migrations/0009_auto_20220815_0803.py b/compensation/migrations/0009_auto_20220815_0803.py
new file mode 100644
index 00000000..a7c00e60
--- /dev/null
+++ b/compensation/migrations/0009_auto_20220815_0803.py
@@ -0,0 +1,32 @@
+# Generated by Django 3.1.3 on 2022-08-15 06:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('konova', '0014_resubmission'),
+        ('compensation', '0008_auto_20220815_0803'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='compensation',
+            name='resubmission',
+        ),
+        migrations.RemoveField(
+            model_name='ecoaccount',
+            name='resubmission',
+        ),
+        migrations.AddField(
+            model_name='compensation',
+            name='resubmissions',
+            field=models.ManyToManyField(blank=True, null=True, related_name='_compensation_resubmissions_+', to='konova.Resubmission'),
+        ),
+        migrations.AddField(
+            model_name='ecoaccount',
+            name='resubmissions',
+            field=models.ManyToManyField(blank=True, null=True, related_name='_ecoaccount_resubmissions_+', to='konova.Resubmission'),
+        ),
+    ]
diff --git a/compensation/migrations/0010_auto_20220815_1030.py b/compensation/migrations/0010_auto_20220815_1030.py
new file mode 100644
index 00000000..2d3f16e2
--- /dev/null
+++ b/compensation/migrations/0010_auto_20220815_1030.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.1.3 on 2022-08-15 08:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('konova', '0014_resubmission'),
+        ('compensation', '0009_auto_20220815_0803'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='compensation',
+            name='resubmissions',
+            field=models.ManyToManyField(blank=True, related_name='_compensation_resubmissions_+', to='konova.Resubmission'),
+        ),
+        migrations.AlterField(
+            model_name='ecoaccount',
+            name='resubmissions',
+            field=models.ManyToManyField(blank=True, related_name='_ecoaccount_resubmissions_+', to='konova.Resubmission'),
+        ),
+    ]
diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py
index 016f8ea9..db01a045 100644
--- a/compensation/views/compensation.py
+++ b/compensation/views/compensation.py
@@ -14,8 +14,9 @@ from compensation.tables import CompensationTable
 from intervention.models import Intervention
 from konova.contexts import BaseContext
 from konova.decorators import *
-from konova.forms import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm, \
+from konova.forms.modals import RemoveModalForm,RemoveDeadlineModalForm, EditDocumentModalForm, \
     ResubmissionModalForm
+from konova.forms import SimpleGeomForm
 from konova.models import Deadline
 from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 from konova.utils.documents import get_document, remove_document
diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py
index 03109a8c..2ebeb1f7 100644
--- a/compensation/views/eco_account.py
+++ b/compensation/views/eco_account.py
@@ -25,14 +25,15 @@ from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm,
 from konova.contexts import BaseContext
 from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \
     shared_access_required
-from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentModalForm, RecordModalForm, \
+from konova.forms.modals import RemoveModalForm, RecordModalForm, \
     RemoveDeadlineModalForm, EditDocumentModalForm, ResubmissionModalForm
+from konova.forms import SimpleGeomForm
 from konova.models import Deadline
 from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
 from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 from konova.utils.documents import get_document, remove_document
 from konova.utils.generators import generate_qr_code
-from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
+from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, \
     CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, \
     COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, \
     DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, \
diff --git a/compensation/views/payment.py b/compensation/views/payment.py
index 2be5455e..84fad5bc 100644
--- a/compensation/views/payment.py
+++ b/compensation/views/payment.py
@@ -15,7 +15,6 @@ from compensation.forms.modalForms import NewPaymentForm, RemovePaymentModalForm
 from compensation.models import Payment
 from intervention.models import Intervention
 from konova.decorators import default_group_required, shared_access_required
-from konova.forms import RemoveModalForm
 from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED
 
 
diff --git a/ema/forms.py b/ema/forms.py
index a7e82c4f..93f23490 100644
--- a/ema/forms.py
+++ b/ema/forms.py
@@ -5,8 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
 Created on: 06.10.21
 
 """
-from dal import autocomplete
-from django import forms
 from user.models import User
 from django.db import transaction
 from django.urls import reverse, reverse_lazy
@@ -16,7 +14,8 @@ from compensation.forms.forms import AbstractCompensationForm, CompensationRespo
     PikCompensationFormMixin
 from ema.models import Ema, EmaDocument
 from intervention.models import Responsibility, Handler
-from konova.forms import SimpleGeomForm, NewDocumentModalForm
+from konova.forms import SimpleGeomForm
+from konova.forms.modals import NewDocumentModalForm
 from user.models import UserActionLogEntry
 
 
diff --git a/ema/migrations/0005_ema_resubmission.py b/ema/migrations/0005_ema_resubmission.py
new file mode 100644
index 00000000..57a1fbe7
--- /dev/null
+++ b/ema/migrations/0005_ema_resubmission.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.1.3 on 2022-08-15 06:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('konova', '0014_resubmission'),
+        ('ema', '0004_ema_is_pik'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='ema',
+            name='resubmission',
+            field=models.ManyToManyField(blank=True, null=True, related_name='_ema_resubmission_+', to='konova.Resubmission'),
+        ),
+    ]
diff --git a/ema/migrations/0006_auto_20220815_0803.py b/ema/migrations/0006_auto_20220815_0803.py
new file mode 100644
index 00000000..44ae7657
--- /dev/null
+++ b/ema/migrations/0006_auto_20220815_0803.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.3 on 2022-08-15 06:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('konova', '0014_resubmission'),
+        ('ema', '0005_ema_resubmission'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='ema',
+            name='resubmission',
+        ),
+        migrations.AddField(
+            model_name='ema',
+            name='resubmissions',
+            field=models.ManyToManyField(blank=True, null=True, related_name='_ema_resubmissions_+', to='konova.Resubmission'),
+        ),
+    ]
diff --git a/ema/migrations/0007_auto_20220815_1030.py b/ema/migrations/0007_auto_20220815_1030.py
new file mode 100644
index 00000000..84429174
--- /dev/null
+++ b/ema/migrations/0007_auto_20220815_1030.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.1.3 on 2022-08-15 08:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('konova', '0014_resubmission'),
+        ('ema', '0006_auto_20220815_0803'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='ema',
+            name='resubmissions',
+            field=models.ManyToManyField(blank=True, related_name='_ema_resubmissions_+', to='konova.Resubmission'),
+        ),
+    ]
diff --git a/ema/views.py b/ema/views.py
index 9cd6dd9d..f07187aa 100644
--- a/ema/views.py
+++ b/ema/views.py
@@ -16,8 +16,9 @@ from intervention.forms.modalForms import ShareModalForm
 from konova.contexts import BaseContext
 from konova.decorators import conservation_office_group_required, shared_access_required
 from ema.models import Ema, EmaDocument
-from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm, RemoveDeadlineModalForm, \
+from konova.forms.modals import RemoveModalForm, RecordModalForm, RemoveDeadlineModalForm, \
     EditDocumentModalForm, ResubmissionModalForm
+from konova.forms import SimpleGeomForm
 from konova.models import Deadline
 from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
 from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
diff --git a/intervention/forms/forms.py b/intervention/forms/forms.py
index b85ba101..15b02fd7 100644
--- a/intervention/forms/forms.py
+++ b/intervention/forms/forms.py
@@ -8,6 +8,7 @@ Created on: 02.12.20
 from dal import autocomplete
 from django import forms
 
+from konova.forms.base_form import BaseForm
 from konova.utils.message_templates import EDITED_GENERAL_DATA
 from user.models import User
 from django.db import transaction
@@ -19,7 +20,7 @@ from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \
     CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
 from intervention.inputs import GenerateInput
 from intervention.models import Intervention, Legal, Responsibility, Handler
-from konova.forms import BaseForm, SimpleGeomForm
+from konova.forms.geometry_form import SimpleGeomForm
 from user.models import UserActionLogEntry
 
 
diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py
index b6445a55..a977c1ce 100644
--- a/intervention/forms/modalForms.py
+++ b/intervention/forms/modalForms.py
@@ -19,7 +19,8 @@ from django.utils.translation import gettext_lazy as _
 from compensation.models import EcoAccount, EcoAccountDeduction
 from intervention.inputs import TextToClipboardInput
 from intervention.models import Intervention, InterventionDocument, RevocationDocument
-from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
+from konova.forms.modals import BaseModalForm
+from konova.forms.modals import NewDocumentModalForm, RemoveModalForm
 from konova.utils.general import format_german_float
 from konova.utils.user_checks import is_default_group_only
 
diff --git a/intervention/migrations/0005_intervention_resubmission.py b/intervention/migrations/0005_intervention_resubmission.py
new file mode 100644
index 00000000..ac489238
--- /dev/null
+++ b/intervention/migrations/0005_intervention_resubmission.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.1.3 on 2022-08-15 06:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('konova', '0014_resubmission'),
+        ('intervention', '0004_auto_20220303_0956'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='intervention',
+            name='resubmission',
+            field=models.ManyToManyField(blank=True, null=True, related_name='_intervention_resubmission_+', to='konova.Resubmission'),
+        ),
+    ]
diff --git a/intervention/migrations/0006_auto_20220815_0803.py b/intervention/migrations/0006_auto_20220815_0803.py
new file mode 100644
index 00000000..8d0bf80d
--- /dev/null
+++ b/intervention/migrations/0006_auto_20220815_0803.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.3 on 2022-08-15 06:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('konova', '0014_resubmission'),
+        ('intervention', '0005_intervention_resubmission'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='intervention',
+            name='resubmission',
+        ),
+        migrations.AddField(
+            model_name='intervention',
+            name='resubmissions',
+            field=models.ManyToManyField(blank=True, null=True, related_name='_intervention_resubmissions_+', to='konova.Resubmission'),
+        ),
+    ]
diff --git a/intervention/migrations/0007_auto_20220815_1030.py b/intervention/migrations/0007_auto_20220815_1030.py
new file mode 100644
index 00000000..b7a2729d
--- /dev/null
+++ b/intervention/migrations/0007_auto_20220815_1030.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.1.3 on 2022-08-15 08:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('konova', '0014_resubmission'),
+        ('intervention', '0006_auto_20220815_0803'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='intervention',
+            name='resubmissions',
+            field=models.ManyToManyField(blank=True, related_name='_intervention_resubmissions_+', to='konova.Resubmission'),
+        ),
+    ]
diff --git a/intervention/views.py b/intervention/views.py
index c55fe722..6a9304e9 100644
--- a/intervention/views.py
+++ b/intervention/views.py
@@ -12,7 +12,8 @@ from intervention.models import Intervention, Revocation, InterventionDocument,
 from intervention.tables import InterventionTable
 from konova.contexts import BaseContext
 from konova.decorators import *
-from konova.forms import SimpleGeomForm, RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm
+from konova.forms import SimpleGeomForm
+from konova.forms.modals import RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm
 from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 from konova.utils.documents import remove_document, get_document
 from konova.utils.generators import generate_qr_code
diff --git a/konova/forms.py b/konova/forms.py
deleted file mode 100644
index 5d8f38e2..00000000
--- a/konova/forms.py
+++ /dev/null
@@ -1,760 +0,0 @@
-"""
-Author: Michel Peltriaux
-Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
-Contact: michel.peltriaux@sgdnord.rlp.de
-Created on: 16.11.20
-
-"""
-import json
-from abc import abstractmethod
-
-from bootstrap_modal_forms.forms import BSModalForm
-from bootstrap_modal_forms.utils import is_ajax
-from django import forms
-from django.contrib import messages
-from django.contrib.gis import gdal
-from django.core.exceptions import ObjectDoesNotExist
-from django.db.models.fields.files import FieldFile
-from django.utils.timezone import now
-
-from compensation.models import EcoAccount
-from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
-from user.models import User
-from django.contrib.gis.forms import MultiPolygonField
-from django.contrib.gis.geos import MultiPolygon, Polygon
-from django.db import transaction
-from django.http import HttpRequest, HttpResponseRedirect
-from django.shortcuts import render
-from django.utils.translation import gettext_lazy as _
-
-from konova.contexts import BaseContext
-from konova.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument, Resubmission
-from konova.settings import DEFAULT_SRID
-from konova.tasks import celery_update_parcels
-from konova.utils.message_templates import FORM_INVALID, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE, DOCUMENT_EDITED
-from user.models import UserActionLogEntry
-
-
-class BaseForm(forms.Form):
-    """
-    Basic form for that holds attributes needed in all other forms
-    """
-    template = None
-    action_url = None
-    action_btn_label = _("Save")
-    form_title = None
-    cancel_redirect = None
-    form_caption = None
-    instance = None  # The data holding model object
-    request = None
-    form_attrs = {}  # Holds additional attributes, that can be used in the template
-    has_required_fields = False  # Automatically set. Triggers hint rendering in templates
-    show_cancel_btn = True
-
-    def __init__(self, *args, **kwargs):
-        self.instance = kwargs.pop("instance", None)
-        super().__init__(*args, **kwargs)
-        if self.request is not None:
-            self.user = self.request.user
-        # Check for required fields
-        for _field_name, _field_val in self.fields.items():
-            if _field_val.required:
-                self.has_required_fields = True
-                break
-
-        self.check_for_recorded_instance()
-
-    @abstractmethod
-    def save(self):
-        # To be implemented in subclasses!
-        pass
-
-    def disable_form_field(self, field: str):
-        """
-        Disables a form field for user editing
-        """
-        self.fields[field].widget.attrs["readonly"] = True
-        self.fields[field].disabled = True
-        self.fields[field].widget.attrs["title"] = _("Not editable")
-
-    def initialize_form_field(self, field: str, val):
-        """
-        Initializes a form field with a value
-        """
-        self.fields[field].initial = val
-
-    def add_placeholder_for_field(self, field: str, val):
-        """
-        Adds a placeholder to a field after initialization without the need to redefine the form widget
-
-        Args:
-            field (str): Field name
-            val (str): Placeholder
-
-        Returns:
-
-        """
-        self.fields[field].widget.attrs["placeholder"] = val
-
-    def load_initial_data(self, form_data: dict, disabled_fields: list = None):
-        """ Initializes form data from instance
-
-        Inserts instance data into form and disables form fields
-
-        Returns:
-
-        """
-        if self.instance is None:
-            return
-        for k, v in form_data.items():
-            self.initialize_form_field(k, v)
-        if disabled_fields:
-            for field in disabled_fields:
-                self.disable_form_field(field)
-
-    def add_widget_html_class(self, field: str, cls: str):
-        """ Adds a HTML class string to the widget of a field
-
-        Args:
-            field (str): The field's name
-            cls (str): The new class string
-
-        Returns:
-
-        """
-        set_class = self.fields[field].widget.attrs.get("class", "")
-        if cls in set_class:
-            return
-        else:
-            set_class += " " + cls
-        self.fields[field].widget.attrs["class"] = set_class
-
-    def remove_widget_html_class(self, field: str, cls: str):
-        """ Removes a HTML class string from the widget of a field
-
-        Args:
-            field (str): The field's name
-            cls (str): The new class string
-
-        Returns:
-
-        """
-        set_class = self.fields[field].widget.attrs.get("class", "")
-        set_class = set_class.replace(cls, "")
-        self.fields[field].widget.attrs["class"] = set_class
-
-    def check_for_recorded_instance(self):
-        """ Checks if the instance is recorded and runs some special logic if yes
-
-        If the instance is recorded, the form shall not display any possibility to
-        edit any data. Instead, the users should get some information about why they can not edit anything.
-
-        There are situations where the form should be rendered regularly,
-        e.g deduction forms for (recorded) eco accounts.
-
-        Returns:
-
-        """
-        from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \
-            RemoveEcoAccountDeductionModalForm
-        is_none = self.instance is None
-        is_other_data_type = not isinstance(self.instance, BaseObject)
-        is_deduction_form_from_account = isinstance(
-            self,
-            (
-                NewDeductionModalForm,
-                ResubmissionModalForm,
-                EditEcoAccountDeductionModalForm,
-                RemoveEcoAccountDeductionModalForm,
-            )
-        ) and isinstance(self.instance, EcoAccount)
-
-        if is_none or is_other_data_type or is_deduction_form_from_account:
-            # Do nothing
-            return
-
-        if self.instance.is_recorded:
-            self.template = "form/recorded_no_edit.html"
-
-
-class RemoveForm(BaseForm):
-    check = forms.BooleanField(
-        label=_("Confirm"),
-        label_suffix=_(""),
-        required=True,
-    )
-
-    def __init__(self, *args, **kwargs):
-        self.object_to_remove = kwargs.pop("object_to_remove", None)
-        self.remove_post_url = kwargs.pop("remove_post_url", "")
-        self.cancel_url = kwargs.pop("cancel_url", "")
-
-        super().__init__(*args, **kwargs)
-
-        self.form_title = _("Remove")
-        if self.object_to_remove is not None:
-            self.form_caption = _("You are about to remove {} {}").format(self.object_to_remove.__class__.__name__, self.object_to_remove)
-        self.action_url = self.remove_post_url
-        self.cancel_redirect = self.cancel_url
-
-    def is_checked(self) -> bool:
-        return self.cleaned_data.get("check", False)
-
-    def save(self, user: User):
-        """ Perform generic removing by running the form typical 'save()' method
-
-        Args:
-            user (User): The performing user
-
-        Returns:
-
-        """
-        if self.object_to_remove is not None and self.is_checked():
-            with transaction.atomic():
-                self.object_to_remove.is_active = False
-                action = UserActionLogEntry.get_deleted_action(user)
-                self.object_to_remove.deleted = action
-                self.object_to_remove.save()
-        return self.object_to_remove
-
-
-class BaseModalForm(BaseForm, BSModalForm):
-    """ A specialzed form class for modal form handling
-
-    """
-    is_modal_form = True
-    render_submit = True
-    template = "modal/modal_form.html"
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.action_btn_label = _("Continue")
-
-    def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
-        """ Generic processing of request
-
-        Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used
-
-        Args:
-            request (HttpRequest): The incoming request
-            msg_success (str): The message in case of successful removing
-            msg_error (str): The message in case of an error
-
-        Returns:
-
-        """
-        redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
-        template = self.template
-        if request.method == "POST":
-            if self.is_valid():
-                if not is_ajax(request.META):
-                    # Modal forms send one POST for checking on data validity. This can be used to return possible errors
-                    # on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
-                    # saving/commiting of the data to the database. is_ajax() performs this check. The first request is
-                    # an ajax call, the second is a regular form POST.
-                    self.save()
-                    messages.success(
-                        request,
-                        msg_success
-                    )
-                return HttpResponseRedirect(redirect_url)
-            else:
-                context = {
-                    "form": self,
-                }
-                context = BaseContext(request, context).context
-                return render(request, template, context)
-        elif request.method == "GET":
-            context = {
-                "form": self,
-            }
-            context = BaseContext(request, context).context
-            return render(request, template, context)
-        else:
-            raise NotImplementedError
-
-
-class SimpleGeomForm(BaseForm):
-    """ A geometry form for rendering geometry read-only using a widget
-
-    """
-    read_only = True
-    geom = MultiPolygonField(
-        srid=DEFAULT_SRID_RLP,
-        label=_("Geometry"),
-        help_text=_(""),
-        label_suffix="",
-        required=False,
-        disabled=False,
-    )
-
-    def __init__(self, *args, **kwargs):
-        self.read_only = kwargs.pop("read_only", True)
-        super().__init__(*args, **kwargs)
-
-        # Initialize geometry
-        try:
-            geom = self.instance.geometry.geom
-            self.empty = geom.empty
-
-            if self.empty:
-                raise AttributeError
-
-            geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
-            geom = json.dumps(geojson)
-        except AttributeError:
-            # If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
-            geom = ""
-            self.empty = True
-
-        self.initialize_form_field("geom", geom)
-
-    def is_valid(self):
-        super().is_valid()
-        is_valid = True
-
-        # Get geojson from form
-        geom = self.data["geom"]
-        if geom is None or len(geom) == 0:
-            # empty geometry is a valid geometry
-            return is_valid
-        geom = json.loads(geom)
-
-        # Write submitted data back into form field to make sure invalid geometry
-        # will be rendered again on failed submit
-        self.initialize_form_field("geom", self.data["geom"])
-
-        # Read geojson into gdal geometry
-        # HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for
-        # this case)
-        features = []
-        features_json = geom.get("features", [])
-        for feature in features_json:
-            g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP)
-            if g.geom_type not in ["Polygon", "MultiPolygon"]:
-                self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
-                is_valid = False
-                return is_valid
-
-            polygon = Polygon.from_ewkt(g.ewkt)
-            is_valid = polygon.valid
-            if not is_valid:
-                self.add_error("geom", polygon.valid_reason)
-                return is_valid
-
-            features.append(polygon)
-        form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
-        for feature in features:
-            form_geom = form_geom.union(feature)
-
-        # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
-        if form_geom.geom_type != "MultiPolygon":
-            form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)
-
-        # Write unioned Multipolygon into cleaned data
-        if self.cleaned_data is None:
-            self.cleaned_data = {}
-        self.cleaned_data["geom"] = form_geom.ewkt
-
-        return is_valid
-
-    def save(self, action: UserActionLogEntry):
-        """ Saves the form's geometry
-
-        Creates a new geometry entry if none is set, yet
-
-        Args:
-            action ():
-
-        Returns:
-
-        """
-        try:
-            if self.instance is None or self.instance.geometry is None:
-                raise LookupError
-            geometry = self.instance.geometry
-            geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP))
-            geometry.modified = action
-
-            geometry.save()
-        except LookupError:
-            # No geometry or linked instance holding a geometry exist --> create a new one!
-            geometry = Geometry.objects.create(
-                geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)),
-                created=action,
-            )
-        # Start the parcel update procedure in a background process
-        celery_update_parcels.delay(geometry.id)
-        return geometry
-
-
-class RemoveModalForm(BaseModalForm):
-    """ Generic removing modal form
-
-    Can be used for anything, where removing shall be confirmed by the user a second time.
-
-    """
-    confirm = forms.BooleanField(
-        label=_("Confirm"),
-        label_suffix=_(""),
-        widget=forms.CheckboxInput(),
-        required=True,
-    )
-
-    def __init__(self, *args, **kwargs):
-        self.template = "modal/modal_form.html"
-        super().__init__(*args, **kwargs)
-        self.form_title = _("Remove")
-        self.form_caption = _("Are you sure?")
-        # Disable automatic w-100 setting for this type of modal form. Looks kinda strange
-        self.fields["confirm"].widget.attrs["class"] = ""
-
-    def save(self):
-        if isinstance(self.instance, BaseObject):
-            self.instance.mark_as_deleted(self.user)
-        else:
-            # If the class does not provide restorable delete functionality, we must delete the entry finally
-            self.instance.delete()
-
-
-class RemoveDeadlineModalForm(RemoveModalForm):
-    """ Removing modal form for deadlines
-
-    Can be used for anything, where removing shall be confirmed by the user a second time.
-
-    """
-    deadline = None
-
-    def __init__(self, *args, **kwargs):
-        deadline = kwargs.pop("deadline", None)
-        self.deadline = deadline
-        super().__init__(*args, **kwargs)
-
-    def save(self):
-        self.instance.remove_deadline(self)
-
-
-class NewDocumentModalForm(BaseModalForm):
-    """ Modal form for new documents
-
-    """
-    title = forms.CharField(
-        label=_("Title"),
-        label_suffix=_(""),
-        max_length=500,
-        widget=forms.TextInput(
-            attrs={
-                "class": "form-control",
-            }
-        )
-    )
-    creation_date = forms.DateField(
-        label=_("Created on"),
-        label_suffix=_(""),
-        help_text=_("When has this file been created? Important for photos."),
-        widget=forms.DateInput(
-            attrs={
-                "type": "date",
-                "data-provide": "datepicker",
-                "class": "form-control",
-            },
-            format="%d.%m.%Y"
-        )
-    )
-    file = forms.FileField(
-        label=_("File"),
-        label_suffix=_(""),
-        help_text=_("Allowed formats: pdf, jpg, png. Max size 15 MB."),
-        widget=forms.FileInput(
-            attrs={
-                "class": "form-control-file",
-            }
-        ),
-    )
-    comment = forms.CharField(
-        required=False,
-        max_length=200,
-        label=_("Comment"),
-        label_suffix=_(""),
-        help_text=_("Additional comment, maximum {} letters").format(200),
-        widget=forms.Textarea(
-            attrs={
-                "cols": 30,
-                "rows": 5,
-                "class": "form-control",
-            }
-        )
-    )
-    document_model = None
-
-    class Meta:
-        abstract = True
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.form_title = _("Add new document")
-        self.form_caption = _("")
-        self.form_attrs = {
-            "enctype": "multipart/form-data",  # important for file upload
-        }
-        if not self.document_model:
-            raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))
-
-    def is_valid(self):
-        super_valid = super().is_valid()
-
-        _file = self.cleaned_data.get("file", None)
-
-        if _file is None or isinstance(_file, FieldFile):
-            # FieldFile declares that no new file has been uploaded and we do not need to check on the file again
-            return super_valid
-
-        mime_type_valid = self.document_model.is_mime_type_valid(_file)
-        if not mime_type_valid:
-            self.add_error(
-                "file",
-                FILE_TYPE_UNSUPPORTED
-            )
-
-        file_size_valid = self.document_model.is_file_size_valid(_file)
-        if not file_size_valid:
-            self.add_error(
-                "file",
-                FILE_SIZE_TOO_LARGE
-            )
-
-        file_valid = mime_type_valid and file_size_valid
-        return super_valid and file_valid
-
-    def save(self):
-        with transaction.atomic():
-            action = UserActionLogEntry.get_created_action(self.user)
-            edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document"))
-
-            doc = self.document_model.objects.create(
-                created=action,
-                title=self.cleaned_data["title"],
-                comment=self.cleaned_data["comment"],
-                file=self.cleaned_data["file"],
-                date_of_creation=self.cleaned_data["creation_date"],
-                instance=self.instance,
-            )
-
-            self.instance.log.add(edited_action)
-            self.instance.modified = edited_action
-            self.instance.save()
-
-            return doc
-
-
-class EditDocumentModalForm(NewDocumentModalForm):
-    document = None
-    document_model = AbstractDocument
-
-    def __init__(self, *args, **kwargs):
-        self.document = kwargs.pop("document", None)
-        super().__init__(*args, **kwargs)
-        self.form_title = _("Edit document")
-        form_data = {
-            "title": self.document.title,
-            "comment": self.document.comment,
-            "creation_date": str(self.document.date_of_creation),
-            "file": self.document.file,
-        }
-        self.load_initial_data(form_data)
-
-
-    def save(self):
-        with transaction.atomic():
-            document = self.document
-            file = self.cleaned_data.get("file", None)
-
-            document.title = self.cleaned_data.get("title", None)
-            document.comment = self.cleaned_data.get("comment", None)
-            document.date_of_creation = self.cleaned_data.get("creation_date", None)
-            if not isinstance(file, FieldFile):
-                document.replace_file(file)
-            document.save()
-
-            self.instance.mark_as_edited(self.user, self.request, edit_comment=DOCUMENT_EDITED)
-
-            return document
-
-
-class RecordModalForm(BaseModalForm):
-    """ Modal form for recording data
-
-    """
-    confirm = forms.BooleanField(
-        label=_("Confirm record"),
-        label_suffix="",
-        widget=forms.CheckboxInput(),
-        required=True,
-    )
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.form_title = _("Record data")
-        self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name)
-        # Disable automatic w-100 setting for this type of modal form. Looks kinda strange
-        self.fields["confirm"].widget.attrs["class"] = ""
-
-        if self.instance.recorded:
-            # unrecord!
-            self.fields["confirm"].label = _("Confirm unrecord")
-            self.form_title = _("Unrecord data")
-            self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name)
-
-        if not isinstance(self.instance, RecordableObjectMixin):
-            raise NotImplementedError
-
-    def is_valid(self):
-        """ Checks for instance's validity and data quality
-
-        Returns:
-
-        """
-        from intervention.models import Intervention
-        super_val = super().is_valid()
-        if self.instance.recorded:
-            # If user wants to unrecord an already recorded dataset, we do not need to perform custom checks
-            return super_val
-        checker = self.instance.quality_check()
-        for msg in checker.messages:
-            self.add_error(
-                "confirm",
-                msg
-            )
-        valid = checker.valid
-        # Special case: Intervention
-        # Add direct checks for related compensations
-        if isinstance(self.instance, Intervention):
-            comps_valid = self._are_compensations_valid()
-            valid = valid and comps_valid
-        return super_val and valid
-
-    def _are_deductions_valid(self):
-        """ Performs validity checks on deductions and their eco-account
-
-        Returns:
-
-        """
-        deductions = self.instance.deductions.all()
-        for deduction in deductions:
-            checker = deduction.account.quality_check()
-            for msg in checker.messages:
-                self.add_error(
-                    "confirm",
-                    f"{deduction.account.identifier}: {msg}"
-                )
-            return checker.valid
-        return True
-
-    def _are_compensations_valid(self):
-        """ Runs a special case for intervention-compensations validity
-
-        Returns:
-
-        """
-        comps = self.instance.compensations.filter(
-            deleted=None,
-        )
-        comps_valid = True
-        for comp in comps:
-            checker = comp.quality_check()
-            comps_valid = comps_valid and checker.valid
-            for msg in checker.messages:
-                self.add_error(
-                    "confirm",
-                    f"{comp.identifier}: {msg}"
-                )
-
-        deductions_valid = self._are_deductions_valid()
-
-        return comps_valid and deductions_valid
-
-    def save(self):
-        with transaction.atomic():
-            if self.cleaned_data["confirm"]:
-                if self.instance.recorded:
-                    self.instance.set_unrecorded(self.user)
-                else:
-                    self.instance.set_recorded(self.user)
-        return self.instance
-
-    def check_for_recorded_instance(self):
-        """ Overwrite the check method for doing nothing on the RecordModalForm
-
-        Returns:
-
-        """
-        pass
-
-
-class ResubmissionModalForm(BaseModalForm):
-    date = forms.DateField(
-        label_suffix=_(""),
-        label=_("Date"),
-        help_text=_("When do you want to be reminded?"),
-        widget=forms.DateInput(
-            attrs={
-                "type": "date",
-                "data-provide": "datepicker",
-                "class": "form-control",
-            },
-            format="%d.%m.%Y"
-        )
-    )
-    comment = forms.CharField(
-        required=False,
-        label=_("Comment"),
-        label_suffix=_(""),
-        help_text=_("Additional comment"),
-        widget=forms.Textarea(
-            attrs={
-                "cols": 30,
-                "rows": 5,
-                "class": "form-control",
-            }
-        )
-    )
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.form_title = _("Resubmission")
-        self.form_caption = _("Set your resubmission for this entry.")
-        self.action_url = None
-
-        try:
-            self.resubmission = self.instance.resubmissions.get(
-                user=self.user
-            )
-            self.initialize_form_field("date", str(self.resubmission.resubmit_on))
-            self.initialize_form_field("comment", self.resubmission.comment)
-        except ObjectDoesNotExist:
-            self.resubmission = Resubmission()
-
-    def is_valid(self):
-        super_valid = super().is_valid()
-        self_valid = True
-
-        date = self.cleaned_data.get("date")
-        today = now().today().date()
-        if date <= today:
-            self.add_error(
-                "date",
-                _("The date should be in the future")
-            )
-            self_valid = False
-
-        return super_valid and self_valid
-
-    def save(self):
-        with transaction.atomic():
-            self.resubmission.user = self.user
-            self.resubmission.resubmit_on = self.cleaned_data.get("date")
-            self.resubmission.comment = self.cleaned_data.get("comment")
-            self.resubmission.save()
-            self.instance.resubmissions.add(self.resubmission)
-        return self.resubmission
-
diff --git a/konova/forms/__init__.py b/konova/forms/__init__.py
new file mode 100644
index 00000000..5840c4d2
--- /dev/null
+++ b/konova/forms/__init__.py
@@ -0,0 +1,11 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+
+from .base_form import *
+from .geometry_form import *
+from .remove_form import *
\ No newline at end of file
diff --git a/konova/forms/base_form.py b/konova/forms/base_form.py
new file mode 100644
index 00000000..065fba17
--- /dev/null
+++ b/konova/forms/base_form.py
@@ -0,0 +1,157 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+from abc import abstractmethod
+
+from django import forms
+from django.utils.translation import gettext_lazy as _
+
+from compensation.models import EcoAccount
+from konova.models import BaseObject
+
+
+class BaseForm(forms.Form):
+    """
+    Basic form for that holds attributes needed in all other forms
+    """
+    template = None
+    action_url = None
+    action_btn_label = _("Save")
+    form_title = None
+    cancel_redirect = None
+    form_caption = None
+    instance = None  # The data holding model object
+    request = None
+    form_attrs = {}  # Holds additional attributes, that can be used in the template
+    has_required_fields = False  # Automatically set. Triggers hint rendering in templates
+    show_cancel_btn = True
+
+    def __init__(self, *args, **kwargs):
+        self.instance = kwargs.pop("instance", None)
+        super().__init__(*args, **kwargs)
+        if self.request is not None:
+            self.user = self.request.user
+        # Check for required fields
+        for _field_name, _field_val in self.fields.items():
+            if _field_val.required:
+                self.has_required_fields = True
+                break
+
+        self.check_for_recorded_instance()
+
+    @abstractmethod
+    def save(self):
+        # To be implemented in subclasses!
+        pass
+
+    def disable_form_field(self, field: str):
+        """
+        Disables a form field for user editing
+        """
+        self.fields[field].widget.attrs["readonly"] = True
+        self.fields[field].disabled = True
+        self.fields[field].widget.attrs["title"] = _("Not editable")
+
+    def initialize_form_field(self, field: str, val):
+        """
+        Initializes a form field with a value
+        """
+        self.fields[field].initial = val
+
+    def add_placeholder_for_field(self, field: str, val):
+        """
+        Adds a placeholder to a field after initialization without the need to redefine the form widget
+
+        Args:
+            field (str): Field name
+            val (str): Placeholder
+
+        Returns:
+
+        """
+        self.fields[field].widget.attrs["placeholder"] = val
+
+    def load_initial_data(self, form_data: dict, disabled_fields: list = None):
+        """ Initializes form data from instance
+
+        Inserts instance data into form and disables form fields
+
+        Returns:
+
+        """
+        if self.instance is None:
+            return
+        for k, v in form_data.items():
+            self.initialize_form_field(k, v)
+        if disabled_fields:
+            for field in disabled_fields:
+                self.disable_form_field(field)
+
+    def add_widget_html_class(self, field: str, cls: str):
+        """ Adds a HTML class string to the widget of a field
+
+        Args:
+            field (str): The field's name
+            cls (str): The new class string
+
+        Returns:
+
+        """
+        set_class = self.fields[field].widget.attrs.get("class", "")
+        if cls in set_class:
+            return
+        else:
+            set_class += " " + cls
+        self.fields[field].widget.attrs["class"] = set_class
+
+    def remove_widget_html_class(self, field: str, cls: str):
+        """ Removes a HTML class string from the widget of a field
+
+        Args:
+            field (str): The field's name
+            cls (str): The new class string
+
+        Returns:
+
+        """
+        set_class = self.fields[field].widget.attrs.get("class", "")
+        set_class = set_class.replace(cls, "")
+        self.fields[field].widget.attrs["class"] = set_class
+
+    def check_for_recorded_instance(self):
+        """ Checks if the instance is recorded and runs some special logic if yes
+
+        If the instance is recorded, the form shall not display any possibility to
+        edit any data. Instead, the users should get some information about why they can not edit anything.
+
+        There are situations where the form should be rendered regularly,
+        e.g deduction forms for (recorded) eco accounts.
+
+        Returns:
+
+        """
+        from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \
+            RemoveEcoAccountDeductionModalForm
+        from konova.forms.modals.resubmission_form import ResubmissionModalForm
+        is_none = self.instance is None
+        is_other_data_type = not isinstance(self.instance, BaseObject)
+        is_deduction_form_from_account = isinstance(
+            self,
+            (
+                NewDeductionModalForm,
+                ResubmissionModalForm,
+                EditEcoAccountDeductionModalForm,
+                RemoveEcoAccountDeductionModalForm,
+            )
+        ) and isinstance(self.instance, EcoAccount)
+
+        if is_none or is_other_data_type or is_deduction_form_from_account:
+            # Do nothing
+            return
+
+        if self.instance.is_recorded:
+            self.template = "form/recorded_no_edit.html"
diff --git a/konova/forms/geometry_form.py b/konova/forms/geometry_form.py
new file mode 100644
index 00000000..3c957aa7
--- /dev/null
+++ b/konova/forms/geometry_form.py
@@ -0,0 +1,133 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+import json
+
+from django.contrib.gis import gdal
+from django.contrib.gis.forms import MultiPolygonField
+from django.contrib.gis.geos import MultiPolygon, Polygon
+from django.utils.translation import gettext_lazy as _
+
+from konova.forms.base_form import BaseForm
+from konova.models import Geometry
+from konova.tasks import celery_update_parcels
+from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
+from user.models import UserActionLogEntry
+
+
+class SimpleGeomForm(BaseForm):
+    """ A geometry form for rendering geometry read-only using a widget
+
+    """
+    read_only = True
+    geom = MultiPolygonField(
+        srid=DEFAULT_SRID_RLP,
+        label=_("Geometry"),
+        help_text=_(""),
+        label_suffix="",
+        required=False,
+        disabled=False,
+    )
+
+    def __init__(self, *args, **kwargs):
+        self.read_only = kwargs.pop("read_only", True)
+        super().__init__(*args, **kwargs)
+
+        # Initialize geometry
+        try:
+            geom = self.instance.geometry.geom
+            self.empty = geom.empty
+
+            if self.empty:
+                raise AttributeError
+
+            geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
+            geom = json.dumps(geojson)
+        except AttributeError:
+            # If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
+            geom = ""
+            self.empty = True
+
+        self.initialize_form_field("geom", geom)
+
+    def is_valid(self):
+        super().is_valid()
+        is_valid = True
+
+        # Get geojson from form
+        geom = self.data["geom"]
+        if geom is None or len(geom) == 0:
+            # empty geometry is a valid geometry
+            return is_valid
+        geom = json.loads(geom)
+
+        # Write submitted data back into form field to make sure invalid geometry
+        # will be rendered again on failed submit
+        self.initialize_form_field("geom", self.data["geom"])
+
+        # Read geojson into gdal geometry
+        # HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for
+        # this case)
+        features = []
+        features_json = geom.get("features", [])
+        for feature in features_json:
+            g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP)
+            if g.geom_type not in ["Polygon", "MultiPolygon"]:
+                self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
+                is_valid = False
+                return is_valid
+
+            polygon = Polygon.from_ewkt(g.ewkt)
+            is_valid = polygon.valid
+            if not is_valid:
+                self.add_error("geom", polygon.valid_reason)
+                return is_valid
+
+            features.append(polygon)
+        form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
+        for feature in features:
+            form_geom = form_geom.union(feature)
+
+        # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
+        if form_geom.geom_type != "MultiPolygon":
+            form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)
+
+        # Write unioned Multipolygon into cleaned data
+        if self.cleaned_data is None:
+            self.cleaned_data = {}
+        self.cleaned_data["geom"] = form_geom.ewkt
+
+        return is_valid
+
+    def save(self, action: UserActionLogEntry):
+        """ Saves the form's geometry
+
+        Creates a new geometry entry if none is set, yet
+
+        Args:
+            action ():
+
+        Returns:
+
+        """
+        try:
+            if self.instance is None or self.instance.geometry is None:
+                raise LookupError
+            geometry = self.instance.geometry
+            geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP))
+            geometry.modified = action
+
+            geometry.save()
+        except LookupError:
+            # No geometry or linked instance holding a geometry exist --> create a new one!
+            geometry = Geometry.objects.create(
+                geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)),
+                created=action,
+            )
+        # Start the parcel update procedure in a background process
+        celery_update_parcels.delay(geometry.id)
+        return geometry
\ No newline at end of file
diff --git a/konova/forms/modals/__init__.py b/konova/forms/modals/__init__.py
new file mode 100644
index 00000000..f922f2de
--- /dev/null
+++ b/konova/forms/modals/__init__.py
@@ -0,0 +1,12 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+from .base_form import *
+from .document_form import *
+from .record_form import *
+from .remove_form import *
+from .resubmission_form import *
diff --git a/konova/forms/modals/base_form.py b/konova/forms/modals/base_form.py
new file mode 100644
index 00000000..a6806578
--- /dev/null
+++ b/konova/forms/modals/base_form.py
@@ -0,0 +1,73 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+from bootstrap_modal_forms.forms import BSModalForm
+from bootstrap_modal_forms.utils import is_ajax
+from django.contrib import messages
+from django.http import HttpResponseRedirect, HttpRequest
+from django.shortcuts import render
+from django.utils.translation import gettext_lazy as _
+
+from konova.contexts import BaseContext
+from konova.forms.base_form import BaseForm
+from konova.utils.message_templates import FORM_INVALID
+
+
+class BaseModalForm(BaseForm, BSModalForm):
+    """ A specialzed form class for modal form handling
+
+    """
+    is_modal_form = True
+    render_submit = True
+    template = "modal/modal_form.html"
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.action_btn_label = _("Continue")
+
+    def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
+        """ Generic processing of request
+
+        Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used
+
+        Args:
+            request (HttpRequest): The incoming request
+            msg_success (str): The message in case of successful removing
+            msg_error (str): The message in case of an error
+
+        Returns:
+
+        """
+        redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
+        template = self.template
+        if request.method == "POST":
+            if self.is_valid():
+                if not is_ajax(request.META):
+                    # Modal forms send one POST for checking on data validity. This can be used to return possible errors
+                    # on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
+                    # saving/commiting of the data to the database. is_ajax() performs this check. The first request is
+                    # an ajax call, the second is a regular form POST.
+                    self.save()
+                    messages.success(
+                        request,
+                        msg_success
+                    )
+                return HttpResponseRedirect(redirect_url)
+            else:
+                context = {
+                    "form": self,
+                }
+                context = BaseContext(request, context).context
+                return render(request, template, context)
+        elif request.method == "GET":
+            context = {
+                "form": self,
+            }
+            context = BaseContext(request, context).context
+            return render(request, template, context)
+        else:
+            raise NotImplementedError
diff --git a/konova/forms/modals/document_form.py b/konova/forms/modals/document_form.py
new file mode 100644
index 00000000..96b4f8e8
--- /dev/null
+++ b/konova/forms/modals/document_form.py
@@ -0,0 +1,163 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+from django import forms
+from django.db import transaction
+from django.db.models.fields.files import FieldFile
+from django.utils.translation import gettext_lazy as _
+
+from konova.forms.modals.base_form import BaseModalForm
+from konova.models import AbstractDocument
+from konova.utils.message_templates import DOCUMENT_EDITED, FILE_SIZE_TOO_LARGE, FILE_TYPE_UNSUPPORTED
+from user.models import UserActionLogEntry
+
+
+class NewDocumentModalForm(BaseModalForm):
+    """ Modal form for new documents
+
+    """
+    title = forms.CharField(
+        label=_("Title"),
+        label_suffix=_(""),
+        max_length=500,
+        widget=forms.TextInput(
+            attrs={
+                "class": "form-control",
+            }
+        )
+    )
+    creation_date = forms.DateField(
+        label=_("Created on"),
+        label_suffix=_(""),
+        help_text=_("When has this file been created? Important for photos."),
+        widget=forms.DateInput(
+            attrs={
+                "type": "date",
+                "data-provide": "datepicker",
+                "class": "form-control",
+            },
+            format="%d.%m.%Y"
+        )
+    )
+    file = forms.FileField(
+        label=_("File"),
+        label_suffix=_(""),
+        help_text=_("Allowed formats: pdf, jpg, png. Max size 15 MB."),
+        widget=forms.FileInput(
+            attrs={
+                "class": "form-control-file",
+            }
+        ),
+    )
+    comment = forms.CharField(
+        required=False,
+        max_length=200,
+        label=_("Comment"),
+        label_suffix=_(""),
+        help_text=_("Additional comment, maximum {} letters").format(200),
+        widget=forms.Textarea(
+            attrs={
+                "cols": 30,
+                "rows": 5,
+                "class": "form-control",
+            }
+        )
+    )
+    document_model = None
+
+    class Meta:
+        abstract = True
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.form_title = _("Add new document")
+        self.form_caption = _("")
+        self.form_attrs = {
+            "enctype": "multipart/form-data",  # important for file upload
+        }
+        if not self.document_model:
+            raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))
+
+    def is_valid(self):
+        super_valid = super().is_valid()
+
+        _file = self.cleaned_data.get("file", None)
+
+        if _file is None or isinstance(_file, FieldFile):
+            # FieldFile declares that no new file has been uploaded and we do not need to check on the file again
+            return super_valid
+
+        mime_type_valid = self.document_model.is_mime_type_valid(_file)
+        if not mime_type_valid:
+            self.add_error(
+                "file",
+                FILE_TYPE_UNSUPPORTED
+            )
+
+        file_size_valid = self.document_model.is_file_size_valid(_file)
+        if not file_size_valid:
+            self.add_error(
+                "file",
+                FILE_SIZE_TOO_LARGE
+            )
+
+        file_valid = mime_type_valid and file_size_valid
+        return super_valid and file_valid
+
+    def save(self):
+        with transaction.atomic():
+            action = UserActionLogEntry.get_created_action(self.user)
+            edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document"))
+
+            doc = self.document_model.objects.create(
+                created=action,
+                title=self.cleaned_data["title"],
+                comment=self.cleaned_data["comment"],
+                file=self.cleaned_data["file"],
+                date_of_creation=self.cleaned_data["creation_date"],
+                instance=self.instance,
+            )
+
+            self.instance.log.add(edited_action)
+            self.instance.modified = edited_action
+            self.instance.save()
+
+            return doc
+
+
+class EditDocumentModalForm(NewDocumentModalForm):
+    document = None
+    document_model = AbstractDocument
+
+    def __init__(self, *args, **kwargs):
+        self.document = kwargs.pop("document", None)
+        super().__init__(*args, **kwargs)
+        self.form_title = _("Edit document")
+        form_data = {
+            "title": self.document.title,
+            "comment": self.document.comment,
+            "creation_date": str(self.document.date_of_creation),
+            "file": self.document.file,
+        }
+        self.load_initial_data(form_data)
+
+    def save(self):
+        with transaction.atomic():
+            document = self.document
+            file = self.cleaned_data.get("file", None)
+
+            document.title = self.cleaned_data.get("title", None)
+            document.comment = self.cleaned_data.get("comment", None)
+            document.date_of_creation = self.cleaned_data.get("creation_date", None)
+            if not isinstance(file, FieldFile):
+                document.replace_file(file)
+            document.save()
+
+            self.instance.mark_as_edited(self.user, self.request, edit_comment=DOCUMENT_EDITED)
+
+            return document
+
diff --git a/konova/forms/modals/record_form.py b/konova/forms/modals/record_form.py
new file mode 100644
index 00000000..812b697a
--- /dev/null
+++ b/konova/forms/modals/record_form.py
@@ -0,0 +1,123 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+from django import forms
+from django.db import transaction
+from django.utils.translation import gettext_lazy as _
+
+from konova.forms.modals.base_form import BaseModalForm
+from konova.models import RecordableObjectMixin
+
+
+class RecordModalForm(BaseModalForm):
+    """ Modal form for recording data
+
+    """
+    confirm = forms.BooleanField(
+        label=_("Confirm record"),
+        label_suffix="",
+        widget=forms.CheckboxInput(),
+        required=True,
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.form_title = _("Record data")
+        self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name)
+        # Disable automatic w-100 setting for this type of modal form. Looks kinda strange
+        self.fields["confirm"].widget.attrs["class"] = ""
+
+        if self.instance.recorded:
+            # unrecord!
+            self.fields["confirm"].label = _("Confirm unrecord")
+            self.form_title = _("Unrecord data")
+            self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name)
+
+        if not isinstance(self.instance, RecordableObjectMixin):
+            raise NotImplementedError
+
+    def is_valid(self):
+        """ Checks for instance's validity and data quality
+
+        Returns:
+
+        """
+        from intervention.models import Intervention
+        super_val = super().is_valid()
+        if self.instance.recorded:
+            # If user wants to unrecord an already recorded dataset, we do not need to perform custom checks
+            return super_val
+        checker = self.instance.quality_check()
+        for msg in checker.messages:
+            self.add_error(
+                "confirm",
+                msg
+            )
+        valid = checker.valid
+        # Special case: Intervention
+        # Add direct checks for related compensations
+        if isinstance(self.instance, Intervention):
+            comps_valid = self._are_compensations_valid()
+            valid = valid and comps_valid
+        return super_val and valid
+
+    def _are_deductions_valid(self):
+        """ Performs validity checks on deductions and their eco-account
+
+        Returns:
+
+        """
+        deductions = self.instance.deductions.all()
+        for deduction in deductions:
+            checker = deduction.account.quality_check()
+            for msg in checker.messages:
+                self.add_error(
+                    "confirm",
+                    f"{deduction.account.identifier}: {msg}"
+                )
+            return checker.valid
+        return True
+
+    def _are_compensations_valid(self):
+        """ Runs a special case for intervention-compensations validity
+
+        Returns:
+
+        """
+        comps = self.instance.compensations.filter(
+            deleted=None,
+        )
+        comps_valid = True
+        for comp in comps:
+            checker = comp.quality_check()
+            comps_valid = comps_valid and checker.valid
+            for msg in checker.messages:
+                self.add_error(
+                    "confirm",
+                    f"{comp.identifier}: {msg}"
+                )
+
+        deductions_valid = self._are_deductions_valid()
+
+        return comps_valid and deductions_valid
+
+    def save(self):
+        with transaction.atomic():
+            if self.cleaned_data["confirm"]:
+                if self.instance.recorded:
+                    self.instance.set_unrecorded(self.user)
+                else:
+                    self.instance.set_recorded(self.user)
+        return self.instance
+
+    def check_for_recorded_instance(self):
+        """ Overwrite the check method for doing nothing on the RecordModalForm
+
+        Returns:
+
+        """
+        pass
diff --git a/konova/forms/modals/remove_form.py b/konova/forms/modals/remove_form.py
new file mode 100644
index 00000000..7a146268
--- /dev/null
+++ b/konova/forms/modals/remove_form.py
@@ -0,0 +1,58 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+from django import forms
+from django.utils.translation import gettext_lazy as _
+
+from konova.forms.modals.base_form import BaseModalForm
+from konova.models import BaseObject
+
+
+class RemoveModalForm(BaseModalForm):
+    """ Generic removing modal form
+
+    Can be used for anything, where removing shall be confirmed by the user a second time.
+
+    """
+    confirm = forms.BooleanField(
+        label=_("Confirm"),
+        label_suffix=_(""),
+        widget=forms.CheckboxInput(),
+        required=True,
+    )
+
+    def __init__(self, *args, **kwargs):
+        self.template = "modal/modal_form.html"
+        super().__init__(*args, **kwargs)
+        self.form_title = _("Remove")
+        self.form_caption = _("Are you sure?")
+        # Disable automatic w-100 setting for this type of modal form. Looks kinda strange
+        self.fields["confirm"].widget.attrs["class"] = ""
+
+    def save(self):
+        if isinstance(self.instance, BaseObject):
+            self.instance.mark_as_deleted(self.user)
+        else:
+            # If the class does not provide restorable delete functionality, we must delete the entry finally
+            self.instance.delete()
+
+
+class RemoveDeadlineModalForm(RemoveModalForm):
+    """ Removing modal form for deadlines
+
+    Can be used for anything, where removing shall be confirmed by the user a second time.
+
+    """
+    deadline = None
+
+    def __init__(self, *args, **kwargs):
+        deadline = kwargs.pop("deadline", None)
+        self.deadline = deadline
+        super().__init__(*args, **kwargs)
+
+    def save(self):
+        self.instance.remove_deadline(self)
\ No newline at end of file
diff --git a/konova/forms/modals/resubmission_form.py b/konova/forms/modals/resubmission_form.py
new file mode 100644
index 00000000..d1d846f6
--- /dev/null
+++ b/konova/forms/modals/resubmission_form.py
@@ -0,0 +1,85 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+import datetime
+
+from django import forms
+from django.core.exceptions import ObjectDoesNotExist
+from django.db import transaction
+from django.utils.translation import gettext_lazy as _
+
+from konova.forms.modals.base_form import BaseModalForm
+from konova.models import Resubmission
+
+
+class ResubmissionModalForm(BaseModalForm):
+    date = forms.DateField(
+        label_suffix=_(""),
+        label=_("Date"),
+        help_text=_("When do you want to be reminded?"),
+        widget=forms.DateInput(
+            attrs={
+                "type": "date",
+                "data-provide": "datepicker",
+                "class": "form-control",
+            },
+            format="%d.%m.%Y"
+        )
+    )
+    comment = forms.CharField(
+        required=False,
+        label=_("Comment"),
+        label_suffix=_(""),
+        help_text=_("Additional comment"),
+        widget=forms.Textarea(
+            attrs={
+                "cols": 30,
+                "rows": 5,
+                "class": "form-control",
+            }
+        )
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.form_title = _("Resubmission")
+        self.form_caption = _("Set your resubmission for this entry.")
+        self.action_url = None
+
+        try:
+            self.resubmission = self.instance.resubmissions.get(
+                user=self.user
+            )
+            self.initialize_form_field("date", str(self.resubmission.resubmit_on))
+            self.initialize_form_field("comment", self.resubmission.comment)
+        except ObjectDoesNotExist:
+            self.resubmission = Resubmission()
+
+    def is_valid(self):
+        super_valid = super().is_valid()
+        self_valid = True
+
+        date = self.cleaned_data.get("date")
+        today = datetime.date.today()
+        if date <= today:
+            self.add_error(
+                "date",
+                _("The date should be in the future")
+            )
+            self_valid = False
+
+        return super_valid and self_valid
+
+    def save(self):
+        with transaction.atomic():
+            self.resubmission.user = self.user
+            self.resubmission.resubmit_on = self.cleaned_data.get("date")
+            self.resubmission.comment = self.cleaned_data.get("comment")
+            self.resubmission.save()
+            self.instance.resubmissions.add(self.resubmission)
+        return self.resubmission
+
diff --git a/konova/forms/remove_form.py b/konova/forms/remove_form.py
new file mode 100644
index 00000000..d5c884a6
--- /dev/null
+++ b/konova/forms/remove_form.py
@@ -0,0 +1,54 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+from django import forms
+from django.db import transaction
+from django.utils.translation import gettext_lazy as _
+
+from konova.forms.base_form import BaseForm
+from user.models import UserActionLogEntry, User
+
+
+class RemoveForm(BaseForm):
+    check = forms.BooleanField(
+        label=_("Confirm"),
+        label_suffix=_(""),
+        required=True,
+    )
+
+    def __init__(self, *args, **kwargs):
+        self.object_to_remove = kwargs.pop("object_to_remove", None)
+        self.remove_post_url = kwargs.pop("remove_post_url", "")
+        self.cancel_url = kwargs.pop("cancel_url", "")
+
+        super().__init__(*args, **kwargs)
+
+        self.form_title = _("Remove")
+        if self.object_to_remove is not None:
+            self.form_caption = _("You are about to remove {} {}").format(self.object_to_remove.__class__.__name__, self.object_to_remove)
+        self.action_url = self.remove_post_url
+        self.cancel_redirect = self.cancel_url
+
+    def is_checked(self) -> bool:
+        return self.cleaned_data.get("check", False)
+
+    def save(self, user: User):
+        """ Perform generic removing by running the form typical 'save()' method
+
+        Args:
+            user (User): The performing user
+
+        Returns:
+
+        """
+        if self.object_to_remove is not None and self.is_checked():
+            with transaction.atomic():
+                self.object_to_remove.is_active = False
+                action = UserActionLogEntry.get_deleted_action(user)
+                self.object_to_remove.deleted = action
+                self.object_to_remove.save()
+        return self.object_to_remove
diff --git a/konova/migrations/0005_auto_20220216_0856.py b/konova/migrations/0005_auto_20220216_0856.py
index 567e2065..8626b7c5 100644
--- a/konova/migrations/0005_auto_20220216_0856.py
+++ b/konova/migrations/0005_auto_20220216_0856.py
@@ -33,6 +33,7 @@ class Migration(migrations.Migration):
 
     dependencies = [
         ('konova', '0004_auto_20220209_0839'),
+        ('compensation', '0002_auto_20220114_0936'),
     ]
 
     operations = [
diff --git a/konova/migrations/0014_resubmission.py b/konova/migrations/0014_resubmission.py
new file mode 100644
index 00000000..f0ef9e7e
--- /dev/null
+++ b/konova/migrations/0014_resubmission.py
@@ -0,0 +1,33 @@
+# Generated by Django 3.1.3 on 2022-08-15 06:03
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('user', '0006_auto_20220815_0759'),
+        ('konova', '0013_auto_20220713_0814'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Resubmission',
+            fields=[
+                ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+                ('resubmit_on', models.DateField(help_text='On which date the resubmission should be performed')),
+                ('resubmission_sent', models.BooleanField(default=False, help_text='Whether a resubmission has been sent or not')),
+                ('comment', models.TextField(blank=True, help_text='Optional comment for the user itself', null=True)),
+                ('created', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='user.useractionlogentry')),
+                ('modified', models.ForeignKey(blank=True, help_text='Last modified', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='user.useractionlogentry')),
+                ('user', models.ForeignKey(help_text='The user who wants to be notifed', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+    ]
diff --git a/konova/models/object.py b/konova/models/object.py
index 0fbd6e8b..8af95e18 100644
--- a/konova/models/object.py
+++ b/konova/models/object.py
@@ -749,7 +749,6 @@ class GeoReferencedMixin(models.Model):
 class ResubmitableObjectMixin(models.Model):
     resubmissions = models.ManyToManyField(
         "konova.Resubmission",
-        null=True,
         blank=True,
         related_name="+",
     )
diff --git a/konova/utils/documents.py b/konova/utils/documents.py
index f9b15160..3e8f6f12 100644
--- a/konova/utils/documents.py
+++ b/konova/utils/documents.py
@@ -5,10 +5,9 @@ Contact: michel.peltriaux@sgdnord.rlp.de
 Created on: 01.09.21
 
 """
-from django.http import FileResponse, HttpRequest, HttpResponse, Http404
-from django.utils.translation import gettext_lazy as _
+from django.http import FileResponse, HttpRequest, Http404
 
-from konova.forms import RemoveModalForm
+from konova.forms.modals import RemoveModalForm
 from konova.models import AbstractDocument
 from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE
 
diff --git a/user/forms.py b/user/forms.py
index 12688b46..0649eaf3 100644
--- a/user/forms.py
+++ b/user/forms.py
@@ -15,7 +15,8 @@ from api.models import APIUserToken
 from intervention.inputs import GenerateInput
 from user.models import User, UserNotification, Team
 
-from konova.forms import BaseForm, BaseModalForm, RemoveModalForm
+from konova.forms.modals import BaseModalForm, RemoveModalForm
+from konova.forms import BaseForm
 
 
 class UserNotificationForm(BaseForm):
diff --git a/user/migrations/0006_auto_20220815_0759.py b/user/migrations/0006_auto_20220815_0759.py
new file mode 100644
index 00000000..53861083
--- /dev/null
+++ b/user/migrations/0006_auto_20220815_0759.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.3 on 2022-08-15 05:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('user', '0005_team_deleted'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='usernotification',
+            name='id',
+            field=models.CharField(choices=[('NOTIFY_ON_SHARED_ACCESS_REMOVED', 'NOTIFY_ON_SHARED_ACCESS_REMOVED'), ('NOTIFY_ON_SHARED_DATA_RECORDED', 'NOTIFY_ON_SHARED_DATA_RECORDED'), ('NOTIFY_ON_SHARED_DATA_DELETED', 'NOTIFY_ON_SHARED_DATA_DELETED'), ('NOTIFY_ON_SHARED_DATA_CHECKED', 'NOTIFY_ON_SHARED_DATA_CHECKED'), ('NOTIFY_ON_SHARED_ACCESS_GAINED', 'NOTIFY_ON_SHARED_ACCESS_GAINED'), ('NOTIFY_ON_DEDUCTION_CHANGES', 'NOTIFY_ON_DEDUCTION_CHANGES')], max_length=500, primary_key=True, serialize=False),
+        ),
+    ]