Set purchase date and price

This commit is contained in:
Gregoo 2025-02-04 17:03:39 +01:00
parent 195f18f141
commit f0cec23da9
17 changed files with 228 additions and 16 deletions

View File

@ -168,6 +168,15 @@
# Default: 3333 # Default: 3333
# BK_PORT=3333 # BK_PORT=3333
# Optional: Format of the timestamp for purchase dates
# Check https://docs.python.org/3/library/time.html#time.strftime for format details
# Default: %d/%m/%Y
# BK_PURCHASE_DATE_FORMAT=%m/%d/%Y
# Optional: Currency to display for purchase prices.
# Default: €
# BK_PURCHASE_CURRENCY=£
# Optional: Change the default order of purchase locations. By default ordered by insertion order. # Optional: Change the default order of purchase locations. By default ordered by insertion order.
# Useful column names for this option are: # Useful column names for this option are:
# - "bricktracker_metadata_purchase_locations"."name" ASC: storage name # - "bricktracker_metadata_purchase_locations"."name" ASC: storage name

View File

@ -16,6 +16,8 @@
- Added: `BK_HIDE_ALL_STORAGES`, hide the "Storages" menu entry - Added: `BK_HIDE_ALL_STORAGES`, hide the "Storages" menu entry
- Added: `BK_STORAGE_DEFAULT_ORDER`, ordering of storages - Added: `BK_STORAGE_DEFAULT_ORDER`, ordering of storages
- Added: `BK_PURCHASE_LOCATION_DEFAULT_ORDER`, ordering of purchase locations - Added: `BK_PURCHASE_LOCATION_DEFAULT_ORDER`, ordering of purchase locations
- Added: `BK_PURCHASE_CURRENCY`, currency to display for purchase prices
- Added: `BK_PURCHASE_DATE_FORMAT`, date format for purchase dates
### Code ### Code
@ -40,7 +42,7 @@
- Ownership - Ownership
- Tags - Tags
- Storage - Storage
- Purchase location - Purchase location, date, price
- Storage - Storage
- Storage content and list - Storage content and list
@ -87,7 +89,7 @@
- Tags - Tags
- Refresh - Refresh
- Storage - Storage
- Purchase location - Purchase location, date, price
- Sets grid - Sets grid
- Collapsible controls depending on screen size - Collapsible controls depending on screen size

View File

@ -41,6 +41,8 @@ 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_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': 'PARTS_FOLDER', 'd': 'parts', 's': True},
{'n': 'PORT', 'd': 3333, 'c': int}, {'n': 'PORT', 'd': 3333, 'c': int},
{'n': 'PURCHASE_DATE_FORMAT', 'd': '%d/%m/%Y'},
{'n': 'PURCHASE_CURRENCY', 'd': ''},
{'n': 'PURCHASE_LOCATION_DEFAULT_ORDER', 'd': '"bricktracker_metadata_purchase_locations"."name" ASC'}, # noqa: E501 {'n': 'PURCHASE_LOCATION_DEFAULT_ORDER', 'd': '"bricktracker_metadata_purchase_locations"."name" ASC'}, # noqa: E501
{'n': 'RANDOM', 'e': 'RANDOM', 'c': bool}, {'n': 'RANDOM', 'e': 'RANDOM', 'c': bool},
{'n': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''}, {'n': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''},

View File

