GnuCash import difficulty and interim solution
Jonathan Kimmitt
jonathan at kimmitt.uk
Sat Jan 6 15:36:45 GMT 2024
(*
Dear kMyMoney developers,
I came across KMyMoney while looking for a replacement for the recently deceased QuickBooks Desktop.
Having gone to the trouble of converting my accounts from Quickbooks to GnuCash, I was a little disappointed
that the “open Gnucash XML file” feature didn’t seem to work. I offer you a crude standalone converter which someone
might be able to work up into a general purpose importer. Comments welcome. At the moment I don’t have non UK
currencies to work with. The extrapolation should be a simple exercise.
Regards,
Jonathan
*)
(* License: GNU General Public Licence Version 2 *)
(* AUTHOR: Dr Jonathan Kimmitt *)
(* can be compiled with "ocamlfind ocamlopt -linkpkg -package unix,xml-light -g translate.ml -o mymoney" *)
open Xml
let unm = ref None
let cnvtim (tm:Unix.tm) = Printf.sprintf "%.2d-%.2d-%.2d" (tm.tm_year+1900) (tm.tm_mon+1) tm.tm_mday
let cnv t = Scanf.sscanf t "%d-%d-%d %d:%d:%d" (fun yr mn dy hr min sec ->
let x={Unix.tm_year=yr-1900;tm_mon=mn-1;tm_mday=dy;tm_hour=hr;tm_min=mn;tm_sec=sec;tm_wday=0;tm_yday=0;tm_isdst=false} in
let _, tm = Unix.mktime x in cnvtim tm)
type item = {
mutable tim_posted: string;
mutable tim_entered: string;
mutable num: string;
mutable desc: string;
mutable tran_id: string;
mutable split_id: string;
mutable recon: string;
mutable recon_date: string;
mutable value: string;
mutable quantity: string;
mutable split_guid: string;
mutable action: string;
mutable memo: string;
}
let empty_item () =
{tim_posted=""; tim_entered=""; num=""; desc=""; tran_id=""; split_id=""; recon=""; recon_date=""; value=""; quantity=""; split_guid=""; action=""; memo="" }
let copy_item {tim_posted; tim_entered; num; desc; tran_id; split_id; recon; recon_date; value; quantity; split_guid; action; memo } =
{tim_posted; tim_entered; num; desc; tran_id; split_id; recon; recon_date; value; quantity; split_guid; action; memo }
let scan1 value = Scanf.sscanf value "%f/%f" (fun a b -> Printf.sprintf "%.2f" (a /. b))
let cnts = ref []
let acch = Hashtbl.create 255
let payeeh = Hashtbl.create 255
let trancnt = ref 0
let mymoney acclst tranlst payees = Element ("KMYMONEY-FILE", [],
[Element ("FILEINFO", [],
[Element ("CREATION_DATE", [("date", "2024-01-05")], []);
Element ("LAST_MODIFIED_DATE", [("date", "2024-01-05")], []);
Element ("VERSION", [("id", "1")], []);
Element ("FIXVERSION", [("id", "5")], [])]);
Element ("USER", [("name", ""); ("email", "")],
[Element ("ADDRESS",
[("zipcode", ""); ("county", ""); ("telephone", ""); ("street", "");
("city", "")],
[])]);
Element ("INSTITUTIONS", [("count", "0")], []);
Element ("PAYEES", [("count", string_of_int (List.length payees))], payees);
Element ("COSTCENTERS", [("count", "0")], []);
Element ("TAGS", [("count", "1")],
[Element ("TAG",
[("id", "G000001"); ("name", "tags"); ("closed", "0");
("tagcolor", "#000000")],
[])]);
Element ("ACCOUNTS", [("count", string_of_int (List.length acclst))], acclst);
Element ("TRANSACTIONS", [("count", string_of_int (List.length tranlst))], tranlst);
Element ("KEYVALUEPAIRS", [],
[Element ("PAIR", [("key", "kmm-baseCurrency"); ("value", "GBP")], []);
Element
("PAIR",
[("key", "kmm-id");
("value", "{93976d56-6861-4c85-aa47-b6c7b4e20175}")],
[])]);
Element ("SCHEDULES", [("count", "0")], []);
Element ("SECURITIES", [("count", "0")], []);
Element ("CURRENCIES", [("count", "1")],
[Element ("CURRENCY",
[("type", "3"); ("id", "GBP"); ("name", "British Pound");
("scf", "100"); ("saf", "100"); ("rounding-method", "7");
("pp", "4"); ("symbol", "£")],
[])]);
Element ("PRICES", [("count", "0")], []);
Element ("REPORTS", [("count", "0")], []);
Element ("BUDGETS", [("count", "0")], []);
Element ("ONLINEJOBS", [("count", "0")], [])])
let payee name id = Element ("PAYEE",
[("id", id); ("name", name); ("reference", "");
("matchignorecase", "1"); ("matchingenabled", "1"); ("email", "");
("matchkey", "^$"); ("usingmatchkey", "0")],
[Element ("ADDRESS",
[("state", ""); ("telephone", ""); ("street", ""); ("city", "");
("postcode", "")],
[])])
let find_payee name =
if Hashtbl.mem payeeh name then
Hashtbl.find payeeh name
else
let id = Printf.sprintf "P%.6d" (1 + Hashtbl.length payeeh) in
let _ = Hashtbl.add payeeh name id in id
let _ = List.iter(function
| Element ("ACCOUNT",
([("type", acctype); ("description", _); ("id", id);
("name", accname); ("lastreconciled", _); ("opened", _);
("institution", _); ("lastmodified", _); ("parentaccount", "");
("number", _); ("currency", "GBP")]), _) ->
Hashtbl.add acch id (accname,acctype,[],ref [], id)
| _ -> failwith "acc''")
[Element
("ACCOUNT",
[("type", "9"); ("description", ""); ("id", "AStd::Asset");
("name", "Asset"); ("lastreconciled", ""); ("opened", "");
("institution", ""); ("lastmodified", ""); ("parentaccount", "");
("number", ""); ("currency", "GBP")],
[]);
Element
("ACCOUNT",
[("type", "10"); ("description", ""); ("id", "AStd::Liability");
("name", "Liability"); ("lastreconciled", ""); ("opened", "");
("institution", ""); ("lastmodified", ""); ("parentaccount", "");
("number", ""); ("currency", "GBP")],
[]);
Element
("ACCOUNT",
[("type", "12"); ("description", ""); ("id", "AStd::Income");
("name", "Income"); ("lastreconciled", ""); ("opened", "");
("institution", ""); ("lastmodified", ""); ("parentaccount", "");
("number", ""); ("currency", "GBP")],
[]);
Element
("ACCOUNT",
[("type", "13"); ("description", ""); ("id", "AStd::Expense");
("name", "Expense"); ("lastreconciled", ""); ("opened", "");
("institution", ""); ("lastmodified", ""); ("parentaccount", "");
("number", ""); ("currency", "GBP")],
[]);
Element
("ACCOUNT",
[("type", "16"); ("description", ""); ("id", "AStd::Equity");
("name", "Equity"); ("lastreconciled", ""); ("opened", "");
("institution", ""); ("lastmodified", ""); ("parentaccount", "");
("number", ""); ("currency", "GBP")],
[])]
let dbgacc = ref None
let othcnv = ref None
let cnvacc = function
| "BANK" -> "AStd::Asset"
| "EXPENSE" -> "AStd::Expense"
| "INCOME" -> "AStd::Asset"
| "ASSET" -> "AStd::Asset"
| "EQUITY" -> "AStd::Equity"
| "LIABILITY" -> "AStd::Liability"
| "RECEIVABLE" -> "AStd::Asset"
| "PAYABLE" -> "AStd::Liability"
| oth -> othcnv := Some oth; failwith "othcnv"
let rec root_typ typ' guid =
match Hashtbl.find acch guid with
| ("Root Account", "ROOT", [], _, _) ->
(try let std = cnvacc typ' in let pth,typ,x,refcnt,p = Hashtbl.find acch std in typ with _ -> "1")
| (nam', typ',
[Element
("act:parent", [("type", "guid")],
[PCData parent_guid])], _, _) -> let ptyp = root_typ typ' parent_guid in
ptyp
| oth -> dbgacc := Some oth; failwith "root_typ"
let validate maxlen nam =
let nam' = ref (String.concat " and " (String.split_on_char '&' nam)) in
String.iter (fun ch ->
nam' := String.concat "" (String.split_on_char ch !nam');
) "!@£$%^&*()_+=-{}[]|\"':;/?<>\"~`,.";
String.sub !nam' 0 (min (String.length !nam') maxlen)
let rec traverse tranlst = function
| Element ("gnc:account", [("version", "2.0.0")], lst) ->
let accname = ref "" in
let acctype = ref "" in
let attr = ref [] in
let guid = ref "" in
List.iter (function
| Element ("act:name", [], [PCData accname']) -> accname := accname'
| Element ("act:id", [("type", "guid")], [PCData guid']) -> guid := guid'
| Element ("act:type", [], [PCData acctyp']) -> acctype := acctyp'
| Element ("act:commodity", [],
[Element ("cmdty:space", [], [PCData "CURRENCY"]);
Element ("cmdty:id", [], [PCData "GBP"])]) -> ()
| Element ("act:commodity-scu", [], [PCData "100"]) -> ()
| attr' -> attr := attr' :: []) lst;
Hashtbl.add acch !guid (!accname,!acctype,!attr,ref [], Printf.sprintf "A%.6d" (1 + Hashtbl.length acch))
| Element ("gnc:transaction", [("version", "2.0.0")], trnlst) ->
let item = empty_item () in
incr trancnt;
List.iter (function
| Element ("trn:id", [("type", "guid")], [PCData tran_id]) -> item.tran_id <- tran_id
| Element ("trn:currency", [],
[Element ("cmdty:space", [], [PCData "CURRENCY"]);
Element ("cmdty:id", [], [PCData "GBP"])]) -> ()
| Element ("trn:num", [], [PCData num]) -> item.num <- num
| Element ("trn:date-posted", [], [Element ("ts:date", [], [PCData date_posted])]) -> item.tim_posted <- cnv date_posted
| Element ("trn:date-entered", [], [Element ("ts:date", [], [PCData date_ent])]) -> item.tim_entered <- cnv date_ent
| Element ("trn:description", [], []) -> ()
| Element ("trn:description", [], [PCData desc]) -> item.desc <- desc
| Element ("trn:slots", [], slots) -> List.iter (function Element ("trn:slot", [], lst) ->
List.iter (function
| Element ("slot:key", [("type", "guid")], [PCData split_id]) -> item.split_id <- split_id
| Element (kw, xmlns, lst) -> failwith kw
| PCData txt -> failwith txt) lst;
| oth -> unm := Some oth) slots
| Element ("trn:splits", [], splits) -> let split_lst = ref [] in List.iteri (fun ix -> function Element ("trn:split", [], lst) ->
List.iter (function
| Element ("split:id", [("type", "guid")], [PCData split_id]) -> item.split_id <- split_id
| Element ("split:reconciled-state", [], [PCData recon]) -> item.recon <- recon
| Element ("split:reconcile-date", [], [Element ("ts:date", [], [PCData recon_date])]) -> item.recon_date <- recon_date
| Element ("split:value", [], [PCData value]) -> item.value <- scan1 value
| Element ("split:quantity", [], [PCData quantity]) -> item.quantity <- scan1 quantity
| Element ("split:account", [("type", "guid")], [PCData split_guid]) -> item.split_guid <- split_guid
| Element ("split:action", [], [PCData action]) -> item.action <- action
| Element ("split:memo", [], [PCData memo]) -> item.memo <- memo
| Element (kw, xmlns, lst) -> failwith kw
| PCData txt -> failwith txt) lst;
let pth,typ,_,_,id = Hashtbl.find acch item.split_guid in
split_lst := Element ("SPLIT",
[("id", Printf.sprintf "S%.4d" (ix+1)); ("reconcileflag", "0"); ("memo", validate 64 item.memo);
("bankid", ""); ("shares", item.quantity); ("value", item.value);
("price", "1/1"); ("payee", find_payee item.desc);
("account", id); ("reconciledate", ""); ("action", "");
("number", "")], []) :: !split_lst;
| oth -> unm := Some oth) splits;
tranlst := (Element ("TRANSACTION",
[("id", Printf.sprintf "T0000000000%.8d" !trancnt);
("commodity", "GBP"); ("memo", "item.desc");
("entrydate", item.tim_entered);
("postdate", item.tim_posted)],
[Element ("SPLITS", [], List.rev !split_lst)])) :: !tranlst
| oth -> unm := Some oth) trnlst
| Element ("gnc:commodity", [("version", "2.0.0")], lst) -> ()
| Element ("gnc:count-data", [("cd:type", ("book"|"commodity"|"account"|"transaction" as ty))], [PCData cnt]) ->
cnts := (ty, cnt) :: !cnts;
| Element ("book:id", [("type", "guid")], [PCData book_id]) -> ()
| Element ("slot:key", [], [PCData key]) -> ()
| Element ("slot:value", [("type", "string")], [PCData slot_value]) -> ()
| Element ("gnc:transaction" as kw, _, _) as trn -> unm := Some trn; failwith kw
| Element (kw, xmlns, lst) as x -> unm := Some x; List.iter (traverse tranlst) lst; unm := None
| PCData txt -> failwith txt
let timcmp ((tim,_), _) ((tim',_), _) = if tim=tim' then 0 else int_of_float(ceil(tim -. tim'))
let cnt' typ = match List.assoc_opt typ !cnts with Some n -> int_of_string n | None -> 0
let _ =
let tranlst = ref [] in
let xml = parse_file Sys.argv.(1) in
let _ = traverse tranlst xml in
let tranlst = List.rev !tranlst in
let accs = cnt' "account" in assert(accs+5=Hashtbl.length acch);
let trans = cnt' "transaction" in assert(!trancnt == trans);
let acclst = ref [] in
let rec typ_helper typ' guid = (let pth,typ,x,refcnt,p = Hashtbl.find acch guid in
if typ = "ROOT" then Hashtbl.find acch (cnvacc typ') else pth,typ,x,refcnt,p) in
Hashtbl.iter (fun guid (nam',typ',attr,refcnt',id') -> match attr with
| [Element ("act:parent", [("type", "guid")], [PCData parent_guid])] ->
let pth,typ,_,refcnt,_ = typ_helper typ' parent_guid in
refcnt := Element ("SUBACCOUNT", [("id", id')], []) :: !refcnt
| [] -> ()
| oth -> failwith "parent") acch;
Hashtbl.iter (fun guid (nam',typ',attr,refcnt',id') -> match attr with
| [Element ("act:parent", [("type", "guid")], [PCData parent_guid])] ->
let pth,typ,_,_,pid = typ_helper typ' parent_guid in
acclst := Element ("ACCOUNT",
[("type", root_typ typ parent_guid); ("description", ""); ("id", id');
("name", validate 24 nam'); ("lastreconciled", "");
("opened", "2024-01-05"); ("institution", ""); ("lastmodified", "");
("parentaccount", pid); ("number", ""); ("currency", "GBP")],
[Element ("SUBACCOUNTS", [], !refcnt')]) :: !acclst;
| [] ->
acclst := Element ("ACCOUNT",
[("type", typ'); ("description", ""); ("id", id');
("name", validate 24 nam'); ("lastreconciled", "");
("opened", "2024-01-05"); ("institution", ""); ("lastmodified", "");
("parentaccount", ""); ("number", ""); ("currency", "GBP")],
[Element ("SUBACCOUNTS", [], !refcnt')]) :: !acclst;
| oth -> failwith "parent") acch;
let payees = ref [] in
Hashtbl.iter (fun k x -> payees := payee (validate 64 k) x :: !payees) payeeh;
let fd = open_out Sys.argv.(2) in
let _ = output_string fd ("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE KMYMONEY-FILE>\n"^to_string_fmt (mymoney !acclst tranlst !payees)) in
close_out fd;
!acclst, tranlst
More information about the KMyMoney-devel
mailing list