""" 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 uuid 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 is_library_item(item): """ Determines if an item belongs to the library system. Returns False for 'other', None, or empty ItemType (Inventory item). Returns True for any specific library type ('Buch', 'CD', 'DVD', etc.). """ if not item: return False item_type = item.get('ItemType', 'other') if not item_type: return False return str(item_type).strip().lower() != 'other' 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. Args: name (str): Name of the item ort (str): Location of the item beschreibung (str): Description of the item images (list, optional): List of image filenames for the item filter (str, optional): Primary filter/category for the item filter2 (str, optional): Secondary filter/category for the item filter3 (str, optional): Tertiary filter/category for the item ansch_jahr (int, optional): Year of acquisition ansch_kost (float, optional): Cost of acquisition code_4 (str, optional): 4-digit identification code reservierbar (bool, optional): Whether the item can be reserved in advance series_group_id (str, optional): Shared group id for same-type batch items series_count (int, optional): Total items in the created batch series_position (int, optional): Position inside the batch (1-based) is_grouped_sub_item (bool, optional): Whether this item is hidden as sub-item parent_item_id (str, optional): Parent item id if this is a sub-item isbn (str, optional): ISBN for books or media items item_type (str, optional): Type of the item (e.g., 'general', 'book', 'cd') library_category (str, optional): Library category for the item Returns: ObjectId: ID of the new item or None if failed """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] # Set default values for optional parameters 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) item_id = result.inserted_id client.close() return item_id except Exception as e: print(f"Error adding item: {e}") return None def remove_item(id): """ Soft-delete an item from the inventory. Args: id (str): ID of the item to remove Returns: bool: True if successful, False otherwise """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) 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, }} ) client.close() 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. For non-grouped items, this returns only the provided id. Args: id (str): ID of any item in the group Returns: list[str]: All related item IDs (including the parent item) """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] base_item = items.find_one(_active_record_query({'_id': ObjectId(id)})) if not base_item: client.close() return [] resolved_ids = set() # Prefer SeriesGroupId because it represents the full logical group. 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'])) client.close() 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: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) 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} ) client.close() 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. Args: id (str): ID of the item to update verfuegbar (bool): New availability status user (str, optional): Username of person who borrowed the item Returns: bool: True if successful, False otherwise """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) 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: # If item is being marked as available, clear the user field update_query['$unset'] = {'User': ""} result = items.update_one( {'_id': ObjectId(id)}, update_query ) client.close() 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. Args: id (str): ID of the item to update exemplare_status (list): List of status objects for each exemplar Returns: bool: True if successful, False otherwise """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) 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} ) client.close() 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). Args: code_4 (str): The code to check exclude_id (str, optional): ID of item to exclude from the check (for edit operations) Returns: bool: True if code is unique, False if already in use """ if not code_4 or code_4.strip() == "": # Empty codes are not considered unique return False client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] # Build query to find items with this code query = {'Code_4': code_4, 'Deleted': {'$ne': True}} # If we're editing an item, exclude it from the uniqueness check if exclude_id: query['_id'] = {'$ne': ObjectId(exclude_id)} # Check if any items with this code exist count = items.count_documents(query) client.close() return count == 0 # === ITEM RETRIEVAL === def get_items(): """ Retrieve all inventory items. Returns: list: List of all inventory item documents with string IDs """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) 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) client.close() return items_list except Exception as e: print(f"Error retrieving items: {e}") return [] def get_available_items(): """ Retrieve all available inventory items. Returns: list: List of available inventory item documents with string IDs """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) 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) client.close() 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. Returns: list: List of borrowed inventory item documents with string IDs """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) 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) client.close() 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: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) 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. Args: name (str): Name of the item to retrieve Returns: dict: The inventory item document or None if not found """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] item = items.find_one(_active_record_query({'Name': name})) client.close() 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. Args: filter_value (str): Filter value to search for Returns: list: List of items matching the filter in primary, secondary, or tertiary category """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] # Use $or to find matches in any filter field query = _active_record_query(_non_library_query({ '$or': [ {'Filter': filter_value}, {'Filter2': filter_value}, {'Filter3': filter_value} ] })) results = list(items.find(query)) client.close() # Convert ObjectId to string 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. Returns: list: Combined list of all primary, secondary and tertiary filter values """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) 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) # Combine filters and remove None/empty values all_filters = [f for f in filters + filters2 + filters3 if f] # Remove duplicates while preserving order unique_filters = [] for f in all_filters: if f not in unique_filters: unique_filters.append(f) client.close() return unique_filters except Exception as e: print(f"Error retrieving filters: {e}") return [] def get_primary_filters(): """ Retrieve all unique primary filter values. Returns: list: List of all primary filter values """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] filters = [f for f in items.distinct('Filter', _active_record_query(_non_library_query())) if f] client.close() # Add predefined values 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. Returns: list: List of all secondary filter values """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] filters = [f for f in items.distinct('Filter2', _active_record_query(_non_library_query())) if f] client.close() # Add predefined values 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. Returns: list: List of all tertiary filter values """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] filters = [f for f in items.distinct('Filter3', _active_record_query(_non_library_query())) if f] client.close() # Add predefined values 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. Args: code_4 (str): 4-digit code to search for Returns: list: List of items matching the code """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] results = list(items.find(_active_record_query(_non_library_query({"Code_4": code_4})))) # Convert ObjectId to string for item in results: item['_id'] = str(item['_id']) client.close() 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. Used to fix problematic or stuck items. Args: id (str): ID of the item to unstick Returns: bool: True if successful, False otherwise """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] ausleihungen = db['ausleihungen'] result = ausleihungen.update_many( {'Item': id, 'Status': {'$nin': ['cancelled', 'deleted']}}, {'$set': { 'Status': 'cancelled', 'CancelledReason': 'unstuck_reset', 'LastUpdated': datetime.datetime.now() }} ) # Also reset the item status items = db['items'] items.update_one( {'_id': ObjectId(id)}, { '$set': { 'Verfuegbar': True, 'LastUpdated': datetime.datetime.now() }, '$unset': {'User': ""} } ) client.close() 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. Args: filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe) Returns: list: List of predefined filter values """ client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] # Use a dedicated collection for filter presets filter_presets = db['filter_presets'] # Find the document for the specified filter filter_doc = filter_presets.find_one({'filter_num': filter_num}) client.close() if filter_doc and 'values' in filter_doc: # Sort values alphabetically return sorted(filter_doc['values']) else: # Create empty document if it doesn't exist client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] filter_presets = db['filter_presets'] filter_presets.update_one( {'filter_num': filter_num}, {'$set': {'values': []}}, upsert=True ) client.close() return [] def add_predefined_filter_value(filter_num, value): """ Add a new predefined value to a filter. Args: filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe) value (str): Value to add Returns: bool: True if value was added, False if it already existed """ client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] filter_presets = db['filter_presets'] # Check if value already exists filter_doc = filter_presets.find_one({ 'filter_num': filter_num, 'values': value }) if filter_doc: # Value already exists client.close() return False # Add the value to the filter result = filter_presets.update_one( {'filter_num': filter_num}, {'$push': {'values': value}}, upsert=True ) client.close() 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. Args: filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe) value (str): Value to remove Returns: bool: True if value was removed, False otherwise """ client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] filter_presets = db['filter_presets'] # Remove the value from the filter result = filter_presets.update_one( {'filter_num': filter_num}, {'$pull': {'values': value}} ) client.close() 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. Args: filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe) old_value (str): Value to replace new_value (str): New value Returns: bool: True if value was updated, False otherwise """ client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] filter_presets = db['filter_presets'] # Check if the new value already exists existing = filter_presets.find_one({ 'filter_num': filter_num, 'values': new_value }) if existing and old_value != new_value: client.close() return False # Update the value in the filter 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}' # Also update all items that use this filter items.update_many( {filter_field: old_value}, {'$set': {filter_field: new_value}} ) client.close() return result.modified_count > 0 def get_filter_names(): """Get customized filter category names.""" client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] names_doc = db.settings.find_one({'setting_type': 'filter_names'}) client.close() 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.""" client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) 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 ) client.close() return True # === LOCATION MANAGEMENT === def get_predefined_locations(): """ Get list of all predefined locations/placement options. Returns: list: List of predefined location strings """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] # Check if settings collection exists, create if not if 'settings' not in db.list_collection_names(): db.create_collection('settings') # Get settings document or create if it doesn't exist settings_collection = db['settings'] location_settings = settings_collection.find_one({'setting_type': 'predefined_locations'}) if not location_settings: # Create default settings document if it doesn't exist settings_collection.insert_one({ 'setting_type': 'predefined_locations', 'locations': [] }) return [] # Return the predefined locations locations = location_settings.get('locations', []) client.close() 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. Args: location (str): Location to add Returns: bool: True if added successfully, False if already exists """ if not location or not isinstance(location, str): return False location = location.strip() if not location: return False try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] settings_collection = db['settings'] # Check if settings document exists, create if not location_settings = settings_collection.find_one({'setting_type': 'predefined_locations'}) if not location_settings: # Create with the new location settings_collection.insert_one({ 'setting_type': 'predefined_locations', 'locations': [location] }) client.close() return True # Check if location already exists (case-insensitive) current_locations = location_settings.get('locations', []) if any(loc.lower() == location.lower() for loc in current_locations): client.close() return False # Add the new location settings_collection.update_one( {'setting_type': 'predefined_locations'}, {'$push': {'locations': location}} ) client.close() 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. Args: location (str): Location to remove Returns: bool: True if removed successfully """ if not location: return False try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] settings_collection = db['settings'] result = settings_collection.update_one( {'setting_type': 'predefined_locations'}, {'$pull': {'locations': location}} ) client.close() 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. Args: item_id (str): ID of the item appointment_data (dict or None): Dictionary containing appointment details (e.g., user, start_time, end_time) or None to clear it. Returns: bool: True if successful, False otherwise """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] # If clearing the appointment if appointment_data is None: update_query = { '$unset': {'NextAppointment': ""}, '$set': {'LastUpdated': datetime.datetime.now()} } else: # Create a copy so we don't mutate the original dictionary passed in data_to_save = appointment_data.copy() # Encrypt the user field if it exists to match the decryption logic at the top 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 ) client.close() return result.modified_count > 0 except Exception as e: print(f"Error updating item next appointment: {e}") return False def clear_item_next_appointment(item_id): """ Clear the next appointment information from an item. Args: item_id (str): ID of the item to update Returns: bool: True if successful, False otherwise """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] result = items.update_one( {'_id': ObjectId(item_id)}, {'$unset': {'NextAppointment': ""}, '$set': {'LastUpdated': datetime.datetime.now()}} ) client.close() return result.modified_count > 0 except Exception as e: print(f"Error clearing item next appointment: {e}") return False def get_items_with_appointments(): """ Retrieve all items that have scheduled appointments. Returns: list: List of items with NextAppointment field """ try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] items_return = items.find({'NextAppointment': {'$exists': True}, 'Deleted': {'$ne': True}}) items_list = [] for item in items_return: item['_id'] = str(item['_id']) items_list.append(item) client.close() return items_list except Exception as e: print(f"Error retrieving items with appointments: {e}") return [] def get_current_status(item_id, decrypt=True): """ Retrieve the current status of an item, decrypting the user field if present. """ oid = _to_object_id(item_id) if not oid: return None try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] item = items.find_one({'_id': oid}, {'Verfuegbar': 1, 'User': 1}) if item: item['_id'] = str(item['_id']) if decrypt: decrypt_item_user_data(item) return item return None except Exception as e: print(f"Error retrieving current status for item {item_id}: {e}") return None def sync_group_codes(primary_obj_id, base_code, individual_codes_list): """ Synchronisiert die Barcodes einer Gruppe im korrekten Schema (angelehnt an das 'Augenmodell groß'-Vorbild). """ if not base_code: return False # Alle Ziel-Codes zusammenführen (Basis-Code an erster Stelle) all_target_codes = [base_code] for c in individual_codes_list: if c and c not in all_target_codes: all_target_codes.append(c) item_count = len(all_target_codes) try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] items = db['items'] primary_item = items.find_one({'_id': ObjectId(primary_obj_id)}) if not primary_item: client.close() return False group_id = primary_item.get('SeriesGroupId') # Wenn es mehr als 1 Item gibt und noch keine Gruppe existiert -> Neue GroupID erzeugen if not group_id and item_count > 1: group_id = str(uuid.uuid4()) # Wenn es nun eine Gruppe gibt (item_count > 1) if item_count > 1: # 1. Haupt-Item (Parent) aktualisieren items.update_one( {'_id': primary_item['_id']}, {'$set': { 'Code_4': base_code, 'SeriesGroupId': group_id, 'SeriesCount': item_count, 'SeriesPosition': 1, 'IsGroupedSubItem': False, 'ParentItemId': None }} ) # Bestehende Gruppenmitglieder laden existing_items = list(items.find({'SeriesGroupId': group_id})) existing_map = {it.get('Code_4'): it for it in existing_items if it.get('Code_4') and str(it['_id']) != str(primary_item['_id'])} # Alle verbleibenden Sub-Codes ab Position 2 abarbeiten processed_sub_ids = [] for idx, code in enumerate(all_target_codes[1:], start=2): if code in existing_map: # Existiert bereits in der Gruppe -> Nur Position und Count aktualisieren sub_item = existing_map[code] processed_sub_ids.append(sub_item['_id']) items.update_one( {'_id': sub_item['_id']}, {'$set': { 'SeriesCount': item_count, 'SeriesPosition': idx, 'IsGroupedSubItem': True, 'ParentItemId': str(primary_item['_id']) }} ) else: # Neu hinzukommender Code -> Als Klon (Sub-Item) erstellen new_sub = primary_item.copy() if '_id' in new_sub: del new_sub['_id'] new_sub.update({ 'Code_4': code, 'SeriesGroupId': group_id, 'SeriesCount': item_count, 'SeriesPosition': idx, 'IsGroupedSubItem': True, 'ParentItemId': str(primary_item['_id']), 'LastUpdated': primary_item.get('LastUpdated') }) inserted_res = items.insert_one(new_sub) processed_sub_ids.append(inserted_res.inserted_id) # Nicht mehr benötigte Sub-Items aus dieser Gruppe entfernen for code, sub_item in existing_map.items(): if sub_item['_id'] not in processed_sub_ids: items.delete_one({'_id': sub_item['_id']}) else: # Fall: Nur 1 einziges Item (keine Gruppe / Gruppe aufgelöst) # Eventuelle alte Sub-Items dieser Gruppe löschen if group_id: items.delete_many({ 'SeriesGroupId': group_id, '_id': {'$ne': primary_item['_id']} }) items.update_one( {'_id': primary_item['_id']}, {'$set': { 'Code_4': base_code, 'SeriesGroupId': None, 'SeriesCount': 1, 'SeriesPosition': 1, 'IsGroupedSubItem': False, 'ParentItemId': None }} ) client.close() return True except Exception as e: print(f"Error syncing group codes: {e}") return False