@ -1,3 +1,4 @@
from datetime import datetime
import logging import logging
import traceback import traceback
from typing import Any, Self, TYPE_CHECKING from typing import Any, Self, TYPE_CHECKING
@ -5,7 +6,7 @@ from uuid import uuid4
from flask import current_app, url_for from flask import current_app, url_for
from .exceptions import NotFoundException from .exceptions import NotFoundException, DatabaseException, ErrorException
from .minifigure_list import BrickMinifigureList from .minifigure_list import BrickMinifigureList
from .part_list import BrickPartList from .part_list import BrickPartList
from .rebrickable_set import RebrickableSet from .rebrickable_set import RebrickableSet
@ -27,6 +28,8 @@ class BrickSet(RebrickableSet):
select_query: str = 'set/select/full' select_query: str = 'set/select/full'
light_query: str = 'set/select/light' light_query: str = 'set/select/light'
insert_query: str = 'set/insert' insert_query: str = 'set/insert'
update_purchase_date_query: str = 'set/update/purchase_date'
update_purchase_price_query: str = 'set/update/purchase_price'
# Delete a set # Delete a set
def delete(self, /) -> None: def delete(self, /) -> None:
@ -152,6 +155,30 @@ class BrickSet(RebrickableSet):
return True return True
# Purchase date
def purchase_date(self, /, *, standard: bool = False) -> str:
if self.fields.purchase_date is not None:
time = datetime.fromtimestamp(self.fields.purchase_date)
if standard:
return time.strftime('%Y/%m/%d')
else:
return time.strftime(
current_app.config['PURCHASE_DATE_FORMAT']
)
else:
return ''
# Purchase price with currency
def purchase_price(self, /) -> str:
if self.fields.purchase_price is not None:
return '{price}{currency}'.format(
price=self.fields.purchase_price,
currency=current_app.config['PURCHASE_CURRENCY']
)
else:
return ''
# Minifigures # Minifigures
def minifigures(self, /) -> BrickMinifigureList: def minifigures(self, /) -> BrickMinifigureList:
return BrickMinifigureList().from_set(self) return BrickMinifigureList().from_set(self)
@ -194,6 +221,80 @@ class BrickSet(RebrickableSet):
return self return self
# Update the purchase date
def update_purchase_date(self, json: Any | None, /) -> Any:
value = json.get('value', None) # type: ignore
try:
if value == '':
value = None
if value is not None:
value = datetime.strptime(value, '%Y/%m/%d').timestamp()
except Exception:
raise ErrorException('{value} is not a date'.format(
value=value,
))
self.fields.purchase_date = value
rows, _ = BrickSQL().execute_and_commit(
self.update_purchase_date_query,
parameters=self.sql_parameters()
)
if rows != 1:
raise DatabaseException('Could not update the purchase date for set {set} ({id})'.format( # noqa: E501
set=self.fields.set,
id=self.fields.id,
))
# Info
logger.info('Purchase date changed to "{value}" for set {set} ({id})'.format( # noqa: E501
value=value,
set=self.fields.set,
id=self.fields.id,
))
return value
# Update the purchase price
def update_purchase_price(self, json: Any | None, /) -> Any:
value = json.get('value', None) # type: ignore
try:
if value == '':
value = None
if value is not None:
value = float(value)
except Exception:
raise ErrorException('{value} is not a number or empty'.format(
value=value,
))
self.fields.purchase_price = value
rows, _ = BrickSQL().execute_and_commit(
self.update_purchase_price_query,
parameters=self.sql_parameters()
)
if rows != 1:
raise DatabaseException('Could not update the purchase price for set {set} ({id})'.format( # noqa: E501
set=self.fields.set,
id=self.fields.id,
))
# Info
logger.info('Purchase price changed to "{value}" for set {set} ({id})'.format( # noqa: E501
value=value,
set=self.fields.set,
id=self.fields.id,
))
return value
# Self url # Self url
def url(self, /) -> str: def url(self, /) -> str:
return url_for('set.details', id=self.fields.id) return url_for('set.details', id=self.fields.id)
@ -230,3 +331,11 @@ class BrickSet(RebrickableSet):
return url_for('storage.details', id=self.fields.storage) return url_for('storage.details', id=self.fields.storage)
else: else:
return '' return ''
# Update purchase date url
def url_for_purchase_date(self, /) -> str:
return url_for('set.update_purchase_date', id=self.fields.id)
# Update purchase price url
def url_for_purchase_price(self, /) -> str:
return url_for('set.update_purchase_price', id=self.fields.id)

View File

