Changeset - a4bba120eb37
[Not reviewed]
0 1 0
Ben Sturmfels (bsturmfels) - 16 months ago 2023-02-11 03:53:30
ben@sturm.com.au
reconlicer: Move match thresholds to top of module
1 file changed with 9 insertions and 6 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reconcile/statement_reconciler.py
Show inline comments
...
 
@@ -58,63 +58,64 @@ Problems in scope:
 

	
 
 - jumping to an individual transaction in a large ledger isn't trivial - Emacs
 
   grep mode is the current best option
 

	
 
 - not all staff use Emacs
 

	
 
 - auditors would prefer Bradley didn't perform reconciliation, ideally not
 
   Rosanne either
 

	
 
 - reconciliation reports are created by hand when there are mismatches
 

	
 
Other related problems we're not dealing with here:
 

	
 
 - after updates to the books files, beancount must be restarted to reflect
 
   updates
 

	
 
 - updates also invalidate the cache meaning restart takes several minutes
 

	
 
 - balance checks are manually updated in
 
   svn/Financial/Ledger/sanity-check-balances.yaml
 

	
 
 - transactions are entered manually and reconciled after the fact, but
 
   importing from statements may be useful in some cases
 

	
 
Current issue:
 

	
 
 - entry_point seems to swallow errors, meaning you get a fairly unhelpful
 
   message if there's an unhandled error
 

	
 
Future possibilities:
 

	
 
 - allow the reconciler to respect manually-applied metadata - not clear how
 
   this would work exactly
 

	
 
 - allow interactive matching where the user can specifiy a match
 

	
 
 - consider combining this with helper.py into one more complete tool that both
 
   reconciles and summarises the unreconciled transactions
 
"""
 

	
 
# TODO:
 
#  - entry_point seems to swallow errors
 
#  - extract the magic numbers
 

	
 
import argparse
 
import collections
 
import copy
 
import csv
 
import datetime
 
import decimal
 
import io
 
import itertools
 
import logging
 
import os
 
import re
 
import sys
 
from typing import Dict, List, Optional, Sequence, Tuple, TextIO
 

	
 
from beancount import loader
 
from beancount.query.query import run_query
 
from colorama import Fore, Style  # type: ignore
 

	
 
from .. import cliutil
 
from .. import config as configmod
 

	
 
if not sys.warnoptions:
 
    import warnings
 
    # Disable annoying warning from thefuzz prompting for a C extension. The
...
 
@@ -129,48 +130,50 @@ logger = logging.getLogger(__name__)
 
# logger.setLevel(logging.DEBUG)
 
# logger.addHandler(logging.StreamHandler())
 

	
 
JUNK_WORDS = [
 
    'software',
 
    'freedom',
 
    'conservancy',
 
    'conse',
 
    'payment',
 
    'echeck',
 
    'bill',
 
    'debit',
 
    'wire',
 
    'credit',
 
    "int'l",
 
    "in.l",
 
    'llc',
 
    'online',
 
    'donation',
 
    'usd',
 
    'inc',
 
]
 
JUNK_WORDS_RES = [re.compile(word, re.IGNORECASE) for word in JUNK_WORDS]
 
ZERO_RE = re.compile('^0+')
 
FULL_MATCH_THRESHOLD = 0.8
 
PARTIAL_MATCH_THRESHOLD = 0.4
 

	
 

	
 
def remove_duplicate_words(text: str) -> str:
 
    unique_words = []
 
    known_words = set()
 
    for word in text.split():
 
        if word.lower() not in known_words:
 
            unique_words.append(word)
 
            known_words.add(word.lower())
 
    return ' '.join(unique_words)
 

	
 

	
 
def remove_payee_junk(payee: str) -> str:
 
    """Clean up payee field to improve quality of fuzzy matching.
 

	
 
    It turns out that bank statement "description" fields are
 
    difficult to fuzzy match on because they're long and
 
    noisey. Truncating them (see standardize_XXX_record fns) and
 
    removing the common junk helps significantly.
 

	
 
    """
 
    for r in JUNK_WORDS_RES:
 
        payee = r.sub('', payee)
 
    payee = ZERO_RE.sub('', payee)
...
 
@@ -331,51 +334,51 @@ def records_match(r1: Dict, r2: Dict) -> Tuple[float, List[str]]:
 
        date_message = f'+/- {diff} days'
 
    else:
 
        date_message = 'date mismatch'
 

	
 
    if r1['amount'] == r2['amount']:
 
        amount_score, amount_message = 2.0, ''
 
    else:
 
        amount_score, amount_message = 0.0, 'amount mismatch'
 

	
 
    # We never consider payee if there's a check_id in the books.
 
    check_message = ''
 
    payee_message = ''
 
    # Sometimes we get unrelated numbers in the statement column with check-ids,
 
    # so we can't match based on the existence of a statement check-id.
 
    if r2['check_id']:
 
        payee_score = 0.0
 
        if r1['check_id'] and r2['check_id'] and r1['check_id'] == r2['check_id']:
 
            check_score = 1.0
 
        else:
 
            check_message = 'check-id mismatch'
 
            check_score = 0.0
 
    else:
 
        check_score = 0.0
 
        payee_score = payee_match(r1['payee'], r2['payee'])
 
        if payee_score > 0.8:
 
        if payee_score > FULL_MATCH_THRESHOLD:
 
            payee_message = ''
 
        elif payee_score > 0.4:
 
        elif payee_score > PARTIAL_MATCH_THRESHOLD:
 
            payee_message = 'partial payee match'
 
        else:
 
            payee_message = 'payee mismatch'
 

	
 
    overall_score = (date_score + amount_score + check_score + payee_score) / 4
 
    overall_message = [m for m in [date_message, amount_message, check_message, payee_message] if m]
 
    return overall_score, overall_message
 

	
 

	
 
def match_statement_and_books(statement_trans: List[Dict], books_trans: List[Dict]) -> Tuple[List[Tuple[List, List, List]], List[Dict], List[Dict]]:
 
    """Match transactions between the statement and books.
 

	
 
    If matched, the books transaction is marked off so that it can
 
    only be matched once. Some transactions will be matched, some will
 
    be on the statement but not the books and some on the books but
 
    not the statement.
 

	
 
    Passes through any unmatched transactions.
 

	
 
    Currently we use the same matching logic for all types of
 
    statements. It's conceivable that you could have special cases to
 
    accurately match some types of statements, but that would be more
 
    work to maintain and test.
 

	
0 comments (0 inline, 0 general)