diff --git a/mo/imports.py b/mo/imports.py
index 74a05cca117585c43bb67c5d1054571f7871a42c..0648efb3f44af21c3b6a0a5fec6d029192a06a18 100644
--- a/mo/imports.py
+++ b/mo/imports.py
@@ -2,6 +2,8 @@ from dataclasses import dataclass
 from enum import auto
 import io
 import re
+from sqlalchemy import and_
+from sqlalchemy.orm import joinedload, Query
 from typing import List, Optional, Any, Dict, Type
 
 import mo.csv
@@ -17,6 +19,7 @@ class ImportType(db.MOEnum):
     participants = auto()
     proctors = auto()
     judges = auto()
+    points = auto()
 
     def friendly_name(self) -> str:
         return import_type_names[self]
@@ -26,6 +29,7 @@ import_type_names = {
     ImportType.participants.name: 'účastníci',
     ImportType.proctors.name: 'dozor',
     ImportType.judges.name: 'opravovatelé',
+    ImportType.points.name: 'body',
 }
 
 
@@ -37,6 +41,7 @@ class Import:
     cnt_new_participants: int = 0
     cnt_new_participations: int = 0
     cnt_new_roles: int = 0
+    cnt_set_points: int = 0
 
     # Veřejné vlastnosti importu
     template_basename: str = "sablona"
@@ -45,6 +50,7 @@ class Import:
     user: db.User
     round: Optional[db.Round]
     contest: Optional[db.Contest]
+    task: Optional[db.Task]
     fmt: FileFormat
     row_class: Type[mo.csv.Row]
     row_example: mo.csv.Row
@@ -76,6 +82,15 @@ class Import:
         logger.info('Import: >> %s', msg)
         return None     # Kdyby bylo otypováno správně jako -> None, při volání by si mypy stěžoval
 
+    def parse_user_id(self, user_id_str: str) -> Optional[int]:
+        if user_id_str == "":
+            return self.error('Chybí ID uživatele')
+
+        try:
+            return int(user_id_str)
+        except ValueError:
+            return self.error('ID uživatele není číslo')
+
     def parse_email(self, email: str) -> Optional[str]:
         if email == "":
             return self.error('Chybí e-mailová adresa')
@@ -187,6 +202,20 @@ class Import:
             self.new_user_ids.append(user.user_id)
         return user
 
+    def parse_points(self, points_str: str) -> Optional[int]:
+        if points_str == "":
+            return None
+
+        try:
+            pts = int(points_str)
+        except ValueError:
+            return self.error('Body nejsou celé číslo')
+
+        if pts < 0:
+            return self.error('Body nesmí být záporné')
+
+        return pts
+
     def find_or_create_participant(self, user: db.User, year: int, school_id: int, birth_year: int, grade: str) -> Optional[db.Participant]:
         sess = db.get_session()
         part = sess.query(db.Participant).get((user.user_id, year))
@@ -287,11 +316,24 @@ class Import:
             args.append(f'round=#{self.round.round_id}')
         if self.contest is not None:
             args.append(f'contest=#{self.contest.contest_id}')
+        if self.task is not None:
+            args.append(f'task=#{self.task.task_id}')
 
         logger.info('Import: %s ze souboru %s: %s', self.log_msg_prefix, path, " ".join(args))
 
     def log_end(self):
