Generate amortization table given start date, loan amount and interest

I’m trying to use grist to explore hypothetical debt amortization schedules, and i’m not sure how to generate new record entries based on input parameters.

How I’d like this to work:

  • Table A has a record for each loan account, with fields for loan amount, interest, duration, start date, ect

  • upon a new record entry in Table A, I would like Table B to populate month-month amortization with a reference column to the account, as well as fields for payment number, date, total payment, principal, interest, and total balance. Eventually I’d like to graph the time series data by account to visualize payoff timelines and total interest accrued.

The part i’m stuck on: I don’t see any documentation for using a formula to create new entries in another table. I imagine similar use-cases would be for something like gantt chart/scheduling tables, but I don’t see templates or examples for dynamic table generation. I do see the “action button” widget, but i’ve been working on a local desktop installation while I test it out on my private data and I’m not sure how to get that widget installed on my instance

Any help would be great, I don’t even know where to start!

I handle these types of use cases by pre-generating the records in Table B with the maximum number of records that would be required. Give them a simple serial number ID. Setting the ID could be automated with a formula like this:

group = TableB.all
serial = list(group).index(rec) + 1

The rest of Table B can then be generating by formulas that reference values in Table A. For example, to assign dates to each record in Table B, Table might have a formula column that gets the start date from Table A and assigns that to the first record and the subsequent records calculate based on that first date.

I’m curious if anyone knows a better way.

I ended up making this an online template to use the action button custom widget, and it works almost they way i’d want.

Frankly, i’d really like the ability to use the grist api from a column formula instead of having to press a button, but this works well enough.

https://docs.getgrist.com/bsTxGj2w4Sv4/Loan-HypotheticalsShare?utm_id=share-doc

Looks like the link is not publicly accessible.
I’ve encountered exactly same problem in other scenario: I wanted to create simple doc for bookkeeping, which require auto generation of a Debit-Credit pair (or more) records in posting table for each record in another, transaction table. The only way it could be done at the moment is with help of external script which:

  • looks for changes in transaction table, and
  • generates/amends/deletes records in posting table

oops, that my mistake! This link should work, I made that link public, but in case I need to share it again:

https://docs.getgrist.com/bsTxGj2w4Sv4/Loan-HypotheticalsShare?utm_id=share-doc

That’s basically what the action button does, except it requires user input to trigger (which isn’t actually a bad thing, since there are some dirty python loops in my formula that will enter an infinite loop if a value is outside of range). I’d love to know how you set up your external script!

Thank you for sharing!
I’ve created sample document here:
[Gen.ledger - Grist]

Add a record or modify existing one in Transactions table. Every minute external script (herebelow) checks for change and updates table Entries (of course, in real app this period shall be much lower). I will leave it running on my machine for a while. It is of course very questionable approach as a lot of code resides outside of the grist document.

#!/usr/bin/env python3

from grist_api import GristDocAPI
import os
from datetime import datetime as dt
from random import choice
from dataclasses import dataclass
from random import random, sample
import logging, time


class MyEx(Exception):
    pass

class GristDoc():
    def __init__(self, updater, logfile = None,
                 server=None, doc_id=None, api_key=None):
        ''' sets connection and 

        Args:
          server: txt, url, e.g https://something.com
          doc_id: txt, get it from grist server
          api_key: txt, generated by grist server
          updater: object with compulsory methods
                  check_updated
                  on_update
        '''
        self.logger = logging.getLogger(__name__)
        logging.basicConfig(
            format='%(asctime)s %(message)s',
            encoding='utf-8',
            level = logging.DEBUG)

        if not updater or not hasattr(updater, 'fetch_updated') \
                or not hasattr(updater, 'on_update'):
                    raise MyEx("no suitable updater instance")
        self.updater            = updater

        self.server             = server
        self.doc_id             = doc_id
        self.api_key            = api_key
        self.update_conn_data()
        self.api                = GristDocAPI(self.doc_id, server = self.server)

    def update_conn_data(self):
        '''fetch connection data, if missing

        Args:
        '''
        for a in ['server','doc_id','api_key']:
            if not getattr(self, a):
                #attempt to fetch value from environment var
                env_var = f'GRIST_{a.upper()}'
                try:
                    val = os.environ[env_var]
                except (KeyError):
                    val = ''
                if not val:
                    raise MyEx(f"attribute {a} is not defined!\nEither provide it as argument to GristDoc class or set environment variable {env_var}")
                setattr(self, a, val)


    def check_for_updates(self):
        '''
        '''
        self.updater.fetch_updated(api = self.api)
        self.logger.info(f'{len(self.updater.updated)} records updated')

    def action_on_update(self):
        '''
        '''
        self.updater.on_update(api = self.api)
        self.logger.info(f'finished action stage')


class UpdaterTransaction():
    def __init__(self):
        pass

    def fetch_updated(self, api):
#        self.updated = api.call('tables/{}/records?filter={"{}":[true]}'.\
#                    format(self.check_table, self.check_column))
        self.updated = api.fetch_table('Transactions',
                    {"ext_check": True})

    def on_update(self, api):
        ''' generate/update records
        '''
        for r in self.updated:
            # this code is for demonstration purpose only
            #  just to illustrate the extend the algorithm taken away
            #   from the grist
            amounts = []
            if r.narration:
                amounts = r.narration.split()
                comment = "amount from the transaction's narration"
            if not amounts:
                comment = "generated amount"
                amounts = []
                for i in range(2):
                    amounts.append(round(random() * 10000 / (12 + i),2))
                amounts.append(-round(sum(amounts),2))
            accounts = api.fetch_table('Accounts')
            accounts_rand = sample(range(len(accounts)), 3)
            #create 3 sample transactions
            new_records = []
            for i in range(3):
                new_records.append(
                        {'transaction': r.id,
                         'account': accounts_rand[i],
                         'amount': amounts[i],
                         'comment': comment
                        }
                    )
            api.add_records(table_name = 'Entries',
                            record_dicts = new_records)
            #mark "parent" record as "done"

        #in real life we shall mark "parent" record as done much promptly
        api.update_records(table_name = 'Transactions', 
                           record_dicts = [ {"id": r.id, "ext_check": False} \
                                   for r in self.updated ])
        self.updated = []


def main():
    d = GristDoc(UpdaterTransaction())
    while True:
        d.check_for_updates()
        d.action_on_update()
        #should be updated much often, of course
        time.sleep(42)

if __name__ == "__main__":
    main()

I’ve gotten around this by basically importing a huge calendar table. That way all your dates are in there and gets around this issue of adding line item via trigger. Your second table is your Loan Data. Then you can apply your loan data over your calendar table which can have your pmnt and int calculations. Then, group calendar by month/qtr/year, or roll up summary data back to loan table. The catch is that, while you can have multiple loans in loan table, you can essentially only have one “Amortization Table” at a time - the specific loan you have applied over your calendar table. I haven’t found a practical issue with this as Amortization schedules aren’t transactional data, so there isn’t really a need to keep individual amortization schedules live unless you want to pull it up. Hope you figured it out!