diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index 5e1f08961d0ade56a28d7e0ce6658d009998106b..a41ecdf6f2c1be4660daed2d3d861621286d7ad9 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -8,7 +8,7 @@ import json
 import locale
 import magic
 from markupsafe import Markup
-from sqlalchemy import func, and_, select
+from sqlalchemy import func, and_, select, not_
 from sqlalchemy.orm import joinedload, aliased
 from sqlalchemy.orm.query import Query
 from sqlalchemy.dialects.postgresql import insert as pgsql_insert
@@ -1558,6 +1558,7 @@ class ContestEditForm(FlaskForm):
                                 choices=[ch for ch in db.RoundState.choices() if ch[0] != 'delegate'],
                                 coerce=db.RoundState.coerce)
     submit = wtforms.SubmitField('Uložit')
+    force_submit = wtforms.SubmitField('Uložit s chybami')
 
 
 @app.route('/org/contest/c/<int:ct_id>/edit', methods=('GET', 'POST'))
@@ -1573,7 +1574,21 @@ def org_contest_edit(ct_id: int):
         form.state.render_kw = {'disabled': ""}
         form.state.description = 'Nastavení kola neumožňuje měnit stav soutěže.'
 
-    if form.validate_on_submit():
+    do_submit = False
+    errors = []
+    offer_force_submit = False
+
+    if form.submit.data or form.force_submit.data:
+        do_submit = form.validate_on_submit()
+        if do_submit:
+            errors = check_contest_state(round, contest, form.state.data)
+            if errors and not form.force_submit.data:
+                do_submit = False
+                offer_force_submit = True
+    else:
+        errors = check_contest_state(round, contest, form.state.data)
+
+    if do_submit:
         form.populate_obj(contest)
 
         if sess.is_modified(contest):
@@ -1597,15 +1612,66 @@ def org_contest_edit(ct_id: int):
 
         return redirect(ctx.url_for('org_contest'))
 
+    if not offer_force_submit:
+        del form.force_submit
+
     return render_template(
         'org_contest_edit.html',
         ctx=ctx,
         round=round,
         contest=contest,
         form=form,
+        errors=errors,
+        offer_force_submit=offer_force_submit,
     )
 
 
+def check_contest_state(round: db.Round, contest: Optional[db.Contest], state: db.RoundState) -> List[str]:
+    """Kontrola stavu soutěží (buď zadané nebo všech v kole) při přepínání stavu."""
+    sess = db.get_session()
+    errors = []
+
+    contests_query = (sess.query(db.Contest)
+                      .filter_by(round=round)
+                      .options(joinedload(db.Contest.place)))
+    if contest is not None:
+        contests_query = contests_query.filter_by(contest_id=contest.contest_id)
+
+    def add_ct_errors(cts: List[db.Contest], msg: str) -> None:
+        if len(cts) > 100:
+            errors.append(f'{msg} ({len(ct_no_score)} soutěží).')
+        else:
+            for c in cts:
+                errors.append(f'{msg} {c.place.name_locative()}.')
+
+    if state in [db.RoundState.graded, db.RoundState.closed]:
+        ct_no_points = (contests_query
+                        .filter((sess.query(db.Solution)
+                                 .join(db.Task, and_(db.Task.task_id == db.Solution.task_id,
+                                                     db.Task.round_id == db.Contest.round_id))
+                                 .join(db.Participation, and_(db.Participation.user_id == db.Solution.user_id,
+                                                              db.Participation.contest_id == db.Contest.contest_id,
+                                                              db.Participation.state == db.PartState.active))
+                                 .join(db.User, and_(db.User.user_id == db.Participation.user_id,
+                                                     not_(db.User.is_test)))
+                                 .filter(db.Solution.points == None))
+                                .exists())
+                        .all())
+        add_ct_errors(ct_no_points, 'Chybí body')
+
+    if not round.is_subround() and state == db.RoundState.closed:
+        ct_no_score = (contests_query
+                       .filter_by(scoretable_id=None)
+                       .filter((sess.query(db.Participation)
+                                .filter(db.Participation.contest_id == db.Contest.contest_id)
+                                .filter_by(state=db.PartState.active))
+                               .exists())
+                       .all())
+        add_ct_errors(ct_no_score, 'Chybí oficiální výsledková listina')
+
+    return errors
+
+
 class ParticipantAddForm(FlaskForm):
     email = mo_fields.Email(validators=[validators.Required()])
     first_name = mo_fields.FirstName(validators=[validators.Optional()])
