From 156dd409e675ad76146552f2eddf1bf034cb7108 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Henrik=20tom=20W=C3=B6rden?= <h.tomwoerden@indiscale.com>
Date: Tue, 5 Nov 2024 15:05:57 +0100
Subject: [PATCH] ENH: Allow to borrow multiple items

The value of "BOX" property of loan may now be a list
---
 .gitlab-ci.yml                                |   4 +-
 loanpy/CHANGELOG.md                           |   1 +
 loanpy/README.md                              |  12 +
 loanpy/integrationtests/basic_test.py         | 440 +++++++++++++++---
 loanpy/pyproject.toml                         |   2 +-
 loanpy/src/loan/accept_loan_request.py        |   9 +-
 loanpy/src/loan/accept_return_request.py      |  52 ++-
 loanpy/src/loan/box_loan.py                   | 159 ++++---
 loanpy/src/loan/conf.py                       |   1 +
 loanpy/src/loan/confirm_loan.py               |  42 +-
 loanpy/src/loan/manual_return.py              |  41 +-
 loanpy/src/loan/reject_return_request.py      |   7 +-
 loanpy/src/loan/request_loan.py               |  33 +-
 loanpy/src/loan/request_return.py             |  13 +-
 loanpy/unittests/test_box_loan.py             |  14 +-
 loanpy/unittests/test_request_loan.py         |  38 +-
 loanpy/unittests/test_request_return.py       |  18 +-
 loanpy/unittests/utils.py                     |   1 +
 .../scripting/home/.pylinkahead.ini           |   1 +
 ...24-10-24T12:10:03.770053767+00:00.dump.sql |   4 +-
 test-profile/profile.yml                      |   4 +-
 21 files changed, 631 insertions(+), 265 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 08cea83..e339dbc 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -176,7 +176,7 @@ code-style:
     - job: build-testenv
       optional: true
   script:
-      - autopep8 -r --diff --exit-code .
+      - autopep8 -r --diff --exit-code --max-line-length=100 .
   allow_failure: true
 
 pylint:
@@ -188,7 +188,7 @@ pylint:
       optional: true
   allow_failure: true
   script:
-    - pylint --unsafe-load-any-extension=y -d all -e E,F src/linkahead_python_package_template
+    - pylint --unsafe-load-any-extension=y -d all -e E,F loanpy/src/loan
 
 # Build the sphinx documentation and make it ready for deployment by Gitlab Pages
 # Special job for serving a static website. See https://docs.gitlab.com/ee/ci/yaml/README.html#pages
diff --git a/loanpy/CHANGELOG.md b/loanpy/CHANGELOG.md
index dd46095..b85d8ec 100644
--- a/loanpy/CHANGELOG.md
+++ b/loanpy/CHANGELOG.md
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [Unreleased] ##
 
 ### Added ###
+* Support for borrowing multiple items
 
 ### Changed ###
 
diff --git a/loanpy/README.md b/loanpy/README.md
index 4e45700..c99c2e2 100644
--- a/loanpy/README.md
+++ b/loanpy/README.md
@@ -22,6 +22,18 @@ If you want to publish the result on PyPi, there is a release.sh helper
 script. Make sure that your `pyproject.toml` contains the correct and complete
 metadata and check the `RELEASE_GUIDELINES.md`
 
+## Configuration
+You should supply the following configuration settings in you `.pylinkahead.ini` file which is used
+by loanpy:
+```
+[Misc]
+sendmail=/usr/local/bin/sendmail_to_file
+entity_loan.curator_mail_from=admin@example.com
+entity_loan.curator_mail_to=admin@example.com
+[sss_helper]
+external_uri='example.com'
+```
+
 ### Unit Tests
 
 Run `tox` or alternatively `pytest unittests/`.
diff --git a/loanpy/integrationtests/basic_test.py b/loanpy/integrationtests/basic_test.py
index 5608091..dd3ef49 100644
--- a/loanpy/integrationtests/basic_test.py
+++ b/loanpy/integrationtests/basic_test.py
@@ -19,31 +19,42 @@
 """
 basic integration test of the loan workflow
 """
-
-from pytest import fixture
 import json
-import linkahead as db
 import re
+from functools import partial
 from tempfile import NamedTemporaryFile
-from linkahead.utils.server_side_scripting import run_server_side_script
-from loan.box_loan import (BORROWER, BOX, COMMENT, DESTINATION, EXHAUST_CONTENTS, LENT,
-                      F_CURRENT_LOCATION,
-                      EXPECTED_RETURN, F_BOX, F_COMMENT, F_DESTINATION,PERSON, LOAN_ACCEPTED,
-                      F_EMAIL, F_EXHAUST_CONTENTS, F_EXPECTED_RETURN_DATE, EMAIL, F_LOAN,
-                      F_FIRST_NAME, F_LAST_NAME, FIRST_NAME, LAST_NAME, LOAN, LOCATION, RETURNED,
-                      RETURN_ACCEPTED,
-                      LOAN_REQUESTED, assert_date_in_future, CONTENT, RETURNLOCATION,
-                      assert_key_in_data, get_box, insert_or_update_person, main, RETURN_REQUESTED,
-                      send_loan_request_mail)
+from unittest.mock import patch
 
+import linkahead as db
+from caosadvancedtools.serverside.helper import init_data_model
+from linkahead.cached import cached_get_entity_by
+from linkahead.utils.get_entity import get_entity_by_id, get_entity_by_name
+from linkahead.utils.server_side_scripting import run_server_side_script
+from loan.accept_loan_request import accept_loan_request
+from loan.accept_return_request import accept_return_request
+from loan.box_loan import (BORROWER, BOX, BOX_BORROWED, BOX_RETURNED, COMMENT, CONTENT, DESTINATION,
+                           EMAIL, EXHAUST_CONTENTS, EXPECTED_RETURN, F_BOX, F_COMMENT,
+                           F_CURRENT_LOCATION, F_DESTINATION, F_EMAIL, F_EXHAUST_CONTENTS,
+                           F_EXPECTED_RETURN_DATE, F_FIRST_NAME, F_LAST_NAME, F_LOAN, FIRST_NAME,
+                           LAST_NAME, LENT, LOAN, LOAN_ACCEPTED, LOAN_REQUESTED, LOCATION, PERSON,
+                           PROPERTIES, RECORD_TYPES, RETURN_ACCEPTED, RETURN_REQUESTED, RETURNED,
+                           RETURNLOCATION, assert_date_in_future, assert_key_in_data,
+                           insert_or_update_person, main, send_loan_request_mail)
+from loan.confirm_loan import confirm_loan
+from loan.manual_return import manual_return
+from loan.request_loan import issue_loan_request
+from loan.request_return import issue_return_request
+from pytest import fixture
 
 TESTLOANCOMMENT = 'This is a test'
 TESTRETURNCOMMENT = 'This is a return test'
+TESTPROP = 'TESTPROP'
 TESTBOXNUMBER = '0123 TEST'
+TESTBOXNUMBER2 = '0124 TEST'
 TESTDESTINATION = 'TEST DEST'
 TESTCURRENTDESTINATION = 'TEST CUR DEST'
 TESTBORROWEREMAIL = 'test@example.com'
-TESTRETURNDATE="2025-03-04"
+TESTRETURNDATE = "2025-03-04"
 TESTFN = "Alice"
 TESTLN = "Wunderland"
 
@@ -55,50 +66,90 @@ def save_dict_to_jsonfile(data):
         json.dump(data, fi)
     return tmp.name
 
