diff --git a/mo/jobs/submit.py b/mo/jobs/submit.py
index 8fe3dfbedee59878a6ed0d2d8f5dd61351587b3f..82d19a8ac380516a0154d1bad93b3805a53d08f6 100644
--- a/mo/jobs/submit.py
+++ b/mo/jobs/submit.py
@@ -116,9 +116,10 @@ class UploadFeedback:
     task: Optional[db.Task] = None
     user: Optional[db.User] = None
     tmp_name: Optional[str] = None
+    is_solution: bool = False
 
 
-def parse_feedback_name(name: str) -> Optional[UploadFeedback]:
+def parse_feedback_name(name: str, round_id: int) -> (Optional[UploadFeedback], Optional[str]):
     name = os.path.basename(name)
 
     # Formát jmen generovaný dávkovým stahováním
@@ -128,7 +129,7 @@ def parse_feedback_name(name: str) -> Optional[UploadFeedback]:
             file_name=name,
             task_code=m['task'],
             user_id=int(m['user_id']),
-        )
+        ), None
 
     # Formát jmen, pod kterými se ukládají jednotlivě stahovaná řešení
     m = re.match(r'(?P<task>[^_]+)_(reseni|opravene)_(?P<user_id>\d+)_', name)
@@ -137,7 +138,7 @@ def parse_feedback_name(name: str) -> Optional[UploadFeedback]:
             file_name=name,
             task_code=m['task'],
             user_id=int(m['user_id']),
-        )
+        ), None
 
     # Formát jmen, pod kterými se dříve ukládala jednotlivě stahovaná řešení
     m = re.match(r'(?P<task>.+)-(reseni|opravene)-(?P<paper_id>\d+)\.', name)
@@ -148,9 +149,32 @@ def parse_feedback_name(name: str) -> Optional[UploadFeedback]:
                 file_name=name,
                 task_code=m['task'],
                 user_id=paper.for_user,
-            )
+            ), None
+
+    # Hromadný upload skenů
+    m = re.match(r'(?P<task>[^_]+)_(?P<what>reseni|opravene)_(?P<lastname>[^_]+)_(?P<firstname>[^_]+)\.', name)
+    if m:
+        users = (db.get_session().query(db.User.user_id)
+                 .select_from(db.Participation)
+                 .join(db.User, db.User.user_id == db.Participation.user_id)
+                 .join(db.Contest, db.Contest.master_contest_id == db.Participation.contest_id)
+                 .filter(db.Contest.round_id == round_id)
+                 .filter(and_(
+                       db.f_unaccent(db.User.first_name).ilike(db.f_unaccent(m['firstname'])),
+                       db.f_unaccent(db.User.last_name ).ilike(db.f_unaccent(m['lastname']))))
+                 .all())
+        if len(users) > 1:
+            return None, f'Nalezeno více soutěžící se jménem "{m["firstname"]} {m["lastname"]}"'
+        elif len(users) == 0:
+            return None, f'Nenalezen žádný soutěžících se jménem "{m["firstname"]} {m["lastname"]}".'
+        return UploadFeedback(
+            file_name=name,
+            task_code=m['task'],
+            user_id=users[0].user_id,
+            is_solution=(m['what']=='reseni'),
+        ), None
 
-    return None
+    return None, f'Nerozpoznáno jméno souboru {name}.'
 
 
 @job_handler(db.JobType.upload_feedback)
@@ -196,7 +220,7 @@ def handle_upload_feedback(the_job: TheJob):
                 contents = zip.infolist()
                 for item in contents:
                     if not item.is_dir():
-                        fb = parse_feedback_name(item.filename)
+                        fb, err = parse_feedback_name(item.filename, round_id)
                         if fb:
                             tmp_file = NamedTemporaryFile(mode='w+b', delete=False)
                             logger.debug(f'Job: Extrahuji {item.filename} do {tmp_file.name}')
@@ -206,7 +230,7 @@ def handle_upload_feedback(the_job: TheJob):
                             fb.tmp_name = tmp_file.name
                             files.append(fb)
                         else:
