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