+
 def delete_stuff():
     to_be_deleted = db.Container()
     to_be_deleted.extend(db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'"))
     to_be_deleted.extend(db.execute_query(f"FIND '{TESTDESTINATION}'"))
     to_be_deleted.extend(db.execute_query(f"FIND '{TESTCURRENTDESTINATION}'"))
     to_be_deleted.extend(db.execute_query(f"FIND '{TESTBOXNUMBER}'"))
+    to_be_deleted.extend(db.execute_query(f"FIND '{TESTBOXNUMBER2}'"))
     to_be_deleted.extend(db.execute_query(f"FIND person with {EMAIL.name}='{TESTBORROWEREMAIL}'"))
-    if len(to_be_deleted)>0:
+    to_be_deleted.extend(db.execute_query(f"FIND Property '{TESTPROP}'"))
+    if len(to_be_deleted) > 0:
         to_be_deleted.delete()
 
+
 @fixture(autouse=True)
 def prepare():
     delete_stuff()
     db.Record(name=TESTBOXNUMBER).add_parent(BOX.name).insert()
+    db.Record(name=TESTBOXNUMBER2).add_parent(BOX.name).insert()
     db.Record(name=TESTDESTINATION).add_parent(LOCATION.name).insert()
     db.Record(name=TESTCURRENTDESTINATION).add_parent(LOCATION.name).insert()
     (db.Record().add_parent(PERSON.name).add_property(EMAIL.name, TESTBORROWEREMAIL)
                 .add_property(FIRST_NAME.name, TESTFN)
                 .add_property(LAST_NAME.name, TESTLN).insert())
+    db.Property(name=TESTPROP, datatype=db.TEXT).insert()
     yield
     delete_stuff()
 
 
-def test_request_loan():
-
-    ##### request loan #####
+@fixture()
+def base_loan_form_data():
     data = {}
     data[F_EXHAUST_CONTENTS] = False
-    data[F_EXPECTED_RETURN_DATE]= "2042-02-02"
-    data[F_BOX]=db.utils.get_entity.get_entity_by_name(TESTBOXNUMBER).id
-    data[F_COMMENT]=TESTLOANCOMMENT
-    data[F_DESTINATION]=db.utils.get_entity.get_entity_by_name(TESTDESTINATION).id
-    data[F_EMAIL]=TESTBORROWEREMAIL
-    data[F_LAST_NAME]=TESTLN
-    data[F_FIRST_NAME]=TESTFN
-
-    response = run_server_side_script("loan_management/request_loan.py",
-                                    #"pos0",
-                                    #option1="val1",
-                                    files={"-p0": save_dict_to_jsonfile(data)},
-                                    **{"auth-token":db.get_connection()._authenticator.auth_token})
-    assert response.stderr is None
+    data[F_EXPECTED_RETURN_DATE] = "2042-02-02"
+    data[F_COMMENT] = TESTLOANCOMMENT
+    data[F_DESTINATION] = get_entity_by_name(TESTDESTINATION).id
+    data[F_EMAIL] = TESTBORROWEREMAIL
+    data[F_LAST_NAME] = TESTLN
+    data[F_FIRST_NAME] = TESTFN
+    return data
+
+
+@fixture()
+def loan_form_data_single(base_loan_form_data):
+    base_loan_form_data[F_BOX] = get_entity_by_name(TESTBOXNUMBER).id
+    return base_loan_form_data
+
+
+@fixture()
+def loan_form_data_multi(base_loan_form_data):
+    base_loan_form_data[F_BOX] = [
+        get_entity_by_name(TESTBOXNUMBER).id,
+        get_entity_by_name(TESTBOXNUMBER2).id]
+    return base_loan_form_data
+
+
+def get_return_request_data(loanid):
+    data = {}
+    data[F_LOAN] = loanid
+    # TODO: this is writen in the content field. Change name?
+    data[F_COMMENT] = TESTRETURNCOMMENT
+    # TODO: this is written in RETURNLOCATION. Change name?
+    data[F_CURRENT_LOCATION] = get_entity_by_name(TESTCURRENTDESTINATION).id
+    data[F_EXPECTED_RETURN_DATE] = TESTRETURNDATE
+    data[F_EMAIL] = TESTBORROWEREMAIL
+    data[F_LAST_NAME] = TESTLN
+    data[F_FIRST_NAME] = TESTFN
+    return data
+
+
+def test_request_loan(loan_form_data_single):
+    data = loan_form_data_single
+
+    # #### request loan ##### #
+    response = run_server_side_script(
+        "loan_management/request_loan.py",
+        # "pos0",
+        # option1="val1",
+        files={"-p0": save_dict_to_jsonfile(data)},
+        **{"auth-token": db.get_connection()._authenticator.auth_token})
+    assert response.stderr is None, response.stderr+response.stdout
     assert response.code == 0
     loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
+    loanid = loan.id
     assert loan.get_property(f"{BOX.name}").value == data[F_BOX]
     assert loan.get_property(f"{LOAN_REQUESTED.name}").value.startswith("20")
     assert loan.get_property(f"{BORROWER.name}").value == db.execute_query(
@@ -109,57 +160,150 @@ def test_request_loan():
     assert loan.get_property(f"{LOAN_ACCEPTED.name}") is None
     assert loan.get_property(f"{LENT.name}") is None
 
-    ##### accept loan #####
+    # #### accept loan ##### #
     data = {}
-    data[F_LOAN] = loan.id
-    response = run_server_side_script("loan_management/accept_loan_request.py",
-                                    #"pos0",
-                                    #option1="val1",
-                                    files={"-p0": save_dict_to_jsonfile(data)},
-                                    **{"auth-token":db.get_connection()._authenticator.auth_token})
+    data[F_LOAN] = loanid
+    response = run_server_side_script(
+        "loan_management/accept_loan_request.py",
+        # "pos0",
+        # option1="val1",
+        files={"-p0": save_dict_to_jsonfile(data)},
+        **{"auth-token": db.get_connection()._authenticator.auth_token})
     assert response.stderr is None, response.stderr
     assert response.code == 0
-    loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
+    loan = get_entity_by_id(loanid)
     assert loan.get_property(f"{LOAN_ACCEPTED.name}").value.startswith("20")
     assert loan.get_property(f"{LENT.name}") is None
     assert loan.get_property(f"{RETURN_REQUESTED.name}") is None
 
-    ##### confirm loan #####
+    # #### confirm loan #### #
     data = {}
-    data[F_LOAN] = loan.id
-    response = run_server_side_script("loan_management/confirm_loan.py",
-                                    #"pos0",
-                                    #option1="val1",
-                                    files={"-p0": save_dict_to_jsonfile(data)},
-                                    **{"auth-token":db.get_connection()._authenticator.auth_token})
+    data[F_LOAN] = loanid
+    response = run_server_side_script(
+        "loan_management/confirm_loan.py",
+        # "pos0",
+        # option1="val1",
+        files={"-p0": save_dict_to_jsonfile(data)},
+        **{"auth-token": db.get_connection()._authenticator.auth_token})
+    assert response.stderr is None, response.stderr
+    assert response.code == 0
+    loan = get_entity_by_id(loanid)
+    assert loan.get_property(f"{LENT.name}").value.startswith("20")
+
+    # #### request return #### #
+    response = run_server_side_script(
+        "loan_management/request_return.py",
+        # "pos0",
+        # option1="val1",
+        files={"-p0": save_dict_to_jsonfile(get_return_request_data(loan.id))},
+        **{"auth-token": db.get_connection()._authenticator.auth_token})
+    assert response.stderr is None, response.stderr
+    assert response.code == 0
+    loan = get_entity_by_id(loanid)
+    assert loan.get_property(f"{LOAN_ACCEPTED.name}").value.startswith("20")
+    assert loan.get_property(f"{CONTENT.name}").value == TESTRETURNCOMMENT
+    assert loan.get_property(f"{EXPECTED_RETURN.name}").value == TESTRETURNDATE
+    assert loan.get_property(
+        f"{RETURNLOCATION.name}").value == get_entity_by_name(TESTCURRENTDESTINATION).id
+    assert loan.get_property(f"{RETURN_REQUESTED.name}").value.startswith("20")
+    assert loan.get_property(f"{RETURN_ACCEPTED.name}") is None
+    # TODO test change of borrower
+
+    # accept return
+    data = {}
+    data[F_LOAN] = loanid
+    response = run_server_side_script(
+        "loan_management/accept_return_request.py",
+        # "pos0",
+        # option1="val1",
+        files={"-p0": save_dict_to_jsonfile(data)},
+        **{"auth-token": db.get_connection()._authenticator.auth_token})
+    assert response.stderr is None, response.stderr
+    assert response.code == 0
+    loan = get_entity_by_id(loanid)
+    assert loan.get_property(f"{RETURN_ACCEPTED.name}").value.startswith("20")
+    assert loan.get_property(f"{RETURNED.name}") is None
+
+    # manual return
+    data = {}
+    data[F_LOAN] = loanid
+    response = run_server_side_script(
+        "loan_management/manual_return.py",
+        # "pos0",
+        # option1="val1",
+        files={"-p0": save_dict_to_jsonfile(data)},
+        **{"auth-token": db.get_connection()._authenticator.auth_token})
     assert response.stderr is None
     assert response.code == 0
+    loan = get_entity_by_id(loanid)
+    assert loan.get_property(f"{RETURNED.name}").value.startswith("20")
+
+
+def test_request_loan_multiple_items(loan_form_data_multi):
+
+    # #### request loan #### #
+    data = loan_form_data_multi
+    box_ids = loan_form_data_multi[F_BOX]
+    box_versionids = [get_entity_by_id(eid=bid).get_versionid() for bid in box_ids]
+
+    response = run_server_side_script(
+        "loan_management/request_loan.py",
+        # "pos0",
+        # option1="val1",
+        files={"-p0": save_dict_to_jsonfile(data)},
+        **{"auth-token": db.get_connection()._authenticator.auth_token})
+    assert response.stderr is None, response.stderr
+    assert response.code == 0
     loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
-    assert loan.get_property(f"{LENT.name}").value.startswith("20")
+    loanid = loan.id
+    assert loan.get_property(f"{BOX.name}").value == data[F_BOX]
 
-    ##### request return #####
+    # #### accept loan #### #
     data = {}
     data[F_LOAN] = loan.id
-    # TODO: this is writen in the content field. Change name?
-    data[F_COMMENT] = TESTRETURNCOMMENT
-    # TODO: this is written in RETURNLOCATION. Change name?
-    data[F_CURRENT_LOCATION]=db.utils.get_entity.get_entity_by_name(TESTCURRENTDESTINATION).id
-    data[F_EXPECTED_RETURN_DATE] = TESTRETURNDATE
-    data[F_EMAIL]=TESTBORROWEREMAIL
-    data[F_LAST_NAME]=TESTLN
-    data[F_FIRST_NAME]=TESTFN
-    response = run_server_side_script("loan_management/request_return.py",
-                                    #"pos0",
-                                    #option1="val1",
-                                    files={"-p0": save_dict_to_jsonfile(data)},
-                                    **{"auth-token":db.get_connection()._authenticator.auth_token})
+    response = run_server_side_script(
+        "loan_management/accept_loan_request.py",
+        # "pos0",
+        # option1="val1",
+        files={"-p0": save_dict_to_jsonfile(data)},
+        **{"auth-token": db.get_connection()._authenticator.auth_token})
     assert response.stderr is None, response.stderr
     assert response.code == 0
-    loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
+    loan = get_entity_by_id(loanid)
     assert loan.get_property(f"{LOAN_ACCEPTED.name}").value.startswith("20")
-    assert loan.get_property(f"{CONTENT.name}").value == TESTRETURNCOMMENT
-    assert loan.get_property(f"{EXPECTED_RETURN.name}").value == TESTRETURNDATE
-    assert loan.get_property(f"{RETURNLOCATION.name}").value == db.utils.get_entity.get_entity_by_name(TESTCURRENTDESTINATION).id
+
+    # #### confirm loan #### #
+    data = {}
+    data[F_LOAN] = loan.id
+    response = run_server_side_script(
+        "loan_management/confirm_loan.py",
+        # "pos0",
+        # option1="val1",
+        files={"-p0": save_dict_to_jsonfile(data)},
+        **{"auth-token": db.get_connection()._authenticator.auth_token})
+    assert response.stderr is None, response.stderr
+    assert response.code == 0
+    loan = get_entity_by_id(loanid)
+    assert loan.get_property(BOX_BORROWED).value == box_versionids
+    assert loan.get_property(f"{LENT.name}").value.startswith("20")
+    box1 = get_entity_by_name(TESTBOXNUMBER)
+    box2 = get_entity_by_name(TESTBOXNUMBER2)
+    assert box1.get_property(f"{LOCATION.name}").value == loan.get_property(
+        f"{DESTINATION.name}").value
+    assert box2.get_property(f"{LOCATION.name}").value == loan.get_property(
+        f"{DESTINATION.name}").value
+
+    # #### request return #### #
+    data = get_return_request_data(loan.id)
+    response = run_server_side_script(
+        "loan_management/request_return.py",
+        # "pos0",
+        # option1="val1",
+        files={"-p0": save_dict_to_jsonfile(data)},
+        **{"auth-token": db.get_connection()._authenticator.auth_token})
+    assert response.stderr is None, response.stderr+response.stdout
+    assert response.code == 0
+    loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
     assert loan.get_property(f"{RETURN_REQUESTED.name}").value.startswith("20")
     assert loan.get_property(f"{RETURN_ACCEPTED.name}") is None
     # TODO test change of borrower
@@ -167,26 +311,168 @@ def test_request_loan():
     # accept return
     data = {}
     data[F_LOAN] = loan.id
-    response = run_server_side_script("loan_management/accept_return_request.py",
-                                    #"pos0",
-                                    #option1="val1",
-                                    files={"-p0": save_dict_to_jsonfile(data)},
-                                    **{"auth-token":db.get_connection()._authenticator.auth_token})
+    response = run_server_side_script(
+        "loan_management/accept_return_request.py",
+        # "pos0",
+        # option1="val1",
+        files={"-p0": save_dict_to_jsonfile(data)},
+        **{"auth-token": db.get_connection()._authenticator.auth_token})
     assert response.stderr is None, response.stderr
     assert response.code == 0
     loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
     assert loan.get_property(f"{RETURN_ACCEPTED.name}").value.startswith("20")
     assert loan.get_property(f"{RETURNED.name}") is None
+    box1 = get_entity_by_name(TESTBOXNUMBER)
+    box2 = get_entity_by_name(TESTBOXNUMBER2)
+    assert box1.get_property(f"{LOCATION.name}").value == loan.get_property(
+        f"{RETURNLOCATION.name}").value
+    assert box2.get_property(f"{LOCATION.name}").value == loan.get_property(
+        f"{RETURNLOCATION.name}").value
 
     # manual return
     data = {}
     data[F_LOAN] = loan.id
-    response = run_server_side_script("loan_management/manual_return.py",
-                                    #"pos0",
-                                    #option1="val1",
-                                    files={"-p0": save_dict_to_jsonfile(data)},
-                                    **{"auth-token":db.get_connection()._authenticator.auth_token})
-    assert response.stderr is None
+    response = run_server_side_script(
+        "loan_management/manual_return.py",
+        # "pos0",
+        # option1="val1",
+        files={"-p0": save_dict_to_jsonfile(data)},
+        **{"auth-token": db.get_connection()._authenticator.auth_token})
+    assert response.stderr is None, response.stderr
     assert response.code == 0
     loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
     assert loan.get_property(f"{RETURNED.name}").value.startswith("20")
+
+
+def test_direct_call(loan_form_data_multi):
+    init_data_model(RECORD_TYPES + PROPERTIES)
+    data = loan_form_data_multi
+    test_prop = cached_get_entity_by(name=TESTPROP)
+    box_ids = loan_form_data_multi[F_BOX]
+    box_initial_versionids = [get_entity_by_id(eid=bid).get_versionid() for bid in box_ids]
+    issue_loan_request(data)
+    loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
+    loanid = loan.id
+    assert loan.get_property(f"{BOX.name}").value == data[F_BOX]
+
+    loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
+    data = {}
+    data[F_LOAN] = loan.id
+    accept_loan_request(data)
+    loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
+    assert loan.get_property(f"{LOAN_ACCEPTED.name}").value.startswith("20")
+
+    # make changes to the boxes after the first request and before loan is confirmed
+    for bid in box_ids:
+        box = get_entity_by_id(eid=bid)
+        box.add_property(id=test_prop.id, value='a')
+        box.update()
+    box_before_confirm_versionids = [get_entity_by_id(eid=bid).get_versionid() for bid in box_ids]
+
+    data = {}
+    data[F_LOAN] = loan.id
+    confirm_loan(data)
+    loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
+    assert loan.get_property(BOX_BORROWED).value == box_before_confirm_versionids
+    assert loan.get_property(f"{LENT.name}").value.startswith("20")
+    box1 = get_entity_by_name(TESTBOXNUMBER)
+    box2 = get_entity_by_name(TESTBOXNUMBER2)
+    assert box1.get_property(f"{LOCATION.name}").value == loan.get_property(
+        f"{DESTINATION.name}").value
+    assert box2.get_property(f"{LOCATION.name}").value == loan.get_property(
+        f"{DESTINATION.name}").value
+
+    data = get_return_request_data(loan.id)
+    issue_return_request(data)
+    loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
+    assert loan.get_property(f"{RETURN_REQUESTED.name}").value.startswith("20")
+    assert loan.get_property(f"{RETURN_ACCEPTED.name}") is None
+
+    data = {}
+    data[F_LOAN] = loan.id
+    accept_return_request(data)
+    loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
+    assert loan.get_property(f"{RETURN_ACCEPTED.name}").value.startswith("20")
+    assert loan.get_property(f"{RETURNED.name}") is None
+    box1 = get_entity_by_name(TESTBOXNUMBER)
+    box2 = get_entity_by_name(TESTBOXNUMBER2)
+    assert box1.get_property(f"{LOCATION.name}").value == loan.get_property(
+        f"{RETURNLOCATION.name}").value
+    assert box2.get_property(f"{LOCATION.name}").value == loan.get_property(
+        f"{RETURNLOCATION.name}").value
+
+    # make changes to the boxes before manual return
+    for bid in box_ids:
+        box = get_entity_by_id(eid=bid)
+        box.add_property(id=test_prop.id, value='a')
+        box.update()
+    box_before_return_versionids = [get_entity_by_id(eid=bid).get_versionid() for bid in box_ids]
+
+    data = {}
+    data[F_LOAN] = loan.id
+    manual_return(data)
+    loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
+    assert loan.get_property(f"{RETURNED.name}").value.startswith("20")
+    assert loan.get_property(f"{RETURNED.name}").value.startswith("20")
+    assert loan.get_property(BOX_BORROWED).value == box_before_confirm_versionids
+    assert loan.get_property(BOX_RETURNED).value == box_before_return_versionids
+
+
+def modded_config(config):
+    config = dict(config)
+    print("changing original config")
+    config["Misc"]["entity_loan.no_location_updates"] = "True"
+    return config
+
+
+@patch('linkahead.get_config', new=partial(modded_config, db.get_config()))
+def test_no_location_setting(loan_form_data_multi):
+    init_data_model(RECORD_TYPES + PROPERTIES)
+    data = loan_form_data_multi
+    box_ids = loan_form_data_multi[F_BOX]
+    box_versionids = [get_entity_by_id(eid=bid).get_versionid() for bid in box_ids]
+
+    issue_loan_request(data)
+    loan = db.execute_query(f"FIND loan with {COMMENT.name}='{TESTLOANCOMMENT}'", unique=True)
+    loanid = loan.id
+
+    data = {}
+    data[F_LOAN] = loanid
+    accept_loan_request(data)
+
+    data = {}
+    data[F_LOAN] = loanid
+    confirm_loan(data)
+    loan = get_entity_by_id(loanid)
+    assert loan.get_property(BOX_BORROWED).value == box_versionids
+    assert loan.get_property(f"{LENT.name}").value.startswith("20")
+    box1 = get_entity_by_name(TESTBOXNUMBER)
+    box2 = get_entity_by_name(TESTBOXNUMBER2)
+    assert box1.get_property(f"{LOCATION.name}") is None
+    assert box2.get_property(f"{LOCATION.name}") is None
+
+    data = get_return_request_data(loan.id)
+    issue_return_request(data)
+    loan = get_entity_by_id(loanid)
+    assert loan.get_property(f"{RETURN_REQUESTED.name}").value.startswith("20")
+    assert loan.get_property(f"{RETURN_ACCEPTED.name}") is None
+
+    data = {}
+    data[F_LOAN] = loan.id
+    accept_return_request(data)
+    loan = get_entity_by_id(loanid)
+    assert loan.get_property(f"{RETURN_ACCEPTED.name}").value.startswith("20")
+    box1 = get_entity_by_name(TESTBOXNUMBER)
+    box2 = get_entity_by_name(TESTBOXNUMBER2)
+    assert box1.get_property(f"{LOCATION.name}") is None
+    assert box2.get_property(f"{LOCATION.name}") is None
+
+    data = {}
+    data[F_LOAN] = loan.id
+    manual_return(data)
+    loan = get_entity_by_id(loanid)
+    assert loan.get_property(f"{RETURNED.name}").value.startswith("20")
+    box1 = get_entity_by_name(TESTBOXNUMBER)
+    box2 = get_entity_by_name(TESTBOXNUMBER2)
+    assert box1.get_property(f"{LOCATION.name}") is None
+    assert box2.get_property(f"{LOCATION.name}") is None
diff --git a/loanpy/pyproject.toml b/loanpy/pyproject.toml
index 5379cb7..0e4f069 100644
--- a/loanpy/pyproject.toml
+++ b/loanpy/pyproject.toml
@@ -25,7 +25,7 @@ requires-python = ">= 3.8"
 dependencies = [
    "caosadvancedtools",
    "py3-validate-email",
-   "linkahead"
+   "linkahead@git+https://gitlab.indiscale.com/caosdb/src/caosdb-pylib.git@dev"
 ]
 
 [project.urls]
diff --git a/loanpy/src/loan/accept_loan_request.py b/loanpy/src/loan/accept_loan_request.py
index b1f521a..79feca3 100755
--- a/loanpy/src/loan/accept_loan_request.py
+++ b/loanpy/src/loan/accept_loan_request.py
@@ -21,11 +21,12 @@
 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,update_loan_state,
-                         LOAN_ACCEPTED, S_LOAN_ACCEPTED, S_LOAN_REQUESTED,
-                         get_borrower_names, set_property)
+from caosadvancedtools.serverside.helper import get_timestamp, print_success
+
+from .box_loan import (F_LOAN, LOAN_ACCEPTED, S_LOAN_ACCEPTED, S_LOAN_REQUESTED, get_borrower_names,
+                       get_loan, main, set_property, update_loan_state)
 
 
 def _accept_loan_request(loan):
diff --git a/loanpy/src/loan/accept_return_request.py b/loanpy/src/loan/accept_return_request.py
index 5a2ea7a..22b0fc5 100755
--- a/loanpy/src/loan/accept_return_request.py
+++ b/loanpy/src/loan/accept_return_request.py
@@ -22,30 +22,33 @@ Accept a return request.
 """
 # @review Timm Fitschen 2022-03-16
 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, RETURN_ACCEPTED,update_loan_state,
-                      RETURNLOCATION, S_RETURN_ACCEPTED, S_RETURN_REQUESTED,
-                      get_borrower_names, set_location_of_borrowed_items)
+from caosadvancedtools.serverside.helper import get_timestamp, print_success
+
+from .box_loan import (BOX_BORROWED, CONTENT, F_LOAN, RETURN_ACCEPTED, RETURNLOCATION,
+                       S_RETURN_ACCEPTED, S_RETURN_REQUESTED, get_borrower_names, get_loan, main,
+                       set_location_of_borrowed_items, set_property, update_loan_state)
 
 
-def _accept_return_request(loan):
+def _accept_return_request(loan, location_updates=True, content_updates=True):
     """Update a loan Record and add the `accepted` Property."""
     # This changes the state from "return_requested" to "return_accepted".
     update_loan_state(loan, S_RETURN_ACCEPTED)
 
-    items = set_location_of_borrowed_items(loan, RETURNLOCATION)
-    if loan.get_property(CONTENT) is not None and loan.get_property(CONTENT).value:
-        for item in items:
-            if item.get_property(CONTENT) is not None:
-                item.get_property(CONTENT).value = loan.get_property(CONTENT).value
-            else:
-                item.add_property(id=CONTENT.retrieve().id,
-                                value=loan.get_property(CONTENT).value)
+    if location_updates:
+        items = set_location_of_borrowed_items(loan, RETURNLOCATION)
+        if (content_updates and loan.get_property(CONTENT) is not None and
+                loan.get_property(CONTENT).value):
+            for item in items:
+                if item.get_property(CONTENT) is not None:
+                    item.get_property(CONTENT).value = loan.get_property(CONTENT).value
+                else:
+                    item.add_property(id=CONTENT.id,
+                                      value=loan.get_property(CONTENT).value)
 
-    items.append(loan)
-    items.update()
+        items.update()
+    loan.update()
 
 
 def accept_return_request(data):
@@ -54,9 +57,18 @@ def accept_return_request(data):
     I.e.add the `returnAccepted` Property to the `Loan` Record.
     """
     loan = get_loan(data[F_LOAN])
-    _accept_return_request(loan)
+
+    config = db.get_config()
+    location_updates = (not ("Misc" in config
+                             and "entity_loan.no_location_updates" in config["Misc"]
+                             and config["Misc"]["entity_loan.no_location_updates"]))
+    content_updates = (not ("Misc" in config and "entity_loan.no_content_updates" in config["Misc"]
+                            and config["Misc"]["entity_loan.no_content_updates"]))
+    _accept_return_request(loan, location_updates=location_updates)
     fn, ln = get_borrower_names(loan)
-    box_id = loan.get_property(BOX_BORROWED).value.split("@")[0]
+    box_id = loan.get_property(BOX_BORROWED).value
+    if not isinstance(box_id, list):
+        box_id = [box_id]
 
     print_success('Thank you for accepting the return request by {fn} {ln}.<br>'
                   'If necessary, location and content of the box have been '
@@ -64,7 +76,9 @@ def accept_return_request(data):
                   '<a href="/Entity/{bid}" title="Reload this page.">Reload</a> '
                   'to view the new Location.<br>'
                   'See <a href="{loan}" title="Go to the accepted loan '
-                  'request.">here</a>'.format(fn=fn, ln=ln, loan=loan.id, bid=box_id))
+                  'request.">here</a>'.format(
+                      fn=fn, ln=ln, loan=loan.id,
+                      bid="&".join([str(el) for el in box_id])))
     return 0
 
 
