File Coverage

blib/lib/XML/QOFQSF.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package XML::QOFQSF;
2 1     1   32474 use warnings;
  1         3  
  1         39  
3 1     1   7 use strict;
  1         3  
  1         37  
4 1     1   573 use XML::Simple;
  0            
  0            
5             use XML::Writer;
6             use IO::File;
7             use Class::Struct;
8             use Date::Parse;
9             use Date::Format;
10             use Math::BigInt;
11             use Data::Random qw(:all);
12             require Exporter;
13              
14             use vars qw (@ISA @EXPORT_OK);
15             @ISA = qw(Exporter);
16             @EXPORT_OK = qw(QSFParse QSFWrite);
17              
18             struct (Account => {
19             "desc" => '$',
20             "account_type" => '$',
21             "code" => '$',
22             "notes" => '$',
23             "name" => '$',
24             "guid" => '$',
25             "parent_account" => 'Account',
26             "tax_related_p" => '$',
27             "non_standard_scu" => '$',
28             "smallest_commodity_unit" => '$',
29             "balance" => '$',
30             "rec_bal" => '$',
31             "p_acc" => '$',
32             });
33              
34             struct (Trans => {
35             "desc" => '$',
36             "notes" => '$',
37             "num" => '$',
38             "guid" => '$',
39             "date_posted" => '$',
40             "date_entered" => '$',
41             "type" => '$',
42             "kvp_path" => '$',
43             "kvp_value" => '$',
44             "kvp_content" => '$',
45             });
46              
47             struct (Split => {
48             "action" => '$',
49             "memo" => '$',
50             "guid" => '$',
51             "account" => 'Account',
52             "trans" => 'Trans',
53             "share_price" => '$',
54             "amount" => '$',
55             "date_reconciled" => '$',
56             "reconcile_flag" => '$',
57             "s_acc" => '$',
58             "s_trans" => '$',
59             });
60              
61             struct (gncEntry => {
62             "discount_method" => '$',
63             "desc" => '$',
64             "action" => '$',
65             "notes" => '$',
66             "discount_type" => '$',
67             "guid" => '$',
68             "invoice_account" => 'Account',
69             "bill_to" => 'Account',
70             "invoice_taxable" => '$',
71             "billable" => '$',
72             "bill_tax_included" => '$',
73             "invoice_tax_included" => '$',
74             "bill_taxable" => '$',
75             "qty" => '$',
76             "bprice" => '$',
77             "iprice" => '$',
78             "date" => '$',
79             "date_entered" => '$',
80             "i_acc" => '$',
81             "b_acc" => '$',
82             });
83              
84             struct (gncAddress => {
85             "city" => '$',
86             "street" => '$',
87             "fax" => '$',
88             "number" => '$',
89             "name" => '$',
90             "email" => '$',
91             "locality" => '$',
92             "phone" => '$',
93             "guid" => '$',
94             "a_owner" => '$',
95             "owner" => 'gncCustomer',
96             });
97             # problem: owner could be more than one type.
98             struct (gncCustomer => {
99             "id" => '$',
100             "notes" => '$',
101             "name" => '$',
102             "guid" => '$',
103             "addr" => 'gncAddress',
104             "shipaddr" => 'gncAddress',
105             "active" => '$',
106             "tax_table_override" => '$',
107             "amount_of_discount" => '$',
108             "amount_of_credit" => '$',
109             "c_addr" => '$',
110             "c_shipaddr" => '$',
111             });
112              
113             struct (gncBillTerm => {
114             "description" => '$',
115             "name" => '$',
116             "bill_type" => '$',
117             "guid" => '$',
118             "amount_of_discount" => '$',
119             "cut_off" => '$',
120             "number_of_days_due" => '$',
121             "number_of_discounted_days" => '$',
122             });
123              
124             struct (gncInvoice => {
125             "id" => '$',
126             "billing_id" => '$',
127             "notes" => '$',
128             "guid" => '$',
129             "terms" => 'gncBillTerm',
130             "account" => 'Account',
131             "posted_txn" => 'Trans',
132             "list_of_entries" => 'gncEntry',
133             "active" => '$',
134             "date_posted" => '$',
135             "date_opened" => '$',
136             "i_terms" => '$',
137             "i_acc" => '$',
138             "i_posted" => '$',
139             "i_entries" => '@',
140             });
141              
142             struct (gncJob => {
143             "id" => '$',
144             "reference" => '$',
145             "name" => '$',
146             "guid" => '$',
147             "active" => '$',
148             });
149              
150             struct (Expense => {
151             "form_of_payment" => '$',
152             "distance_unit" => '$',
153             "expense_vendor" => '$',
154             "expense_city" => '$',
155             "expense_attendees" => '$',
156             "category" => '$',
157             "expense_note" => '$',
158             "type_of_expense" => '$',
159             "guid" => '$',
160             "expense_amount" => '$',
161             "expense_date" => '$',
162             "currency_code" => '$',
163             "kvp_mnemonic" => '$',
164             "kvp_string" => '$',
165             "kvp_fraction" => '$',
166             # end of external values.
167             # numeric handlers
168             "amt_numerator" => '$',
169             "amt_denominator" => '$',
170             # kvp handlers
171             "kvp_key" => '$',
172             "kvp_prefix" => '$',
173             });
174              
175             struct (Contact => {
176             "entryCity" => '$',
177             "entryCustom4" => '$',
178             "entryPhone1" => '$',
179             "entryZip" => '$',
180             "entryLastname" => '$',
181             "entryPhone2" => '$',
182             "entryNote" => '$',
183             "category" => '$',
184             "entryFirstname" => '$',
185             "entryPhone3" => '$',
186             "entryTitle" => '$',
187             "entryPhone4" => '$',
188             "entryCompany" => '$',
189             "entryPhone5" => '$',
190             "entryState" => '$',
191             "entryCustom1" => '$',
192             "entryAddress" => '$',
193             "entryCustom2" => '$',
194             "entryCountry" => '$',
195             "entryCustom3" => '$',
196             "guid" => '$',
197             });
198              
199             struct (Appointment => {
200             "category" => '$',
201             "note" => '$',
202             "repeat_type" => '$',
203             "description" => '$',
204             "advance_unit" => '$',
205             "repeat_day" => '$',
206             "repeat_week_start" => '$',
207             "guid" => '$',
208             "use_alarm" => '$',
209             "repeat_forever" => '$',
210             "transient_repeat" => '$',
211             "untimed_event" => '$',
212             "start_time" => '$',
213             "end_time" => '$',
214             "repeat_end" => '$',
215             "repeat_frequency" => '$',
216             "exception_count" => '$',
217             "alarm_advance" => '$',
218             });
219              
220             struct (ToDo => {
221             "todo_note" => '$',
222             "todo_description" => '$',
223             "category" => '$',
224             "guid" => '$',
225             "date_due" => '$',
226             "todo_priority" => '$',
227             "todo_complete" => '$',
228             "todo_length" => '$',
229             });
230              
231             # %objects is the meta-data: the sequence and type of data.
232             my %objects = ();
233              
234             # @foo_seq is the sequence of each field in the XML or database.
235             my @todo_seq = (
236             [ { 'todo_note' => 'string' } ],
237             [ { 'category' => 'string' } ],
238             [ { 'todo_description' => 'string' } ],
239             [ { 'guid' => 'guid' } ],
240             [ { 'date_due' => 'time' } ],
241             [ { 'todo_priority' => 'gint32' } ],
242             [ { 'todo_complete' => 'gint32' } ],
243             [ { 'todo_length' => 'gint32' } ],
244             );
245              
246             my @exp_seq = (
247             [ { 'form_of_payment' => 'string' } ],
248             [ { 'distance_unit' => 'string' } ],
249             [ { 'expense_vendor' => 'string' } ],
250             [ { 'expense_city' => 'string' } ],
251             [ { 'expense_attendees' => 'string' } ],
252             [ { 'category' => 'string' } ],
253             [ { 'expense_note' => 'string' } ],
254             [ { 'type_of_expense' => 'string' } ],
255             [ { 'guid' => 'guid' } ],
256             [ { 'expense_amount' => 'numeric' } ],
257             [ { 'expense_date' => 'time' } ],
258             [ { 'currency_code' => 'gint32' } ],
259             [ { 'kvp_mnemonic' => 'string' } ],
260             [ { 'kvp_string' => 'string' } ],
261             [ { 'kvp_fraction' => 'gint64' } ],
262             );
263              
264             my @app_seq = (
265             [ { 'category' => 'string' } ],
266             [ { 'note' => 'string' } ],
267             [ { 'repeat_type' => 'string' } ],
268             [ { 'description' => 'string' } ],
269             [ { 'advance_unit' => 'string' } ],
270             [ { 'repeat_day' => 'string' } ],
271             [ { 'repeat_week_start' => 'string' } ],
272             [ { 'guid' => 'guid' } ],
273             [ { 'use_alarm' => 'boolean' } ],
274             [ { 'repeat_forever' => 'boolean' } ],
275             [ { 'transient_repeat' => 'boolean' } ],
276             [ { 'untimed_event' => 'boolean' } ],
277             [ { 'start_time' => 'time' } ],
278             [ { 'end_time'=> 'time' } ],
279             [ { 'repeat_end' => 'time' } ],
280             [ { 'repeat_frequency' => 'gint32' } ],
281             [ { 'exception_count' => 'gint32' } ],
282             [ { 'alarm_advance' => 'gint32' } ],
283             );
284              
285             my @addr_seq = (
286             [ { 'entryCity' => 'string' } ],
287             [ { 'entryCustom4' => 'string' } ],
288             [ { 'entryPhone1' => 'string' } ],
289             [ { 'entryZip' => 'string' } ],
290             [ { 'entryLastname' => 'string' } ],
291             [ { 'entryPhone2' => 'string' } ],
292             [ { 'entryNote' => 'string' } ],
293             [ { 'category' => 'string' } ],
294             [ { 'entryFirstname' => 'string' } ],
295             [ { 'entryPhone3' => 'string' } ],
296             [ { 'entryTitle' => 'string' } ],
297             [ { 'entryPhone4' => 'string' } ],
298             [ { 'entryCompany' => 'string' } ],
299             [ { 'entryPhone5' => 'string' } ],
300             [ { 'entryState' => 'string' } ],
301             [ { 'entryCustom1' => 'string' } ],
302             [ { 'entryAddress' => 'string' } ],
303             [ { 'entryCustom2' => 'string' } ],
304             [ { 'entryCountry' => 'string' } ],
305             [ { 'entryCustom3' => 'string' } ],
306             [ { 'guid' => 'guid' } ],
307             );
308              
309             # todo : add the rest of the objects.
310              
311             $objects{'pilot_todo'} = \@todo_seq;
312             $objects{'pilot_address'} = \@addr_seq;
313             $objects{'pilot_datebook'} = \@app_seq;
314             $objects{'pilot_expenses'} = \@exp_seq;
315             $objects{'gpe_expenses'} = \@exp_seq;
316              
317             # %object_list is the instance data
318             my %object_list;
319             my (@expenses, @contacts, @appointments, @splits, @accounts,
320             @transactions, @gncinvoices, @gnccustomers, @gncbillterms,
321             @gncaddresses, @gncentries, @gncjobs, @todos );
322              
323             my $build = sub
324             {
325             my $doc = shift;
326             @expenses = @contacts = @appointments = @splits = @accounts = ();
327             @transactions = @gncinvoices = @gnccustomers = @gncbillterms = ();
328             @gncaddresses = @gncentries = @gncjobs = @todos = ();
329             foreach my $key (keys (%{$doc->{book}})){
330             next if ($key ne "object");
331             my @object = (@{$doc->{book}->{object}});
332             foreach my $g (@object)
333             {
334             if (($g->{type} eq 'pilot_expenses') or ($g->{type} eq 'gpe_expenses'))
335             {
336             my $e = new Expense;
337             my $strings = $g->{'string'};
338             foreach my $s (@$strings)
339             {
340             $e->form_of_payment ($s->{'content'})
341             if ($s->{'type'} eq 'form_of_payment');
342             $e->distance_unit ($s->{'content'})
343             if ($s->{'type'} eq 'distance_unit');
344             $e->expense_vendor ($s->{'content'})
345             if ($s->{'type'} eq 'expense_vendor');
346             $e->expense_city ($s->{'content'})
347             if ($s->{'type'} eq 'expense_city');
348             $e->expense_attendees ($s->{'content'})
349             if ($s->{'type'} eq 'expense_attendees');
350             $e->category ($s->{'content'})
351             if ($s->{'type'} eq 'category');
352             $e->expense_note ($s->{'content'})
353             if ($s->{'type'} eq 'expense_note');
354             $e->type_of_expense ($s->{'content'})
355             if ($s->{'type'} eq 'type_of_expense');
356             }
357             my $guids = $g->{'guid'};
358             foreach my $s (@$guids)
359             {
360             $e->guid ($s->{'content'})
361             if ($s->{'type'} eq 'guid');
362             }
363             $e->expense_amount(eval($g->{'numeric'}->{content}));
364             $e->expense_date(str2time($g->{'time'}->{content}));
365             $e->currency_code($g->{'gint32'}->{content});
366             my $kvps = $g->{'kvp'};
367             foreach my $s (@$kvps)
368             {
369             $e->kvp_mnemonic($s->{'content'})
370             if ($s->{'path'} eq 'expense/currency/mnemonic');
371             $e->kvp_string($s->{'content'})
372             if ($s->{'path'} eq 'expense/currency/symbol');
373             $e->kvp_fraction($s->{'content'})
374             if ($s->{'path'} eq 'expense/currency/fraction');
375             }
376             push @expenses, $e;
377             }
378             if ($g->{type} eq 'pilot_datebook')
379             {
380             my $d = new Appointment;
381             my $strings = $g->{'string'};
382             foreach my $s (@$strings)
383             {
384             $d->category ($s->{'content'})
385             if ($s->{'type'} eq 'category');
386             $d->note ($s->{'content'})
387             if ($s->{'type'} eq 'note');
388             $d->repeat_type ($s->{'content'})
389             if ($s->{'type'} eq 'repeat_type');
390             $d->description ($s->{'content'})
391             if ($s->{'type'} eq 'description');
392             $d->advance_unit ($s->{'content'})
393             if ($s->{'type'} eq 'advance_unit');
394             $d->repeat_day ($s->{'content'})
395             if ($s->{'type'} eq 'repeat_day');
396             $d->repeat_week_start ($s->{'content'})
397             if ($s->{'type'} eq 'repeat_week_start');
398             }
399             my $guids = $g->{'guid'};
400             foreach my $s (@$guids)
401             {
402             $d->guid ($s->{'content'})
403             if ($s->{'type'} eq 'guid');
404             }
405             my $booleans = $g->{'boolean'};
406             foreach my $s (@$booleans)
407             {
408             $d->use_alarm ($s->{'content'})
409             if ($s->{'type'} eq 'use_alarm');
410             $d->repeat_forever ($s->{'content'})
411             if ($s->{'type'} eq 'repeat_forever');
412             $d->transient_repeat ($s->{'content'})
413             if ($s->{'type'} eq 'transient_repeat');
414             $d->untimed_event ($s->{'content'})
415             if ($s->{'type'} eq 'untimed_event');
416             }
417             my $times = $g->{'time'};
418             foreach my $s (@$times)
419             {
420             $d->start_time (str2time($s->{content}))
421             if ($s->{'type'} eq 'start_time');
422             $d->end_time (str2time($s->{content}))
423             if ($s->{'type'} eq 'end_time');
424             $d->repeat_end (str2time($s->{content}))
425             if ($s->{'type'} eq 'repeat_end');
426             }
427             my $ints = $g->{'gint32'};
428             foreach my $s (@$ints)
429             {
430             $d->repeat_frequency ($s->{content})
431             if ($s->{'type'} eq 'repeat_frequency');
432             $d->exception_count ($s->{content})
433             if ($s->{'type'} eq 'exception_count');
434             $d->alarm_advance($s->{content})
435             if ($s->{'type'} eq 'alarm_advance');
436             }
437             push @appointments, $d;
438             }
439             if ($g->{type} eq 'pilot_address')
440             {
441             my $c = new Contact;
442             my $strings = $g->{'string'};
443             foreach my $s (@$strings)
444             {
445             $c->entryCity($s->{content})
446             if ($s->{'type'} eq 'entryCity');
447             $c->entryCustom4($s->{content})
448             if ($s->{'type'} eq 'entryCustom4');
449             $c->entryPhone1($s->{content})
450             if ($s->{'type'} eq 'entryPhone1');
451             $c->entryZip($s->{content})
452             if ($s->{'type'} eq 'entryZip');
453             $c->entryLastname($s->{content})
454             if ($s->{'type'} eq 'entryLastname');
455             $c->entryPhone2($s->{content})
456             if ($s->{'type'} eq 'entryPhone2');
457             $c->entryNote($s->{content})
458             if ($s->{'type'} eq 'entryNote');
459             $c->category($s->{content})
460             if ($s->{'type'} eq 'category');
461             $c->entryFirstname($s->{content})
462             if ($s->{'type'} eq 'entryFirstname');
463             $c->entryPhone3($s->{content})
464             if ($s->{'type'} eq 'entryPhone3');
465             $c->entryTitle($s->{content})
466             if ($s->{'type'} eq 'entryTitle');
467             $c->entryPhone4($s->{content})
468             if ($s->{'type'} eq 'entryPhone4');
469             $c->entryCompany($s->{content})
470             if ($s->{'type'} eq 'entryCompany');
471             $c->entryPhone5($s->{content})
472             if ($s->{'type'} eq 'entryPhone5');
473             $c->entryState($s->{content})
474             if ($s->{'type'} eq 'entryState');
475             $c->entryCustom1($s->{content})
476             if ($s->{'type'} eq 'entryCustom1');
477             $c->entryAddress($s->{content})
478             if ($s->{'type'} eq 'entryAddress');
479             $c->entryCustom2($s->{content})
480             if ($s->{'type'} eq 'entryCustom2');
481             $c->entryCountry($s->{content})
482             if ($s->{'type'} eq 'entryCountry');
483             $c->entryCustom3($s->{content})
484             if ($s->{'type'} eq 'entryCustom3');
485             }
486             my $guids = $g->{'guid'};
487             foreach my $s (@$guids)
488             {
489             $c->guid ($s->{'content'})
490             if ($s->{'type'} eq 'guid');
491             }
492             push @contacts, $c;
493             }
494             if ($g->{'type'} eq 'pilot_todo')
495             {
496             my $t = new ToDo;
497             my $strings = $g->{'string'};
498             foreach my $s (@$strings)
499             {
500             $t->todo_note($s->{content})
501             if ($s->{'type'} eq 'todo_note');
502             $t->todo_description($s->{content})
503             if ($s->{'type'} eq 'todo_description');
504             $t->category($s->{content})
505             if ($s->{'type'} eq 'category');
506             }
507             my $guids = $g->{'guid'};
508             foreach my $s (@$guids)
509             {
510             $t->guid ($s->{'content'})
511             if ($s->{'type'} eq 'guid');
512             }
513             $t->date_due(str2time($g->{'time'}->{content}));
514             my $ints = $g->{'gint32'};
515             foreach my $s (@$ints)
516             {
517             $t->todo_priority($s->{content})
518             if ($s->{'type'} eq 'todo_priority');
519             $t->todo_complete($s->{content})
520             if ($s->{'type'} eq 'todo_complete');
521             $t->todo_length($s->{content})
522             if ($s->{'type'} eq 'todo_length');
523             }
524             push @todos, $t;
525             }
526             if ($g->{type} eq 'Trans')
527             {
528             my $t = new Trans;
529             my $strings = $g->{'string'};
530             foreach my $s (@$strings)
531             {
532             $t->desc($s->{content})
533             if ($s->{'type'} eq 'desc');
534             $t->notes($s->{content})
535             if ($s->{'type'} eq 'notes');
536             $t->num($s->{content})
537             if ($s->{'type'} eq 'num');
538             }
539             my $guids = $g->{'guid'};
540             foreach my $s (@$guids)
541             {
542             $t->guid ($s->{'content'})
543             if ($s->{'type'} eq 'guid');
544             }
545             my $times = $g->{'time'};
546             foreach my $s (@$times)
547             {
548             $t->date_posted(str2time($s->{content}))
549             if ($s->{'type'} eq 'date_posted');
550             $t->date_entered(str2time($s->{content}))
551             if ($s->{'type'} eq 'date_entered');
552             }
553             $t->type($g->{'character'}->{content});
554             $t->kvp_path($g->{'kvp'}->{path});
555             $t->kvp_value($g->{'kvp'}->{value});
556             $t->kvp_content($g->{'kvp'}->{content});
557             push @transactions, $t;
558             }
559             if ($g->{type} eq 'Account')
560             {
561             my $a = new Account;
562             my $strings = $g->{'string'};
563             foreach my $s (@$strings)
564             {
565             $a->desc($s->{content})
566             if ($s->{'type'} eq 'desc');
567             $a->account_type($s->{content})
568             if ($s->{'type'} eq 'account_type');
569             $a->code($s->{content})
570             if ($s->{'type'} eq 'code');
571             $a->notes($s->{content})
572             if ($s->{'type'} eq 'notes');
573             $a->name($s->{content})
574             if ($s->{'type'} eq 'name');
575             }
576             my $check = @{$g->{'guid'}};
577             if ($check == 1)
578             {
579             $a->guid($g->{'guid'}->[0]->{content});
580             }
581             else
582             {
583             $a->guid($g->{'guid'}->[0]->{content});
584             $a->p_acc($g->{'guid'}->[1]->{content});
585             }
586             $a->tax_related_p($g->{'boolean'}->[0]->{content});
587             $a->non_standard_scu($g->{'boolean'}->[1]->{content});
588             $a->smallest_commodity_unit($g->{'gint32'}->{content});
589             push @accounts, $a;
590             }
591             if ($g->{type} eq 'Split')
592             {
593             my $s = new Split;
594             if ($g->{'string'}->[0]->{type} eq 'action') {
595             $s->action($g->{'string'}->[0]->{content});
596             $s->memo($g->{'string'}->[1]->{content});
597             }
598             else {
599             $s->action($g->{'string'}->[1]->{content});
600             $s->memo($g->{'string'}->[0]->{content});
601             }
602             if ($g->{'numeric'}->[0]->{type} eq 'share-price') {
603             # TODO: recreate the numeric for QSFWrite?
604             $s->share_price(eval($g->{'numeric'}->[0]->{content}));
605             $s->amount(eval($g->{'numeric'}->[1]->{content}));
606             }
607             else {
608             $s->share_price(eval($g->{'numeric'}->[1]->{content}));
609             $s->memo(eval($g->{'numeric'}->[0]->{content}));
610             }
611             $s->date_reconciled(str2time($g->{'date'}->{content}));
612             $s->reconcile_flag($g->{'character'}->{content});
613             if ($g->{'guid'}->[0]->{type} eq 'guid') {
614             $s->guid($g->{'guid'}->[0]->{content});
615             $s->s_acc($g->{'guid'}->[1]->{content});
616             $s->s_trans($g->{'guid'}->[2]->{content});
617             }
618             if ($g->{'guid'}->[1]->{type} eq 'guid') {
619             $s->guid($g->{'guid'}->[1]->{content});
620             $s->s_acc($g->{'guid'}->[0]->{content});
621             $s->s_trans($g->{'guid'}->[2]->{content});
622             }
623             if ($g->{'guid'}->[2]->{type} eq 'guid') {
624             $s->guid($g->{'guid'}->[2]->{content});
625             $s->s_acc($g->{'guid'}->[1]->{content});
626             $s->s_trans($g->{'guid'}->[0]->{content});
627             }
628             if ($g->{'guid'}->[0]->{type} eq 'account') {
629             $s->guid($g->{'guid'}->[2]->{content});
630             $s->s_acc($g->{'guid'}->[0]->{content});
631             $s->s_trans($g->{'guid'}->[1]->{content});
632             }
633             push @splits, $s;
634             }
635             if ($g->{type} eq 'gncEntry')
636             {
637             my $ge = new gncEntry;
638             $ge->discount_method($g->{'string'}->[0]->{content});
639             $ge->desc($g->{'string'}->[1]->{content});
640             $ge->action($g->{'string'}->[2]->{content});
641             $ge->notes($g->{'string'}->[3]->{content});
642             $ge->discount_type($g->{'string'}->[4]->{content});
643             $ge->guid($g->{'guid'}->[0]->{content});
644             $ge->i_acc($g->{'guid'}->[1]->{content});
645             $ge->b_acc($g->{'guid'}->[2]->{content});
646             $ge->invoice_taxable($g->{'boolean'}->[0]->{content});
647             $ge->billable($g->{'boolean'}->[1]->{content});
648             $ge->bill_tax_included($g->{'boolean'}->[2]->{content});
649             $ge->invoice_tax_included($g->{'boolean'}->[3]->{content});
650             $ge->bill_taxable($g->{'boolean'}->[4]->{content});
651             $ge->qty(eval($g->{'numeric'}->[0]->{content}));
652             $ge->bprice(eval($g->{'numeric'}->[1]->{content}));
653             $ge->iprice(eval($g->{'numeric'}->[2]->{content}));
654             $ge->date(str2time($g->{'time'}->[0]->{content}));
655             $ge->date_entered(str2time($g->{'time'}->[1]->{content}));
656             push @gncentries, $ge;
657             }
658             if ($g->{type} eq 'gncInvoice')
659             {
660             my $gi = new gncInvoice;
661             $gi->id($g->{'string'}->[0]->{content});
662             $gi->billing_id($g->{'string'}->[1]->{content});
663             $gi->notes($g->{'string'}->[2]->{content});
664             $gi->guid($g->{'guid'}->[0]->{content});
665             $gi->i_terms($g->{'guid'}->[1]->{content});
666             $gi->i_acc($g->{'guid'}->[2]->{content});
667             $gi->i_posted($g->{'guid'}->[3]->{content});
668             # list of entries is incomplete in the upstream QOF code.
669             # $gi->i_entries($g->{'guid'}->[4]->{content});
670             $gi->active($g->{'boolean'}->{content});
671             $gi->date_posted(str2time($g->{'date'}->[0]->{content}));
672             $gi->date_opened(str2time($g->{'date'}->[1]->{content}));
673             push @gncinvoices, $gi;
674             }
675             if ($g->{type} eq 'gncBillTerm')
676             {
677             my $gbt = new gncBillTerm;
678             $gbt->description($g->{'string'}->[0]->{content});
679             $gbt->name($g->{'string'}->[1]->{content});
680             $gbt->bill_type($g->{'string'}->[2]->{content});
681             $gbt->guid($g->{'guid'}->[0]->{content});
682             $gbt->amount_of_discount(eval($g->{'numeric'}->{content}));
683             $gbt->cut_off($g->{'gint32'}->[0]->{content});
684             $gbt->number_of_days_due($g->{'gint32'}->[1]->{content});
685             $gbt->number_of_discounted_days($g->{'gint32'}->[2]->{content});
686             push @gncbillterms, $gbt;
687             }
688             if ($g->{type} eq 'gncJob')
689             {
690             my $gj = new gncJob;
691             $gj->id($g->{'string'}->[0]->{content});
692             $gj->reference($g->{'string'}->[1]->{content});
693             $gj->name($g->{'string'}->[2]->{content});
694             $gj->guid($g->{'guid'}->[0]->{content});
695             $gj->active($g->{'boolean'}->{content});
696             # need an owner record
697             push @gncjobs, $gj;
698             }
699             if ($g->{type} eq 'gncCustomer')
700             {
701             my $gc = new gncCustomer;
702             $gc->id($g->{'string'}->[0]->{content});
703             $gc->notes($g->{'string'}->[1]->{content});
704             $gc->name($g->{'string'}->[2]->{content});
705             $gc->guid($g->{'guid'}->[0]->{content});
706             $gc->c_addr($g->{'guid'}->[1]->{content});
707             $gc->c_shipaddr($g->{'guid'}->[2]->{content});
708             $gc->active($g->{'boolean'}->[0]->{content});
709             $gc->tax_table_override($g->{'boolean'}->[1]->{content});
710             $gc->amount_of_discount(eval($g->{'numeric'}->[0]->{content}));
711             $gc->amount_of_credit(eval($g->{'numeric'}->[1]->{content}));
712             push @gnccustomers, $gc;
713             }
714             if ($g->{type} eq 'gncAddress')
715             {
716             my $ga = new gncAddress;
717             $ga->city($g->{'string'}->[0]->{content});
718             $ga->street($g->{'string'}->[1]->{content});
719             $ga->fax($g->{'string'}->[2]->{content});
720             $ga->number($g->{'string'}->[3]->{content});
721             $ga->name($g->{'string'}->[4]->{content});
722             $ga->email($g->{'string'}->[5]->{content});
723             $ga->locality($g->{'string'}->[6]->{content});
724             $ga->phone($g->{'string'}->[7]->{content});
725             $ga->guid($g->{'guid'}->[0]->{content});
726             $ga->a_owner($g->{'guid'}->[1]->{content});
727             push @gncaddresses, $ga;
728             }
729             }
730             }
731             # now cross-reference the guids
732             foreach my $splt (@splits)
733             {
734             foreach my $t (@transactions)
735             {
736             $splt->trans($t) if ($t->guid eq $splt->s_trans);
737             }
738             foreach my $a (@accounts)
739             {
740             $splt->account($a) if ($a->guid eq $splt->s_acc);
741             }
742             }
743             foreach my $t (@accounts)
744             {
745             foreach my $a (@accounts)
746             {
747             next if (!$t->p_acc);
748             $t->parent_account($a) if ($a->guid eq $t->p_acc);
749             }
750             }
751             foreach my $i (@gncinvoices)
752             {
753             foreach my $a (@accounts)
754             {
755             next if (!$i->i_acc);
756             $i->account($a) if ($a->guid eq $i->i_acc);
757             }
758             foreach my $t (@transactions)
759             {
760             $i->posted_txn($t) if ($t->guid eq $i->i_posted);
761             }
762             foreach my $b (@gncbillterms)
763             {
764             next if (!$i->i_terms);
765             $i->terms($b) if ($b->guid eq $i->i_terms);
766             }
767             # handle list of entries
768             }
769             foreach my $e (@gncentries)
770             {
771             foreach my $a (@accounts)
772             {
773             # $e->invoice_account($a) if ($a->guid eq $e->i_acc);
774             # $e->bill_to($a) if ($a->guid eq $e->b_acc);
775             }
776             }
777             foreach my $c (@gnccustomers)
778             {
779             foreach my $a (@gncaddresses)
780             {
781             $c->addr($a) if ($a->guid eq $c->c_addr);
782             $c->shipaddr($a) if ($a->guid eq $c->c_shipaddr);
783             # $a->owner($c) if ($c->guid eq $a->a_owner);
784             }
785             }
786             };
787              
788             my $guid = sub
789             {
790             my @random_chars = rand_chars( set => 'numeric', min => 8, max => 8, shuffle => 1 );
791             my $r = join("", @random_chars);
792             @random_chars = rand_chars( set => 'numeric', min => 8, max => 8, shuffle => 1 );
793             $r .= join("", @random_chars);
794             @random_chars = rand_chars( set => 'numeric', min => 8, max => 8, shuffle => 1 );
795             $r .= join("", @random_chars);
796             @random_chars = rand_chars( set => 'numeric', min => 8, max => 8, shuffle => 1 );
797             $r .= join("", @random_chars);
798             @random_chars = rand_chars( set => 'numeric', min => 8, max => 8, shuffle => 1 );
799             $r .= join("", @random_chars);
800             my $x = Math::BigInt->new("$r");
801             my $g = $x->as_hex();
802             $g =~ s/^0x//;
803             $g =~ /([0-9a-f]{32})/;
804             return $1;
805             };
806              
807             =head1 NAME
808              
809             XML::QOFQSF - convert personal data to and from QSF XML files
810              
811             Support for the QOF SQLite backend will be added in a separate module in due course.
812              
813             =head1 VERSION
814              
815             Version 0.05
816              
817             =cut
818              
819             our $VERSION = '0.05';
820              
821             =head1 SYNOPSIS
822              
823             Provides a single home for all QOF objects expressed as QSF XML. A similar
824             module for the SQLite backend is also planned. To have your QOF object included,
825             simply send me a sample QSF XML file. A script to create the content is also planned.
826              
827             A little code snippet.
828              
829             use XML::QOFQSF qw(QSFParse QSFWrite);
830             use Date::Parse;
831             use Date::Format;
832              
833             my $file = "qsf-mileage.xml";
834             my %obj = QSFParse("$file");
835             my $expenses = $obj{'pilot_expenses'};
836              
837             my $exp_count = @$expenses;
838             print "Status: $exp_count expenses\n";
839             my $template = "%A, %o %B %Y";
840             my $total_miles = 0;
841             foreach my $a (@$expenses)
842             {
843             if ($a->type_of_expense eq "Mileage")
844             {
845             $total_miles += $a->expense_amount;
846             print $a->expense_amount . " " . $a->distance_unit . " : ";
847             print $a->expense_vendor . " " . $a->expense_city;
848             print " on " . time2str($template, $a->expense_date) . "\n";
849             }
850             }
851              
852             print "Total: $total_miles\n";
853              
854             my $c = new Appointment;
855             $c->description("short summary");
856             $c->guid($guid_str); # QOF identifier like 5429307bcae611b59b4e2bedc77b2d68
857             $c->note("long description");
858             $c->repeat_type("repeatNone"); # dictated by pilot-qof source
859             $c->repeat_forever("false"); # boolean
860             $c->use_alarm("false"); # boolean
861             $c->untimed_event("false"); #boolean
862             $c->transient_repeat("false");
863             $c->start_time("2003-08-08T09:00:00Z");
864             $c->end_time("2003-08-08T17:30:00Z");
865             $c->repeat_end("2003-08-08T17:30:00Z");
866             $c->repeat_frequency(0);
867             $c->exception_count(0);
868             $c->alarm_advance(0);
869              
870             my %obj;
871             my @datebook=();
872             push @datebook, $c;
873              
874             $obj{'pilot_datebook'} = \@datebook;
875              
876             QSFWrite(\%obj);
877              
878             =head1 EXPORT
879              
880             XML::QOFQSF exports two functions, QSFParse to parse a QSF XML file and
881             QSFWrite to write data to a new QSF XML file. QSFParse reads data from
882             the file into an array of objects of each supported type and references
883             to each array are added to the object_list hash using the object name
884             as the key. A similar hash can be passed to QSFWrite to generate a new
885             QSF XML file.
886              
887             =head1 Query Object Framework (QOF)
888              
889             QOF, the Query Object Framework, provides a set of C Language utilities
890             for performing generic structured complex queries on a set of data held
891             by a set of C/C++ objects. This framework is unique in that it does NOT
892             require SQL or any database at all to perform the query. Thus, it allows
893             programmers to add query support to their applications without having to
894             hook into an SQL Database.
895              
896             Typically, if you have an app, and you want to add the ability to show
897             a set of reports, you will need the ability to perform queries in order
898             to get the data you need to show a report. Of course, you can always
899             write a set of ad-hoc subroutines to return the data that you need. But
900             this kind of a programming style is not extensible: just wait till you
901             get a user who wants a slightly different report.
902              
903             The idea behind QOF is to provide a generic framework so that any query
904             can be executed, including queries designed by the end-user. Normally,
905             this is possible only if you use a database that supports SQL, and then
906             only if you deeply embed the database into your application. QOF provides
907             simpler, more natural way to work with objects.
908              
909             XML::QOFQSF extends this functionality to provide a simple, scriptable,
910             interface to QOF data. When combined with the SQL-type queries supported
911             by the QOF application, this provides a flexible method for handling,
912             organising, converting and synchronising all kinds of compatible data.
913              
914             Currently, QOF applications are based around PIM data (Personal Information
915             Management) like contacts, calendar, expenses and todo lists. QOF is also
916             an integral part of GnuCash and support for financial objects is pending
917             (when the cashutil application is stable). In theory, QOF can be used with
918             any kind of data that can be expressed in the variables available.
919              
920             =head1 OBJECTS
921              
922             pilot-qof objects (pilot_address, pilot_expenses, pilot_datebook
923             and pilot_todo) are supported. gpe-expenses is also supported. Outline
924             support is included for cashutil objects but as cashutil is currently
925             unreleased, full support is pending.
926              
927             XML::QOFQSF objects are identical to the Query Object Framework (QOF)
928             Objects used by applications like pilot-qof. The module is not intended to
929             be a perl binding of any kind, merely a data conduit that understands the
930             various elements of a QOF Object.
931              
932             L
933              
934             L
935              
936             L
937              
938             L
939              
940             The Perl structs follow the QSF XML tag names and XML::QOFQSF expects values
941             that match the underlying QOF object - as output by QSF XML. Variable names
942             must therefore comply with XML rules for attribute values and with Perl syntax
943             rules - e.g. foo-bar is invalid, use foo_bar instead. The same variable names
944             are also used as field names in SQLite.
945              
946             Detailed information on QSF variables:
947             L
948              
949             =over 2
950              
951             =item *
952              
953             B : any valid XML/Perl string.
954              
955             B : Original QOF GUID, if preserved, otherwise blank for new.
956              
957             B : valid XML boolean, true or false.
958              
959             B : Not fully supported in XML::QOFQSF yet, these are precise
960             numerical variables expressed as a numerator and denominator: 599/100
961             = 5.99 or 5456321/148547 = 36.73127697 - see QOF for more information.
962              
963             B
964             Coordinated Universal Time (UTC) syntax to be timezone independent. This
965             generates timestamps of the form: 2004-11-29T19:15:34Z - you can reproduce
966             the same timestamps with time2str from Date::Format using the template:
967             C<"%Y-%m-%dT%H:%M:%SZ"> or with the GNU C Library formatting string
968             C<%Y-%m-%dT%H:%M:%SZ> - remember to use gmtime() NOT localtime()!. From
969             the command line, use the -u switch with the date command:
970             C. Note that the QOF library can deal with
971             dates far outside the range of GNU C, date and many Perl modules because
972             dates are handled as 64bit signed values.
973              
974             B : 32bit integer
975              
976             B : 64bit integer
977              
978             B : Unused in XML::QOFQSF (no objects currently use it).
979              
980             B : Single character field.
981              
982             B : Key-value pairs. Incomplete support in XML::QOFQSF so far.
983              
984             =back
985              
986             C<
987             struct (ToDo =E {
988             "todo_note" =E '$',
989             "todo_description" =E '$',
990             "category" =E '$',
991             "guid" =E '$',
992             "date_due" =E '$',
993             "todo_priority" =E '$',
994             "todo_complete" =E '$',
995             "todo_length" =E '$',
996             });
997             >
998              
999             C<
1000             Eobject type="pilot_todo" count="1"E
1001             Estring type="todo_note"/E
1002             Estring type="todo_description"Eshort summaryE/stringE
1003             Estring type="category"EBusinessE/stringE
1004             Eguid type="guid"Ea018fa4d88d7439bbe0c94978ba78c82E/guidE
1005             Etime type="date_due"E2005-07-27T00:00:00ZE/timeE
1006             Egint32 type="todo_priority"E1E/gint32E
1007             Egint32 type="todo_complete"E1E/gint32E
1008             Egint32 type="todo_length"E0E/gint32E
1009             E/objectE
1010             >
1011              
1012             QOF Objects can also reference other QOF Objects via the GUID -
1013             support for this functionality is pending in XML::QOFQSF.
1014              
1015             =head1 QOF GUID
1016              
1017             QOF uses 128bit identifiers expressed as 32bit hexadecimal strings. Where these
1018             strings are preserved in the converted data (e.g. as UID fields), XML::QOFQSF
1019             can use these strings to recreate the original GUID. This can be used to retain
1020             data integrity by uniquely identifying instances across formats. If a GUID is
1021             lost for any reason, XML::QOFQSF will create a new GUID - methods exist in the
1022             main QOF library (written in C) to merge such instances into other datasets.
1023              
1024             =head1 SUPPORT
1025              
1026             For detailed support on QOF Objects, QSF XML, XML::QOFQSF or datafreedom-perl,
1027             please use the QOF-devel mailing list:
1028              
1029             L
1030              
1031             =head1 SCRIPTS
1032              
1033             XML::QOFQSF was written to support the 'datafreedom' scripts developed
1034             for 'pilot-qof' which will probably become a package in their own right.
1035              
1036             L
1037              
1038             Examples:
1039             New scripts are continually being added to the datafreedom packages. Some
1040             existing scripts packaged with F include:
1041              
1042             =over 2
1043              
1044             =item *
1045              
1046             B : parse a QSF XML file and prepare a simple invoice
1047              
1048             F I<-x data.xml --invoice-city -t 2006-11-09> | F -
1049              
1050             =item *
1051              
1052             B : parse an iCal file and create a QSF XML pilot_datebook instance
1053              
1054             F F
1055              
1056             =back
1057              
1058             =head1 FUNCTIONS
1059              
1060             =head2 QSFParse
1061              
1062             Passed a QSF XML filename (or '-' for stdin), returns a hash of array references,
1063             indexed by the name of the objects found in the QSF XML file.
1064              
1065             =cut
1066              
1067             sub QSFParse {
1068             my $file = $_[0];
1069             my $xs1 = XML::Simple->new();
1070             my $doc = $xs1->XMLin($file, forcearray => [ 'guid' ]);
1071             $build->($doc);
1072             $object_list{'pilot_address'} = \@contacts;
1073             $object_list{'pilot_expenses'} = \@expenses;
1074             $object_list{'gpe_expenses'} = \@expenses;
1075             $object_list{'pilot_datebook'} = \@appointments;
1076             $object_list{'pilot_todo'} = \@todos;
1077             $object_list{'Split'} = \@splits;
1078             $object_list{'Account'} = \@accounts;
1079             $object_list{'Trans'} = \@transactions;
1080             $object_list{'gncBillTerm'} = \@gncbillterms;
1081             $object_list{'gncInvoice'} = \@gncinvoices;
1082             $object_list{'gncEntry'} = \@gncentries;
1083             $object_list{'gncAddress'} = \@gncaddresses;
1084             $object_list{'gncCustomer'} = \@gnccustomers;
1085             $object_list{'gncJob'} = \@gncjobs;
1086             return %object_list;
1087             }
1088              
1089             =head2 QSFWrite
1090              
1091             Passed a hash of array references containing QOF objects. The hash must be indexed by
1092             the name of the objects. Use QSFParse to obtain an example hash. Prints the XML to stdout.
1093              
1094             =cut
1095              
1096             sub QSFWrite
1097             {
1098             my $output = new IO::File("");
1099             my $qofns = "http://qof.sourceforge.net/";
1100             my $writer = new XML::Writer(NAMESPACES => 1, OUTPUT => $output, DATA_MODE => 1,
1101             DATA_INDENT => 2, PREFIX_MAP => {$qofns => ''}, ENCODING => 'UTF-8');
1102             $writer->xmlDecl();
1103             $writer->startTag([$qofns, "qof-qsf"]);
1104             $writer->startTag("book", "count" => '1');
1105             $writer->startTag("book-guid");
1106             my $guid_str = $guid->();
1107             $writer->characters("$guid_str");
1108             $writer->endTag("book-guid");
1109             my $count = 0;
1110             my ($obj)=@_;
1111             # foreach type of object
1112             foreach my $type (keys %$obj)
1113             {
1114             my $dataset = $$obj{$type};
1115             # foreach instance of one type of object
1116             foreach my $data (@$dataset)
1117             {
1118             $count++;
1119             $writer->startTag("object", 'type' => $type, 'count' => "$count");
1120             my $sequence = $objects{$type};
1121             # read the parameters in a set sequence
1122             foreach my $sequence_hash (@$sequence)
1123             {
1124             # each sequence hash and field hash only has one entry.
1125             my $field_hash = $sequence_hash->[0];
1126             my $field = (keys (%$field_hash))[0];
1127             my $param = $$field_hash{$field};
1128             # handle guid specially - generate one if not found in original.
1129             if ($field eq 'guid')
1130             {
1131             $writer->startTag($param, 'type' => $field);
1132             if ($data->$field) { $writer->characters($data->$field); }
1133             else {
1134             my $guid_str = $guid->();
1135             $writer->characters("$guid_str");
1136             }
1137             $writer->endTag('guid');
1138             next;
1139             }
1140             if (defined $data->$field)
1141             {
1142             $writer->startTag($param, 'type' => $field);
1143             $writer->characters($data->$field);
1144             $writer->endTag($param);
1145             }
1146             else
1147             {
1148             $writer->emptyTag($param, 'type' => $field);
1149             }
1150             }
1151             $writer->endTag('object');
1152             }
1153             }
1154             $writer->endTag("book");
1155             $writer->endTag("qof-qsf");
1156             $writer->end();
1157             }
1158              
1159             =head1 AUTHOR
1160              
1161             Neil Williams, C<< >>
1162              
1163             =head1 BUGS
1164              
1165             Please report any bugs or feature requests to
1166             C, or through the web interface at
1167             L.
1168             I will be notified, and then you'll automatically be notified of progress on
1169             your bug as I make changes.
1170              
1171             =head1 SUPPORT
1172              
1173             You can find documentation for this module with the perldoc command.
1174              
1175             perldoc XML::QOFQSF
1176              
1177             You can also look for information at:
1178              
1179             =over 4
1180              
1181             =item * AnnoCPAN: Annotated CPAN documentation
1182              
1183             L
1184              
1185             =item * CPAN Ratings
1186              
1187             L
1188              
1189             =item * RT: CPAN's request tracker
1190              
1191             L
1192              
1193             =item * Search CPAN
1194              
1195             L
1196              
1197             =back
1198              
1199             =head1 COPYRIGHT & LICENSE
1200              
1201             Copyright 2007 Neil Williams.
1202              
1203             This program is free software; you can redistribute it and/or modify
1204             it under the terms of the GNU General Public License as published by
1205             the Free Software Foundation; either version 2 of the License, or
1206             (at your option) any later version.
1207              
1208             This program is distributed in the hope that it will be useful,
1209             but WITHOUT ANY WARRANTY; without even the implied warranty of
1210             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1211             GNU General Public License for more details.
1212              
1213             You should have received a copy of the GNU General Public License
1214             along with this program; if not, write to the Free Software
1215             Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1216              
1217             =cut
1218              
1219             1; # End of XML::QOFQSF