"""Find all unclassified / unprocessed transactions and prompt the user to classifyand edit them.This is one of the nice tools that Roastery has on offer and is what allows you toeasily and quickly edit large amounts of transaction data. Any edits made by theend user are saved in a JSON file that can be version controlled with git."""importdatetimeimportjsonimporttypingfromcollectionsimportdefaultdictfrombeancountimportloaderfrombeancount.coreimportdatafrombeancount.core.numberimportDfrombeancount.core.positionimportPositionfrombeancount.query.queryimportrun_queryfromroasteryimporttermfromroastery.configimportConfig__all__=["main","ManualEdits",]
[docs]classManualEdits(typing.TypedDict):""" Manually edits applied to an :py:class:`roastery.importer.Entry` by a user. User-overridden data is stored in a JSON file that is indexed by the :py:obj:`roastery.importer.Entry.digest`. Roastery loads these manual edits from :py:obj:`roastery.config.Config.manual_edits_path`. """payee:straccount:strnarration:strtags:list[str]links:list[str]
classUnprocessed(typing.Protocol):"""Utility type representing an unprocessed entry."""date:datetime.dateposition:Positionpayee:strnarration:strdigest:strtype:strdefdisplay(item)->None:amount=item.position.units.numbercurrency=item.position.units.currencyifitem.position.units<=data.Amount(D("0"),"EUR"):color="green"amount=amount*-1else:color="red"message=f"[bold blue]{item.date}[/bold blue] {item.payee} [bold {color}]{amount}{currency}[/bold {color}]"to_log=[message,item.narration]ifitem.narrationelse[message]term.log(*to_log,style="bold blue")defget_unprocessed(entries,options)->list[Unprocessed]:query=""" select date, position, payee, narration, any_meta("digest") as digest, any_meta("type") as type where account ~ "Unknown" """res_type,res_rows=run_query(entries,options,query)returnres_rows
[docs]defmain(config:Config)->None:""" Find all unclassified transactions and prompt the user to assign them to a category. This function depends on FZF to provide the interactive prompt that allows the user to select their preferred category. This function assumes that ``fzf`` is installed and available on ``PATH``. :param config: The configuration to use to find files on disk. """entries,errors,options=loader.load_file(config.journal_path)accounts={entry.accountforentryinentriesifisinstance(entry,data.Open)}accounts=[accountforaccountinaccountsif"Assets:Bank"notinaccountand"Equity:Opening-Balances"notinaccount]to_save=defaultdict(dict)to_skip=set(json.loads(config.skip_path.read_text()))try:foriteminget_unprocessed(entries,options):ifitem.digestinto_skip:continuedisplay(item)account_or_skip=term.select_fuzzy_search("Select account",options=accounts+["Skip"])ifaccount_or_skip=="Skip":to_skip.add(item.digest)else:payee_pretty=(item.payee.title()ifitem.payee.isupper()elseitem.payee)item_edits={"account":account_or_skip,"payee":term.ask("Payee",default=payee_pretty),"narration":term.ask("Narration",default=item.narration),}to_save[item.digest]=item_editsexceptKeyboardInterrupt:passtry:prev=json.loads(config.manual_edits_path.read_text())except(ValueError,FileNotFoundError):prev={}config.manual_edits_path.write_text(json.dumps(prev|to_save,indent=4)+"\n")config.skip_path.write_text(json.dumps(sorted(to_skip),indent=4)+"\n")