diff --git a/loanpy/src/loan/box_loan.py b/loanpy/src/loan/box_loan.py
index fea7172..f810c49 100644
--- a/loanpy/src/loan/box_loan.py
+++ b/loanpy/src/loan/box_loan.py
@@ -27,19 +27,22 @@ import sys
 import traceback
 from datetime import datetime
 
-import caosdb as db
-from caosadvancedtools.serverside.helper import (DataModelError, get_data,
-                                                 init_data_model,get_timestamp,
-                                                 parse_arguments, print_error,
-                                                 print_info, print_warning,
-                                                 send_mail)
+import linkahead as db
+from caosadvancedtools.serverside.helper import (DataModelError, get_data, get_timestamp,
+                                                 init_data_model, parse_arguments, print_error,
+                                                 print_info, print_warning, send_mail)
 from caosadvancedtools.serverside.logging import configure_server_side_logging
+from linkahead.cached import cached_get_entity_by
+from linkahead.common.models import get_id_from_versionid, value_matches_versionid
 from linkahead.exceptions import EmptyUniqueQueryError
+from linkahead.utils.get_entity import get_entity_by_id
 from validate_email import validate_email
+
 from .conf import *
 
 LOGGER_NAME = "box_loan"
 LOGGER = logging.getLogger(LOGGER_NAME)
