diff --git a/.env.sample b/.env.sample
index 44d52fec..8f129392 100644
--- a/.env.sample
+++ b/.env.sample
@@ -168,6 +168,12 @@
 # Default: 3333
 # BK_PORT=3333
 
+# Optional: Change the default order of purchase locations. By default ordered by insertion order.
+# Useful column names for this option are:
+# - "bricktracker_metadata_purchase_locations"."name" ASC: storage name
+# Default: "bricktracker_metadata_purchase_locations"."name" ASC
+# BK_PURCHASE_LOCATION_DEFAULT_ORDER="bricktracker_metadata_purchase_locations"."name" ASC
+
 # Optional: Shuffle the lists on the front page.
 # Default: false
 # Legacy name: RANDOM
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b50f3427..efcdb16d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,7 +14,8 @@
 - Added: `BK_SHOW_GRID_SORT`, show the sort options on the grid by default
 - Added: `BK_SHOW_GRID_FILTERS`, show the filter options on the grid by default
 - Added: `BK_HIDE_ALL_STORAGES`, hide the "Storages" menu entry
-- Added: `BK_MINIFIGURES_DEFAULT_ORDER`, ordering of storages
+- Added: `BK_STORAGE_DEFAULT_ORDER`, ordering of storages
+- Added: `BK_PURCHASE_LOCATION_DEFAULT_ORDER`, ordering of purchase locations
 
 ### Code
 
@@ -39,6 +40,7 @@
     - Ownership
     - Tags
     - Storage
+    - Purchase location
 
 - Storage
     - Storage content and list
@@ -85,6 +87,7 @@
         - Tags
         - Refresh
         - Storage
+        - Purchase location
 
 - Sets grid
     - Collapsible controls depending on screen size
diff --git a/bricktracker/app.py b/bricktracker/app.py
index af005d96..f0afe429 100644
--- a/bricktracker/app.py
+++ b/bricktracker/app.py
@@ -17,6 +17,7 @@ from bricktracker.views.admin.database import admin_database_page
 from bricktracker.views.admin.image import admin_image_page
 from bricktracker.views.admin.instructions import admin_instructions_page
 from bricktracker.views.admin.owner import admin_owner_page
+from bricktracker.views.admin.purchase_location import admin_purchase_location_page  # noqa: E501
 from bricktracker.views.admin.retired import admin_retired_page
 from bricktracker.views.admin.status import admin_status_page
 from bricktracker.views.admin.storage import admin_storage_page
@@ -88,6 +89,7 @@ def setup_app(app: Flask) -> None:
     app.register_blueprint(admin_instructions_page)
     app.register_blueprint(admin_retired_page)
     app.register_blueprint(admin_owner_page)
+    app.register_blueprint(admin_purchase_location_page)
     app.register_blueprint(admin_status_page)
     app.register_blueprint(admin_storage_page)
     app.register_blueprint(admin_tag_page)
diff --git a/bricktracker/config.py b/bricktracker/config.py
index 5b9788fb..729049a9 100644
--- a/bricktracker/config.py
+++ b/bricktracker/config.py
@@ -41,6 +41,7 @@ CONFIG: Final[list[dict[str, Any]]] = [
     {'n': 'PARTS_DEFAULT_ORDER', 'd': '"rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "bricktracker_parts"."spare" ASC'},  # noqa: E501
     {'n': 'PARTS_FOLDER', 'd': 'parts', 's': True},
     {'n': 'PORT', 'd': 3333, 'c': int},
+    {'n': 'PURCHASE_LOCATION_DEFAULT_ORDER', 'd': '"bricktracker_metadata_purchase_locations"."name" ASC'},  # noqa: E501
     {'n': 'RANDOM', 'e': 'RANDOM', 'c': bool},
     {'n': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''},
     {'n': 'REBRICKABLE_IMAGE_NIL', 'd': 'https://rebrickable.com/static/img/nil.png'},  # noqa: E501
diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py
index 0b2f61cd..88d26235 100644
--- a/bricktracker/metadata.py
+++ b/bricktracker/metadata.py
@@ -48,7 +48,7 @@ class BrickMetadata(BrickRecord):
     def as_column(self, /) -> str:
         return '{kind}_{id}'.format(
             id=self.fields.id,
-            kind=self.kind.lower()
+            kind=self.kind.lower().replace(' ', '-')
         )
 
     # HTML dataset name
@@ -90,8 +90,6 @@ class BrickMetadata(BrickRecord):
 
     # Rename the entry
     def rename(self, /) -> None:
-        self.safe()
-
         self.update_field('name', value=self.fields.name)
 
     # Make the name "safe"
@@ -159,7 +157,7 @@ class BrickMetadata(BrickRecord):
         )
 
         if rows != 1:
-            raise DatabaseException('Could not update the field "{field}" for {kind} {name} ({id})'.format(  # noqa: E501
+            raise DatabaseException('Could not update the field "{field}" for {kind} "{name}" ({id})'.format(  # noqa: E501
                 field=field,
                 kind=self.kind,
                 name=self.fields.name,
diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py
index 3b98a132..71245483 100644
--- a/bricktracker/metadata_list.py
+++ b/bricktracker/metadata_list.py
@@ -7,13 +7,21 @@ from .exceptions import NotFoundException
 from .fields import BrickRecordFields
 from .record_list import BrickRecordList
 from .set_owner import BrickSetOwner
+from .set_purchase_location import BrickSetPurchaseLocation
 from .set_status import BrickSetStatus
 from .set_storage import BrickSetStorage
 from .set_tag import BrickSetTag
 
 logger = logging.getLogger(__name__)
 
-T = TypeVar('T', BrickSetOwner, BrickSetStatus, BrickSetStorage, BrickSetTag)
+T = TypeVar(
+    'T',
+    BrickSetOwner,
+    BrickSetPurchaseLocation,
+    BrickSetStatus,
+    BrickSetStorage,
+    BrickSetTag
+)
 
 
 # Lego sets metadata list
diff --git a/bricktracker/record_list.py b/bricktracker/record_list.py
index 23da29bc..18dcb10c 100644
--- a/bricktracker/record_list.py
+++ b/bricktracker/record_list.py
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
     from .rebrickable_set import RebrickableSet
     from .set import BrickSet
     from .set_owner import BrickSetOwner
+    from .set_purchase_location import BrickSetPurchaseLocation
     from .set_status import BrickSetStatus
     from .set_storage import BrickSetStorage
     from .set_tag import BrickSetTag
@@ -20,6 +21,7 @@ T = TypeVar(
     'BrickPart',
     'BrickSet',
     'BrickSetOwner',
+    'BrickSetPurchaseLocation',
     'BrickSetStatus',
     'BrickSetStorage',
     'BrickSetTag',
diff --git a/bricktracker/reload.py b/bricktracker/reload.py
index b2247ea4..38929f68 100644
--- a/bricktracker/reload.py
+++ b/bricktracker/reload.py
@@ -1,6 +1,7 @@
 from .instructions_list import BrickInstructionsList
 from .retired_list import BrickRetiredList
 from .set_owner_list import BrickSetOwnerList
+from .set_purchase_location_list import BrickSetPurchaseLocationList
 from .set_status_list import BrickSetStatusList
 from .set_storage_list import BrickSetStorageList
 from .set_tag_list import BrickSetTagList
@@ -17,6 +18,9 @@ def reload() -> None:
         # Reload the set owners
         BrickSetOwnerList.new(force=True)
 
+        # Reload the set purchase locations
+        BrickSetPurchaseLocationList.new(force=True)
+
         # Reload the set statuses
         BrickSetStatusList.new(force=True)
 
diff --git a/bricktracker/set.py b/bricktracker/set.py
index 6368d40c..fb972092 100644
--- a/bricktracker/set.py
+++ b/bricktracker/set.py
@@ -10,6 +10,7 @@ from .minifigure_list import BrickMinifigureList
 from .part_list import BrickPartList
 from .rebrickable_set import RebrickableSet
 from .set_owner_list import BrickSetOwnerList
+from .set_purchase_location_list import BrickSetPurchaseLocationList
 from .set_status_list import BrickSetStatusList
 from .set_storage_list import BrickSetStorageList
 from .set_tag_list import BrickSetTagList
@@ -63,6 +64,13 @@ class BrickSet(RebrickableSet):
                 )
                 self.fields.storage = storage.fields.id
 
+                # Save the purchase location
+                purchase_location = BrickSetPurchaseLocationList.get(
+                    data.get('purchase_location', ''),
+                    allow_none=True
+                )
+                self.fields.purchase_location = purchase_location.fields.id
+
                 # Insert into database
                 self.insert(commit=False)
 
diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py
index deaf269b..a8f5faa4 100644
--- a/bricktracker/set_list.py
+++ b/bricktracker/set_list.py
@@ -5,6 +5,8 @@ from flask import current_app
 from .record_list import BrickRecordList
 from .set_owner import BrickSetOwner
 from .set_owner_list import BrickSetOwnerList
+from .set_purchase_location import BrickSetPurchaseLocation
+from .set_purchase_location_list import BrickSetPurchaseLocationList
 from .set_status_list import BrickSetStatusList
 from .set_storage import BrickSetStorage
 from .set_storage_list import BrickSetStorageList
@@ -175,6 +177,8 @@ def set_metadata_lists(
     str,
     Union[
         list[BrickSetOwner],
+        list[BrickSetPurchaseLocation],
+        BrickSetPurchaseLocation,
         list[BrickSetStorage],
         BrickSetStorageList,
         list[BrickSetTag]
@@ -182,6 +186,7 @@ def set_metadata_lists(
 ]:
     return {
         'brickset_owners': BrickSetOwnerList.list(),
+        'brickset_purchase_locations': BrickSetPurchaseLocationList.list(as_class=as_class),  # noqa: E501
         'brickset_storages': BrickSetStorageList.list(as_class=as_class),
         'brickset_tags': BrickSetTagList.list(),
     }
diff --git a/bricktracker/set_purchase_location.py b/bricktracker/set_purchase_location.py
new file mode 100644
index 00000000..801ccf82
--- /dev/null
+++ b/bricktracker/set_purchase_location.py
@@ -0,0 +1,13 @@
+from .metadata import BrickMetadata
+
+
+# Lego set purchase location metadata
+class BrickSetPurchaseLocation(BrickMetadata):
+    kind: str = 'purchase location'
+
+    # Queries
+    delete_query: str = 'set/metadata/purchase_location/delete'
+    insert_query: str = 'set/metadata/purchase_location/insert'
+    select_query: str = 'set/metadata/purchase_location/select'
+    update_field_query: str = 'set/metadata/purchase_location/update/field'
+    update_set_value_query: str = 'set/metadata/purchase_location/update/value'
diff --git a/bricktracker/set_purchase_location_list.py b/bricktracker/set_purchase_location_list.py
new file mode 100644
index 00000000..3ffae4bc
--- /dev/null
+++ b/bricktracker/set_purchase_location_list.py
@@ -0,0 +1,42 @@
+import logging
+from typing import Self
+
+from flask import current_app
+
+from .metadata_list import BrickMetadataList
+from .set_purchase_location import BrickSetPurchaseLocation
+
+logger = logging.getLogger(__name__)
+
+
+# Lego sets purchase location list
+class BrickSetPurchaseLocationList(
+    BrickMetadataList[BrickSetPurchaseLocation]
+):
+    kind: str = 'set purchase locations'
+
+    # Queries
+    select_query = 'set/metadata/purchase_location/list'
+    all_query = 'set/metadata/purchase_location/all'
+
+    # Set value endpoint
+    set_value_endpoint: str = 'set.update_purchase_location'
+
+    # Load all purchase locations
+    @classmethod
+    def all(cls, /) -> Self:
+        new = cls.new()
+        new.override()
+
+        for record in new.select(
+            override_query=cls.all_query,
+            order=current_app.config['PURCHASE_LOCATION_DEFAULT_ORDER']
+        ):
+            new.records.append(new.model(record=record))
+
+        return new
+
+    # Instantiate the list with the proper class
+    @classmethod
+    def new(cls, /, *, force: bool = False) -> Self:
+        return cls(BrickSetPurchaseLocation, force=force)
diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql
index fbc86e0d..333868d0 100644
--- a/bricktracker/sql/set/base/base.sql
+++ b/bricktracker/sql/set/base/base.sql
@@ -1,6 +1,7 @@
 SELECT
     {% block id %}{% endblock %}
     "bricktracker_sets"."storage",
+    "bricktracker_sets"."purchase_location",
     "rebrickable_sets"."set",
     "rebrickable_sets"."number",
     "rebrickable_sets"."version",
diff --git a/bricktracker/sql/set/insert.sql b/bricktracker/sql/set/insert.sql
index 9a46f88b..bc933c33 100644
--- a/bricktracker/sql/set/insert.sql
+++ b/bricktracker/sql/set/insert.sql
@@ -1,9 +1,11 @@
 INSERT OR IGNORE INTO "bricktracker_sets" (
     "id",
     "set",
-    "storage"
+    "storage",
+    "purchase_location"
 ) VALUES (
     :id,
     :set,
-    :storage
+    :storage,
+    :purchase_location
 )
diff --git a/bricktracker/sql/set/metadata/purchase_location/base.sql b/bricktracker/sql/set/metadata/purchase_location/base.sql
new file mode 100644
index 00000000..8ac33ca1
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/base.sql
@@ -0,0 +1,6 @@
+SELECT
+    "bricktracker_metadata_purchase_locations"."id",
+    "bricktracker_metadata_purchase_locations"."name"
+FROM "bricktracker_metadata_purchase_locations"
+
+{% block where %}{% endblock %}
diff --git a/bricktracker/sql/set/metadata/purchase_location/delete.sql b/bricktracker/sql/set/metadata/purchase_location/delete.sql
new file mode 100644
index 00000000..489dfd07
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/delete.sql
@@ -0,0 +1,10 @@
+BEGIN TRANSACTION;
+
+DELETE FROM "bricktracker_metadata_purchase_locations"
+WHERE "bricktracker_metadata_purchase_locations"."id" IS NOT DISTINCT FROM '{{ id }}';
+
+UPDATE "bricktracker_sets"
+SET "purchase_location" = NULL
+WHERE "bricktracker_sets"."purchase_location" IS NOT DISTINCT FROM '{{ id }}';
+
+COMMIT;
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/purchase_location/insert.sql b/bricktracker/sql/set/metadata/purchase_location/insert.sql
new file mode 100644
index 00000000..22fc5879
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/insert.sql
@@ -0,0 +1,11 @@
+BEGIN TRANSACTION;
+
+INSERT INTO "bricktracker_metadata_purchase_locations" (
+    "id",
+    "name"
+) VALUES (
+    '{{ id }}',
+    '{{ name }}'
+);
+
+COMMIT;
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/purchase_location/list.sql b/bricktracker/sql/set/metadata/purchase_location/list.sql
new file mode 100644
index 00000000..2a0813b5
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/list.sql
@@ -0,0 +1 @@
+{% extends 'set/metadata/purchase_location/base.sql' %}
diff --git a/bricktracker/sql/set/metadata/purchase_location/select.sql b/bricktracker/sql/set/metadata/purchase_location/select.sql
new file mode 100644
index 00000000..a9e6161f
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/select.sql
@@ -0,0 +1,5 @@
+{% extends 'set/metadata/purchase_location/base.sql' %}
+
+{% block where %}
+WHERE "bricktracker_metadata_purchase_locations"."id" IS NOT DISTINCT FROM :id
+{% endblock %}
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/purchase_location/update/field.sql b/bricktracker/sql/set/metadata/purchase_location/update/field.sql
new file mode 100644
index 00000000..323d98d1
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/update/field.sql
@@ -0,0 +1,3 @@
+UPDATE "bricktracker_metadata_purchase_locations"
+SET "{{field}}" = :value
+WHERE "bricktracker_metadata_purchase_locations"."id" IS NOT DISTINCT FROM :id
diff --git a/bricktracker/sql/set/metadata/purchase_location/update/value.sql b/bricktracker/sql/set/metadata/purchase_location/update/value.sql
new file mode 100644
index 00000000..d27469e1
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/update/value.sql
@@ -0,0 +1,3 @@
+UPDATE "bricktracker_sets"
+SET "purchase_location" = :value
+WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :set_id
diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py
index 08cdafcb..749b3df2 100644
--- a/bricktracker/views/admin/admin.py
+++ b/bricktracker/views/admin/admin.py
@@ -10,6 +10,8 @@ from ...rebrickable_image import RebrickableImage
 from ...retired_list import BrickRetiredList
 from ...set_owner import BrickSetOwner
 from ...set_owner_list import BrickSetOwnerList
+from ...set_purchase_location import BrickSetPurchaseLocation
+from ...set_purchase_location_list import BrickSetPurchaseLocationList
 from ...set_storage import BrickSetStorage
 from ...set_storage_list import BrickSetStorageList
 from ...set_status import BrickSetStatus
@@ -36,6 +38,7 @@ def admin() -> str:
     database_version: int = -1
     instructions: BrickInstructionsList | None = None
     metadata_owners: list[BrickSetOwner] = []
+    metadata_purchase_locations: list[BrickSetPurchaseLocation] = []
     metadata_statuses: list[BrickSetStatus] = []
     metadata_storages: list[BrickSetStorage] = []
     metadata_tags: list[BrickSetTag] = []
@@ -54,6 +57,7 @@ def admin() -> str:
         instructions = BrickInstructionsList()
 
         metadata_owners = BrickSetOwnerList.list()
+        metadata_purchase_locations = BrickSetPurchaseLocationList.list()
         metadata_statuses = BrickSetStatusList.list(all=True)
         metadata_storages = BrickSetStorageList.list()
         metadata_tags = BrickSetTagList.list()
@@ -81,6 +85,7 @@ def admin() -> str:
     open_instructions = request.args.get('open_instructions', None)
     open_logout = request.args.get('open_logout', None)
     open_owner = request.args.get('open_owner', None)
+    open_purchase_location = request.args.get('open_purchase_location', None)
     open_retired = request.args.get('open_retired', None)
     open_status = request.args.get('open_status', None)
     open_storage = request.args.get('open_storage', None)
@@ -89,6 +94,7 @@ def admin() -> str:
 
     open_metadata = (
         open_owner or
+        open_purchase_location or
         open_status or
         open_storage or
         open_tag
@@ -113,6 +119,7 @@ def admin() -> str:
         database_version=database_version,
         instructions=instructions,
         metadata_owners=metadata_owners,
+        metadata_purchase_locations=metadata_purchase_locations,
         metadata_statuses=metadata_statuses,
         metadata_storages=metadata_storages,
         metadata_tags=metadata_tags,
@@ -126,12 +133,14 @@ def admin() -> str:
         open_logout=open_logout,
         open_metadata=open_metadata,
         open_owner=open_owner,
+        open_purchase_location=open_purchase_location,
         open_retired=open_retired,
         open_status=open_status,
         open_storage=open_storage,
         open_tag=open_tag,
         open_theme=open_theme,
         owner_error=request.args.get('owner_error'),
+        purchase_location_error=request.args.get('purchase_location_error'),
         retired=BrickRetiredList(),
         status_error=request.args.get('status_error'),
         storage_error=request.args.get('storage_error'),
diff --git a/bricktracker/views/admin/purchase_location.py b/bricktracker/views/admin/purchase_location.py
new file mode 100644
index 00000000..48e7b7d5
--- /dev/null
+++ b/bricktracker/views/admin/purchase_location.py
@@ -0,0 +1,84 @@
+from flask import (
+    Blueprint,
+    redirect,
+    request,
+    render_template,
+    url_for,
+)
+from flask_login import login_required
+from werkzeug.wrappers.response import Response
+
+from ..exceptions import exception_handler
+from ...reload import reload
+from ...set_purchase_location import BrickSetPurchaseLocation
+
+admin_purchase_location_page = Blueprint(
+    'admin_purchase_location',
+    __name__,
+    url_prefix='/admin/purchase_location'
+)
+
+
+# Add a metadata purchase location
+@admin_purchase_location_page.route('/add', methods=['POST'])
+@login_required
+@exception_handler(
+    __file__,
+    post_redirect='admin.admin',
+    error_name='purchase_location_error',
+    open_purchase_location=True
+)
+def add() -> Response:
+    BrickSetPurchaseLocation().from_form(request.form).insert()
+
+    reload()
+
+    return redirect(url_for('admin.admin', open_purchase_location=True))
+
+
+# Delete the metadata purchase location
+@admin_purchase_location_page.route('<id>/delete', methods=['GET'])
+@login_required
+@exception_handler(__file__)
+def delete(*, id: str) -> str:
+    return render_template(
+        'admin.html',
+        delete_purchase_location=True,
+        purchase_location=BrickSetPurchaseLocation().select_specific(id),
+        error=request.args.get('purchase_location_error')
+    )
+
+
+# Actually delete the metadata purchase location
+@admin_purchase_location_page.route('<id>/delete', methods=['POST'])
+@login_required
+@exception_handler(
+    __file__,
+    post_redirect='admin_purchase_location.delete',
+    error_name='purchase_location_error'
+)
+def do_delete(*, id: str) -> Response:
+    purchase_location = BrickSetPurchaseLocation().select_specific(id)
+    purchase_location.delete()
+
+    reload()
+
+    return redirect(url_for('admin.admin', open_purchase_location=True))
+
+
+# Rename the metadata purchase location
+@admin_purchase_location_page.route('<id>/rename', methods=['POST'])
+@login_required
+@exception_handler(
+    __file__,
+    post_redirect='admin.admin',
+    error_name='purchase_location_error',
+    open_purchase_location=True
+)
+def rename(*, id: str) -> Response:
+    purchase_location = BrickSetPurchaseLocation().select_specific(id)
+    purchase_location.from_form(request.form).rename()
+
+    reload()
+
+    return redirect(url_for('admin.admin', open_purchase_location=True))
diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py
index 7f397da7..0777f4e9 100644
--- a/bricktracker/views/set.py
+++ b/bricktracker/views/set.py
@@ -18,6 +18,7 @@ from ..part import BrickPart
 from ..set import BrickSet
 from ..set_list import BrickSetList, set_metadata_lists
 from ..set_owner_list import BrickSetOwnerList
+from ..set_purchase_location_list import BrickSetPurchaseLocationList
 from ..set_status_list import BrickSetStatusList
 from ..set_storage_list import BrickSetStorageList
 from ..set_tag_list import BrickSetTagList
@@ -40,6 +41,25 @@ def list() -> str:
     )
 
 
+# Change the value of purchase location
+@set_page.route('/<id>/purchase_location', methods=['POST'])
+@login_required
+@exception_handler(__file__, json=True)
+def update_purchase_location(*, id: str) -> Response:
+    brickset = BrickSet().select_light(id)
+    purchase_location = BrickSetPurchaseLocationList.get(
+        request.json.get('value', ''),  # type: ignore
+        allow_none=True
+    )
+
+    value = purchase_location.update_set_value(
+        brickset,
+        value=purchase_location.fields.id
+    )
+
+    return jsonify({'value': value})
+
+
 # Change the state of a owner
 @set_page.route('/<id>/owner/<metadata_id>', methods=['POST'])
 @login_required
diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js
index 311eac50..8a4e5bba 100644
--- a/static/scripts/socket/set.js
+++ b/static/scripts/socket/set.js
@@ -16,6 +16,7 @@ class BrickSetSocket extends BrickSocket {
         this.html_input = document.getElementById(`${id}-set`);
         this.html_no_confim = document.getElementById(`${id}-no-confirm`);
         this.html_owners = document.getElementById(`${id}-owners`);
+        this.html_purchase_location = document.getElementById(`${id}-purchase-location`);
         this.html_storage = document.getElementById(`${id}-storage`);
         this.html_tags = document.getElementById(`${id}-tags`);
 
@@ -152,6 +153,12 @@ class BrickSetSocket extends BrickSocket {
                 });
             }
 
+            // Grab the purchase location
+            let purchase_location = null;
+            if (this.html_purchase_location) {
+                purchase_location = this.html_purchase_location.value;
+            }
+
             // Grab the storage
             let storage = null;
             if (this.html_storage) {
@@ -177,6 +184,7 @@ class BrickSetSocket extends BrickSocket {
             this.socket.emit(this.messages.IMPORT_SET, {
                 set: (set !== undefined) ? set : this.html_input.value,
                 owners: owners,
+                purchase_location: purchase_location,
                 storage: storage,
                 tags: tags,
                 refresh: this.refresh
@@ -293,6 +301,10 @@ class BrickSetSocket extends BrickSocket {
             this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled);
         }
 
+        if (this.html_purchase_location) {
+            this.html_purchase_location.disabled = !enabled;
+        }
+
         if (this.html_storage) {
             this.html_storage.disabled = !enabled;
         }
diff --git a/templates/add.html b/templates/add.html
index 9a0deebf..d9a94629 100644
--- a/templates/add.html
+++ b/templates/add.html
@@ -51,9 +51,22 @@
               </div>
               {{ accordion.footer() }}
             {% endif %}
+            {% if brickset_purchase_locations | length %}
+              {{ accordion.header('Purchase location', 'purchase-location', 'metadata', icon='building-line') }}
+              <label class="visually-hidden" for="add-purchase-location">{{ name }}</label>
+              <div class="input-group">
+                <select id="add-purchase-location" class="form-select" autocomplete="off">
+                  <option value="" selected><i>None</i></option>
+                  {% for purchase_location in brickset_purchase_locations %}
+                    <option value="{{ purchase_location.fields.id }}">{{ purchase_location.fields.name }}</option>
+                  {% endfor %}
+                </select>
+              </div>
+              {{ accordion.footer() }}
+            {% endif %}
             {% if brickset_storages | length %}
               {{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }}
-              <label class="visually-hidden" for="storage">{{ name }}</label>
+              <label class="visually-hidden" for="add-storage">{{ name }}</label>
               <div class="input-group">
                 <select id="add-storage" class="form-select" autocomplete="off">
                   <option value="" selected><i>None</i></option>
diff --git a/templates/admin.html b/templates/admin.html
index 22a7e1a7..87dbdb4a 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -18,6 +18,8 @@
             {% include 'admin/database/delete.html' %}
             {% elif delete_owner %}
             {% include 'admin/owner/delete.html' %}
+            {% elif delete_purchase_location %}
+            {% include 'admin/purchase_location/delete.html' %}
             {% elif delete_status %}
             {% include 'admin/status/delete.html' %}
             {% elif delete_storage %}
@@ -39,10 +41,11 @@
               {% include 'admin/theme.html' %}
               {% include 'admin/retired.html' %}
               {{ accordion.header('Set metadata', 'metadata', 'admin', expanded=open_metadata, icon='profile-line', class='p-0') }}
-              {% include 'admin/owner.html' %}
-              {% include 'admin/status.html' %}
-              {% include 'admin/storage.html' %}
-              {% include 'admin/tag.html' %}
+                {% include 'admin/owner.html' %}
+                {% include 'admin/purchase_location.html' %}
+                {% include 'admin/status.html' %}
+                {% include 'admin/storage.html' %}
+                {% include 'admin/tag.html' %}
               {{ accordion.footer() }}
               {% include 'admin/database.html' %}
               {% include 'admin/configuration.html' %}
diff --git a/templates/admin/purchase_location.html b/templates/admin/purchase_location.html
new file mode 100644
index 00000000..12675768
--- /dev/null
+++ b/templates/admin/purchase_location.html
@@ -0,0 +1,42 @@
+{% import 'macro/accordion.html' as accordion %}
+
+{{ accordion.header('Set purchase locations', 'purchase-location', 'metadata', expanded=open_purchase_location, icon='building-line', class='p-0') }}
+{% if purchase_location_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ purchase_location_error }}.</div>{% endif %}
+<ul class="list-group list-group-flush">
+  {% if metadata_purchase_locations | length %}
+    {% for purchase_location in metadata_purchase_locations %}
+      <li class="list-group-item">
+        <form action="{{ url_for('admin_purchase_location.rename', id=purchase_location.fields.id) }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
+          <div class="col-12 flex-grow-1">
+            <label class="visually-hidden" for="name-{{ purchase_location.fields.id }}">Name</label>
+            <div class="input-group">
+              <div class="input-group-text">Name</div>
+              <input type="text" class="form-control" id="name-{{ purchase_location.fields.id }}" name="name" value="{{ purchase_location.fields.name }}">
+              <button type="submit" class="btn btn-primary"><i class="ri-edit-line"></i> Rename</button>
+            </div>
+          </div>
+          <div class="col-12">
+            <a href="{{ url_for('admin_purchase_location.delete', id=purchase_location.fields.id) }}" class="btn btn-danger" role="button"><i class="ri-delete-bin-2-line"></i> Delete</a>
+          </div>
+        </form>
+      </li>
+    {% endfor %}
+  {% else %}
+    <li class="list-group-item text-center"><i class="ri-error-warning-line"></i> No purchase location found.</li>
+  {% endif %}
+  <li class="list-group-item">
+    <form action="{{ url_for('admin_purchase_location.add') }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
+      <div class="col-12 flex-grow-1">
+        <label class="visually-hidden" for="name">Name</label>
+        <div class="input-group">
+          <div class="input-group-text">Name</div>
+          <input type="text" class="form-control" id="name" name="name" value="">
+        </div>
+      </div>
+      <div class="col-12">
+        <button type="submit" class="btn btn-primary"><i class="ri-add-circle-line"></i> Add</button>
+      </div>
+    </form>
+  </li>
+</ul>
+{{ accordion.footer() }}
diff --git a/templates/admin/purchase_location/delete.html b/templates/admin/purchase_location/delete.html
new file mode 100644
index 00000000..c53d8c22
--- /dev/null
+++ b/templates/admin/purchase_location/delete.html
@@ -0,0 +1,19 @@
+{% import 'macro/accordion.html' as accordion %}
+
+{{ accordion.header('Set purchase locations danger zone', 'purchase-location-danger', 'admin', expanded=true, danger=true, class='text-end') }}
+<form action="{{ url_for('admin_purchase_location.do_delete', id=purchase_location.fields.id) }}" method="post">
+  {% if purchase_location_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ purchase_location_error }}.</div>{% endif %}
+  <div class="alert alert-danger text-center" role="alert">You are about to <strong>delete a set purchase location</strong>. This action is irreversible.</div>
+  <div class="row row-cols-lg-auto g-3 align-items-center">
+    <div class="col-12 flex-grow-1">
+      <div class="input-group">
+        <div class="input-group-text">Name</div>
+        <input type="text" class="form-control" value="{{ purchase_location.fields.name }}" disabled>
+      </div>
+    </div>
+  </div>
+  <hr class="border-bottom">
+  <a class="btn btn-danger" href="{{ url_for('admin.admin', open_purchase_location=true) }}" role="button"><i class="ri-arrow-left-long-line"></i> Back to the admin</a>
+  <button type="submit" class="btn btn-danger"><i class="ri-delete-bin-2-line"></i> Delete <strong>the set purchase location</strong></button>
+</form>
+{{ accordion.footer() }}
diff --git a/templates/macro/badge.html b/templates/macro/badge.html
index ea2be584..f1c9f89f 100644
--- a/templates/macro/badge.html
+++ b/templates/macro/badge.html
@@ -65,6 +65,18 @@
   {% endif %}
 {% endmacro %}
 
+{% macro purchase_location(item, purchase_locations, solo=false, last=false) %}
+  {% if purchase_locations and item.fields.purchase_location in purchase_locations.mapping %}
+    {% set purchase_location = purchase_locations.mapping[item.fields.purchase_location] %}
+    {% if last %}
+      {% set tooltip=purchase_location.fields.name %}
+    {% else %}
+      {% set text=purchase_location.fields.name %}
+    {% endif %}
+    {{ badge(check=purchase_location, solo=solo, last=last, color='light border', icon='building-line', text=text, tooltip=tooltip) }}
+  {% endif %}
+{% endmacro %}
+
 {% macro set(set, solo=false, last=false, url=None, id=None) %}
   {% if id %}
       {% set url=url_for('set.details', id=id) %}
diff --git a/templates/set/card.html b/templates/set/card.html
index 20642066..7c2525d1 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -17,6 +17,11 @@
     {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
       data-has-damaged="{{ (item.fields.total_damaged > 0) | int }}" data-damaged="{{ item.fields.total_damaged }}"
     {% endif %}
+    data-has-purchase-location="{{ item.fields.purchase_location is not none | int }}"
+    {% if item.fields.purchase_location is not none %}
+      data-purchase-location="{{ item.fields.purchase_location }}"
+      {% if item.fields.purchase_location in brickset_purchase_locations.mapping %}data-search-purchase-location="{{ brickset_purchase_locations.mapping[item.fields.purchase_location].fields.name | lower }}"{% endif %}
+    {% endif %}
     data-has-storage="{{ item.fields.storage is not none | int }}"
     {% if item.fields.storage is not none %}
       data-storage="{{ item.fields.storage }}"
@@ -63,6 +68,7 @@
       {{ badge.owner(item, owner, solo=solo, last=last) }}
     {% endfor %}
     {{ badge.storage(item, brickset_storages, solo=solo, last=last) }}
+    {{ badge.purchase_location(item, brickset_purchase_locations, solo=solo, last=last) }}
     {% if not last %}
       {% if not solo %}
         {{ badge.instructions(item, solo=solo, last=last) }}
diff --git a/templates/set/filter.html b/templates/set/filter.html
index 8f3b4109..24454c25 100644
--- a/templates/set/filter.html
+++ b/templates/set/filter.html
@@ -60,6 +60,22 @@
       </div>
     </div>
   {% endif %}
+  {% if brickset_purchase_locations | length %}
+    <div class="col-12 flex-grow-1">
+      <label class="visually-hidden" for="grid-owner">Purchase location</label>
+      <div class="input-group">
+        <span class="input-group-text"><i class="ri-building-line"></i><span class="ms-1 d-none d-md-inline"> Purchase location</span></span>
+        <select id="grid-purchase-location" class="form-select"
+          data-filter="value" data-filter-attribute="purchase-location"
+          autocomplete="off">
+          <option value="" selected>All</option>
+          {% for purchase_location in brickset_purchase_locations %}
+            <option value="{{ purchase_location.fields.id }}">{{ purchase_location.fields.name }}</option>
+          {% endfor %}
+        </select>
+      </div>
+    </div>
+  {% endif %}
   {% if brickset_storages | length %}
     <div class="col-12 flex-grow-1">
       <label class="visually-hidden" for="grid-owner">Storage</label>
diff --git a/templates/set/management.html b/templates/set/management.html
index 1a8c2b7b..8d675f9a 100644
--- a/templates/set/management.html
+++ b/templates/set/management.html
@@ -1,44 +1,53 @@
 {% if g.login.is_authenticated() %}
   {{ accordion.header('Management', 'set-management', 'set-details', icon='settings-4-line', class='p-0') }}
     {{ accordion.header('Owners', 'owner', 'set-management', icon='group-line', class='p-0') }}
-    <ul class="list-group list-group-flush">
-    {% if brickset_owners | length %}
-      {% for owner in brickset_owners %}
-        <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li>
-      {% endfor %}
-    {% else %}
-      <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No owner found.</li>
+      <ul class="list-group list-group-flush">
+      {% if brickset_owners | length %}
+        {% for owner in brickset_owners %}
+          <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li>
+        {% endfor %}
+      {% else %}
+        <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No owner found.</li>
+        {% endif %}
+      </ul>
+      <div class="list-group list-group-flush border-top">
+        <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
+      </div>
+    {{ accordion.footer() }}
+    {{ accordion.header('Purchase location', 'purchase-location', 'set-management', icon='building-line') }}
+      {% if brickset_purchase_locations | length %}
+        {{ form.select('Purchase location', item, 'purchase_location', brickset_purchase_locations, delete=delete) }}
+      {% else %}
+        <p class="text-center"><i class="ri-error-warning-line"></i> No purchase location found.</p>
       {% endif %}
-    </ul>
-    <div class="list-group list-group-flush border-top">
-      <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
-    </div>
-  {{ accordion.footer() }}
+      <hr>
+      <a href="{{ url_for('admin.admin', open_purchase_location=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set purchase locations</a>
+    {{ accordion.footer() }}
     {{ accordion.header('Storage', 'storage', 'set-management', icon='archive-2-line') }}
-    {% if brickset_storages | length %}
-      {{ form.select('Storage', item, 'storage', brickset_storages, delete=delete) }}
-    {% else %}
-      <p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
-    {% endif %}
-    <hr>
-      <a href="{{ url_for('admin.admin', open_storage=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set storages</a>
-  {{ accordion.footer() }}
-    {{ accordion.header('Tags', 'tag', 'set-management', icon='price-tag-2-line', class='p-0') }}
-    <ul class="list-group list-group-flush">
-    {% if brickset_tags | length %}
-      {% for tag in brickset_tags %}
-        <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, tag, delete=delete) }}</li>
-      {% endfor %}
-    {% else %}
-      <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No tag found.</li>
+      {% if brickset_storages | length %}
+        {{ form.select('Storage', item, 'storage', brickset_storages, delete=delete) }}
+      {% else %}
+        <p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
       {% endif %}
-    </ul>
-    <div class="list-group list-group-flush border-top">
-      <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_tag=true) }}"><i class="ri-settings-4-line"></i> Manage the set tags</a>
-    </div>
-  {{ accordion.footer() }}
+      <hr>
+      <a href="{{ url_for('admin.admin', open_storage=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set storages</a>
+    {{ accordion.footer() }}
+    {{ accordion.header('Tags', 'tag', 'set-management', icon='price-tag-2-line', class='p-0') }}
+      <ul class="list-group list-group-flush">
+      {% if brickset_tags | length %}
+        {% for tag in brickset_tags %}
+          <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, tag, delete=delete) }}</li>
+        {% endfor %}
+      {% else %}
+        <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No tag found.</li>
+        {% endif %}
+      </ul>
+      <div class="list-group list-group-flush border-top">
+        <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_tag=true) }}"><i class="ri-settings-4-line"></i> Manage the set tags</a>
+      </div>
+    {{ accordion.footer() }}
     {{ accordion.header('Data', 'data', 'set-management', icon='database-2-line') }}
-    <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
+      <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
     {{ accordion.footer() }}
   {{ accordion.footer() }}
 {% endif %}
diff --git a/templates/sets.html b/templates/sets.html
index 5af93c93..4832160c 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -10,7 +10,7 @@
       <label class="visually-hidden" for="grid-search">Search</label>
       <div class="input-group">
         <span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-md-inline"> Search</span></span>
-        <input id="grid-search" data-search-exact="name,number,parts,searchStorage,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner, storage, tag" value="">
+        <input id="grid-search" data-search-exact="name,number,parts,searchPurchaseLocation,searchStorage,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner, purchase location, storage, tag" value="">
       </div>
     </div>
     <div class="col-12">