Bug 24448: Add Koha::Biblio->subscriptions_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 subscriptions_count
521
522 my $subscriptions_count = $self->subscriptions_count
523
524 Returns the count of the the related Koha::Subscriptions object for this biblio
525
526 =cut
527
528 sub subscriptions_count {
529     my ($self) = @_;
530
531     return $self->subscriptions->count;
532 }
533
534 =head3 has_items_waiting_or_intransit
535
536 my $itemsWaitingOrInTransit = $biblio->has_items_waiting_or_intransit
537
538 Tells if this bibliographic record has items waiting or in transit.
539
540 =cut
541
542 sub has_items_waiting_or_intransit {
543     my ( $self ) = @_;
544
545     if ( Koha::Holds->search({ biblionumber => $self->id,
546                                found => ['W', 'T'] })->count ) {
547         return 1;
548     }
549
550     foreach my $item ( $self->items->as_list ) {
551         return 1 if $item->get_transfer;
552     }
553
554     return 0;
555 }
556
557 =head2 get_coins
558
559 my $coins = $biblio->get_coins;
560
561 Returns the COinS (a span) which can be included in a biblio record
562
563 =cut
564
565 sub get_coins {
566     my ( $self ) = @_;
567
568     my $record = $self->metadata->record;
569
570     my $pos7 = substr $record->leader(), 7, 1;
571     my $pos6 = substr $record->leader(), 6, 1;
572     my $mtx;
573     my $genre;
574     my ( $aulast, $aufirst ) = ( '', '' );
575     my @authors;
576     my $title;
577     my $hosttitle;
578     my $pubyear   = '';
579     my $isbn      = '';
580     my $issn      = '';
581     my $publisher = '';
582     my $pages     = '';
583     my $titletype = '';
584
585     # For the purposes of generating COinS metadata, LDR/06-07 can be
586     # considered the same for UNIMARC and MARC21
587     my $fmts6 = {
588         'a' => 'book',
589         'b' => 'manuscript',
590         'c' => 'book',
591         'd' => 'manuscript',
592         'e' => 'map',
593         'f' => 'map',
594         'g' => 'film',
595         'i' => 'audioRecording',
596         'j' => 'audioRecording',
597         'k' => 'artwork',
598         'l' => 'document',
599         'm' => 'computerProgram',
600         'o' => 'document',
601         'r' => 'document',
602     };
603     my $fmts7 = {
604         'a' => 'journalArticle',
605         's' => 'journal',
606     };
607
608     $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
609
610     if ( $genre eq 'book' ) {
611             $genre = $fmts7->{$pos7} if $fmts7->{$pos7};
612     }
613
614     ##### We must transform mtx to a valable mtx and document type ####
615     if ( $genre eq 'book' ) {
616             $mtx = 'book';
617             $titletype = 'b';
618     } elsif ( $genre eq 'journal' ) {
619             $mtx = 'journal';
620             $titletype = 'j';
621     } elsif ( $genre eq 'journalArticle' ) {
622             $mtx   = 'journal';
623             $genre = 'article';
624             $titletype = 'a';
625     } else {
626             $mtx = 'dc';
627     }
628
629     if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
630
631         # Setting datas
632         $aulast  = $record->subfield( '700', 'a' ) || '';
633         $aufirst = $record->subfield( '700', 'b' ) || '';
634         push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
635
636         # others authors
637         if ( $record->field('200') ) {
638             for my $au ( $record->field('200')->subfield('g') ) {
639                 push @authors, $au;
640             }
641         }
642
643         $title     = $record->subfield( '200', 'a' );
644         my $subfield_210d = $record->subfield('210', 'd');
645         if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
646             $pubyear = $1;
647         }
648         $publisher = $record->subfield( '210', 'c' ) || '';
649         $isbn      = $record->subfield( '010', 'a' ) || '';
650         $issn      = $record->subfield( '011', 'a' ) || '';
651     } else {
652
653         # MARC21 need some improve
654
655         # Setting datas
656         if ( $record->field('100') ) {
657             push @authors, $record->subfield( '100', 'a' );
658         }
659
660         # others authors
661         if ( $record->field('700') ) {
662             for my $au ( $record->field('700')->subfield('a') ) {
663                 push @authors, $au;
664             }
665         }
666         $title = $record->field('245')->as_string('ab');
667         if ($titletype eq 'a') {
668             $pubyear   = $record->field('008') || '';
669             $pubyear   = substr($pubyear->data(), 7, 4) if $pubyear;
670             $isbn      = $record->subfield( '773', 'z' ) || '';
671             $issn      = $record->subfield( '773', 'x' ) || '';
672             $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a') || q{};
673             my @rels = $record->subfield( '773', 'g' );
674             $pages = join(', ', @rels);
675         } else {
676             $pubyear   = $record->subfield( '260', 'c' ) || '';
677             $publisher = $record->subfield( '260', 'b' ) || '';
678             $isbn      = $record->subfield( '020', 'a' ) || '';
679             $issn      = $record->subfield( '022', 'a' ) || '';
680         }
681
682     }
683
684     my @params = (
685         [ 'ctx_ver', 'Z39.88-2004' ],
686         [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
687         [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
688         [ "rft.${titletype}title", $title ],
689     );
690
691     # rft.title is authorized only once, so by checking $titletype
692     # we ensure that rft.title is not already in the list.
693     if ($hosttitle and $titletype) {
694         push @params, [ 'rft.title', $hosttitle ];
695     }
696
697     push @params, (
698         [ 'rft.isbn', $isbn ],
699         [ 'rft.issn', $issn ],
700     );
701
702     # If it's a subscription, these informations have no meaning.
703     if ($genre ne 'journal') {
704         push @params, (
705             [ 'rft.aulast', $aulast ],
706             [ 'rft.aufirst', $aufirst ],
707             (map { [ 'rft.au', $_ ] } @authors),
708             [ 'rft.pub', $publisher ],
709             [ 'rft.date', $pubyear ],
710             [ 'rft.pages', $pages ],
711         );
712     }
713
714     my $coins_value = join( '&amp;',
715         map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
716
717     return $coins_value;
718 }
719
720 =head2 get_openurl
721
722 my $url = $biblio->get_openurl;
723
724 Returns url for OpenURL resolver set in OpenURLResolverURL system preference
725
726 =cut
727
728 sub get_openurl {
729     my ( $self ) = @_;
730
731     my $OpenURLResolverURL = C4::Context->preference('OpenURLResolverURL');
732
733     if ($OpenURLResolverURL) {
734         my $uri = URI->new($OpenURLResolverURL);
735
736         if (not defined $uri->query) {
737             $OpenURLResolverURL .= '?';
738         } else {
739             $OpenURLResolverURL .= '&amp;';
740         }
741         $OpenURLResolverURL .= $self->get_coins;
742     }
743
744     return $OpenURLResolverURL;
745 }
746
747 =head3 is_serial
748
749 my $serial = $biblio->is_serial
750
751 Return boolean true if this bibbliographic record is continuing resource
752
753 =cut
754
755 sub is_serial {
756     my ( $self ) = @_;
757
758     return 1 if $self->serial;
759
760     my $record = $self->metadata->record;
761     return 1 if substr($record->leader, 7, 1) eq 's';
762
763     return 0;
764 }
765
766 =head3 custom_cover_image_url
767
768 my $image_url = $biblio->custom_cover_image_url
769
770 Return the specific url of the cover image for this bibliographic record.
771 It is built regaring the value of the system preference CustomCoverImagesURL
772
773 =cut
774
775 sub custom_cover_image_url {
776     my ( $self ) = @_;
777     my $url = C4::Context->preference('CustomCoverImagesURL');
778     if ( $url =~ m|{isbn}| ) {
779         my $isbn = $self->biblioitem->isbn;
780         $url =~ s|{isbn}|$isbn|g;
781     }
782     if ( $url =~ m|{normalized_isbn}| ) {
783         my $normalized_isbn = C4::Koha::GetNormalizedISBN($self->biblioitem->isbn);
784         $url =~ s|{normalized_isbn}|$normalized_isbn|g;
785     }
786     if ( $url =~ m|{issn}| ) {
787         my $issn = $self->biblioitem->issn;
788         $url =~ s|{issn}|$issn|g;
789     }
790
791     my $re = qr|{(?<field>\d{3})\$(?<subfield>.)}|;
792     if ( $url =~ $re ) {
793         my $field = $+{field};
794         my $subfield = $+{subfield};
795         my $marc_record = $self->metadata->record;
796         my $value = $marc_record->subfield($field, $subfield);
797         $url =~ s|$re|$value|;
798     }
799
800     return $url;
801 }
802
803 =head3 to_api
804
805     my $json = $biblio->to_api;
806
807 Overloaded method that returns a JSON representation of the Koha::Biblio object,
808 suitable for API output. The related Koha::Biblioitem object is merged as expected
809 on the API.
810
811 =cut
812
813 sub to_api {
814     my ($self, $args) = @_;
815
816     my @embeds = keys %{ $args->{embed} };
817     my $remaining_embeds = {};
818
819     foreach my $embed (@embeds) {
820         $remaining_embeds = delete $args->{embed}->{$embed}
821             unless $self->can($embed);
822     }
823
824     my $response = $self->SUPER::to_api( $args );
825     my $biblioitem = $self->biblioitem->to_api({ embed => $remaining_embeds });
826
827     return { %$response, %$biblioitem };
828 }
829
830 =head3 to_api_mapping
831
832 This method returns the mapping for representing a Koha::Biblio object
833 on the API.
834
835 =cut
836
837 sub to_api_mapping {
838     return {
839         biblionumber     => 'biblio_id',
840         frameworkcode    => 'framework_id',
841         unititle         => 'uniform_title',
842         seriestitle      => 'series_title',
843         copyrightdate    => 'copyright_date',
844         datecreated      => 'creation_date'
845     };
846 }
847
848 =head2 Internal methods
849
850 =head3 type
851
852 =cut
853
854 sub _type {
855     return 'Biblio';
856 }
857
858 =head1 AUTHOR
859
860 Kyle M Hall <kyle@bywatersolutions.com>
861
862 =cut
863
864 1;