diff --git a/bin/p-score b/bin/p-score
new file mode 100755
index 0000000000000000000000000000000000000000..cd15d5ce30ccbfe08726e41bccd0042252b8a116
--- /dev/null
+++ b/bin/p-score
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+# Generátor výsledkové listiny pro MO-P
+
+import argparse
+from sqlalchemy.orm import joinedload
+
+import mo.db as db
+from mo.score import Score
+from mo.util import die, init_standalone
+
+parser = argparse.ArgumentParser(description='Vygeneruje výsledkovou listinu MO-P')
+parser.add_argument('year', type=int)
+parser.add_argument('seq', type=int)
+
+args = parser.parse_args()
+init_standalone()
+sess = db.get_session()
+
+
+def get_results(round, contests):
+    results = {}
+    for contest in contests:
+        place_code = contest.place.get_code()
+        print(f"Počítám oblast {place_code}")
+
+        score = Score(round, contest)
+        results[place_code] = score.get_sorted_results()
+
+        for msg in score.get_messages():
+            if msg[0] != 'info':
+                print(f'\t{msg[0].upper()}: {msg[1]}')
+
+    return results
+
+
+def write_tex(round, tasks, contests, results):
+    with open('final.tex', 'w') as out:
+        out.write(r'\def\HranicePostupu{%s}' % (round.score_winner_limit,) + "\n")
+        out.write(r'\def\HraniceUspesnychResitelu{%s}' % (round.score_successful_limit,) + "\n")
+        out.write('\n')
+        for c in contests:
+            res = results[c.place.get_code()]
+            if round.seq == 2:
+                out.write(r'\kraj{%s}' % c.place.name + '\n')
+                if not res:
+                    out.write(r'\nobody' + '\n')
+                    out.write(r'\endkraj' + '\n\n')
+                    continue
+
+            out.write(r'\begintable' + '\n')
+            prev_typ = ""
+
+            for r in res:
+                if r.winner:
+                    typ = 'v'
+                elif r.successful:
+                    typ = 'u'
+                else:
+                    typ = 'n'
+                if typ != prev_typ:
+                    if prev_typ:
+                        out.write(r'\sep%s' % typ)
+                    prev_typ = typ
+                out.write(r'\%s' % typ)
+
+                cols = []
+                o = r.order
+                if not r.successful or o.continuation:
+                    cols.append("")
+                elif o.span > 1:
+                    cols.append(f'{o.place}.--{o.place + o.span - 1}.')
+                else:
+                    cols.append(f'{o.place}.')
+
+                cols.append(r.user.full_name())
+                cols.append(r.pant.school_place.name)
+                cols.append(r.pant.grade)
+
+                sol_map = r.get_sols_map()
+                for t in tasks:
+                    s = sol_map.get(t.task_id)
+                    if s is not None:
+                        cols.append(s.points)
+                    else:
+                        cols.append('--')
+
+                cols.append(r.get_total_points())
+
+                out.write("".join(['{' + str(col) + '}' for col in cols]) + '\n')
+
+            out.write(r'\endtable' + '\n')
+            if round.seq == 2:
+                out.write(r'\endkraj' + '\n\n')
+
+
+def write_html(round, tasks, contests, results):
+    num_cols = 4 + len(tasks) + 1
+    with open('final.html', 'w') as out:
+        for c in contests:
+            out.write(f'<tr><th colspan={num_cols}>{c.place.name}\n')
+
+            res = results[c.place.get_code()]
+            if not res:
+                out.write(f'<tr class=nobody><td colspan={num_cols}>Nikdo se nezúčastnil.\n')
+                out.write(f'<tr><td colspan={num_cols}>\n\n')
+                continue
+
+            for r in res:
+                if r.winner:
+                    out.write('<tr class=marked>')
+                elif r.successful:
+                    out.write('<tr class=success>')
+                else:
+                    out.write('<tr>')
+
+                cols = []
+                o = r.order
+                if not r.successful or o.continuation:
+                    cols.append("")
+                elif o.span > 1:
+                    cols.append(f'{o.place}.–{o.place + o.span - 1}.')
+                else:
+                    cols.append(f'{o.place}.')
+
+                cols.append(r.user.full_name())
+                cols.append(r.pant.school_place.name)
+                cols.append(r.pant.grade)
+
+                sol_map = r.get_sols_map()
+                for t in tasks:
+                    s = sol_map.get(t.task_id)
+                    if s is not None:
+                        cols.append(s.points)
+                    else:
+                        cols.append('–')
+
+                cols.append(r.get_total_points())
+
+                out.write("".join(['<td>' + str(col) for col in cols]) + '\n')
+
+            out.write(f'<tr><td colspan={num_cols}>\n\n')
+
+
+round = sess.query(db.Round).filter_by(year=args.year, category='P', seq=args.seq).one()
+print(f"Kolo {round.round_code()}")
+
+tasks = sess.query(db.Task).filter_by(round=round).order_by(db.Task.code).all()
+
+contests = (sess.query(db.Contest)
+            .filter_by(round=round)
+            .options(joinedload(db.Contest.place))
+            .all())
+assert contests
+contests.sort(key=lambda c: c.place.get_code())
+
+results = get_results(round, contests)
+write_tex(round, tasks, contests, results)
+write_html(round, tasks, contests, results)
diff --git a/mo/db.py b/mo/db.py
index 769e839d8e6e251ea413d37bc984d35964387890..1aa61c26d2acb1657f124280411d6057f6cdc5be 100644
--- a/mo/db.py
+++ b/mo/db.py
@@ -291,6 +291,9 @@ class User(Base):
     def sort_key(self) -> Tuple[str, str, int]:
         return (locale.strxfrm(self.last_name), locale.strxfrm(self.first_name), self.user_id)
 
