X-Git-Url: https://git.dogcows.com/gitweb?p=chaz%2Fhomebank;a=blobdiff_plain;f=src%2Fhb-import-qif.c;fp=src%2Fhb-import-qif.c;h=98eb0b3d6c18b6fe1e37b16b0d023ae5f121e700;hp=0000000000000000000000000000000000000000;hb=996fa4ab9f6b836001f8ad0eecbfd3821687fea7;hpb=27f6e3b112df235c8e9afc9911b1f6bce208a001 diff --git a/src/hb-import-qif.c b/src/hb-import-qif.c new file mode 100644 index 0000000..98eb0b3 --- /dev/null +++ b/src/hb-import-qif.c @@ -0,0 +1,817 @@ +/* HomeBank -- Free, easy, personal accounting for everyone. + * Copyright (C) 1995-2016 Maxime DOYEN + * + * This file is part of HomeBank. + * + * HomeBank is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * HomeBank is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "homebank.h" + +//#include "ui-assist-import.h" +#include "hb-import.h" + +/****************************************************************************/ +/* Debug macros */ +/****************************************************************************/ +#define MYDEBUG 0 + +#if MYDEBUG +#define DB(x) (x); +#else +#define DB(x); +#endif + +/* our global datas */ +extern struct HomeBank *GLOBALS; +extern struct Preferences *PREFS; + + +/* = = = = = = = = = = = = = = = = */ +static QIF_Tran * +da_qif_tran_malloc(void) +{ + return g_malloc0(sizeof(QIF_Tran)); +} + + +static void +da_qif_tran_free(QIF_Tran *item) +{ +gint i; + + if(item != NULL) + { + if(item->date != NULL) + g_free(item->date); + if(item->info != NULL) + g_free(item->info); + if(item->payee != NULL) + g_free(item->payee); + if(item->memo != NULL) + g_free(item->memo); + if(item->category != NULL) + g_free(item->category); + if(item->account != NULL) + g_free(item->account); + + for(i=0;isplits[i]; + + if(s->memo != NULL) + g_free(s->memo); + if(s->category != NULL) + g_free(s->category); + } + + g_free(item); + } +} + + +static void +da_qif_tran_destroy(QifContext *ctx) +{ +GList *qiflist = g_list_first(ctx->q_tra); + + while (qiflist != NULL) + { + QIF_Tran *item = qiflist->data; + da_qif_tran_free(item); + qiflist = g_list_next(qiflist); + } + g_list_free(ctx->q_tra); + ctx->q_tra = NULL; +} + + +static void +da_qif_tran_new(QifContext *ctx) +{ + ctx->q_tra = NULL; +} + + +static void +da_qif_tran_move(QIF_Tran *sitem, QIF_Tran *ditem) +{ + if(sitem != NULL && ditem != NULL) + { + memcpy(ditem, sitem, sizeof(QIF_Tran)); + memset(sitem, 0, sizeof(QIF_Tran)); + } +} + + +static void +da_qif_tran_append(QifContext *ctx, QIF_Tran *item) +{ + ctx->q_tra = g_list_append(ctx->q_tra, item); +} + + +/* = = = = = = = = = = = = = = = = */ + +gdouble +hb_qif_parser_get_amount(gchar *string) +{ +gdouble amount; +gint l, i; +gchar *new_str, *p; +gint ndcount = 0; +gchar dc; + + //DB( g_print("\n[qif] hb_qif_parser_get_amount\n") ); + + + amount = 0.0; + dc = '?'; + + l = strlen(string) - 1; + + // the first non-digit is a grouping, or a decimal separator + // if the non-digit is after a 3 digit serie, it might be a grouping + + for(i=l;i>=0;i--) + { + //DB( g_print(" %d :: %c :: ds='%c' ndcount=%d\n", i, string[i], dc, ndcount) ); + + if( string[i] == '-' || string[i] == '+' ) continue; + + if( g_ascii_isdigit( string[i] )) + { + ndcount++; + } + else + { + if( (ndcount != 3) && (string[i] == '.' || string[i]==',') ) + { + dc = string[i]; + } + ndcount = 0; + } + } + + //DB( g_print(" s='%s' :: ds='%c'\n", string, dc) ); + + + new_str = g_malloc (l+3); //#1214077 + p = new_str; + for(i=0;i<=l;i++) + { + if( g_ascii_isdigit( string[i] ) || string[i] == '-' ) + { + *p++ = string[i]; + } + else + if( string[i] == dc ) + *p++ = '.'; + } + *p++ = '\0'; + amount = g_ascii_strtod(new_str, NULL); + + //DB( g_print(" -> amount was='%s' => to='%s' double='%f'\n", string, new_str, amount) ); + + g_free(new_str); + + return amount; +} + +/* O if m-d-y (american) + 1 if d-m-y (european) */ +/* obsolete 4.5 +static gint +hb_qif_parser_guess_datefmt(QifContext *ctx) +{ +gboolean retval = TRUE; +GList *qiflist; +gboolean r, valid; +gint d, m, y; + + DB( g_print("(qif) get_datetype\n") ); + + qiflist = g_list_first(ctx->q_tra); + while (qiflist != NULL) + { + QIF_Tran *item = qiflist->data; + + r = hb_qif_parser_get_dmy(item->date, &d, &m, &y); + valid = g_date_valid_dmy(d, m, y); + + DB( g_print(" -> date: %s :: %d %d %d :: %d\n", item->date, d, m, y, valid ) ); + + if(valid == FALSE) + { + retval = FALSE; + break; + } + + qiflist = g_list_next(qiflist); + } + + return retval; +} +*/ + +static Transaction * +account_qif_get_child_transfer(Transaction *src, GList *list) +{ +Transaction *item; + + DB( g_print("([qif] get_child_transfer\n") ); + + //DB( g_print(" search: %d %s %f %d=>%d\n", src->date, src->wording, src->amount, src->account, src->kxferacc) ); + + list = g_list_first(list); + while (list != NULL) + { + item = list->data; + if( item->paymode == PAYMODE_INTXFER) + { + if( src->date == item->date && + src->kacc == item->kxferacc && + src->kxferacc == item->kacc && + ABS(src->amount) == ABS(item->amount) ) + { + //DB( g_print(" found : %d %s %f %d=>%d\n", item->date, item->wording, item->amount, item->account, item->kxferacc) ); + + return item; + } + } + list = g_list_next(list); + } + + //DB( g_print(" not found...\n") ); + + return NULL; +} + + +static gint +hb_qif_parser_get_block_type(gchar *qif_line) +{ +gchar **typestr; +gint type = QIF_NONE; + + DB( g_print("--------\n[qif] block type\n") ); + + //DB( g_print(" -> str: %s type: %d\n", qif_line, type) ); + + + if(g_str_has_prefix(qif_line, "!Account") || g_str_has_prefix(qif_line, "!account")) + { + type = QIF_ACCOUNT; + } + else + { + typestr = g_strsplit(qif_line, ":", 2); + + if( g_strv_length(typestr) == 2 ) + { + gchar *qif_line = g_utf8_casefold(typestr[1], -1); + + //DB( g_print(" -> str[1]: %s\n", typestr[1]) ); + + if( g_str_has_prefix(qif_line, "bank") ) + { + type = QIF_TRANSACTION; + } + else + if( g_str_has_prefix(qif_line, "cash") ) + { + type = QIF_TRANSACTION; + } + else + if( g_str_has_prefix(qif_line, "ccard") ) + { + type = QIF_TRANSACTION; + } + else + if( g_str_has_prefix(qif_line, "invst") ) + { + type = QIF_TRANSACTION; + } + else + if( g_str_has_prefix(qif_line, "oth a") ) + { + type = QIF_TRANSACTION; + } + else + if( g_str_has_prefix(qif_line, "oth l") ) + { + type = QIF_TRANSACTION; + } + else + if( g_str_has_prefix(qif_line, "security") ) + { + type = QIF_SECURITY; + } + else + if( g_str_has_prefix(qif_line, "prices") ) + { + type = QIF_PRICES; + } + + g_free(qif_line); + } + g_strfreev(typestr); + } + + //DB( g_print(" -> return type: %d\n", type) ); + + + return type; +} + +static void +hb_qif_parser_parse(QifContext *ctx, gchar *filename, const gchar *encoding) +{ +GIOChannel *io; +QIF_Tran tran = { 0 }; + + DB( g_print("\n[qif] hb_qif_parser_parse\n") ); + + io = g_io_channel_new_file(filename, "r", NULL); + if(io != NULL) + { + gchar *qif_line; + GError *err = NULL; + gint io_stat; + gint type = QIF_NONE; + gchar *value = NULL; + gchar *cur_acc; + + DB( g_print(" -> encoding should be %s\n", encoding) ); + if( encoding != NULL ) + { + g_io_channel_set_encoding(io, encoding, NULL); + } + + DB( g_print(" -> encoding is %s\n", g_io_channel_get_encoding(io)) ); + + cur_acc = g_strdup(QIF_UNKNOW_ACCOUNT_NAME); + + for(;;) + { + io_stat = g_io_channel_read_line(io, &qif_line, NULL, NULL, &err); + + if( io_stat == G_IO_STATUS_EOF ) + break; + if( io_stat == G_IO_STATUS_ERROR ) + { + DB (g_print(" + ERROR %s\n",err->message)); + break; + } + if( io_stat == G_IO_STATUS_NORMAL ) + { + hb_string_strip_crlf(qif_line); + + //DB (g_print("** new QIF line: '%s' **\n", qif_line)); + + //start qif parsing + if(g_str_has_prefix(qif_line, "!")) /* !Type: or !Option: or !Account otherwise ignore */ + { + type = hb_qif_parser_get_block_type(qif_line); + DB ( g_print("-> ---- QIF block: '%s' (type = %d) ----\n", qif_line, type) ); + } + + value = &qif_line[1]; + + if( type == QIF_ACCOUNT ) + { + switch(qif_line[0]) + { + case 'N': // Name + { + g_free(cur_acc); + g_strstrip(value); + cur_acc = g_strdup(value); + DB ( g_print(" name: '%s'\n", value) ); + break; + } + + case 'T': // Type of account + { + + DB ( g_print(" type: '%s'\n", value) ); + break; + } + + case 'L': // Credit limit (only for credit card accounts) + if(g_str_has_prefix(qif_line, "L")) + { + + DB ( g_print(" credit limit: '%s'\n", value) ); + break; + } + + case '$': // Statement balance amount + { + + DB ( g_print(" balance: '%s'\n", value) ); + break; + } + + case '^': // end + { + DB ( g_print("should create account '%s' here\n", cur_acc) ); + + DB ( g_print(" ----------------\n") ); + break; + } + } + } + + if( type == QIF_TRANSACTION ) + { + switch(qif_line[0]) + { + case 'D': //date + { + gchar *ptr; + + // US Quicken seems to be using the ' to indicate post-2000 two-digit years + //(such as 01/01'00 for Jan 1 2000) + ptr = g_strrstr (value, "\'"); + if(ptr != NULL) { *ptr = '/'; } + + ptr = g_strrstr (value, " "); + if(ptr != NULL) { *ptr = '0'; } + + g_free(tran.date); + tran.date = g_strdup(value); + break; + } + + case 'T': // amount + { + tran.amount = hb_qif_parser_get_amount(value); + break; + } + + case 'C': // cleared status + { + tran.reconciled = FALSE; + if(g_str_has_prefix(value, "X") || g_str_has_prefix(value, "R") ) + { + tran.reconciled = TRUE; + } + tran.cleared = FALSE; + if(g_str_has_prefix(value, "*") || g_str_has_prefix(value, "c") ) + { + tran.cleared = TRUE; + } + break; + } + + case 'N': // check num or reference number + { + if(*value != '\0') + { + g_free(tran.info); + g_strstrip(value); + tran.info = g_strdup(value); + } + break; + } + + case 'P': // payee + { + if(*value != '\0') + { + g_free(tran.payee); + g_strstrip(value); + tran.payee = g_strdup(value); + } + break; + } + + case 'M': // memo + { + if(*value != '\0') + { + g_free(tran.memo); + tran.memo = g_strdup(value); + } + break; + } + + case 'L': // category + { + // LCategory of transaction + // L[Transfer account name] + // LCategory of transaction/Class of transaction + // L[Transfer account]/Class of transaction + // this is managed at insertion + if(*value != '\0') + { + g_free(tran.category); + g_strstrip(value); + tran.category = g_strdup(value); + } + break; + } + + case 'S': + case 'E': + case '$': + { + if(tran.nb_splits < TXN_MAX_SPLIT) + { + switch(qif_line[0]) + { + case 'S': // split category + { + QIFSplit *s = &tran.splits[tran.nb_splits]; + if(*value != '\0') + { + g_free(s->category); + g_strstrip(value); + s->category = g_strdup(value); + } + break; + } + + case 'E': // split memo + { + QIFSplit *s = &tran.splits[tran.nb_splits]; + if(*value != '\0') + { + g_free(s->memo); + s->memo = g_strdup(value); + } + break; + } + + case '$': // split amount + { + QIFSplit *s = &tran.splits[tran.nb_splits]; + + s->amount = hb_qif_parser_get_amount(value); + // $ line normally end a split + #if MYDEBUG == 1 + g_print(" -> new split added: [%d] S=%s, E=%s, $=%.2f\n", tran.nb_splits, s->category, s->memo, s->amount); + #endif + + tran.nb_splits++; + break; + } + } + + } + // end split + break; + } + + case '^': // end of line + { + QIF_Tran *newitem; + + //fix: 380550 + if( tran.date ) + { + tran.account = g_strdup(cur_acc); + + DB ( g_print(" -> store qif txn: dat:'%s' amt:%.2f pay:'%s' mem:'%s' cat:'%s' acc:'%s' nbsplit:%d\n", tran.date, tran.amount, tran.payee, tran.memo, tran.category, tran.account, tran.nb_splits) ); + + newitem = da_qif_tran_malloc(); + da_qif_tran_move(&tran, newitem); + da_qif_tran_append(ctx, newitem); + } + + //unvalid tran + tran.date = 0; + //todo: should clear mem alloc here + + tran.nb_splits = 0; + break; + } + + } + // end of switch + + } + // end QIF_TRANSACTION + } + // end of stat normal + g_free(qif_line); + } + // end of for loop + + g_free(cur_acc); + g_io_channel_unref (io); + } + +} + + +/* +** this is our main qif entry point +*/ +GList * +account_import_qif(gchar *filename, ImportContext *ictx) +{ +QifContext ctx = { 0 }; +GList *qiflist; +GList *list = NULL; + + DB( g_print("\n[qif] account import qif\n") ); + + // allocate our GLists + da_qif_tran_new(&ctx); + ctx.is_ccard = FALSE; + + // parse !! + hb_qif_parser_parse(&ctx, filename, ictx->encoding); + + // check iso date format in file + //isodate = hb_qif_parser_check_iso_date(&ctx); + //DB( g_print(" -> date is dd/mm/yy: %d\n", isodate) ); + + DB( g_print("\n\n -> transform to hb txn\n") ); + + DB( g_print(" -> %d qif txn\n", g_list_length(ctx.q_tra)) ); + + // transform our qif transactions to homebank ones + qiflist = g_list_first(ctx.q_tra); + while (qiflist != NULL) + { + QIF_Tran *item = qiflist->data; + Transaction *newope, *child; + Account *accitem; + Payee *payitem; + Category *catitem; + gchar *name, *tmpmemo, *tmppayee; + gint nsplit; + + newope = da_transaction_malloc(); + + newope->date = hb_date_get_julian(item->date, ictx->datefmt); + if( newope->date == 0 ) + ictx->cnt_err_date++; + + //newope->paymode = atoi(str_array[1]); + //newope->info = g_strdup(str_array[2]); + + //#916690 manage memo, swap memo/payee + tmpmemo = item->memo; + tmppayee = item->payee; + if( PREFS->dtex_qifswap ) + { + tmpmemo = item->payee; + tmppayee = item->memo; + } + + if( PREFS->dtex_qifmemo ) + newope->wording = g_strdup(tmpmemo); + + newope->info = g_strdup(item->info); + newope->amount = item->amount; + + //#773282 invert amount for ccard accounts + if(ctx.is_ccard) + newope->amount *= -1; + + // payee + append + if( tmppayee != NULL ) + { + payitem = da_pay_get_by_name(tmppayee); + if(payitem == NULL) + { + //DB( g_print(" -> append pay: '%s'\n", tmppayee ) ); + + payitem = da_pay_malloc(); + payitem->name = g_strdup(tmppayee); + payitem->imported = TRUE; + da_pay_append(payitem); + + ictx->cnt_new_pay += 1; + } + newope->kpay = payitem->key; + } + + // LCategory of transaction + // L[Transfer account name] + // LCategory of transaction/Class of transaction + // L[Transfer account]/Class of transaction + if( item->category != NULL ) + { + if(g_str_has_prefix(item->category, "[")) // this is a transfer account name + { + gchar *accname; + + //DB ( g_print(" -> transfer to: '%s'\n", item->category) ); + + //remove brackets + accname = hb_strdup_nobrackets(item->category); + + accitem = import_create_account(accname, NULL); + + newope->kxferacc = accitem->key; + newope->paymode = PAYMODE_INTXFER; + + g_free(accname); + } + else + { + //DB ( g_print(" -> append cat: '%s'\n", item->category) ); + + catitem = da_cat_append_ifnew_by_fullname(item->category, TRUE ); + if( catitem != NULL ) + { + ictx->cnt_new_cat += 1; + newope->kcat = catitem->key; + } + } + } + + // splits, if not a xfer + if( newope->paymode != PAYMODE_INTXFER ) + { + for(nsplit=0;nsplitnb_splits;nsplit++) + { + QIFSplit *s = &item->splits[nsplit]; + Split *hbs; + guint32 kcat = 0; + + DB( g_print(" -> append split %d: '%s' '%.2f' '%s'\n", nsplit, s->category, s->amount, s->memo) ); + + if( s->category != NULL ) + { + catitem = da_cat_append_ifnew_by_fullname(s->category, TRUE ); // TRUE = imported + if( catitem != NULL ) + { + kcat = catitem->key; + } + } + + hbs = da_split_new(kcat, s->amount, s->memo); + + da_splits_append(newope->splits, hbs); + + //da_transaction_splits_append(newope, hbs); + hbs = NULL; + } + } + + // account + append + name = strcmp(QIF_UNKNOW_ACCOUNT_NAME, item->account) == 0 ? "" : item->account; + + DB( g_print(" -> account name is '%s'\n", name ) ); + + accitem = import_create_account(name, NULL); + + newope->kacc = accitem->key; + + newope->flags |= OF_ADDED; + if( newope->amount > 0 ) + newope->flags |= OF_INCOME; + + if( item->reconciled ) + newope->status = TXN_STATUS_RECONCILED; + else + if( item->cleared ) + newope->status = TXN_STATUS_CLEARED; + + child = account_qif_get_child_transfer(newope, list); + if( child != NULL) + { + //DB( g_print(" -> transaction already exist\n" ) ); + + da_transaction_free(newope); + } + else + { + //DB( g_print(" -> append trans. acc:'%s', memo:'%s', val:%.2f\n", item->account, item->memo, item->amount ) ); + + list = g_list_append(list, newope); + } + + qiflist = g_list_next(qiflist); + } + + // destroy our GLists + da_qif_tran_destroy(&ctx); + + DB( g_print(" -> %d txn converted\n", g_list_length(list)) ); + DB( g_print(" -> %d errors\n", ictx->cnt_err_date) ); + + + + + return list; +} + +