Creating an Accounting Program in Julia, for Pluto

I keep going in circles, do I use a macro, or do I use a function? How do I a get this


as a dataframe

and this
newstyletransactions2
as a dataframe

with one line of code.

These examples are from different sources, but the ledger(bottom) has each account as a title. The journal(top) documents one transaction (a row in the ledger), with the Name of each account(the columns of the ledger) that is modified in the transaction.

To process a transaction the sums of credit and debit must match.

1 Like

This is what I have so far

using DataFrames

Ledger=DataFrame(
	chequing=[0],
	expenses=[0],
	income=[0])

I want to create a function or a macro, which has a journal entry, with a unique identifier, a credited account (one to start but more later), a debited account(again one now, more later), the date of the transaction, and a memo (called description in sample ledger), that describes the transaction, but this is less important.

I’m not sure how to make use of functions or macros (or another type of metaprograming) to accomplish this task.

1 Like

This is a good start.
I have some problems with the english financial terms (ledger,checking,…) , because I am not a finance person, but I think this is less important. More important is patience, we need to go step by step, no shortcuts.
First: lets cancel macros out of our minds. Why? Metaprogramming is about generating code, special syntax to generate code, in other words “programming programs”, therefore the meta. It almost never makes sense to start with that. So: no macros for now.

If I understand it right: can we just concentrate on the ledger(s) for now? A journal can always be added later, isn’t it?

Ok, next step? A function to add an entry to a ledger?

4 Likes
ledger=DataFrame(
	date=[""],
	chequing=[0],
	expenses=[0],
	income=[0])

new_row=(date="",chequing=[0],expenses=[0],income=[0])

function ledgertransaction(
		new_date,
		credit_account,
		credit_amount,
		debit_account,
		debit_amount)
		
		push!(ledger,new_row...,
		date=new_date,
		credit_account=credit_amount, 
		debit_account=debit_amount)
end

but when I execute :

ledgertransaction("Jan 1, 2000",expenses,100,chequing,100)

It says that expenses is not defined.

Use "expenses" not expenses.

that works, but if you look at DataFrames, that’s not supposed to be a string.

The account name is always just a string, isn’t it? The same way the amount is always just a number.

Is the column name a string? I’m just trying to add a row to the DataFrame named ‘ledger’.

No, but you’re not trying to use the column name, you’re just specifying the value that goes into the credit_account column for this row.

No, I’m specifying the column that column_amount goes into. This is the ledger, not the journal.

Can we go to Zulip’s data channel, I think it will be easier to use than Discourse for this kind of thing.

OK, Zulip

1 Like

If you are writing an accounting program using Julia, you should be using Decimal Floating Point instead of Binary Floating Point. This way, you can make sure that your cents add up correctly…

$ julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.6.1 (2021-04-23)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> 0.1 + 0.1 + 0.1
0.30000000000000004

