Bug 17600: Standardize our EXPORT_OK
[srvgit] / C4 / ILSDI / Services.pm
1 package C4::ILSDI::Services;
2
3 # Copyright 2009 SARL Biblibre
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use strict;
21 use warnings;
22
23 use C4::Members;
24 use C4::Items qw( get_hostitemnumbers_of );
25 use C4::Circulation qw( CanBookBeRenewed barcodedecode CanBookBeIssued AddRenewal );
26 use C4::Accounts;
27 use C4::Biblio qw( GetMarcBiblio );
28 use C4::Reserves qw( CanBookBeReserved IsAvailableForItemLevelRequest CalculatePriority AddReserve CanItemBeReserved );
29 use C4::Context;
30 use C4::Auth;
31 use CGI qw ( -utf8 );
32 use DateTime;
33 use C4::Auth;
34 use Koha::DateUtils qw( dt_from_string );
35
36 use Koha::Biblios;
37 use Koha::Checkouts;
38 use Koha::Items;
39 use Koha::Libraries;
40 use Koha::Patrons;
41
42 =head1 NAME
43
44 C4::ILS-DI::Services - ILS-DI Services
45
46 =head1 DESCRIPTION
47
48 Each function in this module represents an ILS-DI service.
49 They all takes a CGI instance as argument and most of them return a
50 hashref that will be printed by XML::Simple in opac/ilsdi.pl
51
52 =head1 SYNOPSIS
53
54     use C4::ILSDI::Services;
55     use XML::Simple;
56     use CGI qw ( -utf8 );
57
58     my $cgi = new CGI;
59
60     $out = LookupPatron($cgi);
61
62     print CGI::header('text/xml');
63     print XMLout($out,
64         noattr => 1,
65         noescape => 1,
66         nosort => 1,
67                 xmldecl => '<?xml version="1.0" encoding="UTF-8" ?>',
68         RootName => 'LookupPatron',
69         SuppressEmpty => 1);
70
71 =cut
72
73 =head1 FUNCTIONS
74
75 =head2 GetAvailability
76
77 Given a set of biblionumbers or itemnumbers, returns a list with
78 availability of the items associated with the identifiers.
79
80 Parameters:
81
82 =head3 id (Required)
83
84 list of either biblionumbers or itemnumbers
85
86 =head3 id_type (Required)
87
88 defines the type of record identifier being used in the request,
89 possible values:
90
91   - bib
92   - item
93
94 =head3 return_type (Optional)
95
96 requests a particular level of detail in reporting availability,
97 possible values:
98
99   - bib
100   - item
101
102 =head3 return_fmt (Optional)
103
104 requests a particular format or set of formats in reporting
105 availability
106
107 =cut
108
109 sub GetAvailability {
110     my ($cgi) = @_;
111
112     my $out = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
113     $out .= "<dlf:collection\n";
114     $out .= "  xmlns:dlf=\"http://diglib.org/ilsdi/1.1\"\n";
115     $out .= "  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n";
116     $out .= "  xsi:schemaLocation=\"http://diglib.org/ilsdi/1.1\n";
117     $out .= "    http://diglib.org/architectures/ilsdi/schemas/1.1/dlfexpanded.xsd\">\n";
118
119     foreach my $id ( split( / /, $cgi->param('id') ) ) {
120         if ( $cgi->param('id_type') eq "item" ) {
121             my ( $biblionumber, $status, $msg, $location ) = _availability($id);
122
123             $out .= "  <dlf:record>\n";
124             $out .= "    <dlf:bibliographic id=\"" . ( $biblionumber || $id ) . "\" />\n";
125             $out .= "    <dlf:items>\n";
126             $out .= "      <dlf:item id=\"" . $id . "\">\n";
127             $out .= "        <dlf:simpleavailability>\n";
128             $out .= "          <dlf:identifier>" . $id . "</dlf:identifier>\n";
129             $out .= "          <dlf:availabilitystatus>" . $status . "</dlf:availabilitystatus>\n";
130             if ($msg)      { $out .= "          <dlf:availabilitymsg>" . $msg . "</dlf:availabilitymsg>\n"; }
131             if ($location) { $out .= "          <dlf:location>" . $location . "</dlf:location>\n"; }
132             $out .= "        </dlf:simpleavailability>\n";
133             $out .= "      </dlf:item>\n";
134             $out .= "    </dlf:items>\n";
135             $out .= "  </dlf:record>\n";
136         } else {
137             my $status;
138             my $msg;
139             my $items = Koha::Items->search({ biblionumber => $id });
140             if ($items->count) {
141                 # Open XML
142                 $out .= "  <dlf:record>\n";
143                 $out .= "    <dlf:bibliographic id=\"" .$id. "\" />\n";
144                 $out .= "    <dlf:items>\n";
145                 # We loop over the items to clean them
146                 while ( my $item = $items->next ) {
147                     my $itemnumber = $item->itemnumber;
148                     my ( $biblionumber, $status, $msg, $location ) = _availability($itemnumber);
149                     $out .= "      <dlf:item id=\"" . $itemnumber . "\">\n";
150                     $out .= "        <dlf:simpleavailability>\n";
151                     $out .= "          <dlf:identifier>" . $itemnumber . "</dlf:identifier>\n";
152                     $out .= "          <dlf:availabilitystatus>" . $status . "</dlf:availabilitystatus>\n";
153                     if ($msg)      { $out .= "          <dlf:availabilitymsg>" . $msg . "</dlf:availabilitymsg>\n"; }
154                     if ($location) { $out .= "          <dlf:location>" . $location . "</dlf:location>\n"; }
155                     $out .= "        </dlf:simpleavailability>\n";
156                     $out .= "      </dlf:item>\n";
157                 }
158                 # Close XML
159                 $out .= "    </dlf:items>\n";
160                 $out .= "  </dlf:record>\n";
161             } else {
162                 $status = "unknown";
163                 $msg    = "Error: could not retrieve availability for this ID";
164             }
165         }
166     }
167     $out .= "</dlf:collection>\n";
168
169     return $out;
170 }
171
172 =head2 GetRecords
173
174 Given a list of biblionumbers, returns a list of record objects that
175 contain bibliographic information, as well as associated holdings and item
176 information. The caller may request a specific metadata schema for the
177 record objects to be returned.
178
179 This function behaves similarly to HarvestBibliographicRecords and
180 HarvestExpandedRecords in Data Aggregation, but allows quick, real time
181 lookup by bibliographic identifier.
182
183 You can use OAI-PMH ListRecords instead of this service.
184
185 Parameters:
186
187   - id (Required)
188     list of system record identifiers
189   - id_type (Optional)
190     Defines the metadata schema in which the records are returned,
191     possible values:
192         - MARCXML
193
194 =cut
195
196 sub GetRecords {
197     my ($cgi) = @_;
198
199     # Check if the schema is supported. For now, GetRecords only supports MARCXML
200     if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) {
201         return { code => 'UnsupportedSchema' };
202     }
203
204     my @records;
205
206     # Loop over biblionumbers
207     foreach my $biblionumber ( split( / /, $cgi->param('id') ) ) {
208
209         # Get the biblioitem from the biblionumber
210         my $biblio = Koha::Biblios->find( $biblionumber );
211         unless ( $biblio ) {
212             push @records, { code => "RecordNotFound" };
213             next;
214         }
215
216         my $biblioitem = $biblio->biblioitem->unblessed;
217
218         my $embed_items = 1;
219         my $record = GetMarcBiblio({
220             biblionumber => $biblionumber,
221             embed_items  => $embed_items });
222         if ($record) {
223             $biblioitem->{marcxml} = $record->as_xml_record();
224         }
225
226         # Get most of the needed data
227         my $biblioitemnumber = $biblioitem->{'biblioitemnumber'};
228         my $checkouts = Koha::Checkouts->search(
229             { biblionumber => $biblionumber },
230             {
231                 join => 'item',
232                 '+select' => ['item.barcode'],
233                 '+as'     => ['barcode'],
234             }
235         )->unblessed;
236         foreach my $checkout (@$checkouts) {
237             delete $checkout->{'borrowernumber'};
238         }
239         my @items            = $biblio->items->as_list;
240
241         $biblioitem->{items}->{item} = [];
242
243         # We loop over the items to clean them
244         foreach my $item (@items) {
245             my %item = %{ $item->unblessed };
246
247             # This hides additionnal XML subfields, we don't need these info
248             delete $item{'more_subfields_xml'};
249
250             # Display branch names instead of branch codes
251             my $home_library    = $item->home_branch;
252             my $holding_library = $item->holding_branch;
253             $item{'homebranchname'}    = $home_library    ? $home_library->branchname    : '';
254             $item{'holdingbranchname'} = $holding_library ? $holding_library->branchname : '';
255
256             if ($item->location) {
257                 my $authorised_value = Koha::AuthorisedValues->find_by_koha_field({ kohafield => 'items.location', authorised_value => $item->location });
258                 if ($authorised_value) {
259                     $item{location_description} = $authorised_value->opac_description;
260                 }
261             }
262
263             if ($item->itype) {
264                 my $itemtype = Koha::ItemTypes->find($item->itype);
265                 if ($itemtype) {
266                     $item{itype_description} = $itemtype->description;
267                 }
268             }
269
270             my $transfer = $item->get_transfer;
271             if ($transfer) {
272                 $item{transfer} = {
273                     datesent => $transfer->datesent,
274                     frombranch => $transfer->frombranch,
275                     tobranch => $transfer->tobranch,
276                 };
277             }
278
279             push @{ $biblioitem->{items}->{item} }, \%item;
280         }
281
282         # Holds
283         my $holds = $biblio->current_holds->unblessed;
284         foreach my $hold (@$holds) {
285             delete $hold->{'borrowernumber'};
286         }
287
288         # Hashref building...
289         $biblioitem->{'reserves'}->{'reserve'} = $holds;
290         $biblioitem->{'issues'}->{'issue'}     = $checkouts;
291
292         push @records, $biblioitem;
293     }
294
295     return { record => \@records };
296 }
297
298 =head2 GetAuthorityRecords
299
300 Given a list of authority record identifiers, returns a list of record
301 objects that contain the authority records. The function user may request
302 a specific metadata schema for the record objects.
303
304 Parameters:
305
306   - id (Required)
307     list of authority record identifiers
308   - schema (Optional)
309     specifies the metadata schema of records to be returned, possible values:
310       - MARCXML
311
312 =cut
313
314 sub GetAuthorityRecords {
315     my ($cgi) = @_;
316
317     # If the user asks for an unsupported schema, return an error code
318     if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) {
319         return { code => 'UnsupportedSchema' };
320     }
321
322     my @records;
323
324     # Let's loop over the authority IDs
325     foreach my $authid ( split( / /, $cgi->param('id') ) ) {
326
327         # Get the record as XML string, or error code
328         push @records, GetAuthorityXML($authid) || { code => 'RecordNotFound' };
329     }
330
331     return { record => \@records };
332 }
333
334 =head2 LookupPatron
335
336 Looks up a patron in the ILS by an identifier, and returns the borrowernumber.
337
338 Parameters:
339
340   - id (Required)
341     an identifier used to look up the patron in Koha
342   - id_type (Optional)
343     the type of the identifier, possible values:
344     - cardnumber
345     - userid
346         - email
347     - borrowernumber
348     - firstname
349         - surname
350
351 =cut
352
353 sub LookupPatron {
354     my ($cgi) = @_;
355
356     my $id      = $cgi->param('id');
357     if(!$id) {
358         return { message => 'PatronNotFound' };
359     }
360
361     my $patrons;
362     my $passed_id_type = $cgi->param('id_type');
363     if($passed_id_type) {
364         $patrons = Koha::Patrons->search( { $passed_id_type => $id } );
365     } else {
366         foreach my $id_type ('cardnumber', 'userid', 'email', 'borrowernumber',
367                      'surname', 'firstname') {
368             $patrons = Koha::Patrons->search( { $id_type => $id } );
369             last if($patrons->count);
370         }
371     }
372     unless ( $patrons->count ) {
373         return { message => 'PatronNotFound' };
374     }
375
376     return { id => $patrons->next->borrowernumber };
377 }
378
379 =head2 AuthenticatePatron
380
381 Authenticates a user's login credentials and returns the identifier for
382 the patron.
383
384 Parameters:
385
386   - username (Required)
387     user's login identifier (userid or cardnumber)
388   - password (Required)
389     user's password
390
391 =cut
392
393 sub AuthenticatePatron {
394     my ($cgi) = @_;
395     my $username = $cgi->param('username');
396     my $password = $cgi->param('password');
397     my ($status, $cardnumber, $userid) = C4::Auth::checkpw( C4::Context->dbh, $username, $password );
398     if ( $status ) {
399         # Get the borrower
400         my $patron = Koha::Patrons->find( { userid => $userid } );
401         return { id => $patron->borrowernumber };
402     }
403     else {
404         return { code => 'PatronNotFound' };
405     }
406 }
407
408 =head2 GetPatronInfo
409
410 Returns specified information about the patron, based on options in the
411 request. This function can optionally return patron's contact information,
412 fine information, hold request information, and loan information.
413
414 Parameters:
415
416   - patron_id (Required)
417     the borrowernumber
418   - show_contact (Optional, default 1)
419     whether or not to return patron's contact information in the response
420   - show_fines (Optional, default 0)
421     whether or not to return fine information in the response
422   - show_holds (Optional, default 0)
423     whether or not to return hold request information in the response
424   - show_loans (Optional, default 0)
425     whether or not to return loan information request information in the response
426   - show_attributes (Optional, default 0)
427     whether or not to return additional patron attributes, when enabled the attributes
428     are limited to those marked as opac visible only.
429
430 =cut
431
432 sub GetPatronInfo {
433     my ($cgi) = @_;
434
435     # Get Member details
436     my $borrowernumber = $cgi->param('patron_id');
437     my $patron = Koha::Patrons->find( $borrowernumber );
438     return { code => 'PatronNotFound' } unless $patron;
439
440     # Cleaning the borrower hashref
441     my $borrower = $patron->unblessed;
442     $borrower->{charges} = sprintf "%.02f", $patron->account->non_issues_charges; # FIXME Formatting should not be done here
443     my $library = Koha::Libraries->find( $borrower->{branchcode} );
444     $borrower->{'branchname'} = $library ? $library->branchname : '';
445     delete $borrower->{'userid'};
446     delete $borrower->{'password'};
447
448     # Contact fields management
449     if ( defined $cgi->param('show_contact') && $cgi->param('show_contact') eq "0" ) {
450
451         # Define contact fields
452         my @contactfields = (
453             'email',              'emailpro',           'fax',                 'mobile',          'phone',             'phonepro',
454             'streetnumber',       'zipcode',            'city',                'streettype',      'B_address',         'B_city',
455             'B_email',            'B_phone',            'B_zipcode',           'address',         'address2',          'altcontactaddress1',
456             'altcontactaddress2', 'altcontactaddress3', 'altcontactfirstname', 'altcontactphone', 'altcontactsurname', 'altcontactzipcode'
457         );
458
459         # and delete them
460         foreach my $field (@contactfields) {
461             delete $borrower->{$field};
462         }
463     }
464
465     # Fines management
466     if ( $cgi->param('show_fines') && $cgi->param('show_fines') eq "1" ) {
467         $borrower->{fines}{fine} = $patron->account->lines->unblessed;
468     }
469
470     # Reserves management
471     if ( $cgi->param('show_holds') && $cgi->param('show_holds') eq "1" ) {
472
473         # Get borrower's reserves
474         my $holds = $patron->holds;
475         while ( my $hold = $holds->next ) {
476
477             my ( $item, $biblio, $biblioitem ) = ( {}, {}, {} );
478             # Get additional informations
479             if ( $hold->itemnumber ) {    # item level holds
480                 $item       = Koha::Items->find( $hold->itemnumber );
481                 $biblio     = $item->biblio;
482                 $biblioitem = $biblio->biblioitem;
483
484                 # Remove unwanted fields
485                 $item = $item->unblessed;
486                 delete $item->{more_subfields_xml};
487                 $biblio     = $biblio->unblessed;
488                 $biblioitem = $biblioitem->unblessed;
489             }
490
491             # Add additional fields
492             my $unblessed_hold = $hold->unblessed;
493             $unblessed_hold->{item}       = { %$item, %$biblio, %$biblioitem };
494             my $library = Koha::Libraries->find( $hold->branchcode );
495             my $branchname = $library ? $library->branchname : '';
496             $unblessed_hold->{branchname} = $branchname;
497             $biblio = Koha::Biblios->find( $hold->biblionumber ); # Should be $hold->get_biblio
498             $unblessed_hold->{title} = $biblio ? $biblio->title : ''; # Just in case, but should not be needed
499
500             push @{ $borrower->{holds}{hold} }, $unblessed_hold;
501
502         }
503     }
504
505     # Issues management
506     if ( $cgi->param('show_loans') && $cgi->param('show_loans') eq "1" ) {
507         my $per_page = $cgi->param('loans_per_page');
508         my $page = $cgi->param('loans_page');
509
510         my $pending_checkouts = $patron->pending_checkouts;
511
512         if ($page || $per_page) {
513             $page ||= 1;
514             $per_page ||= 10;
515             $borrower->{total_loans} = $pending_checkouts->count();
516             $pending_checkouts = $pending_checkouts->search(undef, {
517                 rows => $per_page,
518                 page => $page,
519             });
520         }
521
522         my @checkouts;
523         while ( my $c = $pending_checkouts->next ) {
524             # FIXME We should only retrieve what is needed in the template
525             my $issue = $c->unblessed_all_relateds;
526             delete $issue->{'more_subfields_xml'};
527             push @checkouts, $issue
528         }
529         $borrower->{'loans'}->{'loan'} = \@checkouts;
530     }
531
532     my $show_attributes = $cgi->param('show_attributes');
533     if ( $show_attributes && $show_attributes eq "1" ) {
534         # FIXME Regression expected here, we do not retrieve the same field as previously
535         # Waiting for answer on bug 14257 comment 15
536         $borrower->{'attributes'} = [
537             map {
538                 $_->type->opac_display
539                   ? {
540                     %{ $_->unblessed },
541                     %{ $_->type->unblessed },
542                     value             => $_->attribute,   # Backward compatibility
543                     value_description => $_->description, # Awkward retro-compability...
544                   }
545                   : ()
546             } $patron->extended_attributes->search
547         ];
548     }
549
550     # Add is expired information
551     $borrower->{'is_expired'} = $patron->is_expired ? 1 : 0;
552
553     return $borrower;
554 }
555
556 =head2 GetPatronStatus
557
558 Returns a patron's status information.
559
560 Parameters:
561
562   - patron_id (Required)
563     the borrower ID
564
565 =cut
566
567 sub GetPatronStatus {
568     my ($cgi) = @_;
569
570     # Get Member details
571     my $borrowernumber = $cgi->param('patron_id');
572     my $patron = Koha::Patrons->find( $borrowernumber );
573     return { code => 'PatronNotFound' } unless $patron;
574
575     # Return the results
576     return {
577         type   => $patron->categorycode,
578         status => 0, # TODO
579         expiry => $patron->dateexpiry,
580     };
581 }
582
583 =head2 GetServices
584
585 Returns information about the services available on a particular item for
586 a particular patron.
587
588 Parameters:
589
590   - patron_id (Required)
591     a borrowernumber
592   - item_id (Required)
593     an itemnumber
594
595 =cut
596
597 sub GetServices {
598     my ($cgi) = @_;
599
600     # Get the member, or return an error code if not found
601     my $borrowernumber = $cgi->param('patron_id');
602     my $patron = Koha::Patrons->find( $borrowernumber );
603     return { code => 'PatronNotFound' } unless $patron;
604
605     my $borrower = $patron->unblessed;
606     # Get the item, or return an error code if not found
607     my $itemnumber = $cgi->param('item_id');
608     my $item = Koha::Items->find($itemnumber);
609     return { code => 'RecordNotFound' } unless $item;
610
611     my @availablefor;
612
613     # Reserve level management
614     my $biblionumber = $item->biblionumber;
615     my $canbookbereserved = CanBookBeReserved( $borrower, $biblionumber );
616     if ($canbookbereserved->{status} eq 'OK') {
617         push @availablefor, 'title level hold';
618         my $canitembereserved = IsAvailableForItemLevelRequest($item, $patron);
619         if ($canitembereserved) {
620             push @availablefor, 'item level hold';
621         }
622     }
623
624     # Reserve cancellation management
625     my $holds = $patron->holds;
626     my @reserveditems;
627     while ( my $hold = $holds->next ) { # FIXME This could be improved
628         push @reserveditems, $hold->itemnumber;
629     }
630     if ( grep { $itemnumber eq $_ } @reserveditems ) {
631         push @availablefor, 'hold cancellation';
632     }
633
634     # Renewal management
635     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
636     if ( $renewal[0] ) {
637         push @availablefor, 'loan renewal';
638     }
639
640     # Issuing management
641     my $barcode = $item->barcode || '';
642     $barcode = barcodedecode($barcode) if ( $barcode && C4::Context->preference('itemBarcodeInputFilter') );
643     if ($barcode) {
644         my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $barcode );
645
646         # TODO push @availablefor, 'loan';
647     }
648
649     my $out;
650     $out->{'AvailableFor'} = \@availablefor;
651
652     return $out;
653 }
654
655 =head2 RenewLoan
656
657 Extends the due date for a borrower's existing issue.
658
659 Parameters:
660
661   - patron_id (Required)
662     a borrowernumber
663   - item_id (Required)
664     an itemnumber
665   - desired_due_date (Required)
666     the date the patron would like the item returned by
667
668 =cut
669
670 sub RenewLoan {
671     my ($cgi) = @_;
672
673     # Get borrower infos or return an error code
674     my $borrowernumber = $cgi->param('patron_id');
675     my $patron = Koha::Patrons->find( $borrowernumber );
676     return { code => 'PatronNotFound' } unless $patron;
677
678     # Get the item, or return an error code
679     my $itemnumber = $cgi->param('item_id');
680     my $item = Koha::Items->find($itemnumber);
681     return { code => 'RecordNotFound' } unless $item;
682
683     # Add renewal if possible
684     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
685     if ( $renewal[0] ) { AddRenewal( $borrowernumber, $itemnumber, undef, undef, undef, undef, 0 ); }
686
687     my $issue = $item->checkout;
688     return unless $issue; # FIXME should be handled
689
690     # Hashref building
691     my $out;
692     $out->{'renewals'} = $issue->renewals;
693     $out->{date_due}   = dt_from_string($issue->date_due)->strftime('%Y-%m-%d %H:%M');
694     $out->{'success'}  = $renewal[0];
695     $out->{'error'}    = $renewal[1];
696
697     return $out;
698 }
699
700 =head2 HoldTitle
701
702 Creates, for a borrower, a biblio-level hold reserve.
703
704 Parameters:
705
706   - patron_id (Required)
707     a borrowernumber
708   - bib_id (Required)
709     a biblionumber
710   - request_location (Required)
711     IP address where the end user request is being placed
712   - pickup_location (Optional)
713     a branch code indicating the location to which to deliver the item for pickup
714   - start_date (Optional)
715     date after which hold request is no longer needed if the document has not been made available
716   - expiry_date (Optional)
717     date after which item returned to shelf if item is not picked up
718
719 =cut
720
721 sub HoldTitle {
722     my ($cgi) = @_;
723
724     # Get the borrower or return an error code
725     my $borrowernumber = $cgi->param('patron_id');
726     my $patron = Koha::Patrons->find( $borrowernumber );
727     return { code => 'PatronNotFound' } unless $patron;
728
729
730     # If borrower is restricted return an error code
731     return { code => 'PatronRestricted' } if $patron->is_debarred;
732
733     # Check for patron expired, category and syspref settings
734     return { code => 'PatronExpired' } if ($patron->category->effective_BlockExpiredPatronOpacActions && $patron->is_expired);
735
736     # Get the biblio record, or return an error code
737     my $biblionumber = $cgi->param('bib_id');
738     my $biblio = Koha::Biblios->find( $biblionumber );
739     return { code => 'RecordNotFound' } unless $biblio;
740
741     my @hostitems = get_hostitemnumbers_of($biblionumber);
742     my @itemnumbers;
743     if (@hostitems){
744         push(@itemnumbers, @hostitems);
745     }
746
747     my $items = Koha::Items->search({ -or => { biblionumber => $biblionumber, itemnumber => { in => \@itemnumbers } } });
748
749     unless ( $items->count ) {
750         return { code => 'NoItems' };
751     }
752
753     my $title = $biblio ? $biblio->title : '';
754
755     # Check if the biblio can be reserved
756     my $code = CanBookBeReserved( $borrowernumber, $biblionumber )->{status};
757     return { code => $code } unless ( $code eq 'OK' );
758
759     my $branch;
760
761     # Pickup branch management
762     if ( $cgi->param('pickup_location') ) {
763         $branch = $cgi->param('pickup_location');
764         return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch);
765     } else { # if the request provide no branch, use the borrower's branch
766         $branch = $patron->branchcode;
767     }
768
769     my $destination = Koha::Libraries->find($branch);
770     return { code => 'libraryNotPickupLocation' } unless $destination->pickup_location;
771     return { code => 'cannotBeTransferred' } unless $biblio->can_be_transferred({ to => $destination });
772
773     my $resdate;
774     if ( $cgi->param('start_date') ) {
775         $resdate = $cgi->param('start_date');
776     }
777
778     my $expdate;
779     if ( $cgi->param('expiry_date') ) {
780         $expdate = $cgi->param('expiry_date');
781     }
782
783     # Add the reserve
784     #    $branch,    $borrowernumber, $biblionumber,
785     #    $constraint, $bibitems,  $priority, $resdate, $expdate, $notes,
786     #    $title,      $checkitem, $found
787     my $priority= C4::Reserves::CalculatePriority( $biblionumber );
788     AddReserve(
789         {
790             branchcode       => $branch,
791             borrowernumber   => $borrowernumber,
792             biblionumber     => $biblionumber,
793             priority         => $priority,
794             reservation_date => $resdate,
795             expiration_date  => $expdate,
796             title            => $title,
797         }
798     );
799
800     # Hashref building
801     my $out;
802     $out->{'title'}           = $title;
803     my $library = Koha::Libraries->find( $branch );
804     $out->{'pickup_location'} = $library ? $library->branchname : '';
805
806     # TODO $out->{'date_available'}  = '';
807
808     return $out;
809 }
810
811 =head2 HoldItem
812
813 Creates, for a borrower, an item-level hold request on a specific item of
814 a bibliographic record in Koha.
815
816 Parameters:
817
818   - patron_id (Required)
819     a borrowernumber
820   - bib_id (Required)
821     a biblionumber
822   - item_id (Required)
823     an itemnumber
824   - pickup_location (Optional)
825     a branch code indicating the location to which to deliver the item for pickup
826   - start_date (Optional)
827     date after which hold request is no longer needed if the item has not been made available
828   - expiry_date (Optional)
829     date after which item returned to shelf if item is not picked up
830
831 =cut
832
833 sub HoldItem {
834     my ($cgi) = @_;
835
836     # Get the borrower or return an error code
837     my $borrowernumber = $cgi->param('patron_id');
838     my $patron = Koha::Patrons->find( $borrowernumber );
839     return { code => 'PatronNotFound' } unless $patron;
840
841     # If borrower is restricted return an error code
842     return { code => 'PatronRestricted' } if $patron->is_debarred;
843
844     # Check for patron expired, category and syspref settings
845     return { code => 'PatronExpired' } if ($patron->category->effective_BlockExpiredPatronOpacActions && $patron->is_expired);
846
847     # Get the biblio or return an error code
848     my $biblionumber = $cgi->param('bib_id');
849     my $biblio = Koha::Biblios->find( $biblionumber );
850     return { code => 'RecordNotFound' } unless $biblio;
851
852     my $title = $biblio ? $biblio->title : '';
853
854     # Get the item or return an error code
855     my $itemnumber = $cgi->param('item_id');
856     my $item = Koha::Items->find($itemnumber);
857     return { code => 'RecordNotFound' } unless $item;
858
859     # If the biblio does not match the item, return an error code
860     return { code => 'RecordNotFound' } if $item->biblionumber ne $biblio->biblionumber;
861
862     # Pickup branch management
863     my $branch;
864     if ( $cgi->param('pickup_location') ) {
865         $branch = $cgi->param('pickup_location');
866         return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch);
867     } else { # if the request provide no branch, use the borrower's branch
868         $branch = $patron->branchcode;
869     }
870
871     # Check for item disponibility
872     my $canitembereserved = C4::Reserves::CanItemBeReserved( $borrowernumber, $itemnumber, $branch )->{status};
873     return { code => $canitembereserved } unless $canitembereserved eq 'OK';
874
875     my $resdate;
876     if ( $cgi->param('start_date') ) {
877         $resdate = $cgi->param('start_date');
878     }
879
880     my $expdate;
881     if ( $cgi->param('expiry_date') ) {
882         $expdate = $cgi->param('expiry_date');
883     }
884
885     # Add the reserve
886     my $priority = C4::Reserves::CalculatePriority($biblionumber);
887     AddReserve(
888         {
889             branchcode       => $branch,
890             borrowernumber   => $borrowernumber,
891             biblionumber     => $biblionumber,
892             priority         => $priority,
893             reservation_date => $resdate,
894             expiration_date  => $expdate,
895             title            => $title,
896             itemnumber       => $itemnumber,
897         }
898     );
899
900     # Hashref building
901     my $out;
902     my $library = Koha::Libraries->find( $branch );
903     $out->{'pickup_location'} = $library ? $library->branchname : '';
904
905     # TODO $out->{'date_available'} = '';
906
907     return $out;
908 }
909
910 =head2 CancelHold
911
912 Cancels an active reserve request for the borrower.
913
914 Parameters:
915
916   - patron_id (Required)
917         a borrowernumber
918   - item_id (Required)
919         a reserve_id
920
921 =cut
922
923 sub CancelHold {
924     my ($cgi) = @_;
925
926     # Get the borrower or return an error code
927     my $borrowernumber = $cgi->param('patron_id');
928     my $patron = Koha::Patrons->find( $borrowernumber );
929     return { code => 'PatronNotFound' } unless $patron;
930
931     # Get the reserve or return an error code
932     my $reserve_id = $cgi->param('item_id');
933     my $hold = Koha::Holds->find( $reserve_id );
934     return { code => 'RecordNotFound' } unless $hold;
935     return { code => 'RecordNotFound' } unless ($hold->borrowernumber == $borrowernumber);
936
937     $hold->cancel;
938
939     return { code => 'Canceled' };
940 }
941
942 =head2 _availability
943
944 Returns, for an itemnumber, an array containing availability information.
945
946  my ($biblionumber, $status, $msg, $location) = _availability($id);
947
948 =cut
949
950 sub _availability {
951     my ($itemnumber) = @_;
952     my $item = Koha::Items->find($itemnumber);
953
954     unless ( $item ) {
955         return ( undef, 'unknown', 'Error: could not retrieve availability for this ID', undef );
956     }
957
958     my $biblionumber = $item->biblioitemnumber;
959     my $library = Koha::Libraries->find( $item->holdingbranch );
960     my $location = $library ? $library->branchname : '';
961
962     if ( $item->notforloan ) {
963         return ( $biblionumber, 'not available', 'Not for loan', $location );
964     } elsif ( $item->onloan ) {
965         return ( $biblionumber, 'not available', 'Checked out', $location );
966     } elsif ( $item->itemlost ) {
967         return ( $biblionumber, 'not available', 'Item lost', $location );
968     } elsif ( $item->withdrawn ) {
969         return ( $biblionumber, 'not available', 'Item withdrawn', $location );
970     } elsif ( $item->damaged ) {
971         return ( $biblionumber, 'not available', 'Item damaged', $location );
972     } else {
973         return ( $biblionumber, 'available', undef, $location );
974     }
975 }
976
977 1;