diff --git a/jarvis/abilities/finance/ability.py b/jarvis/abilities/finance/ability.py index 1c63513..9e7df05 100644 --- a/jarvis/abilities/finance/ability.py +++ b/jarvis/abilities/finance/ability.py @@ -43,13 +43,13 @@ def before_create(self) -> None: {"no_expenses_matched": "Det finns inga utgifter " "sparade med angivna " "kriterier", - "no_users_matches": "Jag hittade ingen " - "användare som matchade. " - "Om du angav ett namn, " - "kontrollera stavningen. " - "Om du inte angav något, " - "kontrollera att du är " - "registrerad."}) + "no_users_matches": "Jag hittade ingen " + "användare som matchade. " + "Om du angav ett namn, " + "kontrollera stavningen. " + "Om du inte angav något, " + "kontrollera att du är " + "registrerad."}) def add_expense(self, message: Message): """ @@ -59,6 +59,7 @@ def add_expense(self, message: Message): expense_name = message.entities.get("expense_name") expense_value = message.entities.get("expense_value") store_for_username = extract_username(message, "store_for_username") + recurring = message.entities.get("recurring") if not expense_value and expense_name: return Reply("Du måste ange både namn och " @@ -71,8 +72,11 @@ def add_expense(self, message: Message): expense = Expense.objects.create(price=expense_value, expense_name=expense_name, - user_reference=user) - stream.put(f"Utgiften sparades för {user.username.capitalize()}. ") + user_reference=user, + recurring_monthly=recurring) + stream.put(f"Utgiften sparades för {user.username.capitalize()}.") + if recurring: + stream.put("Utgiften är markerad som återkommande.") stream.put(expense) return stream @@ -83,6 +87,7 @@ def get_expenses(self, message: Message): username_for_query = extract_username(message, "username_for_query") get_latest = message.entities["show_most_recent_expense"] last_accounting_entry_date = self._get_last_accounting_entry_date() + recurring_expenses_only = message.entities["recurring_expenses_only"] stream = ReplyStream() try: @@ -92,25 +97,32 @@ def get_expenses(self, message: Message): return Reply(self.storage["default_replies"]["no_users_matches"]) if get_latest: - latest_expense = Expense.objects.latest(user=user) - return Reply(latest_expense) + return Reply(Expense.objects.latest(user=user)) + if recurring_expenses_only: + return ReplyStream(Expense.objects.recurring(user=user)) expenses = Expense.objects.within_period( range_start=last_accounting_entry_date, user=user) + recurring_expenses_only = Expense.objects.recurring(user=user) - if not expenses: - return Reply( - self.storage["default_replies"]["no_expenses_matched"]) + if not expenses and not recurring_expenses_only: + return Reply(self.storage["default_replies"]["no_expenses_matched"]) if message.entities.get("sum_expenses"): expenses_sum = expenses.sum("price") + recurring_sum = recurring_expenses_only.sum("price") period_str = (f"{last_accounting_entry_date.strftime('%Y-%m-%d')} - " f"{datetime.now().strftime('%Y-%m-%d')}") stream.put(f"Nuvarande konteringsperiod: {period_str}") return Reply(f"Summan för {user.username.capitalize()} " f"under denna konteringsperiod är hittills: " - f"**{expenses_sum}**:-") + f"**{expenses_sum + recurring_sum}**:-.\n" + f"Varav återkommande utgifter: **{recurring_sum}**:-\n" + f"Varav engångsutgifter: **{expenses_sum}**:-") + + expenses = list(expenses.all()) + expenses.extend(recurring_expenses_only.all()) return ReplyStream(expenses) @classmethod @@ -296,36 +308,34 @@ def calculate_split_expenses(self, message): reply_stream = ReplyStream() accounting_entry = AccountingEntry() dt_fmt = pyttman.app.settings.DATETIME_FORMAT - reply_stream.put(f"Konteringsunderlag: {datetime.now().strftime(dt_fmt)}") - period = f"{query_range_start.strftime('%Y-%m-%d')} - " \ - f"{datetime.now().strftime('%Y-%m-%d')}" + reply_stream.put(f"**Konteringsunderlag**: {datetime.now().strftime(dt_fmt)}") + period = f"**{query_range_start.strftime('%Y-%m-%d')} - " \ + f"{datetime.now().strftime('%Y-%m-%d')}**" reply_stream.put(f"Konteringsperiod: {period}") msg = f"**Konteringsperiod: {period}**\n" valid = False while calculations: calculation = calculations.pop() - if not calculation.quota_of_total: - continue username = calculation.user.username.capitalize() accounting_entry.participants.append(calculation.user) - msg = f"**{username}**:\n" - msg += f"{username} har betalat {calculation.paid_amount:.2f}:- " \ - f"denna period vilket utgör {calculation.quota_of_total * 100:.2f}% av " \ - f"det totala beloppet {calculator.total_expense_sum:.2f}:-.\n" - msg += f"{username} har en månadslön som motsvarar " \ - f"{calculation.income_quotient * 100:.2f}:- av den " \ + msg = f"\n:moneybag: **{username}:**\n" + msg += f"**Summa:** {calculation.paid_amount:.2f}:- \n" \ + f"**Belastning:** {calculation.quota_of_total * 100:.2f}% av " \ + f"totalen {calculator.total_expense_sum:.2f}:-.\n" + if calculation.recurring_expenses: + msg += f"**Varav återkommande utgifter:** {calculation.recurring_expenses:.2f}:- \n" + + msg += f"**Månadslön:** {calculation.income_quotient * 100:.2f}% av den " \ f"totala inkomsten av deltagarna.\n" if calculation.ingoing_compensation: - msg += f"{username} har överbetalat sin del och ska bli kompenserad " \ - f"med {calculation.ingoing_compensation:.2f}:- från övriga " \ + msg += f"**Ska kompenseras med:** {calculation.ingoing_compensation:.2f}:- från övriga " \ f"deltagare." elif calculation.outgoing_compensation: - msg += f"{username} har inte betalat sin del och ska kompensera " \ - f"övriga deltagare med {calculation.outgoing_compensation:.2f}:- " + msg += f"**Ska kompensera andra med** {calculation.outgoing_compensation:.2f}:-" else: - msg += f"{username} har betalat exakt sin kvot och ska varken " \ + msg += f"**{username} har betalat exakt sin kvot och ska varken " \ f"kompenseras eller kompensera andra." valid = True @@ -334,7 +344,7 @@ def calculate_split_expenses(self, message): accounting_entry.accounting_result = msg if not valid: return Reply("Det finns inga utgifter att kontera för, sedan förra konteringen: " - f"{query_range_start.strftime('%Y-%m-%d %H:%M')}") + f"{query_range_start.strftime('%Y-%m-%d %H:%M')}") if message.entities["close_current_period"]: accounting_entry.save() reply_stream.put("Kontering sparad. Utgifter som läggs till från och med nu " diff --git a/jarvis/abilities/finance/calculator.py b/jarvis/abilities/finance/calculator.py index f3154d0..456fa1a 100644 --- a/jarvis/abilities/finance/calculator.py +++ b/jarvis/abilities/finance/calculator.py @@ -18,6 +18,7 @@ class SharedFinancesCalculator: The resulting data describes who has to compensate whom, and by how much, respectively. """ + def __init__(self): self.total_expense_sum: Decimal = Decimal(0) @@ -30,6 +31,7 @@ class SharedExpenseCalculation: outgoing_compensation: Decimal = field(default=None) ingoing_compensation: Decimal = field(default=None) quota_of_total: Decimal = field(default=None) + recurring_expenses: Decimal = field(default=None) @classmethod def enrolled_usernames(cls): @@ -56,6 +58,9 @@ def calculate_split(self, calculations, processed = [], [] total_sum = Decimal( Expense.objects.within_period(range_start, range_end).sum("price")) + total_sum += Decimal( + Expense.objects.recurring().sum("price") + ) # Get the total combined income of all participants. try: @@ -70,13 +75,15 @@ def calculate_split(self, for user in participant_users: ingoing_compensation = outgoing_compensation = Decimal(0) calculation = self.SharedExpenseCalculation(user=user) - paid_amount = Expense.objects.within_period( + normal_expenses = Expense.objects.within_period( user=user, range_start=range_start, range_end=range_end ).sum("price") - - paid_amount = Decimal(paid_amount) + recurring_expenses = Expense.objects.recurring( + user=user + ).sum("price") + paid_amount = Decimal(normal_expenses + recurring_expenses) income_quotient = user.profile.gross_income / combined_income expected_paid_amount_based_on_income = total_sum * income_quotient @@ -90,8 +97,11 @@ def calculate_split(self, calculation.expected_paid_amount_based_on_income = expected_paid_amount_based_on_income calculation.ingoing_compensation = ingoing_compensation calculation.outgoing_compensation = outgoing_compensation + calculation.recurring_expenses = recurring_expenses if total_sum > 0: calculation.quota_of_total = calculation.paid_amount / total_sum + else: + calculation.quota_of_total = Decimal(0) calculations.append(calculation) self.total_expense_sum = total_sum return calculations diff --git a/jarvis/abilities/finance/intents.py b/jarvis/abilities/finance/intents.py index ede02f3..bb63099 100644 --- a/jarvis/abilities/finance/intents.py +++ b/jarvis/abilities/finance/intents.py @@ -21,6 +21,9 @@ class AddExpense(Intent): expense_value = IntEntityField() store_for_username = TextEntityField(valid_strings=SharedFinancesCalculator .enrolled_usernames) + recurring = BoolEntityField( + message_contains=("återkommande", "upprepande", + "upprepad", "repeterande")) def respond(self, message: Message) -> Union[Reply, ReplyStream]: return self.ability.add_expense(message) @@ -53,6 +56,9 @@ class GetExpenses(Intent): month = TextEntityField(valid_strings=Month.names_as_list, default=None) username_for_query = TextEntityField( valid_strings=SharedFinancesCalculator.enrolled_usernames) + recurring_expenses_only = BoolEntityField( + message_contains=("återkommande", "upprepande", + "upprepad", "repeterande")) def respond(self, message: Message) -> Union[Reply, ReplyStream]: """ diff --git a/jarvis/abilities/finance/models.py b/jarvis/abilities/finance/models.py index 16973ff..dda1578 100644 --- a/jarvis/abilities/finance/models.py +++ b/jarvis/abilities/finance/models.py @@ -31,8 +31,10 @@ def recurring(self, user: User = None) -> QuerySet: :param user: User owning the Expense documents :return: QuerySet[Expense] """ - return self.filter(user_reference=user, - recurring_monthly=True) + query = self.filter(recurring_monthly=True) + if user is not None: + query = query.filter(user_reference=user) + return query def within_period(self, range_start: datetime, @@ -44,7 +46,8 @@ def within_period(self, if range_end is None: range_end = datetime.now() + timedelta(days=1) query = self.filter(created__gte=range_start, - created__lte=range_end) + created__lte=range_end, + recurring_monthly=False) if user is not None: query = query.filter(user_reference=user) return query @@ -84,7 +87,10 @@ def __str__(self): account_month = f":calendar: **{account_month} {year}**\n" created_date = self.created.strftime(self.output_date_format) created_date = f":clock: **{created_date}**\n" - return name + price + created_date + account_month + sep + recurring = "" + if self.recurring_monthly: + recurring = ":repeat: **Upprepande**\n" + return name + price + created_date + account_month + recurring + sep class Debt(me.Document): diff --git a/jarvis/abilities/timekeeper/ability.py b/jarvis/abilities/timekeeper/ability.py index c122cfe..9bd62cb 100644 --- a/jarvis/abilities/timekeeper/ability.py +++ b/jarvis/abilities/timekeeper/ability.py @@ -52,13 +52,14 @@ def save_workshift_from_string(self, message: Message): from_timestamp = message.entities["from_timestamp"] to_timestamp = message.entities["to_timestamp"] project_name = message.entities["project_name"] + until_now = message.entities["until_now"] if (project := Project.objects.get_by_name_or_default_project( project_name)) is None: return self.complain_no_project_was_chosen() workshifts_entered_as_datetime = from_datetime and to_datetime - workshifts_entered_as_time = from_timestamp and to_timestamp + workshifts_entered_as_time = from_timestamp and (to_timestamp or until_now) if not (workshifts_entered_as_datetime or workshifts_entered_as_time): return Reply("Ange när arbetspasset började och slutade. " @@ -73,12 +74,14 @@ def save_workshift_from_string(self, message: Message): dt_format = app.settings.TIMESTAMP_FORMAT start_datetime = end_datetime = datetime.now() timestamp_start = datetime.strptime(from_timestamp, dt_format) - timestamp_end = datetime.strptime(to_timestamp, dt_format) - start_datetime = start_datetime.replace(hour=timestamp_start.hour, minute=timestamp_start.minute) - end_datetime = end_datetime.replace(hour=timestamp_end.hour, - minute=timestamp_end.minute) + if until_now: + end_datetime = datetime.now() + else: + timestamp_end = datetime.strptime(to_timestamp, dt_format) + end_datetime = end_datetime.replace(hour=timestamp_end.hour, + minute=timestamp_end.minute) try: if start_datetime > end_datetime: diff --git a/jarvis/abilities/timekeeper/intents.py b/jarvis/abilities/timekeeper/intents.py index 123373b..7d63517 100644 --- a/jarvis/abilities/timekeeper/intents.py +++ b/jarvis/abilities/timekeeper/intents.py @@ -90,6 +90,7 @@ class CreateWorkShiftFromString(Intent): from_timestamp = StringEntityField(identifier=TimeStampIdentifier) to_timestamp = StringEntityField(identifier=TimeStampIdentifier) project_name = StringEntityField(valid_strings=Project.all_project_names) + until_now = BoolEntityField(message_contains=("nu",)) def respond(self, message: Message) -> Reply | ReplyStream: return self.ability.save_workshift_from_string(message) diff --git a/jarvis/settings.py b/jarvis/settings.py index 9cd832f..1e234f6 100644 --- a/jarvis/settings.py +++ b/jarvis/settings.py @@ -68,7 +68,7 @@ LOG_TO_STDOUT = True APP_NAME = "jarvis" -APP_VERSION = "1.9.1" +APP_VERSION = "1.9.2" DATETIME_FORMAT = "%Y-%m-%d-%H:%M" TIMESTAMP_FORMAT = "%H:%M"