diff --git a/warehouse/__init__.py b/warehouse/__init__.py
index 6924b54a477fca8175b014e981aa9198b8a8ff3b..9e5208731d5314cc1e00e5c71007a71f508372d7 100644
--- a/warehouse/__init__.py
+++ b/warehouse/__init__.py
@@ -111,25 +111,32 @@ def logout():
 @app.route('/items/')
 def item_list():
 	query = Item.query
-	if 'search' in request.values:
-		item = Item.query.get(request.values['search'].strip().upper())
+	search = request.values.get('search', '').strip()
+	if search:
+		item = Item.query.get(search.upper())
 		if item:
 			return redirect(url_for('item_view', item_id=item.id))
-		item = Item.query.filter_by(qr_code=request.values['search'].upper().strip().split('/C/')[-1]).first()
+		item = Item.query.filter_by(qr_code=search.upper().split('/C/')[-1]).first()
 		if item:
 			return redirect(url_for('item_view', item_id=item.id))
-		keywords = request.values['search'].strip().split()
+		keywords = search.split()
 		for keyword in keywords:
 			query = query.filter(db.or_(Item.name.contains(keyword), Item.description.contains(keyword)))
+	else:
+		query = query.filter(Item.parent == None)
 	return render_template('item/list.html', page=query.order_by('name').paginate(per_page=10))
 
 @app.route('/items/add', methods=['GET', 'POST'])
 def item_add():
+	parent = None
+	if 'parent' in request.values:
+		parent = Item.query.get_or_404(request.values['parent'])
 	if request.method == 'GET':
-		return render_template('item/add.html')
+		return render_template('item/add.html', parent=parent)
 	item = Item(
 		name=request.form['name'],
-		description=request.form.get('description', '')
+		description=request.form.get('description', ''),
+		parent=parent
 	)
 	db.session.add(item)
 	db.session.commit()
@@ -218,21 +225,59 @@ def item_delete_photo(item_id, photo_id):
 @app.route('/item/<item_id>/locate', methods=['GET', 'POST'])
 def item_locate(item_id):
 	item = Item.query.get_or_404(item_id)
-	if request.method == 'GET':
-		query = Location.query
-		if 'search' in request.values:
-			keywords = request.values['search'].strip().split()
-			for keyword in keywords:
-				query = query.filter(db.or_(Location.name.contains(keyword), Location.description.contains(keyword)))
-		locations = query.all()
-		def include_parents_r(location):
-			if location.parent and location.parent not in locations:
-				locations.append(location.parent)
-				include_parents_r(location.parent)
-		for location in list(locations):
-			include_parents_r(location)
-		return render_template('item/locate.html', item=item, locations=locations)
-	item.location = Location.query.get(request.form['location_id'])
+
+	instant_item_match = None
+	if request.method == 'GET' and 'search' in request.values:
+		# Try instant match
+		items = Item.query.filter(db.or_(
+			Item.id == request.values['search'].strip().upper(),
+			Item.qr_code == request.values['search'].upper().strip().split('/C/')[-1]
+		)).all()
+		if len(items) == 1:
+			instant_item_match = items[0]
+
+	if request.method == 'GET' and not instant_item_match:
+		if request.values.get('tab') == 'items':
+			query = Item.query.filter(Item.id != item.id)
+			if 'search' in request.values:
+				keywords = request.values['search'].strip().split()
+				for keyword in keywords:
+					query = query.filter(db.or_(Item.name.contains(keyword), Item.description.contains(keyword)))
+			page = query.order_by('name').paginate(per_page=10)
+			return render_template('item/locate.html', item=item, page=page, tab='items')
+		else:
+			query = Location.query
+			if 'search' in request.values:
+				keywords = request.values['search'].strip().split()
+				for keyword in keywords:
+					query = query.filter(db.or_(Location.name.contains(keyword), Location.description.contains(keyword)))
+			locations = query.all()
+			def include_parents_r(location):
+				if location.parent and location.parent not in locations:
+					locations.append(location.parent)
+					include_parents_r(location.parent)
+			for location in list(locations):
+				include_parents_r(location)
+			return render_template('item/locate.html', item=item, locations=locations, tab='locations')
+
+	if 'clear' in request.form:
+		item.location = None
+		item.parent = None
+	elif 'parent_id' in request.form or instant_item_match:
+		item.location = None
+		if instant_item_match:
+			parent = instant_item_match
+		else:
+			parent = Item.query.get(request.form['parent_id'])
+		tmp = parent
+		while tmp.parent and tmp != item:
+			tmp = tmp.parent
+		if tmp == item:
+			raise Exception()
+		item.parent = parent
+	else:
+		item.location = Location.query.get(request.form['location_id'])
+		item.parent = None
 	db.session.commit()
 	return redirect(url_for('item_view', item_id=item_id))
 