-                            the_job.error(f'Nerozpoznáno jméno souboru {item.filename}')
+                            the_job.error(err)
         except zipfile.BadZipFile as e:
             logger.debug(f'Job: Nahraný soubor není validní ZIP: {str(e)}')
             the_job.error('Chybný formát souboru. Je to opravdu ZIP?')
@@ -266,7 +290,7 @@ def handle_upload_feedback(the_job: TheJob):
         paper = db.Paper(
             for_user_obj=fb.user,
             task=fb.task,
-            type=db.PaperType.feedback,
+            type=db.PaperType.solution if fb.is_solution else db.PaperType.feedback,
             uploaded_by_obj=job.user,
         )
         try:
@@ -279,7 +303,10 @@ def handle_upload_feedback(the_job: TheJob):
                    .filter_by(task=fb.task, user=fb.user)
                    .with_for_update()
                    .one())
-            sol.final_feedback_obj = paper
+            if fb.is_solution:
+                sol.final_submit_obj = paper
+            else:
+                sol.final_feedback_obj = paper
 
             sess.commit()
             return True
diff --git a/mo/web/templates/org_generic_batch_upload.html b/mo/web/templates/org_generic_batch_upload.html
index 82fbf74a531a8141f9f4b3dc8d4879e0376981da..e31b913066330416a9d190e3fb270ad5ed810100 100644
--- a/mo/web/templates/org_generic_batch_upload.html
+++ b/mo/web/templates/org_generic_batch_upload.html
@@ -1,7 +1,7 @@
 {% extends "base.html" %}
 {% import "bootstrap/wtf.html" as wtf %}
 
-{% block title %}Nahrání opravených řešení{% if task %} úlohy {{ task.code }}: {{ task.name }}{% endif %}{% endblock %}
+{% block title %}Nahrání odevzdaných či opravených řešení{% if task %} úlohy {{ task.code }}: {{ task.name }}{% endif %}{% endblock %}
 {% block breadcrumbs %}
 {{ ctx.breadcrumbs(action="Nahrát ZIP řešení") }}
 {% endblock %}
@@ -9,12 +9,11 @@
 {% block body %}
 <p>Zde můžete nahrát najednou více PDF souborů zabalených do jednoho souboru typu ZIP.
 
-<p>Soubory opravených řešení se musí jmenovat stejně jako původní soubory účastnických řešení.
-Na zařazení souborů do adresářů nezáleží.
-
 <p>Pokud ZIP obsahuje jen podmnožinu účastníků, řešení ostatních účastníků se nezmění.
 Pokud nahrajete řešení téhož účastníka znovu, uloží se nová verze.
 
+<p>Na zařazení souborů do adresářů <i>nezáleží</i>.
+
 <form action="https://{{ bucket_name }}.storage.googleapis.com" method="post" class="form" enctype="multipart/form-data" role="form">
     {% for key, value in policy.items() %}
     <input type="hidden" name="{{ key }}" value="{{ value }}">
@@ -25,4 +24,21 @@ Pokud nahrajete řešení téhož účastníka znovu, uloží se nová verze.
     <input class="btn btn-primary" type="submit" value="Odeslat">
 </form>
 
+<h4>Opravená řešení</h4>
+
+<p>Soubory opravených řešení se musí jmenovat stejně jako původně z Osmo stažené soubory odevzdaných řešení.
+<p>Alternativně použijte formát:
+<pre>
+&lt;<i>kód úlohy</i>&gt;_opravene_&lt;<i>příjmení</i>&gt;_&lt;<i>jméno</i>&gt;.pdf
+</pre>
+<p>tj. například <kbd>B2-1_opravene_pilny_josef.pdf</kbd>. U jména a příjmení nezáleží na diakritice, ani velikosti písmen. Pokud existuje v rámci kola více soutěžících se stejným jménem, skončí nahrání chybou, jejich řešení je třeba nahrát jednotlivě.
+
+<h4>Odevzdaná řešení</h4>
+
+<p>Soubory odevzdaných řešení musí být pojmenované podle této šablony:
+<pre>
+&lt;<i>kód úlohy</i>&gt;_reseni_&lt;<i>příjmení</i>&gt;_&lt;<i>jméno</i>&gt;.pdf
+</pre>
+<p>tj. například <kbd>B2-1_reseni_pilny_josef.pdf</kbd>.
+
 {% endblock %}