Bug 11243: make vendor list distinguish between active and canceled items
[koha.git] / C4 / Creators / Lib.pm
1 package C4::Creators::Lib;
2
3 # Copyright 2009 Foundations Bible College.
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 use strict;
21 use warnings;
22
23 use autouse 'Data::Dumper' => qw(Dumper);
24
25 use C4::Context;
26 use C4::Debug;
27
28 BEGIN {
29     use version; our $VERSION = qv('3.07.00.049');
30     use base qw(Exporter);
31     our @EXPORT = qw(get_all_templates
32                      get_all_layouts
33                      get_all_profiles
34                      get_all_image_names
35                      get_batch_summary
36                      get_label_summary
37                      get_card_summary
38                      get_barcode_types
39                      get_label_types
40                      get_font_types
41                      get_text_justification_types
42                      get_output_formats
43                      get_column_names
44                      get_table_names
45                      get_unit_values
46                      html_table
47     );
48 }
49
50 =head1 NAME
51
52 C4::Creators::Lib
53
54 =cut
55
56 =head1 FUNCTIONS
57
58 =cut
59
60 #=head2 C4::Creators::Lib::_SELECT()
61 #
62 #    This function returns a recordset upon success and 1 upon failure. Errors are logged to the Apache log.
63 #
64 #    examples:
65 #
66 #        my $field_value = _SELECT(field_name, table_name, condition);
67 #
68 #=cut
69
70 sub _SELECT {
71     my @params = @_;
72     my $query = "SELECT $params[0] FROM $params[1]";
73     $params[2] ? $query .= " WHERE $params[2];" : $query .= ';';
74     my $sth = C4::Context->dbh->prepare($query);
75 #    $sth->{'TraceLevel'} = 3;
76     $sth->execute();
77     if ($sth->err) {
78         warn sprintf('Database returned the following error: %s', $sth->errstr);
79         return 1;
80     }
81     my $record_set = [];
82     while (my $row = $sth->fetchrow_hashref()) {
83         push(@$record_set, $row);
84     }
85     return $record_set;
86 }
87
88 my $barcode_types = [
89     {type => 'CODE39',          name => 'Code 39',              desc => 'Translates the characters 0-9, A-Z, \'-\', \'*\', \'+\', \'$\', \'%\', \'/\', \'.\' and \' \' to a barcode pattern.',                                  selected => 0},
90     {type => 'CODE39MOD',       name => 'Code 39 + Modulo43',   desc => 'Translates the characters 0-9, A-Z, \'-\', \'*\', \'+\', \'$\', \'%\', \'/\', \'.\' and \' \' to a barcode pattern. Encodes Mod 43 checksum.',         selected => 0},
91     {type => 'CODE39MOD10',     name => 'Code 39 + Modulo10',   desc => 'Translates the characters 0-9, A-Z, \'-\', \'*\', \'+\', \'$\', \'%\', \'/\', \'.\' and \' \' to a barcode pattern. Encodes Mod 10 checksum.',         selected => 0},
92     {type => 'COOP2OF5',        name => 'COOP2of5',             desc => 'Creates COOP2of5 barcodes from a string consisting of the numeric characters 0-9',                                                                     selected => 0},
93     {type => 'EAN13',           name => 'EAN13',                desc => 'Creates EAN13 barcodes from a string of 12 or 13 digits. The check number (the 13:th digit) is calculated if not supplied.',                           selected => 0},
94 #    {type => 'EAN8',            name => 'EAN8',                 desc => 'Translates a string of 7 or 8 digits to EAN8 barcodes. The check number (the 8:th digit) is calculated if not supplied.',                              selected => 0},
95 #    {type => 'IATA2of5',        name => 'IATA2of5',             desc => 'Creates IATA2of5 barcodes from a string consisting of the numeric characters 0-9',                                                                     selected => 0},
96     {type => 'INDUSTRIAL2OF5',  name => 'Industrial2of5',       desc => 'Creates Industrial2of5 barcodes from a string consisting of the numeric characters 0-9',                                                               selected => 0},
97 #    {type => 'ITF',             name => 'Interleaved2of5',      desc => 'Translates the characters 0-9 to a barcodes. These barcodes could also be called 'Interleaved2of5'.',                                                  selected => 0},
98 #    {type => 'MATRIX2OF5',      name => 'Matrix2of5',           desc => 'Creates Matrix2of5 barcodes from a string consisting of the numeric characters 0-9',                                                                   selected => 0},
99 #    {type => 'NW7',             name => 'NW7',                  desc => 'Creates a NW7 barcodes from a string consisting of the numeric characters 0-9',                                                                        selected => 0},
100 #    {type => 'UPCA',            name => 'UPCA',                 desc => 'Translates a string of 11 or 12 digits to UPCA barcodes. The check number (the 12:th digit) is calculated if not supplied.',                           selected => 0},
101 #    {type => 'UPCE',            name => 'UPCE',                 desc => 'Translates a string of 6, 7 or 8 digits to UPCE barcodes. If the string is 6 digits long, '0' is added first in the string. The check number (the 8:th digit) is calculated if not supplied.',                                 selected => 0},
102 ];
103
104 my $label_types = [
105     {type => 'BIB',     name => 'Biblio',               desc => 'Only the bibliographic data is printed.',                              selected => 0},
106     {type => 'BARBIB',  name => 'Barcode/Biblio',       desc => 'Barcode proceeds bibliographic data.',                                 selected => 0},
107     {type => 'BIBBAR',  name => 'Biblio/Barcode',       desc => 'Bibliographic data proceeds barcode.',                                 selected => 0},
108     {type => 'ALT',     name => 'Alternating',          desc => 'Barcode and bibliographic data are printed on alternating labels.',    selected => 0},
109     {type => 'BAR',     name => 'Barcode',              desc => 'Only the barcode is printed.',                                         selected => 0},
110 ];
111
112 my $font_types = [
113     {type => 'TR',      name => 'Times-Roman',                  selected => 0},
114     {type => 'TB',      name => 'Times-Bold',                   selected => 0},
115     {type => 'TI',      name => 'Times-Italic',                 selected => 0},
116     {type => 'TBI',     name => 'Times-Bold-Italic',            selected => 0},
117     {type => 'C',       name => 'Courier',                      selected => 0},
118     {type => 'CB',      name => 'Courier-Bold',                 selected => 0},
119     {type => 'CO',      name => 'Courier-Oblique',              selected => 0},
120     {type => 'CBO',     name => 'Courier-Bold-Oblique',         selected => 0},
121     {type => 'H',       name => 'Helvetica',                    selected => 0},
122     {type => 'HB',      name => 'Helvetica-Bold',               selected => 0},
123     {type => 'HBO',     name => 'Helvetica-Bold-Oblique',       selected => 0},
124 ];
125
126 my $text_justification_types = [
127     {type => 'L',       name => 'Left',                         selected => 0},
128     {type => 'C',       name => 'Center',                       selected => 0},
129     {type => 'R',       name => 'Right',                        selected => 0},
130 #    {type => 'F',       name => 'Full',                         selected => 0},
131 ];
132
133 my $unit_values = [
134     {type       => 'POINT',      desc    => 'PostScript Points',  value   => 1,                 selected => 0},
135     {type       => 'AGATE',      desc    => 'Adobe Agates',       value   => 5.1428571,         selected => 0},
136     {type       => 'INCH',       desc    => 'US Inches',          value   => 72,                selected => 0},
137     {type       => 'MM',         desc    => 'SI Millimeters',     value   => 2.83464567,        selected => 0},
138     {type       => 'CM',         desc    => 'SI Centimeters',     value   => 28.3464567,        selected => 0},
139 ];
140
141 my $output_formats = [
142     {type       => 'pdf',       desc    => 'PDF File'},
143     {type       => 'csv',       desc    => 'CSV File'},
144 ];
145
146 =head2 C4::Creators::Lib::get_all_templates()
147
148   my $templates = get_all_templates();
149
150 This function returns a reference to a hash containing all templates upon success and 1 upon failure. Errors are logged to the Apache log.
151
152 =cut
153
154 sub get_all_templates {
155     my %params = @_;
156     my @templates = ();
157     my $query = "SELECT " . ($params{'field_list'} ? $params{'field_list'} : '*') . " FROM creator_templates";
158     $query .= ($params{'filter'} ? " WHERE $params{'filter'} " : '');
159     $query .= ($params{'orderby'} ? " ORDER BY $params{'orderby'} " : '');
160     my $sth = C4::Context->dbh->prepare($query);
161     $sth->execute();
162     if ($sth->err) {
163         warn sprintf('Database returned the following error: %s', $sth->errstr);
164         return -1;
165     }
166     ADD_TEMPLATES:
167     while (my $template = $sth->fetchrow_hashref) {
168         push(@templates, $template);
169     }
170     return \@templates;
171 }
172
173 =head2 C4::Creators::Lib::get_all_layouts()
174
175   my $layouts = get_all_layouts();
176
177 This function returns a reference to a hash containing all layouts upon success and 1 upon failure. Errors are logged to the Apache log.
178
179 =cut
180
181 sub get_all_layouts {
182     my %params = @_;
183     my @layouts = ();
184     my $query = "SELECT " . ($params{'field_list'} ? $params{'field_list'} : '*') . " FROM creator_layouts";
185     $query .= ($params{'filter'} ? " WHERE $params{'filter'} " : '');
186     $query .= ($params{'orderby'} ? " ORDER BY $params{'orderby'} " : '');
187     my $sth = C4::Context->dbh->prepare($query);
188     $sth->execute();
189     if ($sth->err) {
190         warn sprintf('Database returned the following error: %s', $sth->errstr);
191         return -1;
192     }
193     ADD_LAYOUTS:
194     while (my $layout = $sth->fetchrow_hashref) {
195         push(@layouts, $layout);
196     }
197     return \@layouts;
198 }
199
200 =head2 C4::Creators::Lib::get_all_profiles()
201
202   my $profiles = get_all_profiles();
203
204   my $profiles = get_all_profiles(field_list => field_list, filter => filter_string);
205
206 This function returns an arrayref whose elements are hashes containing all profiles upon success and 1 upon failure. Errors are logged
207 to the Apache log. Two parameters are accepted. The first limits the field(s) returned. This parameter should be string of comma separted
208 fields. ie. "field_1, field_2, ...field_n" The second limits the records returned based on a string containing a valud SQL 'WHERE' filter.
209
210 NOTE: Do not pass in the keyword 'WHERE.'
211
212 =cut
213
214 sub get_all_profiles {
215     my %params = @_;
216     my @profiles = ();
217     my $query = "SELECT " . ($params{'field_list'} ? $params{'field_list'} : '*') . " FROM printers_profile";
218     $query .= ($params{'filter'} ? " WHERE $params{'filter'};" : ';');
219     my $sth = C4::Context->dbh->prepare($query);
220 #    $sth->{'TraceLevel'} = 3 if $debug;
221     $sth->execute();
222     if ($sth->err) {
223         warn sprintf('Database returned the following error: %s', $sth->errstr);
224         return -1;
225     }
226     ADD_PROFILES:
227     while (my $profile = $sth->fetchrow_hashref) {
228         push(@profiles, $profile);
229     }
230     return \@profiles;
231 }
232
233 =head2 C4::Creators::Lib::get_all_image_names()
234
235 =cut
236
237 sub get_all_image_names {
238     my $image_names = [];
239     my $query = "SELECT image_name FROM creator_images";
240     my $sth = C4::Context->dbh->prepare($query);
241 #    $sth->{'TraceLevel'} = 3 if $debug;
242     $sth->execute();
243     if ($sth->err) {
244         warn sprintf('Database returned the following error: %s', $sth->errstr);
245         return -1;
246     }
247     grep {push @$image_names, {type => $$_[0], name => $$_[0], selected => 0}} @{$sth->fetchall_arrayref([0])};
248     return $image_names;
249 }
250
251 =head2 C4::Creators::Lib::get_batch_summary()
252
253   my $batches = get_batch_summary();
254
255   my $batches = get_batch_summary(filter => filter_string);
256
257 This function returns an arrayref whose elements are hashes containing the batch_ids of current batches along with the item count
258 for each batch upon success and 1 upon failure. Item counts are stored under the key '_item_count' Errors are logged to the Apache log.
259 One parameter is accepted which limits the records returned based on a string containing a valud SQL 'WHERE' filter.
260
261 NOTE: Do not pass in the keyword 'WHERE.'
262
263 =cut
264
265 sub get_batch_summary {
266     my %params = @_;
267     my @batches = ();
268     my $query = "SELECT batch_id,count(batch_id) as _item_count FROM creator_batches WHERE creator=?";
269     $query .= ($params{'filter'} ? " AND $params{'filter'}" : '');
270     $query .= " GROUP BY batch_id";
271     my $sth = C4::Context->dbh->prepare($query);
272 #    $sth->{'TraceLevel'} = 3;
273     $sth->execute($params{'creator'});
274     if ($sth->err) {
275         warn sprintf('Database returned the following error on attempted SELECT: %s', $sth->errstr);
276         return -1;
277     }
278     while (my $batch = $sth->fetchrow_hashref) {
279         push(@batches, $batch);
280     }
281     return \@batches;
282 }
283
284 =head2 C4::Creators::Lib::get_label_summary()
285
286   my $labels = get_label_summary();
287
288   my $labels = get_label_summary(items => @item_list);
289
290 This function returns an arrayref whose elements are hashes containing the label_ids of current labels along with the item count
291 for each label upon success and 1 upon failure. Item counts are stored under the key '_item_count' Errors are logged to the Apache log.
292 One parameter is accepted which limits the records returned based on a string containing a valud SQL 'WHERE' filter.
293
294 NOTE: Do not pass in the keyword 'WHERE.'
295
296 =cut
297
298 sub get_label_summary {
299     my %params = @_;
300     my $label_number = 0;
301     my @label_summaries = ();
302     my $query = "     SELECT b.title, b.author, bi.itemtype, i.barcode, i.biblionumber, i.itype
303                       FROM creator_batches AS c LEFT JOIN items AS i ON (c.item_number=i.itemnumber)
304                       LEFT JOIN biblioitems AS bi ON (i.biblioitemnumber=bi.biblioitemnumber)
305                       LEFT JOIN biblio AS b ON (bi.biblionumber=b.biblionumber)
306                       WHERE itemnumber=? AND batch_id=?;
307                   ";
308     my $sth = C4::Context->dbh->prepare($query);
309     foreach my $item (@{$params{'items'}}) {
310         $label_number++;
311         $sth->execute($item->{'item_number'}, $params{'batch_id'});
312         if ($sth->err) {
313             warn sprintf('Database returned the following error on attempted SELECT: %s', $sth->errstr);
314             return -1;
315         }
316         my $record = $sth->fetchrow_hashref;
317         my $label_summary;
318         $label_summary->{'_label_number'} = $label_number;
319         $record->{'author'} =~ s/[^\.|\w]$// if $record->{'author'};  # strip off ugly trailing chars... but not periods or word chars
320         $record->{'title'} =~ s/\W*$//;  # strip off ugly trailing chars
321         # FIXME contructing staff interface URLs should be done *much* higher up the stack - for the most part, C4 module code
322         # should not know that it's part of a web app
323         $record->{'title'} = '<a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=' . $record->{'biblionumber'} . '"> ' . $record->{'title'} . '</a>';
324         $label_summary->{'_summary'} = $record->{'title'} . " | " . ($record->{'author'} ? $record->{'author'} : 'N/A');
325         $label_summary->{'_item_type'} = C4::Context->preference("item-level_itypes") ? $record->{'itype'} : $record->{'itemtype'};
326         $label_summary->{'_barcode'} = $record->{'barcode'};
327         $label_summary->{'_item_number'} = $item->{'item_number'};
328         $label_summary->{'_label_id'} = $item->{'label_id'};
329         push (@label_summaries, $label_summary);
330     }
331     return \@label_summaries;
332 }
333
334 =head2 C4::Creators::Lib::get_card_summary()
335
336   my $cards = get_card_summary();
337
338   my $cards = get_card_summary(items => @item_list);
339
340 This function returns an arrayref whose elements are hashes containing the label_ids of current cards along with the item count
341 for each card upon success and 1 upon failure. Item counts are stored under the key '_item_count' Errors are logged to the Apache log.
342 One parameter is accepted which limits the records returned based on a string containing a valud SQL 'WHERE' filter.
343
344 NOTE: Do not pass in the keyword 'WHERE.'
345
346 =cut
347
348 sub get_card_summary {
349     my %params = @_;
350     my $card_number = 0;
351     my @card_summaries = ();
352     my $query = "SELECT CONCAT_WS(', ', surname, firstname) AS name, cardnumber FROM borrowers WHERE borrowernumber=?;";
353     my $sth = C4::Context->dbh->prepare($query);
354     foreach my $item (@{$params{'items'}}) {
355         $card_number++;
356         $sth->execute($item->{'borrower_number'});
357         if ($sth->err) {
358             warn sprintf('Database returned the following error on attempted SELECT: %s', $sth->errstr);
359             return -1;
360         }
361         my $record = $sth->fetchrow_hashref;
362         my $card_summary->{'_card_number'} = $card_number;
363         $card_summary->{'_summary'} = $record->{'name'};
364         $card_summary->{'borrowernumber'} = $item->{'borrower_number'};
365         $card_summary->{'_label_id'} = $item->{'label_id'};
366         push (@card_summaries, $card_summary);
367     }
368     return \@card_summaries;
369 }
370
371 =head2 C4::Creators::Lib::get_barcode_types()
372
373   my $barcode_types = get_barcode_types();
374
375 This function returns a reference to an array of hashes containing all barcode types along with their name and description.
376
377 =cut
378
379 sub get_barcode_types {
380     return $barcode_types;
381 }
382
383 =head2 C4::Creators::Lib::get_label_types()
384
385   my $label_types = get_label_types();
386
387 This function returns a reference to an array of hashes containing all label types along with their name and description.
388
389 =cut
390
391 sub get_label_types {
392     return $label_types;
393 }
394
395 =head2 C4::Creators::Lib::get_font_types()
396
397   my $font_types = get_font_types();
398
399 This function returns a reference to an array of hashes containing all font types along with their name and description.
400
401 =cut
402
403 sub get_font_types {
404     return $font_types;
405 }
406
407 =head2 C4::Creators::Lib::get_text_justification_types()
408
409   my $text_justification_types = get_text_justification_types();
410
411 This function returns a reference to an array of hashes containing all text justification types along with their name and description.
412
413 =cut
414
415 sub get_text_justification_types {
416     return $text_justification_types;
417 }
418
419 =head2 C4::Creators::Lib::get_unit_values()
420
421   my $unit_values = get_unit_values();
422
423 This function returns a reference to an array of  hashes containing all unit types along with their description and multiplier.
424 NOTE: All units are relative to a PostScript Point.
425 There are 72 PS points to the inch.
426
427 =cut
428
429 sub get_unit_values {
430     return $unit_values;
431 }
432
433 =head2 C4::Creators::Lib::get_output_formats()
434
435   my $label_output_formats = get_output_formats();
436
437 This function returns a reference to an array of hashes containing all label output formats along with their description.
438
439 =cut
440
441 sub get_output_formats {
442     return $output_formats;
443 }
444
445 =head2 C4::Creators::Lib::get_column_names($table_name)
446
447 Return an arrayref of an array containing the column names of the supplied table.
448
449 =cut
450
451 sub get_column_names {
452     my $table = shift;
453     my $dbh = C4::Context->dbh();
454     my $column_names = [];
455     my $sth = $dbh->column_info(undef,undef,$table,'%');
456     while (my $info = $sth->fetchrow_hashref()){
457         $$column_names[$info->{'ORDINAL_POSITION'}] = $info->{'COLUMN_NAME'};
458     }
459     return $column_names;
460 }
461
462 =head2 C4::Creators::Lib::get_table_names($search_term)
463
464 Return an arrayref of an array containing the table names which contain the supplied search term.
465
466 =cut
467
468 sub get_table_names {
469     my $search_term = shift;
470     my $dbh = C4::Context->dbh();
471     my $table_names = [];
472     my $sth = $dbh->table_info(undef,undef,"%$search_term%");
473     while (my $info = $sth->fetchrow_hashref()){
474         push (@$table_names, $info->{'TABLE_NAME'});
475     }
476     return $table_names;
477 }
478
479 =head2 C4::Creators::Lib::html_table()
480
481 This function returns an arrayref of an array of hashes contianing the supplied data formatted suitably to
482 be passed off as a template parameter and used to build an html table.
483
484    my $table = html_table(header_fields, array_of_row_data);
485    $template->param(
486        table_loop => $table,
487    );
488
489     html example:
490
491         <table>
492             [% FOREACH table_loo IN table_loop %]
493                 [% IF ( table_loo.header_fields ) %]
494                     <tr>
495                         [% FOREACH header_field IN table_loo.header_fields %]
496                             <th>[% header_field.field_label %]</th>
497                         [% END %]
498                     </tr>
499                 [% ELSE %]
500                     <tr>
501                         [% FOREACH text_field IN table_loo.text_fields %]
502                             [% IF ( text_field.select_field ) %]
503                                 <td><input type="checkbox" name="action" value="[% text_field.field_value %]"></td>
504                             [% ELSE %]
505                                 <td>[% text_field.field_value %]</td>
506                             [% END %]
507                         [% END %]
508                     </tr>
509                 [% END %]
510             [% END %]
511         </table>
512
513 =cut
514
515 sub html_table {
516     my $headers = shift;
517     my $data = shift;
518     return undef if scalar(@$data) == 0;      # no need to generate a table if there is not data to display
519     my $table = [];
520     my $fields = [];
521     my @table_columns = ();
522     my ($row_index, $col_index) = (0,0);
523     my $cols = 0;       # number of columns to wrap on
524     my $field_count = 0;
525     my $select_value = undef;
526     my $link_field = undef;
527     POPULATE_HEADER:
528     foreach my $header (@$headers) {
529         my @key = keys %$header;
530         if ($key[0] eq 'select' ) {
531             push (@table_columns, $key[0]);
532             $$fields[$col_index] = {hidden => 0, select_field => 0, field_name => ($key[0]), field_label => $header->{$key[0]}{'label'}};
533             # do special formatting stuff....
534             $select_value = $header->{$key[0]}{'value'};
535         }
536         else {
537             # do special formatting stuff....
538             $link_field->{$key[0]} = ($header->{$key[0]}{'link_field'} == 1 ? 1 : 0);
539             push (@table_columns, $key[0]);
540             $$fields[$col_index] = {hidden => 0, select_field => 0, field_name => ($key[0]), field_label => $header->{$key[0]}{'label'}};
541         }
542         $field_count++;
543         $col_index++;
544     }
545     $$table[$row_index] = {header_fields => $fields};
546     $cols = $col_index;
547     $field_count *= scalar(@$data);     # total fields to be displayed in the table
548     $col_index = 0;
549     $row_index++;
550     $fields = [];
551     POPULATE_TABLE:
552     foreach my $db_row (@$data) {
553         POPULATE_ROW:
554         foreach my $table_column (@table_columns) {
555             if (grep {$table_column eq $_} keys %$db_row) {
556                 $$fields[$col_index] = {hidden => 0, link_field => $link_field->{$table_column}, select_field => 0, field_name => ($table_column . "_tbl"), field_value => $db_row->{$table_column}};
557                 $col_index++;
558                 next POPULATE_ROW;
559             }
560             elsif ($table_column =~ m/^_((.*)_(.*$))/) {   # this a special case
561                 my $table_name = get_table_names($2);
562                 my $record_set = _SELECT($1, @$table_name[0], $2 . "_id = " . $db_row->{$2 . "_id"});
563                 $$fields[$col_index] = {hidden => 0, link_field => $link_field->{$table_column}, select_field => 0, field_name => ($table_column . "_tbl"), field_value => $$record_set[0]{$1}};
564                 $col_index++;
565                 next POPULATE_ROW;
566             }
567             elsif ($table_column eq 'select' ) {
568                 $$fields[$col_index] = {hidden => 0, select_field => 1, field_name => 'select', field_value => $db_row->{$select_value}};
569             }
570         }
571         $$table[$row_index] = {text_fields => $fields};
572         $col_index = 0;
573         $row_index++;
574         $fields = [];
575     }
576     return $table;
577 }
578
579 1;
580 __END__
581
582 =head1 AUTHOR
583
584 Chris Nighswonger <cnighswonger AT foundations DOT edu>
585
586 =cut