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