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::Auth qw( haspermission );
30 use C4::Barcodes::ValueBuilder;
35 use List::MoreUtils qw( any );
36 use MARC::Record::MiJ;
38 use Try::Tiny qw( catch try );
39 use JSON qw( decode_json );
47 Controller function that handles retrieving a single biblio object
52 my $c = shift->openapi->valid_input or return;
55 $attributes = { prefetch => [ 'metadata' ] } # don't prefetch metadata if not needed
56 unless $c->req->headers->accept =~ m/application\/json/;
58 my $biblio = Koha::Biblios->find( { biblionumber => $c->param('biblio_id') }, $attributes );
60 return $c->render_resource_not_found("Bibliographic record")
65 if ( $c->req->headers->accept =~ m/application\/json/ ) {
68 json => $c->objects->to_api($biblio),
72 my $metadata = $biblio->metadata;
73 my $record = $metadata->record;
74 my $schema = $metadata->schema // C4::Context->preference("marcflavour");
80 text => $record->as_xml_record($schema),
85 data => $record->to_mij
90 text => $record->as_usmarc
94 format => 'text/plain',
95 text => $record->as_formatted
101 "application/marcxml+xml",
102 "application/marc-in-json",
111 $c->unhandled_exception($_);
117 Controller function that handles deleting a biblio object
122 my $c = shift->openapi->valid_input or return;
124 my $biblio = Koha::Biblios->find( $c->param('biblio_id') );
126 return $c->render_resource_not_found("Bibliographic record")
130 my $error = DelBiblio( $biblio->id );
135 openapi => { error => $error }
139 return $c->render_resource_deleted;
143 $c->unhandled_exception($_);
149 Controller function that handles retrieving a single biblio object
154 my $c = shift->openapi->valid_input or return;
156 my $biblio = Koha::Biblios->find(
157 { biblionumber => $c->param('biblio_id') },
158 { prefetch => ['metadata'] } );
160 return $c->render_resource_not_found("Bibliographic record")
165 my $metadata = $biblio->metadata;
166 my $record = $metadata->record;
168 my $opachiddenitems_rules = C4::Context->yaml_preference('OpacHiddenItems');
169 my $patron = $c->stash('koha.user');
171 # Check if the biblio should be hidden for unprivileged access
172 # unless there's a logged in user, and there's an exception for it's
174 unless ( $patron and $patron->category->override_hidden_items ) {
175 if ( $biblio->hidden_in_opac( { rules => $opachiddenitems_rules } ) ) {
176 return $c->render_resource_not_found("Bibliographic record");
180 my $schema = $metadata->schema // C4::Context->preference("marcflavour");
182 my $record_processor = Koha::RecordProcessor->new({
183 filters => 'ViewPolicy',
186 frameworkcode => $biblio->frameworkcode
189 # Apply framework's filtering to MARC::Record object
190 $record_processor->process($record);
196 text => $record->as_xml_record($schema),
201 data => $record->to_mij
206 text => $record->as_usmarc
210 format => 'text/plain',
211 text => $record->as_formatted
216 "application/marcxml+xml",
217 "application/marc-in-json",
225 $c->unhandled_exception($_);
232 Controller function that handles retrieving biblio's bookings
237 my $c = shift->openapi->valid_input or return;
239 my $biblio = Koha::Biblios->find( { biblionumber => $c->param('biblio_id') }, { prefetch => ['bookings'] } );
241 return $c->render_resource_not_found("Bibliographic record")
246 my $bookings_rs = $biblio->bookings;
247 my $bookings = $c->objects->search($bookings_rs);
253 $c->unhandled_exception($_);
259 Controller function that handles retrieving biblio's items
264 my $c = shift->openapi->valid_input or return;
266 my $biblio = Koha::Biblios->find( { biblionumber => $c->param('biblio_id') }, { prefetch => ['items'] } );
267 my $bookable_only = $c->param('bookable');
269 return $c->render_resource_not_found("Bibliographic record")
274 # FIXME We need to order_by serial.publisheddate if we have _order_by=+me.serial_issue_number
275 # FIXME Do we always need host_items => 1 or depending on a flag?
276 # FIXME Should we prefetch => ['issue','branchtransfer']?
277 my $items_rs = $biblio->items( { host_items => 1 } )->search_ordered( {}, { join => 'biblioitem' } );
278 $items_rs = $items_rs->filter_by_bookable if $bookable_only;
279 # FIXME We need to order_by serial.publisheddate if we have _order_by=+me.serial_issue_number
280 my $items = $c->objects->search($items_rs);
287 $c->unhandled_exception($_);
293 Controller function that handles creating a biblio's item
298 my $c = shift->openapi->valid_input or return;
301 my $biblio_id = $c->param('biblio_id');
302 my $biblio = Koha::Biblios->find( $biblio_id );
304 return $c->render_resource_not_found("Bibliographic record")
307 my $body = $c->req->json;
309 $body->{biblio_id} = $biblio_id;
311 # Don't save extended subfields yet. To be done in another bug.
312 $body->{extended_subfields} = undef;
314 my $item = Koha::Item->new_from_api($body);
316 if ( !defined $item->barcode ) {
318 # FIXME This should be moved to Koha::Item->store
319 my $autoBarcode = C4::Context->preference('autoBarcode');
322 if ( !$autoBarcode || $autoBarcode eq 'OFF' ) {
325 elsif ( $autoBarcode eq 'incremental' ) {
327 C4::Barcodes::ValueBuilder::incremental::get_barcode;
329 elsif ( $autoBarcode eq 'annual' ) {
330 my $year = Koha::DateUtils::dt_from_string()->year();
332 C4::Barcodes::ValueBuilder::annual::get_barcode(
335 elsif ( $autoBarcode eq 'hbyymmincr' ) {
337 # Generates a barcode where
338 # hb = home branch Code,
339 # yymm = year/month catalogued,
340 # incr = incremental number,
341 # reset yearly -fbcit
342 my $now = Koha::DateUtils::dt_from_string();
343 my $year = $now->year();
344 my $month = $now->month();
345 my $homebranch = $item->homebranch // '';
347 C4::Barcodes::ValueBuilder::hbyymmincr::get_barcode(
348 { year => $year, mon => $month } );
349 $barcode = $homebranch . $barcode;
351 elsif ( $autoBarcode eq 'EAN13' ) {
353 # not the best, two catalogers could add the same
354 # barcode easily this way :/
355 my $query = "select max(abs(barcode)) from items";
356 my $dbh = C4::Context->dbh;
357 my $sth = $dbh->prepare($query);
360 while ( my ($last) = $sth->fetchrow_array ) {
363 my $ean = CheckDigits('ean');
364 if ( $ean->is_valid($nextnum) ) {
365 my $next = $ean->basenumber($nextnum) + 1;
366 $nextnum = $ean->complete($next);
368 '0' x ( 13 - length($nextnum) ) . $nextnum; # pad zeros
371 warn "ERROR: invalid EAN-13 $nextnum, using increment";
377 warn "ERROR: unknown autoBarcode: $autoBarcode";
379 $item->barcode($barcode) if $barcode;
382 $item->store->discard_changes;
386 openapi => $c->objects->to_api($item),
390 if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
393 openapi => { error => 'Duplicate barcode.' }
396 $c->unhandled_exception($_);
402 Controller function that handles updating a biblio's item
407 my $c = shift->openapi->valid_input or return;
410 my $biblio_id = $c->param('biblio_id');
411 my $item_id = $c->param('item_id');
412 my $biblio = Koha::Biblios->find( { biblionumber => $biblio_id } );
414 return $c->render_resource_not_found("Bibliographic record")
417 my $item = $biblio->items->find({ itemnumber => $item_id });
419 return $c->render_resource_not_found("Item")
422 my $body = $c->req->json;
424 $body->{biblio_id} = $biblio_id;
426 # Don't save extended subfields yet. To be done in another bug.
427 $body->{extended_subfields} = undef;
429 $item->set_from_api($body);
431 $item->store->discard_changes;
435 openapi => $c->objects->to_api($item),
439 if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
442 openapi => { error => 'Duplicate barcode.' }
445 $c->unhandled_exception($_);
451 List Koha::Checkout objects
456 my $c = shift->openapi->valid_input or return;
458 my $checked_in = $c->param('checked_in');
459 $c->req->params->remove('checked_in');
462 my $biblio = Koha::Biblios->find( $c->param('biblio_id') );
464 return $c->render_resource_not_found("Bibliographic record")
469 ? $c->objects->search( $biblio->old_checkouts )
470 : $c->objects->search( $biblio->current_checkouts );
474 openapi => $checkouts
478 $c->unhandled_exception($_);
482 =head3 pickup_locations
484 Method that returns the possible pickup_locations for a given biblio
485 used for building the dropdown selector
489 sub pickup_locations {
490 my $c = shift->openapi->valid_input or return;
492 my $biblio = Koha::Biblios->find( $c->param('biblio_id') );
494 return $c->render_resource_not_found("Bibliographic record")
497 my $patron = Koha::Patrons->find( $c->param('patron_id') );
498 $c->req->params->remove('patron_id');
503 openapi => { error => "Patron not found" }
509 my $pl_set = $biblio->pickup_locations( { patron => $patron } );
512 if ( C4::Context->preference('AllowHoldPolicyOverride') ) {
514 my $libraries_rs = Koha::Libraries->search( { pickup_location => 1 } );
515 my $libraries = $c->objects->search($libraries_rs);
519 $library->{needs_override} = (
520 any { $_->branchcode eq $library->{library_id} }
530 my $pickup_locations = $c->objects->search($pl_set);
531 @response = map { $_->{needs_override} = Mojo::JSON->false; $_; } @{$pickup_locations};
536 openapi => \@response
540 $c->unhandled_exception($_);
544 =head3 get_items_public
546 Controller function that handles retrieving biblio's items, for unprivileged
551 sub get_items_public {
552 my $c = shift->openapi->valid_input or return;
554 my $biblio = Koha::Biblios->find(
555 $c->param('biblio_id'),
556 { prefetch => ['items'] }
559 return $c->render_resource_not_found("Bibliographic record")
564 my $patron = $c->stash('koha.user');
566 my $items_rs = $biblio->items->filter_by_visible_in_opac({ patron => $patron });
567 my $items = $c->objects->search( $items_rs );
574 $c->unhandled_exception($_);
580 Set rating for the logged in user
586 my $c = shift->openapi->valid_input or return;
588 my $biblio = Koha::Biblios->find( $c->param('biblio_id') );
590 $c->render_resource_not_found("Bibliographic record")
593 my $patron = $c->stash('koha.user');
598 { error => "Cannot rate. Reason: must be logged-in" }
602 my $body = $c->req->json;
603 my $rating_value = $body->{rating};
607 my $rating = Koha::Ratings->find(
609 biblionumber => $biblio->biblionumber,
610 borrowernumber => $patron->borrowernumber,
613 $rating->delete if $rating;
615 if ( $rating_value ) { # Cannot set to 0 from the UI
616 $rating = Koha::Rating->new(
618 biblionumber => $biblio->biblionumber,
619 borrowernumber => $patron->borrowernumber,
620 rating_value => $rating_value,
625 Koha::Ratings->search( { biblionumber => $biblio->biblionumber } );
626 my $average = $ratings->get_avg_rating;
631 rating => $rating && $rating->in_storage ? $rating->rating_value : undef,
633 count => $ratings->count
638 $c->unhandled_exception($_);
644 Controller function that handles creating a biblio object
649 my $c = shift->openapi->valid_input or return;
652 my $headers = $c->req->headers;
654 my $flavour = $headers->header('x-record-schema');
655 $flavour //= C4::Context->preference('marcflavour');
657 my $record_source_id = $headers->header('x-record-source-id');
659 if ($record_source_id) {
661 # We've been passed a record source. Verify they are allowed to
662 unless ( haspermission( $c->stash('koha.user')->userid, { editcatalogue => 'set_record_sources' } ) ) {
665 openapi => { error => 'You do not have permission to set the record source' }
672 my $frameworkcode = $headers->header('x-framework-id');
673 my $content_type = $headers->content_type;
675 if ( $content_type =~ m/application\/marcxml\+xml/ ) {
676 $record = MARC::Record->new_from_xml( $c->req->body, 'UTF-8', $flavour );
678 elsif ( $content_type =~ m/application\/marc-in-json/ ) {
679 $record = MARC::Record->new_from_mij_structure( $c->req->json );
681 elsif ( $content_type =~ m/application\/marc/ ) {
682 $record = MARC::Record->new_from_usmarc( $c->req->body );
688 "application/marcxml+xml",
689 "application/marc-in-json",
695 my ( $duplicatebiblionumber, $duplicatetitle );
696 ( $duplicatebiblionumber, $duplicatetitle ) = FindDuplicate($record);
698 my $confirm_not_duplicate = $headers->header('x-confirm-not-duplicate');
703 error => "Duplicate biblio $duplicatebiblionumber",
705 ) unless !$duplicatebiblionumber || $confirm_not_duplicate;
707 my ( $biblio_id ) = AddBiblio( $record, $frameworkcode, { record_source_id => $record_source_id } );
711 openapi => { id => $biblio_id }
715 $c->unhandled_exception($_);
721 Controller function that handles modifying an biblio object
726 my $c = shift->openapi->valid_input or return;
728 my $biblio = Koha::Biblios->find( $c->param('biblio_id') );
730 $c->render_resource_not_found("Bibliographic record")
734 my $headers = $c->req->headers;
736 my $flavour = $headers->header('x-record-schema');
737 $flavour //= C4::Context->preference('marcflavour');
739 my $frameworkcode = $headers->header('x-framework-id') || $biblio->frameworkcode;
741 my $content_type = $headers->content_type;
745 if ( $content_type =~ m/application\/marcxml\+xml/ ) {
746 $record = MARC::Record->new_from_xml( $c->req->body, 'UTF-8', $flavour );
748 elsif ( $content_type =~ m/application\/marc-in-json/ ) {
749 $record = MARC::Record->new_from_mij_structure( $c->req->json );
751 elsif ( $content_type =~ m/application\/marc/ ) {
752 $record = MARC::Record->new_from_usmarc( $c->req->body );
759 "application/marcxml+xml",
760 "application/marc-in-json",
766 ModBiblio( $record, $biblio->id, $frameworkcode );
770 openapi => { id => $biblio->id }
774 $c->unhandled_exception($_);
780 Controller function that handles retrieving a single biblio object
785 my $c = shift->openapi->valid_input or return;
787 my @prefetch = qw(biblioitem);
788 push @prefetch, 'metadata' # don't prefetch metadata if not needed
789 unless $c->req->headers->accept =~ m/application\/json/;
791 my $rs = Koha::Biblios->search( undef, { prefetch => \@prefetch });
792 my $biblios = $c->objects->search_rs( $rs, [(sub{ $rs->api_query_fixer( $_[0], '', $_[1] ) })] );
796 if ( $c->req->headers->accept =~ m/application\/json(;.*)?$/ ) {
799 json => $c->objects->to_api( $biblios ),
803 $c->req->headers->accept =~ m/application\/marcxml\+xml(;.*)?$/ )
805 $c->res->headers->add( 'Content-Type', 'application/marcxml+xml' );
808 text => $biblios->print_collection('marcxml')
812 $c->req->headers->accept =~ m/application\/marc-in-json(;.*)?$/ )
814 $c->res->headers->add( 'Content-Type', 'application/marc-in-json' );
817 data => $biblios->print_collection('mij')
820 elsif ( $c->req->headers->accept =~ m/application\/marc(;.*)?$/ ) {
821 $c->res->headers->add( 'Content-Type', 'application/marc' );
824 text => $biblios->print_collection('marc')
827 elsif ( $c->req->headers->accept =~ m/text\/plain(;.*)?$/ ) {
830 text => $biblios->print_collection('txt')
837 "application/json", "application/marcxml+xml",
838 "application/marc-in-json", "application/marc",
845 $c->unhandled_exception($_);
851 Controller function that handles merging two biblios. If an optional
852 MARCXML is provided as the request body, this MARCXML replaces the
853 bibliodata of the merge target biblio. Syntax format inside the request body
854 must match with the Marc format used into Koha installation (MARC21 or UNIMARC)
859 my $c = shift->openapi->valid_input or return;
860 my $ref_biblionumber = $c->param('biblio_id');
861 my $json = decode_json( $c->req->body );
862 my $bn_merge = $json->{'biblio_id_to_merge'};
863 my $framework = $json->{'framework_to_use'} // q{};
864 my $rules = $json->{'rules'} || q{override};
865 my $override_rec = $json->{'datarecord'} // q{};
867 my $biblio = Koha::Biblios->find($ref_biblionumber);
868 if ( not defined $biblio ) {
871 json => { error => sprintf( "[%s] biblio to merge into not found", $ref_biblionumber ) }
874 my $frombib = Koha::Biblios->find($bn_merge);
875 if ( not defined $frombib ) {
878 json => { error => sprintf( "[%s] from which to merge not found", $bn_merge ) }
882 if ( ( $rules eq 'override_ext' ) && ( $override_rec eq '' ) ) {
887 "With the rule 'override_ext' you need to insert a bib record in marc-in-json format into 'record' field."
892 if ( ( $rules eq 'override' ) && ( $framework ne '' ) ) {
895 json => { error => "With the rule 'override' you can not use the field 'framework_to_use'." }
900 if ( $rules eq 'override_ext' ) {
901 my $record = MARC::Record::MiJ->new_from_mij_structure($override_rec);
902 $record->encoding('UTF-8');
903 $framework ||= $biblio->frameworkcode;
904 my $chk = ModBiblio( $record, $ref_biblionumber, $framework );
905 if ( $chk != 1 ) { die "Error on ModBiblio"; } # ModBiblio returns 1 if everything as gone well
908 $biblio->merge_with( [$bn_merge] );
914 data => $biblio->metadata->record->to_mij
918 $c->render( status => 400, json => { error => $@ } );