Files
Inventarsystem/Web/modules/database/items.py
T

846 lines
26 KiB
Python
Executable File

"""
Inventory Items Management
=========================
This module manages inventory items in the database. It provides comprehensive
functionality for creating, updating, retrieving and filtering inventory items.
Key Features:
- Creating and updating inventory items
- Retrieving items by ID, name, or filters
- Managing item availability status
- Supporting images and categorization
- Retrieving filter/category values for UI components
Collection Structure:
- items: Stores all inventory item records with their metadata
- Required fields: Name, Ort, Beschreibung
- Optional fields: Images, Filter, Filter2, Filter3, Anschaffungsjahr, Anschaffungskosten, Code_4
- Status fields: Verfuegbar, User (if currently borrowed)
"""
from bson.objectid import ObjectId
from bson.errors import InvalidId
import datetime
import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient
import Web.modules.inventarsystem.data_protection as dp
def safe_decrypt_user(encrypted_user):
"""
Safely decrypt an encrypted username string.
Returns the original string if decryption fails or if input is empty/None.
"""
if not encrypted_user:
return encrypted_user
try:
return dp.decrypt_text(encrypted_user)
except Exception as e:
print(f"Error decrypting user data: {e}")
# Return fallback value or None to prevent downstream crashes
return "[Decryption Failed]"
def decrypt_item_user_data(item):
"""
Decrypts encrypted user fields within an inventory item document in-place.
Args:
item (dict): MongoDB document representing an item.
Returns:
dict: The item with decrypted user fields.
"""
if not item:
return item
# 1. Decrypt top-level 'User' field if present
if 'User' in item and item['User']:
item['User'] = safe_decrypt_user(item['User'])
# 2. Decrypt nested 'user' field in 'NextAppointment' if present
if 'NextAppointment' in item and isinstance(item['NextAppointment'], dict):
if 'user' in item['NextAppointment']:
item['NextAppointment']['user'] = safe_decrypt_user(item['NextAppointment']['user'])
return item
def _to_object_id(id_str):
"""Safely convert a string to ObjectId."""
try:
return ObjectId(id_str)
except (InvalidId, TypeError):
return None
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'other', 'schoolbook', 'Buch', 'Schulbuch', 'schulbuch')
def _non_library_query(extra_query=None):
"""Build a query that excludes library media items from normal inventory."""
base_query = {'ItemType': {'$nin': list(LIBRARY_ITEM_TYPES)}}
if extra_query:
base_query.update(extra_query)
return base_query
def _active_record_query(extra_query=None):
"""Build a query that excludes logically deleted records."""
base_query = {'Deleted': {'$ne': True}}
if extra_query:
base_query.update(extra_query)
return base_query
# === ITEM MANAGEMENT ===
def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, filter3=None,
ansch_jahr=None, ansch_kost=None, code_4=None, reservierbar=True,
series_group_id=None, series_count=1, series_position=1,
is_grouped_sub_item=False, parent_item_id=None,
isbn=None, item_type='general', library_category=None, is_library=False):
"""
Add a new item to the inventory.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
if images is None:
images = []
item = {
'Name': name,
'Ort': ort,
'Beschreibung': beschreibung,
'Images': images,
'Verfuegbar': True,
'Reservierbar': reservierbar,
'Filter': filter,
'Filter2': filter2,
'Filter3': filter3,
'Anschaffungsjahr': ansch_jahr,
'Anschaffungskosten': ansch_kost,
'Code_4': code_4,
'ISBN': isbn,
'library_category': library_category,
'ItemType': item_type,
'is_library': is_library,
'SeriesGroupId': series_group_id,
'SeriesCount': series_count,
'SeriesPosition': series_position,
'IsGroupedSubItem': is_grouped_sub_item,
'ParentItemId': parent_item_id,
'Created': datetime.datetime.now(),
'LastUpdated': datetime.datetime.now()
}
result = items.insert_one(item)
return result.inserted_id
except Exception as e:
print(f"Error adding item: {e}")
return None
def remove_item(id):
"""
Soft-delete an item from the inventory.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
result = items.update_one(
{'_id': ObjectId(id), 'Deleted': {'$ne': True}},
{'$set': {
'Deleted': True,
'DeletedAt': datetime.datetime.now(),
'LastUpdated': datetime.datetime.now(),
'Verfuegbar': False,
}}
)
return result.modified_count > 0
except Exception as e:
print(f"Error removing item: {e}")
return False
def get_group_item_ids(id):
"""
Resolve all item ids that belong to the same grouped series as the given item.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
base_item = items.find_one(_active_record_query({'_id': ObjectId(id)}))
if not base_item:
return []
resolved_ids = set()
series_group_id = base_item.get('SeriesGroupId')
if series_group_id:
for group_item in items.find(_active_record_query({'SeriesGroupId': series_group_id}), {'_id': 1}):
resolved_ids.add(str(group_item['_id']))
else:
resolved_ids.add(str(base_item['_id']))
parent_item_id = base_item.get('ParentItemId')
if parent_item_id:
resolved_ids.add(str(parent_item_id))
for sibling in items.find(_active_record_query({'ParentItemId': str(parent_item_id), 'IsGroupedSubItem': True}), {'_id': 1}):
resolved_ids.add(str(sibling['_id']))
else:
for child in items.find(_active_record_query({'ParentItemId': str(base_item['_id']), 'IsGroupedSubItem': True}), {'_id': 1}):
resolved_ids.add(str(child['_id']))
return list(resolved_ids)
except Exception as e:
print(f"Error resolving group item IDs: {e}")
return []
def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter2, filter3,
ansch_jahr, ansch_kost, code_4, reservierbar, isbn=None, item_type='general'):
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
old_item = items.find_one({'_id': ObjectId(id)})
if not old_item:
return False
series_group_id = old_item.get('SeriesGroupId')
shared_update = {
'Name': name,
'Ort': ort,
'Beschreibung': beschreibung,
'Images': images,
'Filter': filter1,
'Filter2': filter2,
'Filter3': filter3,
'Anschaffungsjahr': ansch_jahr,
'Anschaffungskosten': ansch_kost,
'Reservierbar': reservierbar,
'ISBN': isbn,
'ItemType': item_type,
'Verfuegbar': verfuegbar,
'LastUpdated': datetime.datetime.now()
}
specific_update = shared_update.copy()
specific_update['Code_4'] = code_4
items.update_one({'_id': ObjectId(id)}, {'$set': specific_update})
if series_group_id:
items.update_many(
{
'SeriesGroupId': series_group_id,
'_id': {'$ne': ObjectId(id)}
},
{'$set': shared_update}
)
return True
except Exception as e:
print(f"Error updating item: {e}")
return False
def update_item_status(id, verfuegbar, user=None):
"""
Update the availability status of an inventory item.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
update_data = {
'Verfuegbar': verfuegbar,
'LastUpdated': datetime.datetime.now()
}
update_query = {'$set': update_data}
if user is not None:
update_data['User'] = dp.encrypt_text(user)
elif verfuegbar:
update_query['$unset'] = {'User': ""}
result = items.update_one(
{'_id': ObjectId(id)},
update_query
)
return result.modified_count > 0
except Exception as e:
print(f"Error updating item status: {e}")
return False
def update_item_exemplare_status(id, exemplare_status):
"""
Update the exemplar status of an inventory item.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
update_data = {
'ExemplareStatus': exemplare_status,
'LastUpdated': datetime.datetime.now()
}
result = items.update_one(
{'_id': ObjectId(id)},
{'$set': update_data}
)
return result.modified_count > 0
except Exception as e:
print(f"Error updating exemplar status: {e}")
return False
def is_code_unique(code_4, exclude_id=None):
"""
Check if a given code is unique (not used by any other item).
"""
if not code_4 or code_4.strip() == "":
return False
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
query = {'Code_4': code_4, 'Deleted': {'$ne': True}}
if exclude_id:
query['_id'] = {'$ne': ObjectId(exclude_id)}
count = items.count_documents(query)
return count == 0
# === ITEM RETRIEVAL ===
def get_items():
"""
Retrieve all inventory items.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
items_return = items.find(_active_record_query(_non_library_query()))
items_list = []
for item in items_return:
item['_id'] = str(item['_id'])
items_list.append(item)
return items_list
except Exception as e:
print(f"Error retrieving items: {e}")
return []
def get_available_items():
"""
Retrieve all available inventory items.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
items_return = items.find(_active_record_query(_non_library_query({'Verfuegbar': True})))
items_list = []
for item in items_return:
item['_id'] = str(item['_id'])
items_list.append(item)
return items_list
except Exception as e:
print(f"Error retrieving available items: {e}")
return []
def get_borrowed_items():
"""
Retrieve all currently borrowed inventory items.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
items_return = items.find(_active_record_query(_non_library_query({'Verfuegbar': False})))
items_list = []
for item in items_return:
item['_id'] = str(item['_id'])
items_list.append(item)
return items_list
except Exception as e:
print(f"Error retrieving borrowed items: {e}")
return []
def get_item(id, decrypt=True):
"""
Retrieve an inventory item by ID, with optional decryption.
"""
item_id = _to_object_id(id)
if not item_id:
return None
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
query = _active_record_query({'_id': item_id})
item = items.find_one(query)
if item:
item['_id'] = str(item['_id'])
if decrypt:
decrypt_item_user_data(item)
return item
except Exception as e:
print(f"Error retrieving item {id}: {e}")
return None
def get_item_by_name(name):
"""
Retrieve a specific inventory item by its name.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
item = items.find_one(_active_record_query(_non_library_query({'Name': name})))
return item
except Exception as e:
print(f"Error retrieving item by name: {e}")
return None
def get_items_by_filter(filter_value):
"""
Retrieve inventory items matching a specific filter/category.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
query = _active_record_query(_non_library_query({
'$or': [
{'Filter': filter_value},
{'Filter2': filter_value},
{'Filter3': filter_value}
]
}))
results = list(items.find(query))
for item in results:
item['_id'] = str(item['_id'])
return results
except Exception as e:
print(f"Error retrieving items by filter: {e}")
return []
def get_filters():
"""
Retrieve all unique filter/category values from the inventory.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
non_library = _active_record_query(_non_library_query())
filters = items.distinct('Filter', non_library)
filters2 = items.distinct('Filter2', non_library)
filters3 = items.distinct('Filter3', non_library)
all_filters = [f for f in filters + filters2 + filters3 if f]
unique_filters = []
for f in all_filters:
if f not in unique_filters:
unique_filters.append(f)
return unique_filters
except Exception as e:
print(f"Error retrieving filters: {e}")
return []
def get_primary_filters():
"""Retrieve all unique primary filter values."""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
filters = [f for f in items.distinct('Filter', _active_record_query(_non_library_query())) if f]
predefined = get_predefined_filter_values(1)
return sorted(list(set(filters + predefined)))
except Exception as e:
print(f"Error retrieving primary filters: {e}")
return []
def get_secondary_filters():
"""Retrieve all unique secondary filter values."""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
filters = [f for f in items.distinct('Filter2', _active_record_query(_non_library_query())) if f]
predefined = get_predefined_filter_values(2)
return sorted(list(set(filters + predefined)))
except Exception as e:
print(f"Error retrieving secondary filters: {e}")
return []
def get_tertiary_filters():
"""Retrieve all unique tertiary filter values."""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
filters = [f for f in items.distinct('Filter3', _active_record_query(_non_library_query())) if f]
predefined = get_predefined_filter_values(3)
return sorted(list(set(filters + predefined)))
except Exception as e:
print(f"Error retrieving tertiary filters: {e}")
return []
def get_item_by_code_4(code_4):
"""
Retrieve inventory items matching a specific 4-digit code.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
results = list(items.find(_active_record_query(_non_library_query({"Code_4": code_4}))))
for item in results:
item['_id'] = str(item['_id'])
return results
except Exception as e:
print(f"Error retrieving item by code: {e}")
return []
# === MAINTENANCE FUNCTIONS ===
def unstuck_item(id):
"""
Remove all borrowing records for a specific item to reset its status.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
ausleihungen = db['ausleihungen']
ausleihungen.update_many(
{'Item': id, 'Status': {'$nin': ['cancelled', 'deleted']}},
{'$set': {
'Status': 'cancelled',
'CancelledReason': 'unstuck_reset',
'LastUpdated': datetime.datetime.now()
}}
)
items = db['items']
items.update_one(
{'_id': ObjectId(id)},
{
'$set': {
'Verfuegbar': True,
'LastUpdated': datetime.datetime.now()
},
'$unset': {'User': ""}
}
)
return True
except Exception as e:
print(f"Error unsticking item: {e}")
return False
def get_predefined_filter_values(filter_num):
"""
Get predefined values for a specific filter.
"""
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
filter_presets = db['filter_presets']
filter_doc = filter_presets.find_one({'filter_num': filter_num})
if filter_doc and 'values' in filter_doc:
return sorted(filter_doc['values'])
else:
filter_presets.update_one(
{'filter_num': filter_num},
{'$set': {'values': []}},
upsert=True
)
return []
def add_predefined_filter_value(filter_num, value):
"""
Add a new predefined value to a filter.
"""
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
filter_presets = db['filter_presets']
filter_doc = filter_presets.find_one({
'filter_num': filter_num,
'values': value
})
if filter_doc:
return False
result = filter_presets.update_one(
{'filter_num': filter_num},
{'$push': {'values': value}},
upsert=True
)
return result.modified_count > 0 or result.upserted_id is not None
def remove_predefined_filter_value(filter_num, value):
"""
Remove a predefined value from a filter.
"""
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
filter_presets = db['filter_presets']
result = filter_presets.update_one(
{'filter_num': filter_num},
{'$pull': {'values': value}}
)
return result.modified_count > 0
def edit_predefined_filter_value(filter_num, old_value, new_value):
"""
Edit a predefined value from a filter and update all matching items.
"""
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
filter_presets = db['filter_presets']
existing = filter_presets.find_one({
'filter_num': filter_num,
'values': new_value
})
if existing and old_value != new_value:
return False
result = filter_presets.update_one(
{'filter_num': filter_num, 'values': old_value},
{'$set': {'values.$': new_value}}
)
if result.modified_count > 0:
items = db['items']
filter_field = 'Filter' if filter_num == 1 else f'Filter{filter_num}'
items.update_many(
{filter_field: old_value},
{'$set': {filter_field: new_value}}
)
return result.modified_count > 0
def get_filter_names():
"""Get customized filter category names."""
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
names_doc = db.settings.find_one({'setting_type': 'filter_names'})
if names_doc and 'names' in names_doc:
return names_doc['names']
return {
'1': 'Fach/Kategorie',
'2': 'System/Bereich',
'3': 'Typ/Art'
}
def set_filter_name(filter_num, name):
"""Set custom name for a filter category."""
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
names = get_filter_names()
names[str(filter_num)] = name
db.settings.update_one(
{'setting_type': 'filter_names'},
{'$set': {'names': names}},
upsert=True
)
return True
# === LOCATION MANAGEMENT ===
def get_predefined_locations():
"""
Get list of all predefined locations/placement options.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
if 'settings' not in db.list_collection_names():
db.create_collection('settings')
settings_collection = db['settings']
location_settings = settings_collection.find_one({'setting_type': 'predefined_locations'})
if not location_settings:
settings_collection.insert_one({
'setting_type': 'predefined_locations',
'locations': []
})
return []
locations = location_settings.get('locations', [])
return sorted(locations)
except Exception as e:
print(f"Error getting predefined locations: {str(e)}")
return []
def add_predefined_location(location):
"""
Add a new predefined location.
"""
if not location or not isinstance(location, str):
return False
location = location.strip()
if not location:
return False
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
settings_collection = db['settings']
location_settings = settings_collection.find_one({'setting_type': 'predefined_locations'})
if not location_settings:
settings_collection.insert_one({
'setting_type': 'predefined_locations',
'locations': [location]
})
return True
current_locations = location_settings.get('locations', [])
if any(loc.lower() == location.lower() for loc in current_locations):
return False
settings_collection.update_one(
{'setting_type': 'predefined_locations'},
{'$push': {'locations': location}}
)
return True
except Exception as e:
print(f"Error adding predefined location: {str(e)}")
return False
def remove_predefined_location(location):
"""
Remove a predefined location.
"""
if not location:
return False
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
settings_collection = db['settings']
result = settings_collection.update_one(
{'setting_type': 'predefined_locations'},
{'$pull': {'locations': location}}
)
return result.modified_count > 0
except Exception as e:
print(f"Error removing predefined location: {str(e)}")
return False
def update_item_next_appointment(item_id, appointment_data):
"""
Update an item with information about its next scheduled appointment.
"""
try:
with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client:
db = client[cfg.MONGODB_DB]
items = db['items']
if appointment_data is None:
update_query = {
'$unset': {'NextAppointment': ""},
'$set': {'LastUpdated': datetime.datetime.now()}
}
else:
data_to_save = appointment_data.copy()
if 'user' in data_to_save and data_to_save['user']:
data_to_save['user'] = dp.encrypt_text(data_to_save['user'])
update_query = {
'$set': {
'NextAppointment': data_to_save,
'LastUpdated': datetime.datetime.now()
}
}
result = items.update_one(
{'_id': ObjectId(item_id)},
update_query
)
return result.modified_count > 0
except Exception as e:
print(f"Error updating item next appointment: {e}")
return False