diff --git a/aleksis/apps/kort/forms.py b/aleksis/apps/kort/forms.py
index 7abb6517ddfdab34f8c70a01cdd8e0e5a225c287..7f22c671539df6a150a98e843815291caae628f8 100644
--- a/aleksis/apps/kort/forms.py
+++ b/aleksis/apps/kort/forms.py
@@ -1,46 +1,89 @@
 from django import forms
+from django.db.models import Q
 from django.utils.translation import gettext as _
 
 from django_ace import AceWidget
-from django_select2.forms import ModelSelect2Widget
+from django_select2.forms import ModelSelect2MultipleWidget
 from material import Fieldset, Layout, Row
 
-from aleksis.apps.kort.models import Card, CardLayout, CardLayoutMediaFile, CardPrinter
+from aleksis.apps.kort.models import CardLayout, CardLayoutMediaFile, CardPrinter
+from aleksis.core.models import Group, Person
 
 
-class CardForm(forms.ModelForm):
+class CardIssueForm(forms.Form):
+    layout = Layout(
+        Fieldset(_("Select person(s) or group(s)"), "persons", "groups"),
+        Fieldset(_("Select validity"), "valid_until"),
+        Fieldset(_("Select layout"), "card_layout"),
+        Fieldset(_("Select printer (optional)"), "printer"),
+    )
     printer = forms.ModelChoiceField(
         queryset=None,
         label=_("Card Printer"),
         help_text=_("Select a printer to directly print the newly issued card."),
         required=False,
     )
+    persons = forms.ModelMultipleChoiceField(
+        queryset=None,
+        label=_("Persons"),
+        required=False,
+        widget=ModelSelect2MultipleWidget(
+            search_fields=[
+                "first_name__icontains",
+                "last_name__icontains",
+                "short_name__icontains",
+            ],
+            attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+        ),
+    )
+    groups = forms.ModelMultipleChoiceField(
+        queryset=None,
+        label=_("Groups"),
+        required=False,
+        widget=ModelSelect2MultipleWidget(
+            search_fields=[
+                "name__icontains",
+                "short_name__icontains",
+            ],
+            attrs={"data-minimum-input-length": 0, "class": "browser-default"},
+        ),
+    )
+    card_layout = forms.ModelChoiceField(queryset=None, label=_("Card layout"), required=True)
+    valid_until = forms.DateField(
+        label=_("Valid until"),
+        required=True,
+    )
 
-    class Meta:
-        model = Card
-        fields = ["person", "valid_until", "layout"]
-
-        widgets = {
-            "person": ModelSelect2Widget(
-                search_fields=[
-                    "first_name__icontains",
-                    "last_name__icontains",
-                    "short_name__icontains",
-                ],
-                attrs={"data-minimum-input-length": 0, "class": "browser-default"},
-            ),
-        }
+    def clean(self):
+        """Clean and validate person data."""
+        cleaned_data = super().clean()
+
+        # Ensure that there is at least one person selected
+        if not cleaned_data.get("persons") and not cleaned_data.get("groups"):
+            raise forms.ValidationError(_("You must select at least one person or group."))
+
+        cleaned_data["all_persons"] = Person.objects.filter(
+            Q(pk__in=cleaned_data.get("persons", []))
+            | Q(member_of__in=cleaned_data.get("groups", []))
+        )
+
+        if not cleaned_data["all_persons"].exists():
+            raise forms.ValidationError(_("The selected groups don't have any members."))
+
+        return cleaned_data
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self.fields["layout"].required = True
 
+        # Assume that users would select the layout if there is only one layout available
         layouts = CardLayout.objects.all()
+        self.fields["card_layout"].queryset = layouts
         if layouts.count() == 1:
-            self.fields["layout"].initial = layouts.first()
+            self.fields["card_layout"].initial = layouts.first()
 
-        printers = CardPrinter.objects.all()
-        self.fields["printer"].queryset = printers
+        self.fields["printer"].queryset = CardPrinter.objects.all()
+        self.fields["persons"].queryset = Person.objects.all()
+        self.fields["groups"].queryset = Group.objects.all()
 
 
 class CardPrinterForm(forms.ModelForm):
@@ -84,16 +127,32 @@ class CardLayoutMediaFileForm(forms.ModelForm):
 
 
 class CardLayoutForm(forms.ModelForm):
