diff --git a/bin/add-role b/bin/add-role
index d1dacbbd2680c5f786b0a534a3ccea50c4581530..42374eeb5b7ba94b684f7261850011dd50687412 100755
--- a/bin/add-role
+++ b/bin/add-role
@@ -1,20 +1,20 @@
 #!/usr/bin/env python3
 
 import argparse
-import re
-import sys
 
 import mo.db as db
 import mo.util
+from mo.util import die
 
 
 parser = argparse.ArgumentParser(description='Přidělí uživateli roli')
 parser.add_argument('--email', type=str, help='e-mailová adresa uživatele')
 parser.add_argument('--uid', type=int, help='ID uživatele')
 parser.add_argument('--role', type=str, help='název role', required=True)
-parser.add_argument('--place-id', type=int, help='omezení role na místo s daným ID')
+parser.add_argument('--place', type=str, help='omezení role na místo s daným kódem')
 parser.add_argument('--cat', type=str, help='omezení role na danou kategorii')
-parser.add_argument('--round', type=str, metavar='YY-R', help='omezení role na dané kolo')
+parser.add_argument('--year', type=int, help='omezení role na daný ročník')
+parser.add_argument('--seq', type=int, help='omezení role na dané pořadí kola')
 
 args = parser.parse_args()
 session = db.get_session()
@@ -29,58 +29,39 @@ else:
     parser.error('Je nutné vybrat uživatele pomocí --email nebo --uid')
 
 if not user:
-    print("Tento uživatel neexistuje", file=sys.stderr)
-    sys.exit(1)
+    die("Tento uživatel neexistuje")
 
 if user.is_admin:
-    print("Tento uživatel je admin, nemá smysl přidělovat mu další práva", file=sys.stderr)
-    sys.exit(1)
+    die("Tento uživatel je admin, nemá smysl přidělovat mu další práva")
 
 if not user.is_org:
-    print("Tento uživatel není organizátor", file=sys.stderr)
-    sys.exit(1)
+    die("Tento uživatel není organizátor")
 
 role = getattr(db.RoleType, args.role, None)
 if role is None:
-    print("Tato role neexistuje", file=sys.stderr)
-    sys.exit(1)
+    die("Tato role neexistuje")
 
-for_round = None
-if args.round:
-    m = re.fullmatch(r'(\d+)-(\d+)', args.round)
-    if not m:
-        parser.error('Chybná syntaxe --round')
-    if not args.cat:
-        parser.error('--round vyžaduje zadání kategorie')
-    round = session.query(db.Round).filter_by(year=int(m[1]), category=args.cat, level=int(m[2])).first()
-    if not round:
-        print("Toto kolo neexistuje", file=sys.stderr)
-        sys.exit(1)
-    for_round = round.round_id
-
-if args.place_id:
-    for_place = args.place_id
+if args.place:
+    for_place = db.get_place_by_code(args.place)
+    if for_place is None:
+        die("Toto místo neexistuje")
 else:
-    place = session.query(db.Place).filter_by(level=1).first()
-    assert place
-    for_place = place.place_id
+    for_place = db.get_root_place()
 
+assert for_place is not None
 ur = db.UserRole(
     user_id=user.user_id,
-    place_id=for_place,
+    place=for_place,
     role=role,
     category=args.cat,
-    round_id=for_round,
+    year=args.year,
+    seq=args.seq,
 )
 session.add(ur)
 session.flush()
 
 mo.util.log(db.LogType.user_role, ur.user_role_id, {
     'action': 'assigned',
-    'user_id': ur.user_id,
-    'place_id': ur.place_id,
-    'role': role.name,
-    'category': ur.category,
-    'round_id': ur.round_id,
+    'new': db.row2dict(ur),
 })
 session.commit()
diff --git a/bin/create-contests b/bin/create-contests
index 271335a916fb877f32c541f6c40b6015534e4950..749d64e0fcda6a77a8d074ec84680fa7d7b60b08 100755
--- a/bin/create-contests
+++ b/bin/create-contests
@@ -1,29 +1,31 @@
 #!/usr/bin/env python3
 
 import argparse
-import sys
 
 import mo.db as db
 import mo.util
+from mo.util import die
 
 parser = argparse.ArgumentParser(description='Založí soutěže pro dané kolo')
-parser.add_argument(dest='round_id', type=int, help='ID kola')
+parser.add_argument(dest='round', type=str, metavar='YY-C-S', help='ID kola')
 parser.add_argument('-n', '--dry-run', default=False, action='store_true', help='pouze ukáže, co by bylo provedeno')
 
 args = parser.parse_args()
 
 sess = db.get_session()
 
-round = sess.query(db.Round).get(args.round_id)
-if not round:
-    print("Kolo s tímto ID neexistuje!", file=sys.stderr)
-    sys.exit(1)
+round_code = mo.util.RoundCode.parse(args.round)
+if round_code is None:
+    die("Chybná syntaxe kódu kola")
+round = mo.util.get_round_by_code(round_code)
+if round is None:
+    die("Kolo s tímto kódem neexistuje!")
 
 regions = sess.query(db.Place).filter_by(level=round.level).all()
 assert regions, "Neexistují žádná místa dané úrovně"
 
 for r in regions:
-    print(f"Zakládám pro místo {r.name}")
+    print(f"Zakládám {round.round_code()} pro místo {r.name}")
     if not args.dry_run:
         c = db.Contest(round=round, place=r)
         sess.add(c)
