diff --git a/contracts/admin.py b/contracts/admin.py
index b6080662ca94e9486ea19da965061c3a53ba0f9c..8a6d17b9411672418ee5ee0b609952d2b496368e 100644
--- a/contracts/admin.py
+++ b/contracts/admin.py
@@ -2,6 +2,7 @@ import copy
 import json
 import logging
 import typing
+import uuid
 
 import requests
 from admin_auto_filters.filters import AutocompleteFilterFactory
@@ -94,7 +95,9 @@ class OwnPermissionsMixin(
 ParentContractApprovedPermissionsMixin = permissions_mixin_factory(
     "contracts.edit_when_approved",
     "contracts.delete_when_approved",
-    obj_conditional=lambda request, obj: get_obj_contract(obj).is_approved,
+    obj_conditional=lambda request, obj: not get_obj_contract(
+        obj
+    ).is_editable_without_approve_permission,
 )
 
 
@@ -163,7 +166,7 @@ class ContractAdmin(
     permissions_mixin_factory(
         "contracts.edit_when_approved",
         "contracts.delete_when_approved",
-        obj_conditional=lambda request, obj: obj.is_approved,
+        obj_conditional=lambda request, obj: not obj.is_editable_without_approve_permission,
     ),
     MarkdownxGuardedModelAdmin,
     NestedModelAdmin,
@@ -251,6 +254,7 @@ class ContractAdmin(
                     ]
                 },
             ),