-    layout = Layout(Row("name"), Row("width", "height"), Row("template"), "css")
+    layout = Layout(
+        Row("name"), Row("required_fields"), Row("width", "height"), Row("template"), "css"
+    )
 
     template = forms.CharField(widget=AceWidget(mode="django"))
     css = forms.CharField(widget=AceWidget(mode="css"))
 
+    required_fields = forms.MultipleChoiceField(
+        label=_("Required data fields"), required=True, choices=Person.syncable_fields_choices()
+    )
+
     class Meta:
         model = CardLayout
-        fields = ["name", "template", "css", "width", "height"]
+        fields = ["name", "template", "css", "width", "height", "required_fields"]
 
 
 CardLayoutMediaFileFormSet = forms.inlineformset_factory(
     CardLayout, CardLayoutMediaFile, form=CardLayoutMediaFileForm
 )
+
+
+class CardIssueFinishForm(forms.Form):
+    layout = Layout()
+    selected_objects = forms.ModelMultipleChoiceField(queryset=None, required=True)
+
+    def __init__(self, *args, **kwargs):
+        queryset = kwargs.pop("queryset")
+        super().__init__(*args, **kwargs)
+        self.fields["selected_objects"].queryset = queryset
diff --git a/aleksis/apps/kort/migrations/0014_auto_20220803_0025.py b/aleksis/apps/kort/migrations/0014_auto_20220803_0025.py
new file mode 100644
index 0000000000000000000000000000000000000000..7935f8719d22e5321b0fe0356a4485e01d1fd55c
--- /dev/null
+++ b/aleksis/apps/kort/migrations/0014_auto_20220803_0025.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.14 on 2022-08-02 22:25
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('kort', '0013_auto_20220530_1939'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='cardlayout',
+            options={'verbose_name': 'Card Layout', 'verbose_name_plural': 'Card Layouts'},
+        ),
+        migrations.AddField(
+            model_name='cardlayout',
+            name='required_fields',
+            field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=['first_name'], size=None, verbose_name='Required data fields'),
+            preserve_default=False,
+        ),
+    ]
diff --git a/aleksis/apps/kort/models.py b/aleksis/apps/kort/models.py
index 83ea2c9fcd3875700de19c5828dc5762248eba66..2e8677e7a0173d8815063775348c781c7acb91c8 100644
--- a/aleksis/apps/kort/models.py
+++ b/aleksis/apps/kort/models.py
@@ -3,6 +3,7 @@ from datetime import timedelta
 from typing import Any, Optional, Union
 
 from django.conf import settings
+from django.contrib.postgres.fields import ArrayField
 from django.core.exceptions import ValidationError
 from django.core.validators import FileExtensionValidator
 from django.db import models
@@ -216,12 +217,12 @@ class CardLayout(ExtensibleModel):
     css = models.TextField(verbose_name=_("Custom CSS"), blank=True)
     width = models.PositiveIntegerField(verbose_name=_("Width"), help_text=_("in mm"))
     height = models.PositiveIntegerField(verbose_name=_("Height"), help_text=_("in mm"))
+    required_fields = ArrayField(models.TextField(), verbose_name=_("Required data fields"))
 
     def get_template(self) -> Template:
         template = self.BASE_TEMPLATE.format(
             width=self.width, height=self.height, css=self.css, template=self.template
         )
-        print(template)
         return Template(template)
 
     def render(self, card: "Card"):
diff --git a/aleksis/apps/kort/tables.py b/aleksis/apps/kort/tables.py
index 7a1f0e7bc1a8dfa0b6ec3f697cc7458231cb1d5b..c607ee34123a4648f132f698a73ffda84bdbe317 100644
--- a/aleksis/apps/kort/tables.py
+++ b/aleksis/apps/kort/tables.py
@@ -1,8 +1,9 @@
+from django.db.models.fields.files import ImageFieldFile
 from django.template.loader import render_to_string
