Bug 24435: Add Koha::Biblio->items_count
[koha.git] / Koha / Biblio.pm
1 package Koha::Biblio;
2
3 # Copyright ByWater Solutions 2014
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 3 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 Modern::Perl;
21
22 use Carp;
23 use List::MoreUtils qw(any);
24 use URI;
25 use URI::Escape;
26
27 use C4::Koha;
28 use C4::Biblio qw();
29
30 use Koha::Database;
31 use Koha::DateUtils qw( dt_from_string );
32
33 use base qw(Koha::Object);
34
35 use Koha::Acquisition::Orders;
36 use Koha::ArticleRequest::Status;
37 use Koha::ArticleRequests;
38 use Koha::Biblio::Metadatas;
39 use Koha::Biblioitems;
40 use Koha::IssuingRules;
41 use Koha::Item::Transfer::Limits;
42 use Koha::Items;
43 use Koha::Libraries;
44 use Koha::Suggestions;
45 use Koha::Subscriptions;
46
47 =head1 NAME
48
49 Koha::Biblio - Koha Biblio Object class
50
51 =head1 API
52
53 =head2 Class Methods
54
55 =cut
56
57 =head3 store
58
59 Overloaded I<store> method to set default values
60
61 =cut
62
63 sub store {
64     my ( $self ) = @_;
65
66     $self->datecreated( dt_from_string ) unless $self->datecreated;
67
68     return $self->SUPER::store;
69 }
70
71 =head3 metadata
72
73 my $metadata = $biblio->metadata();
74
75 Returns a Koha::Biblio::Metadata object
76
77 =cut
78
79 sub metadata {
80     my ( $self ) = @_;
81
82     my $metadata = $self->_result->metadata;
83     return Koha::Biblio::Metadata->_new_from_dbic($metadata);
84 }
85
86 =head3 orders
87
88 my $orders = $biblio->orders();
89
90 Returns a Koha::Acquisition::Orders object
91
92 =cut
93
94 sub orders {
95     my ( $self ) = @_;
96
97     my $orders = $self->_result->orders;
98     return Koha::Acquisition::Orders->_new_from_dbic($orders);
99 }
100
101 =head3 active_orders_count
102
103 my $orders_count = $biblio->active_orders_count();
104
105 Returns the number of active acquisition orders related to this biblio.
106 An order is considered active when it is not cancelled (i.e. when datecancellation
107 is not undef).
108
109 =cut
110
111 sub active_orders_count {
112     my ( $self ) = @_;
113
114     return $self->orders->search({ datecancellationprinted => undef })->count;
115 }
116
117 =head3 can_article_request
118
119 my $bool = $biblio->can_article_request( $borrower );
120
121 Returns true if article requests can be made for this record
122
123 $borrower must be a Koha::Patron object
124
125 =cut
126
127 sub can_article_request {
128     my ( $self, $borrower ) = @_;
129
130     my $rule = $self->article_request_type($borrower);
131     return q{} if $rule eq 'item_only' && !$self->items()->count();
132     return 1 if $rule && $rule ne 'no';
133
134     return q{};
135 }
136
137 =head3 can_be_transferred
138
139 $biblio->can_be_transferred({ to => $to_library, from => $from_library })
140
141 Checks if at least one item of a biblio can be transferred to given library.
142
143 This feature is controlled by two system preferences:
144 UseBranchTransferLimits to enable / disable the feature
145 BranchTransferLimitsType to use either an itemnumber or ccode as an identifier
146                          for setting the limitations
147
148 Performance-wise, it is recommended to use this method for a biblio instead of
149 iterating each item of a biblio with Koha::Item->can_be_transferred().
150
151 Takes HASHref that can have the following parameters:
152     MANDATORY PARAMETERS:
153     $to   : Koha::Library
154     OPTIONAL PARAMETERS:
155     $from : Koha::Library # if given, only items from that
156                           # holdingbranch are considered
157
158 Returns 1 if at least one of the item of a biblio can be transferred
159 to $to_library, otherwise 0.
160
161 =cut
162
163 sub can_be_transferred {
164     my ($self, $params) = @_;
165
166     my $to   = $params->{to};
167     my $from = $params->{from};
168
169     return 1 unless C4::Context->preference('UseBranchTransferLimits');
170     my $limittype = C4::Context->preference('BranchTransferLimitsType');
171
172     my $items;
173     foreach my $item_of_bib ($self->items->as_list) {
174         next unless $item_of_bib->holdingbranch;
175         next if $from && $from->branchcode ne $item_of_bib->holdingbranch;
176         return 1 if $item_of_bib->holdingbranch eq $to->branchcode;
177         my $code = $limittype eq 'itemtype'
178             ? $item_of_bib->effective_itemtype
179             : $item_of_bib->ccode;
180         return 1 unless $code;
181         $items->{$code}->{$item_of_bib->holdingbranch} = 1;
182     }
183
184     # At this point we will have a HASHref containing each itemtype/ccode that
185     # this biblio has, inside which are all of the holdingbranches where those
186     # items are located at. Then, we will query Koha::Item::Transfer::Limits to
187     # find out whether a transfer limits for such $limittype from any of the
188     # listed holdingbranches to the given $to library exist. If at least one
189     # holdingbranch for that $limittype does not have a transfer limit to given
190     # $to library, then we know that the transfer is possible.
191     foreach my $code (keys %{$items}) {
192         my @holdingbranches = keys %{$items->{$code}};
193         return 1 if Koha::Item::Transfer::Limits->search({
194             toBranch => $to->branchcode,
195             fromBranch => { 'in' => \@holdingbranches },
196             $limittype => $code
197         }, {
198             group_by => [qw/fromBranch/]
199         })->count == scalar(@holdingbranches) ? 0 : 1;
200     }
201
202     return 0;
203 }
204
205
206 =head3 pickup_locations
207
208 @pickup_locations = $biblio->pickup_locations( {patron => $patron } )
209
210 Returns possible pickup locations for this biblio items, according to patron's home library (if patron is defined and holds are allowed only from hold groups)
211 and if item can be transferred to each pickup location.
212
213 =cut
214
215 sub pickup_locations {
216     my ($self, $params) = @_;
217
218     my $patron = $params->{patron};
219
220     my @pickup_locations;
221     foreach my $item_of_bib ($self->items->as_list) {
222         push @pickup_locations, $item_of_bib->pickup_locations( {patron => $patron} );
223     }
224
225     my %seen;
226     @pickup_locations =
227       grep { !$seen{ $_->branchcode }++ } @pickup_locations;
228
229     return wantarray ? @pickup_locations : \@pickup_locations;
230 }
231
232 =head3 hidden_in_opac
233
234 my $bool = $biblio->hidden_in_opac({ [ rules => $rules ] })
235
236 Returns true if the biblio matches the hidding criteria defined in $rules.
237 Returns false otherwise.
238
239 Takes HASHref that can have the following parameters:
240     OPTIONAL PARAMETERS:
241     $rules : { <field> => [ value_1, ... ], ... }
242
243 Note: $rules inherits its structure from the parsed YAML from reading
244 the I<OpacHiddenItems> system preference.
245
246 =cut
247
248 sub hidden_in_opac {
249     my ( $self, $params ) = @_;
250
251     my $rules = $params->{rules} // {};
252
253     my @items = $self->items->as_list;
254
255     return 0 unless @items; # Do not hide if there is no item
256
257     return !(any { !$_->hidden_in_opac({ rules => $rules }) } @items);
258 }
259
260 =head3 article_request_type
261
262 my $type = $biblio->article_request_type( $borrower );
263
264 Returns the article request type based on items, or on the record
265 itself if there are no items.
266
267 $borrower must be a Koha::Patron object
268
269 =cut
270
271 sub article_request_type {
272     my ( $self, $borrower ) = @_;
273
274     return q{} unless $borrower;
275
276     my $rule = $self->article_request_type_for_items( $borrower );
277     return $rule if $rule;
278
279     # If the record has no items that are requestable, go by the record itemtype
280     $rule = $self->article_request_type_for_bib($borrower);
281     return $rule if $rule;
282
283     return q{};
284 }
285
286 =head3 article_request_type_for_bib
287
288 my $type = $biblio->article_request_type_for_bib
289
290 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record
291
292 =cut
293
294 sub article_request_type_for_bib {
295     my ( $self, $borrower ) = @_;
296
297     return q{} unless $borrower;
298
299     my $borrowertype = $borrower->categorycode;
300     my $itemtype     = $self->itemtype();
301
302     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule({ categorycode => $borrowertype, itemtype => $itemtype });
303
304     return q{} unless $issuing_rule;
305     return $issuing_rule->article_requests || q{}
306 }
307
308 =head3 article_request_type_for_items
309
310 my $type = $biblio->article_request_type_for_items
311
312 Returns the article request type 'yes', 'no', 'item_only', 'bib_only', for the given record's items
313
314 If there is a conflict where some items are 'bib_only' and some are 'item_only', 'bib_only' will be returned.
315
316 =cut
317
318 sub article_request_type_for_items {
319     my ( $self, $borrower ) = @_;
320
321     my $counts;
322     foreach my $item ( $self->items()->as_list() ) {
323         my $rule = $item->article_request_type($borrower);
324         return $rule if $rule eq 'bib_only';    # we don't need to go any further
325         $counts->{$rule}++;
326     }
327
328     return 'item_only' if $counts->{item_only};
329     return 'yes'       if $counts->{yes};
330     return 'no'        if $counts->{no};
331     return q{};
332 }
333
334 =head3 article_requests
335
336 my @requests = $biblio->article_requests
337
338 Returns the article requests associated with this Biblio
339
340 =cut
341
342 sub article_requests {
343     my ( $self, $borrower ) = @_;
344
345     $self->{_article_requests} ||= Koha::ArticleRequests->search( { biblionumber => $self->biblionumber() } );
346
347     return wantarray ? $self->{_article_requests}->as_list : $self->{_article_requests};
348 }
349
350 =head3 article_requests_current
351
352 my @requests = $biblio->article_requests_current
353
354 Returns the article requests associated with this Biblio that are incomplete
355
356 =cut
357
358 sub article_requests_current {
359     my ( $self, $borrower ) = @_;
360
361     $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
362         {
363             biblionumber => $self->biblionumber(),
364             -or          => [
365                 { status => Koha::ArticleRequest::Status::Pending },
366                 { status => Koha::ArticleRequest::Status::Processing }
367             ]
368         }
369     );
370
371     return wantarray ? $self->{_article_requests_current}->as_list : $self->{_article_requests_current};
372 }
373
374 =head3 article_requests_finished
375
376 my @requests = $biblio->article_requests_finished
377
378 Returns the article requests associated with this Biblio that are completed
379
380 =cut
381
382 sub article_requests_finished {
383     my ( $self, $borrower ) = @_;
384
385     $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
386         {
387             biblionumber => $self->biblionumber(),
388             -or          => [
389                 { status => Koha::ArticleRequest::Status::Completed },
390                 { status => Koha::ArticleRequest::Status::Canceled }
391             ]
392         }
393     );
394
395     return wantarray ? $self->{_article_requests_finished}->as_list : $self->{_article_requests_finished};
396 }
397
398 =head3 items
399
400 my $items = $biblio->items();
401
402 Returns the related Koha::Items object for this biblio
403
404 =cut
405
406 sub items {
407     my ($self) = @_;
408
409     my $items_rs = $self->_result->items;
410
411     return Koha::Items->_new_from_dbic( $items_rs );
412 }
413
414 =head3 items_count
415
416 my $items_count = $biblio->items();
417
418 Returns the count of the the related Koha::Items object for this biblio
419
420 =cut
421
422 sub items_count {
423     my ($self) = @_;
424
425     return $self->_result->items->count;
426 }
427
428 =head3 itemtype
429
430 my $itemtype = $biblio->itemtype();
431
432 Returns the itemtype for this record.
433
434 =cut
435
436 sub itemtype {
437     my ( $self ) = @_;
438
439     return $self->biblioitem()->itemtype();
440 }
441
442 =head3 holds
443
444 my $holds = $biblio->holds();
445
446 return the current holds placed on this record
447
448 =cut
449
450 sub holds {
451     my ( $self, $params, $attributes ) = @_;
452     $attributes->{order_by} = 'priority' unless exists $attributes->{order_by};
453     my $hold_rs = $self->_result->reserves->search( $params, $attributes );
454     return Koha::Holds->_new_from_dbic($hold_rs);
455 }
456
457 =head3 current_holds
458
459 my $holds = $biblio->current_holds
460
461 Return the holds placed on this bibliographic record.
462 It does not include future holds.
463
464 =cut
465
466 sub current_holds {
467     my ($self) = @_;
468     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
469     return $self->holds(
470         { reservedate => { '<=' => $dtf->format_date(dt_from_string) } } );
471 }
472
473 =head3 biblioitem
474
475 my $field = $self->biblioitem()->itemtype
476
477 Returns the related Koha::Biblioitem object for this Biblio object
478
479 =cut
480
481 sub biblioitem {
482     my ($self) = @_;
483
484     $self->{_biblioitem} ||= Koha::Biblioitems->find( { biblionumber => $self->biblionumber() } );
485
486     return $self->{_biblioitem};
487 }
488
489 =head3 suggestions
490
491 my $suggestions = $self->suggestions
492
493 Returns the related Koha::Suggestions object for this Biblio object
494
495 =cut
496
497 sub suggestions {
498     my ($self) = @_;
499
500     my $suggestions_rs = $self->_result->suggestions;
501     return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
502 }
503
504 =head3 subscriptions
505
506 my $subscriptions = $self->subscriptions
507
508 Returns the related Koha::Subscriptions object for this Biblio object
509
510 =cut
511
512 sub subscriptions {
513     my ($self) = @_;
514
515     $self->{_subscriptions} ||= Koha::Subscriptions->search( { biblionumber => $self->biblionumber } );
516
517     return $self->{_subscriptions};
518 }
519
520 =head3 has_items_waiting_or_intransit
521
522 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
523
524 Tells if this bibliographic record has items waiting or in transit.
525
526 =cut
527
528 sub has_items_waiting_or_intransit {
529     my ( $self ) = @_;
530
531     if ( Koha::Holds->search({ biblionumber => $self->id,
532                                found => ['W', 'T'] })->count ) {
533         return 1;
534     }
535
536     foreach my $item ( $self->items->as_list ) {
537         return 1 if $item->get_transfer;
538     }
539
540     return 0;
541 }
542
543 =head2 get_coins
544
545 my $coins = $biblio->get_coins;
546
547 Returns the COinS (a span) which can be included in a biblio record
548
549 =cut
550
551 sub get_coins {
552     my ( $self ) = @_;
553
554     my $record = $self->metadata->record;
555
556     my $pos7 = substr $record->leader(), 7, 1;
557     my $pos6 = substr $record->leader(), 6, 1;
558     my $mtx;
559     my $genre;
560     my ( $aulast, $aufirst ) = ( '', '' );
561     my @authors;
562     my $title;
563     my $hosttitle;
564     my $pubyear   = '';
565     my $isbn      = '';
566     my $issn      = '';
567     my $publisher = '';
568     my $pages     = '';
569     my $titletype = '';
570
571     # For the purposes of generating COinS metadata, LDR/06-07 can be
572     # considered the same for UNIMARC and MARC21
573     my $fmts6 = {
574         'a' => 'book',
575         'b' => 'manuscript',
576         'c' => 'book',
577         'd' => 'manuscript',
578         'e' => 'map',
579         'f' => 'map',
580         'g' => 'film',
581         'i' => 'audioRecording',
582         'j' => 'audioRecording',
583         'k' => 'artwork',
584         'l' => 'document',
585         'm' => 'computerProgram',
586         'o' => 'document',
587         'r' => 'document',
588     };
589     my $fmts7 = {
590         'a' => 'journalArticle',
591         's' => 'journal',
592     };
593
594     $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
595
596     if ( $genre eq 'book' ) {
597             $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
598     }
599
600     ##### We must transform mtx to a valable mtx and document type ####
601     if ( $genre eq 'book' ) {
602             $mtx = 'book';
603             $titletype = 'b';
604     } elsif ( $genre eq 'journal' ) {
605             $mtx = 'journal';
606             $titletype = 'j';
607     } elsif ( $genre eq 'journalArticle' ) {
608             $mtx   = 'journal';
609             $genre = 'article';
610             $titletype = 'a';
611     } else {
612             $mtx = 'dc';
613     }
614
615     if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
616
617         # Setting datas
618         $aulast  = $record->subfield( '700', 'a' ) || '';
619         $aufirst = $record->subfield( '700', 'b' ) || '';
620         push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
621
622         # others authors
623         if ( $record->field('200') ) {
624             for my $au ( $record->field('200')->subfield('g') ) {
625                 push @authors, $au;
626             }
627         }
628
629         $title     = $record->subfield( '200', 'a' );
630         my $subfield_210d = $record->subfield('210', 'd');
631         if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
632             $pubyear = $1;
633         }
634         $publisher = $record->subfield( '210', 'c' ) || '';
635         $isbn      = $record->subfield( '010', 'a' ) || '';
636         $issn      = $record->subfield( '011', 'a' ) || '';
637     } else {
638
639         # MARC21 need some improve
640
641         # Setting datas
642         if ( $record->field('100') ) {
643             push @authors, $record->subfield( '100', 'a' );
644         }
645
646         # others authors
647         if ( $record->field('700') ) {
648             for my $au ( $record->field('700')->subfield('a') ) {
649                 push @authors, $au;
650             }
651         }
652         $title = $record->field('245')->as_string('ab');
653         if ($titletype eq 'a') {
654             $pubyear   = $record->field('008') || '';
655             $pubyear   = substr($pubyear->data(), 7, 4) if $pubyear;
656             $isbn      = $record->subfield( '773', 'z' ) || '';
657             $issn      = $record->subfield( '773', 'x' ) || '';
658             $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
659             my @rels = $record->subfield( '773', 'g' );
660             $pages = join(', ', @rels);
661         } else {
662             $pubyear   = $record->subfield( '260', 'c' ) || '';
663             $publisher = $record->subfield( '260', 'b' ) || '';
664             $isbn      = $record->subfield( '020', 'a' ) || '';
665             $issn      = $record->subfield( '022', 'a' ) || '';
666         }
667
668     }
669
670     my @params = (
671         [ 'ctx_ver', 'Z39.88-2004' ],
672         [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
673         [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
674         [ "rft.${titletype}title", $title ],
675     );
676
677     # rft.title is authorized only once, so by checking $titletype
678     # we ensure that rft.title is not already in the list.
679     if ($hosttitle and $titletype) {
680         push @params, [ 'rft.title', $hosttitle ];
681     }
682
683     push @params, (
684         [ 'rft.isbn', $isbn ],
685         [ 'rft.issn', $issn ],
686     );
687
688     # If it's a subscription, these informations have no meaning.
689     if ($genre ne 'journal') {
690         push @params, (
691             [ 'rft.aulast', $aulast ],
692             [ 'rft.aufirst', $aufirst ],
693             (map { [ 'rft.au', $_ ] } @authors),
694             [ 'rft.pub', $publisher ],
695             [ 'rft.date', $pubyear ],
696             [ 'rft.pages', $pages ],
697         );
698     }
699
700     my $coins_value = join( '&amp;',
701         map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
702
703     return $coins_value;
704 }
705
706 =head2 get_openurl
707
708 my $url = $biblio->get_openurl;
709
710 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
711
712 =cut
713
714 sub get_openurl {
715     my ( $self ) = @_;
716
717     my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
718
719     if ($OpenURLResolverURL) {
720         my $uri = URI->new($OpenURLResolverURL);
721
722         if (not defined $uri->query) {
723             $OpenURLResolverURL .= '?';
724         } else {
725             $OpenURLResolverURL .= '&amp;';
726         }
727         $OpenURLResolverURL .= $self->get_coins;
728     }
729
730     return $OpenURLResolverURL;
731 }
732
733 =head3 is_serial
734
735 my $serial = $biblio->is_serial
736
737 Return boolean true if this bibbliographic record is continuing resource
738
739 =cut
740
741 sub is_serial {
742     my ( $self ) = @_;
743
744     return 1 if $self->serial;
745
746     my $record = $self->metadata->record;
747     return 1 if substr($record->leader, 7, 1) eq 's';
748
749     return 0;
750 }
751
752 =head3 custom_cover_image_url
753
754 my $image_url = $biblio->custom_cover_image_url
755
756 Return the specific url of the cover image for this bibliographic record.
757 It is built regaring the value of the system preference CustomCoverImagesURL
758
759 =cut
760
761 sub custom_cover_image_url {
762     my ( $self ) = @_;
763     my $url = C4::Context->preference('CustomCoverImagesURL');
764     if ( $url =~ m|{isbn}| ) {
765         my $isbn = $self->biblioitem->isbn;
766         $url =~ s|{isbn}|$isbn|g;
767     }
768     if ( $url =~ m|{normalized_isbn}| ) {
769         my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
770         $url =~ s|{normalized_isbn}|$normalized_isbn|g;
771     }
772     if ( $url =~ m|{issn}| ) {
773         my $issn = $self->biblioitem->issn;
774         $url =~ s|{issn}|$issn|g;
775     }
776
777     my $re = qr|{(?<field>\d{3})\$(?<subfield>.)}|;
778     if ( $url =~ $re ) {
779         my $field = $+{field};
780         my $subfield = $+{subfield};
781         my $marc_record = $self->metadata->record;
782         my $value = $marc_record->subfield($field, $subfield);
783         $url =~ s|$re|$value|;
784     }
785
786     return $url;
787 }
788
789 =head3 to_api
790
791     my $json = $biblio->to_api;
792
793 Overloaded method that returns a JSON representation of the Koha::Biblio object,
794 suitable for API output. The related Koha::Biblioitem object is merged as expected
795 on the API.
796
797 =cut
798
799 sub to_api {
800     my ($self, $args) = @_;
801
802     my @embeds = keys %{ $args->{embed} };
803     my $remaining_embeds = {};
804
805     foreach my $embed (@embeds) {
806         $remaining_embeds = delete $args->{embed}->{$embed}
807             unless $self->can($embed);
808     }
809
810     my $response = $self->SUPER::to_api( $args );
811     my $biblioitem = $self->biblioitem->to_api({ embed => $remaining_embeds });
812
813     return { %$response, %$biblioitem };
814 }
815
816 =head3 to_api_mapping
817
818 This method returns the mapping for representing a Koha::Biblio object
819 on the API.
820
821 =cut
822
823 sub to_api_mapping {
824     return {
825         biblionumber     => 'biblio_id',
826         frameworkcode    => 'framework_id',
827         unititle         => 'uniform_title',
828         seriestitle      => 'series_title',
829         copyrightdate    => 'copyright_date',
830         datecreated      => 'creation_date'
831     };
832 }
833
834 =head2 Internal methods
835
836 =head3 type
837
838 =cut
839
840 sub _type {
841     return 'Biblio';
842 }
843
844 =head1 AUTHOR
845
846 Kyle M Hall <kyle@bywatersolutions.com>
847
848 =cut
849
850 1;