gncmerge.py
changeset 1 db0e341384e1
equal deleted inserted replaced
0:811cd790a493 1:db0e341384e1
       
     1 #!/usr/bin/env python
       
     2 # -*- coding: utf-8 -*-
       
     3 import gnucash
       
     4 import datetime
       
     5 import logging
       
     6 
       
     7 ZERO = gnucash.GncNumeric()
       
     8 
       
     9 def recurse_all_splits(account):
       
    10     for acc in account.get_descendants():
       
    11         for split in acc.GetSplitList():
       
    12             yield split
       
    13 
       
    14 def check_in_date(start_date, end_date, d):
       
    15     return start_date <= d and d <= end_date
       
    16 
       
    17 def check_split_date_func(start_date, end_date):
       
    18     def f(split):
       
    19         parent = split.GetParent()
       
    20         if parent == None:
       
    21             return False
       
    22         d = datetime.date.fromtimestamp(split.GetParent().GetDate())
       
    23         return start_date <= d and d <= end_date
       
    24     return f
       
    25 
       
    26 def generate_splits(root, start, end):
       
    27     f = check_split_date_func(start, end)
       
    28     return (split for split in recurse_all_splits(root) if f(split))
       
    29 
       
    30 def generate_summary_amounts(root, start, end):
       
    31     prevAccount = None
       
    32     prevAccountName = "<None>"
       
    33     zero = (0, ZERO, ZERO)
       
    34     pos = zero
       
    35     neg = zero
       
    36     for split in generate_splits(root, start, end):
       
    37         account = split.account
       
    38         amount = split.GetAmount()
       
    39         value = split.GetValue()
       
    40         name = account.get_full_name()
       
    41         is_neg = amount.negative_p()
       
    42         if account.get_full_name() != prevAccountName:
       
    43             if prevAccount != None:
       
    44                 yield (prevAccount,) + pos
       
    45                 yield (prevAccount,) + neg
       
    46             prevAccount = account
       
    47             prevAccountName = name
       
    48             if is_neg:
       
    49                 pos = zero
       
    50                 neg = (1, amount, value)
       
    51             else:
       
    52                 pos = (1, amount, value)
       
    53                 neg = zero
       
    54         else:
       
    55             tmp = neg if is_neg else pos
       
    56             tmp = (tmp[0]+1,
       
    57                    tmp[1].add(amount, gnucash.GNC_DENOM_AUTO, gnucash.GNC_HOW_DENOM_EXACT),
       
    58                    tmp[2].add(value, gnucash.GNC_DENOM_AUTO, gnucash.GNC_HOW_DENOM_EXACT))
       
    59             if is_neg:
       
    60                 neg = tmp
       
    61             else:
       
    62                 pos = tmp
       
    63     if prevAccount != None:
       
    64         yield (prevAccount,) + pos
       
    65         yield (prevAccount,) + neg
       
    66 
       
    67 def transaction_to_string(trans, currency):
       
    68     result = str(datetime.date.fromtimestamp(trans.GetDate()))
       
    69     result += " " + trans.GetDescription() + "\n"
       
    70     for split in trans.GetSplitList():
       
    71         result += "\t" + split.account.get_full_name() + " "
       
    72         result += " "
       
    73         c = split.account.GetCommodity()
       
    74         a = split.GetAmount()
       
    75         result += str(a.to_double()) + " " + c.get_nice_symbol()
       
    76         if not currency.equal(c):
       
    77             v = split.GetValue()
       
    78             result += " => " + str(v.to_double()) + " " + currency.get_nice_symbol()
       
    79             result += " ( " + str(v.div(a, gnucash.GNC_DENOM_AUTO, gnucash.GNC_HOW_DENOM_EXACT).to_double())
       
    80             result += " " + c.get_nice_symbol() + " )"
       
    81         result += "\n"
       
    82     imbl = trans.GetImbalanceValue()
       
    83     result += "Balance: " + str(imbl.to_double()) + " " + currency.get_nice_symbol()
       
    84     if not imbl.equal(ZERO):
       
    85         for c, v in trans.GetImbalance():
       
    86             result += "\n\t"
       
    87             result += " => " + str(v.to_double()) + " " + c.get_nice_symbol() 
       
    88     return result
       
    89 
       
    90 def delete_transactions(root, start, end, filt):
       
    91     removed = 0
       
    92     for split in generate_splits(root, start, end):
       
    93         t = split.GetParent()
       
    94         if filt(t):
       
    95             removed += t.CountSplits()
       
    96             t.Destroy()
       
    97     logging.info("%d splits removed.", removed)
       
    98 
       
    99 def transaction_not_equal_p(trans):
       
   100     gtrans = trans.GetGUID()        
       
   101     def f(t):
       
   102         return not gtrans.equal(t.GetGUID())
       
   103     return f
       
   104 
       
   105 def generate_summary_splits(root, start, end):
       
   106     book = root.get_book()
       
   107     for account, count, amount, value in generate_summary_amounts(root, start, end):
       
   108         if not amount.zero_p():
       
   109             split = gnucash.Split(book)
       
   110             split.SetAccount(account)
       
   111             price = value.div(amount, gnucash.GNC_DENOM_AUTO, gnucash.GNC_HOW_DENOM_EXACT)
       
   112             split.SetSharePriceAndAmount(price, amount)
       
   113             yield split, count
       
   114     
       
   115 def create_summary(root, start, end, currency, title):
       
   116     logging.info("Creating summary %s for %s between %s and %s",
       
   117                  title, root.get_full_name(), start, end)
       
   118     book = root.get_book()
       
   119     trans = gnucash.Transaction(book)
       
   120     trans.BeginEdit()
       
   121     trans.SetDate(end.day, end.month, end.year)
       
   122     trans.SetDescription(title)
       
   123     trans.SetCurrency(currency)
       
   124     total = 0
       
   125     for split, count in generate_summary_splits(root, start, end):
       
   126         split.SetParent(trans)
       
   127         total += count
       
   128     if trans.CountSplits() > 0:
       
   129         logging.info("%d splits summary into one transaction '%s'.", total, title)
       
   130         logging.debug(transaction_to_string(trans, currency))
       
   131         assert(trans.IsBalanced())
       
   132         trans.CommitEdit()
       
   133     if total > 0:
       
   134         delete_transactions(root, start, end, transaction_not_equal_p(trans))
       
   135     else:
       
   136         logging.info("No splits found for this date")
       
   137 
       
   138 def make_summary(root, currency, start, end, title):
       
   139     create_summary(root, start, end, currency, title)
       
   140 
       
   141 def lookup_currency(book, currency):
       
   142     return book.get_table().lookup("CURRENCY", currency)
       
   143 
       
   144 ## Datetime support
       
   145 ONEDAY = datetime.timedelta(days=1)
       
   146 
       
   147 def first_day_of_month(d):
       
   148     return datetime.date(d.year, d.month, 1)
       
   149 def first_day_of_year(d):
       
   150     return datetime.date(d.year, 1, 1)
       
   151 
       
   152 def next_day(d):
       
   153     return d + ONEDAY
       
   154 def next_month(d):
       
   155     if d.month == 12:
       
   156         return datetime.date(d.year+1, 1, 1)
       
   157     else:
       
   158         return datetime.date(d.year, d.month + 1, 1)
       
   159 def next_year(d):
       
   160     return datetime.date(d.year + 1, 1, 1)
       
   161 
       
   162 def datetime_iter(start, end, next_f):
       
   163     d = start
       
   164     while end == None or d < end:
       
   165         yield d
       
   166         d = next_f(d)
       
   167 
       
   168 def datetime_iter_range(start, end, next_f):
       
   169     for d in datetime_iter(start, end, next_f):
       
   170         yield d, next_f(d) - ONEDAY
       
   171     
       
   172 def day_iter(start, end=None):
       
   173     return (d for d in datetime_iter(start, end, next_day))
       
   174 def month_iter(start, end=None):
       
   175     return (d for d in datetime_iter(first_day_of_month(start), end, next_month))
       
   176 def year_iter(start, end=None):
       
   177     return (d for d in datetime_iter(first_day_of_year(start), end, next_year))
       
   178 def day_range_iter(start, end=None):
       
   179     return ((d,d) for d in datetime_iter(start, end, next_day))
       
   180 def month_range_iter(start, end=None):
       
   181     return (d for d in datetime_iter_range(first_day_of_month(start), end, next_month))
       
   182 def year_range_iter(start, end=None):
       
   183     return (d for d in datetime_iter_range(first_day_of_year(start), end, next_year))
       
   184 
       
   185 
       
   186 def main(filename, title, currency, daterange):
       
   187     try:
       
   188         session = gnucash.Session(filename, is_new=False)
       
   189         root = session.book.get_root_account()
       
   190         curr = lookup_currency(session.book, currency)
       
   191         for s, e in daterange:
       
   192             make_summary(root, curr, s, e, title)
       
   193         session.save()
       
   194         session.end()
       
   195     except:
       
   196         logging.exception("Exception occured")
       
   197     finally:
       
   198         if 'session' in locals():
       
   199             session.destroy()
       
   200 
       
   201 def usage(msg):
       
   202     import sys
       
   203     print msg
       
   204     print "Usage:", sys.argv[0], \
       
   205         "filename.gnc", "transaction_title", "currency", \
       
   206         "yearly|monthly|daily", \
       
   207         "start_year start_month start_day", \
       
   208         "end_year end_month end_day"
       
   209     exit(-1)
       
   210             
       
   211 if __name__ == '__main__':
       
   212     # logging.basicConfig(level=logging.DEBUG)
       
   213     import sys
       
   214     if len(sys.argv) != 11:
       
   215         usage("Invalid number of arguments")
       
   216         exit(-1)
       
   217     cmd, filename, title, currency, period, syear, smonth, sday, eyear, emonth, eday = sys.argv
       
   218     start = datetime.date(int(syear), int(smonth), int(sday))
       
   219     end = datetime.date(int(eyear), int(emonth), int(eday))
       
   220     if period == "daily":
       
   221         daterange = day_range_iter(start, end)
       
   222     elif period == "monthly":
       
   223         daterange = month_range_iter(start, end)
       
   224     elif period == "yearly":
       
   225         daterange = year_range_iter(start, end)
       
   226     else:
       
   227         usage("Invalid period '%s'" % (period,))
       
   228     main(filename, title, currency, daterange)