diff --git a/README.md b/README.md index fec94b0..a4ce960 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ python3.9 test.py /addincome - Add Income Entry +/backlog - Add backdated transaction + /getdaytransaction - Retrieve transaction from dates /getoverall - Retrieve overall transaction for a month diff --git a/bot/common.py b/bot/common.py index 66f409c..2c96622 100644 --- a/bot/common.py +++ b/bot/common.py @@ -32,4 +32,6 @@ class ConversationState(Enum): INCOME, WORK_PLACE, CPF, - ) = range(24) + BACKLOG, + ADD_BACKLOG_ENTRY, + ) = range(26) diff --git a/bot/google_sheet_service.py b/bot/google_sheet_service.py index b021e1e..2e0b981 100644 --- a/bot/google_sheet_service.py +++ b/bot/google_sheet_service.py @@ -32,8 +32,11 @@ quick_others_range = "Tracker!I3:J13" quick_transport_range = "Tracker!G3:H13" +start_column_index = 0 +end_column_index = 11 -def get_main_dropdown_value(sheet_id, entry_type): + +def get_main_dropdown_value(spreadsheet_id, entry_type): range = [] if entry_type == EntryType.TRANSPORT: range = transport_range @@ -44,7 +47,7 @@ def get_main_dropdown_value(sheet_id, entry_type): results = ( sheets_api.spreadsheets() .values() - .get(spreadsheetId=sheet_id, range=range) + .get(spreadsheetId=spreadsheet_id, range=range) .execute() ) @@ -60,7 +63,7 @@ def get_main_dropdown_value(sheet_id, entry_type): return values[0] -def get_sub_dropdown_value(sheet_id, main_value, entry_type): +def get_sub_dropdown_value(spreadsheet_id, main_value, entry_type): range = [] if entry_type == EntryType.OTHERS: range = others_sub_range @@ -69,7 +72,7 @@ def get_sub_dropdown_value(sheet_id, main_value, entry_type): results = ( sheets_api.spreadsheets() .values() - .batchGet(spreadsheetId=sheet_id, ranges=range) + .batchGet(spreadsheetId=spreadsheet_id, ranges=range) .execute() ) @@ -86,42 +89,42 @@ def get_sub_dropdown_value(sheet_id, main_value, entry_type): return flat_list -def update_prev_day(sheet_id, month, first_row): - last_row = get_new_row(sheet_id, month) - # Write the message to the Google Sheet +def update_prev_day(spreadsheet_id, month, first_row, last_row=0): + if last_row == 0: + last_row = get_last_entered_row(spreadsheet_id, month) body = {"values": [[f"=SUM(C{first_row}:H{last_row})"]]} range_name = f"{month}!B{first_row}" sheets_api.spreadsheets().values().update( - spreadsheetId=sheet_id, + spreadsheetId=spreadsheet_id, range=range_name, valueInputOption="USER_ENTERED", body=body, ).execute() -def get_new_row(sheet_id, month): +def get_last_entered_row(spreadsheet_id, month): result = ( sheets_api.spreadsheets() .values() - .get(spreadsheetId=sheet_id, range=f"{month}!A:K") + .get(spreadsheetId=spreadsheet_id, range=f"{month}!A:K") .execute() ) values = result.get("values", []) return len(values) -def create_date(sheet_id, day, month, first_row): +def create_date(spreadsheet_id, day, month, first_row): body = {"values": [[day]]} range_name = f"{month}!A{first_row}" sheets_api.spreadsheets().values().update( - spreadsheetId=sheet_id, + spreadsheetId=spreadsheet_id, range=range_name, valueInputOption="USER_ENTERED", body=body, ).execute() -def create_entry(sheet_id, month, row_tracker, row_data): +def create_entry(spreadsheet_id, month, row_tracker, row_data): entry_type = row_data[0] price = row_data[1].strip() remarks = row_data[2].strip() @@ -142,18 +145,107 @@ def create_entry(sheet_id, month, row_tracker, row_data): f"{month}!{sheet_column_start}{row_tracker}:{sheet_column_end}{row_tracker}" ) sheets_api.spreadsheets().values().update( - spreadsheetId=sheet_id, + spreadsheetId=spreadsheet_id, range=range_name, valueInputOption="USER_ENTERED", body=body, ).execute() -def get_trackers(sheet_id): +def get_sheet_id_by_title(spreadsheet_id, title_to_find): + sheet_metadata = sheets_api.spreadsheets().get(spreadsheetId=spreadsheet_id).execute() + sheets = sheet_metadata.get('sheets', '') + + for sheet in sheets: + title = sheet.get("properties", {}).get("title") + if title == title_to_find: + return sheet.get("properties", {}).get("sheetId") + + return None + + +def create_backlog_entry(spreadsheet_id, backlog_day, backlog_month, row_data): + entry_type = row_data[0] + price = row_data[1].strip() + remarks = row_data[2].strip() + category = row_data[3].strip() + payment = row_data[4].strip() + + day_first_entry_index = get_day_first_entry_index(spreadsheet_id, backlog_month, backlog_day) + row_to_move = int(get_first_row_to_move(spreadsheet_id, backlog_month, backlog_day)) + last_row_to_move = int(get_last_entered_row(spreadsheet_id, backlog_month)) + new_entry_row = row_to_move + sheet_id = get_sheet_id_by_title(spreadsheet_id, backlog_month.title()) + + if row_to_move is None: + new_entry_row = last_row_to_move + 1 + else: + requests = [ + { + "copyPaste": { + "source": { + "sheetId": sheet_id, + "startRowIndex": row_to_move - 1, + "endRowIndex": last_row_to_move, + "startColumnIndex": start_column_index, + "endColumnIndex": end_column_index + }, + "destination": { + "sheetId": sheet_id, + "startRowIndex": row_to_move, + "endRowIndex": last_row_to_move + 1, + "startColumnIndex": start_column_index, + "endColumnIndex": end_column_index + }, + "pasteType": "PASTE_NORMAL", + "pasteOrientation": "NORMAL" + } + } + ] + + sheets_api.spreadsheets().batchUpdate( + spreadsheetId=spreadsheet_id, + body={"requests": requests} + ).execute() + + clear_range = f"{backlog_month}!A{new_entry_row}:K{new_entry_row}" + sheets_api.spreadsheets().values().clear( + spreadsheetId=spreadsheet_id, + range=clear_range + ).execute() + + if day_first_entry_index is None: + create_date(spreadsheet_id, backlog_day, backlog_month, new_entry_row) + + data = [price, remarks, category, payment] + sheet_column_start = "H" + sheet_column_end = "K" + if entry_type == EntryType.TRANSPORT: + remarks_list = [remark.strip() for remark in remarks.split(",")] + sheet_column_start = "C" + sheet_column_end = "G" + data = [price] + remarks_list + [category, payment] + + body = {"values": [data]} + range_name = ( + f"{backlog_month}!{sheet_column_start}{new_entry_row}:{sheet_column_end}{new_entry_row}" + ) + sheets_api.spreadsheets().values().update( + spreadsheetId=spreadsheet_id, + range=range_name, + valueInputOption="USER_ENTERED", + body=body, + ).execute() + + update_prev_day(spreadsheet_id, backlog_month, day_first_entry_index, new_entry_row) + + + +def get_trackers(spreadsheet_id): result = ( sheets_api.spreadsheets() .values() - .get(spreadsheetId=sheet_id, range=tracker_range) + .get(spreadsheetId=spreadsheet_id, range=tracker_range) .execute() ) values = result.get("values", []) @@ -163,7 +255,7 @@ def get_trackers(sheet_id): return -def update_rows(sheet_id, day, new_row, first_row): +def update_rows(spreadsheet_id, day, new_row, first_row): values = [[day] + [new_row] * 2 + [first_row]] range_name = tracker_range body = {"values": values} @@ -171,7 +263,7 @@ def update_rows(sheet_id, day, new_row, first_row): sheets_api.spreadsheets() .values() .update( - spreadsheetId=sheet_id, + spreadsheetId=spreadsheet_id, range=range_name, valueInputOption="USER_ENTERED", body=body, @@ -180,12 +272,12 @@ def update_rows(sheet_id, day, new_row, first_row): request.execute() -def row_incremental(sheet_id, entry_type): +def row_incremental(spreadsheet_id, entry_type): range_name = tracker_range response = ( sheets_api.spreadsheets() .values() - .get(spreadsheetId=sheet_id, range=range_name, majorDimension="ROWS") + .get(spreadsheetId=spreadsheet_id, range=range_name, majorDimension="ROWS") .execute() ) @@ -199,19 +291,44 @@ def row_incremental(sheet_id, entry_type): body = {"values": [row_values]} sheets_api.spreadsheets().values().update( - spreadsheetId=sheet_id, + spreadsheetId=spreadsheet_id, range=range_name, valueInputOption="USER_ENTERED", body=body, ).execute() -def get_quick_add_settings(sheet_id, entry_type): +def row_incremental_all(spreadsheet_id): + range_name = tracker_range + response = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=range_name, majorDimension="ROWS") + .execute() + ) + + values = response.get("values", []) + if values: + row_values = values[0] + row_values[1] = str(int(row_values[1]) + 1) # Increment others count + row_values[2] = str(int(row_values[2]) + 1) # Increment transport count + row_values[3] = str(int(row_values[3]) + 1) # Increment first row count + + body = {"values": [row_values]} + sheets_api.spreadsheets().values().update( + spreadsheetId=spreadsheet_id, + range=range_name, + valueInputOption="USER_ENTERED", + body=body, + ).execute() + + +def get_quick_add_settings(spreadsheet_id, entry_type): range_name = quick_add_range response = ( sheets_api.spreadsheets() .values() - .get(spreadsheetId=sheet_id, range=range_name, majorDimension="ROWS") + .get(spreadsheetId=spreadsheet_id, range=range_name, majorDimension="ROWS") .execute() ) @@ -229,7 +346,7 @@ def get_quick_add_settings(sheet_id, entry_type): return None -def update_quick_add_settings(sheet_id, entry_type, payment, type): +def update_quick_add_settings(spreadsheet_id, entry_type, payment, type): if entry_type == EntryType.TRANSPORT: range_1 = tracker_transport_1 range_2 = tracker_transport_2 @@ -241,7 +358,7 @@ def update_quick_add_settings(sheet_id, entry_type, payment, type): sheets_api.spreadsheets() .values() .get( - spreadsheetId=sheet_id, + spreadsheetId=spreadsheet_id, range=f"Tracker!{range_1}:{range_2}", ) .execute() @@ -252,14 +369,14 @@ def update_quick_add_settings(sheet_id, entry_type, payment, type): new_row = [payment, type] body = {"values": [new_row]} sheets_api.spreadsheets().values().update( - spreadsheetId=sheet_id, + spreadsheetId=spreadsheet_id, range=range_name, valueInputOption="USER_ENTERED", body=body, ).execute() -def get_quick_add_list(sheet_id, entry_type): +def get_quick_add_list(spreadsheet_id, entry_type): if entry_type == EntryType.TRANSPORT: range_name = quick_transport_range else: @@ -267,7 +384,7 @@ def get_quick_add_list(sheet_id, entry_type): response = ( sheets_api.spreadsheets() .values() - .get(spreadsheetId=sheet_id, range=range_name) + .get(spreadsheetId=spreadsheet_id, range=range_name) .execute() ) @@ -279,16 +396,17 @@ def get_quick_add_list(sheet_id, entry_type): return settings_list -def get_day_transaction(sheet_id, month, date): +def get_day_transaction(spreadsheet_id, month, date): result = ( sheets_api.spreadsheets() .values() - .get(spreadsheetId=sheet_id, range=f"{month}!A:A") + .get(spreadsheetId=spreadsheet_id, range=f"{month}!A:A") .execute() ) values = result.get("values", []) flat_list = [item for sublist in values for item in sublist or [""]] - + if (date) not in flat_list: + return None, None, None first_row = flat_list.index(date) first_row += 1 last_row = ( @@ -300,7 +418,7 @@ def get_day_transaction(sheet_id, month, date): sheets_api.spreadsheets() .values() .batchGet( - spreadsheetId=sheet_id, + spreadsheetId=spreadsheet_id, ranges=[ f"{month}!B{first_row}", f"{month}!C{first_row}:G{last_row}", @@ -319,11 +437,47 @@ def get_day_transaction(sheet_id, month, date): return total_spend, transport_values, other_values -def get_work_place(sheet_id): +def get_first_row_to_move(spreadsheet_id, month, date): + result = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=f"{month}!A:A") + .execute() + ) + values = result.get("values", []) + flat_list = [item for sublist in values for item in sublist or [""]] + next_date = str(int(date) + 1) + while next_date not in flat_list and int(next_date) < 32: + next_date = str(int(next_date) + 1) + + try: + last_row = flat_list.index(next_date) + except ValueError: + return None + return last_row + 1 + + +def get_day_first_entry_index(spreadsheet_id, month, date): + result = ( + sheets_api.spreadsheets() + .values() + .get(spreadsheetId=spreadsheet_id, range=f"{month}!A:A") + .execute() + ) + values = result.get("values", []) + flat_list = [item for sublist in values for item in sublist or [""]] + if (date) not in flat_list: + return None + first_row = flat_list.index(date) + first_row += 1 + + return first_row + +def get_work_place(spreadsheet_id): result = ( sheets_api.spreadsheets() .values() - .get(spreadsheetId=sheet_id, range=income_range) + .get(spreadsheetId=spreadsheet_id, range=income_range) .execute() ) values = result.get("values", []) @@ -332,7 +486,7 @@ def get_work_place(sheet_id): return flattened_list -def update_income(sheet_id, month, row_data): +def update_income(spreadsheet_id, month, row_data): data_mo = row_data[:3] data_r = [row_data[-1]] body_mo = {"values": [data_mo]} @@ -341,7 +495,7 @@ def update_income(sheet_id, month, row_data): result = ( sheets_api.spreadsheets() .values() - .get(spreadsheetId=sheet_id, range=f"{month}!M5:M10") + .get(spreadsheetId=spreadsheet_id, range=f"{month}!M5:M10") .execute() ) values = result.get("values", []) @@ -352,7 +506,7 @@ def update_income(sheet_id, month, row_data): range_name_mo = f"{month}!M{last_row}:O{last_row}" range_name_r = f"{month}!R{last_row}:R{last_row}" sheets_api.spreadsheets().values().update( - spreadsheetId=sheet_id, + spreadsheetId=spreadsheet_id, range=range_name_mo, valueInputOption="USER_ENTERED", body=body_mo, @@ -360,7 +514,7 @@ def update_income(sheet_id, month, row_data): body_r = {"values": [data_r]} sheets_api.spreadsheets().values().update( - spreadsheetId=sheet_id, + spreadsheetId=spreadsheet_id, range=range_name_r, valueInputOption="USER_ENTERED", body=body_r, @@ -368,11 +522,11 @@ def update_income(sheet_id, month, row_data): return True -def get_overall(sheet_id, month): +def get_overall(spreadsheet_id, month): result = ( sheets_api.spreadsheets() .values() - .get(spreadsheetId=sheet_id, range=f"{month}{overall_range}") + .get(spreadsheetId=spreadsheet_id, range=f"{month}{overall_range}") .execute() ) return result.get("values", []) diff --git a/bot/telegram_bot.py b/bot/telegram_bot.py index 46bdf99..72ea39e 100644 --- a/bot/telegram_bot.py +++ b/bot/telegram_bot.py @@ -85,7 +85,7 @@ def set_up(update, context) -> int: "%b" ) gs.update_prev_day(sheet_id, prev_month, first_row) - new_row = gs.get_new_row(sheet_id, month) + new_row = gs.get_last_entered_row(sheet_id, month) first_row = new_row + 1 gs.update_rows(sheet_id, day, new_row, first_row) gs.create_date(sheet_id, day, month, first_row) @@ -442,7 +442,10 @@ def payment(update, context) -> int: update.callback_query.edit_message_text( f'Payment type: {context.user_data["payment"]}', reply_markup=None ) - log_transaction(context.user_data, update) + if context.user_data["backlog"]: + backlog_transaction(context.user_data, update) + else: + log_transaction(context.user_data, update) update.callback_query.message.reply_text("Transaction logged.") return ConversationHandler.END except Exception as e: @@ -465,7 +468,10 @@ def subpayment(update, context) -> int: update.callback_query.edit_message_text( f'Payment type: {context.user_data["payment"]}', reply_markup=None ) - log_transaction(context.user_data, update) + if context.user_data["backlog"]: + backlog_transaction(context.user_data, update) + else: + log_transaction(context.user_data, update) update.callback_query.message.reply_text("Transaction logged.") return ConversationHandler.END @@ -513,7 +519,7 @@ def log_transaction(user_data, update): first_row = 5 gs.update_rows(sheet_id, 1, new_row, first_row) else: - new_row = gs.get_new_row(sheet_id, month) + new_row = gs.get_last_entered_row(sheet_id, month) first_row = new_row + 1 gs.update_rows(sheet_id, day, new_row, first_row) if update.callback_query and update.callback_query.message: @@ -540,8 +546,35 @@ def log_transaction(user_data, update): gs.create_entry(sheet_id, month, other_row_tracker, row_data) +def backlog_transaction(user_data, update): + sheet_id = user_data["sheet_id"] + + backlog_day = user_data["backlog_day"] + backlog_month = user_data["backlog_month"] + + # datatime data + current_datetime = dt.datetime.now(timezone) + month = current_datetime.strftime("%b") + + # if backlog month is current month, need to move all one down + if backlog_month.title() == month: + gs.row_incremental_all(sheet_id) + + + # user input data + entry_type = user_data["entry_type"] + payment = user_data["payment"] + price = user_data["price"] + category = user_data["category"] + remarks = user_data["remarks"] + row_data = [entry_type, price, remarks, category, payment] + + # create backlog entry + gs.create_backlog_entry(sheet_id, backlog_day, backlog_month, row_data) + def cancel(update, context): update.message.reply_text(END_TEXT, reply_markup=None) + context.user_data.clear() return ConversationHandler.END @@ -686,6 +719,12 @@ def handle_get_transaction(update, context): total_spend, transport_values, other_values = gs.get_day_transaction( sheet_id, month, day ) + if total_spend == None and transport_values == None and other_values == None: + update.message.reply_text( + f"No transaction found for {day} {month}", reply_markup=None + ) + return ConversationHandler.END + if not total_spend: total_spend = "To be determine" else: @@ -812,6 +851,34 @@ def cpf(update, context) -> int: update.callback_query.message.reply_text(ERROR_TEXT) return ConversationHandler.END +def backlog(update, context) -> int: + context.user_data.clear() + update.message.reply_text(BACKLOG_DATE_TEXT) + context.user_data["backlog"] = True + return CS.ADD_BACKLOG_ENTRY + +def add_backlog_entry(update, context) -> int: + reply = update.message.text + if utils.check_date_format(reply): + if reply == dt.datetime.now(timezone).strftime("%d %b").lstrip("0"): + context.user_data["backlog"] = False + else: + day, month = reply.split(" ") + context.user_data["backlog_day"] = day + context.user_data["backlog_month"] = month + else: + update.message.reply_text(BACKLOG_DATE_TEXT) + return CS.ADD_BACKLOG_ENTRY + + telegram_id = update.effective_user.id + context.user_data["sheet_id"] = db.get_user_sheet_id(telegram_id) + update.message.reply_text( + ENTRY_TYPE_TEXT, + reply_markup=utils.create_inline_markup( + [entry_type.value for entry_type in EntryType] + ), + ) + return CS.ENTRY def setup_handlers(dispatcher): # Configuration-related states and handlers @@ -833,6 +900,7 @@ def setup_handlers(dispatcher): CS.SUBCATEGORY: [CallbackQueryHandler(subcategory)], CS.PAYMENT: [CallbackQueryHandler(payment)], CS.SUBPAYMENT: [CallbackQueryHandler(subpayment)], + CS.ADD_BACKLOG_ENTRY: [MessageHandler(Filters.text & ~Filters.command, add_backlog_entry)], } # Quick add-related states and handlers @@ -867,6 +935,7 @@ def setup_handlers(dispatcher): CommandHandler("addincome", add_income), CommandHandler("getdaytransaction", get_day_transaction), CommandHandler("getoverall", get_overall), + CommandHandler("backlog", backlog), ], states={ CS.SET_UP: [MessageHandler(Filters.text & ~Filters.command, set_up)], diff --git a/bot/text_str.py b/bot/text_str.py index 127fc99..ff26f1b 100644 --- a/bot/text_str.py +++ b/bot/text_str.py @@ -68,3 +68,5 @@ CHOOSE_INCOME_SOURCE_TEXT = "Please choose your income source" CPF_TEXT = "Is there CPF?" INCOME_LIMIT_TEXT = "You have exceed the number of income allowed! (max 6)" + +BACKLOG_DATE_TEXT = "Please enter the date of the entry in this format: DD MMM\ne.g 16 Mar" \ No newline at end of file diff --git a/release_notes.md b/release_notes.md index 84e3463..7ecc565 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,4 +1,8 @@ # Release Notes +## Version 2.1.7 - Date 16 Dec 2023 +### New Features 🆕 +- You can now add backdated transactions with `/backlog` command! 🎉 + ## Version 2.1.6 - Date 16 Dec 2023 ### Bug Fix 🛠️ - Fix empty subcategory and subpayment bug