line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
1
|
|
|
1
|
|
1294
|
use 5.006; |
|
1
|
|
|
|
|
2
|
|
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
package MoneyWorks; |
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
our $VERSION = '0.10'; # Update MoneyWorks.pod, too. |
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
use # |
8
|
1
|
|
|
1
|
|
4
|
strict; use # |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
14
|
|
9
|
1
|
|
|
1
|
|
3
|
warnings; no # |
|
1
|
|
|
|
|
7
|
|
|
1
|
|
|
|
|
26
|
|
10
|
1
|
|
|
1
|
|
2
|
warnings qw 'utf8 parenthesis regexp once qw'; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
32
|
|
11
|
1
|
|
|
1
|
|
4
|
use warnings'register; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
87
|
|
12
|
|
|
|
|
|
|
|
13
|
1
|
|
|
1
|
|
3
|
use Carp 'croak'; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
43
|
|
14
|
1
|
|
|
1
|
|
2
|
use Exporter 5.57 'import'; |
|
1
|
|
|
|
|
13
|
|
|
1
|
|
|
|
|
23
|
|
15
|
1
|
|
|
1
|
|
397
|
use IPC::Open3; |
|
1
|
|
|
|
|
2834
|
|
|
1
|
|
|
|
|
41
|
|
16
|
1
|
|
|
1
|
|
5
|
use Scalar::Util 'blessed'; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
54
|
|
17
|
1
|
|
|
1
|
|
3
|
use Symbol 'geniosym'; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
59
|
|
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
our @EXPORT = qw( mw_cli_quote mw_str_quote); |
20
|
|
|
|
|
|
|
our %EXPORT_TAGS = ( all => \@EXPORT ); |
21
|
|
|
|
|
|
|
BEGIN { |
22
|
1
|
|
|
1
|
|
2
|
*IMPORT = \&import; |
23
|
1
|
|
|
|
|
31
|
undef *import; |
24
|
|
|
|
|
|
|
} |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
our @BinPaths = ( |
27
|
|
|
|
|
|
|
'/Applications/MoneyWorks Gold.app/Contents/MacOS/MoneyWorks Gold', |
28
|
|
|
|
|
|
|
'/usr/bin/moneyworks', |
29
|
|
|
|
|
|
|
'C:/Program Files/MoneyWorks Gold/MoneyWorks Gold.exe', |
30
|
|
|
|
|
|
|
); |
31
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
#our %Fields; # defined further down, to keep it out of the whey |
33
|
|
|
|
|
|
|
|
34
|
1
|
|
|
1
|
|
3
|
no constant 1.03 (); |
|
1
|
|
|
|
|
15
|
|
|
1
|
|
|
|
|
25
|
|
35
|
|
|
|
|
|
|
use constant::lexical { |
36
|
|
|
|
|
|
|
# publicly accessible fields |
37
|
1
|
|
|
|
|
7
|
_rego => 0, |
38
|
|
|
|
|
|
|
_user => 1, |
39
|
|
|
|
|
|
|
_pswd => 2, |
40
|
|
|
|
|
|
|
_file => 3, |
41
|
|
|
|
|
|
|
_bina => 4, |
42
|
|
|
|
|
|
|
_live => 5, |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
# behind the scenes |
45
|
|
|
|
|
|
|
_hndl => 6, |
46
|
|
|
|
|
|
|
_prid => 7, |
47
|
1
|
|
|
1
|
|
397
|
}; |
|
1
|
|
|
|
|
1299
|
|
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
sub new { |
50
|
1
|
|
|
1
|
0
|
12053
|
my ($class,%args) = @_; |
51
|
1
|
|
|
|
|
1
|
my $self = []; |
52
|
1
|
|
|
|
|
3
|
$self->[_rego] = delete $args{rego}; |
53
|
1
|
|
|
|
|
1
|
$self->[_user] = delete $args{user}; |
54
|
1
|
|
|
|
|
1
|
$self->[_pswd] = delete $args{password}; |
55
|
1
|
|
|
|
|
2
|
$self->[_file] = delete $args{file}; |
56
|
|
|
|
|
|
|
|
57
|
1
|
50
|
|
|
|
3
|
unless( $self->[_bina] = delete $args{bin} ) { |
58
|
|
|
|
|
|
|
# find the executable if we can |
59
|
1
|
|
|
|
|
3
|
for(@BinPaths) { |
60
|
3
|
50
|
|
|
|
91
|
-e and $self->[_bina] = $_, last; |
61
|
|
|
|
|
|
|
} |
62
|
|
|
|
|
|
|
} |
63
|
|
|
|
|
|
|
|
64
|
1
|
50
|
|
|
|
4
|
$self->[_live] = exists $args{keep_alive} ? delete $args{keep_alive} : 1; |
65
|
1
|
|
|
|
|
5
|
bless $self, $class;; |
66
|
|
|
|
|
|
|
} |
67
|
|
|
|
|
|
|
|
68
|
0
|
|
|
0
|
1
|
0
|
sub rego { unshift @_, _rego; goto &_accessor } |
|
0
|
|
|
|
|
0
|
|
69
|
0
|
|
|
0
|
1
|
0
|
sub user { unshift @_, _user; goto &_accessor } |
|
0
|
|
|
|
|
0
|
|
70
|
0
|
|
|
0
|
1
|
0
|
sub password { unshift @_, _pswd; goto &_accessor } |
|
0
|
|
|
|
|
0
|
|
71
|
0
|
|
|
0
|
1
|
0
|
sub file { unshift @_, _file; goto &_accessor } |
|
0
|
|
|
|
|
0
|
|
72
|
1
|
|
|
1
|
1
|
2
|
sub bin { unshift @_, _bina; goto &_accessor } |
|
1
|
|
|
|
|
3
|
|
73
|
0
|
|
|
0
|
1
|
0
|
sub keep_alive{ unshift @_, _live; goto &_accessor } |
|
0
|
|
|
|
|
0
|
|
74
|
|
|
|
|
|
|
sub _accessor { |
75
|
1
|
50
|
|
1
|
|
7
|
@_ > 2 ? ( $_[1][$_[0]] = $_[2], $_[1]->close, $_[2] ) : $_[1][$_[0]] |
76
|
|
|
|
|
|
|
} |
77
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
sub command { |
79
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
80
|
0
|
|
|
|
|
0
|
my $command = shift; |
81
|
|
|
|
|
|
|
|
82
|
0
|
0
|
|
|
|
0
|
croak "Commands cannot contain line breaks" if $command =~ /[\r\n]/; |
83
|
0
|
0
|
|
|
|
0
|
warnings'warnif(__PACKAGE__,"Command contains null chars") |
84
|
|
|
|
|
|
|
if $command =~ y/\0//d; |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
# Used by the SIGPIPE handle |
87
|
0
|
|
|
|
|
0
|
my $tries; |
88
|
0
|
|
|
|
|
0
|
MoneyWorks_COMMAND: |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
my($rh,$wh,$maybe_open_file); |
91
|
0
|
|
|
|
|
0
|
my $tmp; # For single-process mode: the stderr handle (which is not even |
92
|
|
|
|
|
|
|
# used) needs to last till the end of the sub to avoid giving the |
93
|
|
|
|
|
|
|
# child proc a SIGPIPE |
94
|
|
|
|
|
|
|
|
95
|
0
|
|
|
|
|
0
|
my $live = $self->[_live]; |
96
|
0
|
0
|
|
|
|
0
|
if($live) { # keep-alive |
97
|
|
|
|
|
|
|
# fetch the handles, creating them if necessary |
98
|
0
|
|
0
|
|
|
0
|
($rh, $wh) = @{ $self->[_hndl] ||= do{ |
|
0
|
|
|
|
|
0
|
|
99
|
|
|
|
|
|
|
# start the process |
100
|
0
|
|
|
|
|
0
|
my $pid = _open($self, my($wh,$rh), my $eh = geniosym); |
101
|
0
|
|
|
|
|
0
|
$self->[_prid] = $pid; |
102
|
0
|
|
|
|
|
0
|
++$maybe_open_file; |
103
|
|
|
|
|
|
|
|
104
|
|
|
|
|
|
|
# return the handles |
105
|
0
|
|
|
|
|
0
|
[$rh,$wh,$eh] # $eh is not used but we hang on to it to avoid SIGPIPING |
106
|
|
|
|
|
|
|
} }; # the child process. |
107
|
|
|
|
|
|
|
} |
108
|
|
|
|
|
|
|
else { # single command (the easy way) |
109
|
0
|
|
|
|
|
0
|
_open( $self, $wh, $rh, $tmp = geniosym ); |
110
|
0
|
|
|
|
|
0
|
++$maybe_open_file; |
111
|
|
|
|
|
|
|
} |
112
|
|
|
|
|
|
|
|
113
|
0
|
|
|
|
|
0
|
local $\ = "\n"; |
114
|
0
|
|
|
|
|
0
|
select +( select($wh), $|=1 )[0]; |
115
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
# open a file if necessary |
117
|
0
|
0
|
0
|
|
|
0
|
if($maybe_open_file and defined $self->[_file]) { |
118
|
|
|
|
|
|
|
# avoid problems with files named -e |
119
|
0
|
|
|
|
|
0
|
(my $file = $self->[_file]) =~ s|^-|./-|; |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
# prepare the open file command |
122
|
0
|
|
|
|
|
0
|
my $command = "open file=".mw_cli_quote($file); |
123
|
0
|
|
|
|
|
0
|
my($u,$p) = @$self[_user,_pswd]; |
124
|
1
|
|
|
1
|
|
509
|
no warnings 'uninitialized'; |
|
1
|
|
|
|
|
6
|
|
|
1
|
|
|
|
|
1189
|
|
125
|
0
|
0
|
0
|
|
|
0
|
defined $u && length $u and |
126
|
|
|
|
|
|
|
$command .= " login=".mw_cli_quote("$u:$p"); |
127
|
|
|
|
|
|
|
|
128
|
0
|
|
|
|
|
0
|
my $retry; |
129
|
|
|
|
|
|
|
local $SIG{PIPE} = sub { |
130
|
0
|
0
|
|
0
|
|
0
|
$tries++ < 3 and $self->close, $retry = 1; |
131
|
0
|
|
|
|
|
0
|
}; |
132
|
|
|
|
|
|
|
# send the command |
133
|
0
|
|
|
|
|
0
|
print $wh $command; |
134
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
# See whether there was a SIGPIPE |
136
|
0
|
0
|
|
|
|
0
|
goto MoneyWorks_COMMAND if $retry; |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
# check result |
139
|
0
|
|
|
|
|
0
|
my $headers = _read_headers($rh); |
140
|
0
|
0
|
|
|
|
0
|
$$headers{Status} eq 'OK'||_croak($self,$headers); |
141
|
|
|
|
|
|
|
} |
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
# send the command |
144
|
0
|
|
|
|
|
0
|
print $wh $command; |
145
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
# parse output headers |
147
|
0
|
|
|
|
|
0
|
my $headers = _read_headers($rh); |
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
# check status |
150
|
0
|
0
|
|
|
|
0
|
$$headers{Status} eq 'OK' or _croak($self,$headers); |
151
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
# return data |
153
|
0
|
0
|
|
|
|
0
|
if(exists $$headers{'Content-Length'}) { # omitted when the empty string |
154
|
0
|
|
|
|
|
0
|
my $data; # is returned |
155
|
0
|
|
|
|
|
0
|
read $rh, $data, $$headers{'Content-Length'}; |
156
|
|
|
|
|
|
|
|
157
|
0
|
|
|
|
|
0
|
$data; |
158
|
|
|
|
|
|
|
} |
159
|
0
|
|
|
|
|
0
|
else { '' } |
160
|
|
|
|
|
|
|
} |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
my @bad_env_vars = qw(PATH IFS CDPATH ENV BASH_ENV); |
163
|
|
|
|
|
|
|
|
164
|
|
|
|
|
|
|
sub _open { |
165
|
0
|
|
|
0
|
|
0
|
my $self = shift; |
166
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
# insanity check |
168
|
0
|
0
|
|
|
|
0
|
defined $self->[_bina] |
169
|
|
|
|
|
|
|
or croak "MoneyWorks could not be run: no path specified"; |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
# remove unsafe env vars temporarily |
172
|
0
|
0
|
|
|
|
0
|
local(@ENV{@bad_env_vars}), delete @ENV{@bad_env_vars} if ${^TAINT}; |
173
|
|
|
|
|
|
|
|
174
|
0
|
|
|
|
|
0
|
my $rego = $self->[_rego]; |
175
|
0
|
0
|
|
|
|
0
|
open3(@_, $self->[_bina], '-h', $rego ? ('-r', $rego) : ()) |
|
|
0
|
|
|
|
|
|
176
|
|
|
|
|
|
|
or croak "MoneyWorks ($self->[_bina]) could not be run: $!"; |
177
|
|
|
|
|
|
|
} |
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
# From: Rowan Daniell |
180
|
|
|
|
|
|
|
# Subject: Re: Concerning HTTP-style output |
181
|
|
|
|
|
|
|
# Date: Mon, 4 May 2009 09:02:46 +1200 |
182
|
|
|
|
|
|
|
# |
183
|
|
|
|
|
|
|
# > The ‘-h’ mode output does not look exactly like HTTP to me :-). It |
184
|
|
|
|
|
|
|
# > seems a lot simpler (which is good). Can I confirm with you that |
185
|
|
|
|
|
|
|
# > the format is as follows? (I’m trying to make sure that my programs |
186
|
|
|
|
|
|
|
# > don’t break in the future because I didn’t take all possibilities. |
187
|
|
|
|
|
|
|
# > of the syntax into account.) Each header is a word followed by a |
188
|
|
|
|
|
|
|
# > colon and a space (chr 32), and then the header’s value verbatim |
189
|
|
|
|
|
|
|
# > (no escapes, quotes or line breaks as per HTTP) followed by a line |
190
|
|
|
|
|
|
|
# > feed (chr 10). A blank line ("\n\n") indicates the end of the |
191
|
|
|
|
|
|
|
# > header. Is this correct? And is the line break character the same |
192
|
|
|
|
|
|
|
# > on both platforms? |
193
|
|
|
|
|
|
|
# |
194
|
|
|
|
|
|
|
# Yes. That is all correct. |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
# Except that after that exchange I found that \r\n is the line break char. |
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
sub _read_headers { |
199
|
0
|
|
|
0
|
|
0
|
my $handle = shift; |
200
|
0
|
|
|
|
|
0
|
local $/ = "\r\n"; |
201
|
|
|
|
|
|
|
|
202
|
0
|
|
|
|
|
0
|
my %headers; |
203
|
|
|
|
|
|
|
my $past_first; |
204
|
0
|
|
|
|
|
0
|
while(my $line = <$handle>) { |
205
|
0
|
0
|
|
|
|
0
|
$line =~ s/\r\n\z// |
206
|
|
|
|
|
|
|
or croak "Mangled output from MoneyWorks (no CRLF): $line"; |
207
|
|
|
|
|
|
|
# When run under root, MoneyWorks sometimes puts |
208
|
|
|
|
|
|
|
# "Address already in use\n" (without the \r) at the beginning of |
209
|
|
|
|
|
|
|
# its output. |
210
|
0
|
0
|
|
|
|
0
|
$past_first++ or $line =~ s/^Address already in use\n//; |
211
|
0
|
0
|
|
|
|
0
|
length $line or last; |
212
|
0
|
0
|
|
|
|
0
|
$line =~ s/^([^:]+): // or croak "Mangled output from MoneyWorks: $line"; |
213
|
0
|
|
|
|
|
0
|
$headers{$1} = $line; |
214
|
|
|
|
|
|
|
} |
215
|
0
|
|
|
|
|
0
|
return \%headers; |
216
|
|
|
|
|
|
|
} |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
sub _croak { # Extracts error message from headers hash |
219
|
0
|
|
|
0
|
|
0
|
my $self = shift; |
220
|
0
|
|
|
|
|
0
|
my $h = shift; |
221
|
0
|
|
|
|
|
0
|
my $msg; |
222
|
0
|
0
|
|
|
|
0
|
if(exists $$h{Diagnostic}) { |
223
|
0
|
|
|
|
|
0
|
($msg = $$h{Diagnostic}) =~ s/^\[ERROR] //; |
224
|
0
|
0
|
|
|
|
0
|
$msg .= ": " if exists $$h{Error}; |
225
|
|
|
|
|
|
|
} |
226
|
0
|
0
|
|
|
|
0
|
$msg .= $$h{Error} if exists $$h{Error}; |
227
|
0
|
|
|
|
|
0
|
$self->close; |
228
|
0
|
|
|
|
|
0
|
croak("Moneyworks error: $msg"); |
229
|
|
|
|
|
|
|
} |
230
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
sub version { |
232
|
0
|
|
|
0
|
1
|
0
|
shift->command('version'); |
233
|
|
|
|
|
|
|
} |
234
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
sub eval { |
236
|
0
|
|
|
0
|
1
|
0
|
my($self,$expr) = @_; |
237
|
0
|
|
|
|
|
0
|
$expr =~ y/\r\n/ /; |
238
|
0
|
|
|
|
|
0
|
shift->command('evaluate expr=' . mw_cli_quote($expr)); |
239
|
|
|
|
|
|
|
} |
240
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
sub import { |
242
|
1
|
|
|
1
|
|
6
|
my($self,%args) = @_; |
243
|
1
|
50
|
33
|
|
|
54
|
defined blessed $self and $self->isa(__PACKAGE__) or goto &IMPORT; |
244
|
|
|
|
|
|
|
|
245
|
0
|
|
|
|
|
0
|
my $data_arg; |
246
|
|
|
|
|
|
|
my $map_arg; |
247
|
|
|
|
|
|
|
|
248
|
0
|
0
|
|
|
|
0
|
if(exists $args{map}) { |
249
|
0
|
0
|
|
|
|
0
|
($map_arg = delete $args{map}) =~ /[\r\n]/ |
250
|
|
|
|
|
|
|
and croak "Import map file names cannot contain line breaks"; |
251
|
|
|
|
|
|
|
# This is a MoneyWorks limitation. The syntax doesn’t allow it. |
252
|
0
|
|
|
|
|
0
|
$map_arg = mw_cli_quote($map_arg); |
253
|
0
|
0
|
|
|
|
0
|
if(exists $args{data_file}) { |
254
|
0
|
|
|
|
|
0
|
$data_arg = 'file=' . mw_cli_quote(delete $args{data_file}); |
255
|
|
|
|
|
|
|
} |
256
|
|
|
|
|
|
|
else { |
257
|
0
|
|
|
|
|
0
|
my $data = delete $args{data}; |
258
|
0
|
|
|
|
|
0
|
$data =~ s/(?:\r\n?|\n)\z//; |
259
|
0
|
0
|
|
|
|
0
|
if($data =~ /[\n\r]/) { |
260
|
|
|
|
|
|
|
# write the data to a temporary file and use that |
261
|
0
|
|
|
|
|
0
|
require File::Temp; |
262
|
0
|
|
|
|
|
0
|
my($fh,$filename) |
263
|
|
|
|
|
|
|
= File'Temp'tempfile(uc suffix => '.txt', uc unlink => 1); |
264
|
0
|
|
|
|
|
0
|
local $\; |
265
|
0
|
|
|
|
|
0
|
print $fh $data; |
266
|
0
|
0
|
|
|
|
0
|
close $fh or croak "Couldn't close temp file for import: $!"; |
267
|
0
|
|
|
|
|
0
|
$data_arg = 'file=' . mw_cli_quote($filename); |
268
|
|
|
|
|
|
|
} |
269
|
0
|
|
|
|
|
0
|
else { $data_arg = 'data=' . mw_cli_quote($data) } |
270
|
|
|
|
|
|
|
} |
271
|
|
|
|
|
|
|
} |
272
|
|
|
|
|
|
|
else { |
273
|
0
|
|
|
|
|
0
|
croak "The map arg to import is not optional"; |
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
=cut |
276
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
This may be added later. There are currently serious problems with it. |
278
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
# fetch the fields for the table |
280
|
|
|
|
|
|
|
my $table = lc delete $args{table}; |
281
|
|
|
|
|
|
|
exists $Fields{$table} or croak "Unrecognised table: $table"; |
282
|
|
|
|
|
|
|
my $fields = $Fields{$table}; |
283
|
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
# create a temporary file |
285
|
|
|
|
|
|
|
require File::Temp; |
286
|
|
|
|
|
|
|
my($fh,$filename) |
287
|
|
|
|
|
|
|
= File'Temp'tempfile(uc suffix => '.txt', uc unlink => 1); |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
# for each record |
290
|
|
|
|
|
|
|
for( map +{%$_}, @{ delete $args{data} } ) { # copy each hash to avoid |
291
|
|
|
|
|
|
|
my $line = ''; # modifying what belongs to the caller |
292
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
# add fields to $line |
294
|
|
|
|
|
|
|
for my $f(@$fields) { |
295
|
|
|
|
|
|
|
no warnings 'uninitialized'; |
296
|
|
|
|
|
|
|
(my $val = delete $_->{$f}) =~ /[\t\r\n]/ |
297
|
|
|
|
|
|
|
and croak "Field values cannot contain tabs or line breaks"; |
298
|
|
|
|
|
|
|
$line .= "$val\t"; |
299
|
|
|
|
|
|
|
} |
300
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
# croak if fields are left over |
302
|
|
|
|
|
|
|
local $" = ' '; |
303
|
|
|
|
|
|
|
%$_ and croak "Invalid fields: @{ keys %$_ }"; |
304
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
# remove trailing tab |
306
|
|
|
|
|
|
|
chop $line; |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
# print to temp file |
309
|
|
|
|
|
|
|
local $\ = "\n"; |
310
|
|
|
|
|
|
|
print $fh $line; |
311
|
|
|
|
|
|
|
} |
312
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
close $fh or croak "Couldn't close temp file for import: $!"; |
314
|
|
|
|
|
|
|
|
315
|
|
|
|
|
|
|
$data_arg = 'file=' . mw_cli_quote($filename); |
316
|
|
|
|
|
|
|
|
317
|
|
|
|
|
|
|
# find the map |
318
|
|
|
|
|
|
|
my $f; |
319
|
|
|
|
|
|
|
# I pilfered this code from Unicode::Collate (and |
320
|
|
|
|
|
|
|
# modified it slightly). |
321
|
|
|
|
|
|
|
for (@INC) { |
322
|
|
|
|
|
|
|
$f = "$_/MoneyWorks/maps/$table.impo"; |
323
|
|
|
|
|
|
|
last if open $fh, $f; |
324
|
|
|
|
|
|
|
$f = undef; |
325
|
|
|
|
|
|
|
} |
326
|
|
|
|
|
|
|
defined $f or |
327
|
|
|
|
|
|
|
croak "MoneyWorks: Can't locate MoneyWorks/maps/$table.impo" . |
328
|
|
|
|
|
|
|
" in \@INC (\@INC contains @INC).\n"; |
329
|
|
|
|
|
|
|
$map_arg = mw_cli_quote($f); |
330
|
|
|
|
|
|
|
|
331
|
|
|
|
|
|
|
=cut |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
} |
334
|
|
|
|
|
|
|
|
335
|
0
|
|
|
|
|
0
|
my $ret = $self->command("import $data_arg map=$map_arg"); |
336
|
0
|
0
|
|
|
|
0
|
return unless defined wantarray; |
337
|
|
|
|
|
|
|
|
338
|
0
|
|
|
|
|
0
|
my %ret; |
339
|
0
|
|
|
|
|
0
|
for(split /;\s*/, $ret) { |
340
|
0
|
|
|
|
|
0
|
@_ = split /:\s*/; |
341
|
0
|
|
|
|
|
0
|
$ret{$_[0]} = $_[1]; |
342
|
|
|
|
|
|
|
} |
343
|
0
|
|
|
|
|
0
|
\%ret; |
344
|
|
|
|
|
|
|
} |
345
|
|
|
|
|
|
|
|
346
|
|
|
|
|
|
|
my %all_fields; |
347
|
|
|
|
|
|
|
|
348
|
|
|
|
|
|
|
sub export { |
349
|
0
|
|
|
0
|
1
|
0
|
my($self,%args) = @_; |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
# determine what the rettype will be |
352
|
0
|
|
|
|
|
0
|
my $using_hash = exists $args{key}; |
353
|
0
|
|
|
|
|
0
|
my $key = delete $args{key}; |
354
|
|
|
|
|
|
|
|
355
|
|
|
|
|
|
|
# get the list of fields |
356
|
0
|
|
|
|
|
0
|
my $table = delete $args{table}; |
357
|
0
|
|
|
|
|
0
|
my $qtable = mw_cli_quote($table); |
358
|
0
|
|
|
|
|
0
|
my $fields = delete $args{fields}; |
359
|
0
|
0
|
0
|
|
|
0
|
defined $fields or $fields = $all_fields{lc $table} ||= [ |
360
|
|
|
|
|
|
|
split "\t", ( |
361
|
|
|
|
|
|
|
$self->command( |
362
|
|
|
|
|
|
|
"export table=$qtable search='='" |
363
|
|
|
|
|
|
|
) =~ /([^\r\n]+)/ |
364
|
|
|
|
|
|
|
)[0] |
365
|
|
|
|
|
|
|
]; |
366
|
|
|
|
|
|
|
|
367
|
|
|
|
|
|
|
# determine whether the key needs to be added to the list of fields |
368
|
0
|
|
|
|
|
0
|
my $key_is_in_fields; |
369
|
0
|
0
|
|
|
|
0
|
if($using_hash) { |
370
|
0
|
|
|
|
|
0
|
for(@$fields) { |
371
|
0
|
0
|
|
|
|
0
|
$_ eq $key and ++$key_is_in_fields, last; |
372
|
|
|
|
|
|
|
} |
373
|
0
|
0
|
|
|
|
0
|
$key_is_in_fields or push @$fields, $key; |
374
|
|
|
|
|
|
|
} |
375
|
|
|
|
|
|
|
|
376
|
|
|
|
|
|
|
# prepare the command |
377
|
0
|
|
|
|
|
0
|
my $command = |
378
|
|
|
|
|
|
|
'export' |
379
|
|
|
|
|
|
|
.' table=' . mw_cli_quote($table) |
380
|
|
|
|
|
|
|
.' format=' . mw_cli_quote( |
381
|
|
|
|
|
|
|
join('\t', map "[$_]", @$fields).'\n' |
382
|
|
|
|
|
|
|
); |
383
|
|
|
|
|
|
|
exists $args{search} and $command .= |
384
|
0
|
0
|
|
|
|
0
|
' search=' . mw_cli_quote(delete $args{search}); |
385
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
# send the command |
387
|
0
|
|
|
|
|
0
|
my $output = $self->command($command); |
388
|
|
|
|
|
|
|
|
389
|
|
|
|
|
|
|
# parse the output |
390
|
0
|
0
|
|
|
|
0
|
my $ret = $using_hash |
391
|
|
|
|
|
|
|
? {} |
392
|
|
|
|
|
|
|
: []; |
393
|
0
|
|
|
|
|
0
|
for my $line(split /\n/, $output) { |
394
|
0
|
|
|
|
|
0
|
my %record; |
395
|
0
|
|
|
|
|
0
|
@record{ @$fields } = split /\t/, $line; |
396
|
|
|
|
|
|
|
$using_hash |
397
|
0
|
0
|
|
|
|
0
|
? $$ret{$record{$key}} = \%record |
398
|
|
|
|
|
|
|
: push @$ret, \%record; |
399
|
0
|
0
|
0
|
|
|
0
|
delete $record{$key} if $using_hash && ! $key_is_in_fields; |
400
|
|
|
|
|
|
|
} |
401
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
# return |
403
|
0
|
|
|
|
|
0
|
$ret; |
404
|
|
|
|
|
|
|
} |
405
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
# ~~~ report |
407
|
|
|
|
|
|
|
|
408
|
0
|
|
|
0
|
1
|
0
|
sub pid { shift->[_prid] } |
409
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
sub close { |
411
|
1
|
|
|
1
|
1
|
1
|
my $self = shift; |
412
|
1
|
|
|
|
|
2
|
my $pid = delete $$self[_prid]; |
413
|
1
|
50
|
|
|
|
47604
|
return unless my $handles = delete $$self[_hndl]; |
414
|
0
|
0
|
0
|
|
|
0
|
close $handles->[1] |
415
|
|
|
|
|
|
|
or $! and croak "Error while terminating MoneyWorks: $!"; |
416
|
0
|
|
|
|
|
0
|
waitpid $pid, 0; |
417
|
|
|
|
|
|
|
() |
418
|
0
|
|
|
|
|
0
|
} |
419
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
# ---------------- Ties ----------------- # |
421
|
|
|
|
|
|
|
|
422
|
|
|
|
|
|
|
sub tie { |
423
|
0
|
|
|
0
|
0
|
0
|
tie my %h, 'MoneyWorks::_table_tie', @_; |
424
|
0
|
|
|
|
|
0
|
\%h; |
425
|
|
|
|
|
|
|
} |
426
|
|
|
|
|
|
|
|
427
|
|
|
|
|
|
|
sub TIEHASH { |
428
|
0
|
|
|
0
|
|
0
|
my($package,%args) = @_; |
429
|
0
|
|
|
|
|
0
|
my $table = delete $args{table}; |
430
|
0
|
|
|
|
|
0
|
my $key = delete $args{key}; |
431
|
0
|
|
|
|
|
0
|
my $self = $package->new(%args); |
432
|
0
|
|
|
|
|
0
|
MoneyWorks::_table_tie->new($self, $table, $key); |
433
|
|
|
|
|
|
|
} |
434
|
|
|
|
|
|
|
|
435
|
|
|
|
|
|
|
{ |
436
|
|
|
|
|
|
|
use constant::lexical { |
437
|
1
|
|
|
|
|
5
|
parent => 0, cached => 1, table => 2, key => 3, row => 4 |
438
|
1
|
|
|
1
|
|
5
|
}; |
|
1
|
|
|
|
|
1
|
|
439
|
|
|
|
|
|
|
|
440
|
|
|
|
|
|
|
sub MoneyWorks::_table_tie::new { |
441
|
0
|
|
|
0
|
|
0
|
my($class,$parent,$table,$key) = @_; |
442
|
0
|
|
|
|
|
0
|
return bless [$parent,undef,$table,$key], $class; |
443
|
|
|
|
|
|
|
} |
444
|
|
|
|
|
|
|
*MoneyWorks::_table_tie::TIEHASH = *MoneyWorks::_table_tie::new; |
445
|
|
|
|
|
|
|
sub MoneyWorks::_table_tie::FETCH { |
446
|
0
|
|
|
0
|
|
0
|
my($self,$row) = @_; |
447
|
0
|
0
|
|
|
|
0
|
return unless $self->EXISTS($row); |
448
|
0
|
|
|
|
|
0
|
CORE::tie |
449
|
|
|
|
|
|
|
my %row, 'MoneyWorks::_row_tie', @$self[parent,table,key], $row; |
450
|
0
|
|
|
|
|
0
|
\%row; |
451
|
|
|
|
|
|
|
} |
452
|
|
|
|
|
|
|
sub MoneyWorks::_table_tie::EXISTS { |
453
|
0
|
|
|
0
|
|
0
|
my($self,$row) = @_; |
454
|
0
|
|
0
|
|
|
0
|
$self->[parent]->command( |
455
|
|
|
|
|
|
|
( |
456
|
|
|
|
|
|
|
$self->[cached] ||= |
457
|
|
|
|
|
|
|
'export' |
458
|
|
|
|
|
|
|
.' table=' . MoneyWorks::mw_cli_quote($self->[table]) |
459
|
|
|
|
|
|
|
.' format="1"' |
460
|
|
|
|
|
|
|
.' search=' |
461
|
|
|
|
|
|
|
) . MoneyWorks::mw_cli_quote( |
462
|
|
|
|
|
|
|
"Replace($self->[key],`\@`,`\1`)=Replace(" |
463
|
|
|
|
|
|
|
. MoneyWorks'mw_str_quote($row) |
464
|
|
|
|
|
|
|
.",`\@`,`\1`)" |
465
|
|
|
|
|
|
|
) |
466
|
|
|
|
|
|
|
); |
467
|
|
|
|
|
|
|
} |
468
|
|
|
|
|
|
|
|
469
|
|
|
|
|
|
|
sub MoneyWorks::_row_tie::TIEHASH { |
470
|
0
|
|
|
0
|
|
0
|
my($class,$parent,$table,$key,$row) = @_; |
471
|
0
|
|
|
|
|
0
|
return bless [$parent,undef,$table,$key,$row], $class; |
472
|
|
|
|
|
|
|
} |
473
|
|
|
|
|
|
|
sub MoneyWorks::_row_tie::FETCH { |
474
|
0
|
|
|
0
|
|
0
|
my($self,$field) = @_; |
475
|
|
|
|
|
|
|
$self->[parent]->eval( |
476
|
|
|
|
|
|
|
'Find(' |
477
|
|
|
|
|
|
|
. mw_str_quote("$self->[table].$field") . ',' |
478
|
|
|
|
|
|
|
. ( |
479
|
|
|
|
|
|
|
$self->[cached] |
480
|
0
|
|
0
|
|
|
0
|
||= do { |
481
|
0
|
|
|
|
|
0
|
(my $row = $$self[row]) =~ y/\@/\1/; |
482
|
0
|
|
|
|
|
0
|
mw_str_quote( |
483
|
|
|
|
|
|
|
"Replace($self->[key],`\@`,`\1`)=" . mw_str_quote($row) |
484
|
|
|
|
|
|
|
) |
485
|
|
|
|
|
|
|
} |
486
|
|
|
|
|
|
|
) |
487
|
|
|
|
|
|
|
.')' |
488
|
|
|
|
|
|
|
); |
489
|
|
|
|
|
|
|
} |
490
|
|
|
|
|
|
|
} |
491
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
# ------------------ Functions ---------------- # |
493
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
|
495
|
|
|
|
|
|
|
sub mw_cli_quote($) { |
496
|
0
|
|
|
0
|
1
|
0
|
my $str = shift; |
497
|
0
|
0
|
|
|
|
0
|
warnings'warnif |
498
|
|
|
|
|
|
|
__PACKAGE__,"Argument to mw_cli_quote contains line breaks" |
499
|
|
|
|
|
|
|
if $str =~ /[\r\n]/; |
500
|
0
|
|
|
|
|
0
|
my $delim = chr 0x7f; |
501
|
0
|
|
|
|
|
0
|
while(index $str, $delim, != -1) { |
502
|
0
|
0
|
|
|
|
0
|
--vec $delim, 0, 8, == 31 |
503
|
|
|
|
|
|
|
and croak "Can't quote $str; no delimiters available" |
504
|
|
|
|
|
|
|
} |
505
|
0
|
|
|
|
|
0
|
"$delim$str$delim"; |
506
|
|
|
|
|
|
|
} |
507
|
|
|
|
|
|
|
|
508
|
|
|
|
|
|
|
{ |
509
|
|
|
|
|
|
|
my %escapes = ( |
510
|
|
|
|
|
|
|
'"' => '\"', |
511
|
|
|
|
|
|
|
' ' => '\t', |
512
|
|
|
|
|
|
|
"\n" => '\n', |
513
|
|
|
|
|
|
|
"\r" => '\r', |
514
|
|
|
|
|
|
|
'\\' => '\\\\', |
515
|
|
|
|
|
|
|
); |
516
|
|
|
|
|
|
|
sub mw_str_quote($) { |
517
|
0
|
|
|
0
|
1
|
0
|
my $str = shift; |
518
|
0
|
0
|
|
|
|
0
|
if($str =~ /`/) { |
519
|
0
|
|
|
|
|
0
|
$str =~ s/(["\t\n\r\\])/$escapes{$1}/g; |
520
|
0
|
|
|
|
|
0
|
return qq/"$str"/; |
521
|
|
|
|
|
|
|
} |
522
|
|
|
|
|
|
|
else { |
523
|
0
|
|
|
|
|
0
|
$str =~ s/([\t\n\r\\])/$escapes{$1}/g; |
524
|
0
|
|
|
|
|
0
|
return "`$str`"; |
525
|
|
|
|
|
|
|
} |
526
|
|
|
|
|
|
|
} |
527
|
|
|
|
|
|
|
} |
528
|
|
|
|
|
|
|
|
529
|
|
|
|
|
|
|
# ------------ Misc stuff -------------- # |
530
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
sub DESTROY { |
532
|
1
|
|
|
1
|
|
112
|
shift->close; |
533
|
|
|
|
|
|
|
} |
534
|
|
|
|
|
|
|
|
535
|
|
|
|
|
|
|
=cut |
536
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
BEGIN { |
538
|
|
|
|
|
|
|
%Fields = ( |
539
|
|
|
|
|
|
|
product => [qw/ Code Supplier SuppliersCode Description Comment Category1 Category2 Category3 Category4 SalesAcct StockAcct COGAcct SellUnit SellPrice SellPriceB SellPriceC SellPriceD SellPriceE SellPriceF QtyBrkSellPriceA1 QtyBrkSellPriceA2 QtyBrkSellPriceA3 QtyBrkSellPriceA4 QtyBrkSellPriceB1 QtyBrkSellPriceB2 QtyBrkSellPriceB3 QtyBrkSellPriceB4 QtyBreak1 QtyBreak2 QtyBreak3 QtyBreak4 BuyUnit BuyPrice ConversionFactor SellDiscount SellDiscountMode ReorderLevel Type Colour UserNum UserText Plussage BuyWeight StockTakeQty StockTakeValue StockTakeNewQty BarCode BuyPriceCurrency Custom1 Custom2 Custom3 Custom4 LeadTimeDays SellWeight Flags MinBuildQty NormalBuildQty /], |
540
|
|
|
|
|
|
|
); |
541
|
|
|
|
|
|
|
name => [qw/ Code Name Contact Position Address1 Address2 Address3 Address4 Delivery1 Delivery2 Delivery3 Delivery4 Phone Fax Category1 Category2 Category3 Category4 CustomerType SupplierType DebtorTerms CreditorTerms Bank AccountName BankBranch TheirRef CreditLimit Discount Comment RecAccount PayAccount Colour Salesperson TaxCode PostCode State BankAccountNumber PaymentMethod DDI eMail Mobile AfterHours Contact2 Position2 DDI2 eMail2 Mobile2 AfterHours2 WebURL ProductPricing SplitAcct1 SplitAcct2 SplitPercent Hold UserNum UserText CustPromptPaymentTerms CustPromptPaymentDiscount SuppPromptPaymentTerms SuppPromptPaymentDiscount |
542
|
|
|
|
|
|
|
Currency CreditCardNum CreditCardExpiry CreditCardName TaxNumber Custom1 Custom2 Custom3 Custom4 DeliveryPostcode DeliveryState AddressCountry DeliveryCountry ReceiptMethod /], |
543
|
|
|
|
|
|
|
} |
544
|
|
|
|
|
|
|
|
545
|
|
|
|
|
|
|
=cut |
546
|
|
|
|
|
|
|
|
547
|
1
|
|
|
1
|
|
982
|
!!*!*!!*!*!!*!*!!*!*!!*!*!!*!*!!*!*!!*!*!!*!*!!*!*!!*!*!!*!*!!*!*!!*! |
|
1
|
|
|
|
|
748
|
|
|
1
|
|
|
|
|
50
|
|