def _define_menu(self, menu_item, menu=None): """ Return the menu definition for a menu. """ menus = { 'amenu': self.acct_menu, 'rmenu': self.record_menu, 'umenu': self.user_menu, 'mmenu': self.menu_menu } if menu is None: try: menu_object = menus[menu_item.lower()] except KeyError: msg = 'selected menu {} not list of available menus'.format( menu_item) logger.error(msg) return None else: menu_object = menu menu_items = [] for item in menu_object['items']: if isinstance(item, tuple): menu_items.append('{}{}'.format(*item)) elif isinstance(item, list): sub_list = [] for sub_item in item: if isinstance(sub_item, tuple): sub_list.append('{}{}'.format(*sub_item)) elif isinstance(sub_item, list): sub_sub_list = [] for sub_sub_item in sub_item: sub_sub_list.append('{}{}'.format(*sub_sub_item)) sub_list.append(sub_sub_list) menu_items.append(sub_list) menu_def = [menu_object['name'], menu_items] return menu_def
def __init__(self, name, entry): """ Arguments: name (str): cash reconciliation rule name. entry (dict): dictionary of optional and required cash reconciliation arguments. """ self.name = name self.id = randint(0, 1000000000) self.element_key = '{NAME}_{ID}'.format(NAME=name, ID=self.id) #self.elements = ['{NAME}_{ID}_{ELEM}'.format(NAME=self.name, ID=self.id, ELEM=i) for i in # ['Main', 'Cancel', 'Save', 'Next', 'FrameWidth', 'FrameHeight', 'PanelWidth', 'PanelHeight']] self.elements = [] try: self.menu_title = entry['MenuTitle'] except KeyError: self.menu_title = name try: self.menu_flags = entry['MenuFlags'] except KeyError: self.menu_flags = None try: self.permissions = entry['AccessPermissions'] except KeyError: # default permission for a cash rule is 'user' self.permissions = 'user' try: self.record_type = entry['RecordType'] except KeyError: msg = 'CashRule {RULE}: missing required parameter "RecordType"' \ .format(RULE=name) logger.error(msg) raise AttributeError(msg)
def format_tables(import_rules): """ Define the table component of query statement. """ joins = ('INNER JOIN', 'JOIN', 'LEFT JOIN', 'LEFT OUTER JOIN', 'RIGHT JOIN', 'RIGHT OUTER JOIN', 'FULL JOIN', 'FULL OUTER JOIN', 'CROSS JOIN') table_rules = [] for i, import_table in enumerate(import_rules): import_rule = import_rules[import_table] join_rule = import_rule.get('Join', None) if join_rule is None and i == 0: table_rules.append(import_table) elif join_rule is None and i > 0: logger.error( 'a join rule is required to join data from import table {TBL}'. format(TBL=import_table)) else: try: join_type, join_on = join_rule[0:2] except ValueError: logger.error( 'import table {TBL} join rule {RULE} requires three components' .format(TBL=import_table, RULE=join_rule)) continue if join_type not in joins: logger.error( 'unknown join type {JOIN} provided for import table {TBL}'. format(TBL=import_table, JOIN=join_type)) continue opt_filters = ' AND '.join(join_rule[2:]) if opt_filters: join_statement = '{JOIN} {TABLE} ON {ON} AND {OPTS}' \ .format(JOIN=join_type, TABLE=import_table, ON=join_on, OPTS=opt_filters) else: join_statement = '{JOIN} {TABLE} ON {ON}'.format( JOIN=join_type, TABLE=import_table, ON=join_on) table_rules.append(join_statement) return ' '.join(table_rules)
def format_import_filters(import_rules): """ Format filter parameters for querying. """ operators = {'=', '!=', '>', '<', '>=', '<=', 'IN', 'NOT IN'} filters = [] for import_table in import_rules: import_rule = import_rules[import_table] try: filter_rules = import_rule["Filters"] except KeyError: continue if filter_rules is None or not isinstance(filter_rules, dict): continue for filter_column in filter_rules: filter_entry = filter_rules[filter_column] try: operator = filter_entry[0].upper() except (IndexError, AttributeError): logger.error( 'the "Filters" parameter of import table {TBL} is missing the operator' .format(TBL=import_table)) continue else: if operator not in operators: logger.error( 'unknown operator {OPER} supplied the "Filters" parameters of import table {TBL}' .format(OPER=operator, TBL=import_table)) continue try: parameters = filter_entry[1:] except IndexError: logger.error( 'the "Filters" parameters of import table {TBL} requires one or more import values' .format(TBL=import_table)) continue else: if len(parameters) == 1: parameters = parameters[0] else: parameters = tuple(parameters) if isinstance(parameters, list) or isinstance(parameters, tuple): values = ['?' for _ in parameters] value = '({VALS})'.format(VALS=', '.join(values)) else: value = '?' if operator in ('IN', 'NOT IN') and 'NULL' not in parameters: filters.append(( '({TBL}.{COL} {OPER} {VAL} OR {TBL}.{COL} IS NULL)'.format( TBL=import_table, COL=filter_column, OPER=operator, VAL=value), parameters)) else: filters.append( ('{TBL}.{COL} {OPER} {VAL}'.format(TBL=import_table, COL=filter_column, OPER=operator, VAL=value), parameters)) return filters
def main(): """ Main function. """ # Theme default_col = mod_const.DEFAULT_COL action_col = mod_const.ACTION_COL text_col = mod_const.TEXT_COL font = mod_const.MAIN_FONT sg.set_options(element_padding=(0, 0), margins=(0, 0), auto_size_buttons=True, auto_size_text=True, background_color=default_col, element_text_color=text_col, element_background_color=default_col, font=font, input_text_color=text_col, text_color=text_col, text_element_background_color=default_col, input_elements_background_color=action_col, button_color=(text_col, default_col), tooltip_font=(mod_const.TOOLTIP_FONT)) # Original window size logger.debug('determining screen size') root = tk.Tk() screen_w = root.winfo_screenwidth() screen_h = root.winfo_screenheight() root.destroy() del root min_w = int(screen_w * 0.8) min_h = int(screen_h * 0.8) if screen_w >= mod_const.WIN_WIDTH: current_w = mod_const.WIN_WIDTH else: current_w = screen_w if screen_h >= mod_const.WIN_HEIGHT: current_h = mod_const.WIN_HEIGHT else: current_h = screen_h # Load the program configuration record_rules = ConfigurationManager(settings.record_rules) settings.records = record_rules audit_rules = ConfigurationManager(settings.audit_rules) cash_rules = ConfigurationManager(settings.cash_rules) bank_rules = ConfigurationManager(settings.bank_rules) acct_methods = [audit_rules, bank_rules] # Configure GUI layout toolbar = ToolBar([audit_rules, cash_rules, bank_rules], record_rules) layout = [ toolbar.layout(win_size=(current_w, current_h)), panel_layout(acct_methods, win_size=(current_w, current_h)) ] # Element keys and names audit_names = audit_rules.print_rules() cash_names = cash_rules.print_rules() bank_names = bank_rules.print_rules() # Create the menu mapper menu_mapper = {} for rule in [ i for acct_method in (audit_rules, cash_rules, bank_rules) for i in acct_method.rules ]: try: rule_submenu = rule.menu_flags except AttributeError: menu_mapper[rule.menu_title] = rule.name else: if not rule_submenu: menu_mapper[rule.menu_title] = rule.name else: for menu_title in rule_submenu: menu_mapper[menu_title] = rule.name # Event metadata current_rule = None debug_win = None # Initialize main window and login window window = sg.Window('REM Tila (v{VER})'.format(VER=__version__), layout, icon=settings.icon, font=mod_const.MAIN_FONT, size=(current_w, current_h), resizable=True, margins=(0, 0), return_keyboard_events=True) window.finalize() window.maximize() window.set_min_size((min_w, min_h)) screen_w, screen_h = window.get_screen_dimensions() logger.debug('screen size is {W} x {H}'.format(W=screen_w, H=screen_h)) user_image = tk.PhotoImage(data=mod_const.USER_ICON) userin_image = tk.PhotoImage(data=mod_const.USERIN_ICON) acct_rules = [i for acct_method in acct_methods for i in acct_method.rules] resize_panels(window, acct_rules) resized = False # Display the home panel window.refresh() home_panel = current_panel = '-HOME-' window[home_panel].update(visible=True) # Bind keyboard events window = settings.set_shortcuts(window, hk_groups=['Navigation']) for acct_rule in acct_rules: acct_rule.bind_keys(window) # Event Loop logger.info('starting the program') while True: event, values = window.read(timeout=100) # Quit program if event == sg.WIN_CLOSED or values['-MMENU-'] == 'Quit': logger.info('exiting the program') if debug_win: debug_win.close() settings.reload_logger(sys.stdout) break # Resize screen if resized and current_panel != home_panel: if current_rule is not None: if not current_rule.current_panel: resized = False continue try: window[current_rule.current_panel].update(visible=False) except KeyError: logger.error( 'unable to resize program display - current panel {PANEL} is missing from the ' 'display window'.format( PANEL=current_panel.current_rule)) print(current_rule.panel_keys) continue window[current_rule.element_key].update(visible=False) window.refresh() window[current_rule.element_key].update(visible=True) window[current_rule.current_panel].update(visible=True) resized = False continue # Resize screen # Get window dimensions try: win_w, win_h = window.size except AttributeError: continue if win_w != current_w or win_h != current_h: logger.debug('new window size is {W} x {H}'.format(W=win_w, H=win_h)) # Update sizable elements resize_panels(window, acct_rules) current_w, current_h = (win_w, win_h) resized = True continue # User login if values['-UMENU-'] == 'Sign In': # user logs on logger.debug('displaying user login screen') mod_win2.login_window() if user.logged_in is True: # logged on successfully logger.info('user signed in as "{}"'.format(user.uid)) # Switch user icon window['-UMENU-'].Widget.configure(image=userin_image) # Enable permission specific actions and menus toolbar.enable(window) # Update user menu items to include the login name toolbar.update_username(window, user.uid) else: logger.warning( 'failed to login to the program as user {}'.format( user.uid)) continue # User log-off if values['-UMENU-'] == 'Sign Out': # user signs out try: in_progress = current_rule.in_progress except AttributeError: in_progress = False # Confirm sign-out if in_progress: # ask to switch first msg = 'An audit is ongoing. Are you sure you would like to quit without saving?' selection = mod_win2.popup_confirm(msg) if selection == 'OK': # Reset the rule and update the panel current_rule = current_rule.reset_rule(window) current_panel = '-HOME-' else: continue else: # no action being taken so ok to switch without asking msg = 'Are you sure you would like to sign-out?' selection = mod_win2.popup_confirm(msg) if selection == 'Cancel': continue try: current_rule.reset_parameters(window) window[current_rule.element_key].update(visible=False) except AttributeError: pass window['-HOME-'].update(visible=True) current_panel = '-HOME-' # Remove all unsaved record IDs associated with the program instance settings.remove_unsaved_ids() # Reset User attributes logger.info('signing out as user {}'.format(user.uid)) user.logout() # Switch user icon window['-UMENU-'].Widget.configure(image=user_image) # Disable all actions and menus toolbar.disable(window) continue # Display the debug window if not debug_win and values['-MMENU-'] == 'Debug': if settings.log_file: mod_win2.popup_notice( 'The debug window is deactivated when logging to a file. Program logs are ' 'being sent to {}'.format(settings.log_file)) else: debug_win = mod_win2.debug_window() debug_win.finalize() # Reload logger with new log stream settings.reload_logger(debug_win['-OUTPUT-'].TKOut) logger.info('setting log output stream to the debug window') continue elif debug_win: debug_event, debug_value = debug_win.read(timeout=1000) if debug_event in (sg.WIN_CLOSED, '-CANCEL-'): debug_win.close() debug_win = None # Reset logging stream to stdout logger.info('resetting log output stream to system output') settings.reload_logger(sys.stdout) elif debug_event == '-CLEAR-': debug_win['-OUTPUT-'].update('') elif debug_event == '-LEVEL-': # Reload logger with new log level log_level = debug_value['-LEVEL-'] logger.info('resetting logging level to {}'.format(log_level)) settings.reload_logger(debug_win['-OUTPUT-'].TKOut, log_level=log_level) else: debug_win['-OUTPUT-'].expand(expand_x=True, expand_y=True, expand_row=True) # Display the edit settings window if values['-MMENU-'] == 'Settings': mod_win2.edit_settings(win_size=window.size) continue # Display "About" window if values['-MMENU-'] == 'About': mod_win2.about() continue # Display the database update window if event == '-DBMENU-': try: mod_win2.database_importer_window( win_size=window.get_screen_size()) except Exception as e: logger.exception( 'importing records to the database failed - {ERR}'.format( ERR=e)) continue # Pull up an existing database record if event == '-RMENU-': # Get Record Type selection record_type = values['-RMENU-'] logger.info( 'displaying selection window for {TYPE} records'.format( TYPE=record_type)) # Get record entry record_entry = settings.records.fetch_rule(record_type, by_title=True) # Display the import record window table_entry = record_entry.import_table table_entry['RecordType'] = record_entry.name import_table = mod_elem.RecordTable(record_entry.name, table_entry) try: mod_win2.record_import_window(import_table, enable_new=False) except Exception as e: msg = 'record importing failed - {ERR}'.format(ERR=e) mod_win2.popup_error(msg) logger.exception(msg) # raise continue # Activate appropriate accounting workflow method panel selected_menu = values['-AMENU-'] if selected_menu in menu_mapper: selected_rule = menu_mapper[selected_menu] if selected_rule in audit_names: # workflow method is a transaction audit # Obtain the selected rule object current_rule = audit_rules.fetch_rule(selected_rule) # Clear the panel current_rule.reset_rule(window, current=True) # Update panel-in-display window[current_panel].update(visible=False) current_panel = current_rule.element_key window[current_panel].update(visible=True) logger.debug( 'panel in view is {NAME}'.format(NAME=current_rule.name)) # Disable the toolbar toolbar.disable(window) continue elif selected_rule in cash_names: # workflow method is cash reconciliation # Obtain the selected rule object current_rule = cash_rules.fetch_rule(selected_rule) # Get the record entry record_type = current_rule.record_type record_entry = settings.records.fetch_rule(record_type) if not record_entry: msg = 'unable to find a configured record type with name {NAME}'.format( NAME=record_type) logger.warning(msg) continue else: logger.debug('the record type selected is {TYPE}'.format( TYPE=record_type)) # Display the import record window table_entry = record_entry.import_table table_entry['RecordType'] = record_type import_table = mod_elem.RecordTable(current_rule.name, table_entry) try: mod_win2.record_import_window(import_table, enable_new=True) except Exception as e: msg = 'record importing failed - {ERR}'.format(ERR=e) mod_win2.popup_error(msg) logger.exception(msg) continue elif selected_rule in bank_names: # workflow method is bank reconciliation # Obtain the selected rule object current_rule = bank_rules.fetch_rule(selected_rule) # Use the menu flag to find the primary account #try: # acct_name = current_rule.menu_flags[selected_menu] #except KeyError: # acct_name = selected_rule # Fetch the primary account #current_acct = current_rule.fetch_account(acct_name) #current_rule._current_account = current_acct.name #current_rule.current_panel = current_acct.key_lookup('Panel') # Clear the panel current_rule.reset_rule(window, current=True) # Update the panel-in-display and the account panel window[current_panel].update(visible=False) current_panel = current_rule.element_key window[current_panel].update(visible=True) #window[current_rule.current_panel].update(visible=True) # Disable toolbar toolbar.disable(window) logger.debug( 'panel in view is {NAME}'.format(NAME=current_rule.name)) continue # Action events if current_rule and event in current_rule.events(): logger.info('running window event {EVENT} of rule {RULE}'.format( EVENT=event, RULE=current_rule.name)) try: current_rule_name = current_rule.run_event( window, event, values) except Exception as e: msg = 'failed to run window event {EVENT} of rule {RULE} - {ERR}'\ .format(EVENT=event, RULE=current_rule.name, ERR=e) mod_win2.popup_error(msg) logger.exception(msg) continue if current_rule_name is None: # Enable toolbar toolbar.enable(window) # Reset current_rule current_rule = None current_panel = '-HOME-' continue window.close()
def toggle_menu(self, window, menu, menu_item, disabled: bool = False): """ Enable or disable a specific menu item. """ menus = { 'amenu': self.acct_menu, 'rmenu': self.record_menu, 'umenu': self.user_menu, 'mmenu': self.menu_menu } try: select_menu = menus[menu.lower()] except KeyError: msg = 'selected menu {} not list of available menus'.format(menu) logger.error(msg) return False status = '' if not disabled else '!' new_menu = [] menu_items = select_menu['items'] for item in menu_items: if isinstance(item, tuple): clean_item = item[1].replace('&', '') if menu_item in (clean_item, clean_item.lower()): item_name = item[1] # Replace menu item with updated status new_menu.append((status, item_name)) else: new_menu.append(item) elif isinstance(item, list): sub_menu = [] for sub_item in item: if isinstance(sub_item, tuple): clean_item = sub_item[1].replace('&', '') if menu_item in (clean_item, clean_item.lower()): item_name = sub_item[1] # Replace menu item with updated status sub_menu.append((status, item_name)) else: sub_menu.append(sub_item) elif isinstance(sub_item, list): sub_sub_menu = [] for sub_sub_item in sub_item: clean_item = sub_sub_item[1].replace('&', '') if menu_item in (clean_item, clean_item.lower()): item_name = sub_sub_item[1] # Replace menu item with updated status sub_sub_menu.append((status, item_name)) else: sub_sub_menu.append(sub_sub_item) sub_menu.append(sub_sub_menu) new_menu.append(sub_menu) # Replace the menu item with updated status select_menu['items'] = new_menu # Update window to reflect updated status of the menu item element_key = self.key_lookup(menu.lower()) window[element_key].update(self._define_menu(menu)) return True
def __init__(self, document): """ Configuration manager. Arguments: document (dict): configuration document. """ doc_id = document["_id"] self.rules = [] if document is not None: try: rule_name = document['name'] except KeyError: msg = 'configuration document {ID} is missing required field "name"'.format( ID=doc_id) logger.error('ConfigurationManager: {MSG}'.format(MSG=msg)) mod_win2.popup_error( 'Configuration Error: {MSG}'.format(MSG=msg)) raise AttributeError(msg) else: self.name = rule_name try: self.title = document['title'] except KeyError: self.title = rule_name try: rules = document['rules'] except KeyError: msg = '{TYPE} configuration is missing required field "rules"'.format( TYPE=self.name) logger.error('ConfigurationManager: {MSG}'.format(MSG=msg)) mod_win2.popup_error( 'Configuration Error: {MSG}'.format(MSG=msg)) raise AttributeError(msg) if rule_name == 'records': manager = mod_records.RecordEntry elif rule_name == 'audit_rules': manager = mod_audit.AuditRule elif rule_name == 'bank_rules': manager = mod_bank.BankRule elif rule_name == 'cash_rules': manager = mod_cash.CashRule else: msg = 'unknown document type {TYPE} provided'.format( TYPE=self.name) logger.error('ConfigurationManager: {MSG}'.format(MSG=msg)) mod_win2.popup_error( 'Configuration Error: {MSG}'.format(MSG=msg)) raise AttributeError(msg) for rule_name in rules: rule = rules[rule_name] self.rules.append(manager(rule_name, rule))