julia> using DFPs
[ Info: Precompiling DFPs [top-level]

julia> d"0.1" + d"0.1" + d"0.1" |> DFP_toCommonString
"0.3000000000000000"

You don’t have access to DFPs (because it is my own private module) but there is a package called decimal I think that more or less does the same thing.

https://github.com/JuliaMath/Decimals.jl

That was low down on my list now, but currently, I’m using integers, this is indeed something I was going to ask about.

Here is a function that creates a journal entry and adds a row to the ledger.

function ledgertransaction(
	#journal_entry,
    date,
    credit_account,
    credit_amount,
    debit_account,
    debit_amount
)

    default_row=Dict("chequing"=>0,"expenses"=>0,"income"=>0)
    spec = Dict("date" => date, credit_account => credit_amount, debit_account => debit_amount)
    row = merge(default_row, spec)
    push!(ledger,row)
	
	journal_entry=DataFrame(
		                   date=["Jan 1, 2000","","",""],
		                   credited_account=["", credit_account,"",""],
						   debited_account=["","",debit_account,""],
		                   credit=[0,credit_amount,0,0],
		                   debit=[0,0,debit_amount,0])


	end
end

Good Morning, to all.
Great, something proceeded, but we need to analyze, what happened and whats wrong or confusing.

Therefor we still drop journal_entry, we are not yet there.
And currency roundings are also ignored for now, we stay with Int, as we are dealing with more basic problems.

The current working code is:

using DataFrames

ledger=DataFrame(
	date=[""],
	chequing=[0],
	expenses=[0],
	income=[0])

function ledgertransaction(
    date,
    credit_account,
    credit_amount,
    debit_account,
    debit_amount
)
    default_row=Dict("chequing"=>0,"expenses"=>0,"income"=>0)
    spec = Dict("date" => date, credit_account => credit_amount, debit_account => debit_amount)
    row = merge(default_row, spec)
    push!(ledger,row)
end

ledgertransaction("Jan 1, 2000","expenses",100,"chequing",100)

It is working but there are some issues:

  1. As the amount of the transactions must be equal, there is no need to pass the value 2 times. The function should therefor more look like:
    ledgertransaction(date,amount,credit_account,debit_account)

  2. As we change ledger we should write it as:
    ledgertransaction!(ledger,...)

  3. We shouldn’t at this point return anything from ledgertransaction! (we should return nothing or true for no error and false for error)

  4. If we change the ledger definition, e.g. adding more accounts as columns or adding the description column, we have to change our function, which is error prone. So, the function should be more generic, so that any ledger can be passed. (this is a bit advanced)

  5. It is possible to pass accounts, which aren’t present in the ledger. There needs to be some checking for this.

  6. If credit_account and debit_account are given equal, only a single amount is accounted, which is an error. A check that those accounts a different is needed.

  7. default_row isn’t a default row, it’s only some default values for some columns. It should be named like that (see point 4.: create default_values generically)

  8. Parameter date can be empty: check for not empty (at least)

As my aim is not to provide just something which works but isn’t understood, I would suggest, that at first, we work on these issues before we proceed. This will also avoid those circles of confusion.

If you agree, take the following complete code, where the new function ledgertransaction! has already been changed (issue 1., 2. and 3., check what I have done to adress these issues, please) and comments and suggestions are filled in, but it is up to you, to make it complete (put your code where you see the #…):

using DataFrames

ledger=DataFrame(
	date=[""],
	chequing=[0],
	expenses=[0],
	income=[0])


function ledgertransaction!(ledger,date,amount,credit_account,debit_account)
	#check if date is not a empty string
	if length(date) == 0
		return false
	end
	
	#check if credit_account is different from debit_account:
	#...

	#check if credit_account exists:
	if !( credit_account in names(ledger) )
		return false
	end
	
	#check if debit_account exists:
	#...
	
	#create a default row more generically:
	#  this is advanced stuff, you may skip it for now
	#...

	#  skipped for now, stay with the original non-generic code:
	default_values=Dict("chequing"=>0,"expenses"=>0,"income"=>0)
	
	spec = Dict("date" => date, credit_account => amount, debit_account => amount)
	row = merge(default_values, spec)

	push!(ledger,row)
	return true
end

The following calls should do fine and return true if an entry was added or false if nothing was added because of any error:

ledgertransaction!(ledger,"Jan 1, 2000",100,"expenses","chequing")
ledgertransaction!(ledger,"Jan 1, 2000",10,"expenses","expenses")
ledgertransaction!(ledger,"Jan 1, 2000",20,"expenses","cheqing")
ledgertransaction!(ledger,"",30,"expenses","chequing")
If you didn't skip the part with the generic *default_values* (issue 4) the following should do also fine:
another_ledger=DataFrame(
	date=[""],
	chequing=[0],
	expenses=[0],
	income=[0],
	sum=[0]
)
ledgertransaction!(another_ledger,"Jan 1, 2000",100,"expenses","sum")

In this example, the additional column sum can be accessed like an account. Of course this is not what a sum is. So this still needs special coding and can not be generic as the meaning of sum is something different.

Give me some feedback, if it’s clear what I did in the code. And of course, the issues may need some discussion.
Whatever questions you have, let’s get them out of the way before you proceed.

2 Likes

I want to pass the value 2 times, because that way it’s easier to find a mistake.

Other than wanting to have both the credit and debit values, what you did looks really good. I changed the if statements, so that it can find all the errors. I guess lets work through the stuff for issue 4.

function ledgertransaction(
    date,
    credit_account,
    credit_amount,
    debit_account,
    debit_amount
)
	
	check=credit_amount-debit_amount
	
	if length(date) == 0
		"Error-- no date entered"
	else
	
	
	if !( credit_account in names(ledger) )
		"Error-- credit account not found"
	else
	
	if !( debit_account in names(ledger) )
		"Error--debit account not found"
	else
	
	if check != 0
		"Error -- credit and debit must match." 

	else

		
    default_row=Dict("chequing"=>0,"expenses"=>0,"income"=>0)
    spec = Dict(
			"date" => date, 
			credit_account => credit_amount, 
			debit_account => debit_amount)
		
    row = merge(default_row, spec)
    
	push!(ledger,row)
	
	journal_entry=DataFrame(
		                   date=["Jan 1, 2000","","",""],
		                   credited_account=["", credit_account,"",""],
						   debited_account=["","",debit_account,""],
		                   credit=[0,credit_amount,0,0],
		                   debit=[0,0,debit_amount,0],
			               
							#balance=[0,
							 #      sum(credit_account),
				              #     sum(add_debit_account),
				               #    0]
							#I need to figure out how to calculate my balance
		)

 
	end
			end
		end
	end
end

Your new function has new issues.
(Thats how the circles start!)

A major issue is, that this new function returns the error strings in case of an error, but in case of no error, it returns journal_entry , which is type instability.
For details see Performance Tips ¡ The Julia Language
(Issue named “type stable”)

Another major issue is, that, as I said (but with not much detail) in issue 2, we are changing ledger. Doing it, like you in your newest function, this would need a global ledger. We want to avoid global variables, see Performance Tips ¡ The Julia Language
(Issue named “global variables”)

The goal for us is to implement your problem in a Julian way. That’s why I still hesitate to adress the journal_entry. For this I see a second function, which again should be implemented in Julian style.

If we do this now in a sloppy way (just producing working code) we will have problems later!

Passing the amount twice is ok, but errors must be handled, as you do. Fine.

The above two new issues, “type stable” and “global variables”) must be cleared, before we proceed! I have to insist :slight_smile:

2 Likes

Reading the performance tips i see that mixing types is an issue, the problem is that if it just returns false, it can take longer to find the error, is there a way around this?