+    def name_sort_key(self) -> Tuple[str, str]:
+        return (locale.strxfrm(self.last_name), locale.strxfrm(self.first_name))
+
 
 class Contest(Base):
     __tablename__ = 'contests'
diff --git a/mo/score.py b/mo/score.py
index aba2ffe7517ffe8c92f4e21e9bbe1ff82896ebda..8d7280eac8c539f5d91d79f43fa7c4a9d1a1bfb4 100644
--- a/mo/score.py
+++ b/mo/score.py
@@ -3,10 +3,11 @@ from sqlalchemy import and_
 from sqlalchemy.orm import joinedload
 from sqlalchemy.orm.query import Query
 from sqlalchemy.sql.expression import select
-from typing import Any, List, Tuple, Optional, Dict
+from typing import Any, List, Tuple, Optional, Dict, Union
 
 import mo.db as db
 from mo.util import normalize_grade
+from mo.util_format import inflect_with_number
 
 
 class ScoreOrder:
@@ -36,15 +37,15 @@ class ScoreResult:
     # vzestupně (pro sestupné třídění podle čísla je potřeba klíč vynásobit -1)
     _order_key: List[Any]
 
+    _null_score_order = ScoreOrder(0)
+
     def __init__(self, user: db.User, pant: db.Participant, pion: db.Participation):
         self.user = user
         self.pant = pant
         self.pion = pion
 
         self._sols = {}
-        self.order = 1
-        self.place_span = 1
-        self.place_continuation = False
+        self.order = ScoreResult._null_score_order
         self.winner = False
         self.successful = False
         self._order_key = []
@@ -75,14 +76,19 @@ class ScoreTask:
 
     def get_difficulty(self) -> Fraction:
         if self.num_solutions == 0:
-            return 0
+            return Fraction(0)
         return Fraction(self.sum_points, self.num_solutions)
 
+    def get_difficulty_str(self) -> str:
+        return f'{self.sum_points}/{self.num_solutions}'
+
 
 class Score:
     round: db.Round
     contest: Optional[db.Contest]
     part_states: List[db.PartState]
+    want_successful: bool
+    want_winners: bool
 
     # Řádky výsledkovky
     _results: Dict[int, ScoreResult]
@@ -104,10 +110,13 @@ class Score:
         self.round = round
         self.contest = contest
         self.part_states = part_states
+        self.want_successful = round.score_successful_limit is not None
+        self.want_winners = round.score_winner_limit is not None
 
         # Příprava subquery na účastníky (contest_subq obsahuje master_contest_id)
         sess = db.get_session()
         if contest:
+            assert contest.master_contest_id is not None
             contest_subq = [contest.master_contest_id]
         else:
             contest_subq = sess.query(db.Contest.master_contest_id).filter_by(round=round)