+LOGGER.addHandler(logging.StreamHandler(stream=sys.stdout))
 
 # Form names
 F_BOX = "box"
@@ -147,8 +150,8 @@ class StateError(RuntimeError):
             "This transition of the loan state is not possible. The allowed target "
             "state is {one}'{expected}' "
             "while the actual target state is '{actual}'.".format(one=one,
-                                                           actual=actual,
-                                                           expected=_expected))
+                                                                  actual=actual,
+                                                                  expected=_expected))
 
 
 class EmailPatternError(RuntimeError):
@@ -192,52 +195,38 @@ def assert_date_in_future(actual, msg=None):
         raise DataError(_msg)
 
 
-def get_record_by_id(parent, entity_id):
-    """ Retrieve a record with a particular parent by id. """
-
-    return db.execute_query("FIND RECORD {} WITH ID = {}".format(parent,
-                                                                 entity_id),
-                            unique=True)
-
-
-def get_box(box):
-    """ Retrieve a box record by id. """
-
-    return get_record_by_id(BOX.name, box)
-
-
 def get_loan(loan_id):
     """ Retrieve a loan record by id. """
 
-    return get_record_by_id(LOAN.name, loan_id)
+    return get_entity_by_id(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:
+    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:
+    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:
+    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:
+    elif old_state == S_RETURN_REQUESTED:
+        if new_state == S_RETURN_ACCEPTED:
             loan.add_property(RETURN_ACCEPTED, get_timestamp())
-        elif new_state==S_LENT:
+        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:
+    elif old_state == S_RETURN_ACCEPTED:
+        if new_state == S_RETURNED:
             loan.add_property(RETURNED, get_timestamp())
         else:
             raise StateError(new_state, [S_RETURNED])
@@ -297,7 +286,7 @@ def get_borrower_names(loan):
         fn = "an"
         ln = "unknown borrower"
     else:
-        borrower = db.Record(id=borrower_property.value).retrieve()
+        borrower: db.Record = db.Record(id=borrower_property.value).retrieve()
         fn = borrower.get_property(FIRST_NAME.name).value
         ln = borrower.get_property(LAST_NAME.name).value
 
@@ -360,27 +349,39 @@ def get_external_server_uri():
     raise RuntimeError("Could not determine the external server uri")
 
 
+def _create_items_description_list(item_ids):
+    description = ""
+    for iid in item_ids:
+        item = get_entity_by_id(iid)
+        description += (f"{item.parents[0].name}: "
+                        f"{get_property_value(item, BOX_NUMBER, 'UNKNOWN')}"
+                        f" - {iid}\n")
+
+    return description
+
+
 def send_loan_request_mail(data, borrower, loan):
     try:
-        boxid = str(loan.get_property(BOX).value).split("@")[0]
-        box = get_box(boxid)
-        boxnumber = get_property_value(box, BOX_NUMBER, "UNKNOWN")
-
-        link = "{uri}Entity/{boxid}&{loanid}".format(
+        item_ids = loan.get_property(BOX).value
+        if not isinstance(item_ids, list):
+            item_ids = [item_ids]
+        description = _create_items_description_list(item_ids)
+        link = "{uri}Entity/{item_ids}&{loanid}".format(
             uri=get_external_server_uri(),
-            boxid=boxid,
+            item_ids="&".join([str(i) for i in item_ids]),
             loanid=loan.id)
 
         body = """Dear Curator,
 
-    a new loan has been request by {borrower} for box number {boxnumber}.
+    a new loan has been request by {borrower} for the following items:
+        {items}.
 
     Loan request:
         {data}
 
     View box and loan records:
         {link}
-    """.format(boxnumber=boxnumber, borrower=get_requester_string(borrower),
+    """.format(items=description, borrower=get_requester_string(borrower),
                data=data, link=link)
 
         send_mail(
@@ -389,30 +390,34 @@ def send_loan_request_mail(data, borrower, loan):
             subject="loan request",
             body=body)
         print_info("An email has been sent to the responsible box curator.")
-    except BaseException as e:
+    except Exception as e:
         print_error("""Sending an email to the responsible box curator failed
 
 for an unknown reason. Please inform the box curator by yourself about your
 loan request and that the email sending failed. Thank you very much for your
-help.""")
+help."""+str(e))
         LOGGER.error(e)
 
 
 def send_return_request_mail(data, returner, loan):
     try:
-        boxid = str(loan.get_property(BOX).value).split("@")[0]
-        box = get_box(boxid)
-        boxnumber = get_property_value(box, BOX_NUMBER, "UNKNOWN")
+        item_ids = loan.get_property(BOX).value
+        if not isinstance(item_ids, list):
+            item_ids = [item_ids]
+        description = _create_items_description_list(item_ids)
         current_location = data[F_CURRENT_LOCATION]
 
-        link = "{uri}Entity/{boxid}&{loanid}".format(
+        link = "{uri}Entity/{item_ids}&{loanid}".format(
             uri=get_external_server_uri(),
-            boxid=boxid,
+            item_ids="&".join([str(i) for i in item_ids]),
             loanid=loan.id)
 
         body = """Dear Curator,
 
-    a new return request by {returner} for box number {boxnumber} is pending.
+    a new return request by {returner} for the following items is pending:
+    {items}
+
+
     The current location of the box according to the return request is
     "{current_location}".
 
@@ -421,7 +426,7 @@ def send_return_request_mail(data, returner, loan):
 
     View box and loan records:
         {link}
-    """.format(boxnumber=boxnumber, returner=get_requester_string(returner),
+    """.format(items=description, returner=get_requester_string(returner),
                data=data, link=link, current_location=current_location)
 
         send_mail(
@@ -430,13 +435,14 @@ def send_return_request_mail(data, returner, loan):
             subject="return request",
             body=body)
         print_info("An email has been sent to the responsible box curator.")
-    except BaseException as e:
+    except Exception as e:
+        tb = traceback.format_exc()
         print_error("""Sending an email to the responsible box curator failed
 
 for an unknown reason. Please inform the box curator by yourself about this
 
 return request and that the email sending failed. Thank you very much for your
-help.""")
+help."""+str(e)+str(tb))
         LOGGER.error(e)
 
 
@@ -459,15 +465,13 @@ def query_person(firstname, lastname, email):
     2. or the first name and the last name
     """
 
-    query=(f'FIND RECORD {PERSON.name} '
+    query = (f'FIND RECORD {PERSON.name} '
              f'WITH {EMAIL.name} = "{email}" '
              f'OR ({FIRST_NAME.name} = "{firstname}" AND {LAST_NAME.name} = "{lastname}") '
              )
     return db.execute_query(query, unique=True)
 
 
-
-
 def _update_person(person, firstname, lastname, email):
     """Update the persons names and email address.
 
@@ -548,7 +552,8 @@ def _caller(func, args):
 
     return func(data)
 
-def get_borrowed_items_from(loan)-> db.Container:
+
+def get_borrowed_items_from(loan) -> db.Container:
     """Retrieves and returns the Records of items that were borrowed
 
     Paramters
@@ -569,10 +574,12 @@ def get_borrowed_items_from(loan)-> db.Container:
         borrowed = [borrowed.value]
     else:
         borrowed = borrowed.value
-    recs = db.Container().extend([db.Record(id=rec_id.split("@")[0]) for rec_id in borrowed])
+    recs = db.Container().extend([
+        db.Record(id=get_id_from_versionid(rec_id)) for rec_id in borrowed])
     return recs.retrieve()
 
-def set_location_of_borrowed_items(loan, kind)-> list(db.Record):
+
+def set_location_of_borrowed_items(loan, kind) -> list(db.Record):
     """Retrieve Records of borrowed items, sets the location and returns the container
 
     Parameters
@@ -581,12 +588,38 @@ def set_location_of_borrowed_items(loan, kind)-> list(db.Record):
     """
     if loan.get_property(kind) is None:
         raise RuntimeError(
-            f"Cannot set {kind} because the loan does not have one. Loan ID: {laon.id}")
+            f"Cannot set {kind} because the loan does not have one. Loan ID: {loan.id}")
     borrowed = get_borrowed_items_from(loan)
     for item in borrowed:
         set_property(item, LOCATION, loan.get_property(kind).value)
     return borrowed
 
+
+def set_references_to_current_version(value):
+    """
+    creates a reference value from a given value by replacing all references with such that point
+    to the current version of the respecive entity.
+
+    If the value is a list, then this is done for all references in the list and thus a list is
+    returned.
+
+    Returns
+    -------
+    Union[list, str]: the new value
+    """
+    islist = isinstance(value, list)
+    if not islist:
+        value = [value]
+
+    b_items = []
+    for item in value:
+        bid = get_id_from_versionid(item) if value_matches_versionid(item) else item
+        b_items.append(str(bid)+"@HEAD")
+    if not islist:
+        b_items = b_items[0]
+    return b_items
+
+
 def main(func, args=None):
     # configure_server_side_logging(LOGGER_NAME)
     try:
@@ -601,11 +634,11 @@ def main(func, args=None):
         print_error(e.args[0])
         code = 4
         LOGGER.debug(e)
-    except BaseException as e:  # pylint: disable=W0703
+    except Exception as e:  # pylint: disable=W0703
         tb = traceback.format_exc()
         print_error("An unknown error occured."
                     "<br>{}<code>{}</code>".format(str(e), tb))
         code = 1
-        LOGGER.debug("Unknown error!!!", e)
+        LOGGER.debug("Unknown error!!!")
     LOGGER.info("exiting with code {}".format(code))
     sys.exit(code)
diff --git a/loanpy/src/loan/conf.py b/loanpy/src/loan/conf.py
index a2eeb2e..16d4bc7 100644
--- a/loanpy/src/loan/conf.py
+++ b/loanpy/src/loan/conf.py
@@ -1,4 +1,5 @@
 import linkahead as db
+
 # RecordTypes
 BOX = db.RecordType(name="Box")
 PERSON = db.RecordType(name="Person")
diff --git a/loanpy/src/loan/confirm_loan.py b/loanpy/src/loan/confirm_loan.py
index d2c69b0..a435b45 100755
--- a/loanpy/src/loan/confirm_loan.py
+++ b/loanpy/src/loan/confirm_loan.py
@@ -22,29 +22,30 @@ Confirm a loan.
 """
 from __future__ import absolute_import
 
-import caosdb as db
+import linkahead 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, get_borrower_names,update_loan_state,
-                       get_loan, main, set_location_of_borrowed_items, set_property)
+from .box_loan import (BOX, BOX_BORROWED, DESTINATION, F_LOAN, LENT, S_LENT, S_LOAN_ACCEPTED,
+                       get_borrower_names, get_loan, main, set_location_of_borrowed_items,
+                       set_property, set_references_to_current_version, update_loan_state)
 
 
 def _set_lent_box(loan):
-    """Set the box version to HEAD.
-
-    This stores the version of the box when it was delivered to the borrower.
-    """
-    # TODO: Adapt datamodel and remove name override
-    box_prop = loan.get_property(BOX)
-    box_prop.name = BOX_BORROWED
-    box_prop.value = str(box_prop.value) + "@HEAD"
+    """ Store the exact versions of borrowed items when it was delivered to the borrower. """
+    references = set_references_to_current_version(loan.get_property(BOX).value)
+    loan.add_property(id=BOX.id, name=BOX_BORROWED, value=references,
+                      datatype=db.LIST(BOX.name) if isinstance(references, list) else BOX.name)
+    loan.update()
 
 
 def set_loan_location(loan):
-    """Set the location of the box to the return location of the loan.  """
-    items = set_location_of_borrowed_items(loan, DESTINATION)
-    items.update()
+    """Set the location of the box to the return location of the loan if configured."""
+
+    config = db.get_config()
+    if not ("Misc" in config and "entity_loan.no_location_updates" in config["Misc"]
+            and config["Misc"]["entity_loan.no_location_updates"]):
+        items = set_location_of_borrowed_items(loan, DESTINATION)
+        items.update()
 
 
 def _confirm_loan(data):
@@ -58,10 +59,6 @@ def _confirm_loan(data):
     # updates the box location
     set_loan_location(loan)
 
-    db.Container().extend([
-        loan
-    ]).update()
-
     return loan
 
 
@@ -73,7 +70,10 @@ def confirm_loan(data):
     loan = _confirm_loan(data)
     fn, ln = get_borrower_names(loan)
     loan = get_loan(data[F_LOAN])
-    box_id = loan.get_property(BOX_BORROWED).value.split("@")[0]
+    box_id = loan.get_property(BOX_BORROWED).value
+
+    if not isinstance(box_id, list):
+        box_id = [box_id]
     print_success('Thank you for confirming the loan request by {fn} {ln}.<br>'
                   'The Location of the Box was updated. '
                   '<a href="/Entity/{bid}" title="Reload this page.">Reload</a> '
@@ -81,7 +81,7 @@ def confirm_loan(data):
                   'You can also checkout the new loan state '
                   '<a href="{loan}" title="Go to the confirmed loan '
                   'record.">here</a>'.format(fn=fn, ln=ln, loan=loan.id,
-                                             bid=box_id))
+                                             bid="&".join([str(el) for el in box_id])))
 
     return 0
 