@ -27,7 +27,7 @@ CREATE TABLE "bricktracker_sets" (
"set" TEXT NOT NULL, "set" TEXT NOT NULL,
"description" TEXT, "description" TEXT,
"storage" TEXT, -- Storage bin location "storage" TEXT, -- Storage bin location
"purchase_date" INTEGER, -- Purchase data "purchase_date" REAL, -- Purchase data
"purchase_location" TEXT, -- Purchase location "purchase_location" TEXT, -- Purchase location
"purchase_price" REAL, -- Purchase price "purchase_price" REAL, -- Purchase price
PRIMARY KEY("id"), PRIMARY KEY("id"),

View File

@ -1,7 +1,9 @@
SELECT SELECT
{% block id %}{% endblock %} {% block id %}{% endblock %}
"bricktracker_sets"."storage", "bricktracker_sets"."storage",
"bricktracker_sets"."purchase_date",
"bricktracker_sets"."purchase_location", "bricktracker_sets"."purchase_location",
"bricktracker_sets"."purchase_price",
"rebrickable_sets"."set", "rebrickable_sets"."set",
"rebrickable_sets"."number", "rebrickable_sets"."number",
"rebrickable_sets"."version", "rebrickable_sets"."version",

View File

@ -0,0 +1,3 @@
UPDATE "bricktracker_sets"
SET "purchase_date" = :purchase_date
WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :id

View File

@ -0,0 +1,3 @@
UPDATE "bricktracker_sets"
SET "purchase_price" = :purchase_price
WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :id

View File

@ -41,6 +41,18 @@ def list() -> str:
) )
# Change the value of purchase date
@set_page.route('/<id>/purchase_date', methods=['POST'])
@login_required
@exception_handler(__file__, json=True)
def update_purchase_date(*, id: str) -> Response:
brickset = BrickSet().select_light(id)
value = brickset.update_purchase_date(request.json)
return jsonify({'value': value})
# Change the value of purchase location # Change the value of purchase location
@set_page.route('/<id>/purchase_location', methods=['POST']) @set_page.route('/<id>/purchase_location', methods=['POST'])
@login_required @login_required
@ -60,6 +72,18 @@ def update_purchase_location(*, id: str) -> Response:
return jsonify({'value': value}) return jsonify({'value': value})
# Change the value of purchase price
@set_page.route('/<id>/purchase_price', methods=['POST'])
@login_required
@exception_handler(__file__, json=True)
def update_purchase_price(*, id: str) -> Response:
brickset = BrickSet().select_light(id)
value = brickset.update_purchase_price(request.json)
return jsonify({'value': value})
# Change the state of a owner # Change the state of a owner
@set_page.route('/<id>/owner/<metadata_id>', methods=['POST']) @set_page.route('/<id>/owner/<metadata_id>', methods=['POST'])
@login_required @login_required

View File

@ -21,6 +21,7 @@ It also uses the following libraries and frameworks:
- `tinysort` (https://github.com/Sjeiti/TinySort) - `tinysort` (https://github.com/Sjeiti/TinySort)
- `sortable` (https://github.com/tofsjonas/sortable) - `sortable` (https://github.com/tofsjonas/sortable)
- `simple-datatables` (https://github.com/fiduswriter/simple-datatables) - `simple-datatables` (https://github.com/fiduswriter/simple-datatables)
- `vanillajs-datepicker` (https://github.com/mymth/vanillajs-datepicker)
The BrickTracker brick logo is part of the Small n' Flat Icons set designed by [Arnaud Chesne](https://iconduck.com/designers/arnaud-chesne). The BrickTracker brick logo is part of the Small n' Flat Icons set designed by [Arnaud Chesne](https://iconduck.com/designers/arnaud-chesne).

View File

@ -1,5 +1,6 @@
// Generic state changer with visual feedback // Generic state changer with visual feedback
// Tooltips require boostrap.Tooltip // Tooltips requires boostrap.Tooltip
// Date requires vanillajs-datepicker
class BrickChanger { class BrickChanger {
constructor(prefix, id, url, parent = undefined) { constructor(prefix, id, url, parent = undefined) {
this.prefix = prefix this.prefix = prefix
@ -51,6 +52,20 @@ class BrickChanger {
changer.change(); changer.change();
})(this)); })(this));
} }
// Date picker
this.picker = undefined;
if (this.html_element.dataset.changerDate == "true") {
this.picker = new Datepicker(this.html_element, {
buttonClass: 'btn',
format: 'yyyy/mm/dd',
});
// Picker fires a custom "changeDate" event
this.html_element.addEventListener("changeDate", ((changer) => (e) => {
changer.change();
})(this));
}
} }
// Clean the status // Clean the status

View File

