コード例 #1
0
    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
コード例 #2
0
ファイル: cash.py プロジェクト: cnthornton/REM
    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)
コード例 #3
0
ファイル: database.py プロジェクト: cnthornton/REM
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)
コード例 #4
0
ファイル: database.py プロジェクト: cnthornton/REM
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
コード例 #5
0
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()
コード例 #6
0
    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
コード例 #7
0
    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))