diff --git a/loanpy/src/loan/manual_return.py b/loanpy/src/loan/manual_return.py
index 1fe79b1..7e04723 100755
--- a/loanpy/src/loan/manual_return.py
+++ b/loanpy/src/loan/manual_return.py
@@ -25,10 +25,10 @@ from __future__ import absolute_import
 import caosdb as db
 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,
-                       get_borrower_names, get_loan, main,update_loan_state,
-                       set_location_of_borrowed_items, set_property)
+from .box_loan import (BOX, BOX_BORROWED, BOX_RETURNED, CONTENT, F_LOAN, RETURNED, RETURNLOCATION,
+                       S_RETURN_ACCEPTED, S_RETURNED, get_borrower_names, get_loan, main,
+                       set_location_of_borrowed_items, set_property,
+                       set_references_to_current_version, update_loan_state)
 
 
 def _set_returned_box(loan):
@@ -36,25 +36,34 @@ def _set_returned_box(loan):
 
     This stores the version of the box that was returned by a borrower.
     """
-    box_id = loan.get_property(BOX_BORROWED).value.split("@")[0]
-    # TODO: Adapt datamodel and remove name override
-    loan.add_property(property=BOX,
-                      name=BOX_RETURNED,
-                      value=box_id + "@HEAD")
+    references = set_references_to_current_version(loan.get_property(BOX_BORROWED).value)
+    loan.add_property(id=BOX.id, name=BOX_RETURNED, value=references,
+                      datatype=db.LIST(BOX.name) if isinstance(references, list) else BOX.name)
 
 
 def set_return_location(loan):
     """Set the location of the box to the return location of the loan.  """
-    items = set_location_of_borrowed_items(loan, RETURNLOCATION)
-    items.update()
+    config = db.get_config()
+    if (not ("Misc" in config and "entity_loan.no_location_updates" in config["Misc"]
+             and config["Misc"]["entity_loan.no_location_updates"])):
+        items = set_location_of_borrowed_items(loan, RETURNLOCATION)
+        items.update()
 
 
 def set_content(loan):
     """Set the content to the box to the one given in the loan.  """
-    box_id = loan.get_property(BOX_BORROWED).value.split("@")[0]
-    box = db.Record(id=box_id).retrieve()
-    set_property(box, CONTENT, loan.get_property(CONTENT).value)
-    box.update()
+    config = db.get_config()
+    if ("Misc" in config
+            and "entity_loan.no_content_updates" in config["Misc"]
+            and config["Misc"]["entity_loan.no_content_updates"]):
+        return
+    items = loan.get_property(BOX_BORROWED).value
+    if not isinstance(items, list):
+        items = [items]
+    for item in items:
+        box = db.Record(id=item).retrieve()
+        set_property(box, CONTENT, loan.get_property(CONTENT).value)
+        box.update()
 
 
 def _manual_return(data):
@@ -87,7 +96,7 @@ def manual_return(data):
     loan = _manual_return(data)
     fn, ln = get_borrower_names(loan)
     loan = get_loan(data[F_LOAN])
-    box_id = loan.get_property(BOX_BORROWED).value.split("@")[0]
+    box_id = loan.get_property(BOX_BORROWED)
 
     print_success('The box borrowed by {fn} {ln} '.format(fn=fn, ln=ln)
                   + 'has been returned and the Box Location has been updated.'
diff --git a/loanpy/src/loan/reject_return_request.py b/loanpy/src/loan/reject_return_request.py
index c381f18..fcbc365 100755
--- a/loanpy/src/loan/reject_return_request.py
+++ b/loanpy/src/loan/reject_return_request.py
@@ -21,11 +21,12 @@
 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,update_loan_state,
-                         S_RETURN_REQUESTED, RETURN_REQUESTED, S_LENT,
-                         get_borrower_names)
+
+from .box_loan import (F_LOAN, RETURN_REQUESTED, S_LENT, S_RETURN_REQUESTED, get_borrower_names,
+                       get_loan, main, update_loan_state)
 
 
 def _reject_return_request(loan):
diff --git a/loanpy/src/loan/request_loan.py b/loanpy/src/loan/request_loan.py
index 723f569..5cb51f2 100755
--- a/loanpy/src/loan/request_loan.py
+++ b/loanpy/src/loan/request_loan.py
@@ -25,13 +25,11 @@ from __future__ import absolute_import
 import linkahead as db
 from caosadvancedtools.serverside.helper import get_timestamp, print_success
 
-from .box_loan import (BORROWER, BOX, COMMENT, DESTINATION, EXHAUST_CONTENTS,
-                      EXPECTED_RETURN, F_BOX, F_COMMENT, F_DESTINATION,
-                      F_EMAIL, F_EXHAUST_CONTENTS, F_EXPECTED_RETURN_DATE,
-                      F_FIRST_NAME, F_LAST_NAME, FIRST_NAME, LAST_NAME, LOAN,
-                      LOAN_REQUESTED, assert_date_in_future,
-                      assert_key_in_data, get_box, insert_or_update_person, main,
-                      send_loan_request_mail)
+from .box_loan import (BORROWER, BOX, COMMENT, DESTINATION, EXHAUST_CONTENTS, EXPECTED_RETURN,
+                       F_BOX, F_COMMENT, F_DESTINATION, F_EMAIL, F_EXHAUST_CONTENTS,
+                       F_EXPECTED_RETURN_DATE, F_FIRST_NAME, F_LAST_NAME, FIRST_NAME, LAST_NAME,
+                       LOAN, LOAN_REQUESTED, assert_date_in_future, assert_key_in_data,
+                       insert_or_update_person, main, send_loan_request_mail)
 
 
 def create_loan(box, borrower, expected_return, exhaust_contents, comment,
@@ -40,7 +38,7 @@ def create_loan(box, borrower, expected_return, exhaust_contents, comment,
 
     loan = db.Record().add_parent(LOAN)
 
-    loan.add_property(BOX, box)
+    loan.add_property(BOX, box, datatype=(db.LIST(BOX.name) if isinstance(box, list) else BOX.name))
     loan.add_property(BORROWER, borrower)
     loan.add_property(EXPECTED_RETURN, expected_return)
     loan.add_property(EXHAUST_CONTENTS, exhaust_contents)
@@ -62,27 +60,32 @@ _OBLIGATORY = [
 ]
 
 
-def _check_data(data):
+def _set_defaults(data):
     if F_EXHAUST_CONTENTS not in data:
         data[F_EXHAUST_CONTENTS] = False
+    return data
+
 
+def _check_data(data):
     for field in _OBLIGATORY:
         assert_key_in_data(data, field)
+    if not (isinstance(data[F_BOX], int) or isinstance(data[F_BOX], list)):
+        raise ValueError(
+            f"the value of {F_BOX} has the wrong type. It should be int or list of ints")
     assert_date_in_future(
         data[F_EXPECTED_RETURN_DATE],
         ("The expected return date needs to be in the future."
          " You submitted '{}'.").format(
             data[F_EXPECTED_RETURN_DATE]))
 
-    return data
-
 
 def _issue_loan_request(data):
     """ Insert a loan record a insert/update a person record. """
-    data = _check_data(data)
+    data = _set_defaults(data)
+    _check_data(data)
     borrower = insert_or_update_person(firstname=data[F_FIRST_NAME],
-                          lastname=data[F_LAST_NAME],
-                          email=data[F_EMAIL])
+                                       lastname=data[F_LAST_NAME],
+                                       email=data[F_EMAIL])
     loan = create_loan(box=data[F_BOX],
                        borrower=borrower,
                        expected_return=data[F_EXPECTED_RETURN_DATE],
@@ -117,7 +120,7 @@ def issue_loan_request(data):
                   '<a href="{loan}" title="Go to the newly created loan '
                   'request.">here</a>'.format(fn=fn, ln=ln, loan=loan.id))
 
-    send_loan_request_mail(data, borrower, loan)
+    # send_loan_request_mail(data, borrower, loan)
 
     return 0
 
diff --git a/loanpy/src/loan/request_return.py b/loanpy/src/loan/request_return.py
index 8cd8516..4142c43 100755
--- a/loanpy/src/loan/request_return.py
+++ b/loanpy/src/loan/request_return.py
@@ -29,12 +29,13 @@ from __future__ import absolute_import
 from caosadvancedtools.serverside.helper import get_timestamp, print_success
 
 from .box_loan import (BORROWER, COMMENT, CONTENT, EXPECTED_RETURN, F_COMMENT,
-                      F_CURRENT_LOCATION, F_EMAIL, F_EXPECTED_RETURN_DATE,
-                      F_FIRST_NAME, F_LAST_NAME, F_LOAN, FIRST_NAME, LAST_NAME,update_loan_state,
-                      RETURN_REQUESTED, RETURNLOCATION, S_LENT, S_RETURN_REQUESTED,
-                      assert_date_in_future, assert_key_in_data,
-                      get_loan, insert_or_update_person, main,
-                      send_return_request_mail, set_property)
+                       F_CURRENT_LOCATION, F_EMAIL, F_EXPECTED_RETURN_DATE,
+                       F_FIRST_NAME, F_LAST_NAME, F_LOAN, FIRST_NAME,
+                       LAST_NAME, RETURN_REQUESTED, RETURNLOCATION, S_LENT,
+                       S_RETURN_REQUESTED, assert_date_in_future,
+                       assert_key_in_data, get_loan, insert_or_update_person,
+                       main, send_return_request_mail, set_property,
+                       update_loan_state)
 
 
 def _check_data(data):
diff --git a/loanpy/unittests/test_box_loan.py b/loanpy/unittests/test_box_loan.py
index ebeccb7..78820ae 100644
--- a/loanpy/unittests/test_box_loan.py
+++ b/loanpy/unittests/test_box_loan.py
@@ -17,20 +17,20 @@
 #  along with this program. If not, see <https://www.gnu.org/licenses/>.
 
 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 linkahead import configure_connection, get_connection
+from linkahead.connection.mockup import MockUpResponse, MockUpServerConnection
 from loan.box_loan import *
 from loan.box_loan import _caller
+from pytest import raises
 
 from utils import get_form_data_example
 
 
-
 def test_return_value_of_caller():
     tval = 1337
-    assert _caller(lambda d: tval, [get_form_data_example()]
-) == tval
+    assert _caller(lambda d: tval, [get_form_data_example()]) == tval
+
 
 def test_create_person():
     p = create_person("anna", "lytik", "a@b.com")
@@ -39,6 +39,7 @@ def test_create_person():
     assert p.get_property(LAST_NAME.name).value == "lytik"
     assert p.get_property(EMAIL.name).value == "a@b.com"
 
+
 def test_email_validation():
     with raises(EmailPatternError):
         assert_email_pattern("@asdf")
@@ -53,6 +54,7 @@ def test_email_validation():
     assert_email_pattern("a@b.da")
     assert_email_pattern("\"#\"@รถ.de")
 
+
 def test_assert_date_in_future():
     # I can't wait for 2050 :-)
     assert assert_date_in_future("2050-01-01") is None
diff --git a/loanpy/unittests/test_request_loan.py b/loanpy/unittests/test_request_loan.py
index 63c9375..8b41f30 100644
--- a/loanpy/unittests/test_request_loan.py
+++ b/loanpy/unittests/test_request_loan.py
@@ -1,13 +1,14 @@
 from os.path import abspath, dirname, join
-from pytest import raises
-from linkahead import get_connection, configure_connection, Record
-from linkahead.connection.mockup import (MockUpServerConnection, MockUpResponse)
+
 from caosadvancedtools.serverside.helper import get_data
-from loan.box_loan import (PERSON, FIRST_NAME, EMAIL, BOX, BORROWER,
-                         EXPECTED_RETURN, EXHAUST_CONTENTS, COMMENT,
-                         DESTINATION, F_FIRST_NAME, F_EMAIL,
-                         F_EXPECTED_RETURN_DATE, DataError, BOX_NUMBER)
-from loan.request_loan import (create_loan, _issue_loan_request)
+from linkahead import Record, configure_connection, get_connection
+from linkahead.connection.mockup import MockUpResponse, MockUpServerConnection
+from loan.box_loan import (BORROWER, BOX, BOX_NUMBER, COMMENT, DESTINATION, EMAIL, EXHAUST_CONTENTS,
+                           EXPECTED_RETURN, F_EMAIL, F_EXPECTED_RETURN_DATE, F_FIRST_NAME,
+                           FIRST_NAME, PERSON, DataError)
+from loan.request_loan import _issue_loan_request, create_loan
+from pytest import raises
+
 from utils import get_form_data_example
 
 
@@ -20,15 +21,12 @@ def test_issue_loan_request_with_wrong_return_date():
 
 def test_create_loan():
     borrower = Record(name="Person1")
-    l = create_loan(1234,
-                    borrower,
-                    "2020-03-23",
-                    False,
-                    "blablabla",
-                    "my office")
-    assert l.get_property(BOX.name).value == 1234
-    assert l.get_property(BORROWER.name).value == borrower
-    assert l.get_property(EXPECTED_RETURN.name).value == "2020-03-23"
-    assert l.get_property(EXHAUST_CONTENTS.name).value == False
-    assert l.get_property(COMMENT.name).value == "blablabla"
-    assert l.get_property(DESTINATION.name).value == "my office"
+    loan = create_loan(1234, borrower, "2020-03-23", False, "blablabla", "my office")
+    assert loan.get_property(BOX.name).value == 1234
+    assert loan.get_property(BORROWER.name).value == borrower
+    assert loan.get_property(EXPECTED_RETURN.name).value == "2020-03-23"
+    assert loan.get_property(EXHAUST_CONTENTS.name).value is False
+    assert loan.get_property(COMMENT.name).value == "blablabla"
+    assert loan.get_property(DESTINATION.name).value == "my office"
+
+    create_loan(1234, borrower, "2020-03-23", False, "blablabla", "my office")
diff --git a/loanpy/unittests/test_request_return.py b/loanpy/unittests/test_request_return.py
index 8f6bd3b..1480596 100644
--- a/loanpy/unittests/test_request_return.py
+++ b/loanpy/unittests/test_request_return.py
@@ -1,14 +1,14 @@
 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 caosadvancedtools.serverside.helper import get_data
-from loan.box_loan import (FIRST_NAME, EMAIL, LAST_NAME, F_FIRST_NAME,
-                         F_LAST_NAME, F_EMAIL, EXPECTED_RETURN, LENT,
-                         RETURN_REQUESTED, PERSON, BORROWER, BOX,
-                         LOAN_REQUESTED, StateError, F_EXPECTED_RETURN_DATE,
-                         DataError, LOAN)
-from loan.request_return import get_loan, _issue_return_request
+from linkahead import configure_connection, get_connection
+from linkahead.connection.mockup import MockUpResponse, MockUpServerConnection
+from loan.box_loan import (BORROWER, BOX, EMAIL, EXPECTED_RETURN, F_EMAIL, F_EXPECTED_RETURN_DATE,
+                           F_FIRST_NAME, F_LAST_NAME, FIRST_NAME, LAST_NAME, LENT, LOAN,
+                           LOAN_REQUESTED, PERSON, RETURN_REQUESTED, DataError, StateError)
+from loan.request_return import _issue_return_request, get_loan
+from pytest import raises
+
 from utils import get_form_data_example
 
 
diff --git a/loanpy/unittests/utils.py b/loanpy/unittests/utils.py
index c569757..bebb192 100644
--- a/loanpy/unittests/utils.py
+++ b/loanpy/unittests/utils.py
@@ -22,5 +22,6 @@ some utility functions
 
 from os.path import abspath, dirname, join
 
+
 def get_form_data_example():
     return abspath(join(dirname(__file__), "request_loan_form.json"))
diff --git a/test-profile/custom/caosdb-server/scripting/home/.pylinkahead.ini b/test-profile/custom/caosdb-server/scripting/home/.pylinkahead.ini
index c909924..0ede554 100644
--- a/test-profile/custom/caosdb-server/scripting/home/.pylinkahead.ini
+++ b/test-profile/custom/caosdb-server/scripting/home/.pylinkahead.ini
@@ -8,5 +8,6 @@ timeout = 5000
 sendmail=/usr/local/bin/sendmail_to_file
 entity_loan.curator_mail_from=admin@indiscale.com
 entity_loan.curator_mail_to=admin@indiscale.com
+entity_loan.update_locations=True
 [sss_helper]
 external_uri = https://example.com:443
diff --git a/test-profile/custom/other/restore/caosdb.2024-10-24T12:10:03.770053767+00:00.dump.sql b/test-profile/custom/other/restore/caosdb.2024-10-24T12:10:03.770053767+00:00.dump.sql
index f4a6848..7c547b0 100644
--- a/test-profile/custom/other/restore/caosdb.2024-10-24T12:10:03.770053767+00:00.dump.sql
+++ b/test-profile/custom/other/restore/caosdb.2024-10-24T12:10:03.770053767+00:00.dump.sql
@@ -1758,7 +1758,7 @@ DELIMITER ;
 /*!50003 SET collation_connection  = utf8_general_ci */ ;
 DELIMITER ;;
 CREATE DEFINER=`root`@`%` FUNCTION `get_head_relative`(EntityID VARCHAR(255),
-    Offset INT UNSIGNED) RETURNS varbinary(255)
+    HeadOffset INT UNSIGNED) RETURNS varbinary(255)
     READS SQL DATA
 BEGIN
     DECLARE InternalEntityID INT UNSIGNED DEFAULT NULL;
@@ -1774,7 +1774,7 @@ BEGIN
             FROM entity_version AS e
             WHERE e.entity_id = InternalEntityID
             ORDER BY e._iversion DESC
-            LIMIT 1 OFFSET Offset
+            LIMIT 1 OFFSET HeadOffset
         );
 END ;;
 DELIMITER ;
diff --git a/test-profile/profile.yml b/test-profile/profile.yml
index 9120f1a..2d96f7f 100644
--- a/test-profile/profile.yml
+++ b/test-profile/profile.yml
@@ -203,4 +203,6 @@ default:
         mode: "copy"
         path: "../loanpy"
         package: "loanpy"
-
+      linkahead:
+        mode: "pip"
+        package: "git+https://gitlab.indiscale.com/caosdb/src/caosdb-pylib.git@dev"
-- 
GitLab