From 455784fbd3e24622266bed865363bd765876e34f Mon Sep 17 00:00:00 2001 From: parfeniukink <45270625+parfeniukink@users.noreply.github.com> Date: Sun, 26 Jun 2022 21:17:37 +0300 Subject: [PATCH] Add debts category (#42) * added a new category to the init_tables script * Created a release 4.2 database migration script * New category is added to the base mapping * Alalytics messages are updated * Added a persent of costs in order to see its relation with the salary --- scripts/db/init_tables.sql | 1 + scripts/db/release_4_2_migration.py | 4 + src/analytics/services.py | 128 ++++++++++++++++------------ src/categories/domain.py | 1 + 4 files changed, 81 insertions(+), 53 deletions(-) create mode 100644 scripts/db/release_4_2_migration.py diff --git a/scripts/db/init_tables.sql b/scripts/db/init_tables.sql index 77ffc2b..7d32ab6 100644 --- a/scripts/db/init_tables.sql +++ b/scripts/db/init_tables.sql @@ -73,6 +73,7 @@ INSERT INTO categories (name) VALUES ('🎁 Gifts'), ('📦 Other'), ('💼 Business'), + ('💸 Debts'), ('🔄 Currency transactions'); INSERT INTO equity (currency, value) VALUES diff --git a/scripts/db/release_4_2_migration.py b/scripts/db/release_4_2_migration.py new file mode 100644 index 0000000..d67d19a --- /dev/null +++ b/scripts/db/release_4_2_migration.py @@ -0,0 +1,4 @@ +from db import database + +with database.cursor() as cursor: + cursor.execute("INSERT INTO categories (name) VALUES ('💸 Debts')") diff --git a/src/analytics/services.py b/src/analytics/services.py index 38cc032..06d36cd 100644 --- a/src/analytics/services.py +++ b/src/analytics/services.py @@ -25,74 +25,84 @@ def __costs_by_category(cls, costs: list[Cost]): attr = "category_id" return groupby(sorted(costs, key=attrgetter(attr)), key=attrgetter(attr)) + @classmethod + def __get_costs_block(cls, name: str, total: Decimal, sign: str = "", percent: Decimal | None = None) -> str: + """Returns specific costs block. Used for basic analytics.""" + + if not total: + return "" + + return "".join( + [ + "\n", + LINE_ITEM.format( + key=BOLD.format(text=name), + value=get_number_in_frames(total), + ), + sign, + ITALIC.format(text=f" ({percent}%)") if percent else "", + ] + ) + @classmethod def __get_formatted_costs_by_currency_basic( - cls, categories_by_id: dict[int, Category], costs: list[Cost] | None + cls, categories_by_id: dict[int, Category], costs: list[Cost] | None, incomes: list[Income] ) -> str: text = "" if not costs: return text + total_salary: Decimal = sum(incomes) # type: ignore + currency_transaction_category: Category = CategoriesService.get_by_name(CategoriesMapping.CURRENCY_TRANSACTIONS) + debts_category: Category = CategoriesService.get_by_name(CategoriesMapping.DEBTS) - # NOTE: Get the real costs without currenty transaction and the second one separately + # NOTE: Get the real costs, currenty transaction costs and debts separately currency_transaction_costs: list[Cost] = [ cost for cost in costs if cost.category_id == currency_transaction_category.id ] - real_costs: list[Cost] = [cost for cost in costs if cost.category_id != currency_transaction_category.id] + debts_costs: list[Cost] = [cost for cost in costs if cost.category_id == debts_category.id] + real_costs: list[Cost] = [ + cost for cost in costs if cost.category_id not in [currency_transaction_category.id, debts_category.id] + ] - total_costs_sum: Decimal = sum(real_costs) # type: ignore + real_costs_sum: Decimal = sum(real_costs) # type: ignore # NOTE: Add USD dollar sign if needed sign = "$" if costs[0].currency == Currencies.get_database_value("USD") else "" for id, costs_group in cls.__costs_by_category(real_costs): category: Category = categories_by_id[id] - total_costs: Decimal = sum(costs_group) # type: ignore + total_real_costs: Decimal = sum(costs_group) # type: ignore text += "".join( - ("\n", LINE_ITEM.format(key=category.name, value=get_number_in_frames((total_costs))), sign) + ("\n", LINE_ITEM.format(key=category.name, value=get_number_in_frames((total_real_costs))), sign) ) - percent = (total_costs * Decimal("100") / total_costs_sum).quantize(Decimal("0.1")) + percent = (total_real_costs * Decimal("100") / real_costs_sum).quantize(Decimal("0.1")) text += "".join((INDENTION, ITALIC.format(text=f"({percent}%)"))) - # NOTE: Add total costs block - text += ( - "".join( - [ - "\n\n", - "-" * 10, - "\n", - LINE_ITEM.format( - key=BOLD.format(text="📉 Real costs"), - value=get_number_in_frames(total_costs_sum), - ), - sign, - ] - ) - if total_costs_sum > Decimal("0") - else "" + # NOTE: Generate the result message + # ################################# + + # NOTE: Add a separator + text += ("\n" * 2) + ("-" * 10) + + # NOTE: Add costs persentage in order to see it with salary relation + real_costs_percentage = ( + ((real_costs_sum / total_salary) * 100).quantize(Decimal("0.01")) if total_salary else Decimal("0") ) - # NOTE: Add currency_transaction costs block - text += ( - "".join( - ( - "\n", - LINE_ITEM.format( - key=BOLD.format(text="🔄 Currency transactions"), - value=get_number_in_frames( - # NOTE: sum() always return Decimal for costs - sum(currency_transaction_costs), # type: ignore - ), - ), - sign, - ) - ) - if currency_transaction_costs - else "" + # NOTE: Add real costs, currency transactions costs and debts costs blocks + text += cls.__get_costs_block( + name="📉 Real costs", total=real_costs_sum, sign=sign, percent=real_costs_percentage + ) + text += cls.__get_costs_block( + name=CategoriesMapping.CURRENCY_TRANSACTIONS, + total=sum(currency_transaction_costs), # type: ignore + sign=sign, ) + text += cls.__get_costs_block(name=CategoriesMapping.DEBTS, total=sum(debts_costs), sign=sign) # type: ignore return text @@ -156,35 +166,47 @@ def __get_basic_report( costs: dict[str, list[Cost]], incomes: dict[str, list[Income]], ) -> str: - message = BOLD.format(text=date) + message: str = BOLD.format(text=date) uah_title = CURRENCY_REPORT_TITLE_MESSAGE.format(currency=Currencies.UAH.value) usd_title = CURRENCY_REPORT_TITLE_MESSAGE.format(currency=Currencies.USD.value) + # NOTE: Get all incomes by category (salary or other incomes) + uah_salary_incomes: list[Income] = [ + i for i in incomes.get(Currencies.get_database_value("UAH"), []) if i.salary + ] + uah_other_incomes: list[Income] = [ + i for i in incomes.get(Currencies.get_database_value("UAH"), []) if i.salary is False + ] + usd_salary_incomes: list[Income] = [ + i for i in incomes.get(Currencies.get_database_value("USD"), []) if i.salary + ] + usd_other_incomes: list[Income] = [ + i for i in incomes.get(Currencies.get_database_value("USD"), []) if i.salary is False + ] + + # NOTE: Create costs messages uah_costs_message = cls.__get_formatted_costs_by_currency_basic( - categories_by_id, costs.get(Currencies.get_database_value("UAH")) + categories_by_id, costs.get(Currencies.get_database_value("UAH")), uah_salary_incomes ) usd_costs_message = cls.__get_formatted_costs_by_currency_basic( - categories_by_id, costs.get(Currencies.get_database_value("USD")) + categories_by_id, costs.get(Currencies.get_database_value("USD")), usd_salary_incomes ) + # NOTE: Create incomes messages uah_salary_incomes_message = IncomesService.get_formatted_incomes( - [i for i in incomes.get(Currencies.get_database_value("UAH"), []) if i.salary], INCOME_SALARY_SOURCE_MESSAGE + uah_salary_incomes, INCOME_SALARY_SOURCE_MESSAGE ) usd_salary_incomes_message = IncomesService.get_formatted_incomes( - [i for i in incomes.get(Currencies.get_database_value("USD"), []) if i.salary], INCOME_SALARY_SOURCE_MESSAGE - ) - uah_other_incomes_message = IncomesService.get_formatted_incomes( - [i for i in incomes.get(Currencies.get_database_value("UAH"), []) if not i.salary], - INCOME_OTHER_SOURCE_MESSAGE, - ) - usd_other_incomes_message = IncomesService.get_formatted_incomes( - [i for i in incomes.get(Currencies.get_database_value("USD"), []) if not i.salary], - INCOME_OTHER_SOURCE_MESSAGE, + usd_salary_incomes, INCOME_SALARY_SOURCE_MESSAGE ) + uah_other_incomes_message = IncomesService.get_formatted_incomes(uah_other_incomes, INCOME_OTHER_SOURCE_MESSAGE) + usd_other_incomes_message = IncomesService.get_formatted_incomes(usd_other_incomes, INCOME_OTHER_SOURCE_MESSAGE) + # NOTE: Concatenate UAH data together if uah_costs_message or uah_salary_incomes_message: message += "\n".join([uah_title, uah_costs_message, uah_salary_incomes_message, uah_other_incomes_message]) + # NOTE: Concatenate USD data together if usd_costs_message or usd_salary_incomes_message: message += "\n".join([usd_title, usd_costs_message, usd_salary_incomes_message, usd_other_incomes_message]) diff --git a/src/categories/domain.py b/src/categories/domain.py index 37938ca..f372069 100644 --- a/src/categories/domain.py +++ b/src/categories/domain.py @@ -30,4 +30,5 @@ class CategoriesMapping(str, Enum): GIFTS = "🎁 Gifts" OTHER = "📦 Other" BUSINESS = "💼 Business" + DEBTS = "💸 Debts" CURRENCY_TRANSACTIONS = "🔄 Currency transactions"