Last active
February 15, 2024 18:01
-
-
Save Mikelem/f4051cab396ec877b9b08bc26b072c4f to your computer and use it in GitHub Desktop.
subscriptions-run subscriptions_json.py first
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from datetime import datetime, date | |
from dateutil.relativedelta import relativedelta | |
from datetime import timedelta | |
import os | |
""" | |
Necessary for sort_maint.py and sort_maint_records.py | |
""" | |
# These DATE STRINGS are absolutely necessary to compare against | |
# First, create a Tuple from today's date for date comparison and sorting: | |
dateToday = date.today() | |
current_mo_day_str = dateToday.strftime("%m-%d") | |
# WHY all of these 'current_mo_day's?!?!?! | |
current_mo_day = dateToday.strftime("%m-%d") # redundant!!! | |
current_mo_day = "('{}',)".format(current_mo_day) | |
current_mo_day = eval(current_mo_day) | |
current_day = dateToday.strftime("%d") # DD | |
current_month = dateToday.strftime("%m") # MM | |
current_year = dateToday.strftime("%Y") # YYYY | |
current_date_plus_mo = dateToday + relativedelta(months=1) | |
# currentDT = date.today # redundant!!! | |
# currentDT_str = datetime.strftime(currentDT, '%d/%m/%Y') | |
future_yr = current_date_plus_mo.strftime("%Y") | |
future_mo = current_date_plus_mo.strftime("%m") | |
# today_day = currentDT.day | |
def terminal_size(): | |
import os | |
# print("Terminal size: os:") | |
size = os.get_terminal_size() | |
# print("Size:",size, type(size)) | |
# print(f'Height: {size[1]}') | |
# print(f'Width: {size[0]}') | |
width=size[0] | |
height=size[1] | |
return width, height | |
t_width = os.get_terminal_size()[0] | |
t_height = os.get_terminal_size()[1] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from date_config import * | |
def date_formatted(next_bill): | |
# Get next_billing_date as string, convert to date obj then format back to string | |
# next_bill = row[8] # next_billing_date | |
next_bill_obj = date.fromisoformat(next_bill) | |
# Format date for nice printing | |
due_day= next_bill_obj.strftime('%d') | |
due_mo = next_bill_obj.strftime('%b') | |
due_yr = next_bill_obj.strftime('%Y') | |
# full_date = (f'{due_day} {due_mo}, {due_yr}') | |
full_date = (f'{due_mo} {due_day}, {due_yr}') | |
# print(full_date) | |
return full_date | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sqlite3 | |
from sqlalchemy import * | |
from date_config import * | |
from format_date import date_formatted | |
import os | |
import json | |
# from menu import Menu_Tree | |
#### THIS FILE CONTAINS THE FOLLOWING FUNCITONS: | |
# | |
# print_header(msg,col_titles): | |
# Called by other functions when printing to screen | |
# | |
# print_vendor_summary(cur, subsTable): # prob don't need subsTable | |
# | |
# print_streaming(name_sorted_data, msg,stream_sorted_data): | |
# | |
# print_all_subs(subscr_df, msg): | |
# | |
# recurring_print(): | |
# def main(subscr_df): | |
""" Print Headers """ | |
def print_header(msg,col_titles): | |
t_width = os.get_terminal_size()[0] | |
t_height = os.get_terminal_size()[1] | |
# print(f'Width{t_width} Height:{t_height}') # DELETE AFTER TESTING | |
half_width = int((t_width - len(msg))/2) | |
quarter_w = int((t_width - len(msg))/4) | |
print(f'{"="*int(t_width)}') | |
print(f'{"="*half_width}{msg}{"="*half_width}') | |
print(col_titles) | |
print(f'{"="*int(t_width)}') | |
""" Print Summary """ | |
def print_vendor_summary(subscr_df): | |
print("PRINTING_VENDOR_SUMMARY") | |
# List of dictionaries: | |
name_sorted_data = sorted(subscr_df.items(), key=lambda item: item[0]) | |
name_sorted_msg = " :: VENDOR SUMMARY :: " | |
msg = name_sorted_msg # Could be redundant or works w/in grand scheme of things | |
col_titles = (f' {"NAME":<23s}{"CATEGORY":19s}{"$/MO":^10} {"RENEWS":^10} {"OWNER":>10}:{"ACCT":8}') | |
""" === PRINT HEADER === """ | |
print_header(msg, col_titles) | |
print("\n") | |
for i in name_sorted_data: | |
print(i[0]) | |
# W Notes: | |
# print(f' {"NAME":<23s}{"CATEGORY":19s}{"$/MO":^10} {"RENEWS":^10}{"OWNER":>10}{"ACCT":>8} {"NOTES":<22}') | |
# W/O Notes: | |
print("ALTERNATE Vendor Summary TITLE, not necessarily better:") | |
print(f'{"="* 85}') | |
print(f' {"NAME":<23s}{"CATEGORY":19s}{"$/MO":^10} {"RENEWS":^10} {"OWNER":>10}:{"ACCT":8}') | |
print(f'{"="* 85}\n') | |
# print Totals for subscriptions and recurring | |
# print("Menu_Tree(1)") | |
# | |
# Menu_Tree() | |
# | |
# print("Menu_Tree(2)") | |
return | |
def print_streaming_sched(name_sorted_data): | |
""" PRINTS STREAMING SERVICES and FILMS """ | |
# print("AMZN LEN:", len("Amazon Prime")) | |
# json_file = "data/subs-test.json" | |
# f = open(json_file,'r+') | |
# subscr_df = json.load(f) #Dictionary | |
# # ALL VENDOR Data Sorted by Vendor Name | |
# name_sorted_data = sorted(subscr_df.items(), key=lambda item: item[0]) | |
""" Streaming Vendors sorted by upcoming due date: """ | |
total_mo_subs = 0.0 | |
total_yr_subs = 0.0 | |
# streaming_sorted_data_sched = sorted(subscr_df.items(), key=lambda item: item[0]) | |
# streaming_sorted_data_sched = sorted(subscr_df.items(), key=lambda item: item[0] + item[1]['category']=='streaming') | |
msg = " :: STREAMING SUMMARY :: " | |
# print_streaming(name_sorted_data, streaming_sorted_msg, stream_sorted_data) | |
""" === PRINT HEADER === """ | |
col_titles = f'{"NAME":14}{"$/month":>9}{" "*10}{"$/annual":^11}{" "*5}{"Billing Per":<10}{" "*4}{" "*1}{"ACCT":^3}{" "*4}{"RENEW DATE":<16s}' | |
print_header(msg, col_titles) | |
len_data = 22+26+47 +4 | |
col_rem = int(t_width - len_data) | |
for item in name_sorted_data: | |
if len(item[1]["notes"]) > col_rem: | |
notes = item[1]["notes"][:col_rem-3]+"..." | |
else: | |
notes = item[1]["notes"] | |
if item[1]['category']=='streaming': | |
# print("\n") | |
print( | |
f'{item[0]:14s}'+ | |
f'${item[1]["price_mo"]:>5.2f}/month'+ | |
f'{" "*6}${item[1]["price_yr"]:>6.2f}/year'+ | |
f'{" "*4}{"Billed: " + item[1]["period"]:>10s}'+ | |
# f'{" "*4}{item[1]["owner"]:>6s}'+ | |
f'{" "*4}{"Acct Chgd: " + item[1]["acct"]:^8s}'+ | |
f'{" "*4}{"Due: " +item[1]["next_billing_date"]}' | |
) | |
# print(*(stream_data[item[0]]['films']), sep=", ") | |
for film in sorted(item[1]['films']): | |
print(f'{" "*6}{film}') | |
if item[1]["status"] == "cancelled": | |
continue | |
else: | |
total_mo_subs += item[1]["price_mo"] | |
total_yr_subs += item[1]["price_yr"] | |
""" === PRINT FOOTER === """ | |
foot_msg_mo = f'{"="*10} TOTAL MONTHLY COSTS: STREAMING ${total_mo_subs:,.2f}' | |
foot_msg_yr = f'; YEARLY: ${total_yr_subs:,.2f}' | |
foot_rtn_msg = " RTN for MAIN MENU " +("="*15) | |
# third_spc_msg = int((t_width-len(foot_msg_mo)-len(foot_rtn_msg))/3)-3 | |
eq_spacers = t_width-(len(foot_msg_mo) + len(foot_msg_yr) +len(foot_rtn_msg)) | |
""" Calculations for footer message: """ | |
# print("Terminal WIDTH:", t_width) | |
# print("Foot msg:", len(foot_msg)) | |
# print("Rtn msg:", len(foot_rtn_msg)) | |
# print("Running total: ", t_width-len(foot_msg)-len(foot_rtn_msg)) | |
# print("Third MSG L:", third_spc_msg) | |
""" 1st FOOTER:""" | |
print(f'\n{foot_msg_mo}{foot_msg_yr}{"="* eq_spacers}{foot_rtn_msg}') | |
# print(f'\n{"="*(third_spc_msg-14)}{foot_msg_mo}{total_mo_subs:,.2f}; {foot_msg_yr}{total_yr_subs:,.2f} {"="*third_spc_msg*2}{foot_rtn_msg}') | |
print(f'\n{"="*t_width}\n') | |
""" === END FOOTER === """ | |
input() | |
os.system('clear') | |
return | |
def print_streaming(stream_sorted_data, stream_data): | |
stream_file = "data/streaming.json" | |
stream_df = open(stream_file,'r+') | |
stream_data = json.load(stream_df) #Dictionary | |
""" Just prints the films that are streaming, no due dates or costs""" | |
""" === PRINT HEADER === """ | |
print(f'\n{"="*t_width}\n') | |
header_msg = f'{"="*15}{" NOW STREAMING "}' | |
head_eq_spacers = t_width-(len(header_msg)) | |
print(f'{header_msg}{"="* head_eq_spacers}') | |
for item in stream_sorted_data: | |
film_str="" | |
print(f'\n{item[0]}:') | |
# print(type(item[1]["films"])) | |
for film in sorted(stream_data[item[0]]['films']): | |
print(f'{" "*16}{film}') | |
# print(*(item[1]["films"]), sep=", ") | |
# """ === PRINT FOOTER === """ | |
# foot_msg_mo = f'{"="*10} TOTAL MONTHLY COSTS: STREAMING ${total_mo_subs:,.2f}' | |
# foot_msg_yr = f'; YEARLY: ${total_yr_subs:,.2f}' | |
foot_rtn_msg = " RTN for MAIN MENU " +("="*15) | |
eq_spacers = t_width-(len(foot_rtn_msg)) | |
""" Calculations for footer message: """ | |
# print("Terminal WIDTH:", t_width) | |
# print("Foot msg:", len(foot_msg)) | |
# print("Rtn msg:", len(foot_rtn_msg)) | |
# print("Running total: ", t_width-len(foot_msg)-len(foot_rtn_msg)) | |
# print("Third MSG L:", third_spc_msg) | |
""" 1st FOOTER:""" | |
print(f'\n{"="* eq_spacers}{foot_rtn_msg}') | |
# print(f'\n{"="*(third_spc_msg-14)}{foot_msg_mo}{total_mo_subs:,.2f}; {foot_msg_yr}{total_yr_subs:,.2f} {"="*third_spc_msg*2}{foot_rtn_msg}') | |
print(f'\n{"="*t_width}\n') | |
input() | |
os.system('clear') | |
return | |
def print_all_subs(date_sorted_data): | |
# name_sorted_data | |
date_sorted_msg = " :: SUBSCRIPTIONS COMING DUE :: " | |
cancelled_vendors = [] | |
total_mo_subs = 0.0 | |
# TODO: | |
""" IF 'SHARED' Divide by 2 to get My costs """ | |
""" === PRINT HEADER === """ | |
col_titles = f'{"NAME":22}{"RENEW DATE":<16s}{"$/MONTHLY":>9}{" "*4}{"$/ANNUAL":^11}{" "*5}{"PERIOD":<10}{" "*4}{"OWNER":8}{" "*1}{"ACCT":^3}{" "*4}{"NOTES":<22}' | |
print_header(date_sorted_msg, col_titles) | |
len_data = 22+26+47 +4 | |
col_rem = int(t_width - len_data) | |
for item in date_sorted_data: | |
next_bill_dateObj=date.fromisoformat(item[1]["next_billing_date"]) | |
if dateToday > next_bill_dateObj and item[1]["status"] == 'cancelled': | |
""" DELETE CANCELLED SUBSCRIPTIONS: """ | |
""" OR leave in dB and put cost @ $0.00 """ | |
cancelled_vendors.append(item[0]) | |
continue | |
if len(item[1]["notes"]) > col_rem: | |
notes = item[1]["notes"][:col_rem-3]+"..." | |
else: | |
notes = item[1]["notes"] | |
if type(item[1]["price_mo"] )is list: | |
price_mo = sum(item[1]["price_mo"]) / len(item[1]["price_mo"]) | |
else: price_mo = item[1]["price_mo"] | |
print( | |
f'{item[0]:22s}'+ | |
f'{item[1]["next_billing_date"]}'+ | |
f'{" "*6}${price_mo:>7.2f}'+ | |
f'{" "*6}${item[1]["price_yr"]:>8.2f}'+ | |
f'{" "*4}{item[1]["period"]:>10s}'+ | |
f'{" "*4}{item[1]["owner"]:>6s}'+ | |
f'{" "*4}{item[1]["acct"]:^8s}'+ | |
f'{" "*2}{notes:{col_rem}}' | |
) | |
total_mo_subs += price_mo | |
""" === PRINT FOOTER === """ | |
foot_msg = " TOTAL MONTHLY COSTS: ALL SUBSCRIPTIONS $" | |
foot_rtn_msg = " RTN for MAIN MENU" | |
third_spc_msg = int((t_width-len(foot_msg)-len(foot_rtn_msg))/3)-3 | |
print(f'\n{"="*(third_spc_msg-11)}{foot_msg}{total_mo_subs:,.2f} {"="*third_spc_msg*2}{foot_rtn_msg}') | |
str_l = ', '.join(cancelled_vendors) | |
print("Cancelled:", str_l) | |
input() | |
os.system('clear') | |
# RECURRING COSTS REPORT: | |
def hard_costs_print(subscr_df, stream_sorted_data): | |
""" DUH! Feed the list of vendors sorted alphabetically and filter out | |
the ones whose categories don't appear in the list of hard_cost_cats !!!""" | |
# includes categories: utilities, health_insurance, credit_card_memb,dues, | |
hard_cost_cats = ['utilities', 'health_insurance', 'credit_card_memb','dues',] | |
sortedHardCosts = [] | |
total_ute_mo_costs = 0 | |
""" === PRINT HEADER === """ | |
print(len("==========================")) | |
print(f'{"="*58}RECURRING/MONTHLY HARD COSTS{"="*58}') | |
print("\n") | |
print(f'{"DUE DATE":<20s}{"NAME":22}{"$MONTHLY":^9} {"$ANNUAL":^8} {"PERIOD":<10}{"OWNER":10}{"ACCT":^8} {"CATEGORY":<22}') | |
for item in subscr_df: | |
pass | |
# TODO: print MY share, taking the 'shared' accts and dividing that number by 2 | |
return | |
# print_all_subs(name_sorted_data) | |
if __name__ == '__main__': | |
# main() | |
""" SUBSCRIPTIONS DATA:""" | |
json_file = "data/subs-test.json" | |
f = open(json_file,'r+') | |
subscr_df = json.load(f) #Dictionary | |
""" STREAMING DATA:""" | |
stream_file = "data/streaming.json" | |
stream_df = open(stream_file,'r+') | |
stream_data = json.load(stream_df) #Dictionary | |
""" SORTED LISTS: """ | |
# List of dictionaries: | |
# ALL VENDOR Data Sorted by Vendor Name | |
name_sorted_data = sorted(subscr_df.items(), key=lambda item: item[0]) | |
# STREAMING Data Sorted by Vendor Name: | |
stream_sorted_data = sorted(stream_data.items(), key=lambda item: item[0]) | |
# ALL Data Sorted by Next_Billing_Date: | |
date_sorted_data = sorted(subscr_df.items(), key=lambda item: (item[1]['next_billing_date']+ item[0])) | |
periods = dict() | |
for item in date_sorted_data: | |
per = item[1]['period'] | |
print(per) | |
periods[per] = periods.get(per, 0) + 1 | |
input("EVALUATE PERIODS") | |
""" FUNCTIONS: """ | |
# Print all Vendors Alphabetically: | |
print_vendor_summary(subscr_df) | |
# Print all vendors according to Due Dates for billing: | |
print_all_subs(date_sorted_data) | |
# Streaming data, no films | |
print_streaming_sched(subscr_df, stream_sorted_data, stream_data) | |
# Streaming data, just films | |
print_streaming(stream_sorted_data, stream_data) | |
# Utilities Vendors - Hard costs sorted by due date: """ | |
# includes categories: utilities, health_insurance, credit_card_memb,dues, | |
hard_costs_print(subscr_df,stream_sorted_data) | |
""" Hard Costs Vendors sorted by Name: """ | |
""" A list with due 'days' rather than upcoming due dates """ | |
# hard_costs= dict([(k,v) for (k,v) in name_sorted_data.items() if v['category']=='utilities' or 'insurance' in v['category']]) | |
print("UTILITIES::::") | |
hard_costs= dict([item for item in name_sorted_data if item[1]['category']=='utilities' or 'insurance' in item[1]['category']]) | |
print("HARD COSTS:") | |
for k,v in hard_costs.items(): | |
print(k,v) | |
print() | |
input("CHECK HARD COSTS") | |
# Cancelled Vendors sorted by due date: """ | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import re | |
from date_config import * | |
import json | |
import math | |
import pandas as pd | |
import statistics | |
""" update_dates (subscr_data): (formerly maint_all) """ | |
""" UPDATE RECORDS BEFORE EXECUTING THE REST OF THE PROGRAM: """ | |
""" Sets the last_bill_date and next_bill_date relative to the current date | |
to assist in sorting records according to what is coming due. | |
Disregards the past. | |
""" | |
""" Maintenance on start: | |
# update renewal date: | |
#if renew date is in the past AND period == 'monthly': | |
# if cancel_chg == 'cancelled': | |
# delete row | |
# else: update next_bill_date/'renews' | |
#if renew date is in the past AND period == 'annual': | |
# Expires ___ TODO TODO TODO | |
# if ['status'] == 'cancelled': | |
print(record)""" | |
periods = ['3yrs', '4yrs', 'annual', 'four_wks','monthly','quarterly','bi-annual'] | |
def json_file_open(): | |
json_file = "data/subs-test_MASTER.json" | |
with open(json_file,'r+') as json_file: | |
subscr_data = json.load(json_file) | |
return subscr_data, json_file | |
if zero_val_recs_chgd != 0: | |
json.dump(subscr_data, json_file, indent = 4) | |
json_file.close() | |
else: | |
json_file.close() | |
print("No records changed here") | |
def delete_vendor(vendor): | |
# When called by if ['status'] == 'cancelled' set default variable in function call | |
# called as module | |
# Opens JSON FILE | |
print("DELETE VENDOR FUNCTION") | |
subscr_data, json_file = json_file_open() | |
# LOOP: ask for file to delete | |
print(f'DELETING RECORD : {vendor}') | |
del subscr_data[vendor] | |
json_file.seek(0) | |
input("CHECK FILE DELETED") | |
# return to json_file_open function | |
return | |
def update_vendor(subscr_data): | |
pass | |
# For UPDATING a Vendor: | |
print(f'Account Details for _______:') | |
# for num, column in enumerate(data.description): | |
# try: | |
# pass | |
# Print floats as dollar amounts" | |
# print(f'[{num+1:2d}] {column[0]:11s}: ${row_data[num]:.2f}') | |
# except: | |
# Print all others normally | |
# print(f'[{num+1:2d}] {column[0]:11s}: {row_data[num]}') | |
print("Update which VENDOR? ") | |
"""ASK user for column/category to UDPATE""" | |
print("'0' to EXIT through the giftshop") | |
is_looping = True | |
while is_looping == True: | |
try: | |
num = int(input("Choice: ")) | |
print(f'{num} {type(num)}') | |
if num >0 and num <16: | |
print("Got the number, continue") | |
break | |
elif num==0: | |
return False | |
else: | |
print("ENTER a number between 1-15!") | |
continue | |
except ValueError: | |
print("Please input an integer only...") | |
continue | |
# else: | |
# print("ENTER a number between 1-17!") | |
# continue | |
data_ind = int(num)-1 | |
# category = data.description[data_ind][0] | |
# print(F"SELECT {category} FROM subscriptions WHERE name='{vendor_name}'") #DELETE WHEN FINISHED | |
# input("CHECK CHOICE") #DELETE WHEN FINISHED | |
# if row_data[data_ind] =="": | |
# row_data_ind="Nothing" | |
# else: | |
# row_data_ind=row_data[data_ind] | |
# print(f'Change {category}: from "{row_data_ind}" to...? ') | |
# ASK User for update info to insert | |
change =input("What are the changes? ") # Thing to change or update | |
# METHOD 2: | |
return | |
def update_quarterly(subscr_data, vendor): | |
#### (3) PERIOD = QUARTERLY | |
qtr_renew_dates = ("01-01", "04-01", "07-01", "10-01") | |
# For quarterly dues (IATSE) | |
# WE START with a date object and | |
# FINISH with a date string | |
# We don't care about the actual last or next_bill_date | |
# Except when it comes to writing to the file. And even then it's inconsequential. | |
last_bill_date = "" | |
next_year_str = (dateToday + relativedelta(years=1)).strftime("%Y") #YYYY | |
today_yyyy_str = dateToday.strftime("%Y") #YYYY | |
today_mm_dd_str = dateToday.strftime("%m-%d") #MM-DD | |
qtr = (dateToday.month-1)//3 | |
# print("QTR:", qtr) | |
# print("QTR DATE:",qtr_renew_dates[qtr]) | |
# print("TODAY-YEAR:",today_yyyy_str,type(today_yyyy_str)) | |
# print("TODAY-MONTH:",today_mm_dd_str,type(today_mm_dd_str)) | |
# print("NEXT YEAR:", next_year_str, type(next_year_str)) | |
# print() | |
"""If the update day falls exactly on the quarter:""" | |
if today_mm_dd_str in qtr_renew_dates: | |
index = qtr_renew_dates.index(today_mm_dd_str) | |
print("INDEX:", index) | |
print("DUE DATE = TODAY") | |
# We're finished | |
last_bill_date = (dateToday - relativedelta(months=3)).strftime("%Y-%m-%d") | |
next_bill_date = dateToday.strftime("%Y-%m-%d") | |
# print("LAST BILL DATE:", last_bill_date) | |
# print("NEXT BILL DATE:", next_bill_date) | |
elif qtr == 3 and today_mm_dd_str not in qtr_renew_dates: | |
# IF the update day falls in the last quarter we need to make the next billing date in Q1 of the next year | |
print("SEE YOU NEXT YEAR") | |
print("NEW RENEW:", next_year_str, type(next_year_str)) | |
next_bill_date = (f'{next_year_str}-{qtr_renew_dates[0]}') | |
last_bill_date = (f'{today_yyyy_str}-{qtr_renew_dates[3]}') | |
# print("NEXT_BILL_DATE:", next_bill_date) | |
# print("LAST_BILL_DATE:", next_bill_date) | |
else: | |
qtr+=1 | |
print("NEW QTR:", qtr_renew_dates[qtr]) # 0-2 | |
next_bill_date = (f'{today_yyyy_str}-{qtr_renew_dates[qtr]}') | |
lbd = date.fromisoformat(next_bill_date) | |
last_bill_date = (lbd - relativedelta(months=3)).strftime("%Y-%m-%d") | |
# print("NEXT_BILL_DATE:", next_bill_date) | |
# print("LAST_BILL_DATE:", next_bill_date) | |
print("LAST_BILL_DATE:", last_bill_date) | |
print("NEXT_BILL_DATE:", next_bill_date) | |
""" WRITE to JSON file: 1""" | |
subscr_data[vendor]['last_bill_date'] = last_bill_date | |
subscr_data[vendor]['next_bill_date'] = next_bill_date | |
# json_file.seek(0) | |
print("\tOKAY to WRITE to JSON: QUARTERLY") | |
return | |
# def new_sort(subscr_data): | |
# for vendor, vend_dict in subscr_data.items(): | |
# print("VENDOR:", vendor) | |
# print("DICT:", vend_dict) | |
# print() | |
# input("CHECK VENDORS") | |
def fix_zero_vals(subscr_data, json_file): | |
"""FIX all the zero values and update the values when 'amt_paid' is a list. """ | |
# TODO: fix the name 'fix_zero_vals' | |
print("We're at zero fixer-upper") | |
""" ------------------------------------------------------------------------------------------------------ | |
We are mainly concerned with price_yr and price_mo. | |
All records should be entered with an 'amt_paid' and a 'period' value or let's face it, | |
what kind of operation are we running here? | |
There are THREE instances where we might need some calculating: | |
1. 'amt_paid' is a <list>. In which case, we need an average of the list of amounts paid. | |
2. 'amt_paid' is a number but there is a zero amount in 'price_yr' or 'price_amt' | |
This will trigger both the 'price_yr' and 'price_amt' amounts to be calculated from 'amt_paid' | |
3. 'amt_paid' = 0. In this case, the services are free and no calculations are necessary. | |
------------------------------------------------------------------------------------------------------ """ | |
zero_val_recs_chgd = 0 | |
for vendor, vend_dict in subscr_data.items(): | |
print() | |
print(vendor) | |
print(vend_dict) | |
""" IF "amt_paid" is a LIST:""" | |
if type(subscr_data[vendor]["amt_paid"]) == list: | |
""" Xcel and Verizon costs may be stored as a list which allows the program to get an average cost | |
from a list of variable costs.""" | |
# TODO: allow updates to the lists | |
print("VENDOR: LIST OF AMTS: ", vendor) # DELETE AFTER TESTING | |
# subscr_data[vendor]["price_mo"] = round((sum(subscr_data[vendor]["amt_paid"])/len(subscr_data[vendor]["amt_paid"])),2) # DELETE AFTER TESTING | |
# subscr_data[vendor]["price_yr"] = round((subscr_data[vendor]["price_mo"]*12),2) # DELETE AFTER TESTING | |
subscr_data[vendor]["price_mo"] = round(statistics.fmean(subscr_data[vendor]["amt_paid"]),2) | |
subscr_data[vendor]["price_yr"] = round((subscr_data[vendor]["price_mo"] * 12),2) | |
# json_file.seek(0) | |
zero_val_recs_chgd+=1 | |
elif subscr_data[vendor]["price_mo"]==0.0 or subscr_data[vendor]["price_yr"]==0.0: | |
""" CALCULATES price_mo AND price_yr if either one has a zero value and | |
WHEN amt_paid is present:""" | |
print("ZEROS have been found") | |
print("PRICE/MO:", subscr_data[vendor]["price_mo"]) | |
print("PRICE/MO:", subscr_data[vendor]["price_yr"]) | |
print("AMT_PAID:", subscr_data[vendor]["amt_paid"]) | |
period = subscr_data[vendor]["period"] | |
print("PERIOD:", period) | |
mo_yr_calc_factors = {'monthly': 12, 'annual': 1, 'four_wks': 13, 'quarterly': 4, '4yrs': .25, 'bi-annual': 2, '3yrs': .3} | |
if subscr_data[vendor]["amt_paid"]>0.0 : | |
subscr_data[vendor]["price_yr"] = round(subscr_data[vendor]["amt_paid"] * mo_yr_calc_factors[period],2) | |
subscr_data[vendor]["price_mo"] = round((subscr_data[vendor]["price_yr"]/12),2) | |
# input("STOP TO EVALUATE MATH FROM AMT_PAID") | |
zero_val_recs_chgd+=1 | |
else: | |
if subscr_data[vendor]["amt_paid"] == 0: | |
print("DID YOU KNOW: no costs are associated with this record?") | |
print("PRICE/MO 2:", subscr_data[vendor]["price_mo"]) | |
print("PRICE/YR 2:", subscr_data[vendor]["price_yr"]) | |
print("AMT_PAID 2:", subscr_data[vendor]["amt_paid"]) | |
# json_file.seek(0) #->->-> REINSTATE if PROBLEM PERSISTS | |
if zero_val_recs_chgd == 0: | |
print("NO RECORDS CHANGED") | |
else: | |
print("We're doing a json_file.seek(0)") | |
# json_file.seek(0) | |
# zero_val_recs_chgd+=1 | |
# print("ZERO monthly found") | |
# subscr_data[vendor]["price_yr"] = round((subscr_data[vendor]["price_mo"]*12),2) | |
# print(f'{vendor} ZERO price/yr ammended to {subscr_data[vendor]["price_yr"]}/year') | |
print("RECORDS CHD:", zero_val_recs_chgd) | |
# input("ZERO REST STOP CHECK\n") | |
# return zero_val_recs_chgd, subscr_data, json_file | |
return zero_val_recs_chgd | |
def update_dates(subscr_data): | |
records_delete=[] | |
recs_date_chgd = 0 | |
records_unchg = 0 | |
records_del = 0 | |
print("TODAY:", dateToday, type(dateToday)) | |
# new_sort(subscr_data) | |
# for record in subscr_data: | |
for vendor, vend_dict in subscr_data.items(): | |
""" TEST BLOCK (remove when finished)""" | |
print("VENDOR:", vendor) | |
print("VEND_DICT:", vend_dict) | |
# input("Check vendor/vend_dict") | |
print() | |
""" END TEST BLOCK """ | |
vendor_name = vendor | |
last_bill_str = subscr_data[vendor]['last_bill_date'] | |
last_bill_dateObj = date.fromisoformat(last_bill_str) | |
next_bill_str = subscr_data[vendor]['next_bill_date'] | |
next_bill_dateObj = date.fromisoformat(next_bill_str) | |
status = subscr_data[vendor]['status'] | |
period = subscr_data[vendor]['period'] | |
print(vendor_name) | |
print("Status:",status) | |
print("Period:",period.upper()) | |
print("LAST:", last_bill_str) | |
print("RENEWS:",next_bill_str) | |
print("Date subtraction:", next_bill_dateObj - dateToday) | |
days_sub = (dateToday - next_bill_dateObj).days | |
if days_sub > 0: | |
date_sub = "days in the past" | |
elif days_sub < 0: | |
date_sub = "days from today" | |
else: | |
date_sub = "...Today!" | |
print("DAYS_SUB", type(days_sub),days_sub) | |
renews_msg = (f'{abs(days_sub)} {date_sub}') | |
print(renews_msg) | |
status_msg_completed = ['cancelled','expired','completed'] | |
# REMOVE CANCELLED SUBSCRIPTIONS: | |
if dateToday > next_bill_dateObj and status in status_msg_completed: | |
""" DELETE CANCELLED SUBSCRIPTIONS: """ | |
""" OR leave in dB and put cost @ $0.00 """ | |
""" OR leave in dB and don't change cost """ | |
print(f'RECORD DELETE: {vendor_name}') | |
print("CANCEL FUNCTION ACTIVATED! DEFCON 3!!!") | |
subscr_data[vendor]["status"]="expired" | |
records_delete.append(vendor_name) | |
records_del+=1 | |
# continue | |
# input("CHECK CANCEL FUNCTION") | |
# DOWNGRADE: Warn that a subscription will need to be adjusted | |
if status == 'downgrade' and (dateToday - next_bill_dateObj).days <= 45: | |
""" Need to know 30-45 days in advance to change payment/status""" | |
# "next_bill_date": "2022-09-23" | |
print("To Be Coded: downgrade") | |
recs_date_chgd+=1 | |
# (1) PERIOD = ANNUAL: | |
elif dateToday > next_bill_dateObj and period == 'annual': | |
"""ADJUST ANNUAL RENEWAL DATES: """ | |
# UPDATES annual renewal date if next_bill_date is past | |
print(f'ANNUAL: {vendor_name}') | |
print("RENEW_DATE in record:", next_bill_str, type(next_bill_str)) | |
next_bill_dateObj_adv = next_bill_dateObj + relativedelta(years=1) | |
# Convert to STRING: | |
next_bill_str_nu = next_bill_dateObj_adv.strftime("%Y-%m-%d") | |
print("Revised:") | |
print("Last bill date:",next_bill_str) | |
print("Next bill date:", next_bill_str_nu) | |
""" WRITE to JSON file: 2""" | |
subscr_data[vendor]['next_bill_date'] = next_bill_str_nu | |
"""REPLACE the last_bill_date w renew_date str:""" | |
subscr_data[vendor]['last_bill_date'] = next_bill_str #subscr_data[vendor]['next_bill_date'] | |
# json_file.seek(0) | |
print("\tOKAY to WRITE to JSON: ANNUAL") | |
recs_date_chgd+=1 | |
#### (2) PERIOD = BI-ANNUAL | |
elif dateToday > next_bill_dateObj and period == 'bi-annual': | |
"""ADJUST BI-ANNUAL RENEWAL DATES: """ | |
# UPDATES bi-annual renewal date if next_bill_date is past | |
print(f'BI-ANNUAL: {vendor_name}') | |
print("RENEW_DATE in record:", next_bill_str, type(next_bill_str)) | |
next_bill_dateObj_adv = next_bill_dateObj + relativedelta(months=6) | |
# Convert to STRING: | |
next_bill_str_nu = next_bill_dateObj_adv.strftime("%Y-%m-%d") | |
print("Revised:") | |
print("Last bill date:",next_bill_str) | |
print("Next bill date:", next_bill_str_nu) | |
""" WRITE to JSON file: 3""" | |
subscr_data[vendor]['next_bill_date'] = next_bill_str_nu | |
"""REPLACE the last_bill_date w renew_date str:""" | |
subscr_data[vendor]['last_bill_date'] = next_bill_str #subscr_data[vendor]['next_bill_date'] | |
# json_file.seek(0) | |
print("\tOKAY to WRITE to JSON: BI-ANNUAL") | |
recs_date_chgd+=1 | |
input("PAUSE-CHECK bi-annual") | |
#### (3) PERIOD = QUARTERLY | |
elif dateToday > next_bill_dateObj and period == 'quarterly': | |
print(f'QUARTERLY: {vendor_name}') | |
update_quarterly(subscr_data, vendor) | |
recs_date_chgd+=1 | |
#### (4) PERIOD = MONTHLY | |
elif dateToday > next_bill_dateObj and period == 'monthly': | |
print(f'MONTHLY: {vendor_name}') | |
print("LAST BILLED in record:", last_bill_dateObj) | |
print("RENEW_DATE in record:", next_bill_dateObj) | |
while dateToday > next_bill_dateObj: | |
next_bill_dateObj = next_bill_dateObj + relativedelta(months=1) | |
print(next_bill_dateObj) | |
last_date_dateObj = next_bill_dateObj - relativedelta(months=1) | |
# Convert to STRING: | |
next_bill_str_nu = next_bill_dateObj.strftime("%Y-%m-%d") | |
last_bill_str_nu = last_date_dateObj.strftime("%Y-%m-%d") | |
print("Revised:") | |
print("Last bill date:",last_bill_str_nu) | |
print("Next bill date:", next_bill_str_nu) | |
""" WRITE to JSON file: 4""" | |
subscr_data[vendor]['last_bill_date'] = last_bill_str_nu | |
subscr_data[vendor]['next_bill_date'] = next_bill_str_nu | |
# json_file.seek(0) | |
print("\tOKAY to WRITE to JSON: MONTHLY") | |
recs_date_chgd+=1 | |
#### (5) PERIOD = four_wks (NYT): | |
elif dateToday > next_bill_dateObj and period == 'four_wks': | |
# FOR the NYT sub which is billed every 4 weeks: | |
# UPDATE 'next_bill_date' + 'last_bill_cycle' | |
print(f'FOUR_WKS: {vendor_name}') | |
print("RENEW_DATE in record:", next_bill_str, type(next_bill_str)) | |
"""Keep adding 28 days to last_bill_date until you reach today""" | |
"""next_bill_date """ | |
td = timedelta(28) | |
print("\n NYT test print") | |
while next_bill_dateObj < dateToday: | |
next_bill_dateObj +=td | |
print(next_bill_dateObj) | |
""" CONVERT TO STR then write to JSON FILE""" | |
last_bill_str = (next_bill_dateObj - td).strftime("%Y-%m-%d") | |
next_bill_str = next_bill_dateObj.strftime("%Y-%m-%d") | |
print(f'Last bill date: {last_bill_str}') | |
print(f'Next bill date: {next_bill_str}') | |
""" WRITE to JSON file: 5""" | |
subscr_data[vendor]['last_bill_date']=last_bill_str | |
subscr_data[vendor]['next_bill_date']= next_bill_str | |
# json_file.seek(0) | |
print("\tOKAY to WRITE to JSON: FOUR WKS") | |
recs_date_chgd+=1 | |
#### (6) PERIOD = 3 yrs | |
elif dateToday > next_bill_dateObj and period == '3yrs': | |
print(f'3YRS: {vendor_name}') | |
print("To be coded 3yrs") | |
if "recurring" == "expires": | |
pass | |
# Contract expired, count as cancelled/expired | |
# if date is past mark "status" as "completed" | |
# print("\tOKAY to WRITE to JSON: 4 YRS") | |
recs_date_chgd+=1 | |
else: | |
pass | |
# Make sure records_unchg gets added to | |
#### (7) PERIOD = 4 yrs | |
elif dateToday > next_bill_dateObj and period == '4yrs': | |
print(f'4YRS: {vendor_name}') | |
print("To be coded 4yrs") | |
print(f'4YRS: {vendor_name}') | |
""" IF EXPIRED: """ | |
if "recurring" == "expires": | |
pass | |
# Contract expired, count as cancelled/expired | |
# if date is past mark "status" as "completed" | |
# print("\tOKAY to WRITE to JSON: 4 YRS") | |
recs_date_chgd+=1 | |
else: | |
pass | |
# Make sure records_unchg gets added to | |
else: | |
print("\tNO CHANGE") | |
records_unchg +=1 | |
# RUNNING COUNT: | |
### REMOVE AFTER TESTING: | |
print("RECORDS CHANGED: ", recs_date_chgd) | |
print("RECORDS UN-CHANGED: ", records_unchg) | |
print("RECORDS DELETED: ", records_del) | |
# input("CHECK RECORDS COUNTS") | |
print("RECORDS CHANGED: ", recs_date_chgd) | |
print("RECORDS UN-CHANGED: ", records_unchg) | |
print("RECORDS DELETED: ", records_del) | |
return recs_date_chgd | |
# def maintenance(subscr_data,json_file,f): | |
# def maintenance(subscr_data, json_file): | |
""" THE IDEA HERE IS TO be able to use the same file for testing over and over, | |
resetting the file to its original state, with: | |
with date references that are out of date and 0.0 values """ | |
def restore_source_json(): | |
print("Restore subs-test_MASTER.json file to original (fucked up state)?") | |
ans = input("Y/N").lower() | |
if ans == "y": | |
out_file = "data/subs-test_MASTER.json" | |
read_file = "data/subs-test_FRESH.json" | |
with open(read_file,'r') as in_file: | |
in_file_obj = json.load(in_file) | |
print(type(in_file_obj)) | |
with open(out_file, "w") as outfile: | |
# json.dump(in_file_obj, outfile, sort_keys=True, indent = 4) | |
json.dump(in_file_obj, outfile, indent = 4) | |
else: | |
print("Leaving file alone") | |
return | |
def maintenance(json_file): | |
""" WHEN USED AS A MODULE, THIS IS WHAT IS CALLED | |
Will fix zero values and update the dates | |
The json file is opened here, locally and closed before proceeding""" | |
with open(json_file,'r') as json_file: | |
subscr_data = json.load(json_file) | |
""" | |
FIRST, check the values and fill in the blanks: | |
""" | |
zero_val_recs_chgd = fix_zero_vals(subscr_data, json_file) | |
# fix_zero_vals(subscr_data, json_file) | |
#recs_date_chgd = update_dates(subscr_data) | |
recs_date_chgd = update_dates(subscr_data) | |
if zero_val_recs_chgd != 0 or recs_date_chgd !=0: | |
with open("data/subs-test_MASTER.json", 'w') as file: | |
# json.dump(subscr_data, file, sort_keys=True, indent = 4, ensure_ascii=False) | |
json.dump(subscr_data, file, indent = 4, ensure_ascii=False) | |
json_file.close() | |
else: | |
json_file.close() | |
print("No records changed here") | |
# zero_val_recs_chgd, subscr_data, json_file = fix_zero_vals(subscr_data, json_file,f) | |
# print("RECORDS CHANGED DATES:", recs_date_chgd) | |
print("RECORDS CHANGED w ZERO VALS:", zero_val_recs_chgd) | |
return | |
if __name__ == '__main__': | |
json_file = "data/subs-test_MASTER.json" | |
maintenance(json_file) | |
exit() | |
periods = {'monthly': 20, 'annual': 22, 'four_wks': 1, 'quarterly': 1, '4yrs': 1, 'bi-annual': 1, '3yrs': 1} | |
""" | |
USES dictionary form, each key is the account | |
""" | |
json_file = "data/subs-data-test_MASTER-d.json" | |
with open(json_file,'r+') as json_file: | |
subscr_data = json.load(json_file) | |
""" FIXES ERRORS/UPDATES DATES: """ | |
# recs_date_chgd = update_dates(subscr_data) | |
zero_val_recs_chgd = fix_zero_vals(subscr_data, json_file) | |
# zero_val_recs_chgd = fix_zero_vals(subscr_data) | |
# json.dump(subscr_data, json_file, sort_keys=True, indent = 4) | |
json.dump(subscr_data, json_file, indent = 4) | |
# print("DATES changed:",recs_date_chgd) | |
print("DONE: 'ZERO' errors fixed:", zero_val_recs_chgd) | |
restore_source_json() | |
# TODO: Should I put all of these under one '__main__' or keep them separated? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import calendar | |
import csv | |
from datetime import datetime, date | |
from dateutil.relativedelta import relativedelta | |
import json | |
import pandas as pd | |
import re | |
from collections import namedtuple | |
# Dependencies: | |
from date_config import * | |
from sort_maint import * | |
from sort_maint_records import maintenance | |
from format_date import * | |
# from categories_col import * | |
from reports_print import * | |
from terminal_size import terminal_size | |
# import reports_print | |
# USES data/subscriptions.db OR: | |
# data/subscriptions_bu.db | |
# --------------------------------------------------------------------------- | |
# ------------------------- ORDER OF OPERATIONS: ---------------------------- | |
# Create a connection to db | |
# (OPTIONAL) Evaluate db row by row using 'select_all_subscriptions()' | |
# Use for eval only, comment out when not necessary. | |
# IMPORTANT: Do this at the start and whenever there is an update to the db | |
# OR should you update before every print statment? Because that's only when you see it. | |
# UPDATE SORT ORDER: update_sort_key(): Orders subscriptions by date starting today. | |
# Ask user to choose a function: | |
# PRINT OUT: subscriptions_print_all(cur) | |
# TODO: Separate real subscriptions (option to life) from monthly costs (necessary for life functions) | |
# TODO: add an option for monthly total for previous month | |
# --- But, how will I get those totals w/o going into QUICKEN? | |
# TODO: Warnings of subscriptions that will renew that should be cancelled | |
# --- EXAMPLE: ApPlE music: Next bill date: June 30, 2022 | |
# TODO: UPDATE APPLETV $6.99(7.61)/month; RENEWS: Jan 12, 2023 | |
# ================= !!!!!!!!!!!!!!! ================= | |
# TODO: Add function to add or delete subscriptions rather than do it manually | |
# ================= !!!!!!!!!!!!!!! ================= | |
# ================= (()()()()()()()()()()()()) ================= | |
# REPORT printing section | |
# ================= (()()()()()()()()()()()()) ================= | |
def select_all_subscriptions(): | |
pass | |
print("PRINTING ALL ROWS...") | |
"""Not used at this time but helpful to evaluate entire database""" | |
""" Use only after updates have been run """ | |
# for row in rows: | |
# print(row) | |
# print("TYPE:", type(row)) | |
# print(row[11]) | |
# print("TYPE [11]:", type(row[11])) | |
# # ELIMINATE THIS PAUSE and MAYBE COMMENT OUT THE function altogether | |
# input("ANALYZE ROW TYPE...CONT or QUIT") | |
# print("Done printing rows") | |
return | |
# ############### PRINT OUT ##################### | |
""" SORT BEFORE PRINT: | |
Sort by next_bill_date | |
THEN by | |
name | |
""" | |
# recurring REPORT: | |
def recurring_print(): | |
pass | |
def subs_only_print(): | |
pass | |
# STREAMING REPORT: | |
def streaming_print(): | |
pass | |
total_stream_mo_costs = 0 | |
# Print all subscriptions in order from today | |
# Eliminate 'cancelled' WHERE next_bill_date<today() | |
""" === PRINT HEADER === """ | |
msg= "STREAMING/MONTHLY COSTS" | |
char_pad_num = ((t_width)-len(msg))/2-2 | |
msg_formatted = (f'{"="*int(char_pad_num)} {msg} {"="*int(char_pad_num)}\n') | |
print(msg_formatted) | |
print("ROW SPACES:", len('NAME $/MO ')) | |
print(f'{"NAME":12}{"$/MO":^10}{" "*4}{"$/YR":^8}{" "*5}{"OWNER":8}{"ACCT":^8}{" "*4}{"RENEWS":^8}') | |
# for row in rows: | |
# total_stream_mo_costs += float(row[2]) # Total cost of monthly subscriptions (price_mo) | |
# Format date for printing: | |
# full_date = date_formatted(row[8]) | |
# print(record) | |
# NAME, PRICE/MO, PRICE/YR, RENEWS, OWNER, ACCT | |
# 12 7 4(sp) | |
# print(f'{row[0]:12} ${float(row[2]):>7,.2f}{" "*4}${float(row[4]):>8.2f} {row[1]:>8s} {row[5]:>8s}{" "*4}{full_date:>8}') | |
print("\n\tWhat to Watch:") | |
# for row in rows: | |
# print(row) | |
# print(f'\n\t{row[0]:7s}:\n\t{row[1]}') | |
msg = (f'TOTAL: ${total_stream_mo_costs:,.2f} / month') | |
char_pad_num = ((t_width)-len(msg))/2-2 | |
msg_formatted = (f'\n{"="*int(char_pad_num)} {msg} {"="*int(char_pad_num)}\n') | |
print(msg_formatted) | |
return | |
# SHARED REPORT: | |
def shared_print(): | |
# TODO: Print programs of interest for streaming and possible dates when the start/end | |
# TODO: Clarify this | |
total_shared_mo_costs = 0 | |
""" === PRINT HEADER === """ | |
msg= "SHARED ACCTS :: MONTHLY COSTS" | |
char_pad_num = ((t_width)-len(msg))/2-2 | |
msg_formatted = (f'{"="*int(char_pad_num)} {msg} {"="*int(char_pad_num)}\n') | |
print(msg_formatted) | |
print(f'{"DUE DATE":<20s}{"NAME":22}{"$MONTHLY":^9} {"$ANNUAL":^8} {"PERIOD":<10}{"OWNER":10}{"ACCT":^8} {"NOTES":<22}') | |
# for row in rows: | |
# total_shared_mo_costs += float(row[2]) # Total cost of monthly subscriptions (price_mo) | |
# Format date for printing: | |
# full_date = date_formatted(row[8]) | |
# print(record) | |
# print("{:20}{:22}${:>7,.2f} ${:>9.2f} {:>8s} {:>8s} {:>8} {:<22}".format( | |
# full_date, | |
# row[0], | |
# float(row[2]), | |
# float(row[4]), | |
# row[9], # period | |
# row[1], # owner | |
# row[5], # acct | |
# row[11])) # Notes | |
msg = (f'TOTAL: ${total_shared_mo_costs:,.2f}') | |
char_pad_num = ((t_width)-len(msg))/2-2 | |
msg_formatted = (f'\n{"="*int(char_pad_num)} {msg} {"="*int(char_pad_num)}\n') | |
print(msg_formatted) | |
return | |
# PRINT ALL REPORT: | |
def subscriptions_date_sorted(): | |
total_sub_mo_costs = 0 | |
col_names = ["full_date", "name", "float(subscr_data[name]['price_mo']","float(subscr_data[name]['price_yr']", | |
"subscr_data[name]['period']","subscr_data[name]['owner']","subscr_data[name]['status']","subscr_data[name]['notes']"] | |
subs_df = pd.read_json("data/subs-test_MASTER.json") | |
df_swapped = subs_df.transpose() | |
selected_columns = col_names | |
# df_selected = df_swapped[col_names] | |
print(df_swapped.head) | |
df_selected = df_selected[df_selected['status']] != "expired" | |
df_selected.sort_values(by='full_date', inplace=True) | |
print("SUBS_DF:") | |
print(subs_df) | |
input("CHECK Dataframe") | |
# Print all subscriptions in order from today | |
""" === PRINT HEADER === """ | |
print(f'{"="*58}SUBSCRIPTIONS/MONTHLY HARD COSTS{"="*58}') | |
print("\n") | |
print(f'{"DUE DATE":<20s}{"NAME":22}{"$MONTHLY":^9} {"$ANNUAL":^8} {"PERIOD":<10}{"OWNER":10}{"ACCT":^8} {"NOTES":<22}') | |
# for row in rows: | |
# total_sub_mo_costs += float(row[2]) # Total cost of monthly subscriptions (price_mo) | |
# Format date for printing: | |
# full_date = date_formatted(row[8]) | |
# # print(record) | |
# print("{:20}{:22}${:>7,.2f} ${:>9.2f} {:>8s} {:>8s} {:>8} {:<22}".format( | |
# full_date, | |
# row[0], | |
# float(row[2]), | |
# float(row[4]), | |
# row[10], #period | |
# row[1], | |
# row[5], | |
# row[12])) | |
# print(f'\n{"="*42} ${total_sub_mo_costs:,.2f} {"="*58}') | |
return | |
def subscriptions_print_all(): # ADDED 'cur' as TEST using separated Menu_Tree() | |
total_mo_subs = 0 | |
# Print all subscriptions in order from today | |
# Eliminate 'cancelled' WHERE next_bill_date<today() | |
# cur.execute("SELECT * from subscriptions WHERE cancel_chg!='cancelled' ORDER BY next_bill_date asc") | |
# rows = cur.fetchall() | |
""" === PRINT HEADER === """ | |
print(f'{"="*58}SUBSCRIPTIONS{"="*58}') | |
print("\n") | |
print(f'{"RENEW DATE":<16s}{"NAME":22}{"$/MONTHLY":^9} {"$/ANNUAL":^8} {"PERIOD":<10} {"OWNER":10} {"STATUS":<10} {"NOTES":<22}') | |
# for name, details in subscr_data.items(): | |
for name in subscr_data.keys(): | |
status_msg_completed = ['cancelled','expired','completed'] | |
# REMOVE CANCELLED SUBSCRIPTIONS: | |
if subscr_data[name]['status'] == 'expired': | |
continue | |
if isinstance(subscr_data[name]['price_mo'], list): | |
print("LIST:", subscr_data[name]['price_mo']) | |
total_mo_subs += sum(subscr_data[name]['price_mo']) / len(subscr_data[name]['price_mo']) | |
else: | |
total_mo_subs += subscr_data[name]['price_mo'] # Total cost of monthly subscriptions (price_mo) | |
# Format date for printing: | |
full_date = date_formatted(subscr_data[name]['next_bill_date']) | |
# RENEW DATE::NAME::$MONTHLY::$ANNUAL::BILLS::OWNER::NOTES | |
# print("\nROW:\n",row) TESTING PURPOSES ONLY | |
# {"RENEW DATE"}{"NAME"}{"$/MONTHLY"}{"$/ANNUAL"}{"PERIOD"}{"OWNER"}{"NOTES"') | |
print("{:16}{:22}${:>8,.2f} ${:>9.2f} {:<10s} {:<6s} {:<10s} {:<22}".format( | |
full_date, | |
name, | |
float(subscr_data[name]['price_mo']), | |
float(subscr_data[name]['price_yr']), | |
subscr_data[name]['period'], | |
subscr_data[name]['owner'], | |
subscr_data[name]['status'], | |
subscr_data[name]['notes'])) | |
# Print Footer: | |
# print(f'TOTAL MONTHLY COSTS (subscriptions only): ${}') | |
print(f'\n{"="*42} ${total_mo_subs:,.2f} {"="*58}') | |
# ================= (()()()()()()()()()()()()) ================= | |
# END REPORT printing section | |
# ================= (()()()()()()()()()()()()) ================= | |
# ================= (()()()()()()()()()()()()) ================= | |
""" UPDATE RECORDS BEFORE EXECUTING THE REST OF THE PROGRAM: """ | |
# ================= (()()()()()()()()()()()()) ================= | |
# Could this be put into one function: See notes at bottom of update_sort.py | |
def update_sort_key(): | |
''' Updates the next_bill_date in the db so that subscription refresh stays current''' | |
# Update annual renewals | |
maintenance() | |
# update_annual() # TODO | |
# update_nyt() | |
# update_next_bill_date() | |
# update_quarterly() | |
def test_outboarding(): | |
print("dateToday:",dateToday) | |
input("check import date_config") | |
# update_nyt() | |
update_quarterly() | |
# update_next_bill_date() | |
def dummy_function(): | |
print('\n The dummy function has finished running.') | |
def get_vend_names(subscr_data): | |
# Make a list (sorted) of names/vendors in subscriptions: | |
# This function could be under 'housekeeping' | |
print("This will be the list of vendors") | |
# TODO: is this used anymore? There is no 'name' in this dictionary/json | |
# vendor_data = sorted(subscr_data, key=lambda x: x['name']) | |
vendor_data = list(subscr_data.keys()) | |
for v in vendor_data: | |
print(v) | |
return vendor_data | |
def vendor_update_mo_yr(): | |
pass | |
# periodically update the price_mo and price_yr columns if there is no info | |
# Conversely, you could have this function run when adding or updating a vendor | |
def vendor_update(): | |
# Ask for Vendor name, compare to vendor list to see if it's valid | |
vendor_name_inp = input("What vendor do you want to update? ") | |
# vendor_data = get_vend_names() | |
is_looping = True | |
# WHILE loop: | |
while is_looping == True: | |
for vendor_name in vendor_data: | |
if vendor_name_inp.lower()== vendor_name.lower(): | |
# print("MATCH", vendor_name) DELETE WHEN FINISHED | |
is_looping = False | |
break | |
else: | |
continue | |
if not is_looping: | |
break # break out of outer loop | |
else: | |
print("Let's try it again.") | |
vendor_name = input("What vendor do you want to update? ") | |
print("Updating:", vendor_name) | |
# TODO: CONTINUE EDITING??? | |
# TODO: if vendor_name == 'Xcel' or vendor_name == 'Verizon': | |
# print("Adding cost to amt_paid list") | |
print("Continue Updating? (y/n)") | |
cont_choice = input("").lower() | |
is_looping = True | |
while is_looping == True: | |
if cont_choice=='y': | |
loop_choice = update_vendor(vendor_name) | |
if loop_choice==False: | |
Menu_Tree() | |
elif cont_choice=='n': | |
break | |
# print(row w/ column names) | |
# input("CHECK DB for UPDATE...") # DELETE WHEN FINISHED | |
Menu_Tree() | |
def vendor_delete(): | |
print("This is where you'll delete an existing vendor") | |
# vendor_name = input("Delete which Vendor? ") | |
still_selecting = True | |
while still_selecting: | |
vendor_name = input(" Delete which Vendor? ") | |
# menu_choice = input(' Enter your selection: ') | |
# if vendor_name.lower() in vendor_data: | |
if vendor_name in vendor_data: | |
still_selecting = False | |
else: | |
print(f' "{vendor_name}" is not a valid entry.') | |
print(f' DELETE: {vendor_name}?') | |
# YES or NO | |
del_choice = input(" YES (y) or NO(any key)").lower() | |
if del_choice == "y": | |
print(f' {vendor_name} DELETED') | |
else: | |
print(f' {vendor_name} DELETED') | |
def vendor_add(): | |
new_vendor_data=[] | |
print("This is where you'll add a new vendor") | |
print("Please add the following information:") | |
print("Press RTN if info isn't handy.") | |
name = input("Vendor Name?: ") | |
# TODO-check whether vendor exists | |
is_looping = True | |
# WHILE loop: | |
while is_looping == True: | |
for vendor in vendor_data: | |
if name.lower()== vendor.lower(): | |
print("MATCH", name) | |
continue | |
else: | |
is_looping = False | |
break | |
if not is_looping: | |
break # break out of outer loop | |
else: | |
print("That vendor is already in the database.") | |
# Escape to main menu or continue | |
name = input("Vendor Name? ") | |
new_vendor_data.append(name) | |
# Limit range of category list because you already have the 'name' | |
for i in range(1,len(cats_l_alt_order)): | |
if i == 3: | |
var = float(input(cats_l_alt_order[i][2])) | |
else: | |
var = input(cats_l_alt_order[i][2]) | |
new_vendor_data.append(var) | |
print("New Vendor Data list:") | |
for i, val in enumerate(new_vendor_data): | |
print(f'{i}: {val}') | |
if val=="": | |
if i == 7: | |
# last_bill_date | |
new_vendor_data.pop(7) | |
# copy start_date | |
new_vendor_data.insert(7, new_vendor_data[6]) | |
if i ==8: | |
# next_bill_date | |
if new_vendor_data[4]=='annual': | |
# Add year to start_date | |
next_bill = date.isoformat(date.fromisoformat(new_vendor_data[6]) + relativedelta(months=1)) | |
elif new_vendor_data[4]=='monthly': | |
# Add year to start_date | |
next_bill = date.isoformat(date.fromisoformat(new_vendor_data[6]) + relativedelta(months=1)) | |
elif new_vendor_data[4]=='monthly': | |
# This has to be worked out w/ the quarterly dues thing. | |
next_bill = date.isoformat(date.fromisoformat(new_vendor_data[6]) + relativedelta(months=3)) | |
elif new_vendor_data[4]=='four-wks': | |
next_bill = date.isoformat(date.fromisoformat(new_vendor_data[6]) + timedelta(28)) | |
new_vendor_data.pop(8) | |
new_vendor_data.insert(8, next_bill) | |
period = new_vendor_data[4] | |
pmt = new_vendor_data[3] | |
if i == 12: | |
# price_mo | |
def price_month(operator, x): | |
return { | |
'annual': lambda: x / 12, | |
'quarterly': lambda: x *4 / 12, | |
'monthly': lambda: x, | |
'four-wks': lambda: x / 4 * 52 / 12, | |
}.get(operator, lambda: None)() | |
price_mo = price_month(period, pmt) | |
new_vendor_data.pop(12) | |
new_vendor_data.insert(12, price_mo) | |
if i == 13: | |
# price_yr | |
def price_year(operator, x): | |
return { | |
'annual': lambda: x, | |
'monthly': lambda: x*12, | |
'quarterly': lambda: x *4, | |
'four-wks': lambda: x / 4 * 52, | |
}.get(operator, lambda: None)() | |
price_yr = price_year(period, pmt) | |
new_vendor_data.pop(13) | |
new_vendor_data.insert(13, price_yr) | |
def reminder(): | |
pass | |
def say_goodbye(): | |
f.close() | |
print('\nExiting through the gift shop.') | |
exit() | |
def get_cols(subscrip_data): | |
# This may be used to determine which columns print to the terminal | |
col_len={ | |
"acctId": 0, | |
"amt_paid": 0, | |
"category": 0, | |
"expire_notice": 0, | |
"expires": 0, | |
"films": 0, | |
"last_bill_date": 0, | |
"name": 0, | |
"next_bill_date": 0, | |
"notes": 0, | |
"owner": 0, | |
"paid_from": 0, | |
"period": 0, | |
"price_mo": 0, | |
"price_yr": 0, | |
"pwd": 0, | |
"recur": 0, | |
"recurring": 0, | |
"start_date": 0, | |
"status": 0, | |
"user": 0, | |
"watch_now_url": 0 | |
} | |
# Get a list of column names | |
# print("RECORDS:") # TROUBLESHOOTING | |
print("Type subscrip_data:", type(subscrip_data)) | |
for record in subscrip_data: | |
print(record, type(record)) | |
for col, v in subscrip_data[record].items(): | |
exist_size = len(str(col_len[col])) | |
new_size = len(str(v)) | |
# print(f'{col}:') # TROUBLESHOOTING | |
# print(f'Existing:{exist_size}') # TROUBLESHOOTING | |
# print(f'New.....:{new_size}') # TROUBLESHOOTING | |
if new_size > exist_size: | |
col_len[col]=new_size | |
# print() | |
print("col_len dict:") | |
for k,v in col_len.items(): | |
print(f'{k:16s}:{v}') | |
input("Check col_len dictionary") | |
# print("col_len", col_len) # TROUBLESHOOTING | |
return col_len | |
# TODO: | |
# Option to print: | |
# Last month | |
# YTD | |
# Last Year | |
# Totals | |
# Add columns: Monthly, Yearly | |
class Menu_Tree(): | |
def __init__(self): | |
self.all_menus = { | |
'main_menu': { | |
"[1] Print Reports": 'print_menu', | |
'[5] Set reminder cancel/change':reminder, | |
"[2] Update Vendor": 'update_menu', | |
"[3] Delete Vendor": vendor_delete, | |
'[4] Add Vendor': vendor_add, | |
'[5] Set reminder cancel/change':reminder, | |
"[0] Exit the program.": say_goodbye}, | |
'print_menu': { | |
"[1] REPORT: Print ALL (alphabetical)": subscriptions_print_all, | |
"[2] REPORT: Print ALL (dated)": subscriptions_date_sorted, | |
"[3] REPORT: Recurring":recurring_print, | |
"[4] REPORT: Subscriptions ONLY": subs_only_print, | |
"[5] REPORT: Streaming": streaming_print, | |
"[6] REPORT: Shared ONLY": shared_print, | |
"[7] REPORT: Upcoming Cancellations": dummy_function, | |
"[8] REPORT: Software": dummy_function, | |
'[9] Go back to main page': 'main_menu', | |
"[0] Exit the program.": say_goodbye}, | |
# Should iterate through the column names, prompting for input | |
'add_menu': { | |
"[1] Add new vendor": vendor_add, | |
"[2] Vendor update": vendor_update, | |
"[3] Delete vendor": vendor_delete, | |
'[4] Go back to main page': 'main_menu', | |
"[0] Exit the program.": say_goodbye}, | |
# Should ask for a name to delete. If that name isn't present, list the names. | |
'delete_menu': { | |
"[1] Search list of: ": dummy_function, | |
"[2] Search a: ": dummy_function, | |
'[4] Go back to main page': 'main_menu', | |
"[0] Exit the program.": say_goodbye}, | |
# Should have a drop down menu of column names | |
'update_menu': { | |
"[1] Update a Record: ": vendor_update, | |
"[2] Update price/mo /yr ": vendor_update_mo_yr, | |
"[3] Is there anything else? ; dummy_function" | |
'[4] Return to the main menu': 'main_menu', | |
"[0] Exit the program.": say_goodbye}} | |
self.run_the_current_command('main_menu') | |
def run_the_current_command(self, command): | |
if type(command) == str: | |
self.current_menu = command | |
command = self.run_the_menus(self.all_menus[command]) | |
else: | |
command() | |
self.run_the_menus(self.all_menus[self.current_menu]) | |
def run_the_menus(self, menu): | |
look_up_table = {label[1: 2]: label for label in menu} | |
# print("Look UP Table:", look_up_table) | |
# for k,v in look_up_table.items(): | |
# print(f'{k}:{v}') | |
# input("PAUSE to check out look_up_table") | |
""" MENU FORMATTING: """ | |
# row_pad = (terminal_rows-(len(menu)+2))/2 | |
# print('\n'* int(row_pad)) | |
print(f'\n{"="* terminal_cols}\n') | |
lbl_space = int(terminal_cols/10) | |
""" MENU PRINT: """ | |
for label in menu: | |
print(f'{"- " * lbl_space}{label}') | |
# print(' - ', label) | |
print(f'\n{"="* terminal_cols}') | |
""" SELECTION LOOP: """ | |
still_selecting = True | |
while still_selecting: | |
menu_choice = input(' Enter your selection: ') | |
if menu_choice != '' and menu_choice in look_up_table: | |
still_selecting = False | |
command = menu[look_up_table[menu_choice]] | |
else: | |
print(f' "{menu_choice}" is not a valid entry.') | |
# print('\n'* int(row_pad)) | |
self.run_the_current_command(command) | |
def json_parse(json_file): | |
# Opening JSON file | |
f = open(json_file,'r+') | |
# return JSON object as LIST of dictionaries: | |
subscr_data = json.load(f) | |
# print 1st record for confirmation: | |
# print(subscr_data[0]) | |
# PRINT A LIST of Vendor Names: | |
# for i in subscr_data: | |
# print(i['name']) | |
# input("PAUSE FOR name list") | |
# PRINT json data: | |
# print(json.dumps(subscr_data, indent=2)) | |
# input("CHECK JSON FILE") | |
return subscr_data, f # json data in list/dict form | |
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#- | |
#-#-#-#-#-#-#-#-#-#-#-#- PROGRAM START #-#-#-#-#-#-#-#-#-#-#-#- | |
def main(): | |
# DECLARE ALL GLOBAL variables | |
global f | |
global json_file | |
global subscr_data | |
global vendor_data # Sorted (alphabetically) List of names | |
global cols_data | |
global owners | |
global cats_l | |
global cats_l_alt_order | |
global terminal_cols, terminal_rows | |
owners = ['mk', 'shared', 'mo'] | |
categories = [] # Perhaps isn't used. | |
# OPEN json for printing, sorting, etc: | |
# json_file = "data/subscriptions.json" | |
json_file = "data/subs-test_MASTER.json" | |
# Get json data in list/dict form: | |
subscr_data,f = json_parse(json_file) | |
# UPDATE should happen here: | |
maintenance(json_file) #sort_maint.py opens, modifies, and closes json file locally | |
# Get size of terminal window | |
terminal_cols, terminal_rows = terminal_size() # May be handled separately | |
print() | |
print(f'T-Size:{terminal_cols} x {terminal_rows}') | |
input("Check terminal size") | |
# Sorted (alphabetically) List of Vendor names | |
# Used in: vendor_update(), vendor_delete(), vendor_add()--UPDATE | |
vendor_data = get_vend_names(subscr_data) | |
cols_data = get_cols(subscr_data) | |
''' MENU should start here''' | |
Menu_Tree() | |
# menu_test() | |
input("CHECK menu-test") | |
# PRINT AGAIN TO SEE IF update_sort_key worked: | |
# select_all_subscriptions() | |
# update_print(, cur) | |
subscriptions_print_all() | |
if __name__ == '__main__': | |
main() | |
# KEY: 'columns' are in dictionary form | |
""" | |
['acct'] | |
['acct_num'] | |
['amt_paid'] | |
['status'] | |
['category'] | |
['films'] | |
['last_bill_date'] | |
['name'] | |
['next_bill_date'] | |
['notes'] | |
['owner'] | |
['period'] | |
['price_mo'] | |
['price_yr'] | |
['recurring'] | |
['start_date'] | |
""" | |
# Example of a nice list comprehension | |
# cols_data= list(map(lambda x: x[0],columns)) | |
# RENAME next_bill_date as "renews" | |
# CANCELLED Should have date of expiry or ['status'] == "expired" | |
# RULES: | |
# if it's an annual sub, it should have a renew/end date | |
# EXAMPLES OF: | |
# calendar module: | |
""" Gets a month name from a date. Pretty simple this time but the module has many more features!""" | |
# format | |
# date/datetime | |
# csv module | |
# date.fromisoformat | |
"""The class method fromisoformat() from the date class of Python datetime module, constructs a date object from a string containing date in ISO format. i.e., yyyy-mm-dd.""" | |
# relativedelta(months=n) | |
# csv.DictReader(data) | |
# Possible addition: | |
# Software tracking: | |
# Separate table | |
# Company | |
# Software name | |
# Incl ser# | |
# Website | |
# Last purchased | |
# Renewal frequency or something like this, see: Quicken, Adobe (monthly, annual, infrequent) | |
# Up to date? | |
# Version |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
def terminal_size(): | |
# print("Terminal size: os:") | |
size = os.get_terminal_size() | |
# print("Size:",size, type(size)) | |
# print(f'Height: {size[1]}') | |
# print(f'Width: {size[0]}') | |
width=size[0] | |
height=size[1] | |
return width, height | |
t_width = os.get_terminal_size()[0] | |
t_height = os.get_terminal_size()[1] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment