diff --git a/README.md b/README.md index 78eeef0..b8b2356 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ OFX (via [ofxgo](https://github.com/aclindsa/ofxgo)). This project is in active development and is not yet ready to be relied upon as your primary accounting software. +## Documentation + +[Documentation in markdown](./docs/index.md) + ## Installation First, install npm, python, curl, and go >= 1.7 in your distribution. Here is diff --git a/accounts_lua.go b/accounts_lua.go index 016a47a..5a076c8 100644 --- a/accounts_lua.go +++ b/accounts_lua.go @@ -107,6 +107,8 @@ func luaAccount__index(L *lua.LState) int { } else { panic("SecurityId not in lua security_map") } + case "SecurityId", "securityid": + L.Push(lua.LNumber(float64(a.SecurityId))) case "Parent", "parent", "ParentAccount", "parentaccount": if a.ParentAccountId == -1 { L.Push(lua.LNil) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..233d415 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,4 @@ +# MoneyGo Documentation + +* [Lua Reports](lua_reports.md) +* [Importing Transactions using OFX](ofx_imports.md) diff --git a/docs/lua_reports.md b/docs/lua_reports.md new file mode 100644 index 0000000..e025954 --- /dev/null +++ b/docs/lua_reports.md @@ -0,0 +1,155 @@ +# Lua Reports + +MoneyGo reports are written in [Lua](https://lua.org), as implemented by +[github.com/yuin/gopher-lua](https://github.com/yuin/gopher-lua), with hooks +added to query the necessary MoneyGo state to generate the report. + +## An Example: Monthly Cash Flow Report + +Before diving into the details, here's an example report that calculates the +difference between income and expenses for each month in the current year: + +``` +function generate() + year = date.now().year + + accounts = get_accounts() + t = tabulation.new(12) + t:title(year .. " Monthly Cash Flow") + series = t:series("Income minus expenses") + + for month=1,12 do + begin_date = date.new(year, month, 1) + end_date = date.new(year, month+1, 1) + + t:label(month, tostring(begin_date)) + cash_flow = 0 + + for id, acct in pairs(accounts) do + if acct.type == account.Expense or acct.type == account.Income then + balance = acct:balance(begin_date, end_date) + --[[ + Note: We should convert balance.amount to the user's default currency + before proceeding here + --]] + cash_flow = cash_flow - balance.amount + end + end + series:value(month, cash_flow) + end + + return t +end +``` + +## Basic Operation + +The lua code behind a report *must* contain a `generate()` function which takes +no arguments. This function is called when generating a report, and must return +a `tabulation` object, created by calling `t = tabulation:new(n)`, where `n` is +the integer number of data values in each of the series of this tabulation (all +series in the same tabulation must have the same number of values). + +### Titles and Labels + +Assuming your tabulation object is `t`, you should then call `t.label(m, +"some_string")` for each value of `m` in `[1, n]` to set the label for the 'm'th +data element in each series. You do not need to do this before creating series, +and can do it lazily as you generate data, if needed. Titles, subtitles, and the +y-axis label (the units) can be set as follows on a tabulation object named `t`: + +* `t.title("The title of my report")` +* `t.subtitle("The subtitle of my report")` +* `t.units("USD ($)")` + +### Data Series + +To create a new top-level series, call `s = t:series("series name")` where `t` +is a tabulation object. Just as for labels for tabulation objects, you set +`s:value(m, number)` for each value of `m` in `[1, n]` (where `n` is the same +integer used to create the tabulation object to which this series belongs). + +Nested series can be created by calling `s2 = s:series("nested series name")`, +where `s` is any already-created series object. Nested series allow for drilling +down to explore the information in more detail. In the web interface, they are +clickable and cause the charts to display the selected series as the new top +level. + +It is assumed that nested series' reported values are not already included in +their parents' values. When being displayed, all the children series values are +added into the parent's. This means that a series may have no values of its own, +but still show up in a chart because it is reporting the sum of its children's +values. + +## Gathering Data + +Collecting/tabulating the data is up to you (chasing this flexibility was the +impetus behind Lua reports in the first place). + +### Accounts and Balances + +You can get a table of account objects for each of your accounts (indexed by +account ID) by calling the global function, `get_accounts()`. Each of these +accounts has several fields describing it: + +* `a.Name` returns the account's name +* `a.Description` returns the account's description +* `a.Type` returns the account's type, as an integer constant. The account type + constants are available on the top-level 'account' object + * `account.Bank` + * `account.Cash` + * `account.Asset` + * `account.Liability` + * `account.Investment` + * `account.Income` + * `account.Expense` + * `account.Trading` + * `account.Equity` + * `account.Receivable` + * `account.Payable` +* `a.TypeName` returns a string representation of the account's type +* `a.Security` returns a security object representing the currency, stock, etc. + of this account. +* `a.Parent` returns this account's parent account, or nil if the account has no + parent. +* `a:Balance` is a function which returns the account balance in the account's + security, optionally over a date range. If no arguments are provided, the + total account balance as of the end of time is returned. If one date is + provided, the balance as of that date is returned. If two dates are provided, + the difference in balances between the first and second dates is returned. + +### Securities + +You can get a table containing all the securities/currencies registered to an +account using the global function `get_securities()`. Each of these securities +has several fields describing it: + +* `s.SecurityId` +* `s.Name` +* `s.Description` +* `s.Symbol` returns the symbol traditionally associated with that security + (i.e. '$' for USD, or BRK.B for class B shares of Berkshire Hathaway) +* `s.Precision` returns the number of digits of precision past the decimal point + that this currency allows for (i.e. 2 for USD) +* `s.Type` returns an int constant which represents what type of security it is + (i.e. stock or currency) + +You can also query for an account's default currency using the global +`get_default_currency()` function. + +### Dates + +In order to make it easier to do operations like finding account balances for a +month at a time, MoneyGo implements it's own date type (eschewing the +traditional Lua implementation). You *must* use the MoneyGo date types when +passing them to any MoneyGo lua functions. To create a date object, you can use +one of two methods: + +1. `date.now()` returns the current date +2. `date.new(2017, 7, 5)` returns a date object representing July 5th, 2017. + Note that this method also accepts a single argument of a table with the + 'year', 'month', and 'day' fields set to int's. + +In addition to supporting conversion to a string, addition, subtraction, and +comparison operators, dates support returning their constituent parts using +`d.Year`, `d.Month`, and `d.Day`. diff --git a/securities_lua.go b/securities_lua.go index 6a9d347..a2e323b 100644 --- a/securities_lua.go +++ b/securities_lua.go @@ -37,6 +37,36 @@ func luaContextGetSecurities(L *lua.LState) (map[int64]*Security, error) { return security_map, nil } +func luaContextGetDefaultCurrency(L *lua.LState) (*Security, error) { + security_map, err := luaContextGetSecurities(L) + if err != nil { + return nil, err + } + + ctx := L.Context() + + user, ok := ctx.Value(userContextKey).(*User) + if !ok { + return nil, errors.New("Couldn't find User in lua's Context") + } + + if security, ok := security_map[user.DefaultCurrency]; ok { + return security, nil + } else { + return nil, errors.New("DefaultCurrency not in lua security_map") + } +} + +func luaGetDefaultCurrency(L *lua.LState) int { + defcurrency, err := luaContextGetDefaultCurrency(L) + if err != nil { + panic("luaGetDefaultCurrency couldn't fetch default currency") + } + + L.Push(SecurityToLua(L, defcurrency)) + return 1 +} + func luaGetSecurities(L *lua.LState) int { security_map, err := luaContextGetSecurities(L) if err != nil { @@ -62,9 +92,13 @@ func luaRegisterSecurities(L *lua.LState) { L.SetField(mt, "__metatable", lua.LString("protected")) getSecuritiesFn := L.NewFunction(luaGetSecurities) L.SetField(mt, "get_all", getSecuritiesFn) + getDefaultCurrencyFn := L.NewFunction(luaGetDefaultCurrency) + L.SetField(mt, "get_default", getDefaultCurrencyFn) - // also register the get_securities function as a global in its own right + // also register the get_securities and get_default functions as globals in + // their own right L.SetGlobal("get_securities", getSecuritiesFn) + L.SetGlobal("get_default_currency", getDefaultCurrencyFn) } func SecurityToLua(L *lua.LState, security *Security) *lua.LUserData {