Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
94c5b91
removed computer-specific debug arguments
ChernayaKoshka Oct 11, 2017
f229f47
added additional samples showing partial support for multiple entities
ChernayaKoshka Oct 11, 2017
792cbd6
Modified to work with current entity model
ChernayaKoshka Oct 11, 2017
1029339
added ability to optionally parse entities in the format "entity/acco…
ChernayaKoshka Oct 11, 2017
07ef7e4
added function to check a transaction to see if the number and type o…
ChernayaKoshka Oct 11, 2017
a4860fe
started first stage of entity transition, all tests pass, but full fu…
ChernayaKoshka Oct 11, 2017
209f67a
added entity validation (number/type validation ONLY), added untested…
ChernayaKoshka Oct 11, 2017
356de3b
!BREAKING CHANGE! Forced entity support, these reports will not work …
ChernayaKoshka Oct 11, 2017
3e158de
updated tests to pass with current entity support
ChernayaKoshka Oct 11, 2017
19d8706
modified so the VERIFY-BALANCE check would make a bit more sense
ChernayaKoshka Oct 15, 2017
577e83e
fixed an issue where passed verifications could still kick errors
ChernayaKoshka Oct 15, 2017
34cd171
added entity verification
ChernayaKoshka Oct 15, 2017
d470d79
cosmetic change
ChernayaKoshka Oct 15, 2017
b5b8eb6
added stacktrace to EntityMismatch exception
ChernayaKoshka Oct 15, 2017
54f76a0
added entity support to CanonicalNameComponent and updated the corres…
ChernayaKoshka Oct 15, 2017
971888c
updated to reflect changes in Canonical type
ChernayaKoshka Oct 15, 2017
5435549
added "collapse" function to collapse list of tuples into (key*value[…
ChernayaKoshka Oct 15, 2017
87d1e69
updated demo to use entity file
ChernayaKoshka Oct 15, 2017
e91b9a2
added entity support to all reports
ChernayaKoshka Oct 15, 2017
be31edc
fixed formatting of account names in ReportBalancesByDates report.
ChernayaKoshka Oct 15, 2017
4d655f5
removed global transactionId state, redefined run for convenience
ChernayaKoshka Oct 20, 2017
cd7636f
removed unnecessary calls to Array.OfList by insantiating an array in…
ChernayaKoshka Oct 20, 2017
7f1fcc5
fixed an issue where sorting sub accoutns by name would take entity i…
ChernayaKoshka Oct 22, 2017
4086df4
Removed TEMPENTITY as it was simply a "TODO" that made the code compile
ChernayaKoshka Oct 22, 2017
65863f6
reports now reflect entities properly and fixed an issue where accoun…
ChernayaKoshka Oct 22, 2017
dd734e5
added entity support
ChernayaKoshka Oct 22, 2017
6f88ade
Text.fmt no longer includes entity by default, as it should be up to …
ChernayaKoshka Oct 22, 2017
e747ad2
updated to work with entities
ChernayaKoshka Oct 22, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/entity_sample.transactions
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
2013-01-01 I began the year with $1000 in my cheque account.
Assets:Bankwest:Cheque $1,000
Equity:OpeningBalances $1,000

2013-01-05 I bought some groceries and paid using the cheque account.
Jacob/Expenses:Food:Groceries $98.53
Jacob/Assets:Bankwest:Cheque -$98.53

2013-01-10 I bought some petrol, and paid using a credit card.
John/Expenses:Motor:Fuel $58.01
John/Liabilities:Bankwest:Visa $58.01

2013-01-15 I paid my electricity bill.
Expenses:Electricity $280.42
Assets:Bankwest:Cheque -$280.42

# I checked my bank statement on the 1st of Feb, and this is what it said.
VERIFY-BALANCE 2013-02-01 Assets:Bankwest:Cheque 719.58
18 changes: 18 additions & 0 deletions examples/unbalanced_entity_sample.transactions
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
2013-01-01 I began the year with $1000 in my cheque account.
John/Assets:Bankwest:Cheque $1,000
John/Equity:OpeningBalances $1,000

2013-01-05 I bought some groceries and paid using the cheque account.
Jacob/Expenses:Food:Groceries $98.53
John/Assets:Bankwest:Cheque -$98.53

2013-01-10 I bought some petrol, and paid using a credit card.
Jacob/Expenses:Motor:Fuel $58.01
Liabilities:Bankwest:Visa $58.01

2013-01-15 I paid my electricity bill.
Expenses:Electricity $280.42
Assets:Bankwest:Cheque -$280.42

# I checked my bank statement on the 1st of Feb, and this is what it said.
VERIFY-BALANCE 2013-02-01 Assets:Bankwest:Cheque 621.05
51 changes: 50 additions & 1 deletion src/fsharp/Ledger/Calculations.fs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ type DateOrderCheck =
| OK
| Problem of previous: Transaction * next: Transaction

type EntityVerification =
| EntityVerificationPassed
| UnbalancedEntities of AccountEntity list

/// Check transactions are in date order. Give two problem transactions if not.
let checkDateOrder (transactions : Transaction list) =
let rec helper (previous : Transaction) (transactions : Transaction list) =
Expand All @@ -45,6 +49,51 @@ let checkDateOrder (transactions : Transaction list) =
| [] -> DateOrderCheck.OK
| (t :: tail) -> (helper t tail)

/// This function will check a list of postings for an uneven number of the entities seen within
/// for example:
///
/// Jacob/Expenses:Motor:Fuel $58.01
/// Liabilities:Bankwest:Visa $58.01
///
/// Will result in the following function result:
/// UnbalancedEntities [(Entity Jacob,true); (Default,true)]
///
/// Assuming that they do balanced, like so:
/// Expenses:Motor:Fuel $58.01
/// Liabilities:Bankwest:Visa $58.01
///
/// then the function will return EntityVerificationPassed. Perhaps there is a better way to check for the #
/// such as keeping a running total and "modding" (%) by 2 to see if there is an even number of entities
/// I'm keeping it like this because I think the concept of flipping "bits" is neat, although overcomplicated
let verifyEntitiesInPostings (postings:Posting list) =
let errors = postings
|> List.fold (fun (bins:(AccountEntity*bool) list) (elem:Posting) ->
let entity = elem.account.Entity
let bin = bins |> List.tryFind (fst >> (=) entity)
match bin with
| Some b -> bins |> List.map (fun (entity,b) -> (entity,not b))
| None -> (entity, true) :: bins) []
|> List.filter (snd >> (=) true)
|> List.map (fun (entity,b) -> entity)
match errors with
| [] -> EntityVerificationPassed
| _ -> UnbalancedEntities errors

/// Performs verifyEntitiesInPostings of each transaction's postings in the supplied list
/// It will then map the results into a tuple of (Transaction*EntityVerification List)
/// This allows us to refer to the offending transaction in errors, as well as checking each
/// transaction individually rather than checking the ENTIRE file for unbalanced entities
/// this allows for a faster tracking of entity issues
let verifyEntitiesInTransactions (transactions:Transaction list) =
transactions
|> List.map (fun transaction ->
(transaction, match verifyEntitiesInPostings transaction.postings with
| EntityVerificationPassed -> []
| UnbalancedEntities entities -> entities))
|> List.filter (snd >> (<>) [])
// F# never ceases to amaze. This bit basically says "filter all items where the second item in a tuple is not an empty list".
// the equivalent is (fun x -> snd x <> []) would achieve the same result

/// Is transaction unbalanced?
let balance (t:Transaction) =
let signedAmount (p: Posting) =
Expand All @@ -66,7 +115,7 @@ let filter (transactions : Transaction list) (first : Date option) (last : Date

// Is a a sub-account of b?
let isSubAccountOf (a: InputNameAccount) (b: InputNameAccount) =
startsWith (canonicalAccountName a) (canonicalAccountName b)
startsWith (canonicalAccountName a) (canonicalAccountName b) && a.Entity=b.Entity

/// XXX: affectedBy(Posting/Transaction) should be a method on AccountName, which should be a class. Do we even need these at all?
let postingAffects (p:Posting) (a: InputNameAccount) =
Expand Down
120 changes: 55 additions & 65 deletions src/fsharp/Ledger/ExcelOutput.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ open InputTypes
open TextOutput
open InternalTypes
open OfficeOpenXml
open Misc

type Destination = ExcelPackage option

Expand Down Expand Up @@ -403,10 +404,7 @@ type Excel =
for c in (txnNumCol-1)..(txnNumCol+2) do
worksheet.Column(c).AutoFit(0.0)

static member writeLine((line: ReportBalances.Line),
(ws : ExcelWorksheet),
(indent: int),
(nextRow: int)) =
static member writeLine((line: ReportBalances.Line), (ws : ExcelWorksheet), (indent: int), (nextRow: int)) =
(Excel.setValue (ws.Cells.[nextRow, 1], line.Balance))
Excel.setValue (ws.Cells.[nextRow, 2+indent], line.Account.AsString)
if indent <> 0 then
Expand All @@ -416,10 +414,7 @@ type Excel =
ws.Row(nextRow).Collapsed <- true
rowAfterChildren

static member writeLines((lines : ReportBalances.Line list),
(ws : ExcelWorksheet),
(indent: int),
(nextRow: int)) =
static member writeLines((lines : ReportBalances.Line list), (ws : ExcelWorksheet), (indent: int), (nextRow: int)) =
match lines with
| [] -> nextRow
| first::rest -> Excel.writeLines(rest, ws, indent, Excel.writeLine(first, ws, indent, nextRow))
Expand All @@ -431,91 +426,86 @@ type Excel =
let worksheet = package.Workbook.Worksheets.Add("Balances")
(setHeader worksheet.Cells.[1, 1] "Balance")
(setHeader worksheet.Cells.[1, 2] "Account")
Excel.writeLines(report.lines, worksheet, 0, 2) |> ignore
Excel.writeLines(report.Lines, worksheet, 0, 2) |> ignore
worksheet.View.FreezePanes(2, 1)
worksheet.OutLineSummaryBelow <- false

static member writeLine((line: ReportChartOfAccounts.Line),
(ws : ExcelWorksheet),
(indent: int),
(nextRow: int)) =
Excel.setValue (ws.Cells.[nextRow, 1+indent], line.Account.AsString)
if indent <> 0 then
static member writeLine((line: ReportChartOfAccounts.Line), (ws : ExcelWorksheet), (indent: int), (nextRow: int)) =
Excel.setValue (ws.Cells.[nextRow, 1+indent], line.Account.Name)
if indent <> 1 then
ws.Row(nextRow).OutlineLevel <- (indent)
// Deliberately avoid collapsing hierarchy. If we're looking, we probably want to
// emphasise details, and it's easy to manually hide them if that's what is wanted.
Excel.writeLines (line.SubAccounts, ws, indent+1, nextRow+1)

static member writeLines((lines : ReportChartOfAccounts.Line list),
(ws : ExcelWorksheet),
(indent: int),
(nextRow: int)) =
match lines with
| [] -> nextRow
| first::rest -> Excel.writeLines(rest, ws, indent, Excel.writeLine(first, ws, indent, nextRow))
static member writeLines((lines : ReportChartOfAccounts.Line list), (ws : ExcelWorksheet), (indent: int), (nextRow: int)) =
match lines with
| [] -> nextRow
| first::rest -> Excel.writeLines(rest, ws, indent, Excel.writeLine(first, ws, indent, nextRow))

static member write((report : ReportChartOfAccounts.Report), (destination : Destination)) =
match destination with
| None -> ()
| Some package ->
let worksheet = package.Workbook.Worksheets.Add("Chart Of Accounts")
(setHeader worksheet.Cells.[1, 1] "Account")
Excel.writeLines(report.Lines, worksheet, 0, 2) |> ignore
worksheet.View.FreezePanes(2, 1)
worksheet.OutLineSummaryBelow <- false

static member writeLine((line: ReportTransactionList.Line),
(ws : ExcelWorksheet),
(nextRow: int)) =

match destination with
| None -> ()
| Some package ->
let worksheet = package.Workbook.Worksheets.Add("Chart Of Accounts")
(setHeader worksheet.Cells.[1, 1] "Account")
report.Lines
|> List.map (fun line -> (line.Account.Entity, line))
|> List.collapse
|> List.fold (fun nextRow (entity,accounts) ->
Excel.setValue(worksheet.Cells.[nextRow,1], entity.AsString)
Excel.writeLines(accounts, worksheet, 1, nextRow+1)) 2
|> ignore
worksheet.View.FreezePanes(2, 1)
worksheet.OutLineSummaryBelow <- false

static member writeLine((line: ReportTransactionList.Line), (ws : ExcelWorksheet), (nextRow: int)) =
let txnCell = ws.Cells.[nextRow, 1]
let dateCell = ws.Cells.[nextRow, 2]
let descCell = ws.Cells.[nextRow, 3]

Excel.setValue (txnCell, (sprintf "txn:%d" line.transaction.id))
txnCell.Style.Border.Top.Style <- OfficeOpenXml.Style.ExcelBorderStyle.Thin

Excel.setValue (dateCell, line.transaction.date)
dateCell.Style.Font.Bold <- true
dateCell.Style.Border.Top.Style <- OfficeOpenXml.Style.ExcelBorderStyle.Thin

Excel.setValue (descCell, line.transaction.description)
descCell.Style.Font.Bold <- true
descCell.Style.Font.Italic <- true
descCell.Style.Border.Top.Style <- OfficeOpenXml.Style.ExcelBorderStyle.Thin

line.transaction.postings
|> List.fold (fun row p ->
Excel.setValue (ws.Cells.[row, 2], p.amount)
Excel.setValue (ws.Cells.[row, 3], p.account.AsString)
row+1) (nextRow+1)


static member writeLines((lines : ReportTransactionList.Line list),
(ws : ExcelWorksheet),
(nextRow: int)) =
match lines with
| [] -> nextRow
| first::rest -> Excel.writeLines(rest, ws, Excel.writeLine(first, ws, nextRow))
static member writeLines((lines : ReportTransactionList.Line list), (ws : ExcelWorksheet), (nextRow: int)) =
match lines with
| [] -> nextRow
| first::rest -> Excel.writeLines(rest, ws, Excel.writeLine(first, ws, nextRow))

static member write((report : ReportTransactionList.Report), (destination : Destination)) =
match destination with
| None -> ()
| Some package ->
let worksheet = package.Workbook.Worksheets.Add("Transactions")

match report.first with
| Some date -> (setHeader worksheet.Cells.[1, 1] "From:")
(setHeader worksheet.Cells.[1, 2] date)
| None -> ()
match report.last with
| Some date -> (setHeader worksheet.Cells.[2, 1] "To:")
(setHeader worksheet.Cells.[2, 2] date)
| None -> ()
(setHeader worksheet.Cells.[3, 1] "Transaction#")
(setHeader worksheet.Cells.[3, 2] "Date/Amount")
(setHeader worksheet.Cells.[3, 3] "Description/Account")
worksheet.View.FreezePanes(4, 1)
Excel.writeLines(report.lines, worksheet, 4) |> ignore
for c in 1..3 do
worksheet.Column(c).AutoFit(0.0)
match destination with
| None -> ()
| Some package ->
let worksheet = package.Workbook.Worksheets.Add("Transactions")

match report.first with
| Some date -> (setHeader worksheet.Cells.[1, 1] "From:")
(setHeader worksheet.Cells.[1, 2] date)
| None -> ()
match report.last with
| Some date -> (setHeader worksheet.Cells.[2, 1] "To:")
(setHeader worksheet.Cells.[2, 2] date)
| None -> ()
(setHeader worksheet.Cells.[3, 1] "Transaction#")
(setHeader worksheet.Cells.[3, 2] "Date/Amount")
(setHeader worksheet.Cells.[3, 3] "Description/Account")
worksheet.View.FreezePanes(4, 1)
Excel.writeLines(report.lines, worksheet, 4) |> ignore
for c in 1..3 do
worksheet.Column(c).AutoFit(0.0)
28 changes: 26 additions & 2 deletions src/fsharp/Ledger/InputTypes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,37 @@
type Date = string
type Description = string

type InputNameAccount = InputName of string
type AccountName = string

type AccountEntity =
| Default
| Entity of string
with
member this.AsString =
match this with
| Default -> "Default"
| Entity e -> e

type InputNameAccount = InputName of (AccountEntity * AccountName)
with
member this.AsString =
match this with (InputName x) -> x
match this with (InputName (entity,name)) -> entity.AsString + "/" + name
member this.Name =
match this with (InputName (entity,name)) -> name
member this.Entity =
match this with (InputName (entity,name)) -> entity

exception BadAccountNameException of name: InputNameAccount * problem: string

exception EntityMismatch of acc1:InputNameAccount * acc2:InputNameAccount
with
member this.ToString =
"Entity Mismatch: " + this.acc1.Entity.AsString + " <> " + this.acc2.Entity.AsString + "\r\n" +
"Additional Information: " + this.acc1.AsString + " <> " + this.acc2.AsString + "\r\n" +
"Stacktrace: " + this.StackTrace

exception BadEntityNameException of name:InputNameAccount * problem: string

type Amount =
/// AUD amounts are stored as cents, and converted to dollars on input/output.
| AUD of int
Expand Down
Loading