@@ -118,10 +127,9 @@ class Score:
             .select_from(db.Participation)
             .join(db.User)
             .join(db.Participant, and_(
-                    db.Participant.user_id == db.Participation.user_id,
-                    db.Participant.year == round.year
-                )
-            ).filter(
+                db.Participant.user_id == db.Participation.user_id,
+                db.Participant.year == round.year
+            )).filter(
                 db.User.is_test == False,
                 db.Participation.state.in_(part_states),
                 db.Participation.contest_id.in_(contest_subq)
@@ -141,7 +149,19 @@ class Score:
         self._load_tasks_and_sols(0, round, contest_subq)
         self._mark_winners()
 
-    def _load_tasks_and_sols(self, step: int, round: db.Round, contest_subq: Query):
+        # Vynecháme účastníky, kteří nic neodevzdali
+        to_remove = []
+        for user_id, results in self._results.items():
+            if not results._sols[0]:
+                to_remove.append(user_id)
+        if to_remove:
+            self._add_message('info',
+                inflect_with_number(len(to_remove), 'Vynechán %s soutěžící', 'Vynecháni %s soutěžící', 'Vynecháno %s soutěžících')
+                + ' bez odevzdaných řešení.')
+            for user_id in to_remove:
+                self._results.pop(user_id)
+
+    def _load_tasks_and_sols(self, step: int, round: db.Round, contest_subq: Union[Query, List[int]]):
         """Obecná funkce na načtení úloh a řešení tohoto nebo předchozího kola"""
         if step in self._tasks:
             return
@@ -186,14 +206,8 @@ class Score:
     def _mark_winners(self):
         for result in self._results.values():
             total_points = result.get_total_points()
-            result.winner = (
-                self.round.score_winner_limit is not None
-                and total_points >= self.round.score_winner_limit
-            )
-            result.successful = (
-                self.round.score_successful_limit is not None
-                and total_points >= self.round.score_successful_limit
-            )
+            result.winner = self.want_winners and total_points >= self.round.score_winner_limit
+            result.successful = self.want_successful and total_points >= self.round.score_successful_limit
 
     def _load_prev_round(self, step: int) -> bool:
         """Načtení úloh a řešení předchozího kola, pokud takové existuje."""
@@ -253,7 +267,7 @@ class Score:
         results: List[ScoreResult] = sorted(self._results.values(), key=lambda result: (
             result._order_key, result.user.last_name, result.user.first_name, result.user.user_id
         ))
-        last: ScoreResult = None
+        last: Optional[ScoreResult] = None
         # Spočítáme pořadí - v prvním kroku prolinkujeme opakující se ScoreOrder na první,
         # ve druhém kroku je pak správně rozkopírujeme s nastaveným continuation na True
         for result in results:
@@ -266,7 +280,7 @@ class Score:
             else:
                 result.order = ScoreOrder(last.order.place + last.order.span)
                 last = result
-        lastOrder: ScoreOrder = None
+        lastOrder: Optional[ScoreOrder] = None
         for result in results:
             if result.order == lastOrder:
                 result.order = ScoreOrder(lastOrder.place, lastOrder.span, True)
@@ -313,10 +327,10 @@ class Score:
                 if last_task is not None and difficulty == last_difficulty and difficulty != 0:
                     self._add_message(
                         "warning",
-                        f"Úlohy {last_task.task.code} a {task.task.code} mají stejnou vypočtenou obtížnost "
+                        f"Úlohy {last_task.task.code} a {task.task.code} mají stejnou vypočtenou obtížnost"
                         + f" {difficulty}, pro výpočet obtížnosti je řadím podle kódu úlohy"
                     )
-                difficulty_report.append(f"{task.task.code} ({round(float(difficulty), 2)} b)")
+                difficulty_report.append(f"{task.task.code} ({task.get_difficulty_str()}={float(difficulty):.2f})")
                 last_task, last_difficulty = task, difficulty
 
             self._add_message(
@@ -337,8 +351,12 @@ class Score:
                 points_from_max = list(sorted(sol_points.values()))
                 points_from_difficult = [sol_points[task_id] for task_id in tasks_by_difficulty]
 
-                # Primárně podle počtu získaných bodů, sekundárně podle bodů od maxima, terciárně podle bodů od nejobtížnější
-                result._order_key.extend((total_points, points_from_max, points_from_difficult))
+                if result.successful or not self.want_successful:
+                    # Primárně podle počtu získaných bodů, sekundárně podle bodů od maxima, terciárně podle bodů od nejobtížnější
+                    result._order_key.extend((total_points, points_from_max, points_from_difficult))
+                else:
+                    # Neúspěšné řešitele třídíme podle počtu získaných bodů, sekundárně podle jména
+                    result._order_key.extend((total_points, result.user.name_sort_key()))
 
             # Otestujeme, jestli teď existují sdílená místa
             if not self._exists_same_order_key():
@@ -373,12 +391,12 @@ class Score:
                     winners += 1
                 if result.successful:
                     successfulls += 1
-            if successfulls > participants/2:
+            if successfulls > participants // 2:
                 self._add_message(
                     "error",
                     f"Počet úspěšných řešitelů ({successfulls}) převyšuje polovinu celkového počtu účastníků ({participants})"
                 )
-            if winners > successfulls/2:
+            if winners > successfulls // 2:
                 self._add_message(
                     "error",
                     f"Počet vítězů ({winners}) převyšuje polovinu počtu úspěšných řešitelů ({successfulls})"
diff --git a/mo/util_format.py b/mo/util_format.py
index d1b83905c11fe8fef9e59c3db63166c2ed9802a2..50e0f43457b6bb887b2935a40c68da9b3d3300f2 100644
--- a/mo/util_format.py
+++ b/mo/util_format.py
@@ -24,6 +24,15 @@ def inflect_by_number(n: int, w1: str, w234: str, wother: str, unitprefix: str =
         return f'{unitprefix}{wother}'
 
 
+def inflect_with_number(n: int, w1: str, w234: str, wother: str) -> str:
+    if n == 1:
+        return w1 % n
+    elif 2 <= n <= 4:
+        return w234 % n
+    else:
+        return wother % n
+
+
 def timeformat(dt: datetime) -> str:
     if dt is None:
         return '–'
diff --git a/mo/web/org_score.py b/mo/web/org_score.py
index 190e5e44911f393116c8f34d6597b0c15a440b87..d0613d2aaae9b55efccc80d830b05af6b3106fdd 100644
--- a/mo/web/org_score.py
+++ b/mo/web/org_score.py
@@ -41,7 +41,7 @@ class SolPointsCell(Cell):
     user: db.User
     sol: Optional[db.Solution]
 
-    def __init__(self, contest_id: int, user: db.User, sol: db.Solution):
+    def __init__(self, contest_id: int, user: db.User, sol: Optional[db.Solution]):
         self.contest_id = contest_id
         self.user = user
         self.sol = sol
@@ -117,8 +117,7 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
     columns = []
     columns.append(Column(key='order', name='poradi', title='Pořadí'))
     if is_export:
-        columns.append(Column(key='winner', name='vitez'))
-        columns.append(Column(key='successful', name='uspesny_resitel'))
+        columns.append(Column(key='status', name='stav'))
     columns.append(Column(key='participant', name='ucastnik', title='Účastník'))
     if is_export:
         columns.append(Column(key='email', name='email'))
@@ -152,21 +151,35 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
         user, pant, pion = result.user, result.pant, result.pion
         school = pant.school_place
         local_pion_ct_id = subcontest_id_map[(round.round_id, pion.contest_id)]
+
+        order_cell: Union[Cell, str]
+        if result.successful or not score.want_successful:
+            order_cell = OrderCell(result.order.place, result.order.span, result.order.continuation)
+        else:
+            order_cell = ""
+
+        if result.winner:
+            status = 'vítěz'
+        elif result.successful:
+            status = 'úspěšný'
+        else:
+            status = ""
+
         row = Row(keys={
-            'order':        OrderCell(result.order.place, result.order.span, result.order.continuation),
-            'winner':       'ano' if result.winner else '',
-            'successful':   'ano' if result.successful else '',
+            'order':        order_cell,
+            'status':       status,
             'user':         user,
             'email':        user.email,
             'participant':  cell_pion_link(user, local_pion_ct_id, user.full_name()),
-            'contest':      CellLink(pion.contest.place.name, url_for('org_contest', id=pion.contest_id)),
+            'contest':      CellLink(pion.contest.place.name or "?", url_for('org_contest', id=pion.contest_id)),
             'pion_place':   pion.place.name,
-            'school':       CellLink(school.name, url_for('org_place', id=school.place_id)),
+            'school':       CellLink(school.name or "?", url_for('org_place', id=school.place_id)),
             'grade':        pant.grade,
             'total_points': result.get_total_points(),
             'birth_year':   pant.birth_year,
             'order_key':    result._order_key,
         })
+
         sols = result.get_sols_map()
         for task in tasks:
             local_sol_ct_id = subcontest_id_map[(task.round_id, pion.contest_id)]
@@ -180,7 +193,7 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
         table_rows.append(row)
 
     filename = f"vysledky_{round.year}-{round.category}-{round.level}"
-    if contest_id:
+    if contest:
         filename += f"_oblast_{contest.place.code or contest.place.place_id}"
     table = Table(
         table_class="data full center",