misery-0.2/0000755000175000017500000000000011727524133012304 5ustar wookeywookeymisery-0.2/src/0000755000175000017500000000000011460156152013067 5ustar wookeywookeymisery-0.2/src/units.mli0000644000175000017500000000012310754123012014722 0ustar wookeywookeyval initialize : unit -> unit val convert : string -> string -> Sumofmoney.amount misery-0.2/src/groupsummary.ml0000644000175000017500000000531310754123012016167 0ustar wookeywookeyopen Accountdbase open Account let fixme_rexp = Str.regexp "^.*FIXME.*$" let print_group channel group_name accts = let sorted_accts = List.sort (fun acct1 -> fun acct2 -> compare (Account.full_name acct1) (Account.full_name acct2)) accts in output_string channel ""; output_string channel group_name; output_string channel ""; List.iter (fun acct -> let fixme_present = Account.fold_txns (fun flag -> fun txn -> if flag then true else Str.string_match fixme_rexp txn.description 0) false acct in output_string channel ""; output_string channel (Account.full_name acct); output_string channel ""; (if fixme_present then output_string channel "  Needs attention."); output_string channel ""; let must_be_zero = try Account.lookup_boolean_variable "must_be_zero" acct with Not_found -> false in let total = Account.total acct in if must_be_zero then begin if Sumofmoney.is_zero (Sumofmoney.default_to_normal total) then output_string channel "OK" else output_string channel "Not correct" end else begin (if Sumofmoney.non_negative_default total then output_string channel "" else if Sumofmoney.very_negative_default total then output_string channel "" else output_string channel ""); output_string channel (Sumofmoney.to_string_default total) end; output_string channel "" ) sorted_accts; output_string channel "" let emit () = Misc.verbose "Reading title file."; let channel = Misc.open_config_file "title" in try let title = input_line channel in Misc.verbose "Preparing to emit the group summary sheet."; let groups = Accountdbase.group "group" in let html_channel = Htmlout.create_page "index" title in output_string html_channel ""; AccountsMap.iter (print_group html_channel) groups; output_string html_channel "
"; Htmlout.finish_page html_channel with End_of_file -> Misc.fail "Could not read title (check title file has contents)" misery-0.2/src/printaccount.mli0000644000175000017500000000014210754123012016272 0ustar wookeywookey(* Dumping of account balance sheets to HTML files. *) val create_balance_sheets : unit -> unit misery-0.2/src/main.ml0000644000175000017500000000041710754123012014341 0ustar wookeywookeylet _ = Units.initialize (); Accountdbase.load_initial_files (); Meets.process (); Splits.process (); Vaccounts.process (); Vaccounts.process_late (); Printaccount.create_balance_sheets (); Groupsummary.emit (); print_endline "Processed successfully." misery-0.2/src/misc.mli0000644000175000017500000000053610754123012014523 0ustar wookeywookey(* Miscellaneous functions. *) val get_root_directory : unit -> string val open_config_file : string -> in_channel val open_config_file_fail_ok : string -> in_channel val iter_files : string -> (string -> in_channel -> unit) -> unit val format_money : int -> string val round : float -> float val verbose : string -> unit val fail : string -> 'a misery-0.2/src/vaccounts.ml0000644000175000017500000000411710754123012015423 0ustar wookeywookeyopen Account open Unix let read_headers_from_channel short_name channel = Misc.verbose ("Reading headers from virtual account file for " ^ short_name); let name = begin try input_line channel with End_of_file -> Misc.fail ( "Couldn't read first line (account name) from virtual account with short name `" ^ short_name ^ "'") end in (name, Variables.read_variables channel) let load_vaccount_file short_name channel = let name, vars = read_headers_from_channel short_name channel in let acct = Account.create_virtual short_name name vars in let _ = Accountdbase.add_account short_name acct in let rec f () = let context = "whilst reading virtual account file `" ^ short_name ^ "'" in try let line = input_line channel in let name, negate = if String.length line > 0 && String.get line 0 = '-' then (String.sub line 1 ((String.length line) - 1)), true else line, false in let acct = Accountdbase.lookup_account name context in let total = Sumofmoney.default_to_normal (Account.total acct) in let tm = Unix.localtime (Unix.time ()) in let total_str = if negate then "Negated total" else "Total" in let txn = { year = tm.tm_year + 1900; month = tm.tm_mon + 1; day = tm.tm_mday; amount = if not negate then total else Sumofmoney.negate total; creditor = "phantom"; description = total_str ^ " of account " ^ (Account.full_name acct); automatically_added = true; linked = false; do_not_symmetrise = true } in Accountdbase.add_txn short_name txn context; f () with End_of_file -> () in f () let process () = Misc.verbose "Processing virtual accounts."; Misc.iter_files "vaccounts" load_vaccount_file; Misc.verbose "Processing of virtual accounts done." let process_late () = Misc.verbose "Processing late virtual accounts."; Misc.iter_files "vaccounts_late" load_vaccount_file; Misc.verbose "Processing of late virtual accounts done." misery-0.2/src/meets.ml0000644000175000017500000003076211371536776014565 0ustar wookeywookey(* Handling of automatic entries for Club meets. *) open Account let meets_pool_short_name = "meets-pool" let lights_pool_short_name = "lamps-pool" let gear_pool_short_name = "personal-gear-pool" let srt_kit_pool_short_name = "srt-pool" type meet_params = { driver_fee : Sumofmoney.amount; non_driver_fee : Sumofmoney.amount; light_fee : Sumofmoney.amount; gear_fee : Sumofmoney.amount; srt_fee : Sumofmoney.amount; breakfast_fee : Sumofmoney.amount; dinner_fee : Sumofmoney.amount } type meet_state = { driver : bool; fuel : Sumofmoney.amount; food : Sumofmoney.amount; breakfasts : int; dinners : int; light : int; gear : int; srt : int; special_meet_fee : Sumofmoney.amount option } let find_variable context name vars = try List.assoc name vars with Not_found -> Misc.fail ("Variable `" ^ name ^ "' not found " ^ context) let first_word line = if line = "" then raise Not_found else try let index = String.index line ' ' in (String.sub line 0 index, String.sub line (index + 1) ((String.length line) - index - 1)) with Not_found -> (line, "") let first_word_required line msg context = try first_word line with Not_found -> Misc.fail (msg ^ " at this point:\n" ^ line ^ "\n" ^ context) let first_word_required_money line msg context = let word, rest = first_word_required line msg context in (Units.convert word context, rest) let first_word_required_int line msg context = let word, rest = first_word_required line msg context in (int_of_string word, rest) let rec iter_n n f = if n = 0 then () else (f (); iter_n (n - 1) f) let enter_txn short_name name day month year caver params state = let context = "whilst entering transactions for meet " ^ short_name in let fee_desc = " for " ^ name in let meet_fee = { year = year; month = month; day = day; creditor = caver; amount = (match state.special_meet_fee with None -> if state.driver then params.driver_fee else params.non_driver_fee | Some fee -> fee); description = (if state.driver then "Driver's meet fee" ^ fee_desc else "Meet fee" ^ fee_desc); automatically_added = true; do_not_symmetrise = true; linked = false } in let fuel = { year = year; month = month; day = day; creditor = caver; amount = state.fuel; description = "Fuel for " ^ name; automatically_added = true; linked = false; do_not_symmetrise = true } in let food = { year = year; month = month; day = day; creditor = caver; amount = state.food; description = "Food shopping for " ^ name; automatically_added = true; linked = false; do_not_symmetrise = true } in let breakfast = { year = year; month = month; day = day; creditor = meets_pool_short_name; amount = Sumofmoney.negate params.breakfast_fee; description = "One breakfast on " ^ name; automatically_added = true; linked = false; do_not_symmetrise = true } in let dinner = { year = year; month = month; day = day; creditor = meets_pool_short_name; amount = Sumofmoney.negate params.dinner_fee; description = "One dinner on " ^ name; automatically_added = true; linked = false; do_not_symmetrise = true } in let light = { year = year; month = month; day = day; creditor = lights_pool_short_name; amount = Sumofmoney.negate params.light_fee; description = "One day's light hire on " ^ name; automatically_added = true; linked = false; do_not_symmetrise = true } in let gear = { year = year; month = month; day = day; creditor = gear_pool_short_name; amount = Sumofmoney.negate params.gear_fee; description = "One day's personal gear hire on " ^ name; automatically_added = true; linked = false; do_not_symmetrise = true } in let srt = { year = year; month = month; day = day; creditor = srt_kit_pool_short_name; amount = Sumofmoney.negate params.srt_fee; description = "One day's SRT kit hire on " ^ name; automatically_added = true; linked = false; do_not_symmetrise = true } in (* The caver's meet fee, credited to the account for the meet. *) Accountdbase.add_txn short_name meet_fee context; (* The caver's meet fee, debited from the caver's account. *) Accountdbase.add_txn_negated caver { meet_fee with creditor = meets_pool_short_name } context; (* The caver's meet fee, credited to the meets pool. *) Accountdbase.add_txn meets_pool_short_name meet_fee context; (if not (Sumofmoney.is_zero state.fuel) then begin (* Any fuel expenditure by the caver, credited to their account. *) Accountdbase.add_txn caver { fuel with creditor = meets_pool_short_name } context; (* Any fuel expenditure by the caver, debited from the meets pool. *) Accountdbase.add_txn_negated meets_pool_short_name fuel context; (* Any fuel expenditure by the caver, debited from the account for the meet. *) Accountdbase.add_txn_negated short_name fuel context end); (if not (Sumofmoney.is_zero state.food) then begin (* Any food expenditure by the caver, credited to their account. *) Accountdbase.add_txn caver { food with creditor = meets_pool_short_name } context; (* Any food expenditure by the caver, debited from the meets pool. *) Accountdbase.add_txn_negated meets_pool_short_name food context; (* Any food expenditure by the caver, debited from the account for the meet. *) Accountdbase.add_txn_negated short_name food context end); (* Breakfasts. *) iter_n state.breakfasts (fun () -> Accountdbase.add_txn caver breakfast context; Accountdbase.add_txn_negated meets_pool_short_name { breakfast with creditor = caver } context; Accountdbase.add_txn_negated short_name { breakfast with creditor = caver } context); (* Dinners. *) iter_n state.dinners (fun () -> Accountdbase.add_txn caver dinner context; Accountdbase.add_txn_negated meets_pool_short_name { dinner with creditor = caver } context; Accountdbase.add_txn_negated short_name { dinner with creditor = caver } context); (* Lights. *) iter_n state.light (fun () -> Accountdbase.add_txn caver light context; Accountdbase.add_txn_negated lights_pool_short_name { light with creditor = caver } context); (* Personal gear sets. *) iter_n state.gear (fun () -> Accountdbase.add_txn caver gear context; Accountdbase.add_txn_negated gear_pool_short_name { gear with creditor = caver } context); (* SRT kits. *) iter_n state.srt (fun () -> Accountdbase.add_txn caver srt context; Accountdbase.add_txn_negated srt_kit_pool_short_name { srt with creditor = caver } context) let date_rexp = Str.regexp "^\\(20[0-1][0-9]\\)-\\([0-9][0-9]\\)-\\([0-9][0-9]\\)" let parse_date str context = if Str.string_match date_rexp str 0 then (int_of_string (Str.matched_group 1 str), int_of_string (Str.matched_group 2 str), int_of_string (Str.matched_group 3 str)) else Misc.fail ("Bad date " ^ str ^ " (must be YYYY-MM-DD)\n" ^ context) let load_meet_file params short_name channel = Misc.verbose ("Reading meet file: " ^ short_name); let context = "whilst reading meet file: " ^ short_name in let vars = Variables.read_variables channel in let meet_name = find_variable context "name" vars in let existing_acct = Accountdbase.lookup_account_nofail short_name in let meet_acct = if existing_acct = None then Account.create short_name meet_name [("group", "Meets")] else let meets_pool = Accountdbase.lookup_account "meets-pool" "this account must be present for correct operation" in let existing_acct' = match existing_acct with None -> assert false | Some x -> x in let prefix = "" in Accountdbase.update_account "meets-pool" (Account.copy existing_acct' meets_pool prefix); existing_acct' in let _ = Accountdbase.add_account short_name meet_acct in let meet_date = find_variable context "date" vars in let year, month, day = parse_date meet_date context in let rec read_line () = try let line = input_line channel in if line = "" || String.sub line 0 1 = "#" then read_line () else let caver, rest = first_word_required line "Short name expected" context in let rec read_modifiers chars state = try let word, rest = first_word chars in let context = "whilst reading line:\n" ^ line in match word with "driver" -> read_modifiers rest { state with driver = true } | "non-driver" -> read_modifiers rest { state with driver = false } | "special-meet-fee" -> let fee, rest = first_word_required_money rest "Meet fee expected" context in read_modifiers rest { state with special_meet_fee = Some fee } | "fuel" -> let fuel, rest = first_word_required_money rest "Fuel amount expected" context in read_modifiers rest { state with fuel = fuel } | "food" -> let food, rest = first_word_required_money rest "Food amount expected" context in read_modifiers rest { state with food = food } | "breakfast" -> let breakfasts, rest = first_word_required_int rest "Number of breakfasts expected" context in read_modifiers rest { state with breakfasts = breakfasts } | "dinner" -> let dinners, rest = first_word_required_int rest "Number of dinners expected" context in read_modifiers rest { state with dinners = dinners } | "light" -> let light, rest = first_word_required_int rest "Number of days of light use expected" context in read_modifiers rest { state with light = light } | "srt" -> let srt, rest = first_word_required_int rest "Number of days of SRT use expected" context in read_modifiers rest { state with srt = srt } | "gear" -> let gear, rest = first_word_required_int rest "Number of days of gear use expected" context in read_modifiers rest { state with gear = gear } | _ -> Misc.fail ("Don't understand first word of: " ^ chars ^ "\n" ^ context) with Not_found -> begin enter_txn short_name meet_name day month year caver params state; read_line () end in read_modifiers rest { driver = false; fuel = Sumofmoney.zero; food = Sumofmoney.zero; breakfasts = 2; dinners = 1; light = 0; gear = 0; srt = 0; special_meet_fee = None } with End_of_file -> () in read_line () let find_variable_money context name vars = Units.convert (find_variable context name vars) context let process () = Misc.verbose "Reading meets config file."; try let channel = Misc.open_config_file_fail_ok "meets" in let vars = Variables.read_variables channel in let context = "whilst reading meets config file" in let params = { driver_fee = find_variable_money context "driver_fee" vars; non_driver_fee = find_variable_money context "non_driver_fee" vars; light_fee = find_variable_money context "light" vars; gear_fee = find_variable_money context "gear" vars; srt_fee = find_variable_money context "srt" vars; breakfast_fee = find_variable_money context "breakfast" vars; dinner_fee = find_variable_money context "dinner" vars } in close_in channel; Misc.verbose "Processing meets."; Misc.iter_files "meets" (load_meet_file params); Misc.verbose "Processing meets done." with Not_found -> Misc.verbose "No meets config file found; skipping." misery-0.2/src/vaccounts.mli0000644000175000017500000000007310754123012015571 0ustar wookeywookeyval process : unit -> unit val process_late : unit -> unit misery-0.2/src/htmlout.ml0000644000175000017500000000143210754123012015107 0ustar wookeywookey(* Utility functions for HTML output. *) let create_page filename title = let root = Misc.get_root_directory () in let path = root ^ "/html/" ^ filename ^ ".html" in try let channel = open_out path in output_string channel ""; output_string channel title; output_string channel ""; output_string channel ""; output_string channel "\n"; output_string channel "

"; output_string channel title; output_string channel "

"; channel with Sys_error _ -> Misc.fail ("Could not create file " ^ path) let finish_page channel = output_string channel ""; close_out channel misery-0.2/src/sumofmoney.mli0000644000175000017500000000173610754123012015774 0ustar wookeywookeytype amount type default_units_amount val create : int (* default units *) -> string -> amount val to_string : amount -> string val is_negative : amount -> bool val zero_default : default_units_amount val zero : amount val abs_default : default_units_amount -> default_units_amount val add : amount -> default_units_amount -> default_units_amount val add_default : default_units_amount -> default_units_amount -> default_units_amount val negate : amount -> amount val negate_default : default_units_amount -> default_units_amount val subtract_default : default_units_amount -> default_units_amount -> default_units_amount val scale_default : float -> float -> default_units_amount -> default_units_amount val to_string_default : default_units_amount -> string val non_negative_default : default_units_amount -> bool val very_negative_default : default_units_amount -> bool val default_to_normal : default_units_amount -> amount val is_zero : amount -> bool misery-0.2/src/groupsummary.mli0000644000175000017500000000003110754123012016330 0ustar wookeywookeyval emit : unit -> unit misery-0.2/src/splits.ml0000644000175000017500000001152710754123012014737 0ustar wookeywookey(* Splitting of accounts' intermediate balances across other accounts. *) open Account open Unix let rexp = Str.regexp "^\\([-a-zA-Z0-9_]+\\)\\(,[0-9.]*\\)?$" (* Format num/denom to a string, avoiding printing decimal places when necessary. *) let format_fraction num denom = let n = int_of_float num in let d = int_of_float denom in if (float_of_int n) = num && (float_of_int d) = denom then Printf.sprintf "%d/%d" n d else Printf.sprintf "%.02f/%.02f" num denom (* Process splits for a given account. *) let load_split_file short_name channel = Misc.verbose ("Reading split file: " ^ short_name); let context = "whilst processing splits for " ^ short_name in let acct = Accountdbase.lookup_account short_name context in let full_name = Account.full_name acct in let total_str = Sumofmoney.to_string_default (Account.total acct) in let rec f acc = try let line = input_line channel in if String.length line > 0 then begin if Str.string_match rexp line 0 then let name = Str.matched_group 1 line in let factor = try let x = Str.matched_group 2 line in float_of_string (String.sub x 1 (String.length x - 1)) with Not_found -> 1.0 in let factor_str = Printf.sprintf "%.2f" factor in let debug = " ...debit " ^ name ^ ", factor " ^ factor_str in Misc.verbose debug; f ((name, factor) :: acc) else Misc.fail ("Bad line whilst loading splits file `" ^ short_name ^ "':\n " ^ line ^ "\n\nThe format is either:\n" ^ "short-name\n\nor:\nshort-name,factor\n\nwhere:\n" ^ "short-name identifies an account to be in the split;\n" ^ "factor is an integer or decimal number that specifies\n" ^ " what proportion to assign to this account.\n" ^ " If omitted, the factor is one.") end else acc with End_of_file -> acc in let debtors = f [] in let total = Account.total acct in let total_added = ref Sumofmoney.zero_default in let factor_total = List.fold_left (fun acc -> fun (_, factor) -> factor +. acc) 0.0 debtors in let tm = Unix.localtime (Unix.time ()) in List.iter (fun (debtor_name, factor) -> let amount = Sumofmoney.scale_default factor factor_total total in let _ = total_added := Sumofmoney.add_default !total_added amount in let fraction = format_fraction factor factor_total in let desc = Printf.sprintf "%s of the total from %s (%s)" fraction short_name full_name total_str in let txn_d = { year = tm.tm_year + 1900; month = tm.tm_mon + 1; day = tm.tm_mday; creditor = short_name; amount = Sumofmoney.default_to_normal amount; description = desc; linked = false; automatically_added = true; do_not_symmetrise = true } in let txn_c = { txn_d with creditor = debtor_name; amount = Sumofmoney.default_to_normal ( Sumofmoney.negate_default amount) } in let context = "whilst splitting " ^ full_name ^ ".\n\n" ^ "The splits/" ^ short_name ^ " file is probably at fault" in Accountdbase.add_txn debtor_name txn_d context; Accountdbase.add_txn short_name txn_c context) debtors; if (Sumofmoney.abs_default !total_added) < (Sumofmoney.abs_default total) then let unlucky = match debtors with (x, _)::_ -> x | _ -> Misc.fail "No entries made for '" ^ short_name ^ "' split" in let extra = Sumofmoney.subtract_default total !total_added in let txn = { year = tm.tm_year + 1900; month = tm.tm_mon + 1; day = tm.tm_mday; creditor = unlucky; amount = Sumofmoney.default_to_normal (Sumofmoney.negate_default extra); description = "Unlucky: arithmetic error from " ^ short_name ^ " split"; linked = false; automatically_added = true; do_not_symmetrise = true } in let txn' = { txn with creditor = short_name; amount = Sumofmoney.default_to_normal extra } in begin Accountdbase.add_txn short_name txn context; Accountdbase.add_txn unlucky txn' context end let process () = Misc.verbose "Processing splits."; Misc.iter_files "splits" load_split_file; Misc.verbose "Processing splits done." misery-0.2/src/vaccount.ml0000644000175000017500000000041010754123012015230 0ustar wookeywookey(* Handling of virtual accounts. *) type vaccount_op = Add | Subtract type vaccount_entry = Account of string | Group of string type vaccount = { name : string; group : string; entries : (vaccount_op * vaccount_entry) list } misery-0.2/src/account.ml0000644000175000017500000002064711371543631015071 0ustar wookeywookey(* Handling of a single account. *) type txn = { year : int; month : int; day : int; creditor : string; amount : Sumofmoney.amount; description : string; automatically_added : bool; do_not_symmetrise : bool; linked : bool } type account = { name : string; short_name : string; variables : (string * string) list; transactions : txn list; income : Sumofmoney.default_units_amount; expenditure : Sumofmoney.default_units_amount; virtual_acct : bool } let short_name acct = acct.short_name let create short_name name variables = { name = name; short_name = short_name; variables = variables; transactions = []; income = Sumofmoney.zero_default; expenditure = Sumofmoney.zero_default; virtual_acct = false } let create_virtual short_name name variables = { name = name; short_name = short_name; variables = variables; transactions = []; income = Sumofmoney.zero_default; expenditure = Sumofmoney.zero_default; virtual_acct = true } let is_virtual acct = acct.virtual_acct (* Read headers from an input channel attached to a file in the initial/ directory. The file pointer should be at the start of the file. *) let read_headers_from_channel short_name channel = Misc.verbose ("Reading headers from initial account file for " ^ short_name); let name = begin try input_line channel with End_of_file -> Misc.fail ( "Couldn't read first line (account name) from account with short name `" ^ short_name ^ "'") end in (name, Variables.read_variables channel) (* Create a transaction list from an input channel representing one of the files in the initial/ directory. The headers must have been read already. Returns a list of transactions, the income, and the expenditure (the latter two in default units). *) let read_transactions_from_channel account_name channel = Misc.verbose ("Reading transactions from initial account file for " ^ account_name); let rexp = Str.regexp "^\\(20[0-1][0-9]\\)-\\([0-9][0-9]\\)-\\([0-9][0-9]\\),\\([!=a-zA-Z0-9-]+\\),\\(-?[a-zA-Z]?[0-9]+\\.[0-9][0-9]\\),\\(.*\\)" in let blank = Str.regexp "^ *$" in let rec f acc income expenditure = try let csv = input_line channel in if String.length csv = 0 || String.sub csv 0 1 = "#" || Str.string_match blank csv 0 then f acc income expenditure else if Str.string_match rexp csv 0 = true then let year = int_of_string (Str.matched_group 1 csv) in let month = int_of_string (Str.matched_group 2 csv) in let day = int_of_string (Str.matched_group 3 csv) in let do_not_symmetrise, linked, creditor = let x = Str.matched_group 4 csv in if String.get x 0 = '!' then (true, false, String.sub x 1 ((String.length x) - 1)) else (if String.get x 0 = '=' then (false, true, String.sub x 1 ((String.length x) - 1)) else (false, false, x)) in let description = Str.matched_group 6 csv in let context = "whilst reading transactions for " ^ account_name in let amount = Units.convert (Str.matched_group 5 csv) context in let new_expenditure = if Sumofmoney.is_negative amount then Sumofmoney.add (Sumofmoney.negate amount) expenditure else expenditure in let new_income = if Sumofmoney.is_negative amount then income else Sumofmoney.add amount income in let debug = Printf.sprintf "Adding transaction: %04d-%02d-%02d creditor=%s amount=%s new_income=%s new_expenditure=%s (%s)" year month day creditor (Sumofmoney.to_string amount) (Sumofmoney.to_string_default new_income) (Sumofmoney.to_string_default new_expenditure) description in Misc.verbose debug; f ({ year = year; month = month; day = day; creditor = creditor; amount = amount; description = description; automatically_added = false; do_not_symmetrise = do_not_symmetrise; linked = linked } :: acc) new_income new_expenditure else Misc.fail ( "Malformed line while reading initial/ file for account `" ^ account_name ^ "':\n" ^ csv ^ "\n\nThe format is:\nYYYY-MM-DD,short-name,amount,description\n" ^ "where:\n YYYY is the year; MM is the month; DD is the day;\n" ^ " short-name is the short name of the creditor or debtor;\n" ^ " amount is the sum involved (e.g. 23.40, E3.40, -E7.60);\n" ^ " description is a text string giving the description.") with End_of_file -> (acc, income, expenditure) in f [] Sumofmoney.zero_default Sumofmoney.zero_default (* Create an account structure from an input channel representing one of the files in the initial/ directory. The file pointer should be at the start of the file. *) let create_account_from_channel short_name channel = Misc.verbose ("Creating account from initial account file " ^ short_name); let (name, variables) = read_headers_from_channel short_name channel in let (txns, income, expenditure) = read_transactions_from_channel name channel in { name = name; short_name = short_name; variables = variables; transactions = txns; income = income; expenditure = expenditure; virtual_acct = false } (* Return the full name of an account. *) let full_name acct = acct.name (* Lookup a variable in an account. *) (* FIXME ought to be case-insensitive *) let lookup_variable var acct = List.assoc var acct.variables let lookup_boolean_variable var acct = let str = List.assoc var acct.variables in match String.lowercase str with "true" -> true | "false" -> false | _ -> Misc.fail ("Bad value for boolean variable `" ^ var ^ "' in account `" ^ acct.name ^ "'") let lookup_integer_variable var acct = int_of_string (List.assoc var acct.variables) (* Return an account's balance in default units. *) let total acct = Sumofmoney.subtract_default acct.income acct.expenditure let income acct = acct.income let expenditure acct = acct.expenditure (* Add a transaction to an account. *) let add_txn txn acct = { acct with transactions = txn :: acct.transactions; income = (if Sumofmoney.is_negative txn.amount then acct.income else Sumofmoney.add txn.amount acct.income); expenditure = (if Sumofmoney.is_negative txn.amount then Sumofmoney.add (Sumofmoney.negate txn.amount) acct.expenditure else acct.expenditure)} (* Add a transaction to an account, but negating the amount. *) let add_txn_negated txn acct = let neg_amount = Sumofmoney.negate txn.amount in { acct with transactions = { txn with amount = neg_amount } :: acct.transactions; income = (if Sumofmoney.is_negative neg_amount then acct.income else Sumofmoney.add neg_amount acct.income); expenditure = (if Sumofmoney.is_negative neg_amount then Sumofmoney.add (Sumofmoney.negate neg_amount) acct.expenditure else acct.expenditure)} (* Map over the transactions of an account. *) let map_txns f acct = { acct with transactions = List.map f acct.transactions } (* Fold over the transactions of an account. *) let fold_txns f init acct = List.fold_left f init acct.transactions let iter_txns f acct = List.iter f acct.transactions let txn_compare t1 t2 = if t1.year > t2.year then 1 else if t1.year < t2.year then -1 else if t1.month > t2.month then 1 else if t1.month < t2.month then -1 else if t1.day > t2.day then 1 else if t1.day < t2.day then -1 else compare t1.creditor t2.creditor let iter_txns_sorted f acct = List.iter f (List.sort txn_compare acct.transactions) let number_of_txns acct = List.length acct.transactions (* Copy all transactions from src to dest. The copied transactions are marked as automatically added and "do not symmetrise". The string "prefix" is added to each description. *) let copy src dest prefix = fold_txns (fun cur_acc -> fun txn -> add_txn ({txn with automatically_added = true; do_not_symmetrise = true; description = prefix ^ txn.description}) cur_acc) dest src misery-0.2/src/units.ml0000644000175000017500000001026510754123012014561 0ustar wookeywookeymodule UnitsMap = Map.Make (struct type t = string let compare = compare end) let units_map = ref UnitsMap.empty let default_unit = ref "" let amount_regexp = Str.regexp "^\\(-\\)?\\([a-zA-Z]\\)?\\([0-9]+\\)\\.\\([0-9][0-9]\\)$" let initialize () = Misc.verbose "Loading units configuration file."; let channel = Misc.open_config_file "units" in let rexp = Str.regexp "^\\([a-zA-Z]\\),\\([ a-zA-Z0-9]+\\),\\([0-9]+\\.[0-9]*\\)\\(,.\\)?$" in try begin default_unit := input_line channel; Misc.verbose ("The default units are: " ^ !default_unit); while true do let line = input_line channel in if Str.string_match rexp line 0 then let unit_char = Str.matched_group 1 line in let name = Str.matched_group 2 line in let factor = Str.matched_group 3 line in let factor2 = begin try let in_terms_of = String.sub (Str.matched_group 4 line) 1 1 in begin try fst (UnitsMap.find in_terms_of !units_map) with Not_found -> Misc.fail ("Unknown units `" ^ in_terms_of ^ "' used in line: " ^ line ^ " in units.csv") end with Not_found -> 1.0 end in let final_factor = (float_of_string factor) *. factor2 in begin Misc.verbose ("Unit char `" ^ unit_char ^ "' is called `" ^ name ^ "' and is worth " ^ (string_of_float final_factor) ^ " of a default unit."); units_map := UnitsMap.add unit_char (final_factor, name) !units_map end else Misc.fail ("Malformed units definition: `" ^ line ^ "'\n" ^ "Check the config/units file. The format of each " ^ "line is either:\nunit-char,name,factor\n\nor:\n" ^ "unit-char,name,factor,in-terms-of\n\nwhere:\n" ^ " unit-char is a single character identifying the unit" ^ " being defined\n" ^ " name is a textual name for the unit\n" ^ " factor is a decimal number saying how many default\n" ^ " units there are in the new unit being defined\n" ^ " in-terms-of is a single unit character (say X) defined earlier\n" ^ " in the file. If this is specified, factor specifies\n" ^ " how many X units there are in the new unit being defined.") done end with Sys_error s -> Misc.fail ("Error whilst reading units file: " ^ s) | End_of_file -> if !default_unit = "" then Misc.fail "End-of-file whilst reading default unit" else close_in channel let convert amount context = if Str.string_match amount_regexp amount 0 then let negative = begin try let _ = Str.matched_group 1 amount in true with Not_found -> false end in let (factor, name), default = begin try let units_str = Str.matched_group 2 amount in begin try (UnitsMap.find units_str !units_map), false with Not_found -> let names = UnitsMap.fold (fun chr -> fun (_, str) -> fun acc -> acc ^ "\n " ^ chr ^ " (" ^ str ^ ")") !units_map "" in Misc.fail ("Unknown units `" ^ units_str ^ "' used in `" ^ amount ^ "'\n" ^ context ^ ".\n\nKnown units are:" ^ names ^ "\n\nIf you want to use `" ^ units_str ^ "' units, edit the " ^ "config/units file.") end with Not_found -> (1.0, ""), true end in let sum_1 = int_of_string (Str.matched_group 3 amount) in let sum_2 = int_of_string (Str.matched_group 4 amount) in let sum = (sum_1*100 + sum_2) * (if negative then -1 else 1) in let str = (Misc.format_money sum) ^ " " ^ name in if factor = 1.0 then Sumofmoney.create sum (if default then "" else str) else let amount = truncate (factor *. (float sum)) in Sumofmoney.create amount (if default then "" else str) else Misc.fail ("Malformed sum of money `" ^ amount ^ "'\n" ^ context ^ ".\n" ^ "Examples of well-formed sums of money:\n " ^ "3.40 E2.34 -E34.63 (where E is a unit character).") misery-0.2/src/sumofmoney.ml0000644000175000017500000000166010754123012015617 0ustar wookeywookeytype amount = int * string type default_units_amount = int let create sum str = (sum, str) let to_string (amount, str) = if str <> "" then (Misc.format_money amount) ^ " (" ^ str ^ ")" else Misc.format_money amount let zero = (0, "0.00") let zero_default = 0 let is_negative (sum, _) = sum < 0 let add (sum, str) sum' = sum + sum' let negate (sum, str) = if str = "" then (-sum, str) else begin if sum < 0 then (-sum, String.sub str 1 (String.length str - 1)) else (-sum, "-" ^ str) end let to_string_default = Misc.format_money let non_negative_default sum = (not (sum <= -1)) let very_negative_default sum = sum < -5000 let negate_default sum = -sum let scale_default numerator denominator sum = truncate (numerator /. denominator *. (float sum)) let abs_default a = abs a let add_default a b = a + b let subtract_default a b = a - b let default_to_normal sum = (sum, "") let is_zero (sum, _) = (sum = 0) misery-0.2/src/meets.mli0000644000175000017500000000003310754123012014675 0ustar wookeywookeyval process : unit -> unit misery-0.2/src/misc.ml0000644000175000017500000000267510754123012014360 0ustar wookeywookey(* Miscellaneous functions. *) let flag_verbose = ref false let get_root_directory () = try Sys.getenv "ACCOUNTS_ROOT" with Not_found -> Sys.getcwd () let fail msg = prerr_endline msg; exit 1 let open_config_file leafname = let path = (get_root_directory ()) ^ "/config/" ^ leafname in try open_in path with Sys_error msg -> fail ("Error whilst opening config file " ^ path ^ ":\n " ^ msg) let open_config_file_fail_ok leafname = let path = (get_root_directory ()) ^ "/config/" ^ leafname in try open_in path with Sys_error msg -> raise Not_found let round amount = (float_of_int (truncate (amount *. 100.0))) /. 100.0 let format_money sum = (* Note: don't use just "sum / 100" to get the minus sign, because when sum is zero, any minus won't appear. *) let s = Printf.sprintf "%d.%02d" (abs (sum / 100)) (abs (sum mod 100)) in if sum < 0 then "-" ^ s else s let verbose str = if !flag_verbose then print_endline str else () let iter_files dir f = let path = (get_root_directory ()) ^ "/" ^ dir in let dir = Unix.opendir path in try while true do let name = Unix.readdir dir in if String.get name 0 <> '.' then begin verbose ("iter_initial_files: name=" ^ name); let channel = open_in (path ^ "/" ^ name) in (f name channel; close_in channel) end else () done with End_of_file -> Unix.closedir dir misery-0.2/src/variables.mli0000644000175000017500000000007210754123012015533 0ustar wookeywookeyval read_variables : in_channel -> (string * string) list misery-0.2/src/splits.mli0000644000175000017500000000003410754123012015077 0ustar wookeywookeyval process : unit -> unit misery-0.2/src/variables.ml0000644000175000017500000000110010754123012015353 0ustar wookeywookeylet header_rexp = Str.regexp "^\\([a-zA-Z_-]*\\)=\\(.*\\)$" let read_variables channel = let rec f acc = try let pos = pos_in channel in let line = input_line channel in if Str.string_match header_rexp line 0 then begin let variable = Str.matched_group 1 line in let value = Str.matched_group 2 line in Misc.verbose ("...read binding " ^ variable ^ "=" ^ value); f ((variable, value) :: acc) end else (seek_in channel pos; acc) with End_of_file -> acc in f [] misery-0.2/src/accountdbase.mli0000644000175000017500000000140710754123012016221 0ustar wookeywookey(* Handling of all individual accounts within the current set of accounts. *) open Account module AccountsMap : Map.S with type key = string val add_account : string -> account -> unit val update_account : string -> account -> unit val lookup_account : string -> string -> account val lookup_account_nofail : string -> (account option) val add_txn : string -> txn -> string -> unit val add_txn_negated : string -> txn -> string -> unit val add_global_txn : txn -> unit val filter_and_fold_accounts : (account -> bool) -> ('a -> account -> 'a) -> 'a -> 'a val sum_integer_variable : string -> int val load_initial_files : unit -> unit val lookup_creditor : string -> string -> string val iter : (account -> unit) -> unit val group : string -> account list AccountsMap.t misery-0.2/src/account.mli0000644000175000017500000000253710754123012015227 0ustar wookeywookey(* Handling of a single account. *) type account type txn = { year : int; month : int; day : int; creditor : string; amount : Sumofmoney.amount; description : string; automatically_added : bool; do_not_symmetrise : bool; linked : bool } val create : string -> string -> (string * string) list -> account val create_virtual : string -> string -> (string * string) list -> account val copy : account -> account -> string -> account val short_name : account -> string val full_name : account -> string val is_virtual : account -> bool val lookup_variable : string -> account -> string val lookup_integer_variable : string -> account -> int val lookup_boolean_variable : string -> account -> bool val add_txn : txn -> account -> account val add_txn_negated : txn -> account -> account val iter_txns : (txn -> unit) -> account -> unit val iter_txns_sorted : (txn -> unit) -> account -> unit val map_txns : (txn -> txn) -> account -> account val fold_txns : ('a -> txn -> 'a) -> 'a -> account -> 'a val total : account -> Sumofmoney.default_units_amount val create_account_from_channel : string -> in_channel -> account val number_of_txns : account -> int val income : account -> Sumofmoney.default_units_amount val expenditure : account -> Sumofmoney.default_units_amount misery-0.2/src/printaccount.ml0000644000175000017500000001620610754123012016131 0ustar wookeywookey(* Dumping of account statements to HTML files. *) open Account open Unix let fixme_rexp = Str.regexp "^.*FIXME.*$" let next_checkpoint = ref (-1, -1, -1) let txn_total = ref 0 let find_next_checkpoint which_day (year, month, day) = if which_day = -1 then (year, month, day) else begin if month = 12 then (year + 1, 1, which_day) else (year, month + 1, which_day) end let date_on_or_after (y, m, d) (y', m', d') = y > y' || (y == y' && (m > m' || (m == m' && d >= d'))) (* Emit HTML for a single transaction. *) let rec print_transaction channel acct context cur_total checkpoint_day txn = incr txn_total; let css_class = if Str.string_match fixme_rexp txn.description 0 then "fixme" else "entry" in let _ = if !next_checkpoint = (-1, -1, -1) then next_checkpoint := find_next_checkpoint checkpoint_day (txn.year, txn.month, txn.day) in let real_name = Accountdbase.lookup_creditor txn.creditor context in let date_str = Printf.sprintf "%d-%02d-%02d" txn.year txn.month txn.day in if not (Sumofmoney.is_zero txn.amount) then begin let amount_str = Sumofmoney.to_string txn.amount in while (checkpoint_day != -1 && date_on_or_after (txn.year, txn.month, txn.day) !next_checkpoint) do let (cp_year, cp_month, cp_day) = !next_checkpoint in let date_str = Printf.sprintf "%d-%02d-%02d" cp_year cp_month cp_day in output_string channel ""; output_string channel ""; output_string channel date_str; output_string channel ""; output_string channel ""; output_string channel "Account balance at this point: "; output_string channel ""; output_string channel (Sumofmoney.to_string_default !cur_total); output_string channel ""; output_string channel ""; next_checkpoint := find_next_checkpoint checkpoint_day !next_checkpoint done; cur_total := Sumofmoney.add txn.amount !cur_total; output_string channel ""; output_string channel ""; output_string channel date_str; output_string channel ""; (if not (Account.is_virtual acct) then begin output_string channel ""; (if txn.creditor <> "phantom" then (output_string channel "")); output_string channel real_name; (if txn.creditor <> "phantom" then output_string channel ""); (if (not txn.automatically_added) && (not txn.do_not_symmetrise) then output_string channel " "); (if txn.automatically_added && txn.do_not_symmetrise then output_string channel " "); (if (not txn.automatically_added) && txn.do_not_symmetrise then output_string channel " "); output_string channel "" end); output_string channel ""; output_string channel amount_str; output_string channel ""; output_string channel txn.description; output_string channel "" end (* Dump an HTML account statement to an output channel. *) let dump acct = next_checkpoint := (-1, -1, -1); let short_name = Account.short_name acct in let tm = Unix.localtime (Unix.time ()) in let now = Printf.sprintf "%02d:%02d on %d-%02d-%02d" tm.tm_hour tm.tm_min (tm.tm_year + 1900) (tm.tm_mon+1) tm.tm_mday in try let page_name = "bill-" ^ short_name in let full_name = Account.full_name acct in let html_channel = Htmlout.create_page page_name full_name in let cur_total = ref Sumofmoney.zero_default in let checkpoint_day = begin try Account.lookup_integer_variable "checkpoint_before_day" acct with Not_found -> -1 end in let context = "whilst generating HTML page for " ^ full_name in output_string html_channel "

Account short name: "; output_string html_channel short_name; (* output_string html_channel "
Group: "; output_string html_channel group;*) output_string html_channel "
Number of transactions: "; output_string html_channel (string_of_int (Account.number_of_txns acct)); (if not (Account.is_virtual acct) then begin output_string html_channel "

Negative amounts indicate that "; output_string html_channel full_name; output_string html_channel " is indebted to a creditor." end); output_string html_channel "

\n"; output_string html_channel "\n"; output_string html_channel ""; (if not (Account.is_virtual acct) then output_string html_channel ""); output_string html_channel ""; output_string html_channel ""; output_string html_channel ""; Account.iter_txns_sorted (print_transaction html_channel acct context cur_total checkpoint_day) acct; let total = Account.total acct in let total_str = Sumofmoney.to_string_default total in let income_str = Sumofmoney.to_string_default (Account.income acct) in let expenditure_str = Sumofmoney.to_string_default (Account.expenditure acct) in output_string html_channel ""; (if (Sumofmoney.non_negative_default total) then output_string html_channel "" else output_string html_channel ""); output_string html_channel ""; output_string html_channel "
Date (YYYY-MM-DD)Creditor/DebtorAmountDescription
"; output_string html_channel "Income:"; output_string html_channel income_str; output_string html_channel "
"; output_string html_channel "Expenditure:"; output_string html_channel expenditure_str; output_string html_channel "
Grand total:"; output_string html_channel total_str; output_string html_channel "
\n"; output_string html_channel "

Go back to summary sheet."; output_string html_channel "

Processed at "; output_string html_channel now; output_string html_channel "."; Htmlout.finish_page html_channel with Sys_error msg -> Misc.fail ("Error dumping HTML for " ^ short_name ^ ":\n " ^ msg) (* Create HTML balance sheets for all accounts. *) let create_balance_sheets () = Accountdbase.iter dump; Printf.printf "Emitted %d transactions.\n" !txn_total misery-0.2/src/accountdbase.ml0000644000175000017500000001161310754123012016050 0ustar wookeywookey(* Handling of all individual accounts within the current set of accounts. *) open Account module AccountsMap = Map.Make (struct type t = string let compare = compare end) (* Map from short names to account records. *) let dbase = ref AccountsMap.empty (* Add an account to the database. *) let add_account short_name acct = dbase := AccountsMap.add short_name acct !dbase (* Update an account in the database. *) let update_account = add_account (* Look up an account by short name. *) let lookup_account short_name context = try AccountsMap.find short_name !dbase with Not_found -> let names = AccountsMap.fold (fun short_name -> fun _ -> fun names -> names ^ "\n " ^ short_name) !dbase "" in Misc.fail ("Lookup of account `" ^ short_name ^ "' failed,\n" ^ context ^ ".\n\nThe following short names are known:" ^ names ^ "\n\nIf the short name `" ^ short_name ^ "' is correct, add " ^ "a new account.") (* Look up an account by short name, no-error version. *) let lookup_account_nofail short_name = try Some (AccountsMap.find short_name !dbase) with Not_found -> None (* Add a transaction to an account identified by a given short name. *) let add_txn name txn context = let acct = Account.add_txn txn (lookup_account name context) in update_account name acct (* Add a negated transaction to an account identified by a given short name. *) let add_txn_negated name txn context = let acct = Account.add_txn_negated txn (lookup_account name context) in update_account name acct (* Add a transaction to all accounts. *) let add_global_txn txn = dbase := AccountsMap.map (Account.add_txn txn) !dbase (* Given a predicate, a folding function and an initial value, apply a fold across all accounts satisfying the predicate. *) let filter_and_fold_accounts pred fold_fn init = AccountsMap.fold (fun short_name -> fun acct -> fun current_value -> if pred acct then fold_fn current_value acct else current_value) !dbase init (* Given a variable name, find those accounts defining it and return the total of their corresponding values, which must be integers. *) let sum_integer_variable var = Misc.verbose ("Summing integer variable `" ^ var ^ "' over all accounts."); AccountsMap.fold (fun _ -> fun acct -> fun current_total -> try current_total + (Account.lookup_integer_variable var acct) with Not_found -> current_total) !dbase 0 (* Load an initial account state from a channel. *) let load_initial_file short_name in_channel = add_account short_name (create_account_from_channel short_name in_channel) (* Load initial account states from the files in the initial/ directory and symmetrise transactions. *) let load_initial_files () = Misc.iter_files "initial" load_initial_file; AccountsMap.iter ( fun short_name -> fun acct -> Account.iter_txns ( fun txn -> if not txn.do_not_symmetrise then let context = "whilst adding other halves of transactions for " ^ short_name in let acct' = lookup_account txn.creditor context in let txn' = { txn with do_not_symmetrise = true; automatically_added = true; linked = false; creditor = short_name } in let f = if txn.linked then Account.add_txn else Account.add_txn_negated in update_account txn.creditor (f txn' acct') ) acct) !dbase (* Lookup a creditor's or debtor's name. *) let lookup_creditor name context = if name = "phantom" then "Phantom" else Account.full_name (lookup_account name context) (* Iterate over all accounts. *) let iter f = AccountsMap.iter (fun _ -> fun acct -> f acct) !dbase (* Group accounts according to a variable's value. *) let group var = Misc.verbose ("Grouping accounts according to value of var: " ^ var); AccountsMap.fold (fun _ -> fun acct -> fun current_groups -> try let value = Account.lookup_variable var acct in Misc.verbose (" ...adding " ^ (Account.short_name acct) ^ " (key " ^ value ^ ")"); let current_members = (try AccountsMap.find value current_groups with Not_found -> []) in AccountsMap.add value (acct::current_members) current_groups with Not_found -> current_groups ) !dbase AccountsMap.empty misery-0.2/Makefile0000644000175000017500000000577510754123012013750 0ustar wookeywookeyall: bin/misery .SUFFIXES: .ml .cmx .mli .cmi SOURCES=src/misc.cmx \ src/sumofmoney.cmx \ src/units.cmx \ src/htmlout.cmx \ src/variables.cmx \ src/account.cmx \ src/accountdbase.cmx \ src/printaccount.cmx \ src/splits.cmx \ src/meets.cmx \ src/groupsummary.cmx \ src/vaccounts.cmx \ src/main.cmx bin/misery: $(SOURCES) ocamlopt -I src -o $@ unix.cmxa str.cmxa $(SOURCES) clean: rm -f src/*.cm{i,x} bin/misery .mli.cmi: ocamlc -c -I src -o $@ $< .ml.cmx: ocamlopt -c -I src -o $@ $< src/account.cmo: src/variables.cmi src/units.cmi src/sumofmoney.cmi \ src/misc.cmi src/account.cmi src/account.cmx: src/variables.cmx src/units.cmx src/sumofmoney.cmx \ src/misc.cmx src/account.cmi src/account.cmi: src/sumofmoney.cmi src/accountdbase.cmo: src/misc.cmi src/account.cmi src/accountdbase.cmi src/accountdbase.cmx: src/misc.cmx src/account.cmx src/accountdbase.cmi src/accountdbase.cmi: src/account.cmi src/groupsummary.cmo: src/sumofmoney.cmi src/misc.cmi src/htmlout.cmx \ src/accountdbase.cmi src/account.cmi src/groupsummary.cmi src/groupsummary.cmx: src/sumofmoney.cmx src/misc.cmx src/htmlout.cmx \ src/accountdbase.cmx src/account.cmx src/groupsummary.cmi src/htmlout.cmo: src/misc.cmi src/htmlout.cmx: src/misc.cmx src/main.cmo: src/vaccounts.cmi src/units.cmi src/splits.cmi \ src/printaccount.cmi src/meets.cmi src/groupsummary.cmi \ src/accountdbase.cmi src/main.cmx: src/vaccounts.cmx src/units.cmx src/splits.cmx \ src/printaccount.cmx src/meets.cmx src/groupsummary.cmx \ src/accountdbase.cmx src/meets.cmo: src/variables.cmi src/units.cmi src/sumofmoney.cmi \ src/misc.cmi src/accountdbase.cmi src/account.cmi src/meets.cmi src/meets.cmx: src/variables.cmx src/units.cmx src/sumofmoney.cmx \ src/misc.cmx src/accountdbase.cmx src/account.cmx src/meets.cmi src/misc.cmo: src/misc.cmi src/misc.cmx: src/misc.cmi src/printaccount.cmo: src/sumofmoney.cmi src/misc.cmi src/htmlout.cmx \ src/accountdbase.cmi src/account.cmi src/printaccount.cmi src/printaccount.cmx: src/sumofmoney.cmx src/misc.cmx src/htmlout.cmx \ src/accountdbase.cmx src/account.cmx src/printaccount.cmi src/splits.cmo: src/sumofmoney.cmi src/misc.cmi src/accountdbase.cmi \ src/account.cmi src/splits.cmi src/splits.cmx: src/sumofmoney.cmx src/misc.cmx src/accountdbase.cmx \ src/account.cmx src/splits.cmi src/sumofmoney.cmo: src/misc.cmi src/sumofmoney.cmi src/sumofmoney.cmx: src/misc.cmx src/sumofmoney.cmi src/units.cmo: src/sumofmoney.cmi src/misc.cmi src/units.cmi src/units.cmx: src/sumofmoney.cmx src/misc.cmx src/units.cmi src/units.cmi: src/sumofmoney.cmi src/vaccounts.cmo: src/variables.cmi src/sumofmoney.cmi src/misc.cmi \ src/accountdbase.cmi src/account.cmi src/vaccounts.cmi src/vaccounts.cmx: src/variables.cmx src/sumofmoney.cmx src/misc.cmx \ src/accountdbase.cmx src/account.cmx src/vaccounts.cmi src/variables.cmo: src/misc.cmi src/variables.cmi src/variables.cmx: src/misc.cmx src/variables.cmi misery-0.2/examples/0000755000175000017500000000000011721037752014122 5ustar wookeywookeymisery-0.2/examples/splits/0002755000175000017500000000000011721040560015431 5ustar wookeywookeymisery-0.2/examples/splits/accommodation0000644000175000017500000000002711721040514020165 0ustar wookeywookeywookey rob mike edward misery-0.2/examples/splits/dinner-bubs0000644000175000017500000000002711721040521017556 0ustar wookeywookeywookey edward mike rob misery-0.2/examples/splits/transport0000644000175000017500000000002711721040557017413 0ustar wookeywookeywookey rob mike edward misery-0.2/examples/splits/lunch-sun0000644000175000017500000000002011721040532017255 0ustar wookeywookeymike edward rob misery-0.2/examples/splits/shopping0000644000175000017500000000002711721040546017204 0ustar wookeywookeywookey rob mike edward misery-0.2/examples/splits/lunch-sat0000644000175000017500000000002211721040526017244 0ustar wookeywookeywookey edward rob misery-0.2/examples/splits/skibeers0000644000175000017500000000002011721040552017152 0ustar wookeywookeymike rob edward misery-0.2/examples/splits/lunch-tue0000644000175000017500000000002711721040540017253 0ustar wookeywookeywookey edward mike rob misery-0.2/examples/html/0002755000175000017500000000000011721040671015062 5ustar wookeywookeymisery-0.2/examples/html/smiley.png0000644000175000017500000000050111716427265017100 0ustar wookeywookey‰PNG  IHDRk ÖIIDAT8Í”;ŽÂ0†?#Жв¢Ê!¶¢âTž¤Ï¨V!$n[-)(ÁPXÎø‘  “x&óå÷øaD„wÕz›ðQvSÂ|™¤YróÄËi ü‚¬lbž{¹ÖŽUñuhƒû£}f 3(—î;·Ä61µ¾ZkcA½±: ëÌw4M7ΠûæZ‹G|]à0Ñþ©×À'ÕíGN*y.ä?,ŠÇ¾r+^rRY¨‹¸©AülßË–xdÍ ù¶@ÌH—8Ýö`PÓƒ³ÊeäпO*7—šùtÒ–@@ò°¾ÜÙbHÏN‰a¾šNq#äÝ-À†*FóµIEND®B`‚misery-0.2/examples/html/exclamation.png0000600000175000017500000000142411530536503020065 0ustar wookeywookey‰PNG  IHDRóÿagAMA±Ž|ûQ“ cHRMz%€ƒùÿ€èu0ê`:—o—©™ÔŸIDATxœb`Àþÿg’ýÿŸÝ—< Í ¬ÿ©ÿ ÿ#"}æãS @Xч· =_?‹ Ðýÿæ%ÓÆŠ\j+zõd;'Ë‚]‘˜èöŸ¡•›Z€Â@ßÞ04ÿÿÎüÿÞm¶ÿ›Ö3‚ zõ„芲vlê}z ±ã?3ƒÕÿ(°+¢Â|þcS@LÈœwª8ÙX€¬¿@ÌÍÀÈ Íðþ H 3CKÅ! ]^…n@¡ðóC+ 7õ‹ÁÕ–h„(ƒ”Ä7 ŸŸAÑðƒŸ×­FtnÀûk ÅŒ @î¿`¾­+ƒ°¯ðo†?_€"¬ íÅ{Îk(F6 €àèÅi†_?sýÿÿšõÿ`X¹@ò¿§µíÿ}Û…ÿÿúÄôÿÿ?°¸·{À7d}FŸ/3ä¾9Çüÿÿ[¾ÿÿïñÃÿÏרþ¯$ýÿÿ/ ÿ ó“íÿåíÀÀ¬Í‚é ˆí‡þÿ¹Ô Œ®ÿ7Šß0ü~™áÿ‘í@ö[ þ Ä?øŸ˜öóø Ó @ ŸN3d¿9tâSžÿÿ¯Ýâ @[€qÏ`ûÿÐ*™ÿÿ?Å~€ ªûËúÿÜ:¡ÿ L¹ ýÄôí3C7+0à~ e‚+#ƒ‚$ƒ½~ƒŽš$àØC ~ T÷ꃡå;K“ÇM ˆ…ù?Ûï_ä~X/7Ãý}ïþ e`d¦ŠûÀˆ%£7@wqÓ;ÐÌ_lB…†RÏàh2áËÖNŽ??À©(ÊÎú‡™é/Ã÷Ÿl@·3B=üŸ›ã/ãów|Ü7ŸˆÜÿþ®× À`%¼ò‡QÿIEND®B`‚misery-0.2/examples/html/padlock.png0000644000175000017500000000076611425106066017216 0ustar wookeywookey‰PNG  IHDR ™Áü½IDAT•²MþöÿðÿùóÿÃÓ²D]u'o&”6PÿÌÞÿúûõÿ÷ÿÿóàÄÌŽYlðää#,/ñ÷õÿùþçûöж¨2,ùµ©®ü]lk·¬ŽÉ¾I`X¤›ûÿÿðñP"b%ÿéôÿôöÿööÿìøW'G$ÿðóã»À ýôô긹î ððð) ÷úôÒÕd'üþæîìÿ  õÓÝ#Õèâ$5÷ öüþüþùÿúûþ# ðõ"øðñý÷ü—¸« | /ðáã öý ÿú÷øïöôõý  ýÿ6ÿ ûÿÿüñûïöéÚÚçÒÙýÿÿý2RHñ ""$467êõóêýøáÿ÷3($J=éû×åà ÿ;]Sýîç§’™ÉŸ­ þîýúóñò ïúöDn`Tngþÿôíó֯ư¸ÅÐÎþÿVHO-@:QBÃs؛ŠIEND®B`‚misery-0.2/examples/html/accounts.css0000644000175000017500000000112211721040667017412 0ustar wookeywookeybody { font-family: sans-serif; margin-left: 5%; margin-right: 5% } td.grandtotalp { background-color: #00ff00 } td.grandtotalm { background-color: #ff8800 } td.grandtotaln { background-color: #ff0000 } td.groupsummary { background-color: #cccccc } tr.incomeexp { background-color: #dddddd } tr.checkpoint { background-color: #aaaaaa } tr.fixme { background-color: #ffdd00 } tr.grandtotalp { background-color: #00ff00 } tr.grandtotaln { background-color: #ff0000 } tr.actual { background-color: #cccccc } tr.notional { background-color: #eeeeee } tr.header { background-color: #6666ff } misery-0.2/examples/initial/0002755000175000017500000000000011721040507015545 5ustar wookeywookeymisery-0.2/examples/initial/smokes0000644000175000017500000000016611721040471016772 0ustar wookeywookeyCiggies bought in resort 2012-02-11,rob,7.00,Ciggies bought in resort 2012-02-11,edward,7.00,Ciggies bought in resort misery-0.2/examples/initial/lifts0000644000175000017500000000045511720747350016624 0ustar wookeywookeySki Lift Tickets group=Expenses must_be_zero=true 2012-02-10,wookey,179.00,3-day Lift Ticket 2012-02-10,rob,179.00,3-day Lift Ticket 2012-02-10,mike,179.00,3-day Lift Ticket 2012-02-10,edward,179.00,3-day Lift Ticket 2012-02-10,edward,66.00,1-day Lift Ticket 2012-02-10,wookey,66.00,1-day Lift Ticket misery-0.2/examples/initial/edward0000644000175000017500000000070611721040432016734 0ustar wookeywookeyEdward Hervey group=skiers 2012-02-10,shopping,47.51,Shopping at Trader Joe's 2012-02-11,lunch-sat,40.81,Lunch on Saturday 2012-02-12,lunch-sun,35.62,Lunch on Sunday 2012-02-10,rob,89.01,Booze at Trader Joe's (Sponsored by Rob) 2012-02-11,shopping,51.55,Shopping in resort 2012-02-11,smokes,14.00,Ciggies bought in resort 2012-02-11,edward,3.40,Lipgoo bought in resort 2012-02-14,lunch-tue,64.00,Mexican on way back 2012-02-14,transport,5.00,Bridge Toll misery-0.2/examples/initial/mike0000644000175000017500000000054411721016774016427 0ustar wookeywookeyMike Turquette group=skiers 2012-02-13,shopping,42.00,Shopping in resort 2012-02-12,rob,139.49,Rob's Bindings (bought by Mike) 2012-02-12,dinner-bubs,110.12,Dinner at Bub's Pub 2012-02-14,transport,67.65,Fuel 2012-02-14,accommodation,20.00,Late-checkout fee 2012-02-13,skibeers,21.00,beers at Snowshoe Johnsons 2012-02-14,transport,25.83,Fuel (final fill) misery-0.2/examples/initial/accommodation0000644000175000017500000000005611720747747020327 0ustar wookeywookeyAccomodation group=Expenses must_be_zero=true misery-0.2/examples/initial/dinner-bubs0000644000175000017500000000032311721016371017676 0ustar wookeywookeyFood at Bub's pub group=Meals and drinks must_be_zero=true 2012-02-14,wookey,15.50,Pizza+beer 2012-02-14,rob,21.50,Pizza+beerx2 2012-02-14,mike,21.00,Burger+beer FIXME 2012-02-14,edward,28.00,Steak+beerx2 FIXME misery-0.2/examples/initial/rob0000644000175000017500000000002711720747373016265 0ustar wookeywookeyRob Clark group=skiers misery-0.2/examples/initial/transport0000644000175000017500000000005311721040476017525 0ustar wookeywookeyTransport group=Expenses must_be_zero=true misery-0.2/examples/initial/lunch-sun0000644000175000017500000000023111720752474017412 0ustar wookeywookeyFood for lunch, sunday group=Meals and drinks must_be_zero=true 2012-02-12,mike,13.95,Dog+beer 2012-02-12,rob,6.00,beer 2012-02-12,edward,13.95,Dog+Beer misery-0.2/examples/initial/shopping0000644000175000017500000000005211721040461017311 0ustar wookeywookeyShopping group=Expenses must_be_zero=true misery-0.2/examples/initial/lunch-sat0000644000175000017500000000024411720752461017374 0ustar wookeywookeyFood for lunch, saturday group=Meals and drinks must_be_zero=true 2012-02-14,wookey,9.95,Burger 2012-02-14,rob,10.50,pasta+beer 2012-02-14,edward,17.95,Burger+Beer misery-0.2/examples/initial/skibeers0000644000175000017500000000007411720747770017315 0ustar wookeywookeyBeers at Snowshoes group=Meals and drinks must_be_zero=true misery-0.2/examples/initial/wookey0000644000175000017500000000030411721040504016775 0ustar wookeywookeyWookey group=skiers 2012-02-03,accommodation,1745.34,Accomodation #2012-02-03,accommodation,P1112.54,Accomodation 2012-02-10,lifts,716.00,Ski Lift Tickets 2012-02-13,lifts,132.00,Ski Lift Tickets misery-0.2/examples/initial/lunch-tue0000644000175000017500000000032211720752510017372 0ustar wookeywookeyFood at Mexican on way home group=Meals and drinks must_be_zero=true 2012-02-14,wookey,14.00,Quesedilla+beer 2012-02-14,rob,14.00,Food+beer 2012-02-14,mike,11.00,Food+Drpepper 2012-02-14,edward,14.00,Food+Beer misery-0.2/examples/vaccounts_late/0002755000175000017500000000000011721022442017124 5ustar wookeywookeymisery-0.2/examples/config/0002755000175000017500000000000011721040410015352 5ustar wookeywookeymisery-0.2/examples/config/units0000644000175000017500000000000411721026073016440 0ustar wookeywookeyUSD misery-0.2/examples/config/title0000644000175000017500000000002111721040406016412 0ustar wookeywookeyKirkwood Skitrip misery-0.2/examples/vaccounts/0002755000175000017500000000000011721040577016130 5ustar wookeywookeymisery-0.2/examples/vaccounts/pools0000644000175000017500000000013211721040575017177 0ustar wookeywookeySum of the expenses group=Totals must_be_zero=true accommodation shopping lifts transport misery-0.2/examples/vaccounts/debtorsandcreditors0000644000175000017500000000011411721040567022110 0ustar wookeywookeyDebtors and creditors group=Totals must_be_zero=true wookey edward mike rob