diff --git a/bin/create-round b/bin/create-round
new file mode 100755
index 0000000000000000000000000000000000000000..4217d23db23bb864394145a57433828219cd84a8
--- /dev/null
+++ b/bin/create-round
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+
+import argparse
+
+import mo.db as db
+import mo.util
+
+parser = argparse.ArgumentParser(description='Založí soutěžní kolo')
+parser.add_argument('-y', '--year', type=int, required=True, help='ročník')
+parser.add_argument('-c', '--cat', type=str, required=True, help='kategorie')
+parser.add_argument('-s', '--seq', type=int, required=True, help='pořadí kola')
+parser.add_argument('-l', '--level', type=int, required=True, help='úroveň v hierarchii oblastí')
+parser.add_argument('-n', '--name', type=str, required=True, help='název kola')
+
+args = parser.parse_args()
+
+sess = db.get_session()
+
+rnd = db.Round(
+    year=args.year,
+    category=args.cat,
+    seq=args.seq,
+    level=args.level,
+    name=args.name,
+)
+
+sess.add(rnd)
+sess.flush()
+
+mo.util.log(
+    type=db.LogType.round,
+    what=rnd.round_id,
+    details={
+        'action': 'created',
+        'new': db.row2dict(rnd),
+    },
+)
+
+sess.commit()
diff --git a/bin/create-user b/bin/create-user
index 13c9dd179ea0178a7698271b84af3103b41f3e13..2f087d11d8ba5abe94dd315a8c04c465c795c4bd 100755
--- a/bin/create-user
+++ b/bin/create-user
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 
 import mo.db as db
+import mo.users
 import mo.util
 import argparse
 
@@ -11,6 +12,7 @@ parser.add_argument(dest='first_name', help='křestní jméno (jedno nebo více)
 parser.add_argument(dest='last_name', help='příjmení (jedno nebo více)')
 parser.add_argument('--org', default=False, action='store_true', help='přidělí uživateli organizátorská práva')
 parser.add_argument('--admin', default=False, action='store_true', help='přidělí uživateli správcovská práva')
+parser.add_argument('--passwd', type=str, help='nastaví počáteční heslo')
 
 args = parser.parse_args()
 
@@ -32,4 +34,8 @@ mo.util.log(db.LogType.user, user.user_id, {
     'is_org': user.is_org,
     'is_admin': user.is_admin
 })
+
+if args.passwd is not None:
+    mo.users.set_password(user, args.passwd)
+
 session.commit()
diff --git a/bin/init-schools b/bin/init-schools
index c5fe314f1beff4c1a8e1f42d8af34bf02a22f182..7991204170c6523e2848f13aeef3733dcf74f6a7 100755
--- a/bin/init-schools
+++ b/bin/init-schools
@@ -30,7 +30,6 @@ def import_schools(path: Path, nuts: str):
 
     with path.open('r') as file:
         columns = parse_header(file.readline())
-        print(columns)
         for line in file:
             f = line.split('\t')
             red_izo = f[columns['Red IZO']]
diff --git a/bin/test-init b/bin/test-init
new file mode 100755
index 0000000000000000000000000000000000000000..700b03932f7ae967cde2b32dc84c3d1eb9627336
--- /dev/null
+++ b/bin/test-init
@@ -0,0 +1,26 @@
+#!/bin/bash
+set -e
+
+psql mo_osmo <db/drop-all.sql
+psql mo_osmo <db/db.ddl
+
+bin/init-regions
+bin/init-schools
+
+bin/create-user mj@ucw.cz Martin Mareš --admin --passwd brum
+
+bin/create-user medved@ucw.cz Baltasis Lokys --org --passwd brum
+bin/add-role --email medved@ucw.cz --role garant --cat P
+bin/add-role --email medved@ucw.cz --role garant --cat A --place PB
+
+bin/create-round -y 70 -c P -s 1 -n 'Školní kolo' -l 1
+bin/create-round -y 70 -c P -s 2 -n 'Krajské kolo' -l 1
+bin/create-round -y 70 -c P -s 3 -n 'Ústřední kolo' -l 0
+
+bin/create-round -y 70 -c A -s 1 -n 'Školní kolo (domácí)' -l 1
+bin/create-round -y 70 -c A -s 2 -n 'Školní kolo (klauzurní)' -l 1
+bin/create-round -y 70 -c A -s 3 -n 'Krajské kolo' -l 1
+bin/create-round -y 70 -c A -s 4 -n 'Ústřední kolo' -l 0
+
+bin/create-contests 70-P-2
+bin/create-contests 70-A-2
diff --git a/db/test-kola.sql b/db/test-kola.sql
deleted file mode 100644
index b0c61096df56564bdb755c2758c885abb9ac5334..0000000000000000000000000000000000000000
--- a/db/test-kola.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-INSERT INTO rounds(year, category, seq, level, name) VALUES
-	(1, 70, 'A', 1, 'Krajské kolo'           , 3 ),
-	(2, 70, 'A', 0, 'Ústrední kolo'          , 4 ),
-	(3, 70, 'A', 4, 'Školní kolo (domácí)'   , 1 ),
-	(5, 70, 'A', 4, 'Školní kolo (klauzurní)', 2 ),
-	(6, 70, 'P', 0, 'Školní kolo'            , 1 ),
-	(7, 70, 'P', 1, 'Krajské kolo'           , 2 ),
-	(8, 70, 'P', 0, 'Ústřední kolo'          , 3 );