diff --git a/loanpy/src/loan/accept_loan_request.py b/loanpy/src/loan/accept_loan_request.py
index fd39bd5ff83cff3ffa67a9d29c3c07f91125a961..4d79edc177ff342f4b12669d42da1620ab92fdac 100755
--- a/loanpy/src/loan/accept_loan_request.py
+++ b/loanpy/src/loan/accept_loan_request.py
@@ -23,17 +23,15 @@ Accept a loan request.
 from __future__ import absolute_import
 import caosdb as db
 from caosadvancedtools.serverside.helper import print_success, get_timestamp
-from .box_loan import (main, get_loan, F_LOAN, assert_loan_state,
+from .box_loan import (main, get_loan, F_LOAN,
                          LOAN_ACCEPTED, S_LOAN_ACCEPTED, S_LOAN_REQUESTED,
                          get_borrower_names, set_property)
 
 
 def _accept_loan_request(loan):
     """Update a loan Record: add the `accepted` Property."""
-    assert_loan_state(loan, S_LOAN_REQUESTED)
-
     # This changes the state from "loan_requested" to "loan_accepted".
-    set_property(loan, LOAN_ACCEPTED, get_timestamp())
+    update_loan_state(loan, LOAN_ACCEPTED)
 
     db.Container().extend([loan]).update()
 
diff --git a/loanpy/src/loan/accept_return_request.py b/loanpy/src/loan/accept_return_request.py
index ba91d876787a0d2124f48702f2b646f88159f3b0..7a647e09ee728b7e2c4c0b83e88d12c95baddf72 100755
--- a/loanpy/src/loan/accept_return_request.py
+++ b/loanpy/src/loan/accept_return_request.py
@@ -25,17 +25,15 @@ from __future__ import absolute_import
 import caosdb as db
 from caosadvancedtools.serverside.helper import print_success, get_timestamp
 from .box_loan import (BOX_BORROWED, CONTENT, main, get_loan, set_property,
-                      F_LOAN, assert_loan_state, RETURN_ACCEPTED,
+                      F_LOAN, RETURN_ACCEPTED,
                       RETURNLOCATION, S_RETURN_ACCEPTED, S_RETURN_REQUESTED,
                       get_borrower_names, set_location_of_borrowed_items)
 
 
 def _accept_return_request(loan):
     """Update a loan Record and add the `accepted` Property."""
-    assert_loan_state(loan, S_RETURN_REQUESTED)
-
     # This changes the state from "return_requested" to "return_accepted".
-    set_property(loan, RETURN_ACCEPTED, get_timestamp())
+    update_loan_state(loan, RETURN_ACCEPTED)
 
     items = set_location_of_borrowed_items(loan, RETURNLOCATION)
     if loan.get_property(CONTENT) is not None and loan.get_property(CONTENT).value:
diff --git a/loanpy/src/loan/box_loan.py b/loanpy/src/loan/box_loan.py
index 08bcac6ab299cd235e9086e228ba02ed30e17e4a..bf2a362d5f5414f9e10707da1b798f4e11451354 100644
--- a/loanpy/src/loan/box_loan.py
+++ b/loanpy/src/loan/box_loan.py
@@ -20,7 +20,7 @@
 Creates a loan request using information provided by a web formular.
 """
 
-from __future__ import absolute_import
+from __future__ import absolute_import, annotations
 
 import logging
 import sys
@@ -29,7 +29,7 @@ from datetime import datetime
 
 import caosdb as db
 from caosadvancedtools.serverside.helper import (DataModelError, get_data,
-                                                 init_data_model,
+                                                 init_data_model,get_timestamp,
                                                  parse_arguments, print_error,
                                                  print_info, print_warning,
                                                  send_mail)
@@ -144,9 +144,9 @@ class StateError(RuntimeError):
         _expected = expected[0] if len(expected) == 1 else ",".join(
             expected[:-1]) + ", or" + expected[-1]
         super().__init__(
-            "This transition of the loan state is not possible. The expected "
+            "This transition of the loan state is not possible. The allowed target "
             "state is {one}'{expected}' "
-            "while the actual state is '{actual}'.".format(one=one,
+            "while the actual target state is '{actual}'.".format(one=one,
                                                            actual=actual,
                                                            expected=_expected))
 
@@ -212,6 +212,39 @@ def get_loan(loan_id):
     return get_record_by_id(LOAN.name, loan_id)
 
 
+def update_loan_state(loan, new_state):
+    old_state = get_loan_state(loan)
+    if old_state==S_LOAN_REQUESTED:
+        if new_state==S_LOAN_ACCEPTED:
+            loan.add_property(LOAN_ACCEPTED, get_timestamp())
+        else:
+            raise StateError(new_state, [S_LOAN_ACCEPTED])
+    elif old_state==S_LOAN_ACCEPTED:
+        if new_state==S_LENT:
+            loan.add_property(LENT, get_timestamp())
+        else:
+            raise StateError(new_state, [S_LENT])
+    elif old_state==S_LENT:
+        if new_state==S_RETURN_REQUESTED:
+            loan.add_property(RETURN_REQUESTED, get_timestamp())
+        else:
+            raise StateError(new_state, [S_RETURN_REQUESTED])
+    elif old_state==S_RETURN_REQUESTED:
+        if new_state==S_RETURN_ACCEPTED:
+            loan.add_property(RETURN_ACCEPTED, get_timestamp())
+        elif new_state==S_LENT:
+            loan.remove_property(RETURN_REQUESTED)
+        else:
+            raise StateError(new_state, [S_RETURN_ACCEPTED, S_LENT])
+    elif old_state==S_RETURN_ACCEPTED:
+        if new_state==S_RETURNED:
+            loan.add_property(RETURNED, get_timestamp())
+        else:
+            raise StateError(new_state, [S_RETURNED])
+    else:
+        raise RuntimeError(f"Old state {old_state} is unknown.")
+
+
 def get_loan_state(loan):
     if loan.get_parent(LOAN) is None:
         raise MissingParentError(loan, LOAN)
diff --git a/loanpy/src/loan/confirm_loan.py b/loanpy/src/loan/confirm_loan.py
index dd0ac89e7028f94fbd7509b91b7b803b662a19b1..e21496f599205e1115f5543348cdf7e40c2cb484 100755
--- a/loanpy/src/loan/confirm_loan.py
+++ b/loanpy/src/loan/confirm_loan.py
@@ -26,7 +26,7 @@ import caosdb as db
 from caosadvancedtools.serverside.helper import get_timestamp, print_success
 
 from .box_loan import (BOX, BOX_BORROWED, DESTINATION, F_LOAN, LENT, S_LENT,
-                       S_LOAN_ACCEPTED, assert_loan_state, get_borrower_names,
+                       S_LOAN_ACCEPTED, get_borrower_names,
                        get_loan, main, set_location_of_borrowed_items, set_property)
 
 
@@ -50,18 +50,14 @@ def set_loan_location(loan):
 def _confirm_loan(data):
     """Update a loan Record and add the `lent` Property."""
     loan = get_loan(data[F_LOAN])
-    assert_loan_state(loan, S_LOAN_ACCEPTED)
-
     # This changes the state from "loan_accepted" to "lent".
-    set_property(loan, LENT, get_timestamp())
+    update_loan_state(loan, S_LENT)
+
     _set_lent_box(loan)
 
     # updates the box location
     set_loan_location(loan)
 
-    # To be sure that it worked:
-    assert_loan_state(loan, S_LENT)
-
     db.Container().extend([
         loan
     ]).update()
diff --git a/loanpy/src/loan/manual_return.py b/loanpy/src/loan/manual_return.py
index 02003259e05e96293f3d614c2b2ad7b682327edf..0bb6bce242b102d4ad28f719b323b2935304d387 100755
--- a/loanpy/src/loan/manual_return.py
+++ b/loanpy/src/loan/manual_return.py
@@ -27,7 +27,7 @@ from caosadvancedtools.serverside.helper import get_timestamp, print_success
 
 from .box_loan import (BOX, BOX_BORROWED, BOX_RETURNED, CONTENT, F_LOAN,
                        RETURNED, RETURNLOCATION, S_RETURN_ACCEPTED, S_RETURNED,
-                       assert_loan_state, get_borrower_names, get_loan, main,
+                       get_borrower_names, get_loan, main,
                        set_location_of_borrowed_items, set_property)
 
 
@@ -60,21 +60,15 @@ def set_content(loan):
 def _manual_return(data):
     """Update a loan Record and add the `returned` Property."""
     loan = get_loan(data[F_LOAN])
-    assert_loan_state(loan, S_RETURN_ACCEPTED)
 
     # This changes the state from "return_accepted" to "returned".
-    # TODO why twice????
-    set_property(loan, RETURNED, get_timestamp())
-    set_property(loan, RETURNED, get_timestamp())
+    update_loan_state(loan, S_RETURNED)
 
     # updates the box location and content
     # *currently this is not wanted*
     # set_return_location(loan)
     # set_content(loan)
 
-    # To be sure that it worked:
-    assert_loan_state(loan, S_RETURNED)
-
     # add returned box
     _set_returned_box(loan)
 
diff --git a/loanpy/src/loan/reject_return_request.py b/loanpy/src/loan/reject_return_request.py
index 58d2349055bf0870de257b6a1a250e18da24a65a..0c068234d620645ae52150cf40e1d62fb4ed0120 100755
--- a/loanpy/src/loan/reject_return_request.py
+++ b/loanpy/src/loan/reject_return_request.py
@@ -23,27 +23,18 @@ Reject a return request.
 from __future__ import absolute_import
 import caosdb as db
 from caosadvancedtools.serverside.helper import print_success
-from .box_loan import (main, get_loan, F_LOAN, assert_loan_state,
+from .box_loan import (main, get_loan, F_LOAN,
                          S_RETURN_REQUESTED, RETURN_REQUESTED, S_LENT,
                          get_borrower_names)
 
 
-def _reject_return_request(data):
+def _reject_return_request(loan):
     """Update a `Loan` Record and remove the `returnRequested` Property."""
-    loan = get_loan(data[F_LOAN])
-    assert_loan_state(loan, S_RETURN_REQUESTED)
 
     # This changes the state from "return_requested" back to "lent".
-    loan.remove_property(RETURN_REQUESTED)
-
-    # To be sure that it worked:
-    assert_loan_state(loan, S_LENT)
+    update_loan_state(loan, S_LENT)
 
-    db.Container().extend([
-        loan
-    ]).update()
-
-    return loan
+    loan.update()
 
 
 def reject_return_request(data):
@@ -51,7 +42,8 @@ def reject_return_request(data):
 
     I.e. update the `Loan` Record and remove the `returnRequested` Property.
     """
-    loan = _reject_return_request(data)
+    loan = get_loan(data[F_LOAN])
+    _reject_return_request(loan)
     fn, ln = get_borrower_names(loan)
 
     print_success('You rejected return request by {fn} {ln}. See '
diff --git a/loanpy/src/loan/request_return.py b/loanpy/src/loan/request_return.py
index 011f85aa206395a7c9a844a56e3184ca5157b081..823e0ce9e49e3844c4829d9c27875e6bef35b67d 100755
--- a/loanpy/src/loan/request_return.py
+++ b/loanpy/src/loan/request_return.py
@@ -33,7 +33,7 @@ from .box_loan import (BORROWER, COMMENT, CONTENT, EXPECTED_RETURN, F_COMMENT,
                       F_FIRST_NAME, F_LAST_NAME, F_LOAN, FIRST_NAME, LAST_NAME,
                       RETURN_REQUESTED, RETURNLOCATION, S_LENT,
                       assert_date_in_future, assert_key_in_data,
-                      assert_loan_state, get_loan, insert_or_update_person, main,
+                      get_loan, insert_or_update_person, main,
                       send_return_request_mail, set_property)
 
 
@@ -53,10 +53,9 @@ def _check_data(data):
 def _issue_return_request(data):
     _check_data(data)
     loan = get_loan(data[F_LOAN])
-    assert_loan_state(loan, S_LENT)
+    update_loan_state(loan, S_RETURN_REQUESTED)
 
     returner = insert_or_update_person(data[F_FIRST_NAME], data[F_LAST_NAME], data[F_EMAIL])
-    loan.add_property(RETURN_REQUESTED, get_timestamp())
 
     set_property(loan, BORROWER, returner.id)
     set_property(loan, EXPECTED_RETURN, data[F_EXPECTED_RETURN_DATE])
diff --git a/loanpy/unittests/test_box_loan.py b/loanpy/unittests/test_box_loan.py
index 77906a04891c4c7da901d8863f9d9f814c7ac829..ebeccb7ac904a6e4803f2e6fb299a62d303bda10 100644
--- a/loanpy/unittests/test_box_loan.py
+++ b/loanpy/unittests/test_box_loan.py
@@ -20,10 +20,8 @@ from os.path import abspath, dirname, join
 from pytest import raises
 from linkahead import get_connection, configure_connection
 from linkahead.connection.mockup import (MockUpServerConnection, MockUpResponse)
-from loan.box_loan import (_caller, create_person,
-                         EMAIL, FIRST_NAME, LAST_NAME, PERSON,
-                         assert_date_in_future, DataError, EmailPatternError,
-                         assert_email_pattern)
+from loan.box_loan import *
+from loan.box_loan import _caller
 
 from utils import get_form_data_example
 
@@ -60,3 +58,30 @@ def test_assert_date_in_future():
     assert assert_date_in_future("2050-01-01") is None
     with raises(DataError):
         assert_date_in_future("1971-01-01")
+
+
+def test_loan_states():
+    loan = db.Record()
+    with raises(MissingParentError):
+        assert_loan_state(loan, S_LOAN_REQUESTED)
+    loan.add_parent(LOAN)
+    assert_loan_state(loan, None)
+    loan.add_property(LOAN_REQUESTED, 'a')
+    update_loan_state(loan, S_LOAN_ACCEPTED)
+    assert_loan_state(loan, S_LOAN_ACCEPTED)
+    update_loan_state(loan, S_LENT)
+    assert_loan_state(loan, S_LENT)
+    with raises(StateError):
+        update_loan_state(loan, S_LOAN_REQUESTED)
+    update_loan_state(loan, S_RETURN_REQUESTED)
+    assert_loan_state(loan, S_RETURN_REQUESTED)
+    update_loan_state(loan, S_LENT)
+    assert_loan_state(loan, S_LENT)
+    update_loan_state(loan, S_RETURN_REQUESTED)
+    assert_loan_state(loan, S_RETURN_REQUESTED)
+    update_loan_state(loan, S_RETURN_ACCEPTED)
+    assert_loan_state(loan, S_RETURN_ACCEPTED)
+    with raises(StateError):
+        update_loan_state(loan, S_LOAN_REQUESTED)
+    update_loan_state(loan, S_RETURNED)
+    assert_loan_state(loan, S_RETURNED)