@ -9,6 +9,7 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.12.0/baguetteBox.css" integrity="sha512-VZ783G3QIpxXpg7tWpzHn+XhjsOCIxFYoSWmyipKCB41OYaB9i4brxAWuY1c8gGCSqKo7uvckzPJhYcdBZQ9gg==" crossorigin="anonymous" referrerpolicy="no-referrer"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.12.0/baguetteBox.css" integrity="sha512-VZ783G3QIpxXpg7tWpzHn+XhjsOCIxFYoSWmyipKCB41OYaB9i4brxAWuY1c8gGCSqKo7uvckzPJhYcdBZQ9gg==" crossorigin="anonymous" referrerpolicy="no-referrer">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-datatables@9.2.1/dist/style.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-datatables@9.2.1/dist/style.min.css">
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.6.0/fonts/remixicon.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/remixicon@4.6.0/fonts/remixicon.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.3.4/dist/css/datepicker-bs5.min.css">
<link href="{{ url_for('static', filename='styles.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='styles.css') }}" rel="stylesheet">
<link rel="icon" type="image/png" sizes="48x48" href="{{ url_for('static', filename='brick.png') }}"> <link rel="icon" type="image/png" sizes="48x48" href="{{ url_for('static', filename='brick.png') }}">
</head> </head>
@ -78,6 +79,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.12.0/baguetteBox.min.js" integrity="sha512-HzIuiABxntLbBS8ClRa7drXZI3cqvkAZ5DD0JCAkmRwUtykSGqzA9uItHivDhRUYnW3MMyY5xqk7qVUHOEMbMA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.12.0/baguetteBox.min.js" integrity="sha512-HzIuiABxntLbBS8ClRa7drXZI3cqvkAZ5DD0JCAkmRwUtykSGqzA9uItHivDhRUYnW3MMyY5xqk7qVUHOEMbMA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.8.1/socket.io.min.js" integrity="sha512-8ExARjWWkIllMlNzVg7JKq9RKWPlJABQUNq6YvAjE/HobctjH/NA+bSiDMDvouBVjp4Wwnf1VP1OEv7Zgjtuxw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.8.1/socket.io.min.js" integrity="sha512-8ExARjWWkIllMlNzVg7JKq9RKWPlJABQUNq6YvAjE/HobctjH/NA+bSiDMDvouBVjp4Wwnf1VP1OEv7Zgjtuxw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@9.2.1/dist/umd/simple-datatables.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/simple-datatables@9.2.1/dist/umd/simple-datatables.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.3.4/dist/js/datepicker-full.min.js"></script>
<!-- BrickTracker scripts --> <!-- BrickTracker scripts -->
<script src="{{ url_for('static', filename='scripts/changer.js') }}"></script> <script src="{{ url_for('static', filename='scripts/changer.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/grid/filter.js') }}"></script> <script src="{{ url_for('static', filename='scripts/grid/filter.js') }}"></script>

View File

@ -65,6 +65,15 @@
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro purchase_date(date, solo=false, last=false) %}
{% if last %}
{% set tooltip=date %}
{% else %}
{% set text=date %}
{% endif %}
{{ badge(check=date, solo=solo, last=last, color='light border', icon='calendar-line', text=text, tooltip=tooltip, collapsible='Date:') }}
{% endmacro %}
{% macro purchase_location(item, purchase_locations, solo=false, last=false) %} {% macro purchase_location(item, purchase_locations, solo=false, last=false) %}
{% if purchase_locations and item.fields.purchase_location in purchase_locations.mapping %} {% if purchase_locations and item.fields.purchase_location in purchase_locations.mapping %}
{% set purchase_location = purchase_locations.mapping[item.fields.purchase_location] %} {% set purchase_location = purchase_locations.mapping[item.fields.purchase_location] %}
@ -73,10 +82,19 @@
{% else %} {% else %}
{% set text=purchase_location.fields.name %} {% set text=purchase_location.fields.name %}
{% endif %} {% endif %}
{{ badge(check=purchase_location, solo=solo, last=last, color='light border', icon='building-line', text=text, tooltip=tooltip) }} {{ badge(check=purchase_location, solo=solo, last=last, color='light border', icon='building-line', text=text, tooltip=tooltip, collapsible='Location:') }}
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro purchase_price(price, solo=false, last=false) %}
{% if last %}
{% set tooltip=price %}
{% else %}
{% set text=price %}
{% endif %}
{{ badge(check=price, solo=solo, last=last, color='light border', icon='wallet-3-line', text=text, tooltip=tooltip, collapsible='Price:') }}
{% endmacro %}
{% macro set(set, solo=false, last=false, url=None, id=None) %} {% macro set(set, solo=false, last=false, url=None, id=None) %}
{% if id %} {% if id %}
{% set url=url_for('set.details', id=id) %} {% set url=url_for('set.details', id=id) %}

