Bug 31799: (follow-up) Tidy up and fix duplicate barcode handling
[koha.git] / Koha / REST / V1 / Biblios.pm
1 package Koha::REST::V1::Biblios;
2
3 # This file is part of Koha.
4 #
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.
9 #
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.
14 #
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>.
17
18 use Modern::Perl;
19
20 use Mojo::Base 'Mojolicious::Controller';
21
22 use Koha::Biblios;
23 use Koha::DateUtils;
24 use Koha::Ratings;
25 use Koha::RecordProcessor;
26 use C4::Biblio qw( DelBiblio AddBiblio ModBiblio );
27 use C4::Search qw( FindDuplicate );
28
29 use C4::Barcodes::ValueBuilder;
30 use C4::Context;
31
32 use Koha::Items;
33
34 use List::MoreUtils qw( any );
35 use MARC::Record::MiJ;
36
37 use Try::Tiny qw( catch try );
38
39 =head1 API
40
41 =head2 Methods
42
43 =head3 get
44
45 Controller function that handles retrieving a single biblio object
46
47 =cut
48
49 sub get {
50     my $c = shift->openapi->valid_input or return;
51
52     my $attributes;
53     $attributes = { prefetch => [ 'metadata' ] } # don't prefetch metadata if not needed
54         unless $c->req->headers->accept =~ m/application\/json/;
55
56     my $biblio = Koha::Biblios->find( { biblionumber => $c->validation->param('biblio_id') }, $attributes );
57
58     unless ( $biblio ) {
59         return $c->render(
60             status  => 404,
61             openapi => {
62                 error => "Object not found."
63             }
64         );
65     }
66
67     return try {
68
69         if ( $c->req->headers->accept =~ m/application\/json/ ) {
70             return $c->render(
71                 status => 200,
72                 json   => $biblio->to_api
73             );
74         }
75         else {
76             my $metadata = $biblio->metadata;
77             my $record   = $metadata->record;
78             my $schema   = $metadata->schema // C4::Context->preference("marcflavour");
79
80             $c->respond_to(
81                 marcxml => {
82                     status => 200,
83                     format => 'marcxml',
84                     text   => $record->as_xml_record($schema),
85                 },
86                 mij => {
87                     status => 200,
88                     format => 'mij',
89                     data   => $record->to_mij
90                 },
91                 marc => {
92                     status => 200,
93                     format => 'marc',
94                     text   => $record->as_usmarc
95                 },
96                 txt => {
97                     status => 200,
98                     format => 'text/plain',
99                     text   => $record->as_formatted
100                 },
101                 any => {
102                     status  => 406,
103                     openapi => [
104                         "application/json",
105                         "application/marcxml+xml",
106                         "application/marc-in-json",
107                         "application/marc",
108                         "text/plain"
109                     ]
110                 }
111             );
112         }
113     }
114     catch {
115         $c->unhandled_exception($_);
116     };
117 }
118
119 =head3 delete
120
121 Controller function that handles deleting a biblio object
122
123 =cut
124
125 sub delete {
126     my $c = shift->openapi->valid_input or return;
127
128     my $biblio = Koha::Biblios->find( $c->validation->param('biblio_id') );
129
130     if ( not defined $biblio ) {
131         return $c->render(
132             status  => 404,
133             openapi => { error => "Object not found" }
134         );
135     }
136
137     return try {
138         my $error = DelBiblio( $biblio->id );
139
140         if ($error) {
141             return $c->render(
142                 status  => 409,
143                 openapi => { error => $error }
144             );
145         }
146         else {
147             return $c->render( status => 204, openapi => "" );
148         }
149     }
150     catch {
151         $c->unhandled_exception($_);
152     };
153 }
154
155 =head3 get_public
156
157 Controller function that handles retrieving a single biblio object
158
159 =cut
160
161 sub get_public {
162     my $c = shift->openapi->valid_input or return;
163
164     my $biblio = Koha::Biblios->find(
165         { biblionumber => $c->validation->param('biblio_id') },
166         { prefetch     => ['metadata'] } );
167
168     unless ($biblio) {
169         return $c->render(
170             status  => 404,
171             openapi => {
172                 error => "Object not found."
173             }
174         );
175     }
176
177     return try {
178
179         my $metadata = $biblio->metadata;
180         my $record   = $metadata->record;
181
182         my $opachiddenitems_rules = C4::Context->yaml_preference('OpacHiddenItems');
183         my $patron = $c->stash('koha.user');
184
185         # Check if the biblio should be hidden for unprivileged access
186         # unless there's a logged in user, and there's an exception for it's
187         # category
188         unless ( $patron and $patron->category->override_hidden_items ) {
189             if ( $biblio->hidden_in_opac({ rules => $opachiddenitems_rules }) )
190             {
191                 return $c->render(
192                     status  => 404,
193                     openapi => {
194                         error => "Object not found."
195                     }
196                 );
197             }
198         }
199
200         my $schema = $metadata->schema // C4::Context->preference("marcflavour");
201
202         my $record_processor = Koha::RecordProcessor->new({
203             filters => 'ViewPolicy',
204             options => {
205                 interface => 'opac',
206                 frameworkcode => $biblio->frameworkcode
207             }
208         });
209         # Apply framework's filtering to MARC::Record object
210         $record_processor->process($record);
211
212         $c->respond_to(
213             marcxml => {
214                 status => 200,
215                 format => 'marcxml',
216                 text   => $record->as_xml_record($schema),
217             },
218             mij => {
219                 status => 200,
220                 format => 'mij',
221                 data   => $record->to_mij
222             },
223             marc => {
224                 status => 200,
225                 format => 'marc',
226                 text   => $record->as_usmarc
227             },
228             txt => {
229                 status => 200,
230                 format => 'text/plain',
231                 text   => $record->as_formatted
232             },
233             any => {
234                 status  => 406,
235                 openapi => [
236                     "application/marcxml+xml",
237                     "application/marc-in-json",
238                     "application/marc",
239                     "text/plain"
240                 ]
241             }
242         );
243     }
244     catch {
245         $c->unhandled_exception($_);
246     };
247 }
248
249 =head3 get_items
250
251 Controller function that handles retrieving biblio's items
252
253 =cut
254
255 sub get_items {
256     my $c = shift->openapi->valid_input or return;
257
258     my $biblio = Koha::Biblios->find( { biblionumber => $c->validation->param('biblio_id') }, { prefetch => ['items'] } );
259
260     unless ( $biblio ) {
261         return $c->render(
262             status  => 404,
263             openapi => {
264                 error => "Object not found."
265             }
266         );
267     }
268
269     return try {
270
271         my $items_rs = $biblio->items;
272         my $items    = $c->objects->search( $items_rs );
273         return $c->render(
274             status  => 200,
275             openapi => $items
276         );
277     }
278     catch {
279         $c->unhandled_exception($_);
280     };
281 }
282
283 =head3 add_item
284
285 Controller function that handles creating a biblio's item
286
287 =cut
288
289 sub add_item {
290     my $c = shift->openapi->valid_input or return;
291
292     try {
293         my $biblio_id = $c->validation->param('biblio_id');
294         my $biblio    = Koha::Biblios->find( $biblio_id );
295
296         unless ($biblio) {
297             return $c->render(
298                 status  => 404,
299                 openapi => { error => "Biblio not found" }
300             );
301         }
302
303         my $body = $c->validation->param('body');
304
305         $body->{biblio_id} = $biblio_id;
306
307         # Don't save extended subfields yet. To be done in another bug.
308         $body->{extended_subfields} = undef;
309
310         my $item = Koha::Item->new_from_api($body);
311
312         if ( !defined $item->barcode ) {
313
314             # FIXME This should be moved to Koha::Item->store
315             my $autoBarcode = C4::Context->preference('autoBarcode');
316             my $barcode     = '';
317
318             if ( !$autoBarcode || $autoBarcode eq 'OFF' ) {
319                 #We do nothing
320             }
321             elsif ( $autoBarcode eq 'incremental' ) {
322                 ($barcode) =
323                   C4::Barcodes::ValueBuilder::incremental::get_barcode;
324             }
325             elsif ( $autoBarcode eq 'annual' ) {
326                 my $year = Koha::DateUtils::dt_from_string()->year();
327                 ($barcode) =
328                   C4::Barcodes::ValueBuilder::annual::get_barcode(
329                     { year => $year } );
330             }
331             elsif ( $autoBarcode eq 'hbyymmincr' ) {
332
333                 # Generates a barcode where
334                 #  hb = home branch Code,
335                 #  yymm = year/month catalogued,
336                 #  incr = incremental number,
337                 #  reset yearly -fbcit
338                 my $now        = Koha::DateUtils::dt_from_string();
339                 my $year       = $now->year();
340                 my $month      = $now->month();
341                 my $homebranch = $item->homebranch // '';
342                 ($barcode) =
343                   C4::Barcodes::ValueBuilder::hbyymmincr::get_barcode(
344                     { year => $year, mon => $month } );
345                 $barcode = $homebranch . $barcode;
346             }
347             elsif ( $autoBarcode eq 'EAN13' ) {
348
349                 # not the best, two catalogers could add the same
350                 # barcode easily this way :/
351                 my $query = "select max(abs(barcode)) from items";
352                 my $dbh   = C4::Context->dbh;
353                 my $sth   = $dbh->prepare($query);
354                 $sth->execute();
355                 my $nextnum;
356                 while ( my ($last) = $sth->fetchrow_array ) {
357                     $nextnum = $last;
358                 }
359                 my $ean = CheckDigits('ean');
360                 if ( $ean->is_valid($nextnum) ) {
361                     my $next = $ean->basenumber($nextnum) + 1;
362                     $nextnum = $ean->complete($next);
363                     $nextnum =
364                       '0' x ( 13 - length($nextnum) ) . $nextnum;    # pad zeros
365                 }
366                 else {
367                     warn "ERROR: invalid EAN-13 $nextnum, using increment";
368                     $nextnum++;
369                 }
370                 $barcode = $nextnum;
371             }
372             else {
373                 warn "ERROR: unknown autoBarcode: $autoBarcode";
374             }
375             $item->barcode($barcode) if $barcode;
376         }
377
378         $item->store->discard_changes;
379
380         $c->render(
381             status  => 201,
382             openapi => $item->to_api
383         );
384     }
385     catch {
386         if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
387             return $c->render(
388                 status  => 409,
389                 openapi => { error => 'Duplicate barcode.' }
390             );
391         }
392         $c->unhandled_exception($_);
393     }
394 }
395
396 =head3 update_item
397
398 Controller function that handles updating a biblio's item
399
400 =cut
401
402 sub update_item {
403     my $c = shift->openapi->valid_input or return;
404
405     try {
406         my $biblio_id = $c->validation->param('biblio_id');
407         my $item_id = $c->validation->param('item_id');
408         my $biblio = Koha::Biblios->find({ biblionumber => $biblio_id });
409         unless ($biblio) {
410             return $c->render(
411                 status  => 404,
412                 openapi => { error => "Biblio not found" }
413             );
414         }
415
416         my $item = $biblio->items->find({ itemnumber => $item_id });
417
418         unless ($item) {
419             return $c->render(
420                 status  => 404,
421                 openapi => { error => "Item not found" }
422             );
423         }
424
425         my $body = $c->validation->param('body');
426
427         $body->{biblio_id} = $biblio_id;
428
429         # Don't save extended subfields yet. To be done in another bug.
430         $body->{extended_subfields} = undef;
431
432         $item->set_from_api($body);
433
434         $item->store->discard_changes;
435
436         $c->render(
437             status => 200,
438             openapi => $item->to_api
439         );
440     }
441     catch {
442         if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
443             return $c->render(
444                 status  => 409,
445                 openapi => { error => 'Duplicate barcode.' }
446             );
447         }
448         $c->unhandled_exception($_);
449     }
450 }
451
452 =head3 get_checkouts
453
454 List Koha::Checkout objects
455
456 =cut
457
458 sub get_checkouts {
459     my $c = shift->openapi->valid_input or return;
460
461     my $checked_in = delete $c->validation->output->{checked_in};
462
463     try {
464         my $biblio = Koha::Biblios->find( $c->validation->param('biblio_id') );
465
466         unless ($biblio) {
467             return $c->render(
468                 status  => 404,
469                 openapi => { error => 'Object not found' }
470             );
471         }
472
473         my $checkouts =
474           ($checked_in)
475           ? $c->objects->search( $biblio->old_checkouts )
476           : $c->objects->search( $biblio->current_checkouts );
477
478         return $c->render(
479             status  => 200,
480             openapi => $checkouts
481         );
482     }
483     catch {
484         $c->unhandled_exception($_);
485     };
486 }
487
488 =head3 pickup_locations
489
490 Method that returns the possible pickup_locations for a given biblio
491 used for building the dropdown selector
492
493 =cut
494
495 sub pickup_locations {
496     my $c = shift->openapi->valid_input or return;
497
498     my $biblio_id = $c->validation->param('biblio_id');
499     my $biblio = Koha::Biblios->find( $biblio_id );
500
501     unless ($biblio) {
502         return $c->render(
503             status  => 404,
504             openapi => { error => "Biblio not found" }
505         );
506     }
507
508     my $patron_id = delete $c->validation->output->{patron_id};
509     my $patron    = Koha::Patrons->find( $patron_id );
510
511     unless ($patron) {
512         return $c->render(
513             status  => 400,
514             openapi => { error => "Patron not found" }
515         );
516     }
517
518     return try {
519
520         my $pl_set = $biblio->pickup_locations( { patron => $patron } );
521
522         my @response = ();
523         if ( C4::Context->preference('AllowHoldPolicyOverride') ) {
524
525             my $libraries_rs = Koha::Libraries->search( { pickup_location => 1 } );
526             my $libraries    = $c->objects->search($libraries_rs);
527
528             @response = map {
529                 my $library = $_;
530                 $library->{needs_override} = (
531                     any { $_->branchcode eq $library->{library_id} }
532                     @{$pl_set->as_list}
533                   )
534                   ? Mojo::JSON->false
535                   : Mojo::JSON->true;
536                 $library;
537             } @{$libraries};
538         }
539         else {
540
541             my $pickup_locations = $c->objects->search($pl_set);
542             @response = map { $_->{needs_override} = Mojo::JSON->false; $_; } @{$pickup_locations};
543         }
544
545         return $c->render(
546             status  => 200,
547             openapi => \@response
548         );
549     }
550     catch {
551         $c->unhandled_exception($_);
552     };
553 }
554
555 =head3 get_items_public
556
557 Controller function that handles retrieving biblio's items, for unprivileged
558 access.
559
560 =cut
561
562 sub get_items_public {
563     my $c = shift->openapi->valid_input or return;
564
565     my $biblio = Koha::Biblios->find( { biblionumber => $c->validation->param('biblio_id') }, { prefetch => ['items'] } );
566
567     unless ( $biblio ) {
568         return $c->render(
569             status  => 404,
570             openapi => {
571                 error => "Object not found."
572             }
573         );
574     }
575
576     return try {
577
578         my $patron = $c->stash('koha.user');
579
580         my $items_rs = $biblio->items->filter_by_visible_in_opac({ patron => $patron });
581         my $items    = $c->objects->search( $items_rs );
582         return $c->render(
583             status  => 200,
584             openapi => $items
585         );
586     }
587     catch {
588         $c->unhandled_exception($_);
589     };
590 }
591
592 =head3 set_rating
593
594 Set rating for the logged in user
595
596 =cut
597
598
599 sub set_rating {
600     my $c = shift->openapi->valid_input or return;
601
602     my $biblio = Koha::Biblios->find( $c->validation->param('biblio_id') );
603
604     unless ($biblio) {
605         return $c->render(
606             status  => 404,
607             openapi => {
608                 error => "Object not found."
609             }
610         );
611     }
612
613     my $patron = $c->stash('koha.user');
614     unless ($patron) {
615         return $c->render(
616             status => 403,
617             openapi =>
618                 { error => "Cannot rate. Reason: must be logged-in" }
619         );
620     }
621
622     my $body   = $c->validation->param('body');
623     my $rating_value = $body->{rating};
624
625     return try {
626
627         my $rating = Koha::Ratings->find(
628             {
629                 biblionumber   => $biblio->biblionumber,
630                 borrowernumber => $patron->borrowernumber,
631             }
632         );
633         $rating->delete if $rating;
634
635         if ( $rating_value ) { # Cannot set to 0 from the UI
636             $rating = Koha::Rating->new(
637                 {
638                     biblionumber   => $biblio->biblionumber,
639                     borrowernumber => $patron->borrowernumber,
640                     rating_value   => $rating_value,
641                 }
642             )->store;
643         };
644         my $ratings =
645           Koha::Ratings->search( { biblionumber => $biblio->biblionumber } );
646         my $average = $ratings->get_avg_rating;
647
648         return $c->render(
649             status  => 200,
650             openapi => {
651                 rating  => $rating && $rating->in_storage ? $rating->rating_value : undef,
652                 average => $average,
653                 count   => $ratings->count
654             },
655         );
656     }
657     catch {
658         $c->unhandled_exception($_);
659     };
660 }
661
662 =head3 add
663
664 Controller function that handles creating a biblio object
665
666 =cut
667
668 sub add {
669     my $c = shift->openapi->valid_input or return;
670
671     try {
672         my $headers = $c->req->headers;
673
674         my $flavour = $headers->header('x-record-schema');
675         $flavour //= C4::Context->preference('marcflavour');
676
677         my $record;
678
679         my $frameworkcode = $headers->header('x-framework-id');
680         my $content_type  = $headers->content_type;
681
682         if ( $content_type =~ m/application\/marcxml\+xml/ ) {
683             $record = MARC::Record->new_from_xml( $c->req->body, 'UTF-8', $flavour );
684         }
685         elsif ( $content_type =~ m/application\/marc-in-json/ ) {
686             $record = MARC::Record->new_from_mij_structure( $c->req->json );
687         }
688         elsif ( $content_type =~ m/application\/marc/ ) {
689             $record = MARC::Record->new_from_usmarc( $c->req->body );
690         }
691         else {
692             return $c->render(
693                 status  => 406,
694                 openapi => [
695                     "application/marcxml+xml",
696                     "application/marc-in-json",
697                     "application/marc"
698                 ]
699             );
700         }
701
702         my ( $duplicatebiblionumber, $duplicatetitle );
703             ( $duplicatebiblionumber, $duplicatetitle ) = FindDuplicate($record);
704
705         my $confirm_not_duplicate = $headers->header('x-confirm-not-duplicate');
706
707         return $c->render(
708             status  => 400,
709             openapi => {
710                 error => "Duplicate biblio $duplicatebiblionumber",
711             }
712         ) unless !$duplicatebiblionumber || $confirm_not_duplicate;
713
714         my ( $biblionumber, $oldbibitemnum );
715             ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
716
717         $c->render(
718             status  => 200,
719             openapi => { id => $biblionumber }
720         );
721     }
722     catch {
723         $c->unhandled_exception($_);
724     };
725 }
726
727 =head3 update
728
729 Controller function that handles modifying an biblio object
730
731 =cut
732
733 sub update {
734     my $c = shift->openapi->valid_input or return;
735
736     my $biblio_id = $c->param('biblio_id');
737     my $biblio    = Koha::Biblios->find($biblio_id);
738
739     if ( ! defined $biblio ) {
740         return $c->render(
741             status  => 404,
742             openapi => { error => "Object not found" }
743         );
744     }
745
746     try {
747         my $headers = $c->req->headers;
748
749         my $flavour = $headers->header('x-record-schema');
750         $flavour //= C4::Context->preference('marcflavour');
751
752         my $frameworkcode = $headers->header('x-framework-id') || $biblio->frameworkcode;
753
754         my $content_type = $headers->content_type;
755
756         my $record;
757
758         if ( $content_type =~ m/application\/marcxml\+xml/ ) {
759             $record = MARC::Record->new_from_xml( $c->req->body, 'UTF-8', $flavour );
760         }
761         elsif ( $content_type =~ m/application\/marc-in-json/ ) {
762             $record = MARC::Record->new_from_mij_structure( $c->req->json );
763         }
764         elsif ( $content_type =~ m/application\/marc/ ) {
765             $record = MARC::Record->new_from_usmarc( $c->req->body );
766         }
767         else {
768             return $c->render(
769                 status  => 406,
770                 openapi => [
771                     "application/json",
772                     "application/marcxml+xml",
773                     "application/marc-in-json",
774                     "application/marc"
775                 ]
776             );
777         }
778
779         ModBiblio( $record, $biblio_id, $frameworkcode );
780
781         $c->render(
782             status  => 200,
783             openapi => { id => $biblio_id }
784         );
785     }
786     catch {
787         $c->unhandled_exception($_);
788     };
789 }
790
791 =head3 list
792
793 Controller function that handles retrieving a single biblio object
794
795 =cut
796
797 sub list {
798     my $c = shift->openapi->valid_input or return;
799
800     my $attributes;
801     $attributes =
802       { prefetch => ['metadata'] }    # don't prefetch metadata if not needed
803       unless $c->req->headers->accept =~ m/application\/json/;
804
805     my $biblios = $c->objects->search_rs( Koha::Biblios->new );
806
807     return try {
808
809         if ( $c->req->headers->accept =~ m/application\/json(;.*)?$/ ) {
810             return $c->render(
811                 status => 200,
812                 json   => $c->objects->to_api( $biblios ),
813             );
814         }
815         elsif (
816             $c->req->headers->accept =~ m/application\/marcxml\+xml(;.*)?$/ )
817         {
818             $c->res->headers->add( 'Content-Type', 'application/marcxml+xml' );
819             return $c->render(
820                 status => 200,
821                 text   => $biblios->print_collection('marcxml')
822             );
823         }
824         elsif (
825             $c->req->headers->accept =~ m/application\/marc-in-json(;.*)?$/ )
826         {
827             $c->res->headers->add( 'Content-Type', 'application/marc-in-json' );
828             return $c->render(
829                 status => 200,
830                 data   => $biblios->print_collection('mij')
831             );
832         }
833         elsif ( $c->req->headers->accept =~ m/application\/marc(;.*)?$/ ) {
834             $c->res->headers->add( 'Content-Type', 'application/marc' );
835             return $c->render(
836                 status => 200,
837                 text   => $biblios->print_collection('marc')
838             );
839         }
840         elsif ( $c->req->headers->accept =~ m/text\/plain(;.*)?$/ ) {
841             return $c->render(
842                 status => 200,
843                 text   => $biblios->print_collection('txt')
844             );
845         }
846         else {
847             return $c->render(
848                 status  => 406,
849                 openapi => [
850                     "application/json",         "application/marcxml+xml",
851                     "application/marc-in-json", "application/marc",
852                     "text/plain"
853                 ]
854             );
855         }
856     }
857     catch {
858         $c->unhandled_exception($_);
859     };
860 }
861
862 1;