MT 1587 (Follow-up) : CSV export for cart and shelves (export_format table creation)
[koha.git] / C4 / Labels.pm
index 9064d36..82c59cc 100644 (file)
@@ -69,9 +69,7 @@ C4::Labels - Functions for printing spine labels and barcodes in Koha
 
 =head1 FUNCTIONS
 
-=over 2
-
-=item get_label_options;
+=head2 get_label_options;
 
        $options = get_label_options()
 
@@ -261,19 +259,25 @@ sub get_text_fields {
 }
 
 =head2 sub add_batch
+
 =over 4
+
  add_batch($batch_type,\@batch_list);
  if $batch_list is supplied,
    create a new batch with those items.
  else, return the next available batch_id.
-=return
+
+=back
+
 =cut
 
 sub add_batch ($;$) {
        my $table = (@_ and 'patroncards' eq shift) ? 'patroncards' : 'labels';
     my $batch_list = (@_) ? shift : undef;
     my $dbh = C4::Context->dbh;
-    my $q ="SELECT MAX(DISTINCT batch_id) FROM $table";
+    # FIXME : batch_id  should be an auto_incr INT.  Temporarily casting as int ( see koha bug 2555 )
+    # until a label_batches table is added, and we can convert batch_id to int.
+    my $q ="SELECT MAX( CAST(batch_id AS SIGNED) ) FROM $table";
     my $sth = $dbh->prepare($q);
     $sth->execute();
     my ($batch_id) = $sth->fetchrow_array || 0;
@@ -509,8 +513,8 @@ sub add_layout {
 
     my (
         $barcodetype,  $title,                 $subtitle,      $isbn,       $issn,
-        $itemtype,     $bcn,            $dcn,        $classif,
-        $subclass,     $itemcallnumber, $author,     $tmpl_id,
+        $itemtype,     $bcn,            $text_justify,        $callnum_split,
+        $itemcallnumber, $author,     $tmpl_id,
         $printingtype, $guidebox,       $startlabel, $layoutname, $formatstring
     ) = @_;
 
@@ -520,15 +524,15 @@ sub add_layout {
     $sth2->execute();
     $query2 = "INSERT INTO labels_conf
             ( barcodetype, title, subtitle, isbn,issn, itemtype, barcode,
-              dewey, classification, subclass, itemcallnumber, author, printingtype,
+              text_justify, callnum_split, itemcallnumber, author, printingtype,
                 guidebox, startlabel, layoutname, formatstring, active )
-               values ( ?, ?,?, ?, ?, ?, ?, ?,  ?,?, ?, ?, ?, ?, ?,?,?, 1 )";
+               values ( ?, ?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?,?, 1 )";
     $sth2 = $dbh->prepare($query2);
     $sth2->execute(
         $barcodetype, $title, $subtitle, $isbn, $issn,
 
-        $itemtype, $bcn,            $dcn,    $classif,
-        $subclass, $itemcallnumber, $author, $printingtype,
+        $itemtype, $bcn,            $text_justify,    $callnum_split,
+        $itemcallnumber, $author, $printingtype,
         $guidebox, $startlabel,     $layoutname, $formatstring
     );
     $sth2->finish;
@@ -541,8 +545,8 @@ sub save_layout {
 
     my (
         $barcodetype,  $title,          $subtitle,     $isbn,       $issn,
-        $itemtype,     $bcn,            $dcn,        $classif,
-        $subclass,     $itemcallnumber, $author,     $tmpl_id,
+        $itemtype,     $bcn,            $text_justify,        $callnum_split,
+        $itemcallnumber, $author,     $tmpl_id,
         $printingtype, $guidebox,       $startlabel, $layoutname, $formatstring,
         $layout_id
     ) = @_;
@@ -552,14 +556,14 @@ sub save_layout {
     my $dbh    = C4::Context->dbh;
     my $query2 = "update labels_conf set 
              barcodetype=?, title=?, subtitle=?, isbn=?,issn=?, 
-            itemtype=?, barcode=?,    dewey=?, classification=?,
-             subclass=?, itemcallnumber=?, author=?,  printingtype=?,  
+            itemtype=?, barcode=?,    text_justify=?, callnum_split=?,
+            itemcallnumber=?, author=?,  printingtype=?,  
                guidebox=?, startlabel=?, layoutname=?, formatstring=? where id = ?";
     my $sth2 = $dbh->prepare($query2);
     $sth2->execute(
         $barcodetype, $title,          $subtitle,      $isbn,       $issn,
-        $itemtype,    $bcn,            $dcn,        $classif,
-        $subclass,    $itemcallnumber, $author,     $printingtype,
+        $itemtype,    $bcn,            $text_justify,        $callnum_split,
+        $itemcallnumber, $author,     $printingtype,
         $guidebox,    $startlabel,     $layoutname, $formatstring,  $layout_id
     );
     $sth2->finish;
@@ -567,7 +571,7 @@ sub save_layout {
     return;
 }
 
-=item GetAllPrinterProfiles;
+=head2 GetAllPrinterProfiles;
 
     @profiles = GetAllPrinterProfiles()
 
@@ -591,7 +595,7 @@ sub GetAllPrinterProfiles {
     return @resultsloop;
 }
 
-=item GetSinglePrinterProfile;
+=head2 GetSinglePrinterProfile;
 
     $profile = GetSinglePrinterProfile()
 
@@ -610,7 +614,7 @@ sub GetSinglePrinterProfile {
     return $template;
 }
 
-=item SaveProfile;
+=head2 SaveProfile;
 
     SaveProfile('parameters')
 
@@ -634,7 +638,7 @@ sub SaveProfile {
     $sth->finish;
 }
 
-=item CreateProfile;
+=head2 CreateProfile;
 
     CreateProfile('parameters')
 
@@ -663,7 +667,7 @@ sub CreateProfile {
     return $error;
 }
 
-=item DeleteProfile;
+=head2 DeleteProfile;
 
     DeleteProfile(prof_id)
 
@@ -682,7 +686,7 @@ sub DeleteProfile {
     return $error;
 }
 
-=item GetAssociatedProfile;
+=head2 GetAssociatedProfile;
 
     $assoc_prof = GetAssociatedProfile(tmpl_id)
 
@@ -705,7 +709,7 @@ sub GetAssociatedProfile {
     return $assoc_prof;
 }
 
-=item SetAssociatedProfile;
+=head2 SetAssociatedProfile;
 
     SetAssociatedProfile($prof_id, $tmpl_id)
 
@@ -726,7 +730,7 @@ sub SetAssociatedProfile {
 }
 
 
-=item GetLabelItems;
+=head2 GetLabelItems;
 
         $options = GetLabelItems()
 
@@ -765,9 +769,10 @@ sub GetLabelItems {
 
         # lets get some summary info from each item
         my $query1 =
-#            FIXME This makes for a very bulky data structure; data from tables w/duplicate col names also gets overwritten...
+#            FIXME This makes for a very bulky data structure; data from tables w/duplicate col names also gets overwritten.
+#            Something like this, perhaps, but this also causes problems because we need more fields sometimes.
 #            SELECT i.barcode, i.itemcallnumber, i.itype, bi.isbn, bi.issn, b.title, b.author
-           "SELECT i.*, bi.*, b.*
+           "SELECT bi.*, i.*, b.*
             FROM items AS i, biblioitems AS bi ,biblio AS b
             WHERE itemnumber=? AND i.biblioitemnumber=bi.biblioitemnumber AND bi.biblionumber=b.biblionumber";
         my $sth1 = $dbh->prepare($query1);
@@ -799,15 +804,18 @@ sub GetItemFields {
     return @fields;
 }
 
-=head GetBarcodeData
+=head2 GetBarcodeData
 
 =over 4
+
 Parse labels_conf.formatstring value
 (one value of the csv, which has already been split)
 and return string from koha tables or MARC record.
+
 =back
+
 =cut
-#'
+
 sub GetBarcodeData {
     my ( $f, $item, $record ) = @_;
     my $kohatables = &_descKohaTables();
@@ -820,7 +828,7 @@ sub GetBarcodeData {
             @{ $kohatables->{items} }
         )
     );
-    while ($f) {
+    while ($f) {  
         $f =~ s/^\s?//;
         if ( $f =~ /^'(.*)'.*/ ) {
             # single quotes indicate a static text string.
@@ -832,25 +840,37 @@ sub GetBarcodeData {
             $f = $';
         }
         elsif ( $f =~ /^([0-9a-z]{3})(\w)(\W?).*?/ ) {
-            my $marc_field = $1;
-            foreach my $subfield ($record->field($marc_field)) {
-                if ( $subfield->subfield('9') eq $item->{'itemnumber'} ) {
-                    $datastring .= $subfield->subfield($2 ) . $3;
-                    last;
+            my ($field,$subf,$ws) = ($1,$2,$3);
+            my $subf_data;
+            my ($itemtag, $itemsubfieldcode) = &GetMarcFromKohaField("items.itemnumber",'');
+            my @marcfield = $record->field($field);
+            if(@marcfield) {
+                if($field eq $itemtag) {  # item-level data, we need to get the right item.
+                    foreach my $itemfield (@marcfield) {
+                        if ( $itemfield->subfield($itemsubfieldcode) eq $item->{'itemnumber'} ) {
+                            $datastring .= $itemfield->subfield($subf ) . $ws;
+                            last;
+                        }
+                    }
+                } else {  # bib-level data, we'll take the first matching tag/subfield.
+                    $datastring .= $marcfield[0]->subfield($subf) . $ws ;
                 }
             }
             $f = $';
         }
         else {
+            warn "failed to parse label formatstring: $f";
             last;    # Failed to match
         }
     }
     return $datastring;
 }
 
-=head descKohaTables
+=head2 descKohaTables
+
 Return a hashref of an array of hashes,
 with name,type keys.
+
 =cut
 
 sub _descKohaTables {
@@ -929,47 +949,78 @@ sub deduplicate_batch {
        return $killed, undef;
 }
 
+our $possible_decimal = qr/\d+(?:\.\d+)?/;
+
 sub split_lccn {
     my ($lccn) = @_;    
-    my ($ll, $wnl, $dec, $cutter, $pubdate) = (0, 0, 0, 0, 0);
     $_ = $lccn;
-    # lccn example 'HE8700.7 .P6T44 1983';
-    my    @splits   = m/
-        (^[a-zA-Z]+)            # HE
-        ([0-9]+\.*[0-9]*)             # 8700.7
+    # lccn examples: 'HE8700.7 .P6T44 1983', 'BS2545.E8 H39 1996';
+    my (@parts) = m/
+        ^([a-zA-Z]+)      # HE          # BS
+        (\d+(?:\.\d)*)    # 8700.7      # 2545
         \s*
-        (\.*[a-zA-Z0-9]*)       # P6T44
+        (\.*\D+\d*)       # .P6         # .E8
         \s*
-        ([0-9]*)                # 1983
-        /x;  
-
-    # strip something occuring spaces too
-    $splits[0] =~ s/\s+$//;
-    $splits[1] =~ s/\s+$//;
-    $splits[2] =~ s/\s+$//;
-
-    # if the regex fails, then just return the whole string, 
-    # better than nothing
-    # FIXME It seems we should handle all cases, have some graceful error handling, or at least inform the caller of the failure to split
-    $splits[0] = $lccn if  $splits[0]  eq '' ;
-    return @splits;
+        (.*)              # T44 1983    # H39 1996   # everything else (except any bracketing spaces)
+        \s*
+        /x;
+    unless (scalar @parts)  {
+        $debug and print STDERR "split_lccn regexp failed to match string: $_\n";
+        push @parts, $_;     # if no match, just push the whole string.
+    }
+    push @parts, split /\s+/, pop @parts;   # split the last piece into an arbitrary number of pieces at spaces
+    $debug and print STDERR "split_lccn array: ", join(" | ", @parts), "\n";
+    return @parts;
 }
 
 sub split_ddcn {
     my ($ddcn) = @_;
-    $ddcn =~ s/\///g;   # in theory we should be able to simply remove all segmentation markers and arrive at the correct call number...
     $_ = $ddcn;
-    # ddcn example R220.3 H2793Z H32 c.2
-    my @splits = m/^([A-Z]{0,3})                # R (OS, REF, etc. up do three letters)
-                    ([0-9]+\.[0-9]*)            # 220.3
-                    \s?                         # space (not requiring anything beyond the call number)
-                    ([a-zA-Z0-9]*\.?[a-zA-Z0-9])# cutter number... maybe, but if so it is in this position (Z indicates literary criticism)
-                    \s?                         # space if it exists
-                    ([a-zA-Z]*\.?[0-9]*)        # other indicators such as cutter for author of literary criticism in this example if it exists
-                    \s?                         # space if ie exists
-                    ([a-zA-Z]*\.?[0-9]*)        # other indicators such as volume number, copy number, edition date, etc. if it exists
-                    /x;
-    return @splits;
+    s/\///g;   # in theory we should be able to simply remove all segmentation markers and arrive at the correct call number...
+    # ddcn examples: 'R220.3 H2793Z H32 c.2', 'BIO JP2 R5c.1'
+
+    my (@parts) = m/
+        ^([a-zA-Z-]+[0-9]?-?[a-zA-Z]*(?:$possible_decimal)?) # R220.3            # BIO   # first example will require extra splitting
+        \s*
+        (.+)                               # H2793Z H32 c.2   # R5c.1   # everything else (except bracketing spaces)
+        \s*
+        /x;
+    unless (scalar @parts)  {
+        $debug and print STDERR "split_ddcn regexp failed to match string: $_\n";
+        push @parts, $_;     # if no match, just push the whole string.
+    }
+
+    if ($parts[ 0] =~ /^([a-zA-Z]+)($possible_decimal)$/) {
+          shift @parts;         # pull off the mathching first element, like example 1
+        unshift @parts, $1, $2; # replace it with the two pieces
+    }
+
+    push @parts, split /\s+/, pop @parts;   # split the last piece into an arbitrary number of pieces at spaces
+
+    if ($parts[-1] !~ /^.*\d-\d.*$/ && $parts[-1] =~ /^(.*\d+)(\D.*)$/) {
+         pop @parts;            # pull off the mathching last element, like example 2
+        push @parts, $1, $2;    # replace it with the two pieces
+    }
+
+    $debug and print STDERR "split_ddcn array: ", join(" | ", @parts), "\n";
+    return @parts;
+}
+
+sub split_fcn {
+    my ($fcn) = @_;
+    my @fcn_split = ();
+    # Split fiction call numbers based on spaces
+    SPLIT_FCN:
+    while ($fcn) {
+        if ($fcn =~ m/([A-Za-z0-9]+\.?[0-9]?)(\W?).*?/x) {
+            push (@fcn_split, $1);
+            $fcn = $';
+        }
+        else {
+            last SPLIT_FCN;     # No match, break out of the loop
+        }
+    }
+    return @fcn_split;
 }
 
 sub DrawSpineText {
@@ -995,23 +1046,26 @@ sub DrawSpineText {
 
     my $vPos = ( $y_pos + ( $label_height - $top_text_margin ) );
 
-    my @str_fields = get_text_fields($layout_id, 'codes' );
+    my @str_fields = get_text_fields($layout_id, 'codes' );  
     my $record = GetMarcBiblio($$item->{biblionumber});
     # FIXME - returns all items, so you can't get data from an embedded holdings field.
     # TODO - add a GetMarcBiblio1item(bibnum,itemnum) or a GetMarcItem(itemnum).
 
     my $old_fontname = $fontname; # We need to keep track of the original font passed in...
-    my $cn_source = $record->subfield('952','2');
+
+    # Grab the cn_source and if that is NULL, the DefaultClassificationSource syspref
+    my $cn_source = ($$item->{'cn_source'} ? $$item->{'cn_source'} : C4::Context->preference('DefaultClassificationSource'));
     for my $field (@str_fields) {
         $field->{'code'} or warn "get_text_fields($layout_id, 'codes') element missing 'code' field";
-        if ($$conf_data->{'formatstring'}) {
-                $field->{'data'} =  GetBarcodeData($field->{'code'},$$item,$record) ;
-        }
-        elsif ($field->{'code'} eq 'itemtype') {
+        if ($field->{'code'} eq 'itemtype') {
             $field->{'data'} = C4::Context->preference('item-level_itypes') ? $$item->{'itype'} : $$item->{'itemtype'};
         }
+        elsif ($$conf_data->{'formatstring'}) {
+            # if labels_conf.formatstring has a value, then it overrides the  hardcoded option.
+            $field->{'data'} =  GetBarcodeData($field->{'code'},$$item,$record) ;
+        }
         else {
-                $field->{data} =   $$item->{$field->{'code'}}  ;
+            $field->{data} =   $$item->{$field->{'code'}}  ;
         }
         # This allows us to print the title in italic (oblique) type... (Times Roman has a different nomenclature.)
         # It seems there should be a better way to handle fonts in the label/patron card tool altogether -fbcit
@@ -1027,12 +1081,16 @@ sub DrawSpineText {
             $str =~ s/\n//g;
             $str =~ s/\r//g;
             my @strings;
-            my @callnumber_list = ('itemcallnumber', '050a', '050b', '082a', '952o'); # Fields which hold call number data
-            if ((grep {$field->{code} =~ m/$_/} @callnumber_list) and ($printingtype eq 'BIB')) { # If the field contains the call number, we do some sp
+            my @callnumber_list = ('itemcallnumber', '050a', '050b', '082a', '952o'); # Fields which hold call number data  ( 060? 090? 092? 099? )
+            if ((grep {$field->{code} =~ m/$_/} @callnumber_list) and ($printingtype eq 'BIB') and ($$conf_data->{'callnum_split'})) { # If the field contains the call number, we do some sp
                 if ($cn_source eq 'lcc') {
                     @strings = split_lccn($str);
+                    @strings = split_fcn($str) if !@strings;    # If it was not a true lccn, try it as a fiction call number
+                    push (@strings, $str) if !@strings;         # If it was not that, send it on unsplit
                 } elsif ($cn_source eq 'ddc') {
                     @strings = split_ddcn($str);
+                    @strings = split_fcn($str) if !@strings;
+                    push (@strings, $str) if !@strings;
                 } else {
                     # FIXME Need error trapping here; something to be informative to the user perhaps -crn
                     push @strings, $str;
@@ -1058,13 +1116,16 @@ sub DrawSpineText {
             }
             # loop for each string line
             foreach my $str (@strings) {
+                next if $str eq '';
                 my $hPos = 0;
-                if ( $printingtype eq 'BIB' ) { #FIXME: This is a hack and needs to be implimented as a text justification option in the template...
-                    # some code to try and center each line on the label based on font size and string point width...
-                    my $stringwidth = prStrWidth($str, $fontname, $fontsize);
-                    my $whitespace = ( $label_width - ( $stringwidth + (2 * $left_text_margin) ) );
-                    $hPos = ( ( $whitespace  / 2 ) + $x_pos + $left_text_margin );
-                    #warn "\$label_width=$label_width \$stringwidth=$stringwidth \$whitespace=$whitespace \$left_text_margin=$left_text_margin for $str\n";
+                my $stringwidth = prStrWidth($str, $fontname, $fontsize);
+                if ( $$conf_data->{'text_justify'} eq 'R' ) { 
+                    $hPos = $x_pos + $label_width - ( $left_text_margin + $stringwidth );
+                } elsif($$conf_data->{'text_justify'} eq 'C') {
+                     # some code to try and center each line on the label based on font size and string point width...
+                     my $whitespace = ( $label_width - ( $stringwidth + (2 * $left_text_margin) ) );
+                     $hPos = ( ( $whitespace  / 2 ) + $x_pos + $left_text_margin );
+                #warn "\$label_width=$label_width \$stringwidth=$stringwidth \$whitespace=$whitespace \$left_text_margin=$left_text_margin for $str\n";
                 } else {
                     $hPos = ( $x_pos + $left_text_margin );
                 }
@@ -1141,6 +1202,7 @@ sub DrawBarcode {
                 ySize         => ( .02 * $height ),
                 xSize         => $xsize_ratio,
                 hide_asterisk => 1,
+                mode          => 'graphic',  # the only other option here is Type3...
             );
         };
         if ($@) {
@@ -1166,6 +1228,7 @@ sub DrawBarcode {
                 ySize         => ( .02 * $height ),
                 xSize         => $xsize_ratio,
                 hide_asterisk => 1,
+                mode          => 'graphic',  # the only other option here is Type3...
             );
         };
 
@@ -1191,7 +1254,8 @@ sub DrawBarcode {
                 ySize         => ( .02 * $height ),
                 xSize         => $xsize_ratio,
                 hide_asterisk => 1,
-                               text         => 0, 
+               text          => 0, 
+                mode          => 'graphic',  # the only other option here is Type3...
             );
         };
 
@@ -1245,7 +1309,7 @@ sub DrawBarcode {
     warn "BAR_WDTH = $bar_width, TOT.BAR.LGHT=$tot_bar_length  R*TOT.BAR =$moo2" if $debug;
 }
 
-=item build_circ_barcode;
+=head2 build_circ_barcode;
 
   build_circ_barcode( $x_pos, $y_pos, $barcode,
                $barcodetype, \$item);
@@ -1309,6 +1373,7 @@ sub build_circ_barcode {
                 xSize => .85,
 
                 ySize => 1.3,
+                mode            => 'graphic',  # the only other option here is Type3...
             );
         };
         if ($@) {
@@ -1541,7 +1606,7 @@ sub build_circ_barcode {
 
 }
 
-=item draw_boundaries
+=head2 draw_boundaries
 
  sub draw_boundaries ($x_pos_spine, $x_pos_circ1, $x_pos_circ2,
                 $y_pos, $spine_width, $label_height, $circ_width)  
@@ -1575,7 +1640,7 @@ sub draw_boundaries {
     }
 }
 
-=item drawbox
+=head2 drawbox
 
        sub drawbox {   $lower_left_x, $lower_left_y, 
                        $upper_right_x, $upper_right_y )
@@ -1613,8 +1678,6 @@ END { }    # module clean-up code here (global destructor)
 1;
 __END__
 
-=back
-
 =head1 AUTHOR
 
 Mason James <mason@katipo.co.nz>