+from django.utils.safestring import SafeString, mark_safe
 from django.utils.translation import gettext as _
 
 from django_tables2 import (
-    A,
     BooleanColumn,
     Column,
     DateTimeColumn,
@@ -10,8 +11,11 @@ from django_tables2 import (
     RelatedLinkColumn,
     Table,
 )
+from django_tables2.utils import A, AttributeDict, computed_values
 
 from aleksis.apps.kort.forms import PrinterSelectForm
+from aleksis.core.models import Person
+from aleksis.core.util.tables import SelectColumn
 
 
 class CardTable(Table):
@@ -82,3 +86,66 @@ class CardLayoutTable(Table):
 
     def render_actions(self, value, record):
         return render_to_string("kort/card_layout/actions.html", dict(pk=value, card_layout=record))
+
+
+class IssueCardPersonsTable(Table):
+    """Table to list persons with all needed data for issueing cards."""
+
+    selected = SelectColumn()
+    status = Column(accessor=A("pk"), verbose_name=_("Status"))
+
+    def get_missing_fields(self, person: Person):
+        """Return a list of missing data fields for the given person."""
+        required_fields = self.card_layout.required_fields
+        missing_fields = []
+        for field in required_fields:
+            if not getattr(person, field, None):
+                missing_fields.append(field)
+        return missing_fields
+
+    def render_selected(self, value: int, record: Person) -> SafeString:
+        """Render the selected checkbox and mark valid rows as selected."""
+        attrs = {"type": "checkbox", "name": "selected_objects", "value": value}
+        if not self.get_missing_fields(record):
+            attrs.update({"checked": "checked"})
+
+        attrs = computed_values(attrs, kwargs={"record": record, "value": value})
+        return mark_safe(  # noqa
+            "<label><input %s/><span></span</label>" % AttributeDict(attrs).as_html()
+        )
+
+    def render_status(self, value: int, record: Person) -> str:
+        """Render the status of the person data."""
+        missing_fields = self.get_missing_fields(record)
+        return render_to_string(
+            "kort/person_status.html",
+            {"missing_fields": missing_fields, "missing_fields_count": len(missing_fields)},
+            self.request,
+        )
+
+    def render_photo(self, value: ImageFieldFile, record: Person) -> str:
+        """Render the photo of the person as circle."""
+        return render_to_string(
+            "kort/picture.html",
+            {
+                "picture": record.photo,
+                "class": "materialize-circle table-circle",
+                "img_class": "materialize-circle",
+            },
+            self.request,
+        )
+
+    def render_avatar(self, value: ImageFieldFile, record: Person) -> str:
+        """Render the avatar of the person as circle."""
+        return render_to_string(
+            "kort/picture.html",
+            {
+                "picture": record.avatar,
+                "class": "materialize-circle table-circle",
+                "img_class": "materialize-circle",
+            },
+            self.request,
+        )
+
+    class Meta:
+        sequence = ["selected", "status", "..."]
diff --git a/aleksis/apps/kort/templates/kort/card/create.html b/aleksis/apps/kort/templates/kort/card/create.html
deleted file mode 100644
index 086400b8641fa9bf9c1d6118c8a161f266456254..0000000000000000000000000000000000000000
--- a/aleksis/apps/kort/templates/kort/card/create.html
+++ /dev/null
@@ -1,24 +0,0 @@
-{# -*- engine:django -*- #}
-
-{% extends "core/base.html" %}
-
-{% load material_form i18n any_js %}
-
-{% block extra_head %}
-  {{ form.media.css }}
-  {% include_css "select2-materialize" %}
-{% endblock %}
-
-{% block browser_title %}{% blocktrans %}Create card{% endblocktrans %}{% endblock %}
-{% block page_title %}{% blocktrans %}Create card{% endblocktrans %}{% endblock %}
-
-
-{% block content %}
-  <form method="post" enctype="multipart/form-data">
-    {% csrf_token %}
-    {% form form=form %}{% endform %}
-    {% include "core/partials/save_button.html" %}
-  </form>
-  {% include_js "select2-materialize" %}
-  {{ form.media.js }}
-{% endblock %}
diff --git a/aleksis/apps/kort/templates/kort/card/issue.html b/aleksis/apps/kort/templates/kort/card/issue.html
new file mode 100644
index 0000000000000000000000000000000000000000..1a8875ed5be3d39c3fc904b6db54cfdb2e5b44a0
--- /dev/null
+++ b/aleksis/apps/kort/templates/kort/card/issue.html
@@ -0,0 +1,90 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load material_form i18n any_js django_tables2 static %}
+
+{% block extra_head %}
+  {{ form.media.css }}
+  {% include_css "select2-materialize" %}
+{% endblock %}
+
+{% block browser_title %}{% blocktrans %}Issue card(s){% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Issue card(s){% endblocktrans %}{% endblock %}
+
+
+{% block content %}
+  {% include_js "select2-materialize" %}
+
+  <form method="post" enctype="multipart/form-data">
+    {% if wizard.steps.index == 0 %}
+      <figure class="alert primary">
+        <i class="material-icons iconify left" data-icon="mdi:information-outline"></i>
+        {% blocktrans %}
+          Please select the persons and/or the groups to whom you want to issue new cards.
+          After clicking on 'Next', you will be able to check whether the data of the persons
+          are complete and include everything needed for the cards.
+        {% endblocktrans %}
+      </figure>
+    {% else %}
+        <figure class="alert primary">
+        <i class="material-icons iconify left" data-icon="mdi:information-outline"></i>
+        {% blocktrans %}
+          In the following table you can see all selected persons and the related data needed for the cards.
+          Please select the persons to whom you want to issue new cards.
+        {% endblocktrans %}
+      </figure>
+    {% endif %}
+
+    <div class="margin-bottom">
+      {% csrf_token %}
+      {{ wizard.management_form }}
+      {% if wizard.form.forms %}
+        {{ wizard.form.management_form }}
+        {% for form in wizard.form.forms %}
+          {% form form=form %}{% endform %}
+          {{ form.media.js }}
+        {% endfor %}
+      {% else %}
+        {% form form=wizard.form %}{% endform %}
+        {{ form.media.js }}
+      {% endif %}
+      {% if persons_table %}
+        {{ wizard.form.selected_objects.errors }}
+        {% render_table persons_table %}
+      {% endif %}
+    </div>
+
+    <div class="row">
+      <div class="col s12">
+        {% if wizard.steps.prev %}
+          <button name="wizard_goto_step" class="btn primary waves-effect waves-light margin-bottom"
+                  type="submit"
+                  value="{{ wizard.steps.prev }}">
+            <i class="material-icons left">arrow_back</i>
+            {% trans "Previous step" %}
+          </button>
+        {% endif %}
+
+        {% if wizard.steps.count|add:"-1" == wizard.steps.index %}
+          <button type="submit" class="btn green  waves-effect waves-light margin-bottom">
+            {% trans "Issue cards for selected persons" %}<i class="material-icons right">send</i>
+          </button>
+        {% else %}
+          <button class="btn primary-color waves-effect waves-light margin-bottom" type="submit"
+                  value="{{ wizard.steps.prev }}">
+            <i class="material-icons right">arrow_forward</i>
+            {% trans "Next step" %}
+          </button>
+        {% endif %}
+
+        <a href="{% url "cards" %}"
+           class="btn-flat waves-effect waves-red red-text margin-bottom">
+          <i class="material-icons left">clear</i> {% trans "Cancel" %}
+        </a>
+      </div>
+    </div>
+  </form>
+
+  <script src="{% static "js/multi_select.js" %}" type="text/javascript"></script>
+{% endblock %}
diff --git a/aleksis/apps/kort/templates/kort/card/list.html b/aleksis/apps/kort/templates/kort/card/list.html
index 5f96123eb76ab63a52dabfc133850b649ae07e18..69da48d34adf62de715f26d562a32272c61027e0 100644
--- a/aleksis/apps/kort/templates/kort/card/list.html
+++ b/aleksis/apps/kort/templates/kort/card/list.html
@@ -14,7 +14,7 @@
   {% if can_create_person %}
     <a class="btn green waves-effect waves-light" href="{% url 'create_card' %}">
       <i class="material-icons left iconify" data-icon="mdi:plus"></i>
-      {% trans "Issue new card" %}
+      {% trans "Issue new card(s)" %}
     </a>
   {% endif %}
 
diff --git a/aleksis/apps/kort/templates/kort/person_status.html b/aleksis/apps/kort/templates/kort/person_status.html
new file mode 100644
index 0000000000000000000000000000000000000000..c87caf4f13c37f1363c05e31aa5664f845bb3319
--- /dev/null
+++ b/aleksis/apps/kort/templates/kort/person_status.html
@@ -0,0 +1,12 @@
+{% load i18n %}
+{% if missing_fields_count %}
+  <span class="red-text">
+    <i class="material-icons iconify left" data-icon="mdi:alert-octagon-outline"></i>
+    {% blocktrans with count=missing_fields_count %}{{ count }} missing data fields{% endblocktrans %}
+  </span>
+{% else %}
+  <span class="green-text">
+    <i class="material-icons iconify left" data-icon="mdi:check-circle-outline"></i>
+    {% blocktrans %}Data complete{% endblocktrans %}
+  </span>
+{% endif %}
diff --git a/aleksis/apps/kort/templates/kort/picture.html b/aleksis/apps/kort/templates/kort/picture.html
new file mode 100644
index 0000000000000000000000000000000000000000..a921461f56e0b285039b3a9a48e25a66ef8cbe02
--- /dev/null
+++ b/aleksis/apps/kort/templates/kort/picture.html
@@ -0,0 +1,11 @@
+{% load i18n %}
+{% if picture %}
+  <div class="{% firstof class "clip-circle" %}">
+    <img class="{% firstof img_class "hundred-percent" %}" src="{{ picture.url }}" alt="{% trans "Person picture" %}"/>
+  </div>
+{% else %}
+  {# There is a user without a person #}
+  <div class="{% firstof class "clip-circle" %} no-image">
+    <i class="material-icons">person</i>
+  </div>
+{% endif %}
diff --git a/aleksis/apps/kort/urls.py b/aleksis/apps/kort/urls.py
index 1d31213bc4cf687d5512c9c96cf796f30b51cdd8..7c5001b4c2c6f4cddc1beb33c1b1701a8ebde172 100644
--- a/aleksis/apps/kort/urls.py
+++ b/aleksis/apps/kort/urls.py
@@ -5,7 +5,7 @@ from . import api, views
 urlpatterns = [
     path("test", views.TestPDFView.as_view(), name="test_pdf"),
     path("cards/", views.CardListView.as_view(), name="cards"),
-    path("cards/create/", views.CardCreateView.as_view(), name="create_card"),
+    path("cards/create/", views.CardIssueView.as_view(), name="create_card"),
     path("cards/<int:pk>/", views.CardDetailView.as_view(), name="card"),
     path(
         "cards/<int:pk>/generate_pdf/",
diff --git a/aleksis/apps/kort/views.py b/aleksis/apps/kort/views.py
index d6181110b7df5aff5e9a1fc8ef820e747087cac8..a168782a66ce19da38ee25d88d878824bd7a2308 100644
--- a/aleksis/apps/kort/views.py
+++ b/aleksis/apps/kort/views.py
@@ -1,7 +1,8 @@
 import json
 
 from django.contrib import messages
-from django.db.models import Count, Q
+from django.db.models import Count, Q, QuerySet
+from django.forms import Form
 from django.http import HttpRequest, HttpResponse
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse, reverse_lazy
@@ -9,20 +10,28 @@ from django.utils.translation import gettext as _
 from django.views import View
 from django.views.generic.detail import DetailView, SingleObjectMixin
 
-from django_tables2 import SingleTableView
+from django_tables2 import RequestConfig, SingleTableView, table_factory
+from formtools.wizard.views import CookieWizardView
 from reversion.views import RevisionMixin
 from rules.contrib.views import PermissionRequiredMixin
 
 from aleksis.apps.kort.forms import (
-    CardForm,
+    CardIssueFinishForm,
+    CardIssueForm,
     CardLayoutForm,
     CardLayoutMediaFileFormSet,
     CardPrinterForm,
     PrinterSelectForm,
 )
 from aleksis.apps.kort.models import Card, CardLayout, CardPrinter, PrintStatus
-from aleksis.apps.kort.tables import CardLayoutTable, CardPrinterTable, CardTable
+from aleksis.apps.kort.tables import (
+    CardLayoutTable,
+    CardPrinterTable,
+    CardTable,
+    IssueCardPersonsTable,
+)
 from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView
+from aleksis.core.models import Person
 from aleksis.core.util.celery_progress import render_progress_page
 from aleksis.core.views import RenderPDFView
 
@@ -60,37 +69,93 @@ class CardListView(PermissionRequiredMixin, RevisionMixin, PrinterSelectMixin, S
         return Card.objects.order_by("-pk")
 
 
-class CardCreateView(PermissionRequiredMixin, RevisionMixin, AdvancedCreateView):
-    """View used to create a card."""
+class CardIssueView(PermissionRequiredMixin, RevisionMixin, CookieWizardView):
+    """View used to issue one or more cards."""
 
     permission_required = "core.create_card_rule"
     context_object_name = "application"
-    template_name = "kort/card/create.html"
-    form_class = CardForm
-    success_message = _("The card has been created successfully.")
+    template_name = "kort/card/issue.html"
+    form_list = [CardIssueForm, CardIssueFinishForm]
+    success_message = _("The cards have been created successfully.")
     success_url = reverse_lazy("cards")
 
-    def form_valid(self, form: CardForm) -> HttpResponse:
-        response = super().form_valid(form)
-        if form.cleaned_data.get("printer"):
-            printer = form.cleaned_data["printer"]
-            try:
-                job = self.object.print_card(printer)
-                messages.success(
-                    self.request,
-                    _(
-                        "The print job #{} for the card {} on "
-                        "the printer {} has been created successfully."
-                    ).format(job.pk, self.object.person, printer.name),
-                )
-            except ValueError as e:
-                messages.error(
-                    self.request,
-                    _(
-                        "The print job couldn't be started because of the following error: {}"
-                    ).format(e),
-                )
-        return response
+    def _get_data(self) -> dict[str, any]:
+        return self.get_cleaned_data_for_step("0")
+
+    def _get_persons(self) -> QuerySet:
+        """Get all persons selected in the first step."""
+        return self._get_data()["all_persons"]
+
+    def get_form_initial(self, step: str) -> dict[str, any]:
+        if step == "1":
+            return {"persons": self._get_persons()}
+        return super().get_form_initial(step)
+
+    def get_form_kwargs(self, step: str = None) -> dict[str, any]:
+        kwargs = super().get_form_kwargs(step)
+        if step == "1":
+            kwargs["queryset"] = self._get_persons()
+        return kwargs
+
+    def get_form_prefix(self, step: str = None, form: Form = None):
+        prefix = super().get_form_prefix(step, form)
+        if step == "1":
+            return None
+        return prefix
+
+    def get_context_data(self, form: Form, **kwargs) -> dict[str, any]:
+        context = super().get_context_data(form, **kwargs)
+        if self.steps.current == "1":
+            table_obj = table_factory(
+                Person,
+                IssueCardPersonsTable,
+                fields=self._get_data()["card_layout"].required_fields,
+            )
+            table_obj.card_layout = self._get_data()["card_layout"]
+            persons_table = table_obj(self._get_persons())
+            context["persons_table"] = RequestConfig(self.request, paginate=False).configure(
+                persons_table
+            )
+
+        return context
+
+    def done(self, form_list: list[Form], **kwargs) -> HttpResponse:
+        first_data = form_list[0].cleaned_data
+        second_data = form_list[1].cleaned_data
+
+        # Firstly, create all the cards
+        cards = []
+        for person in second_data["selected_objects"]:
+            card = Card(
+                person=person,
+                layout=first_data["card_layout"],
+                valid_until=first_data["valid_until"],
+            )
+            cards.append(card)
+        Card.objects.bulk_create(cards)
+        messages.success(self.request, self.success_message)
+
+        # Secondly, print the cards (if activated)
+        if first_data.get("printer"):
+            printer = first_data["printer"]
+            for card in cards:
+                try:
+                    job = card.print_card(printer)
+                    messages.success(
+                        self.request,
+                        _(
+                            "The print job #{} for the card {} on "
+                            "the printer {} has been created successfully."
+                        ).format(job.pk, card.person, printer.name),
+                    )
+                except ValueError as e:
+                    messages.error(
+                        self.request,
+                        _(
+                            "The print job couldn't be started because of the following error: {}"
+                        ).format(e),
+                    )
+        return redirect(self.success_url)
 
 
 class CardDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
diff --git a/pyproject.toml b/pyproject.toml
index a70a6284ea6e98e1397af5db11bff02be20194c2..b24acf37e60e3e20b241d1ddc515411c01752759 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,6 +28,7 @@ python = "^3.9"
 aleksis-core = "^2.8"
 python-barcode = "^0.14.0"
 django-ace = "^1.0.12"
+django-formtools = "^2.3"
 
 [tool.poetry.dev-dependencies]
 aleksis-builddeps = "*"