View File

@ -17,19 +17,22 @@
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro input(name, id, prefix, url, value, all=none, read_only=none) %} {% macro input(name, id, prefix, url, value, all=none, read_only=none, icon=none, suffix=none, date=false) %}
{% if all or read_only %} {% if all or read_only %}
{{ value }} {{ value }}
{% else %} {% else %}
<label class="visually-hidden" for="{{ prefix }}-{{ id }}">{{ name }}</label> <label class="visually-hidden" for="{{ prefix }}-{{ id }}">{{ name }}</label>
<div class="input-group"> <div class="input-group">
{% if icon %}<span class="input-group-text"><i class="ri-{{ icon }} me-1"></i><span class="ms-1 d-none d-md-inline"> {{ name }}</span></span>{% endif %}
<input class="form-control form-control-sm flex-shrink-1" type="text" id="{{ prefix }}-{{ id }}" value="{% if value %}{{ value }}{% endif %}" <input class="form-control form-control-sm flex-shrink-1" type="text" id="{{ prefix }}-{{ id }}" value="{% if value %}{{ value }}{% endif %}"
{% if g.login.is_authenticated() %} {% if g.login.is_authenticated() %}
data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}" data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}"
{% if date %}data-changer-date="true"{% endif %}
{% else %} {% else %}
disabled disabled
{% endif %} {% endif %}
autocomplete="off"> autocomplete="off">
{% if suffix %}<span class="input-group-text d-none d-md-inline">{{ suffix }}</span>{% endif %}
{% if g.login.is_authenticated() %} {% if g.login.is_authenticated() %}
<span id="status-{{ prefix }}-{{ id }}" class="input-group-text ri-save-line"></span> <span id="status-{{ prefix }}-{{ id }}" class="input-group-text ri-save-line"></span>
<button id="clear-{{ prefix }}-{{ id }}" type="button" class="btn btn-sm btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button> <button id="clear-{{ prefix }}-{{ id }}" type="button" class="btn btn-sm btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
@ -45,7 +48,7 @@
{% set prefix=metadata_list.as_prefix() %} {% set prefix=metadata_list.as_prefix() %}
<label class="visually-hidden" for="{{ prefix }}-{{ item.fields.id }}">{{ name }}</label> <label class="visually-hidden" for="{{ prefix }}-{{ item.fields.id }}">{{ name }}</label>
<div class="input-group"> <div class="input-group">
{% if icon %}<span class="input-group-text"><i class="ri-{{ icon }}"></i></span>{% endif %} {% if icon %}<span class="input-group-text"><i class="ri-{{ icon }} me-1"></i><span class="ms-1 d-none d-md-inline"> {{ name }}</span></span>{% endif %}
<select id="{{ prefix }}-{{ item.fields.id }}" class="form-select" <select id="{{ prefix }}-{{ item.fields.id }}" class="form-select"
{% if not delete %} {% if not delete %}
data-changer-id="{{ item.fields.id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ metadata_list.url_for_set_value(item.fields.id) }}" data-changer-id="{{ item.fields.id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ metadata_list.url_for_set_value(item.fields.id) }}"

View File