+            ("Stav", {"fields": ["status"]}),
             (
                 "Doplňující informace",
                 {
@@ -275,33 +279,22 @@ class ContractAdmin(
                 "publishing_rejection_comment",
             )
 
-        if obj is not None and request.user.has_perm("contracts.approve"):
-            fieldsets.insert(
-                5,
-                ("Schválení", {"fields": ["is_approved"]}),
-            )
-
         return fieldsets
 
-    def get_fieldsets_and_inlines_order(self, context) -> list:
-        order = [
-            FieldsetInlineOrder.FIELDSET,
-            FieldsetInlineOrder.FIELDSET,
-            FieldsetInlineOrder.FIELDSET,
-            FieldsetInlineOrder.INLINE,
-            FieldsetInlineOrder.INLINE,
-            FieldsetInlineOrder.INLINE,
-            FieldsetInlineOrder.FIELDSET,
-            FieldsetInlineOrder.INLINE,
-            FieldsetInlineOrder.INLINE,
-            FieldsetInlineOrder.FIELDSET,
-            FieldsetInlineOrder.FIELDSET,
-        ]
-
-        if context["user"].has_perm("contracts.approve"):
-            order.insert(11, FieldsetInlineOrder.FIELDSET)
-
-        return order
+    fieldsets_and_inlines_order = order = [
+        FieldsetInlineOrder.FIELDSET,
+        FieldsetInlineOrder.FIELDSET,
+        FieldsetInlineOrder.FIELDSET,
+        FieldsetInlineOrder.INLINE,
+        FieldsetInlineOrder.INLINE,
+        FieldsetInlineOrder.INLINE,
+        FieldsetInlineOrder.FIELDSET,
+        FieldsetInlineOrder.INLINE,
+        FieldsetInlineOrder.INLINE,
+        FieldsetInlineOrder.FIELDSET,
+        FieldsetInlineOrder.INLINE,
+        FieldsetInlineOrder.FIELDSET,
+    ]
 
     def get_queryset(self, request):
         queryset = super().get_queryset(request)
@@ -314,7 +307,8 @@ class ContractAdmin(
 
         if not request.user.has_perm("contracts.approve"):
             queryset = queryset.filter(
-                models.Q(is_approved=True) | models.Q(created_by=request.user)
+                models.Q(status=Contract.StatusTypes.APPROVED)
+                | models.Q(created_by=request.user)
             )
 
         return queryset
@@ -343,45 +337,125 @@ class ContractAdmin(
 
         from users.models import User
 
-        if is_new:
-            try:
-                sso_ids = []
-
-                for user in User.objects.filter(is_staff=True).all():
-                    if user.is_superuser or user.has_perm("contracts.approve"):
-                        sso_ids.append(user.sso_id)
-
-                requests.post(
-                    settings.NASTENKA_API_URL,
-                    data=json.dumps(
-                        {
-                            "name": f"Nová smlouva ke schválení - {obj.name}",
-                            "description": (
-                                obj.summary
-                                if obj.summary is not None
-                                else "Bez popisu."
-                            ),
-                            "contract_id": obj.id,
-                            "sso_ids": sso_ids,
-                        }
-                    ),
-                    headers={
-                        "Authorization": f"Token {settings.NASTENKA_API_TOKEN}",
-                        "Content-Type": "application/json",
-                    },
-                )
-            except Exception as exception:
-                logger.error(
-                    "Failed to synchronizace Nástěnka notices: %s", str(exception)
-                )
+        if is_new or obj.status != form.initial["status"]:
+            headers = {
+                "Authorization": f"Token {settings.NASTENKA_API_TOKEN}",
+                "Content-Type": "application/json",
+            }
+
+            if obj.status == obj.StatusTypes.TO_BE_APPROVED:
+                try:
+                    sso_ids = []
+
+                    for user in User.objects.filter(is_staff=True).all():
+                        print(user, user.has_perm("contracts.approve"))
+
+                        if user.has_perm("contracts.approve"):
+                            sso_ids.append(user.sso_id)
+
+                    notice = requests.post(
+                        settings.NASTENKA_API_URL,
+                        data=json.dumps(
+                            {
+                                "name": f"Smlouva ke schválení - {obj.name}",
+                                "description": (
+                                    obj.summary
+                                    if obj.summary is not None
+                                    else "Bez popisu."
+                                ),
+                                "contract_id": obj.id,
+                                "sso_ids": sso_ids,
+                            }
+                        ),
+                        headers=headers,
+                    )
+
+                    notice.raise_for_status()
+                    notice = notice.json()
+
+                    obj.to_be_approved_nastenka_notice_id = uuid.UUID(notice["id"])
+                    obj.save()
+                except Exception as exception:
+                    logger.error(
+                        'Failed to send out notice "to be approved" Nástěnka notification: %s',
+                        str(exception),
+                    )
+            elif obj.status == obj.StatusTypes.APPROVED:
+                try:
+                    user = User.objects.filter(id=obj.created_by.id).first()
+
+                    requests.post(
+                        settings.NASTENKA_API_URL,
+                        data=json.dumps(
+                            {
+                                "name": f"Smlouva schválena - {obj.name}",
+                                "description": (
+                                    obj.summary
+                                    if obj.summary is not None
+                                    else "Bez popisu."
+                                ),
+                                "contract_id": obj.id,
+                                "sso_ids": [user.sso_id],
+                            }
+                        ),
+                        headers=headers,
+                    ).raise_for_status()
+
+                    if obj.to_be_approved_nastenka_notice_id is not None:
+                        requests.delete(
+                            f"{settings.NASTENKA_API_URL}/{obj.to_be_approved_nastenka_notice_id}",
+                            headers=headers,
+                        ).raise_for_status()
+
+                        obj.to_be_approved_nastenka_notice_id = None
+                        obj.save()
+                except Exception as exception:
+                    logger.error(
+                        'Failed to send out "contract approved" Nástěnka notification: %s',
+                        str(exception),
+                    )
+            elif obj.status == obj.StatusTypes.REJECTED:
+                try:
+                    user = User.objects.filter(id=obj.created_by.id).first()
+
+                    requests.post(
+                        settings.NASTENKA_API_URL,
+                        data=json.dumps(
+                            {
+                                "name": f"Smlouva odmítnuta - {obj.name}",
+                                "description": (
+                                    obj.summary
+                                    if obj.summary is not None
+                                    else "Bez popisu."
+                                ),
+                                "contract_id": obj.id,
+                                "sso_ids": [user.sso_id],
+                            }
+                        ),
+                        headers=headers,
+                    ).raise_for_status()
+
+                    if obj.to_be_approved_nastenka_notice_id is not None:
+                        requests.delete(
+                            f"{settings.NASTENKA_API_URL}/{obj.to_be_approved_nastenka_notice_id}",
+                            headers=headers,
+                        ).raise_for_status()
+
+                        obj.to_be_approved_nastenka_notice_id = None
+                        obj.save()
+                except Exception as exception:
+                    logger.error(
+                        'Failed to send out "contract rejected" Nástěnka notification: %s',
+                        str(exception),
+                    )
 
         return parent_save_response
 
     def has_change_permission(self, request, obj=None):
         if (
             obj is not None
-            and obj.is_approved
             and not request.user.has_perm("contracts.edit_when_approved")
+            and not obj.is_editable_without_approve_permission
         ):
             return False
 
@@ -390,8 +464,8 @@ class ContractAdmin(
     def has_delete_permission(self, request, obj=None):
         if (
             obj is not None
-            and obj.is_approved
             and not request.user.has_perm("contracts.delete_when_approved")
+            and not obj.is_editable_without_approve_permission
         ):
             return False
 
@@ -405,7 +479,7 @@ class ContractAdmin(
             "Naše smluvná strana", "contractee_signatures__contractee"
         ),
         AutocompleteFilterFactory("Jiná smluvní strana", "signee_signatures__signee"),
-        "is_approved",
+        "status",
         "is_valid",
         "is_public",
         "paper_form_state",
@@ -416,7 +490,7 @@ class ContractAdmin(
 
     list_display = (
         "name",
-        "is_approved",
+        "status",
         "is_public",
         "created_on",
     )
@@ -539,7 +613,9 @@ class SigneeSignatureRepresentativeAdmin(
     permissions_mixin_factory(
         "contracts.edit_when_approved",
         "contracts.delete_when_approved",
-        obj_conditional=lambda request, obj: get_obj_signee_contract(obj).is_approved,
+        obj_conditional=lambda request, obj: not get_obj_signee_contract(
+            obj
+        ).is_editable_without_approve_permission,
     ),
     permissions_mixin_factory(
         "contracts.edit_others",
@@ -556,9 +632,9 @@ class ContracteeSignatureRepresentativeAdmin(
     permissions_mixin_factory(
         "contracts.edit_when_approved",
         "contracts.delete_when_approved",
-        obj_conditional=lambda request, obj: get_obj_contractee_contract(
+        obj_conditional=lambda request, obj: not get_obj_contractee_contract(
             obj
-        ).is_approved,
+        ).is_editable_without_approve_permission,
     ),
     permissions_mixin_factory(
         "contracts.edit_others",
diff --git a/contracts/forms.py b/contracts/forms.py
index a33c8323e440aed38d6a8cd758980f0385424ae9..04ca974db87942eedf120174c3f6ae49f203d9de 100644
--- a/contracts/forms.py
+++ b/contracts/forms.py
@@ -28,14 +28,15 @@ class ContractAdminForm(forms.ModelForm):
 
         if (
             not self.current_user.is_superuser
-            and self.instance.is_approved
-            is not clean_data.get("is_approved", self.instance.is_approved)
+            and self.instance.status
+            in (self.instance.StatusTypes.APPROVED, self.instance.StatusTypes.REJECTED)
+            and (self.instance.status == clean_data.get("status", self.instance.status))
             and self.current_user == self.instance.created_by
         ):
             raise ValidationError(
                 {
-                    "is_approved": (
-                        "Smlouva nemůže být schválena uživatelem, " "který ji nahrál."
+                    "status": (
+                        "Smlouva nemůže být schválena uživatelem, který ji nahrál."
                     )
                 }
             )
diff --git a/contracts/migrations/0063_remove_contract_is_approved_contract_status.py b/contracts/migrations/0063_remove_contract_is_approved_contract_status.py
new file mode 100644
index 0000000000000000000000000000000000000000..ccb43df82625f9983f95891dd28493acd83fd888
--- /dev/null
+++ b/contracts/migrations/0063_remove_contract_is_approved_contract_status.py
@@ -0,0 +1,43 @@
+# Generated by Django 4.1.4 on 2023-07-01 13:37
+
+from django.db import migrations, models
+
+
+def migrate_between_fields(apps, schema) -> None:
+    Contract = apps.get_model("contracts", "contract")
+
+    from contracts.models import Contract as ContractOrignalModel
+
+    for contract in Contract.objects.filter(is_approved=True).all():
+        contract.status = ContractOrignalModel.StatusTypes.APPROVED
+        contract.save()
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("contracts", "0062_contract_paper_form_person_responsible"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="contract",
+            name="status",
+            field=models.CharField(
+                choices=[
+                    ("work_in_progress", "Rozpracovaná"),
+                    ("to_be_approved", "Ke schválení"),
+                    ("approved", "Schválená"),
+                    ("rejected", "Zamítnutá"),
+                ],
+                default="work_in_progress",
+                help_text='Označením jako "Ke schválení" se smlouva předá schvalovateli.',
+                max_length=16,
+                verbose_name="Status",
+            ),
+        ),
+        migrations.RunPython(migrate_between_fields),
+        migrations.RemoveField(
+            model_name="contract",
+            name="is_approved",
+        ),
+    ]
diff --git a/contracts/migrations/0064_contract_to_be_approved_nastenka_notice_id.py b/contracts/migrations/0064_contract_to_be_approved_nastenka_notice_id.py
new file mode 100644
index 0000000000000000000000000000000000000000..ca479c82c41c1e2d298dd26b9b4a40b63e19bc77
--- /dev/null
+++ b/contracts/migrations/0064_contract_to_be_approved_nastenka_notice_id.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.1.4 on 2023-07-01 16:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("contracts", "0063_remove_contract_is_approved_contract_status"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="contract",
+            name="to_be_approved_nastenka_notice_id",
+            field=models.UUIDField(
+                blank=True,
+                null=True,
+                verbose_name="ID oznámení o nové smlouvě v Nástěnce",
+            ),
+        ),
+    ]
diff --git a/contracts/models.py b/contracts/models.py
index c948ca56e8065b48a4b6dd3ee1e21c28904ceee5..c2a6786767a195a5e860efae497005df6669e7e1 100644
--- a/contracts/models.py
+++ b/contracts/models.py
@@ -40,7 +40,7 @@ class OwnPermissionsMixin(models.Model):
 
 class ContractCountMixin(models.Model):
     def get_contract_count(self, user) -> None:
-        filter = {"is_approved": True}
+        filter = {"status": Contract.StatusTypes.APPROVED}
 
         if not user.has_perm("contract.view_confidential"):
             filter["is_public"] = True
@@ -53,10 +53,10 @@ class ContractCountMixin(models.Model):
 
 class SignatureCountMixin(models.Model):
     def get_signature_count(self, user) -> None:
-        filter = {"contract__is_approved": True}
+        filter = {"contract__status": Contract.StatusTypes.APPROVED}
 
         if not user.has_perm("contract.view_confidential"):
-            filter["contract__is_approved"] = True
+            filter["contract__is_public"] = True
 
         return self.signatures.filter(**filter).count()
 
@@ -490,13 +490,18 @@ class Contract(NameStrMixin, models.Model):
 
     # BEGIN Approval fields
 
-    is_approved = models.BooleanField(
-        verbose_name="Je schválená",
-        default=False,
-        help_text=(
-            "Mohou měnit jen schvalovatelé. Pokud je "
-            "smlouva veřejná, schválením se vypustí ven."
-        ),
+    class StatusTypes(models.TextChoices):
+        WORK_IN_PROGRESS = "work_in_progress", "Rozpracovaná"
+        TO_BE_APPROVED = "to_be_approved", "Ke schválení"
+        APPROVED = "approved", "Schválená"
+        REJECTED = "rejected", "Zamítnutá"
+
+    status = models.CharField(
+        choices=StatusTypes.choices,
+        max_length=16,
+        default=StatusTypes.WORK_IN_PROGRESS,
+        verbose_name="Status",
+        help_text='Označením jako "Ke schválení" se smlouva předá schvalovateli.',
     )
 
     # END Approval fields
@@ -675,6 +680,10 @@ class Contract(NameStrMixin, models.Model):
         help_text="Poznámky jsou viditelné pro všechny, kteří mohou smlouvu spravovat a pro tajné čtenáře.",
     )
 
+    to_be_approved_nastenka_notice_id = models.UUIDField(
+        blank=True, null=True, verbose_name="ID oznámení o nové smlouvě v Nástěnce"
+    )
+
     @property
     def primary_contract_url(self) -> typing.Union[None, str]:
         if self.primary_contract is None:
@@ -685,6 +694,13 @@ class Contract(NameStrMixin, models.Model):
             args=(self.primary_contract.id,),
         )
 
+    @property
+    def is_editable_without_approve_permission(self) -> bool:
+        return self.status not in (
+            self.StatusTypes.TO_BE_APPROVED,
+            self.StatusTypes.APPROVED,
+        )
+
     @property
     def url(self) -> str:
         return reverse(
diff --git a/contracts/views.py b/contracts/views.py
index a3f23e9f6ba62af623e9d113e7c2d5299a432f64..d6917270669ea37ef9a93c3d66e4b54f829f3d4a 100644
--- a/contracts/views.py
+++ b/contracts/views.py
@@ -53,7 +53,7 @@ def get_paginated_contracts(request, filter=None, annotations=None) -> tuple:
     if filter is None:
         filter = models.Q()
 
-    filter = filter & models.Q(is_approved=True)
+    filter = filter & models.Q(status=Contract.StatusTypes.APPROVED)
 
     if not request.user.has_perm("contracts.view_confidential"):
         additional_filter = models.Q(is_public=True)
@@ -93,7 +93,7 @@ def index(request):
 
 
 def view_contract(request, id: int):
-    filter = models.Q(is_approved=True)
+    filter = models.Q(status=Contract.StatusTypes.APPROVED)
 
     if not request.user.has_perm("contracts.view_confidential"):
         filter = filter & (
diff --git a/registry/templates/admin/index.html b/registry/templates/admin/index.html
index f96c70270891fdaecba7b90c9f114447e32223e3..7f6888e3b7504c23f352f5f88f1ffc2378ae4f42 100644
--- a/registry/templates/admin/index.html
+++ b/registry/templates/admin/index.html
@@ -6,7 +6,7 @@
     <div class="index-action-buttons">
         {% if request.user.can_approve_contracts %}
             <a
-                href="contracts/contract/?is_approved__exact=0"
+                href="contracts/contract/?status__exact=approved"
                 aria-role="button"
             >Smlouvy ke schválení ({{ request.user.contracts_to_approve_count }})</a>
         {% endif %}
diff --git a/users/models.py b/users/models.py
index ab09f228f296456be463480262c1e102c048a9f6..23c2b6e3dd0fdadcb1571cab5c733485bfae30d1 100644
--- a/users/models.py
+++ b/users/models.py
@@ -63,7 +63,7 @@ class User(pirates_models.AbstractUser):
 
         from contracts.models import Contract
 
-        return Contract.objects.filter(is_approved=False).count()
+        return Contract.objects.filter(status=Contract.StatusTypes.APPROVED).count()
 
     # https://docs.djangoproject.com/en/4.1/ref/models/instances/#customizing-model-loading
     @classmethod