1 package Koha::REST::V1::Biblios;
3 # This file is part of Koha.
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20 use Mojo::Base 'Mojolicious::Controller';
25 use Koha::RecordProcessor;
26 use C4::Biblio qw( DelBiblio AddBiblio ModBiblio );
27 use C4::Search qw( FindDuplicate );
29 use C4::Barcodes::ValueBuilder;
34 use List::MoreUtils qw( any );
35 use MARC::Record::MiJ;
37 use Try::Tiny qw( catch try );
38 use JSON qw( decode_json );
46 Controller function that handles retrieving a single biblio object
51 my $c = shift->openapi->valid_input or return;
54 $attributes = { prefetch => [ 'metadata' ] } # don't prefetch metadata if not needed
55 unless $c->req->headers->accept =~ m/application\/json/;
57 my $biblio = Koha::Biblios->find( { biblionumber => $c->param('biblio_id') }, $attributes );
63 error => "Object not found."
70 if ( $c->req->headers->accept =~ m/application\/json/ ) {
73 json => $biblio->to_api
77 my $metadata = $biblio->metadata;
78 my $record = $metadata->record;
79 my $schema = $metadata->schema // C4::Context->preference("marcflavour");
85 text => $record->as_xml_record($schema),
90 data => $record->to_mij
95 text => $record->as_usmarc
99 format => 'text/plain',
100 text => $record->as_formatted
106 "application/marcxml+xml",
107 "application/marc-in-json",
116 $c->unhandled_exception($_);
122 Controller function that handles deleting a biblio object
127 my $c = shift->openapi->valid_input or return;
129 my $biblio = Koha::Biblios->find( $c->param('biblio_id') );
131 if ( not defined $biblio ) {
134 openapi => { error => "Object not found" }
139 my $error = DelBiblio( $biblio->id );
144 openapi => { error => $error }
148 return $c->render( status => 204, openapi => "" );
152 $c->unhandled_exception($_);
158 Controller function that handles retrieving a single biblio object
163 my $c = shift->openapi->valid_input or return;
165 my $biblio = Koha::Biblios->find(
166 { biblionumber => $c->param('biblio_id') },
167 { prefetch => ['metadata'] } );
173 error => "Object not found."
180 my $metadata = $biblio->metadata;
181 my $record = $metadata->record;
183 my $opachiddenitems_rules = C4::Context->yaml_preference('OpacHiddenItems');
184 my $patron = $c->stash('koha.user');
186 # Check if the biblio should be hidden for unprivileged access
187 # unless there's a logged in user, and there's an exception for it's
189 unless ( $patron and $patron->category->override_hidden_items ) {
190 if ( $biblio->hidden_in_opac({ rules => $opachiddenitems_rules }) )
195 error => "Object not found."
201 my $schema = $metadata->schema // C4::Context->preference("marcflavour");
203 my $record_processor = Koha::RecordProcessor->new({
204 filters => 'ViewPolicy',
207 frameworkcode => $biblio->frameworkcode
210 # Apply framework's filtering to MARC::Record object
211 $record_processor->process($record);
217 text => $record->as_xml_record($schema),
222 data => $record->to_mij
227 text => $record->as_usmarc
231 format => 'text/plain',
232 text => $record->as_formatted
237 "application/marcxml+xml",
238 "application/marc-in-json",
246 $c->unhandled_exception($_);
253 Controller function that handles retrieving biblio's bookings
258 my $c = shift->openapi->valid_input or return;
260 my $biblio = Koha::Biblios->find( { biblionumber => $c->param('biblio_id') }, { prefetch => ['bookings'] } );
265 openapi => { error => "Object not found." }
271 my $bookings_rs = $biblio->bookings;
272 my $bookings = $c->objects->search($bookings_rs);
278 $c->unhandled_exception($_);
284 Controller function that handles retrieving biblio's items
289 my $c = shift->openapi->valid_input or return;
291 my $biblio = Koha::Biblios->find( { biblionumber => $c->param('biblio_id') }, { prefetch => ['items'] } );
292 my $bookable_only = $c->param('bookable');
297 openapi => { error => "Object not found." }
303 my $items_rs = $biblio->items;
304 $items_rs = $items_rs->filter_by_bookable if $bookable_only;
305 my $items = $c->objects->search($items_rs);
311 $c->unhandled_exception($_);
317 Controller function that handles creating a biblio's item
322 my $c = shift->openapi->valid_input or return;
325 my $biblio_id = $c->param('biblio_id');
326 my $biblio = Koha::Biblios->find( $biblio_id );
331 openapi => { error => "Biblio not found" }
335 my $body = $c->req->json;
337 $body->{biblio_id} = $biblio_id;
339 # Don't save extended subfields yet. To be done in another bug.
340 $body->{extended_subfields} = undef;
342 my $item = Koha::Item->new_from_api($body);
344 if ( !defined $item->barcode ) {
346 # FIXME This should be moved to Koha::Item->store
347 my $autoBarcode = C4::Context->preference('autoBarcode');
350 if ( !$autoBarcode || $autoBarcode eq 'OFF' ) {
353 elsif ( $autoBarcode eq 'incremental' ) {
355 C4::Barcodes::ValueBuilder::incremental::get_barcode;
357 elsif ( $autoBarcode eq 'annual' ) {
358 my $year = Koha::DateUtils::dt_from_string()->year();
360 C4::Barcodes::ValueBuilder::annual::get_barcode(
363 elsif ( $autoBarcode eq 'hbyymmincr' ) {
365 # Generates a barcode where
366 # hb = home branch Code,
367 # yymm = year/month catalogued,
368 # incr = incremental number,
369 # reset yearly -fbcit
370 my $now = Koha::DateUtils::dt_from_string();
371 my $year = $now->year();
372 my $month = $now->month();
373 my $homebranch = $item->homebranch // '';
375 C4::Barcodes::ValueBuilder::hbyymmincr::get_barcode(
376 { year => $year, mon => $month } );
377 $barcode = $homebranch . $barcode;
379 elsif ( $autoBarcode eq 'EAN13' ) {
381 # not the best, two catalogers could add the same
382 # barcode easily this way :/
383 my $query = "select max(abs(barcode)) from items";
384 my $dbh = C4::Context->dbh;
385 my $sth = $dbh->prepare($query);
388 while ( my ($last) = $sth->fetchrow_array ) {
391 my $ean = CheckDigits('ean');
392 if ( $ean->is_valid($nextnum) ) {
393 my $next = $ean->basenumber($nextnum) + 1;
394 $nextnum = $ean->complete($next);
396 '0' x ( 13 - length($nextnum) ) . $nextnum; # pad zeros
399 warn "ERROR: invalid EAN-13 $nextnum, using increment";
405 warn "ERROR: unknown autoBarcode: $autoBarcode";
407 $item->barcode($barcode) if $barcode;
410 $item->store->discard_changes;
414 openapi => $item->to_api
418 if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
421 openapi => { error => 'Duplicate barcode.' }
424 $c->unhandled_exception($_);
430 Controller function that handles updating a biblio's item
435 my $c = shift->openapi->valid_input or return;
438 my $biblio_id = $c->param('biblio_id');
439 my $item_id = $c->param('item_id');
440 my $biblio = Koha::Biblios->find( { biblionumber => $biblio_id } );
444 openapi => { error => "Biblio not found" }
448 my $item = $biblio->items->find({ itemnumber => $item_id });
453 openapi => { error => "Item not found" }
457 my $body = $c->req->json;
459 $body->{biblio_id} = $biblio_id;
461 # Don't save extended subfields yet. To be done in another bug.
462 $body->{extended_subfields} = undef;
464 $item->set_from_api($body);
466 $item->store->discard_changes;
470 openapi => $item->to_api
474 if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
477 openapi => { error => 'Duplicate barcode.' }
480 $c->unhandled_exception($_);
486 List Koha::Checkout objects
491 my $c = shift->openapi->valid_input or return;
493 my $checked_in = $c->param('checked_in');
494 $c->req->params->remove('checked_in');
497 my $biblio = Koha::Biblios->find( $c->param('biblio_id') );
502 openapi => { error => 'Object not found' }
508 ? $c->objects->search( $biblio->old_checkouts )
509 : $c->objects->search( $biblio->current_checkouts );
513 openapi => $checkouts
517 $c->unhandled_exception($_);
521 =head3 pickup_locations
523 Method that returns the possible pickup_locations for a given biblio
524 used for building the dropdown selector
528 sub pickup_locations {
529 my $c = shift->openapi->valid_input or return;
531 my $biblio = Koha::Biblios->find( $c->param('biblio_id') );
536 openapi => { error => "Biblio not found" }
540 my $patron = Koha::Patrons->find( $c->param('patron_id') );
541 $c->req->params->remove('patron_id');
546 openapi => { error => "Patron not found" }
552 my $pl_set = $biblio->pickup_locations( { patron => $patron } );
555 if ( C4::Context->preference('AllowHoldPolicyOverride') ) {
557 my $libraries_rs = Koha::Libraries->search( { pickup_location => 1 } );
558 my $libraries = $c->objects->search($libraries_rs);
562 $library->{needs_override} = (
563 any { $_->branchcode eq $library->{library_id} }
573 my $pickup_locations = $c->objects->search($pl_set);
574 @response = map { $_->{needs_override} = Mojo::JSON->false; $_; } @{$pickup_locations};
579 openapi => \@response
583 $c->unhandled_exception($_);
587 =head3 get_items_public
589 Controller function that handles retrieving biblio's items, for unprivileged
594 sub get_items_public {
595 my $c = shift->openapi->valid_input or return;
597 my $biblio = Koha::Biblios->find(
598 $c->param('biblio_id'),
599 { prefetch => ['items'] }
606 error => "Object not found."
613 my $patron = $c->stash('koha.user');
615 my $items_rs = $biblio->items->filter_by_visible_in_opac({ patron => $patron });
616 my $items = $c->objects->search( $items_rs );
623 $c->unhandled_exception($_);
629 Set rating for the logged in user
635 my $c = shift->openapi->valid_input or return;
637 my $biblio = Koha::Biblios->find( $c->param('biblio_id') );
643 error => "Object not found."
648 my $patron = $c->stash('koha.user');
653 { error => "Cannot rate. Reason: must be logged-in" }
657 my $body = $c->req->json;
658 my $rating_value = $body->{rating};
662 my $rating = Koha::Ratings->find(
664 biblionumber => $biblio->biblionumber,
665 borrowernumber => $patron->borrowernumber,
668 $rating->delete if $rating;
670 if ( $rating_value ) { # Cannot set to 0 from the UI
671 $rating = Koha::Rating->new(
673 biblionumber => $biblio->biblionumber,
674 borrowernumber => $patron->borrowernumber,
675 rating_value => $rating_value,
680 Koha::Ratings->search( { biblionumber => $biblio->biblionumber } );
681 my $average = $ratings->get_avg_rating;
686 rating => $rating && $rating->in_storage ? $rating->rating_value : undef,
688 count => $ratings->count
693 $c->unhandled_exception($_);
699 Controller function that handles creating a biblio object
704 my $c = shift->openapi->valid_input or return;
707 my $headers = $c->req->headers;
709 my $flavour = $headers->header('x-record-schema');
710 $flavour //= C4::Context->preference('marcflavour');
714 my $frameworkcode = $headers->header('x-framework-id');
715 my $content_type = $headers->content_type;
717 if ( $content_type =~ m/application\/marcxml\+xml/ ) {
718 $record = MARC::Record->new_from_xml( $c->req->body, 'UTF-8', $flavour );
720 elsif ( $content_type =~ m/application\/marc-in-json/ ) {
721 $record = MARC::Record->new_from_mij_structure( $c->req->json );
723 elsif ( $content_type =~ m/application\/marc/ ) {
724 $record = MARC::Record->new_from_usmarc( $c->req->body );
730 "application/marcxml+xml",
731 "application/marc-in-json",
737 my ( $duplicatebiblionumber, $duplicatetitle );
738 ( $duplicatebiblionumber, $duplicatetitle ) = FindDuplicate($record);
740 my $confirm_not_duplicate = $headers->header('x-confirm-not-duplicate');
745 error => "Duplicate biblio $duplicatebiblionumber",
747 ) unless !$duplicatebiblionumber || $confirm_not_duplicate;
749 my ( $biblionumber, $oldbibitemnum );
750 ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
754 openapi => { id => $biblionumber }
758 $c->unhandled_exception($_);
764 Controller function that handles modifying an biblio object
769 my $c = shift->openapi->valid_input or return;
771 my $biblio = Koha::Biblios->find( $c->param('biblio_id') );
773 if ( ! defined $biblio ) {
776 openapi => { error => "Object not found" }
781 my $headers = $c->req->headers;
783 my $flavour = $headers->header('x-record-schema');
784 $flavour //= C4::Context->preference('marcflavour');
786 my $frameworkcode = $headers->header('x-framework-id') || $biblio->frameworkcode;
788 my $content_type = $headers->content_type;
792 if ( $content_type =~ m/application\/marcxml\+xml/ ) {
793 $record = MARC::Record->new_from_xml( $c->req->body, 'UTF-8', $flavour );
795 elsif ( $content_type =~ m/application\/marc-in-json/ ) {
796 $record = MARC::Record->new_from_mij_structure( $c->req->json );
798 elsif ( $content_type =~ m/application\/marc/ ) {
799 $record = MARC::Record->new_from_usmarc( $c->req->body );
806 "application/marcxml+xml",
807 "application/marc-in-json",
813 ModBiblio( $record, $biblio->id, $frameworkcode );
817 openapi => { id => $biblio->id }
821 $c->unhandled_exception($_);
827 Controller function that handles retrieving a single biblio object
832 my $c = shift->openapi->valid_input or return;
834 my @prefetch = qw(biblioitem);
835 push @prefetch, 'metadata' # don't prefetch metadata if not needed
836 unless $c->req->headers->accept =~ m/application\/json/;
838 my $rs = Koha::Biblios->search( undef, { prefetch => \@prefetch });
839 my $biblios = $c->objects->search_rs( $rs, [(sub{ $rs->api_query_fixer( $_[0], '', $_[1] ) })] );
843 if ( $c->req->headers->accept =~ m/application\/json(;.*)?$/ ) {
846 json => $c->objects->to_api( $biblios ),
850 $c->req->headers->accept =~ m/application\/marcxml\+xml(;.*)?$/ )
852 $c->res->headers->add( 'Content-Type', 'application/marcxml+xml' );
855 text => $biblios->print_collection('marcxml')
859 $c->req->headers->accept =~ m/application\/marc-in-json(;.*)?$/ )
861 $c->res->headers->add( 'Content-Type', 'application/marc-in-json' );
864 data => $biblios->print_collection('mij')
867 elsif ( $c->req->headers->accept =~ m/application\/marc(;.*)?$/ ) {
868 $c->res->headers->add( 'Content-Type', 'application/marc' );
871 text => $biblios->print_collection('marc')
874 elsif ( $c->req->headers->accept =~ m/text\/plain(;.*)?$/ ) {
877 text => $biblios->print_collection('txt')
884 "application/json", "application/marcxml+xml",
885 "application/marc-in-json", "application/marc",
892 $c->unhandled_exception($_);
898 Controller function that handles merging two biblios. If an optional
899 MARCXML is provided as the request body, this MARCXML replaces the
900 bibliodata of the merge target biblio. Syntax format inside the request body
901 must match with the Marc format used into Koha installation (MARC21 or UNIMARC)
906 my $c = shift->openapi->valid_input or return;
907 my $ref_biblionumber = $c->param('biblio_id');
908 my $json = decode_json( $c->req->body );
909 my $bn_merge = $json->{'biblio_id_to_merge'};
910 my $framework = $json->{'framework_to_use'};
911 my $rules = $json->{'rules'};
912 my $override_rec = $json->{'datarecord'};
914 if ( ( !defined $rules ) || ( $rules eq '' ) ) { $rules = 'override'; }
915 if ( ( !defined $override_rec ) ) { $override_rec = ''; }
916 if ( ( !defined $framework ) ) { $framework = ''; }
918 my $biblio = Koha::Biblios->find($ref_biblionumber);
919 if ( not defined $biblio ) {
922 json => { error => sprintf( "[%s] biblio to merge into not found", $ref_biblionumber ) }
925 my $frombib = Koha::Biblios->find($bn_merge);
926 if ( not defined $frombib ) {
929 json => { error => sprintf( "[%s] from which to merge not found", $bn_merge ) }
933 if ( ( $rules eq 'override_ext' ) && ( $override_rec eq '' ) ) {
938 "With the rule 'override_ext' you need to insert a bib record in marc-in-json format into 'record' field."
943 if ( ( $rules eq 'override' ) && ( $framework ne '' ) ) {
946 json => { error => "With the rule 'override' you can not use the field 'framework_to_use'." }
952 if ( $rules eq 'override_ext' ) {
953 my $record = MARC::Record::MiJ->new_from_mij_structure($override_rec);
954 $record->encoding('UTF-8');
955 if ( $framework eq '' ) { $framework = $biblio->frameworkcode; }
956 my $chk = ModBiblio( $record, $ref_biblionumber, $framework );
957 if ( $chk != 1 ) { die "Error on ModBiblio"; } # ModBiblio returns 1 if everything as gone well
958 my @biblio_ids_to_merge = ($bn_merge);
959 $results = $biblio->merge_with( \@biblio_ids_to_merge );
961 if ( $rules eq 'override' ) {
962 my @biblio_ids_to_merge = ($bn_merge);
963 $results = $biblio->merge_with( \@biblio_ids_to_merge );
967 return $c->render( status => 400, json => { error => $@ } );
973 data => $biblio->metadata->record->to_mij