@ -17,6 +17,8 @@
{% if not config['HIDE_TABLE_DAMAGED_PARTS'] %} {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
data-has-damaged="{{ (item.fields.total_damaged > 0) | int }}" data-damaged="{{ item.fields.total_damaged }}" data-has-damaged="{{ (item.fields.total_damaged > 0) | int }}" data-damaged="{{ item.fields.total_damaged }}"
{% endif %} {% endif %}
{% if item.fields.purchase_date is not none %}data-purchase-date="{{ item.fields.purchase_date }}"{% endif %}
{% if item.fields.purchase_price is not none %}data-purchase-price="{{ item.fields.purchase_price }}"{% endif %}
data-has-purchase-location="{{ item.fields.purchase_location is not none | int }}" data-has-purchase-location="{{ item.fields.purchase_location is not none | int }}"
{% if item.fields.purchase_location is not none %} {% if item.fields.purchase_location is not none %}
data-purchase-location="{{ item.fields.purchase_location }}" data-purchase-location="{{ item.fields.purchase_location }}"
@ -68,8 +70,10 @@
{{ badge.owner(item, owner, solo=solo, last=last) }} {{ badge.owner(item, owner, solo=solo, last=last) }}
{% endfor %} {% endfor %}
{{ badge.storage(item, brickset_storages, solo=solo, last=last) }} {{ 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 last %}
{{ badge.purchase_date(item.purchase_date(), solo=solo, last=last) }}
{{ badge.purchase_location(item, brickset_purchase_locations, solo=solo, last=last) }}
{{ badge.purchase_price(item.purchase_price(), solo=solo, last=last) }}
{% if not solo %} {% if not solo %}
{{ badge.instructions(item, solo=solo, last=last) }} {{ badge.instructions(item, solo=solo, last=last) }}
{% endif %} {% endif %}

View File

@ -1,5 +1,5 @@
{% if g.login.is_authenticated() %} {% if g.login.is_authenticated() %}
{{ accordion.header('Management', 'set-management', 'set-details', icon='settings-4-line', class='p-0') }} {{ accordion.header('Management', 'set-management', 'set-details', icon='settings-4-line', class='p-0', expanded=true) }}
{{ accordion.header('Owners', 'owner', 'set-management', icon='group-line', class='p-0') }} {{ accordion.header('Owners', 'owner', 'set-management', icon='group-line', class='p-0') }}
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
{% if brickset_owners | length %} {% if brickset_owners | length %}
@ -14,12 +14,23 @@
<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> <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> </div>
{{ accordion.footer() }} {{ accordion.footer() }}
{{ accordion.header('Purchase location', 'purchase-location', 'set-management', icon='building-line') }} {{ accordion.header('Purchase', 'purchase', 'set-management', icon='wallet-3-line', expanded=true) }}
<div class="alert alert-info" role="alert">The expected date format here is <code>yyyy/mm/dd</code> (year/month/day), but you can configured how it is displayed in the set card with the <code>PURCHASE_DATE_FORMAT</code> variable.</div>
<div class="row row-cols-lg-auto g-1 justify-content-start align-items-center pb-2">
<div class="col-12">
{{ form.input('Date', item.fields.id, 'date', item.url_for_purchase_date(), item.purchase_date(standard=true), icon='calendar-line', date=true) }}
</div>
<div class="col-12 flex-grow-1">
{{ form.input('Price', item.fields.id, 'price', item.url_for_purchase_price(), item.fields.purchase_price, suffix=config['PURCHASE_CURRENCY'], icon='wallet-3-line') }}
</div>
<div class="col-12 flex-grow-1">
{% if brickset_purchase_locations | length %} {% if brickset_purchase_locations | length %}
{{ form.select('Purchase location', item, 'purchase_location', brickset_purchase_locations, delete=delete) }} {{ form.select('Location', item, 'purchase_location', brickset_purchase_locations, icon='building-line', delete=delete) }}
{% else %} {% else %}
<p class="text-center"><i class="ri-error-warning-line"></i> No purchase location found.</p> <i class="ri-error-warning-line"></i> No purchase location found.
{% endif %} {% endif %}
</div>
</div>
<hr> <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> <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.footer() }}

View File

@ -22,6 +22,10 @@
<button id="sort-damaged" type="button" class="btn btn-outline-primary mb-2" <button id="sort-damaged" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="damaged" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xl-inline"> Damaged</span></button> data-sort-attribute="damaged" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xl-inline"> Damaged</span></button>
{% endif %} {% endif %}
<button id="sort-purchase-date" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="purchase-date" data-sort-desc="true"><i class="ri-calendar-line"></i><span class="d-none d-xl-inline"> Date</span></button>
<button id="sort-purchase-price" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="purchase-price" data-sort-desc="true"><i class="ri-wallet-3-line"></i><span class="d-none d-xl-inline"> Price</span></button>
<button id="sort-clear" type="button" class="btn btn-outline-dark mb-2" <button id="sort-clear" type="button" class="btn btn-outline-dark mb-2"
data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xl-inline"> Clear</span></button> data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xl-inline"> Clear</span></button>
</div> </div>