-        logger.info(f'Import: Hotovo (rows={self.cnt_rows} users={self.cnt_new_users} p-ants={self.cnt_new_participants} p-ions={self.cnt_new_participations} roles={self.cnt_new_roles})')
+        args = [f'rows=#{self.cnt_rows}']
+        for key, val in [
+            ('users', self.cnt_new_users),
+            ('p-ants', self.cnt_new_participants),
+            ('p-ions', self.cnt_new_participations),
+            ('roles', self.cnt_new_roles),
+            ('points', self.cnt_set_points),
+        ]:
+            if val > 0:
+                args.append(f'{key}={val}')
+        logger.info('Import: Hotovo (%s)', " ".join(args))
+
         if self.contest is not None:
             mo.util.log(
                 type=db.LogType.contest,
@@ -362,6 +404,7 @@ class Import:
                 sess.rollback()
 
     def get_template(self) -> str:
+        # Odvozené třídy mohou přetížit
         out = io.StringIO()
         mo.csv.write(file=out, fmt=self.fmt, row_class=self.row_class, rows=[self.row_example])
         return out.getvalue()
@@ -541,7 +584,114 @@ class JudgeImport(Import):
         self.add_role(user, place, db.RoleType.opravovatel)
 
 
-def create_import(user: db.User, type: ImportType, fmt: FileFormat, round: Optional[db.Round] = None, contest: Optional[db.Contest] = None):
+@dataclass
+class PointsImportRow(mo.csv.Row):
+    user_id: str = ""
+    krestni: str = ""
+    prijmeni: str = ""
+    body: str = ""
+
+
+class PointsImport(Import):
+    row_class = PointsImportRow
+    log_msg_prefix = 'Body'
+
+    def setup(self):
+        assert self.round is not None
+        assert self.task is not None
+        self.log_details = {'action': 'import-points', 'task': self.task.code}
+        self.template_basename = 'body-' + self.task.code
+
+    def _pion_sol_query(self) -> Query:
+        sess = db.get_session()
+        query = (sess.query(db.Participation, db.Solution)
+                 .select_from(db.Participation)
+                 .outerjoin(db.Solution, and_(db.Solution.user_id == db.Participation.user_id, db.Solution.task == self.task))
+                 .options(joinedload(db.Participation.user)))
+
+        if self.contest is not None:
+            query = query.filter(db.Participation.contest == self.contest)
+        else:
+            contest_query = sess.query(db.Contest.contest_id).filter_by(round=self.round)
+            query = query.filter(db.Participation.contest_id.in_(contest_query.subquery()))
+
+        return query
+
+    def import_row(self, r: mo.csv.Row):
+        assert isinstance(r, PointsImportRow)
+        num_prev_errs = len(self.errors)
+        user_id = self.parse_user_id(r.user_id)
+        krestni = self.parse_name(r.krestni)
+        prijmeni = self.parse_name(r.prijmeni)
+        body = self.parse_points(r.body)
+
+        if (len(self.errors) > num_prev_errs
+                or user_id is None
+                or krestni is None
+                or prijmeni is None):
+            return
+
+        assert self.round is not None
+        assert self.task is not None
+
+        sess = db.get_session()
+        query = self._pion_sol_query().filter(db.Solution.user_id == user_id)
+        pion_sols = query.all()
+        if not pion_sols:
+            return self.error('Soutěžící nenalezen v tomto kole')
+        elif len(pion_sols) > 1:
+            return self.error('Soutěžící v tomto kole soutěží vícekrát, neumím zpracovat')
+        pion, sol = pion_sols[0]
+
+        if self.contest is not None:
+            if pion.contest != self.contest:
+                return self.error('Soutěžící nesoutěží v této oblasti')
+
+        rights = self.gatekeeper.rights_for_contest(pion.contest)
+        if not rights.can_edit_points(self.round):
+            return self.error('Nemáte právo na úpravu bodů')
+
+        user = pion.user
+        if user.first_name != krestni or user.last_name != prijmeni:
+            return self.error('Neodpovídá ID a jméno soutěžícího')
+
+        if sol is None:
+            return self.error('Tento soutěžící úlohu neodevzdal')
+
+        if sol.points != body:
+            sol.points = body
+            sess.add(db.PointsHistory(
+                task=self.task,
+                participant_id=user_id,
+                user=self.user,
+                points_at=mo.now,
+                points=body,
+            ))
+            self.cnt_set_points += 1
+
+    def get_template(self) -> str:
+        rows = []
+        for pion, sol in sorted(self._pion_sol_query().all(), key=lambda pair: pair[0].user.sort_key()):
+            if sol is not None:
+                user = pion.user
+                rows.append(PointsImportRow(
+                    user_id=user.user_id,
+                    krestni=user.first_name,
+                    prijmeni=user.last_name,
+                    body=sol.points,
+                ))
+
+        out = io.StringIO()
+        mo.csv.write(file=out, fmt=self.fmt, row_class=self.row_class, rows=rows)
+        return out.getvalue()
+
+
+def create_import(user: db.User,
+                  type: ImportType,
+                  fmt: FileFormat,
+                  round: Optional[db.Round] = None,
+                  contest: Optional[db.Contest] = None,
+                  task: Optional[db.Task] = None):
     imp: Import
     if type == ImportType.participants:
         imp = ContestImport()
@@ -549,12 +699,15 @@ def create_import(user: db.User, type: ImportType, fmt: FileFormat, round: Optio
         imp = ProctorImport()
     elif type == ImportType.judges:
         imp = JudgeImport()
+    elif type == ImportType.points:
+        imp = PointsImport()
     else:
         assert False, "Neznámý typ importu"
 
     imp.user = user
     imp.round = round
     imp.contest = contest
+    imp.task = task
     imp.fmt = fmt
     imp.gatekeeper = mo.rights.Gatekeeper(user)
     imp.setup()