penny-bin 0.12.0.0 → 0.14.0.0
raw patch · 13 files changed
+933/−488 lines, 13 filesdep ~multiargdep ~penny-lib
Dependency ranges changed: multiarg, penny-lib
Files
- doc/ledger-grammar.org +9/−13
- doc/penny-fit-sample.hs +182/−0
- doc/penny-ofx.ini +70/−0
- man/penny-fit.1 +0/−245
- man/penny-fit.7 +374/−0
- man/penny-reconcile.1 +39/−14
- man/penny-reprint.1 +26/−9
- man/penny-suite.7 +48/−48
- penny-bin.cabal +20/−10
- penny-diff.hs +35/−49
- penny-reconcile.hs +35/−34
- penny-reprint.hs +22/−13
- penny-selloff.hs +73/−53
doc/ledger-grammar.org view
@@ -241,22 +241,18 @@ * FlagNumberPayee -<numberPayee> ::= <number> White* <quotedLvl1Payee>?-<payeeNumber> ::= <quotedLvl1Payee> White* <number>?-<afterFlag> ::= <numberPayee> | <payeeNumber>-<flagFirst> ::= <flag> White* <afterFlag>?+Here we add an additional element to the specification. Square+brackets denote permutations. Elements inside may occur in any order. -<flagPayee> ::= <flag> White* <quotedLvl1Payee>?-<payeeFlag> ::= <quotedLvl1Payee> White* <flag>?-<afterNumber> ::= <flagPayee> | <payeeFlag>-<numberFirst> ::= <number> White* <afterNumber>?+<flagWhite> ::= <flag> White*+<numWhite> ::= <number> White*+<payeeWhite> ::= <payee> White* -<numberFlag> ::= <number> White* <flag>?-<flagNumber> ::= <flag> White* <number>?-<afterPayee> ::= <numberFlag> <flagNumber>-<payeeFirst> ::= <quotedLvl1Payee> White* <afterPayee>?+<flagOpt> ::= <flagWhite>?+<numOpt> ::= <numWhite>?+<payeeOpt> ::= <payeeWhite>? -<flagNumPayee> ::= <flagFirst> | <numberFirst> | <payeeFirst>+<flagNumPayee> ::= [ <flagOpt> <numOpt> <payeeOpt> ] * Posting
+ doc/penny-fit-sample.hs view
@@ -0,0 +1,182 @@+{-# OPTIONS_GHC -Wall #-}+-- | This file is a sample of how to configure penny-fit. You will+-- have to adapt it for your own needs. Rename the file to+-- FILENAME.hs, where FILENAME is the name you want for your program+-- (an easy choice is "penny-fit".) Then, compile the file with+--+-- ghc --make FILENAME+--+-- and if all goes well you will have a program to use.+module Main where++import Penny.Brenner+import Data.Version++-- | The Config type configures all the financial institution accounts.+config :: Config+config = Config+ { defaultFitAcct = Nothing+ -- ^ If you have a default financial institution account, use Just+ -- ACCOUNT here. For example, if I wanted to use my amex account+ -- by default, I would put @Just amex@ here. I don't want to use+ -- an account by default; I want to be required to explicitly+ -- state an account, so I put Nothing here.++ , moreFitAccts = [ visa, checking, saving ]+ -- ^ This is a list of financial institution accounts in addition+ -- to the default (if you have one.)+ }++visa :: FitAcct+visa = FitAcct+ { fitAcctName = "visa"+ -- ^ This is the name by which you will identify this account on+ -- the command line (it will be case sensitive). You pick+ -- financial institution accounts by using the @-f@ option.++ , fitAcctDesc = unlines+ [ "Main Visa card account."+ , "To find the downloads, log in and then click on"+ , "Statements --> Download --> Quicken."+ ]+ -- ^ A description of the financial institution account. This+ -- appears when you use the @info@ command, so you can put+ -- information in here like where to find the downloads on the+ -- bank website.++ , dbLocation = "/home/massysett/ledger/visa"+ -- ^ The location of the database of financial institution+ -- postings. You can make this path relative, in which case it is+ -- interpreted relative to the current directory at runtime, or+ -- absolute. No expansion of tildes, environment variables,+ -- etc. is performed.++ , pennyAcct = "Liabilities:Current:Amex"+ -- ^ Postings from this financial institution appear under this+ -- account in your ledger file(s).++ , defaultAcct = "Expenses:Unclassified"+ -- ^ When penny-fit finds a financial institution posting and it+ -- does not have a matching posting in your ledger, it must create+ -- a new transaction with two postings. One posting will be in the+ -- pennyAcct specified above and the other posting will be in this+ -- account.++ , currency = "$"+ -- ^ All postings will be in this currency++ , groupSpecs = GroupSpecs NoGrouping NoGrouping+ -- ^ When penny-fit reprints your ledger, it uses this to+ -- determine how to perform digit grouping for the quantities in+ -- the output. This takes the form+ --+ -- GroupSpecs G1 G2+ --+ -- where G1 and G2 can each be+ --+ -- NoGrouping, GroupLarge, or GroupAll+ --+ -- G1 specifies how to group digits to the left of the decimal+ -- point. G2 specifies how to group digits to the right of the+ -- decimal point. All grouping creates groups of three digits.+ --+ -- NoGrouping means do not do any digit grouping at+ -- all.+ --+ -- GroupLarge means perform digit grouping, but only if the+ -- number to be grouped is greater than 9,999 (if grouping to the+ -- left of the decimal point) or if there are more than 4 decimal+ -- places (if grouping to the right of the decimal point.)+ --+ -- GroupAll means group whenever there are at least four digits to+ -- be grouped.++ , translator = IncreaseIsCredit+ -- ^ Postings from your financial institution are specified in+ -- terms of increases or decreases. Postings in your ledger are+ -- specified in terms of debits or credits. The translator+ -- specifies how to convert a posting from your financial+ -- institution to a posting in the pennyAcct in your ledger. For+ -- deposit accounts (e.g. checking) you will typically use+ -- IncreaseIsDebit; for liability accounts (e.g. credit cards) you+ -- will typically use IncreaseIsCredit.++ , side = CommodityOnLeft+ -- ^ When penny-fit creates new postings it must put the commodity+ -- either to the left of the quantity or to the right of the+ -- quantity. Accordingly your choices here are CommodityOnLeft or+ -- CommodityOnRight.++ , spaceBetween = NoSpaceBetween+ -- ^ When penny-fit creates new postings it must decide whether to+ -- put a space between the commodity and the quantity. Accordingly+ -- your choices here are SpaceBetween or NoSpaceBetween.++ , parser = ofxParser+ -- ^ This determines how to parse the data you have+ -- downloaded. Currently there is only one parser, which handles+ -- OFX data. Many financial institutions provide OFX data so this+ -- will get you a long way. If you want to write additional+ -- parsers you can provide your own function of this type (perhaps+ -- your bank only provides CSV files, for example.)++ , toLincolnPayee = usePayeeOrDesc+ -- ^ Sometimes the financial institution provides Payee information,+ -- sometimes it does not. Sometimes the Desc might have additional+ -- information that you might want to remove. This function can be+ -- used to do that. The resulting Lincoln Payee is used for any+ -- transactions that are created by the merge command. The resulting+ -- payee is also used when comparing new financial institution+ -- postings to already existing ledger transactions in order to+ -- guess at which payee and accounts to create in the transactions+ -- created by the merge command.+ --+ -- 'usePayeeOrDesc' simply uses the payee if it is available;+ -- otherwise, it uses the description. (Many banks provide+ -- descriptions only and do not provide separate payee information.)+ }++checking :: FitAcct+checking = FitAcct+ { fitAcctName = "checking"+ , fitAcctDesc = "Main checking account."+ , dbLocation = "/home/massysett/ledger/checking"+ , pennyAcct = "Assets:Current:Checking"+ , defaultAcct = "Expenses:Unclassified"+ , currency = "$"+ , groupSpecs = GroupSpecs NoGrouping NoGrouping+ , translator = IncreaseIsDebit+ , side = CommodityOnLeft+ , spaceBetween = NoSpaceBetween+ , toLincolnPayee = usePayeeOrDesc+ , parser = ofxParser+ }++saving :: FitAcct+saving = FitAcct+ { fitAcctName = "saving"+ , fitAcctDesc = "Main saving account."+ , dbLocation = "/home/massysett/ledger/saving"+ , pennyAcct = "Assets:Current:Checking:Omari"+ , defaultAcct = "Expenses:Unclassified"+ , currency = "$"+ , groupSpecs = GroupSpecs NoGrouping NoGrouping+ , translator = IncreaseIsDebit+ , side = CommodityOnLeft+ , spaceBetween = NoSpaceBetween+ , toLincolnPayee = usePayeeOrDesc+ , parser = ofxParser+ }++-- Leave things below this line alone (unless you know what you're+-- doing of course.)++-- brennerMain requires that you supply a version. You can edit this+-- as you see fit, or use the version that Cabal supplies.+version :: Version+version = Version [1] []++-- | Always leave these two lines the same (unless you know what you+-- are doing of course).+main :: IO ()+main = brennerMain version config
+ doc/penny-ofx.ini view
@@ -0,0 +1,70 @@+# Sample configuration file for penny-ofx++# Optional default financial institution account. If you do not+# specify a default account, penny-ofx will require that you specify+# one using the -f option every time you run penny-ofx.+default_fit_acct = bigbank++# Example: a credit card from Bigbank. All of the fields shown are+# required. You can configure multiple financial institution accounts.+[bigbank]++# File in which to store database information for this account. You+# must use a different file for each account. Therefore, be careful if+# you cut and paste when you create new accounts in this file, as you+# must change the filename here.+file = /home/massysett/ledger/bigbank.bin++# Postings from this account appear under this account in your ledger+# file(s).+penny_acct = Liabilities:Current:Bigbank++# When penny-fit finds a financial institution posting and it+# does not have a matching posting in your ledger, it must create+# a new transaction with two postings. One posting will be in the+# pennyAcct specified above and the other posting will be in this+# account.+default_acct = Expenses:Unclassified++# All postings will be in this currency+currency = $++# The next two options control digit grouping when penny-ofx reprints+# your ledger. groupLeft and groupRight control whether digits are+# grouped to the left and to the right of the radix point,+# respectively. Available choices are:+#+# none - do not group any digits+#+# large - group only if the number to be grouped is greater than 9,999+# (if grouping to the left of the decimal point) or if there are more+# than 4 decimal places (if grouping to the right of the decimal+# point.)+#+# all - group whenever there are at least four digits to be grouped.+group_left = all+group_right = none++# Postings from your financial institution are specified in terms of+# increases or decreases. Postings in your ledger are specified in+# terms of debits or credits. The translator specifies how to convert+# a posting from your financial institution to a posting in the+# pennyAcct in your ledger.+increase_is = Credit++# When penny-fit creates new postings it must put the commodity+# either to the left of the quantity or to the right of the+# quantity.+commodity_on = Left++# When penny-fit creates new postings it must decide whether to+# put a space between the commodity and the quantity.+space_between = False++# Give some helpful information here about where to find the file on+# your bank website. You can use multiline strings, as shown, by+# indenting subsequent lines.+more_info = Statements from bigbank.+ Bigbank makes its statements available by clicking on+ "Account Info", then logging in, then clicking on+ "Statements".
− man/penny-fit.1
@@ -1,245 +0,0 @@-.TH penny-fit 7--.SH NAME-penny-fit - Penny financial institution statements parser--.SH SYNOPSIS-.B penny-fit-[global-options] COMMAND [local-options] ARGS--.SH DESCRIPTION-.B penny-fit-works with data you have downloaded from your financial-institution. It parses statements that you have downloaded and adds-transactions from the statements to your ledger, skipping those that-have already been added. It also helps you reconcile your ledger with-financial institution statements.--First you will have to configure and compile a-.B penny-fit-binary. You can use the-.B penny-fit-sample.hs-file in the-.B penny-bin-package as an example. The comments in that file should help you get-started.--.SH IMPORTING--To use-.BR penny-fit ,-first you will download the appropriate data from your financial-institution and place it in a file.-.I penny-fit -h-will tell give you a little more information about the place to look-on your institution's web site to download the data. Currently-.B penny-fit-can handle Bank of America and American Express data; maybe you can-add support for more institutions.--Then, run-.IR "penny-fit -f ACCOUNT import FILENAME" .-The first time you run this command, you will have to add the-.I --new-option after the-.I import-command, which allows the creation of a new database. Without this-option, if the database is not found,-.B penny-fit-quits with an error message. The ACCOUNT must be a financial-institution account that you configured in your-.B penny-fit-binary (if you configured a default account and you want to use that,-you can omit the-.I -f-option.) The FILENAME is the location of the data that you just-downloaded.--The-.I import-command will examine the data that you downloaded. Using the unique-identifiers already assigned to each posting by your financial-institution, the-.I import-command determines whether you have already downloaded each particular-posting. If the posting is new,-.I import-assigns a different, unique number to the posting. This is called a-.IR U-number .-The U-number allows you to uniquely identify each posting that you-download. The data from the financial institution, along with the-U-number, is added to a database at the location specified in your-configuration.-.I import-automatically skips postings that have already been processed, so you-do not have to worry about importing duplicate postings.--.SH MERGING--Next you will want to merge new postings into your ledger. Do this by-running-.I penny-fit -f ACCOUNT merge LEDGER_FILE...-where LEDGER_FILE is one or more filenames for your ledger files.-.I merge-examines your ledgers to see if each of the postings in the database-for this financial institution is represented in your ledger. To do-this it looks at the postings in the-.I pennyAcct-specified in your configuration. For each U-number in the database,-.I merge-sees if there is a posting in the-.I pennyAcct-with a tag bearing the U-number (e.g. if the U-number is 5, it looks-for the tag-.IR U5 .)-If a posting has more than one U-number tag, only the first is used;-the others are ignored. If such a posting is found,-.I merge-moves on to the next U-number in the database.--If no matching posting is found for a U-number,-.I merge-sees if there is a matching posting that does-.I not-have a U-number tag. If there is a posting in the-.I pennyAcct-that has the same quantity and date as the financial institution-posting,-.I merge-will then examine the debit or credit of the ledger posting. This-table describes whether-.I penny-fit-will find a match:--.TS-tab(:);-l l l l-- - - --l l l l.-T{-If the financial institution posting is a-T}:T{-and translator is-T}:T{-and the ledger posting is a-T}:T{-then is there a match?-T}-increase:IncreaseIsDebit:debit:Yes-increase:IncreaseIsDebit:credit:No-increase:IncreaseIsCredit:debit:No-increase:IncreaseIsCredit:credit:Yes-decrease:IncreaseIsDebit:debit:No-decrease:IncreaseIsDebit:credit:Yes-decrease:IncreaseIsCredit:debit:Yes-decrease:IncreaseIsCredit:credit:No-.TE--If-.B penny-fit-finds a match for a financial institution posting in this way, then it-will assign a new U-number tag to the posting. If-.B penny-fit-does not find a match, then it will create an entirely new transaction-and append it to the end of your ledger.--If it is creating an entirely new transaction,-.B penny-fit-will attempt to give the new transaction the same account and payee-information that you have used for similar transactions in the-past. To do this,-.B penny-fit-will first search through the database to find the most recent-financial institution posting that has the same payee as the one of-the new transaction. If one is found,-.B penny-fit-then searches through the postings in your ledger file to find the one-that has the same U-number and account as the old financial-institution posting. If it is found,-.B penny-fit-will assign the payee name found on the posting in the ledger to the-new posting. Also, if the posting found in the ledger has exactly one-sibling posting,-.B penny-fit-will assign the same account name from that sibling to the new-sibling.--You can turn off this automatic assignment of information by using the-.I --no-auto-or -.I -n-option to the-.I merge-command.--The result of-.I merge-is printed to standard output. You will probably want to redirect it-to a new file. Use-.BR diff (1)-or-.BR penny-diff (1)-to see what changes-.I merge-made. Typically you will need to edit the output somewhat.--.SH RECONCILING-Next you may wish to reconcile your ledger with your financial-institution data (that is, "balance the checkbook".) Typically the-most time-consuming part of this process is finding the postings in-your ledger that match the postings on your bank statement.-.B penny-fit clear-will help with this, dramatically speeding up the process. To do-this, download data from your financial institution that corresponds-to the data that is covered within the current statement period. Run-.B penny-fit import-and-.B penny-fit merge-as described above. Then run--.EX-penny-fit -f ACCOUNT clear FIT_FILE LEDGER_FILE...-.EE--where FIT_FILE is the data file you downloaded from your financial-institution, and LEDGER_FILE contains your ledger data. The-.I clear-command will mark as cleared (that is, assign a-.I C-flag to) all postings in your LEDGER_FILEs that correspond to one of-the postings in the FIT_FILE. It does this by matching the U-number-tags on your postings to the U-numbers in the database. If a posting-has more than one U-number tag, only the first is used; the others are-ignored. The results are printed to standard output; you will probably-want to redirect this to a file. Once you have verified that things-are as they should be, you can use-.BR penny-reconcile (1)-to mark the cleared postings as reconciled.-.BR penny-basics (7)-has more details on how to use-.B penny-when reconciling a financial institution statement.--.SH OTHER COMMANDS-The-.I database-command prints the database for a particular financial institution to-standard output in human-readable form (the database unfortunately is-not in plain human-readable text.) For instance you might use this to-see what U-number is assigned to a particular financial institution-posting.--The-.I print-command parses a downloaded file of financial institution data and-prints the result to standard output. This is useful for seeing the-contents of a financial institution data file, or for testing new-parsers.--.SH BUGS-To quote another man page: "Bugs? You must be kidding, there are no-bugs in this software. But if we happen to be wrong, send us an email-with as much detail as possible to" omari@smileystation.com.--.SH SEE ALSO-.BR penny-suite (7)
+ man/penny-fit.7 view
@@ -0,0 +1,374 @@+.TH penny-fit 7+.+.SH NAME+penny-fit - Penny financial institution statements parser+.+.SH SYNOPSIS+.B penny-fit+[global-options] COMMAND [local-options] ARGS+.+.SH DESCRIPTION+.+.B penny-fit+works with data you have downloaded from your financial+institution.+.+It parses statements that you have downloaded and adds+transactions from the statements to your ledger, skipping those that+have already been added.+.+It also helps you reconcile your ledger with+financial institution statements.+.+.P+First you will have to configure and compile a+.B penny-fit+binary.+.+You can use the+.B penny-fit-sample.hs+file in the+.I doc+directory of the+.B penny-bin+package as an example.+.+Alternatively, if you know your way around Haskell, see the Haddock+documentation for the+.I Penny.Brenner+module.+.+The comments in that file should help you get+started.+.+.P+Currently Penny includes a parser for Open Financial+Exchange, or OFX, data.+.+Many banks make information available in this+format, as Quicken and the now-defunct Microsoft Money both support+the format.+.+.SH IMPORTING+.+To use+.BR penny-fit ,+first you will download the appropriate data from your financial+institution and place it in a file.+.+.I penny-fit info+will tell give you a little more information about the place to look+on your institution's web site to download the data, if you have+configured that information in your binary.+.+.P+Then, run+.IR "penny-fit -f ACCOUNT import FILENAME" .+The first time you run this command, you will have to add the+.I --new+option after the+.I import+command, which allows the creation of a new database.+.+Without this+option, if the database is not found,+.B penny-fit+quits with an error message.+.+The ACCOUNT must be a financial+institution account that you configured in your+.B penny-fit+binary (if you configured a default account and you want to use that,+you can omit the+.I -f+option).+.+The FILENAME is the location of the data that you just+downloaded.+.+.P+The+.I import+command will examine the data that you downloaded.+.+Using the unique+identifiers already assigned to each posting by your financial+institution, the+.I import+command determines whether you have already downloaded each particular+posting.+.+If the posting is new,+.I import+assigns a different, unique number to the posting.+.+This is called a+.IR U-number .+The U-number allows you to uniquely identify each posting that you+download.+.+The data from the financial institution, along with the+U-number, is added to a database at the location specified in your+configuration.+.I import+automatically skips postings that have already been processed, so you+do not have to worry about importing duplicate postings.+.+.SH MERGING+.+Next you will want to merge new postings into your ledger.+.+Do this by+running+.I penny-fit -f ACCOUNT merge LEDGER_FILE...+where LEDGER_FILE is one or more filenames for your ledger files.+.+.I merge+examines your ledgers to see if each of the postings in the database+for this financial institution is represented in your ledger.+.+To do+this it looks at the postings in the+.I pennyAcct+specified in your configuration.+.+For each U-number in the database,+.I merge+sees if there is a posting in the+.I pennyAcct+with a tag bearing the U-number (e.g. if the U-number is 5, it looks+for the tag+.IR U5 ).+If a posting has more than one U-number tag, only the first is used;+the others are ignored.+.+If such a posting is found,+.I merge+moves on to the next U-number in the database.+.+If no matching posting is found for a U-number,+.I merge+sees if there is a matching posting that does+.I not+have a U-number tag.+.+If there is a posting in the+.I pennyAcct+that has the same quantity and date as the financial institution+posting,+.I merge+will then examine the debit or credit of the ledger posting.+.+.P+This table describes whether+.I penny-fit+will find a match:+.+.TS+tab(:);+l l l l+- - - -+l l l l.+T{+If the financial institution posting is a+T}:T{+and translator is+T}:T{+and the ledger posting is a+T}:T{+then is there a match?+T}+increase:IncreaseIsDebit:debit:Yes+increase:IncreaseIsDebit:credit:No+increase:IncreaseIsCredit:debit:No+increase:IncreaseIsCredit:credit:Yes+decrease:IncreaseIsDebit:debit:No+decrease:IncreaseIsDebit:credit:Yes+decrease:IncreaseIsCredit:debit:Yes+decrease:IncreaseIsCredit:credit:No+.TE+.+If+.B penny-fit+finds a match for a financial institution posting in this way, then it+will assign a new U-number tag to the posting.+.+If+.B penny-fit+does not find a match, then it will create an entirely new transaction+and append it to the end of your ledger.+.+.P+If it is creating an entirely new transaction,+.B penny-fit+will attempt to give the new transaction the same account and payee+information that you have used for similar transactions in the+past.+.+To do this,+.B penny-fit+will first search through the database to find the most recent+financial institution posting that has the same payee as the one of+the new transaction. If one is found,+.B penny-fit+then searches through the postings in your ledger file to find the one+that has the same U-number and account as the old financial+institution posting.+.+If it is found,+.B penny-fit+will assign the payee name found on the posting in the ledger to the+new posting.+.+Also, if the posting found in the ledger has exactly one+sibling posting,+.B penny-fit+will assign the same account name from that sibling to the new+sibling.+.+You can turn off this automatic assignment of information by using the+.I --no-auto+or +.I -n+option to the+.I merge+command.+.+.P+The result of+.I merge+is printed to standard output, unless you use the+.I --output FILENAME+or+.I -o FILENAME+option, in which case the output is sent to+.IR FILENAME .+.+You can use multiple+.I -o+options.+.+To explicitly send output to standard output, use+.IR "-o -" .+.+Use+.BR diff (1)+or+.BR penny-diff (1)+to see what changes+.I merge+made.+.+Typically you will need to edit the output somewhat.+.+.SH RECONCILING+.+Next you may wish to reconcile your ledger with your financial+institution data (that is, "balance the checkbook").+.+Typically the+most time-consuming part of this process is finding the postings in+your ledger that match the postings on your bank statement.+.+.B penny-fit clear+will help with this, dramatically speeding up the process.+.+To do+this, download data from your financial institution that corresponds+to the data that is covered within the current statement period.+.+Run+.B penny-fit import+and+.B penny-fit merge+as described above. Then run+.+.P+.EX+penny-fit -f ACCOUNT clear FIT_FILE LEDGER_FILE...+.EE+.+.P+where FIT_FILE is the data file you downloaded from your financial+institution, and LEDGER_FILE contains your ledger data.+.+The+.I clear+command will mark as cleared (that is, assign a+.I C+flag to) all postings in your LEDGER_FILEs that correspond to one of+the postings in the FIT_FILE.+.+It does this by matching the U-number+tags on your postings to the U-numbers in the database.+.+If a posting+has more than one U-number tag, only the first is used; the others are+ignored.+.+.P+As with the +.I merge+command, the results are printed to standard output unless you use the+.I --output FILENAME+or+.I -o FILENAME+option.+.+Once you have verified that things+are as they should be, you can use+.BR penny-reconcile (1)+to mark the cleared postings as reconciled.+.+.BR penny-basics (7)+has more details on how to use+.B penny+when reconciling a financial institution statement.+.+.SH OTHER COMMANDS+.+The+.I database+command prints the database for a particular financial institution to+standard output in human-readable form (the database unfortunately is+not in plain human-readable text).+.+For instance you might use this to+see what U-number is assigned to a particular financial institution+posting.+.+.P+The+.I print+command parses a downloaded file of financial institution data and+prints the result to standard output.+.+This is useful for seeing the+contents of a financial institution data file, or for testing new+parsers.+.+.P+Every+.B penny-fit+command has a+.I -h+and a+.I --help+option.+.+There is also a global+.I --help+option, as in+.IR "penny-fit --help" .+.+.SH BUGS+To quote another man page: "Bugs?+.+You must be kidding, there are no+bugs in this software.+.+But if we happen to be wrong, send us an email+with as much detail as possible to" omari@smileystation.com.+.+.SH SEE ALSO+.BR penny-suite (7)
man/penny-reconcile.1 view
@@ -1,12 +1,13 @@ .TH penny-reconcile 1-+. .SH NAME penny-reconcile - mark cleared postings as reconciled-+. .SH SYNOPSIS .B penny-reconcile+[options] FILE...-+. .SH DESCRIPTION Finds all postings in the input ledger files whose flag is exactly one letter: the letter@@ -18,25 +19,47 @@ postings in your ledger that match a posting on your bank statement with a .I C-flag. Because only postings from the current statement will be marked with a+flag.+.+Because only postings from the current statement will be marked with a .I C flag, it is easier to use .BR penny (1)-to list only the postings that you have just cleared. Then, after+to list only the postings that you have just cleared.+.+Then, after ensuring that the statement is properly reconciled, .B penny-reconcile will automatically mark all the posts reconciled.-+. If no .IR FILE ", or " FILE " is " - , read standard input.-+.+.SH OPTIONS+.+.TP+.IR "--output FILENAME", " -o FILENAME"+.+send output to+.I FILENAME+instead of standard output.+.+You can use multiple+.I --output+options; to explicitly print to standard output, use+.IR "--output -" .+. .SH VERSUS sed(1) OR YOUR TEXT EDITOR You could do this with .BR sed (1)-or your text editor. Unlike those programs,+or your text editor.+.+Unlike those programs, .B penny-reconcile-knows the structure of a ledger file. So+knows the structure of a ledger file.+.+So .B penny-reconcile will not, for example, change the text .IR [C] " to " [R]@@ -44,9 +67,11 @@ .I [C] appears within a comment, while a naive .BR sed (1)-script would do so. This would happen only rarely though, so you might+script would do so.+.+This would happen only rarely though, so you might be just fine using a query-replace function in your text editor.-+. Also, .B penny-reconcile will tidy up your ledger file--that is, it might rearrange or delete@@ -55,16 +80,16 @@ This might be good or bad. .BR sed (1) or your text editor, on the other hand, will not do this.-+. .SH EXIT STATUS 0 if everything went fine; some other value if something went wrong (e.g. a ledger file could not be parsed.)-+. .SH BUGS Please report bugs in the program or documentation to .MT omari@smileystation.com Omari Norman. .ME-+. .SH SEE ALSO .BR penny-suite (7)
man/penny-reprint.1 view
@@ -1,19 +1,22 @@ .TH penny-reprint 1-+. .SH NAME penny-reprint - read and reprint Penny ledger-+. .SH SYNOPSIS .B penny-reprint+[options] .I FILE...-+. .SH DESCRIPTION-+. Reads the Penny ledger(s) you specify and prints them to standard-output, in a (hopefully) more tidy format. All comments are retained,+output, in a (hopefully) more tidy format.+.+All comments are retained, but blank lines and insignicant whitespace within the transactions (such as the amount of whitespace between multiple tags) are ignored.-+. If no .IR FILE , or@@ -21,16 +24,30 @@ is .IR - , read standard input.-+.+.SH OPTIONS+.+.TP+.IR "--output FILENAME", " -o FILENAME"+.+send output to+.I FILENAME+instead of standard output.+.+You can use multiple+.I --output+options; to explicitly print to standard output, use+.IR "--output -" .+. .SH EXIT STATUS 0 if everything went fine; another value if there were problems (such as inability to read a ledger file.)-+. .SH BUGS Please report any bugs in the program or documentation to .MT omari@smileystation.com Omari Norman .ME-+. .SH SEE ALSO .BR penny-suite (7)
man/penny-suite.7 view
@@ -1,136 +1,136 @@ .TH penny-suite 7-+. .SH NAME penny-suite - extensible double-entry accounting system-+. .SH DESCRIPTION-+. This manual page lists all the different components of Penny and also catalogues all the documentation files and manual pages that are available.-+. .SH PENNY PROGRAMS-+. Penny consists of many programs. Each has its own manual page.-+. .TP .BR penny (1)-+. reports on postings in your ledger file-+. .TP .BR penny-selloff (1)-+. calculate capital gains and losses on commodity sales-+. .TP .BR penny-diff (1)-+. show differences between ledger files-+. .TP .BR penny-reprint (1)-+. tidy up a ledger file, retaining comments-+. .TP .BR penny-reconcile (1)-+. marks cleared postings as reconciled-+. .SH PENNY MANUAL PAGES-+. In addition to the manual pages shown above, a few more overview man pages are available.-+. .TP .BR penny-basics (7)-+. getting started with .B penny-+. .TP .BR penny-examples (7)-+. more examples of .B penny usage-+. .TP .BR penny-commodities (7)-+. tracking multiple commodities, such as stocks, with Penny-+. .TP .BR penny-fit (1)-+. describes how you might be able to create a program that automatically parses downloaded statements from your financial institution and merges the resulting postings into your ledger file.-+. .TP .BR penny-custom (7)-+. how to make a custom \fBpenny\fR program with your own settings-+. .SH SAMPLE FILES-+. There are many files available filled with sample data. They are in the .I examples directory of the .B penny-bin tarball.-+. .TP .BR starter.pny-+. describes the basics of the Penny ledger file format, and contains sample data. The .BR penny-basics (7) manual page uses the sample data from this file.-+. .TP .BR stocks.pny-+. shows how to use Penny to track multiple commodities, like stocks-+. .TP .BR stocks-realized.pny-+. shows an example of the results of using .BR penny-selloff (1)--+.+. .SH TEXT FILES-+. Much documentation is available only in plain-text form. These files are in the .B doc directory of the .B penny-bin tarball.-+. .TP .BR amex-file-format.org-+. the file format American Express uses for its downloadable data-+. .TP .BR bofa-file-format.org-+. the file format Bank of America uses for its downloadable data-+. .TP .BR dependencies.dot-+. the dependencies between the various parts of the Penny library-+. .TP .BR ledger-grammar.org-+. an EBNF grammar for the ledger file format-+. .SH HADDOCK-+. Feel free to examine the Haskell source code of the Penny library, which also contains Haddock documentation markup.
penny-bin.cabal view
@@ -1,5 +1,5 @@ Name: penny-bin-Version: 0.12.0.0+Version: 0.14.0.0 Cabal-version: >=1.8 Build-Type: Simple License: BSD3@@ -53,6 +53,13 @@ . + * You can easily build a program to process downloads of Open+ Financial Exchange data from your financial institution. Your+ program will merge new transactions into your ledger+ automatically.++ .+ * Full Unicode support. .@@ -166,6 +173,8 @@ , README , doc/*.org , doc/*.dot+ , doc/*.ini+ , doc/*.hs , examples/*.pny , man/*.1 , man/*.7@@ -178,7 +187,7 @@ Executable penny Build-depends: base ==4.*,- penny-lib ==0.12.*+ penny-lib ==0.14.* Main-is: penny-main.hs Other-modules: Paths_penny_bin@@ -192,13 +201,13 @@ Executable penny-selloff Build-depends: base == 4.*,- penny-lib ==0.12.*,+ penny-lib ==0.14.*, explicit-exception ==0.1.*, containers ==0.5.*, semigroups ==0.9.*, text ==0.11.*, parsec ==3.1.*,- multiarg ==0.14.*,+ multiarg ==0.16.*, transformers ==0.3.* Main-is: penny-selloff.hs@@ -212,9 +221,9 @@ Executable penny-diff Build-depends: base ==4.*,- penny-lib ==0.12.*,+ penny-lib ==0.14.*, text ==0.11.*,- multiarg ==0.14.*,+ multiarg ==0.16.*, explicit-exception == 0.1.* Main-is: penny-diff.hs@@ -229,8 +238,8 @@ Executable penny-reprint Build-depends: base ==4.*- , multiarg ==0.14.*- , penny-lib ==0.12.*+ , multiarg ==0.16.*+ , penny-lib ==0.14.* , pretty-show ==1.5.* , text ==0.11.* @@ -243,9 +252,9 @@ Executable penny-reconcile Build-depends: base ==4.*- , penny-lib ==0.12.*+ , penny-lib ==0.14.* , text ==0.11.*- , multiarg ==0.14.*+ , multiarg ==0.16.* , explicit-exception ==0.1.* main-is: penny-reconcile.hs@@ -278,3 +287,4 @@ Flag build-reconcile Description: Build the penny-reconcile executable Default: True+
penny-diff.hs view
@@ -1,16 +1,16 @@ module Main where -import qualified Control.Exception as CEx-import qualified Control.Monad.Exception.Synchronous as Ex+import Control.Arrow (first, second) import Data.Either (partitionEithers) import Data.Maybe (fromJust) import Data.List (deleteFirstsBy) import qualified System.Console.MultiArg as M import qualified Penny.Liberty as Ly import qualified Penny.Lincoln as L-import qualified Penny.Lincoln.Predicates as P+import Penny.Lincoln ((==~)) import qualified Penny.Copper as C import qualified Penny.Copper.Render as CR+import qualified Penny.Steel.Sums as S import Data.Maybe (mapMaybe) import qualified Data.Text as X import qualified Data.Text.IO as TIO@@ -59,26 +59,27 @@ deriving (Eq, Show) -- | All possible items, but excluding blank lines.-data NonBlankItem- = Transaction L.Transaction- | Price L.PricePoint- | Comment C.Comment- deriving (Eq, Show)+type NonBlankItem =+ S.S3 L.Transaction L.PricePoint C.Comment +removeMeta+ :: L.Transaction+ -> (L.TopLineCore, L.Ents L.PostingCore)+removeMeta+ = first L.tlCore+ . second (fmap L.pdCore)+ . L.unTransaction+ clonedNonBlankItem :: NonBlankItem -> NonBlankItem -> Bool clonedNonBlankItem nb1 nb2 = case (nb1, nb2) of- (Transaction t1, Transaction t2) -> P.clonedTransactions t1 t2- (Price p1, Price p2) -> p1 == p2- (Comment c1, Comment c2) -> c1 == c2+ (S.S3a t1, S.S3a t2) -> removeMeta t1 ==~ removeMeta t2+ (S.S3b p1, S.S3b p2) -> p1 ==~ p2+ (S.S3c c1, S.S3c c2) -> c1 == c2 _ -> False -toNonBlankItem :: C.Item -> Maybe NonBlankItem-toNonBlankItem i = case i of- C.Transaction t -> Just (Transaction t)- C.PricePoint p -> Just (Price p)- C.IComment c -> Just (Comment c)- _ -> Nothing-+toNonBlankItem :: C.LedgerItem -> Maybe NonBlankItem+toNonBlankItem = S.caseS4 (Just . S.S3a) (Just . S.S3b) (Just . S.S3c)+ (const Nothing) showLineNum :: File -> Int -> X.Text showLineNum f i = X.pack ("\n" ++ arrow ++ " " ++ show i ++ "\n")@@ -97,15 +98,16 @@ -> File -> L.Transaction -> Maybe X.Text-renderTransaction gs f t = fmap addHeader $ CR.transaction gs t+renderTransaction gs f t = fmap addHeader $ CR.transaction gs (noMeta t) where- (L.Family tl _ _ _) = L.unTransaction t- lin = case L.tMemo tl of- Nothing -> L.unTopLineLine . fromJust- . L.tTopLineLine $ tl- Just _ -> L.unTopMemoLine . fromJust- . L.tTopMemoLine $ tl+ lin = case L.tMemo . L.tlCore . fst . L.unTransaction $ t of+ Nothing -> L.unTopLineLine . L.tTopLineLine . fromJust+ . L.tlFileMeta . fst . L.unTransaction $ t+ Just _ -> L.unTopMemoLine . fromJust . L.tTopMemoLine . fromJust+ . L.tlFileMeta . fst . L.unTransaction $ t addHeader x = (showLineNum f lin) `X.append` x+ noMeta txn = let (tl, es) = L.unTransaction txn+ in (L.tlCore tl, fmap L.pdCore es) renderPrice :: CR.GroupSpecs -> File -> L.PricePoint -> Maybe X.Text renderPrice gs f p = fmap addHeader $ CR.price gs p@@ -114,16 +116,14 @@ addHeader x = (showLineNum f lin) `X.append` x renderNonBlankItem :: CR.GroupSpecs -> File -> NonBlankItem -> Maybe X.Text-renderNonBlankItem gs f n = case n of- Transaction t -> renderTransaction gs f t- Price p -> renderPrice gs f p- Comment c -> CR.comment c+renderNonBlankItem gs f =+ S.caseS3 (renderTransaction gs f) (renderPrice gs f) CR.comment runPennyDiff :: CR.GroupSpecs -> IO () runPennyDiff co = do (f1, f2, dts) <- parseCommandLine- l1 <- parseFile f1- l2 <- parseFile f2+ l1 <- C.open [f1]+ l2 <- C.open [f2] let (r1, r2) = doDiffs l1 l2 showDiffs co dts (r1, r2) case (r1, r2) of@@ -167,36 +167,22 @@ -- but not in file2, and snd p is items that appear in file2 but not -- in file1. doDiffs- :: C.Ledger- -> C.Ledger+ :: [C.LedgerItem]+ -> [C.LedgerItem] -> ([NonBlankItem], [NonBlankItem]) doDiffs l1 l2 = (r1, r2) where- mkNbList = mapMaybe toNonBlankItem . C.unLedger+ mkNbList = mapMaybe toNonBlankItem (nb1, nb2) = (mkNbList l1, mkNbList l2) df = deleteFirstsBy clonedNonBlankItem (r1, r2) = (nb1 `df` nb2, nb2 `df` nb1) -parseFile :: String -> IO C.Ledger-parseFile s = do- eiTxt <- CEx.try $ TIO.readFile s- txt <- case eiTxt of- Left e -> failure (show (e :: IOError))- Right g -> return g- let fn = L.Filename . X.pack $ s- c = C.FileContents txt- parsed = C.parse [(fn, c)]- case parsed of- Ex.Exception e -> failure (show e)- Ex.Success g -> return g-- -- | Returns a tuple with the first filename, the second filename, and -- an indication of which differences to show. parseCommandLine :: IO (String, String, DiffsToShow) parseCommandLine = do as <- M.simpleWithHelp help M.Intersperse allOpts- (Right . Filename)+ (return . Right . Filename) let (doVer, args) = partitionEithers as toFilename a = case a of Filename s -> Just s
penny-reconcile.hs view
@@ -2,40 +2,40 @@ import Data.Maybe (fromMaybe, fromJust) import qualified Data.Text as X-import qualified Data.Text.IO as TIO import Control.Monad (guard) import qualified Penny.Copper as C import qualified Penny.Lincoln as L import qualified Penny.Liberty as Ly+import qualified Penny.Steel.Sums as S import qualified System.Console.MultiArg as MA import qualified Paths_penny_bin as PPB -- | Changes a posting to mark it reconciled, if it was already marked -- as cleared.-changePosting :: L.Posting -> L.PostingChangeData-changePosting p = fromMaybe L.emptyPostingChangeData $ do- fl <- L.pFlag p+changePosting :: L.PostingData -> L.PostingData+changePosting p = fromMaybe p $ do+ let c = L.pdCore p+ fl <- L.pFlag c guard (L.unFlag fl == X.singleton 'C') let fl' = L.Flag . X.singleton $ 'R'- return $ L.emptyPostingChangeData { L.pcFlag = Just (Just fl') }+ c' = c { L.pFlag = Just fl' }+ return p { L.pdCore = c' } -- | Changes a TopLine to mark it as reconciled, if it was already -- marked as cleared.-changeTopLine :: L.TopLine -> L.TopLineChangeData-changeTopLine t = fromMaybe L.emptyTopLineChangeData $ do- fl <- L.tFlag t+changeTopLine :: L.TopLineData -> L.TopLineData+changeTopLine t = fromMaybe t $ do+ let c = L.tlCore t+ fl <- L.tFlag c guard (L.unFlag fl == X.singleton 'C') let fl' = L.Flag . X.singleton $ 'R'- return $ L.emptyTopLineChangeData { L.tcFlag = Just (Just fl') }+ c' = c { L.tFlag = Just fl' }+ return t { L.tlCore = c' } changeTransaction :: L.Transaction -> L.Transaction-changeTransaction t =- let fam = L.mapParent changeTopLine- . L.mapChildren changePosting- . L.unTransaction- $ t- in L.changeTransaction fam t+changeTransaction (L.Transaction (tl, es)) =+ L.Transaction (changeTopLine tl, fmap changePosting es) help :: String -> String help pn = unlines@@ -48,6 +48,10 @@ , "changed." , "" , "Options:"+ , " --output FILENAME, -o FILENAME"+ , " send output to FILENAME rather than standard output"+ , " (multiple -o options are allowed; use \"-\" for standard"+ , " output)" , " -h, --help - Show help and exit." , " --version - Show version and exit" ]@@ -55,27 +59,24 @@ groupSpecs :: C.GroupSpecs groupSpecs = C.GroupSpecs C.NoGrouping C.NoGrouping --- | The first element if the pair is a no-op if the user does not--- need to see the version, or an IO action to print the version if--- the user wants to see it. The second element is the list of command--- line arguments.-type Opts = (IO (), [String])--allOpts :: [MA.OptSpec (Opts -> Opts)]-allOpts = [ fmap (\act (_, ss) -> (act, ss)) $- Ly.version PPB.version- ]+type ShowVer = IO ()+type Printer = X.Text -> IO ()+type PosArg = String+type Arg = S.S3 ShowVer Printer PosArg -posArg :: String -> Opts -> Opts-posArg s (a, ss) = (a, s:ss)+allOpts :: [MA.OptSpec Arg]+allOpts =+ [ fmap S.S3a $ Ly.version PPB.version+ , fmap S.S3b Ly.output+ ] main :: IO () main = do- as <- MA.simpleWithHelp help MA.Intersperse allOpts posArg- let opts = foldr ($) (return (), []) as- fst opts- led <- C.open . snd $ opts- let led' = C.mapLedger (C.mapItem id id changeTransaction) led- rend = fromJust $ C.ledger groupSpecs led'- TIO.putStr rend+ as <- MA.simpleWithHelp help MA.Intersperse allOpts (fmap return S.S3c)+ let (showVer, printers, posArgs) = S.partitionS3 as+ sequence_ showVer+ led <- C.open posArgs+ let led' = map (S.mapS4 changeTransaction id id id) led+ rend = fromJust $ mapM (C.item groupSpecs) (map C.stripMeta led')+ let txt = X.concat rend in txt `seq` (Ly.processOutput printers txt)
penny-reprint.hs view
@@ -3,8 +3,9 @@ import qualified Penny.Copper as C import qualified Penny.Copper.Render as R import qualified Penny.Liberty as Ly-import qualified Data.Text.IO as TIO+import qualified Data.Text as X import qualified System.Console.MultiArg as MA+import qualified Penny.Steel.Sums as S import qualified Paths_penny_bin as PPB @@ -21,6 +22,10 @@ , "Result is printed to standard output." , "" , "Options:"+ , " --output FILENAME, -o FILENAME"+ , " send output to FILENAME rather than standard output"+ , " (multiple -o options are allowed; use \"-\" for standard"+ , " output)" , " --help, -h - show help and exit" , " --version - show version and exit" ]@@ -29,21 +34,25 @@ groupSpecs = R.GroupSpecs R.NoGrouping R.NoGrouping type MaybeShowVer = IO ()-type Opts = (MaybeShowVer, [String])+type Printer = X.Text -> IO ()+type PosArg = String -posArg :: String -> Opts -> Opts-posArg s (a, ss) = (a, s:ss)+type Arg = S.S3 MaybeShowVer Printer PosArg -allOpts :: [MA.OptSpec (Opts -> Opts)]-allOpts = [ fmap (\a (_, ss) -> (a, ss))- $ Ly.version PPB.version ]+allOpts :: [MA.OptSpec Arg]+allOpts =+ [ fmap S.S3a $ Ly.version PPB.version+ , fmap S.S3b $ Ly.output+ ] main :: IO () main = do- as <- MA.simpleWithHelp help MA.Intersperse allOpts posArg- let opts = foldr ($) (return (), []) as- fst opts- l <- C.open . snd $ opts- case R.ledger groupSpecs l of+ as <- MA.simpleWithHelp help MA.Intersperse allOpts (fmap return S.S3c)+ let (showVers, printers, posArgs) = S.partitionS3 as+ sequence_ showVers+ l <- C.open posArgs+ case mapM (R.item groupSpecs) (map C.stripMeta l) of Nothing -> error "could not render final ledger."- Just x -> TIO.putStr x+ Just x ->+ let txt = X.concat x+ in txt `seq` (Ly.processOutput printers txt)
penny-selloff.hs view
@@ -93,20 +93,19 @@ module Main (main) where +import Control.Arrow (first)+import Control.Applicative ((<$>), (<*>), pure) import qualified Control.Monad.Exception.Synchronous as Ex import Control.Monad (when) import qualified Control.Monad.Trans.State as St import Control.Monad.Trans.Class (lift)-import Data.Foldable (toList) import Data.List (find)-import Data.List.NonEmpty (NonEmpty((:|)))-import qualified Data.List.NonEmpty as NE import Data.Maybe (isJust, mapMaybe, catMaybes, fromMaybe) import Data.Text (pack) import qualified Data.Text as X import qualified Data.Text.IO as TIO import qualified Penny.Lincoln.Balance as Bal-import qualified Penny.Lincoln.Transaction.Unverified as U+import qualified Penny.Steel.Sums as S import qualified Penny.Cabin.Balance.Util as BU import Penny.Cabin.Options (ShowZeroBalances(..)) import qualified Penny.Copper as Cop@@ -116,7 +115,7 @@ import qualified Penny.Lincoln as L import qualified Data.Map as M import qualified System.Console.MultiArg as MA-import Text.Parsec as Parsec+import qualified Text.Parsec as Parsec import qualified Paths_penny_bin as PPB groupingSpec :: CR.GroupSpecs@@ -128,7 +127,6 @@ = ParseFail MA.Error | NoInputArgs | ProceedsParseFailed Parsec.ParseError- | LedgerParseError Cop.ErrorMsg | NoSelloffAccount | NotThreeSelloffSubAccounts | BadSelloffBalance@@ -157,11 +155,12 @@ allOpts :: [MA.OptSpec (Opts -> Opts)] allOpts = [ fmap (\a (_, ss) -> (a, ss)) $ Ly.version PPB.version ] -data ParseResult = ParseResult ProceedsAcct Cop.Ledger+data ParseResult = ParseResult ProceedsAcct [Cop.LedgerItem] parseCommandLine :: IO ParseResult parseCommandLine = do- as <- MA.simpleWithHelp help MA.Intersperse allOpts posArg+ as <- MA.simpleWithHelp help MA.Intersperse allOpts+ (fmap return posArg) let opts = foldr ($) (return (), []) as fst opts x:xs <- case snd opts of@@ -183,17 +182,14 @@ , " --version - show version and exit." ] -calcBalances :: Cop.Ledger -> [(L.Account, L.Balance)]+calcBalances :: [Cop.LedgerItem] -> [(L.Account, L.Balance)] calcBalances =- let toTxn i = case i of- Cop.Transaction t -> Just t- _ -> Nothing- in BU.flatten- . BU.balances (ShowZeroBalances False)- . map (L.Box ())- . concatMap L.postFam- . mapMaybe toTxn- . Cop.unLedger+ BU.flatten+ . BU.balances (ShowZeroBalances False)+ . map (\p -> ((), p))+ . concatMap L.transactionToPostings+ . mapMaybe (S.caseS4 Just (const Nothing) (const Nothing)+ (const Nothing)) newtype Group = Group { unGroup :: L.SubAccount } deriving (Show, Eq)@@ -242,16 +238,14 @@ Bal.NonZero col -> Just (cy, col) ps = mapMaybe toPair . M.toList $ m findBal dc = Ex.fromMaybe BadSelloffBalance- . find ((== dc) . Bal.drCr . snd)+ . find ((== dc) . Bal.colDrCr . snd) $ ps (cyStock, (Bal.Column _ qtyStock)) <- findBal L.Debit (cyCurr, (Bal.Column _ qtyCurr)) <- findBal L.Credit let sellStock = SelloffStock- (L.Amount qtyStock cyStock- (Just L.CommodityOnLeft) (Just L.SpaceBetween))+ (L.Amount qtyStock cyStock) sellCurr = SelloffCurrency- (L.Amount qtyCurr cyCurr- (Just L.CommodityOnLeft) (Just L.SpaceBetween))+ (L.Amount qtyCurr cyCurr) return (sellStock, sellCurr) @@ -317,7 +311,7 @@ Bal.NonZero col -> Just (cy, col) ps = mapMaybe toPair . M.toList $ m findBal dc = Ex.fromMaybe BadPurchaseBalance- . find ((== dc) . Bal.drCr . snd)+ . find ((== dc) . Bal.colDrCr . snd) $ ps (cyStock, (Bal.Column _ qtyStock)) <- findBal L.Credit (cyCurr, (Bal.Column _ qtyCurr)) <- findBal L.Debit@@ -375,9 +369,9 @@ return (Just (p, br)) L.RightBiggerBy unsoldStockQty -> do- let alloced = L.allocate pcq (sq :| [unsoldStockQty])+ let alloced = L.allocate pcq (sq, [unsoldStockQty]) basisSold = case alloced of- x :| (_ : []) -> x+ (x, (_ : [])) -> x _ -> error "stRealizeBasis: error" let css' = case mayCss of Nothing -> CostSharesSold basisSold@@ -441,14 +435,28 @@ Nothing -> return . NoChange $ ls Just (qt, gl) -> do nePurchs <- Ex.fromMaybe NoPurchaseInformation- . NE.nonEmpty $ ls- let qtys = fmap (unPurchaseCurrencyQty . piCurrencyQty . fst)+ . uncons $ ls+ let qtys = mapNE (unPurchaseCurrencyQty . piCurrencyQty . fst) nePurchs alloced = L.allocate qt qtys let mkCapChange (p, br) q = (p, br, CapitalChange q)- r = toList $ NE.zipWith mkCapChange nePurchs alloced+ r = flattenNE $ zipNE mkCapChange nePurchs alloced return $ WithCapitalChanges r gl +mapNE :: (a -> b) -> (a, [a]) -> (b, [b])+mapNE f (a, as) = (f a, map f as)++flattenNE :: (a, [a]) -> [a]+flattenNE (a, as) = a:as++uncons :: [a] -> Maybe (a, [a])+uncons as = case as of+ [] -> Nothing+ x:xs -> Just (x, xs)++zipNE :: (a -> b -> c) -> (a, [a]) -> (b, [b]) -> (c, [c])+zipNE f (a, as) (b, bs) = (f a b, zipWith f as bs)+ memo :: SaleDate -> L.Memo memo (SaleDate sd) = let dTxt = CR.dateTime sd@@ -459,17 +467,21 @@ payee :: L.Payee payee = L.Payee . pack $ "Realize gain or loss" -topLine :: SaleDate -> U.TopLine-topLine sd = (U.emptyTopLine (unSaleDate sd))- { U.tPayee = Just payee- , U.tMemo = Just . memo $ sd+topLine :: SaleDate -> L.TopLineData+topLine sd =+ let core = (L.emptyTopLineCore (unSaleDate sd))+ { L.tPayee = Just payee+ , L.tMemo = Just . memo $ sd }+ in L.TopLineData { L.tlCore = core+ , L.tlFileMeta = Nothing+ , L.tlGlobal = Nothing } basisOffsets :: SelloffInfo -> PurchaseDate -> BasisRealiztn- -> (U.Posting, U.Posting)+ -> ((L.Entry, L.PostingData), (L.Entry, L.PostingData)) basisOffsets s pd p = (po enDr, po enCr) where ac = L.Account [basis, grp, dt]@@ -477,15 +489,23 @@ dt = dateToSubAcct . unPurchaseDate $ pd enDr = L.Entry L.Debit (L.Amount (unRealizedStockQty . brStockQty $ p)- (L.commodity . unSelloffStock . siStock $ s)- (Just L.CommodityOnLeft) (Just L.SpaceBetween))+ (L.commodity . unSelloffStock . siStock $ s)) enCr = L.Entry L.Credit (L.Amount (unRealizedCurrencyQty . brCurrencyQty $ p)- (L.commodity . unSelloffCurrency . siCurrency $ s)- (Just L.CommodityOnLeft) (Just L.SpaceBetween))- po en = (U.emptyPosting ac)- { U.pEntry = Just en }+ (L.commodity . unSelloffCurrency . siCurrency $ s))+ po en = (en, emptyPostingData ac) +emptyPostingData :: L.Account -> L.PostingData+emptyPostingData a =+ let core = (L.emptyPostingCore a)+ { L.pSide = Just L.CommodityOnLeft+ , L.pSpaceBetween = Just L.SpaceBetween+ }+ in L.PostingData { L.pdCore = core+ , L.pdFileMeta = Nothing+ , L.pdGlobal = Nothing+ }+ dateToSubAcct :: L.DateTime -> L.SubAccount dateToSubAcct = L.SubAccount . CR.dateTime @@ -519,25 +539,21 @@ -> SelloffCurrency -> CapitalChange -> L.Entry-capChangeEntry gl sc cc = L.Entry dc (L.Amount qt cy sd sb)+capChangeEntry gl sc cc = L.Entry dc (L.Amount qt cy) where dc = case gl of Gain -> L.Credit Loss -> L.Debit cy = L.commodity . unSelloffCurrency $ sc qt = unCapitalChange cc- sd = Just L.CommodityOnLeft- sb = Just L.SpaceBetween capChangePstg :: SelloffInfo -> GainOrLoss -> CapitalChange -> PurchaseInfo- -> U.Posting-capChangePstg si gl cc p =- (U.emptyPosting ac)- { U.pEntry = Just en }+ -> (L.Entry, L.PostingData)+capChangePstg si gl cc p = (en, emptyPostingData ac) where ac = capChangeAcct gl si p en = capChangeEntry gl (siCurrency si) cc@@ -547,10 +563,10 @@ proceedsPstgs :: SelloffInfo- -> (U.Posting, U.Posting)+ -> ((L.Entry, L.PostingData), (L.Entry, L.PostingData)) proceedsPstgs si = (po dr, po cr) where- po en = (U.emptyPosting ac) { U.pEntry = Just en }+ po en = (en, emptyPostingData ac) ac = L.Account [proceeds, gr, dt] gr = unGroup . siGroup $ si dt = dateToSubAcct . unSaleDate . siSaleDate $ si@@ -562,10 +578,11 @@ :: SelloffInfo -> WithCapitalChanges -> L.Transaction-mkTxn si wcc = Ex.resolve err exTxn+mkTxn si wcc = fromMaybe err exTxn where- err = const $ error "mkTxn: making transaction failed"- exTxn = L.transaction $ L.Family tl p1 p2 ps+ err = error "mkTxn: making transaction failed"+ exTxn = (\topl es -> L.Transaction (topl, es))+ <$> pure tl <*> L.ents entInputs tl = topLine . siSaleDate $ si (p1, p2) = proceedsPstgs si ps = case wcc of@@ -580,10 +597,11 @@ where (b1, b2) = basisOffsets si (piDate p) br c = capChangePstg si gl cc p+ entInputs = map (first Just) (p1:p2:ps) makeOutput :: ProceedsAcct- -> Cop.Ledger+ -> [Cop.LedgerItem] -> Err X.Text makeOutput pa ldgr = do let bals = calcBalances ldgr@@ -597,6 +615,8 @@ . (`X.snoc` '\n') . fromMaybe (error "makeOutput: transaction did not render") . CR.transaction groupingSpec+ . (\t -> let (tl, es) = L.unTransaction t+ in (L.tlCore tl, fmap L.pdCore es)) . mkTxn si $ wcc