Show enumchron, copynumber in opac detail iff present.
[koha.git] / C4 / Labels.pm
blob06d1cebdff7a4f63d3b3bc66d4b010ac19146526
1 package C4::Labels;
3 # Copyright 2006 Katipo Communications.
5 # This file is part of Koha.
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.
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.
16 # You should have received a copy of the GNU General Public License along with
17 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
18 # Suite 330, Boston, MA 02111-1307 USA
20 use strict;
21 use vars qw($VERSION @ISA @EXPORT);
23 use PDF::Reuse;
24 use Text::Wrap;
25 use Algorithm::CheckDigits;
26 use C4::Members;
27 use C4::Branch;
28 use Data::Dumper;
29 # use Smart::Comments;
31 BEGIN {
32 $VERSION = 0.03;
33 require Exporter;
34 @ISA = qw(Exporter);
35 @EXPORT = qw(
36 &get_label_options &GetLabelItems
37 &build_circ_barcode &draw_boundaries
38 &drawbox &GetActiveLabelTemplate
39 &GetAllLabelTemplates &DeleteTemplate
40 &GetSingleLabelTemplate &SaveTemplate
41 &CreateTemplate &SetActiveTemplate
42 &SaveConf &DrawSpineText &GetTextWrapCols
43 &GetUnitsValue &DrawBarcode &DrawPatronCardText
44 &get_printingtypes &GetPatronCardItems
45 &get_layouts
46 &get_barcode_types
47 &get_batches &delete_batch
48 &add_batch &printText
49 &GetItemFields
50 &get_text_fields
51 get_layout &save_layout &add_layout
52 &set_active_layout &by_order
53 &build_text_dropbox
54 &delete_layout &get_active_layout
55 &get_highest_batch
56 &deduplicate_batch
57 &GetAllPrinterProfiles &GetSinglePrinterProfile
58 &SaveProfile &CreateProfile &DeleteProfile
59 &GetAssociatedProfile &SetAssociatedProfile
63 my $DEBUG = 0;
65 =head1 NAME
67 C4::Labels - Functions for printing spine labels and barcodes in Koha
69 =head1 FUNCTIONS
71 =over 2
73 =item get_label_options;
75 $options = get_label_options()
77 Return a pointer on a hash list containing info from labels_conf table in Koha DB.
79 =cut
82 sub get_label_options {
83 my $dbh = C4::Context->dbh;
84 my $query2 = " SELECT * FROM labels_conf where active = 1";
85 my $sth = $dbh->prepare($query2);
86 $sth->execute();
87 my $conf_data = $sth->fetchrow_hashref;
88 $sth->finish;
89 return $conf_data;
92 sub get_layouts {
94 ## FIXME: this if/else could be compacted...
95 my $dbh = C4::Context->dbh;
96 my @data;
97 my $query = " Select * from labels_conf";
98 my $sth = $dbh->prepare($query);
99 $sth->execute();
100 my @resultsloop;
101 while ( my $data = $sth->fetchrow_hashref ) {
103 $data->{'fieldlist'} = get_text_fields( $data->{'id'} );
104 push( @resultsloop, $data );
106 $sth->finish;
108 # @resultsloop
110 return @resultsloop;
113 sub get_layout {
114 my ($layout_id) = @_;
115 my $dbh = C4::Context->dbh;
117 # get the actual items to be printed.
118 my $query = " Select * from labels_conf where id = ?";
119 my $sth = $dbh->prepare($query);
120 $sth->execute($layout_id);
121 my $data = $sth->fetchrow_hashref;
122 $sth->finish;
123 return $data;
126 sub get_active_layout {
127 my ($layout_id) = @_;
128 my $dbh = C4::Context->dbh;
130 # get the actual items to be printed.
131 my $query = " Select * from labels_conf where active = 1";
132 my $sth = $dbh->prepare($query);
133 $sth->execute();
134 my $data = $sth->fetchrow_hashref;
135 $sth->finish;
136 return $data;
139 sub delete_layout {
140 my ($layout_id) = @_;
141 my $dbh = C4::Context->dbh;
143 # get the actual items to be printed.
144 my $query = "delete from labels_conf where id = ?";
145 my $sth = $dbh->prepare($query);
146 $sth->execute($layout_id);
147 $sth->finish;
150 sub get_printingtypes {
151 my ($layout_id) = @_;
152 my @printtypes;
153 # FIXME: hard coded print types
154 push( @printtypes, { code => 'BAR', desc => "barcode" } );
155 push( @printtypes, { code => 'BIB', desc => "biblio" } );
156 push( @printtypes, { code => 'BARBIB', desc => "barcode / biblio" } );
157 push( @printtypes, { code => 'BIBBAR', desc => "biblio / barcode" } );
158 push( @printtypes, { code => 'ALT', desc => "alternating labels" } );
159 push( @printtypes, { code => 'PATCRD', desc => "patron cards" } );
161 my $conf = get_layout($layout_id);
162 my $active_printtype = $conf->{'printingtype'};
164 # lop thru layout, insert selected to hash
166 foreach my $printtype (@printtypes) {
167 if ( $printtype->{'code'} eq $active_printtype ) {
168 $printtype->{'active'} = 'MOO';
171 return @printtypes;
174 sub build_text_dropbox {
175 my ($order) = @_;
177 # my @fields = get_text_fields();
178 # my $field_count = scalar @fields;
179 my $field_count = 10; # <----------- FIXME hard coded
181 my @lines;
182 !$order
183 ? push( @lines, { num => '', selected => '1' } )
184 : push( @lines, { num => '' } );
185 for ( my $i = 1 ; $i <= $field_count ; $i++ ) {
186 my $line = { num => "$i" };
187 $line->{'selected'} = 1 if $i eq $order;
188 push( @lines, $line );
191 # add a blank row too
193 return @lines;
196 sub get_text_fields {
197 my ($layout_id, $sorttype) = @_;
199 my ( $a, $b, $c, $d, $e, $f, $g, $h, $i ,$j, $k );
201 my $sortorder = get_layout($layout_id);
203 # These fields are hardcoded based on the template for label-edit-layout.pl
205 $a = {
206 code => 'itemtype',
207 desc => "Item Type",
208 order => $sortorder->{'itemtype'}
211 $b = {
212 code => 'dewey',
213 desc => "Dewey",
214 order => $sortorder->{'dewey'}
217 $c = {
218 code => 'issn',
219 desc => "ISSN",
220 order => $sortorder->{'issn'}
223 $d = {
224 code => 'isbn',
225 desc => "ISBN",
226 order => $sortorder->{'isbn'}
229 $e = {
230 code => 'class',
231 desc => "Classification",
232 order => $sortorder->{'class'}
235 $f = {
236 code => 'subclass',
237 desc => "Sub-Class",
238 order => $sortorder->{'subclass'}
241 $g = {
242 code => 'barcode',
243 desc => "Barcode",
244 order => $sortorder->{'barcode'}
247 $h = {
248 code => 'author',
249 desc => "Author",
250 order => $sortorder->{'author'}
253 $i = {
254 code => 'title',
255 desc => "Title",
256 order => $sortorder->{'title'}
259 $j = {
260 code => 'itemcallnumber',
261 desc => "Call Number",
262 order => $sortorder->{'itemcallnumber'}
265 $k = {
266 code => 'subtitle',
267 desc => "Subtitle",
268 order => $sortorder->{'subtitle'}
271 my @text_fields = ( $a, $b, $c, $d, $e, $f, $g, $h, $i ,$j, $k );
273 my @new_fields;
274 foreach my $field (@text_fields) {
275 push( @new_fields, $field ) if $field->{'order'} > 0;
278 my @sorted_fields = sort by_order @new_fields;
280 my $active_fields;
282 if ($sorttype eq 'codes') { # FIXME: This sub should really always return the array of hashrefs and let the caller take what he wants from that -fbcit
283 return @sorted_fields;
284 } else {
285 foreach my $field (@sorted_fields) {
286 $active_fields .= "$field->{'desc'} ";
288 return $active_fields;
293 sub by_order {
294 $$a{order} <=> $$b{order};
297 =head2 sub add_batch
298 =over 4
299 add_batch($batch_type,\@batch_list);
300 if $batch_list is supplied,
301 create a new batch with those items.
302 else, return the next available batch_id.
303 =return
304 =cut
305 sub add_batch {
306 my ( $batch_type,$batch_list ) = @_;
307 my $new_batch;
308 my $dbh = C4::Context->dbh;
309 my $q ="SELECT MAX(DISTINCT batch_id) FROM $batch_type";
310 my $sth = $dbh->prepare($q);
311 $sth->execute();
312 my ($batch_id) = $sth->fetchrow_array;
313 $sth->finish;
314 if($batch_id) {
315 $batch_id++;
316 } else {
317 $batch_id = 1;
319 # TODO: let this block use $batch_type
320 if(ref($batch_list) && ($batch_type eq 'labels') ) {
321 my $sth = $dbh->prepare("INSERT INTO labels (`batch_id`,`itemnumber`) VALUES (?,?)");
322 for my $item (@$batch_list) {
323 $sth->execute($batch_id,$item);
326 return $batch_id;
329 #FIXME: Needs to be ported to receive $batch_type
330 # ... this looks eerily like add_batch() ...
331 sub get_highest_batch {
332 my $new_batch;
333 my $dbh = C4::Context->dbh;
334 my $q =
335 "select distinct batch_id from labels order by batch_id desc limit 1";
336 my $sth = $dbh->prepare($q);
337 $sth->execute();
338 my $data = $sth->fetchrow_hashref;
339 $sth->finish;
341 if ( !$data->{'batch_id'} ) {
342 $new_batch = 1;
344 else {
345 $new_batch = $data->{'batch_id'};
348 return $new_batch;
352 sub get_batches {
353 my ( $batch_type ) = @_;
354 my $dbh = C4::Context->dbh;
355 my $q = "SELECT batch_id, COUNT(*) AS num FROM $batch_type GROUP BY batch_id";
356 my $sth = $dbh->prepare($q);
357 $sth->execute();
358 my @resultsloop;
359 while ( my $data = $sth->fetchrow_hashref ) {
360 push( @resultsloop, $data );
362 $sth->finish;
364 # Not sure why we are doing this rather than simply telling the user that no batches are currently defined.
365 # So I'm commenting this out and modifying label-manager.tmpl to properly inform the user as stated. -fbcit
366 # adding a dummy batch=1 value , if none exists in the db
367 # if ( !scalar(@resultsloop) ) {
368 # push( @resultsloop, { batch_id => '1' , num => '0' } );
370 return @resultsloop;
373 sub delete_batch {
374 my ($batch_id, $batch_type) = @_;
375 warn "Deleteing batch of type $batch_type";
376 my $dbh = C4::Context->dbh;
377 my $q = "DELETE FROM $batch_type WHERE batch_id = ?";
378 my $sth = $dbh->prepare($q);
379 $sth->execute($batch_id);
380 $sth->finish;
383 sub get_barcode_types {
384 my ($layout_id) = @_;
385 my $layout = get_layout($layout_id);
386 my $barcode = $layout->{'barcodetype'};
387 my @array;
389 push( @array, { code => 'CODE39', desc => 'Code 39' } );
390 push( @array, { code => 'CODE39MOD', desc => 'Code39 + Modulo43' } );
391 push( @array, { code => 'CODE39MOD10', desc => 'Code39 + Modulo10' } );
392 push( @array, { code => 'ITF', desc => 'Interleaved 2 of 5' } );
394 foreach my $line (@array) {
395 if ( $line->{'code'} eq $barcode ) {
396 $line->{'active'} = 1;
400 return @array;
403 sub GetUnitsValue {
404 my ($units) = @_;
405 my $unitvalue;
407 $unitvalue = '1' if ( $units eq 'POINT' );
408 $unitvalue = '2.83464567' if ( $units eq 'MM' );
409 $unitvalue = '28.3464567' if ( $units eq 'CM' );
410 $unitvalue = 72 if ( $units eq 'INCH' );
411 return $unitvalue;
414 sub GetTextWrapCols {
415 my ( $font, $fontsize, $label_width, $left_text_margin ) = @_;
416 my $string = '0';
417 my $strwidth;
418 my $count = 0;
419 # my $textlimit = $label_width - ($left_text_margin);
420 my $textlimit = $label_width - ( 2* $left_text_margin);
422 while ( $strwidth < $textlimit ) {
423 $strwidth = prStrWidth( $string, $font, $fontsize );
424 $string = $string . '0';
425 #warn "strwidth:$strwidth, textlimit:$textlimit, count:$count string:$string";
426 $count++;
428 return $count;
431 sub GetActiveLabelTemplate {
432 my $dbh = C4::Context->dbh;
433 my $query = " SELECT * FROM labels_templates where active = 1 limit 1";
434 my $sth = $dbh->prepare($query);
435 $sth->execute();
436 my $active_tmpl = $sth->fetchrow_hashref;
437 $sth->finish;
438 return $active_tmpl;
441 sub GetSingleLabelTemplate {
442 my ($tmpl_id) = @_;
443 my $dbh = C4::Context->dbh;
444 my $query = " SELECT * FROM labels_templates where tmpl_id = ?";
445 my $sth = $dbh->prepare($query);
446 $sth->execute($tmpl_id);
447 my $template = $sth->fetchrow_hashref;
448 $sth->finish;
449 return $template;
452 sub SetActiveTemplate {
454 my ($tmpl_id) = @_;
456 my $dbh = C4::Context->dbh;
457 my $query = " UPDATE labels_templates SET active = NULL";
458 my $sth = $dbh->prepare($query);
459 $sth->execute();
461 $query = "UPDATE labels_templates SET active = 1 WHERE tmpl_id = ?";
462 $sth = $dbh->prepare($query);
463 $sth->execute($tmpl_id);
464 $sth->finish;
467 sub set_active_layout {
469 my ($layout_id) = @_;
470 my $dbh = C4::Context->dbh;
471 my $query = " UPDATE labels_conf SET active = NULL";
472 my $sth = $dbh->prepare($query);
473 $sth->execute();
475 $query = "UPDATE labels_conf SET active = 1 WHERE id = ?";
476 $sth = $dbh->prepare($query);
477 $sth->execute($layout_id);
478 $sth->finish;
481 sub DeleteTemplate {
482 my ($tmpl_id) = @_;
483 my $dbh = C4::Context->dbh;
484 my $query = " DELETE FROM labels_templates where tmpl_id = ?";
485 my $sth = $dbh->prepare($query);
486 $sth->execute($tmpl_id);
487 $sth->finish;
490 sub SaveTemplate {
491 my (
492 $tmpl_id, $tmpl_code, $tmpl_desc, $page_width,
493 $page_height, $label_width, $label_height, $topmargin,
494 $leftmargin, $cols, $rows, $colgap,
495 $rowgap, $font, $fontsize, $units
496 ) = @_;
497 warn "Passed \$font:$font";
498 my $dbh = C4::Context->dbh;
499 my $query =
500 " UPDATE labels_templates SET tmpl_code=?, tmpl_desc=?, page_width=?,
501 page_height=?, label_width=?, label_height=?, topmargin=?,
502 leftmargin=?, cols=?, rows=?, colgap=?, rowgap=?, font=?, fontsize=?,
503 units=?
504 WHERE tmpl_id = ?";
506 my $sth = $dbh->prepare($query);
507 $sth->execute(
508 $tmpl_code, $tmpl_desc, $page_width, $page_height,
509 $label_width, $label_height, $topmargin, $leftmargin,
510 $cols, $rows, $colgap, $rowgap,
511 $font, $fontsize, $units, $tmpl_id
513 my $dberror = $sth->errstr;
514 $sth->finish;
515 return $dberror;
518 sub CreateTemplate {
519 my $tmpl_id;
520 my (
521 $tmpl_code, $tmpl_desc, $page_width, $page_height,
522 $label_width, $label_height, $topmargin, $leftmargin,
523 $cols, $rows, $colgap, $rowgap,
524 $font, $fontsize, $units
525 ) = @_;
527 my $dbh = C4::Context->dbh;
529 my $query = "INSERT INTO labels_templates (tmpl_code, tmpl_desc, page_width,
530 page_height, label_width, label_height, topmargin,
531 leftmargin, cols, rows, colgap, rowgap, font, fontsize, units)
532 VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
534 my $sth = $dbh->prepare($query);
535 $sth->execute(
536 $tmpl_code, $tmpl_desc, $page_width, $page_height,
537 $label_width, $label_height, $topmargin, $leftmargin,
538 $cols, $rows, $colgap, $rowgap,
539 $font, $fontsize, $units
541 my $dberror = $sth->errstr;
542 $sth->finish;
543 return $dberror;
546 sub GetAllLabelTemplates {
547 my $dbh = C4::Context->dbh;
549 # get the actual items to be printed.
550 my @data;
551 my $query = " Select * from labels_templates ";
552 my $sth = $dbh->prepare($query);
553 $sth->execute();
554 my @resultsloop;
555 while ( my $data = $sth->fetchrow_hashref ) {
556 push( @resultsloop, $data );
558 $sth->finish;
560 #warn Dumper @resultsloop;
561 return @resultsloop;
564 #sub SaveConf {
565 sub add_layout {
567 my (
568 $barcodetype, $title, $subtitle, $isbn, $issn,
569 $itemtype, $bcn, $dcn, $classif,
570 $subclass, $itemcallnumber, $author, $tmpl_id,
571 $printingtype, $guidebox, $startlabel, $layoutname
572 ) = @_;
574 my $dbh = C4::Context->dbh;
575 my $query2 = "update labels_conf set active = NULL";
576 my $sth2 = $dbh->prepare($query2);
577 $sth2->execute();
578 $query2 = "INSERT INTO labels_conf
579 ( barcodetype, title, subtitle, isbn,issn, itemtype, barcode,
580 dewey, class, subclass, itemcallnumber, author, printingtype,
581 guidebox, startlabel, layoutname, active )
582 values ( ?, ?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?,?,?, 1 )";
583 $sth2 = $dbh->prepare($query2);
584 $sth2->execute(
585 $barcodetype, $title, $subtitle, $isbn, $issn,
587 $itemtype, $bcn, $dcn, $classif,
588 $subclass, $itemcallnumber, $author, $printingtype,
589 $guidebox, $startlabel, $layoutname
591 $sth2->finish;
593 SetActiveTemplate($tmpl_id);
594 return;
597 sub save_layout {
599 my (
600 $barcodetype, $title, $subtitle, $isbn, $issn,
601 $itemtype, $bcn, $dcn, $classif,
602 $subclass, $itemcallnumber, $author, $tmpl_id,
603 $printingtype, $guidebox, $startlabel, $layoutname,
604 $layout_id
605 ) = @_;
606 ### $layoutname
607 ### $layout_id
609 my $dbh = C4::Context->dbh;
610 my $query2 = "update labels_conf set
611 barcodetype=?, title=?, subtitle=?, isbn=?,issn=?,
612 itemtype=?, barcode=?, dewey=?, class=?,
613 subclass=?, itemcallnumber=?, author=?, printingtype=?,
614 guidebox=?, startlabel=?, layoutname=? where id = ?";
615 my $sth2 = $dbh->prepare($query2);
616 $sth2->execute(
617 $barcodetype, $title, $subtitle, $isbn, $issn,
618 $itemtype, $bcn, $dcn, $classif,
619 $subclass, $itemcallnumber, $author, $printingtype,
620 $guidebox, $startlabel, $layoutname, $layout_id
622 $sth2->finish;
624 return;
627 =item GetAllPrinterProfiles;
629 @profiles = GetAllPrinterProfiles()
631 Returns an array of references-to-hash, whos keys are .....
633 =cut
635 sub GetAllPrinterProfiles {
637 my $dbh = C4::Context->dbh;
638 my @data;
639 my $query = "SELECT * FROM printers_profile AS pp INNER JOIN labels_templates AS lt ON pp.tmpl_id = lt.tmpl_id; ";
640 my $sth = $dbh->prepare($query);
641 $sth->execute();
642 my @resultsloop;
643 while ( my $data = $sth->fetchrow_hashref ) {
644 push( @resultsloop, $data );
646 $sth->finish;
648 return @resultsloop;
651 =item GetSinglePrinterProfile;
653 $profile = GetSinglePrinterProfile()
655 Returns a hashref whos keys are...
657 =cut
659 sub GetSinglePrinterProfile {
660 my ($prof_id) = @_;
661 my $dbh = C4::Context->dbh;
662 my $query = " SELECT * FROM printers_profile WHERE prof_id = ?; ";
663 my $sth = $dbh->prepare($query);
664 $sth->execute($prof_id);
665 my $template = $sth->fetchrow_hashref;
666 $sth->finish;
667 return $template;
670 =item SaveProfile;
672 SaveProfile('parameters')
674 When passed a set of parameters, this function updates the given profile with the new parameters.
676 =cut
678 sub SaveProfile {
679 my (
680 $prof_id, $offset_horz, $offset_vert, $creep_horz, $creep_vert, $units
681 ) = @_;
682 my $dbh = C4::Context->dbh;
683 my $query =
684 " UPDATE printers_profile
685 SET offset_horz=?, offset_vert=?, creep_horz=?, creep_vert=?, unit=?
686 WHERE prof_id = ? ";
687 my $sth = $dbh->prepare($query);
688 $sth->execute(
689 $offset_horz, $offset_vert, $creep_horz, $creep_vert, $units, $prof_id
691 $sth->finish;
694 =item CreateProfile;
696 CreateProfile('parameters')
698 When passed a set of parameters, this function creates a new profile containing those parameters
699 and returns any errors.
701 =cut
703 sub CreateProfile {
704 my (
705 $prof_id, $printername, $paper_bin, $tmpl_id, $offset_horz,
706 $offset_vert, $creep_horz, $creep_vert, $units
707 ) = @_;
708 my $dbh = C4::Context->dbh;
709 my $query =
710 " INSERT INTO printers_profile (prof_id, printername, paper_bin, tmpl_id,
711 offset_horz, offset_vert, creep_horz, creep_vert, unit)
712 VALUES(?,?,?,?,?,?,?,?,?) ";
713 my $sth = $dbh->prepare($query);
714 $sth->execute(
715 $prof_id, $printername, $paper_bin, $tmpl_id, $offset_horz,
716 $offset_vert, $creep_horz, $creep_vert, $units
718 my $error = $sth->errstr;
719 $sth->finish;
720 return $error;
723 =item DeleteProfile;
725 DeleteProfile(prof_id)
727 When passed a profile id, this function deletes that profile from the database and returns any errors.
729 =cut
731 sub DeleteProfile {
732 my ($prof_id) = @_;
733 my $dbh = C4::Context->dbh;
734 my $query = " DELETE FROM printers_profile WHERE prof_id = ?";
735 my $sth = $dbh->prepare($query);
736 $sth->execute($prof_id);
737 my $error = $sth->errstr;
738 $sth->finish;
739 return $error;
742 =item GetAssociatedProfile;
744 $assoc_prof = GetAssociatedProfile(tmpl_id)
746 When passed a template id, this function returns the parameters from the currently associated printer profile
747 in a hashref where key=fieldname and value=fieldvalue.
749 =cut
751 sub GetAssociatedProfile {
752 my ($tmpl_id) = @_;
753 my $dbh = C4::Context->dbh;
754 # First we find out the prof_id for the associated profile...
755 my $query = "SELECT * FROM labels_profile WHERE tmpl_id = ?";
756 my $sth = $dbh->prepare($query);
757 $sth->execute($tmpl_id);
758 my $assoc_prof = $sth->fetchrow_hashref;
759 $sth->finish;
760 # Then we retrieve that profile and return it to the caller...
761 $assoc_prof = GetSinglePrinterProfile($assoc_prof->{'prof_id'});
762 return $assoc_prof;
765 =item SetAssociatedProfile;
767 SetAssociatedProfile($prof_id, $tmpl_id)
769 When passed both a profile id and template id, this function establishes an association between the two. No more
770 than one profile may be associated with any given template at the same time.
772 =cut
774 sub SetAssociatedProfile {
776 my ($prof_id, $tmpl_id) = @_;
778 my $dbh = C4::Context->dbh;
779 my $query = "INSERT INTO labels_profile (prof_id, tmpl_id) VALUES (?,?) ON DUPLICATE KEY UPDATE prof_id = ?";
780 my $sth = $dbh->prepare($query);
781 $sth->execute($prof_id, $tmpl_id, $prof_id);
782 $sth->finish;
785 =item GetLabelItems;
787 $options = GetLabelItems()
789 Returns an array of references-to-hash, whos keys are the field from the biblio, biblioitems, items and labels tables in the Koha database.
791 =cut
794 sub GetLabelItems {
795 my ($batch_id) = @_;
796 my $dbh = C4::Context->dbh;
798 my @resultsloop = ();
799 my $count;
800 my @data;
801 my $sth;
803 if ($batch_id) {
804 my $query3 = "Select * from labels where batch_id = ? order by labelid ";
805 $sth = $dbh->prepare($query3);
806 $sth->execute($batch_id);
809 else {
811 my $query3 = "Select * from labels";
812 $sth = $dbh->prepare($query3);
813 $sth->execute();
815 my $cnt = $sth->rows;
816 my $i1 = 1;
817 while ( my $data = $sth->fetchrow_hashref ) {
819 # lets get some summary info from each item
820 my $query1 = "
821 select i.*, bi.*, b.* from items i,biblioitems bi,biblio b
822 where itemnumber=? and i.biblioitemnumber=bi.biblioitemnumber and
823 bi.biblionumber=b.biblionumber";
825 my $sth1 = $dbh->prepare($query1);
826 $sth1->execute( $data->{'itemnumber'} );
828 my $data1 = $sth1->fetchrow_hashref();
829 $data1->{'labelno'} = $i1;
830 $data1->{'labelid'} = $data->{'labelid'};
831 $data1->{'batch_id'} = $batch_id;
832 $data1->{'summary'} =
833 "$data1->{'barcode'}, $data1->{'title'}, $data1->{'isbn'}";
835 push( @resultsloop, $data1 );
836 $sth1->finish;
838 $i1++;
840 $sth->finish;
841 return @resultsloop;
845 sub GetItemFields {
846 my @fields = qw (
847 barcode title subtitle
848 dewey isbn issn author class
849 itemtype subclass itemcallnumber
852 return @fields;
855 sub GetPatronCardItems {
857 my ( $batch_id ) = @_;
858 my @resultsloop;
860 my $dbh = C4::Context->dbh;
861 # my $query = "SELECT * FROM patroncards WHERE batch_id = ? ORDER BY borrowernumber";
862 my $query = "SELECT * FROM patroncards WHERE batch_id = ? ORDER BY cardid";
863 my $sth = $dbh->prepare($query);
864 $sth->execute($batch_id);
865 my $cardno = 1;
866 while ( my $data = $sth->fetchrow_hashref ) {
867 my $patron_data = GetMember( $data->{'borrowernumber'} );
868 $patron_data->{'branchname'} = GetBranchName( $patron_data->{'branchcode'} );
869 $patron_data->{'cardno'} = $cardno;
870 $patron_data->{'cardid'} = $data->{'cardid'};
871 $patron_data->{'batch_id'} = $batch_id;
872 push( @resultsloop, $patron_data );
873 $cardno++;
875 $sth->finish;
876 return @resultsloop;
880 sub deduplicate_batch {
881 my ( $batch_id, $batch_type ) = @_;
882 my $query = "
883 SELECT DISTINCT
884 batch_id," . (($batch_type eq 'labels') ? 'itemnumber' : 'borrowernumber') . ",
885 count(". (($batch_type eq 'labels') ? 'labelid' : 'cardid') . ") as count
886 FROM $batch_type
887 WHERE batch_id = ?
888 GROUP BY " . (($batch_type eq 'labels') ? 'itemnumber' : 'borrowernumber') . ",batch_id
889 HAVING count > 1
890 ORDER BY batch_id,
891 count DESC ";
892 my $sth = C4::Context->dbh->prepare($query);
893 $sth->execute($batch_id);
894 warn $sth->errstr if $sth->errstr;
895 $sth->rows or return undef, $sth->errstr;
897 my $del_query = "
898 DELETE
899 FROM $batch_type
900 WHERE batch_id = ?
901 AND " . (($batch_type eq 'labels') ? 'itemnumber' : 'borrowernumber') . " = ?
902 ORDER BY timestamp ASC
904 my $killed = 0;
905 while (my $data = $sth->fetchrow_hashref()) {
906 my $itemnumber = $data->{(($batch_type eq 'labels') ? 'itemnumber' : 'borrowernumber')} or next;
907 my $limit = $data->{count} - 1 or next;
908 my $sth2 = C4::Context->dbh->prepare("$del_query LIMIT $limit");
909 # die sprintf "$del_query LIMIT %s\n (%s, %s)", $limit, $batch_id, $itemnumber;
910 # $sth2->execute($batch_id, C4::Context->dbh->quote($data->{itemnumber}), $data->{count} - 1)
911 $sth2->execute($batch_id, $itemnumber) and
912 $killed += ($data->{count} - 1);
913 warn $sth2->errstr if $sth2->errstr;
915 return $killed, undef;
918 sub DrawSpineText {
920 my ( $x_pos, $y_pos, $label_height, $label_width, $fontname, $fontsize, $left_text_margin,
921 $text_wrap_cols, $item, $conf_data, $printingtype, $nowrap ) = @_;
923 # FIXME: we need to fix the column name mismatch betwen labels_conf.class, and bibitems.classification
924 $$item->{'class'} = $$item->{'classification'};
926 # Replaced item's itemtype with the more user-friendly description...
927 my $dbh = C4::Context->dbh;
928 my %itemtypes;
929 my $sth = $dbh->prepare("SELECT itemtype,description FROM itemtypes");
930 $sth->execute();
931 while ( my $data = $sth->fetchrow_hashref ) {
932 $$item->{'itemtype'} = $data->{'description'} if ($$item->{'itemtype'} eq $data->{'itemtype'});
935 $Text::Wrap::columns = $text_wrap_cols;
936 $Text::Wrap::separator = "\n";
938 my $str;
940 my $top_text_margin = ( $fontsize + 3 ); #FIXME: This should be a template parameter and passed in...
941 my $line_spacer = ( $fontsize * 1 ); # number of pixels between text rows (This is actually leading: baseline to baseline minus font size. Recommended starting point is 20% of font size.).
943 my $layout_id = $$conf_data->{'id'};
945 my $vPos = ( $y_pos + ( $label_height - $top_text_margin ) );
947 my @str_fields = get_text_fields($layout_id, 'codes' );
948 my @fields;
949 foreach my $field (@str_fields) {
950 push (@fields, $field->{'code'});
953 my $old_fontname = $fontname; # We need to keep track of the original font passed in...
955 foreach my $field (@fields) {
956 # This allows us to print the title in italic (oblique) type... (Times Roman has a different nomenclature.)
957 # It seems there should be a better way to handle fonts in the label/patron card tool altogether -fbcit
958 ($field eq 'title') ? (($old_fontname =~ /T/) ? ($fontname = 'TI') : ($fontname = ($old_fontname . 'O'))) : ($fontname = $old_fontname);
959 my $font = prFont($fontname);
960 # if the display option for this field is selected in the DB,
961 # and the item record has some values for this field, display it.
962 if ( $$conf_data->{"$field"} && $$item->{"$field"} ) {
963 # get the string
964 $str = $$item->{"$field"};
965 # strip out naughty existing nl/cr's
966 $str =~ s/\n//g;
967 $str =~ s/\r//g;
968 my @strings;
969 if ($field eq 'itemcallnumber') { # If the field contains the call number, we do some special processing on it here...
970 if (($nowrap == 0) || (!$nowrap)) { # wrap lines based on segmentation markers: '/' (other types of segmentation markers can be added as needed here or this could be added as a syspref.)
971 while ( $str =~ /\// ) {
972 $str =~ /^(.*)\/(.*)$/;
973 unshift @strings, $2;
974 $str = $1;
976 unshift @strings, $str;
977 } else {
978 push @strings, $str; # or if we are not wrapping the call number just send it along as we found it...
980 } else { # Here we will strip out all trailing '/' in fields other than the call number...
981 $str =~ s/\/$//g;
982 push @strings, $str;
984 # loop for each string line
985 foreach my $str (@strings) {
986 my $hPos;
987 if ( $printingtype eq 'BIB' ) { #FIXME: This is a hack and needs to be implimented as a text justification option in the template...
988 # some code to try and center each line on the label based on font size and string point width...
989 my $stringwidth = prStrWidth($str, $fontname, $fontsize);
990 my $whitespace = ( $label_width - ( $stringwidth + (2 * $left_text_margin) ) );
991 $hPos = ( ( $whitespace / 2 ) + $x_pos + $left_text_margin );
992 #warn "\$label_width=$label_width \$stringwidth=$stringwidth \$whitespace=$whitespace \$left_text_margin=$left_text_margin for $str\n";
993 } else {
994 $hPos = ( $x_pos + $left_text_margin );
996 PrintText( $hPos, $vPos, $font, $fontsize, $str );
997 $vPos = $vPos - $line_spacer;
999 } # if field is
1000 } #foreach feild
1003 sub PrintText {
1004 my ( $hPos, $vPos, $font, $fontsize, $text ) = @_;
1005 my $str = "BT /$font $fontsize Tf $hPos $vPos Td ($text) Tj ET";
1006 prAdd($str);
1009 sub DrawPatronCardText {
1011 my ( $x_pos, $y_pos, $label_height, $label_width, $fontname, $fontsize, $left_text_margin,
1012 $text_wrap_cols, $text, $printingtype )
1013 = @_;
1015 my $top_text_margin = 25; #FIXME: This should be a template parameter and passed in...
1017 my $vPos = ( $y_pos + ( $label_height - $top_text_margin ) );
1018 my $font = prFont($fontname);
1020 my $hPos;
1022 foreach my $line (keys %$text) {
1023 warn "Current text is \"$line\" and font size for \"$line\" is $text->{$line} points";
1024 # some code to try and center each line on the label based on font size and string point width...
1025 my $stringwidth = prStrWidth($line, $fontname, $text->{$line});
1026 my $whitespace = ( $label_width - ( $stringwidth + (2 * $left_text_margin) ) );
1027 $hPos = ( ( $whitespace / 2 ) + $x_pos + $left_text_margin );
1029 PrintText( $hPos, $vPos, $font, $text->{$line}, $line );
1030 my $line_spacer = ( $text->{$line} * 1 ); # number of pixels between text rows (This is actually leading: baseline to baseline minus font size. Recommended starting point is 20% (0.20) of font size.).
1031 $vPos = $vPos - ($line_spacer + $text->{$line}); # Linefeed equiv: leading + font size
1035 # Not used anywhere.
1037 #sub SetFontSize {
1039 # my ($fontsize) = @_;
1040 #### fontsize
1041 # my $str = "BT/F13 30 Tf288 720 Td( AAAAAAAAAA ) TjET";
1042 # prAdd($str);
1045 sub DrawBarcode {
1047 # x and y are from the top-left :)
1048 my ( $x_pos, $y_pos, $height, $width, $barcode, $barcodetype ) = @_;
1049 my $num_of_bars = length($barcode);
1050 my $bar_width = $width * .8; # %80 of length of label width
1051 my $tot_bar_length;
1052 my $bar_length;
1053 my $guard_length = 10;
1054 my $xsize_ratio;
1056 if ( $barcodetype eq 'CODE39' ) {
1057 $bar_length = '17.5';
1058 $tot_bar_length =
1059 ( $bar_length * $num_of_bars ) + ( $guard_length * 2 );
1060 $xsize_ratio = ( $bar_width / $tot_bar_length );
1061 eval {
1062 PDF::Reuse::Barcode::Code39(
1063 x => ( $x_pos + ( $width / 10 ) ),
1064 y => ( $y_pos + ( $height / 10 ) ),
1065 value => "*$barcode*",
1066 ySize => ( .02 * $height ),
1067 xSize => $xsize_ratio,
1068 hide_asterisk => 1,
1071 if ($@) {
1072 warn "$barcodetype, $barcode FAILED:$@";
1076 elsif ( $barcodetype eq 'CODE39MOD' ) {
1078 # get modulo43 checksum
1079 my $c39 = CheckDigits('code_39');
1080 $barcode = $c39->complete($barcode);
1082 $bar_length = '19';
1083 $tot_bar_length =
1084 ( $bar_length * $num_of_bars ) + ( $guard_length * 2 );
1085 $xsize_ratio = ( $bar_width / $tot_bar_length );
1086 eval {
1087 PDF::Reuse::Barcode::Code39(
1088 x => ( $x_pos + ( $width / 10 ) ),
1089 y => ( $y_pos + ( $height / 10 ) ),
1090 value => "*$barcode*",
1091 ySize => ( .02 * $height ),
1092 xSize => $xsize_ratio,
1093 hide_asterisk => 1,
1097 if ($@) {
1098 warn "$barcodetype, $barcode FAILED:$@";
1101 elsif ( $barcodetype eq 'CODE39MOD10' ) {
1103 # get modulo43 checksum
1104 my $c39_10 = CheckDigits('visa');
1105 $barcode = $c39_10->complete($barcode);
1107 $bar_length = '19';
1108 $tot_bar_length =
1109 ( $bar_length * $num_of_bars ) + ( $guard_length * 2 );
1110 $xsize_ratio = ( $bar_width / $tot_bar_length );
1111 eval {
1112 PDF::Reuse::Barcode::Code39(
1113 x => ( $x_pos + ( $width / 10 ) ),
1114 y => ( $y_pos + ( $height / 10 ) ),
1115 value => "*$barcode*",
1116 ySize => ( .02 * $height ),
1117 xSize => $xsize_ratio,
1118 hide_asterisk => 1,
1119 text => 0,
1123 if ($@) {
1124 warn "$barcodetype, $barcode FAILED:$@";
1129 elsif ( $barcodetype eq 'COOP2OF5' ) {
1130 $bar_length = '9.43333333333333';
1131 $tot_bar_length =
1132 ( $bar_length * $num_of_bars ) + ( $guard_length * 2 );
1133 $xsize_ratio = ( $bar_width / $tot_bar_length ) * .9;
1134 eval {
1135 PDF::Reuse::Barcode::COOP2of5(
1136 x => ( $x_pos + ( $width / 10 ) ),
1137 y => ( $y_pos + ( $height / 10 ) ),
1138 value => $barcode,
1139 ySize => ( .02 * $height ),
1140 xSize => $xsize_ratio,
1143 if ($@) {
1144 warn "$barcodetype, $barcode FAILED:$@";
1148 elsif ( $barcodetype eq 'INDUSTRIAL2OF5' ) {
1149 $bar_length = '13.1333333333333';
1150 $tot_bar_length =
1151 ( $bar_length * $num_of_bars ) + ( $guard_length * 2 );
1152 $xsize_ratio = ( $bar_width / $tot_bar_length ) * .9;
1153 eval {
1154 PDF::Reuse::Barcode::Industrial2of5(
1155 x => ( $x_pos + ( $width / 10 ) ),
1156 y => ( $y_pos + ( $height / 10 ) ),
1157 value => $barcode,
1158 ySize => ( .02 * $height ),
1159 xSize => $xsize_ratio,
1162 if ($@) {
1163 warn "$barcodetype, $barcode FAILED:$@";
1167 my $moo2 = $tot_bar_length * $xsize_ratio;
1169 warn "$x_pos, $y_pos, $barcode, $barcodetype" if $DEBUG;
1170 warn "BAR_WDTH = $bar_width, TOT.BAR.LGHT=$tot_bar_length R*TOT.BAR =$moo2" if $DEBUG;
1173 =item build_circ_barcode;
1175 build_circ_barcode( $x_pos, $y_pos, $barcode,
1176 $barcodetype, \$item);
1178 $item is the result of a previous call to GetLabelItems();
1180 =cut
1183 sub build_circ_barcode {
1184 my ( $x_pos_circ, $y_pos, $value, $barcodetype, $item ) = @_;
1186 #warn Dumper \$item;
1188 #warn "value = $value\n";
1190 #$DB::single = 1;
1192 if ( $barcodetype eq 'EAN13' ) {
1194 #testing EAN13 barcodes hack
1195 $value = $value . '000000000';
1196 $value =~ s/-//;
1197 $value = substr( $value, 0, 12 );
1199 #warn $value;
1200 eval {
1201 PDF::Reuse::Barcode::EAN13(
1202 x => ( $x_pos_circ + 27 ),
1203 y => ( $y_pos + 15 ),
1204 value => $value,
1206 # prolong => 2.96,
1207 # xSize => 1.5,
1209 # ySize => 1.2,
1211 # added for xpdf compat. doesnt use type3 fonts., but increases filesize from 20k to 200k
1212 # i think its embedding extra fonts in the pdf file.
1213 # mode => 'graphic',
1216 if ($@) {
1217 $item->{'barcodeerror'} = 1;
1219 #warn "EAN13BARCODE FAILED:$@";
1222 #warn $barcodetype;
1225 elsif ( $barcodetype eq 'Code39' ) {
1227 eval {
1228 PDF::Reuse::Barcode::Code39(
1229 x => ( $x_pos_circ + 9 ),
1230 y => ( $y_pos + 15 ),
1231 value => $value,
1233 # prolong => 2.96,
1234 xSize => .85,
1236 ySize => 1.3,
1239 if ($@) {
1240 $item->{'barcodeerror'} = 1;
1242 #warn "CODE39BARCODE $value FAILED:$@";
1245 #warn $barcodetype;
1249 elsif ( $barcodetype eq 'Matrix2of5' ) {
1251 #warn "MATRIX ELSE:";
1253 #testing MATRIX25 barcodes hack
1254 # $value = $value.'000000000';
1255 $value =~ s/-//;
1257 # $value = substr( $value, 0, 12 );
1258 #warn $value;
1260 eval {
1261 PDF::Reuse::Barcode::Matrix2of5(
1262 x => ( $x_pos_circ + 27 ),
1263 y => ( $y_pos + 15 ),
1264 value => $value,
1266 # prolong => 2.96,
1267 # xSize => 1.5,
1269 # ySize => 1.2,
1272 if ($@) {
1273 $item->{'barcodeerror'} = 1;
1275 #warn "BARCODE FAILED:$@";
1278 #warn $barcodetype;
1282 elsif ( $barcodetype eq 'EAN8' ) {
1284 #testing ean8 barcodes hack
1285 $value = $value . '000000000';
1286 $value =~ s/-//;
1287 $value = substr( $value, 0, 8 );
1289 #warn $value;
1291 #warn "EAN8 ELSEIF";
1292 eval {
1293 PDF::Reuse::Barcode::EAN8(
1294 x => ( $x_pos_circ + 42 ),
1295 y => ( $y_pos + 15 ),
1296 value => $value,
1297 prolong => 2.96,
1298 xSize => 1.5,
1300 # ySize => 1.2,
1304 if ($@) {
1305 $item->{'barcodeerror'} = 1;
1307 #warn "BARCODE FAILED:$@";
1310 #warn $barcodetype;
1314 elsif ( $barcodetype eq 'UPC-E' ) {
1315 eval {
1316 PDF::Reuse::Barcode::UPCE(
1317 x => ( $x_pos_circ + 27 ),
1318 y => ( $y_pos + 15 ),
1319 value => $value,
1320 prolong => 2.96,
1321 xSize => 1.5,
1323 # ySize => 1.2,
1327 if ($@) {
1328 $item->{'barcodeerror'} = 1;
1330 #warn "BARCODE FAILED:$@";
1333 #warn $barcodetype;
1336 elsif ( $barcodetype eq 'NW7' ) {
1337 eval {
1338 PDF::Reuse::Barcode::NW7(
1339 x => ( $x_pos_circ + 27 ),
1340 y => ( $y_pos + 15 ),
1341 value => $value,
1342 prolong => 2.96,
1343 xSize => 1.5,
1345 # ySize => 1.2,
1349 if ($@) {
1350 $item->{'barcodeerror'} = 1;
1352 #warn "BARCODE FAILED:$@";
1355 #warn $barcodetype;
1358 elsif ( $barcodetype eq 'ITF' ) {
1359 eval {
1360 PDF::Reuse::Barcode::ITF(
1361 x => ( $x_pos_circ + 27 ),
1362 y => ( $y_pos + 15 ),
1363 value => $value,
1364 prolong => 2.96,
1365 xSize => 1.5,
1367 # ySize => 1.2,
1371 if ($@) {
1372 $item->{'barcodeerror'} = 1;
1374 #warn "BARCODE FAILED:$@";
1377 #warn $barcodetype;
1380 elsif ( $barcodetype eq 'Industrial2of5' ) {
1381 eval {
1382 PDF::Reuse::Barcode::Industrial2of5(
1383 x => ( $x_pos_circ + 27 ),
1384 y => ( $y_pos + 15 ),
1385 value => $value,
1386 prolong => 2.96,
1387 xSize => 1.5,
1389 # ySize => 1.2,
1392 if ($@) {
1393 $item->{'barcodeerror'} = 1;
1395 #warn "BARCODE FAILED:$@";
1398 #warn $barcodetype;
1401 elsif ( $barcodetype eq 'IATA2of5' ) {
1402 eval {
1403 PDF::Reuse::Barcode::IATA2of5(
1404 x => ( $x_pos_circ + 27 ),
1405 y => ( $y_pos + 15 ),
1406 value => $value,
1407 prolong => 2.96,
1408 xSize => 1.5,
1410 # ySize => 1.2,
1413 if ($@) {
1414 $item->{'barcodeerror'} = 1;
1416 #warn "BARCODE FAILED:$@";
1419 #warn $barcodetype;
1423 elsif ( $barcodetype eq 'COOP2of5' ) {
1424 eval {
1425 PDF::Reuse::Barcode::COOP2of5(
1426 x => ( $x_pos_circ + 27 ),
1427 y => ( $y_pos + 15 ),
1428 value => $value,
1429 prolong => 2.96,
1430 xSize => 1.5,
1432 # ySize => 1.2,
1435 if ($@) {
1436 $item->{'barcodeerror'} = 1;
1438 #warn "BARCODE FAILED:$@";
1441 #warn $barcodetype;
1444 elsif ( $barcodetype eq 'UPC-A' ) {
1446 eval {
1447 PDF::Reuse::Barcode::UPCA(
1448 x => ( $x_pos_circ + 27 ),
1449 y => ( $y_pos + 15 ),
1450 value => $value,
1451 prolong => 2.96,
1452 xSize => 1.5,
1454 # ySize => 1.2,
1457 if ($@) {
1458 $item->{'barcodeerror'} = 1;
1460 #warn "BARCODE FAILED:$@";
1463 #warn $barcodetype;
1469 =item draw_boundaries
1471 sub draw_boundaries ($x_pos_spine, $x_pos_circ1, $x_pos_circ2,
1472 $y_pos, $spine_width, $label_height, $circ_width)
1474 This sub draws boundary lines where the label outlines are, to aid in printer testing, and debugging.
1476 =cut
1479 sub draw_boundaries {
1481 my (
1482 $x_pos_spine, $x_pos_circ1, $x_pos_circ2, $y_pos,
1483 $spine_width, $label_height, $circ_width
1484 ) = @_;
1486 my $y_pos_initial = ( ( 792 - 36 ) - 90 );
1487 $y_pos = $y_pos_initial; # FIXME - why are we ignoring the y_pos parameter by redefining it?
1488 my $i = 1;
1490 for ( $i = 1 ; $i <= 8 ; $i++ ) {
1492 &drawbox( $x_pos_spine, $y_pos, ($spine_width), ($label_height) );
1494 #warn "OLD BOXES x=$x_pos_spine, y=$y_pos, w=$spine_width, h=$label_height";
1495 &drawbox( $x_pos_circ1, $y_pos, ($circ_width), ($label_height) );
1496 &drawbox( $x_pos_circ2, $y_pos, ($circ_width), ($label_height) );
1498 $y_pos = ( $y_pos - $label_height );
1503 =item drawbox
1505 sub drawbox { $lower_left_x, $lower_left_y,
1506 $upper_right_x, $upper_right_y )
1508 this is a low level sub, that draws a pdf box, it is called by draw_boxes
1510 FYI: the $upper_right_x and $upper_right_y values are RELATIVE to $lower_left_x and $lower_left_y
1512 and $lower_left_x, $lower_left_y are ABSOLUTE, this caught me out!
1514 =cut
1517 sub drawbox {
1518 my ( $llx, $lly, $urx, $ury ) = @_;
1520 # warn "llx,y= $llx,$lly , urx,y=$urx,$ury \n";
1522 my $str = "q\n"; # save the graphic state
1523 $str .= "0.5 w\n"; # border color red
1524 $str .= "1.0 0.0 0.0 RG\n"; # border color red
1525 # $str .= "0.5 0.75 1.0 rg\n"; # fill color blue
1526 $str .= "1.0 1.0 1.0 rg\n"; # fill color white
1528 $str .= "$llx $lly $urx $ury re\n"; # a rectangle
1529 $str .= "B\n"; # fill (and a little more)
1530 $str .= "Q\n"; # save the graphic state
1532 prAdd($str);
1536 END { } # module clean-up code here (global destructor)
1539 __END__
1541 =back
1543 =head1 AUTHOR
1545 Mason James <mason@katipo.co.nz>
1547 =cut