-
Notifications
You must be signed in to change notification settings - Fork 6
/
qb_trans_to_gc
executable file
·246 lines (223 loc) · 7.01 KB
/
qb_trans_to_gc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
#!/usr/bin/python
#
# A simple QuickBooks transaction importer
#
# See https://lwn.net/Articles/729087/ for the article that describes
# this program.
#
# Copyright 2017 Jonathan Corbet.
# This program may be distributed under the terms of the GNU General
# Public License, version 2 or later.
#
# This program is provided with no warranty of any kind. You, and only
# you, are responsible for the integrity of your accounting data.
#
import sys
# If needed, Uncomment the below line and modify with your
# site-package path containing gnucash bindings.
# sys.path.insert(0, "/my/path/to/site-packages/gnucash")
import csv, argparse
import remap
import gnucash
from gnucash import gnucash_business, gnucash_core_c
import regex
#
# Turn a float value into the gnucash equivalent.
#
SCALE = 1000
def GCVal(value):
value=value.replace(',', '')
dollars, cents = list(map(int, value.split('.')))
ival = dollars*SCALE
if value[0] != '-':
ival += cents*(SCALE/100)
else:
ival -= cents*(SCALE/100)
return gnucash.GncNumeric(int(ival), SCALE)
#
# Gnucash implements an account hierarchy but makes us walk it
# ourselves.
#
def LookupAccount(path):
acct = root_acct
for component in path.split('/'):
acct = acct.lookup_by_name(component)
if not acct:
return None
return acct
def SetDate(trans, date):
month, day, year = list(map(int, date.split('/')))
# Handle old, two digit dates with an arbitrary 1980 pivot
if year < 100:
if year < 80:
year = year + 2000;
else:
year = year + 1900
trans.SetDate(day, month, year)
acctregex = regex.compile(r'(?<=^\d* [—-] )(.*)')
coderegex = regex.compile(r'^\d*')
def ReadTransaction(reader):
entry = next(reader)
print(entry)
#
# QB helpfully puts in a couple of crap lines with an empty
# name. Drop them.
#
try:
if entry['']:
return
except KeyError:
pass
#
# Set up the overall transaction.
#
# FIRST LINE TRANSACTION DATA:
# NOTE: for a Journal export, we assume the first line
# of a transaction has the Trans #, Type, and Date.
# Optionally, there may be a 'Num' value
#
trans = gnucash.Transaction(book)
trans.BeginEdit()
trans.SetCurrency(dollars)
description = ( "(qb: " +
"Type: " + entry['Type'])
try:
if entry['Num']:
description = description + " Num: " + entry['Num']
except KeyError:
pass
description = description + ")"
trans.SetDescription(description)
trans.SetNum(entry['Trans #'])
SetDate(trans, entry['Date'])
# for a Journal Export, we assume entries are reconciled
# reconciled status will be invalidated
# if there's an imbalance is found
reconciled = 'y'
#
# Basic theory here (FOR JOURNAL EXPORT):
# QB dumps a pile of splits into the file
# without grouping them into transactions. The signal that we've
# found the last split is that the balance goes to zero. This
# *could* screw us, since that is possible in a legitimate transaction.
# But with luck it won't actually happen.
#
# Note the the overall entry is also the first split.
#
while True:
#
# Read in the Split (aka Leg) of the transaction
#
#
# first evaluate the debit and credit values
#
debit = None
credit = None
try:
if entry['Debit']:
debit = entry['Debit']
if entry['Credit']:
credit = entry['Credit']
except KeyError:
pass
#
# Are we done for this transaction?
# The last entry should have just credit AND debit entries.
#
if (debit is not None) and (credit is not None):
if credit != debit:
reconciled = 'n' #mark imbalanced transaction as needing reconciliation
break
#
# Put together the split info.
#
split = gnucash.Split(book)
#
# NOTE: I'm interpreting the debit and credit
# values (in relation to the account)
# as swapped in the Journal export.
# So, for a given split,
# - a debit value seen is a credit (positive num) to the account
# - a credit value seen is a debit (negative num) to the account
#
if (debit is not None) and (credit is None):
split.SetValue(GCVal(debit))
split.SetAmount(GCVal(debit))
if (debit is None) and (credit is not None):
split.SetValue(GCVal(credit) * -1.0)
split.SetAmount(GCVal(credit) * -1.0)
acct_matches = acctregex.search(entry['Account'])
code_matches = coderegex.search(entry['Account'])
if ((acct_matches is None) or
(len(acct_matches.groups()) == 0) or
(code_matches is None)):
print('Unknown account', entry['Account'])
account = LookupAccount('Miscellaneous')
entry = next(reader)
continue
code = code_matches.group(0)
account = root_acct.lookup_by_code(code)
split.SetAccount(account)
qb_migration_memo = entry['Memo']
try:
if entry['Name']:
qb_migration_memo = (qb_migration_memo +
" (qb: " +
"name:\"" + entry['Name'] +
") ")
except KeyError:
pass
split.SetMemo(qb_migration_memo)
split.SetParent(trans)
gnucash_core_c.xaccSplitSetReconcile(split.get_instance(),
reconciled)
# move to next leg of this transaction
entry = next(reader)
#
# Finalize and commit transaction
#
trans.CommitEdit()
#
# Check args and open files
#
#
# Here we do the argparsery
#
def setupargs():
p = argparse.ArgumentParser()
p.add_argument('-m', '--mapfile', required = False, default = None,
help = 'Name of account-name remapping file')
p.add_argument('-o', '--override', required = False, action = 'store_true',
help = 'Override lock on the gnucash file', default = False)
p.add_argument('csvfile', help = 'The CSV file to process')
p.add_argument('gcfile', help = 'The gnucash file to import into')
return p
args = setupargs().parse_args()
try:
tfile = open(args.csvfile, 'r')
except IOError as e:
print('Unable to open %s, %s' % (args.csvfile, e))
sys.exit(1)
#
# Load the mapfile if there is one.
#
if args.mapfile:
remap.load_mapfile(args.mapfile)
#
# Set up with gnucash
#
session = gnucash.Session(args.gcfile, mode=gnucash.SessionOpenMode.SESSION_NORMAL_OPEN)
book = session.book
dollars = book.get_table().lookup('CURRENCY', 'USD')
root_acct = book.get_root_account()
#
# Plow through the data.
#
reader = csv.DictReader(tfile)
while True:
try:
ReadTransaction(reader)
except StopIteration:
break
session.save()
session.end()