diff --git a/mo/web/org_round.py b/mo/web/org_round.py
index e6fa946d32ebd1382b2857cb1a47961035a241bf..ba0b140e9f87cb2afad6cf47f05a4b1e95eb60d6 100644
--- a/mo/web/org_round.py
+++ b/mo/web/org_round.py
@@ -23,7 +23,7 @@ import mo.util
 from mo.util_format import inflect_with_number
 from mo.web import app
 import mo.web.fields as mo_fields
-from mo.web.org_contest import get_context, get_prev_round
+from mo.web.org_contest import get_context, get_prev_round, check_contest_state
 
 
 @app.route('/org/contest/')
@@ -374,8 +374,6 @@ def org_round_task_edit(round_id: int, task_id: int):
 
 
 class RoundEditForm(FlaskForm):
-    _for_round: Optional[db.Round] = None
-
     name = mo_fields.String("Název", render_kw={'autofocus': True})
     code = mo_fields.String("Kód",
         description="Kód kola používaný v kódech úloh ('1', 'S' apod.). Není-li vyplněn, použije se pořadí kola v kategorii.",
@@ -407,26 +405,7 @@ class RoundEditForm(FlaskForm):
     enroll_advert = mo_fields.String("Popis v přihlášce")
     has_messages = wtforms.BooleanField("Zprávičky pro účastníky (aktivuje možnost vytvářet novinky zobrazované účastníkům)")
     submit = wtforms.SubmitField('Uložit')
-
-    def validate_state(self, field):
-        if field.data != db.RoundState.preparing:
-            if self.ct_tasks_start.data is None:
-                raise ValidationError('Není-li nastaven času začátku soutěže, stav musí být "připravuje se"')
-            if self._for_round is not None:
-                num_tasks = db.get_session().query(db.Task).filter_by(round=self._for_round).count()
-                if num_tasks == 0:
-                    raise ValidationError('Nejsou-li definovány žádné úlohy, stav musí být "připravuje se"')
-
-    def abstract_validate_time_order(self, field):
-        if field.data is not None:
-            if any([i.data is not None and i.data > field.data for i in [self.ct_tasks_start, self.pr_tasks_start]]):
-                raise ValidationError('Soutěž nesmí skončit dříve než začne.')
-
-    def validate_ct_submit_end(self, field):
-        self.abstract_validate_time_order(field)
-
-    def validate_pr_submit_end(self, field):
-        self.abstract_validate_time_order(field)
+    force_submit = wtforms.SubmitField('Uložit s chybami')
 
 
 @app.route('/org/contest/r/<int:round_id>/edit', methods=('GET', 'POST'))
@@ -436,7 +415,6 @@ def org_round_edit(round_id: int):
     round = ctx.round
 
     form = RoundEditForm(obj=round)
-    form._for_round = round
     if round.is_subround():
         # podkolo nemá nastavení výsledkové listiny
         del form.score_mode
@@ -445,7 +423,22 @@ def org_round_edit(round_id: int):
         del form.points_step
         # ani nastavení přihlašování
         del form.enroll_mode
-    if form.validate_on_submit():
+
+    do_submit = False
+    errors = []
+    offer_force_submit = False
+
+    if form.submit.data or form.force_submit.data:
+        do_submit = form.validate_on_submit()
+        if do_submit:
+            errors = check_round_settings(round, form)
+            if errors and not form.force_submit.data:
+                do_submit = False
+                offer_force_submit = True
+    else:
+        errors = check_round_settings(round, form)
+
+    if do_submit:
         form.populate_obj(round)
         if round.code == "":
             round.code = str(round.seq)
@@ -479,14 +472,48 @@ def org_round_edit(round_id: int):
 
         return redirect(ctx.url_for('org_round'))
 
+    if not offer_force_submit:
+        del form.force_submit
+
     return render_template(
         'org_round_edit.html',
         ctx=ctx,
         round=round,
         form=form,
+        errors=errors,
+        offer_force_submit=offer_force_submit,
     )
 
 
+def check_round_settings(round: db.Round, form: RoundEditForm) -> List[str]:
+    state = form.state.data
+    errors = []
+    sess = db.get_session()
+
+    if state != db.RoundState.preparing:
+        if form.ct_tasks_start.data is None:
+            errors.append('Není nastaven čas začátku soutěže, a přitom stav není „připravuje se“.')
+        num_tasks = sess.query(db.Task).filter_by(round=round).count()
+        if num_tasks == 0:
+            errors.append('Nejsou definovány žádné úlohy, a přitom stav není „připravuje se“.')
+
+    if _time_crossed(form.pr_tasks_start, form.ct_tasks_start):
+        errors.append('Dozor má úlohy k dispozici později než účastníci.')
+    if _time_crossed(form.ct_tasks_start, form.ct_submit_end):
+        errors.append('Soutěž pro účastníky skončí dřív než začne.')
+    if _time_crossed(form.ct_tasks_start, form.pr_submit_end):
+        errors.append('Odevzdávání pro dozor skončí dřív než soutěž začne.')
+
+    errors.extend(check_contest_state(round, None, state))
+    return errors
+
+
+def _time_crossed(first_field: mo_fields.DateTime, second_field: mo_fields.DateTime) -> bool:
+    first = first_field.data
+    second = second_field.data
+    return first is not None and second is not None and first > second
+
+
 @app.route('/org/contest/r/<int:round_id>/task-statement/zadani.pdf')
 def org_task_statement(round_id: int):
     ctx = get_context(round_id=round_id)
diff --git a/mo/web/templates/org_contest_edit.html b/mo/web/templates/org_contest_edit.html
index 39e4c0fb729dba67e6d6786387dbf5f43873b232..1a3a62f1b3c935e7ee053a6cebea4f71367112a7 100644
--- a/mo/web/templates/org_contest_edit.html
+++ b/mo/web/templates/org_contest_edit.html
@@ -7,6 +7,21 @@
 {% endblock %}
 {% block body %}
 
-{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
+{% if errors %}
+<div class='alert alert-danger'>
+<p>V nastavení soutěže byly nalezeny následující problémy:
+<ul>
+	{% for e in errors %}
+	<li>{{ e }}
+	{% endfor %}
+</ul>
+<p>Chyby prosím opravte.
+{% if offer_force_submit %}
+Pokud přesto chcete nastavení použít, použijte tlačíko „Uložit s chybami“.
+{% endif %}
+</div>
+{% endif %}
+
+{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary', 'force_submit': 'danger'}) }}
 
 {% endblock %}
diff --git a/mo/web/templates/org_round_edit.html b/mo/web/templates/org_round_edit.html
index 0e42ebd87aba8af08e40e202dfaccb731dba5d33..642fc31b4874d858ba365f91490bc9c1f89b5251 100644
--- a/mo/web/templates/org_round_edit.html
+++ b/mo/web/templates/org_round_edit.html
@@ -7,6 +7,21 @@
 {% endblock %}
 {% block body %}
 
-{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
+{% if errors %}
+<div class='alert alert-danger'>
+<p>V nastavení kola byly nalezeny následující problémy:
+<ul>
+	{% for e in errors %}
+	<li>{{ e }}
+	{% endfor %}
+</ul>
+<p>Chyby prosím opravte.
+{% if offer_force_submit %}
+Pokud přesto chcete nastavení použít, použijte tlačíko „Uložit s chybami“.
+{% endif %}
+</div>
+{% endif %}
+
+{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary', 'force_submit': 'danger'}) }}
 
 {% endblock %}