diff --git a/warehouse/migrations/versions/c020310b73b5_subitems.py b/warehouse/migrations/versions/c020310b73b5_subitems.py
new file mode 100644
index 0000000000000000000000000000000000000000..93ed3a3c525b680a03705f4b176ccc4e846890de
--- /dev/null
+++ b/warehouse/migrations/versions/c020310b73b5_subitems.py
@@ -0,0 +1,25 @@
+"""Subitems
+
+Revision ID: c020310b73b5
+Revises: bf26c4ca926e
+Create Date: 2023-04-23 23:18:08.573480
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+# revision identifiers, used by Alembic.
+revision = 'c020310b73b5'
+down_revision = 'bf26c4ca926e'
+branch_labels = None
+depends_on = None
+
+def upgrade():
+    with op.batch_alter_table('item', schema=None) as batch_op:
+        batch_op.add_column(sa.Column('parent_id', sa.String(length=5), nullable=True))
+        batch_op.create_foreign_key('fk_item_parent_id_item', 'item', ['parent_id'], ['id'])
+
+def downgrade():
+    with op.batch_alter_table('item', schema=None) as batch_op:
+        batch_op.drop_constraint('fk_item_parent_id_item', type_='foreignkey')
+        batch_op.drop_column('parent_id')
diff --git a/warehouse/models.py b/warehouse/models.py
index b81eed22213076b93d1ce68fcbc12375073f7213..f96c8c86e2c46d5a6347537a6f43d76ba8fc7d0b 100644
--- a/warehouse/models.py
+++ b/warehouse/models.py
@@ -85,5 +85,16 @@ class Item(db.Model):
 	photo_id = db.Column(db.Integer(), db.ForeignKey('photo.id'))
 	photo = db.relationship('Photo')
 	photos = db.relationship('Photo', secondary='item_photo')
+	# location and parent are mutually exclusive!
 	location_id = db.Column(db.String(5), db.ForeignKey('location.id'))
 	location = db.relationship('Location')
+	parent_id = db.Column(db.String(5), db.ForeignKey('item.id'))
+	children = db.relationship('Item', backref=db.backref('parent', remote_side=[id]))
+
+	@db.validates('photo')
+	def validate_user(self, key, value):
+		if not value:
+			return value
+		if value not in self.photos:
+			self.photos.append(value)
+		return value
diff --git a/warehouse/templates/item/add.html b/warehouse/templates/item/add.html
index a29719e0dd0d1d8a29059dc4877646cc3e7371de..137106ded434c3ade8dfd6c2c18ad7c028f5062d 100644
--- a/warehouse/templates/item/add.html
+++ b/warehouse/templates/item/add.html
@@ -1,8 +1,16 @@
 {% extends 'layout.html' %}
 
 {% block body %}
+{% if parent %}
+<h1>Add subitem of {{ parent.name }}</h1>
+{% else %}
+<h1>Add new item</h1>
+{% endif %}
 <form method="POST" class="form">
 	<input type="hidden" name="csrf_token" value="{{ request.csrf_token }}">
+	{% if parent %}
+	<input type="hidden" name="parent" value="{{ parent.id }}">
+	{% endif %}
 	<input name="name" placeholder="Name" class="form-control" required>
 	<textarea name="description" placeholder="Description" class="form-control mt-2" rows=10></textarea>
 	<div class="form-check my-2">
diff --git a/warehouse/templates/item/locate.html b/warehouse/templates/item/locate.html
index 50000e9613f40638bbc7049488a22a79e911de26..b6dd2e8f1234ce47e012f7a841995fb7460693c1 100644
--- a/warehouse/templates/item/locate.html
+++ b/warehouse/templates/item/locate.html
@@ -20,7 +20,15 @@
 <h1>Set location for {{ item.name }}</h1>
 
 <div class="d-flex justify-content-end gap-1">
+	<form method="POST" class="form-inline">
+		<input type="hidden" name="csrf_token" value="{{ request.csrf_token }}">
+		<input type="hidden" name="clear" value="1">
+		<button class="btn btn-outline-secondary" type="submit">Clear location/parent</button>
+	</form>
 	<form class="form-inline">
+		{% if tab == 'items' %}
+		<input type="hidden" name="tab" value="items">
+		{% endif %}
 		<div class="input-group">
 			<input type="text" class="form-control" name="search" value="{{ request.args.search }}" placeholder="Search" autofocus>
 			<button class="btn btn-outline-secondary" type="submit">Search</button>
@@ -30,18 +38,81 @@
 
 <form method="POST">
 	<input type="hidden" name="csrf_token" value="{{ request.csrf_token }}">
-	<table class="table table-hover">
-		<thead>
-			<tr>
-				<th scope="col">Location</th>
-				<th scope="col"></th>
-			</tr>
-		</thead>
-		<tbody>
-			{% for location in locations if not location.parent %}
-				{{ location_recursive(location) }}
-			{% endfor %}
-		</tbody>
-	</table>
+	<ul class="nav nav-tabs" role="tablist">
+		<li class="nav-item" role="presentation">
+			<a href="{{ url_for('item_locate', item_id=item.id, search=request.args.get('search')) }}" class="nav-link {{ 'active' if tab == 'locations' }}" id="locations-tab" role="tab" aria-controls="locations-tab-pane" aria-selected="true">Locations</a>
+		</li>
+		<li class="nav-item" role="presentation">
+			<a href="{{ url_for('item_locate', item_id=item.id, search=request.args.get('search'), tab='items') }}" class="nav-link {{ 'active' if tab == 'items' }}" id="items-tab" role="tab" aria-controls="items-tab-pane" aria-selected="true">Items</a>
+		</li>
+	</ul>
+
+	<div class="tab-content">
+		{% if tab == 'locations' %}
+		<div class="tab-pane show active" role="tabpanel" aria-labelledby="locations-tab" tabindex="0">
+			<table class="table table-hover">
+				<tbody>
+					{% for location in locations if not location.parent %}
+						{{ location_recursive(location) }}
+					{% endfor %}
+				</tbody>
+			</table>
+		</div>
+		{% elif tab == 'items' %}
+		<div class="tab-pane show active" role="tabpanel" aria-labelledby="items-tab" tabindex="0">
+			<table class="table table-hover">
+				<tbody>
+					{% for possible_parent in page.items %}
+					<tr>
+						<td style="width: 140px;">
+							{% if possible_parent.photo %}
+								<div style="width: 130px; height: 100px;" class="d-flex align-items-center justify-content-center border text-bg-light">
+									<img src="{{ url_for('photo_small', photo_id=possible_parent.photo.id) }}" style="max-width: 100%; max-height: 100%;">
+								</div>
+							{% endif %}
+						</td>
+						<td>
+							{{ possible_parent.name }}
+							<p><small class="text-muted">{{ possible_parent.description|markdown(short=True) }}</small></p>
+						</td>
+						<td>
+							<button type="submit" name="parent_id" value="{{ possible_parent.id }}" class="float-end btn btn-sm btn-primary">Select</button>
+						</td>
+					</tr>
+					{% endfor %}
+				</tbody>
+			</table>
+
+			<div class="d-flex justify-content-center">
+				<nav aria-label="...">
+					<ul class="pagination">
+						{% if page.has_prev %}
+							<li class="page-item"><a class="page-link" href="{{ url_for('item_locate', item_id=item.id, tab='items', search=request.args.get('search'), page=page.prev_num) }}">Previous</a></li>
+						{% else %}
+							<li class="page-item disabled"><span class="page-link">Previous</span></li>
+						{% endif %}
+
+						{% for _page in page.iter_pages() %}
+							{% if not item %}
+								<li class="page-item disabled"><span class="page-link">...</span></li>
+							{% elif _page != page.page %}
+								<li class="page-item"><a class="page-link" href="{{ url_for('item_locate', item_id=item.id, tab='items', search=request.args.get('search'), page=_page) }}">{{ _page }}</a></li>
+							{% else %}
+								<li class="page-item active" aria-current="page"><span class="page-link">{{ _page }}</span></li>
+							{% endif %}
+						{% endfor %}
+
+						{% if page.has_next %}
+						<li class="page-item"><a class="page-link" href="{{ url_for('item_locate', item_id=item.id, tab='items', search=request.args.get('search'), page=page.next_num) }}">Next</a></li>
+						{% else %}
+						<li class="page-item disabled"><span class="page-link">Next</span></li>
+						{% endif %}
+					</ul>
+				</nav>
+			</div>
+
+		</div>
+		{% endif %}
+	</div>
 </form>
 {% endblock %}
diff --git a/warehouse/templates/item/view.html b/warehouse/templates/item/view.html
index 5a81be6316fb018e94f65e41ce731323d709804c..e47c3cbcc24757f29900a27d455b064cd031ee40 100644
--- a/warehouse/templates/item/view.html
+++ b/warehouse/templates/item/view.html
@@ -42,17 +42,48 @@
 			<img src="{{ url_for('photo_medium', photo_id=item.photo.id) }}" style="max-width: 100%; max-height: 100%;">
 		</a>
 		{% else %}
-		<div style="width: 100%; height: 100%; min-width: 390px; min-height: 300px;" class="d-flex align-items-center justify-content-center border text-bg-light"></div>
+		<div style="width: 100%; height: 100%; min-height: 300px;" class="d-flex align-items-center justify-content-center border text-bg-light"></div>
 		{% endif %}
 	</div>
 	<div class="col-12 col-md-8">
 		<h1>{{ item.name }}</h1>
 		<p class="h5 text-muted">{{ item.id }}</p>
 		{{ item.description|markdown }}
-		<p><b>Location:</b> {{ item.location.name if item.location }} <a href="{{ url_for('item_locate', item_id=item.id) }}" class="btn btn-primary btn-sm">Change</a></p>
+		<p>
+			{% if item.parent %}
+
+			<b>Part of:</b> <a href="{{ url_for('item_view', item_id=item.parent.id) }}">{{ item.parent.name }} ({{ item.parent.id }})</a> <a href="{{ url_for('item_locate', item_id=item.id) }}" class="btn btn-primary btn-sm">Change</a>
+			{% else %}
+			<b>Location:</b> {{ item.location.name if item.location }} <a href="{{ url_for('item_locate', item_id=item.id) }}" class="btn btn-primary btn-sm">Change</a>
+			{% endif %}
+		</p>
 	</div>
 </div>
 
+<h2>Subitems</h2>
+<div class="d-flex justify-content-end gap-1 mb-1">
+	<a href="{{ url_for('item_add', parent=item.id) }}" class="btn btn-primary">Add subitem</a>
+</div>
+<table class="table table-hover">
+	<tbody>
+		{% for subitem in item.children %}
+		<tr>
+			<td style="width: 140px;">
+				{% if subitem.photo %}
+					<a href="{{ url_for('item_view', item_id=subitem.id) }}" style="width: 130px; height: 100px;" class="d-flex align-items-center justify-content-center border text-bg-light">
+						<img src="{{ url_for('photo_small', photo_id=subitem.photo.id) }}" style="max-width: 100%; max-height: 100%;">
+					</a>
+				{% endif %}
+			</td>
+			<td>
+				<a href="{{ url_for('item_view', item_id=subitem.id) }}">{{ subitem.name }}</a>
+				<p><small class="text-muted">{{ subitem.description|markdown(short=True) }}</small></p>
+			</td>
+		</tr>
+		{% endfor %}
+	</tbody>
+</table>
+
 <h2>Photos</h2>
 <table class="table table-hover">
 	<tbody>
@@ -68,16 +99,16 @@
 			</td>
 			<td>
 				<div class="d-flex justify-content-end gap-1">
-					<a href="{{ url_for('item_delete_photo', item_id=item.id, photo_id=photo.id) }}" class="btn btn-danger">Delete</a>
+					<a href="{{ url_for('item_delete_photo', item_id=item.id, photo_id=photo.id) }}" class="btn btn-sm btn-danger">Delete</a>
 					{% if item.photo != photo %}
 					<form method="POST" action="{{ url_for('item_set_photo', item_id=item.id, photo_id=photo.id) }}">
 						<input type="hidden" name="csrf_token" value="{{ request.csrf_token }}">
-						<button type="submit" class="btn btn-primary">Make primary</button>
+						<button type="submit" class="btn btn-sm btn-primary">Make primary</button>
 					</form>
 					{% else %}
 					<form method="POST" action="{{ url_for('item_clear_photo', item_id=item.id) }}">
 						<input type="hidden" name="csrf_token" value="{{ request.csrf_token }}">
-						<button type="submit" class="btn btn-light">Clear primary</button>
+						<button type="submit" class="btn btn-sm btn-light">Clear primary</button>
 					</form>
 					{% endif %}
 				</div>
@@ -86,4 +117,5 @@
 		{% endfor %}
 	</tbody>
 </table>
+
 {% endblock %}