30a03bceb2d2f99aafac13fec640387e0aaba4e8
[koha-ffzg.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 $holds  = $biblio->current_holds->unblessed;
231         my $checkouts = Koha::Checkouts->search(
232             { biblionumber => $biblionumber },
233             {
234                 join => 'item',
235                 '+select' => ['item.barcode'],
236                 '+as'     => ['barcode'],
237             }
238         )->unblessed;
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         # Hashref building...
283         $biblioitem->{'reserves'}->{'reserve'} = $holds;
284         $biblioitem->{'issues'}->{'issue'}     = $checkouts;
285
286         push @records, $biblioitem;
287     }
288
289     return { record => \@records };
290 }
291
292 =head2 GetAuthorityRecords
293
294 Given a list of authority record identifiers, returns a list of record
295 objects that contain the authority records. The function user may request
296 a specific metadata schema for the record objects.
297
298 Parameters:
299
300   - id (Required)
301     list of authority record identifiers
302   - schema (Optional)
303     specifies the metadata schema of records to be returned, possible values:
304       - MARCXML
305
306 =cut
307
308 sub GetAuthorityRecords {
309     my ($cgi) = @_;
310
311     # If the user asks for an unsupported schema, return an error code
312     if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) {
313         return { code => 'UnsupportedSchema' };
314     }
315
316     my @records;
317
318     # Let's loop over the authority IDs
319     foreach my $authid ( split( / /, $cgi->param('id') ) ) {
320
321         # Get the record as XML string, or error code
322         push @records, GetAuthorityXML($authid) || { code => 'RecordNotFound' };
323     }
324
325     return { record => \@records };
326 }
327
328 =head2 LookupPatron
329
330 Looks up a patron in the ILS by an identifier, and returns the borrowernumber.
331
332 Parameters:
333
334   - id (Required)
335     an identifier used to look up the patron in Koha
336   - id_type (Optional)
337     the type of the identifier, possible values:
338     - cardnumber
339     - userid
340         - email
341     - borrowernumber
342     - firstname
343         - surname
344
345 =cut
346
347 sub LookupPatron {
348     my ($cgi) = @_;
349
350     my $id      = $cgi->param('id');
351     if(!$id) {
352         return { message => 'PatronNotFound' };
353     }
354
355     my $patrons;
356     my $passed_id_type = $cgi->param('id_type');
357     if($passed_id_type) {
358         $patrons = Koha::Patrons->search( { $passed_id_type => $id } );
359     } else {
360         foreach my $id_type ('cardnumber', 'userid', 'email', 'borrowernumber',
361                      'surname', 'firstname') {
362             $patrons = Koha::Patrons->search( { $id_type => $id } );
363             last if($patrons->count);
364         }
365     }
366     unless ( $patrons->count ) {
367         return { message => 'PatronNotFound' };
368     }
369
370     return { id => $patrons->next->borrowernumber };
371 }
372
373 =head2 AuthenticatePatron
374
375 Authenticates a user's login credentials and returns the identifier for
376 the patron.
377
378 Parameters:
379
380   - username (Required)
381     user's login identifier (userid or cardnumber)
382   - password (Required)
383     user's password
384
385 =cut
386
387 sub AuthenticatePatron {
388     my ($cgi) = @_;
389     my $username = $cgi->param('username');
390     my $password = $cgi->param('password');
391     my ($status, $cardnumber, $userid) = C4::Auth::checkpw( C4::Context->dbh, $username, $password );
392     if ( $status ) {
393         # Get the borrower
394         my $patron = Koha::Patrons->find( { userid => $userid } );
395         return { id => $patron->borrowernumber };
396     }
397     else {
398         return { code => 'PatronNotFound' };
399     }
400 }
401
402 =head2 GetPatronInfo
403
404 Returns specified information about the patron, based on options in the
405 request. This function can optionally return patron's contact information,
406 fine information, hold request information, and loan information.
407
408 Parameters:
409
410   - patron_id (Required)
411     the borrowernumber
412   - show_contact (Optional, default 1)
413     whether or not to return patron's contact information in the response
414   - show_fines (Optional, default 0)
415     whether or not to return fine information in the response
416   - show_holds (Optional, default 0)
417     whether or not to return hold request information in the response
418   - show_loans (Optional, default 0)
419     whether or not to return loan information request information in the response
420   - show_attributes (Optional, default 0)
421     whether or not to return additional patron attributes, when enabled the attributes
422     are limited to those marked as opac visible only.
423
424 =cut
425
426 sub GetPatronInfo {
427     my ($cgi) = @_;
428
429     # Get Member details
430     my $borrowernumber = $cgi->param('patron_id');
431     my $patron = Koha::Patrons->find( $borrowernumber );
432     return { code => 'PatronNotFound' } unless $patron;
433
434     # Cleaning the borrower hashref
435     my $borrower = $patron->unblessed;
436     $borrower->{charges} = sprintf "%.02f", $patron->account->non_issues_charges; # FIXME Formatting should not be done here
437     my $library = Koha::Libraries->find( $borrower->{branchcode} );
438     $borrower->{'branchname'} = $library ? $library->branchname : '';
439     delete $borrower->{'userid'};
440     delete $borrower->{'password'};
441
442     # Contact fields management
443     if ( defined $cgi->param('show_contact') && $cgi->param('show_contact') eq "0" ) {
444
445         # Define contact fields
446         my @contactfields = (
447             'email',              'emailpro',           'fax',                 'mobile',          'phone',             'phonepro',
448             'streetnumber',       'zipcode',            'city',                'streettype',      'B_address',         'B_city',
449             'B_email',            'B_phone',            'B_zipcode',           'address',         'address2',          'altcontactaddress1',
450             'altcontactaddress2', 'altcontactaddress3', 'altcontactfirstname', 'altcontactphone', 'altcontactsurname', 'altcontactzipcode'
451         );
452
453         # and delete them
454         foreach my $field (@contactfields) {
455             delete $borrower->{$field};
456         }
457     }
458
459     # Fines management
460     if ( $cgi->param('show_fines') && $cgi->param('show_fines') eq "1" ) {
461         $borrower->{fines}{fine} = $patron->account->lines->unblessed;
462     }
463
464     # Reserves management
465     if ( $cgi->param('show_holds') && $cgi->param('show_holds') eq "1" ) {
466
467         # Get borrower's reserves
468         my $holds = $patron->holds;
469         while ( my $hold = $holds->next ) {
470
471             my ( $item, $biblio, $biblioitem ) = ( {}, {}, {} );
472             # Get additional informations
473             if ( $hold->itemnumber ) {    # item level holds
474                 $item       = Koha::Items->find( $hold->itemnumber );
475                 $biblio     = $item->biblio;
476                 $biblioitem = $biblio->biblioitem;
477
478                 # Remove unwanted fields
479                 $item = $item->unblessed;
480                 delete $item->{more_subfields_xml};
481                 $biblio     = $biblio->unblessed;
482                 $biblioitem = $biblioitem->unblessed;
483             }
484
485             # Add additional fields
486             my $unblessed_hold = $hold->unblessed;
487             $unblessed_hold->{item}       = { %$item, %$biblio, %$biblioitem };
488             my $library = Koha::Libraries->find( $hold->branchcode );
489             my $branchname = $library ? $library->branchname : '';
490             $unblessed_hold->{branchname} = $branchname;
491             $biblio = Koha::Biblios->find( $hold->biblionumber ); # Should be $hold->get_biblio
492             $unblessed_hold->{title} = $biblio ? $biblio->title : ''; # Just in case, but should not be needed
493
494             push @{ $borrower->{holds}{hold} }, $unblessed_hold;
495
496         }
497     }
498
499     # Issues management
500     if ( $cgi->param('show_loans') && $cgi->param('show_loans') eq "1" ) {
501         my $per_page = $cgi->param('loans_per_page');
502         my $page = $cgi->param('loans_page');
503
504         my $pending_checkouts = $patron->pending_checkouts;
505
506         if ($page || $per_page) {
507             $page ||= 1;
508             $per_page ||= 10;
509             $borrower->{total_loans} = $pending_checkouts->count();
510             $pending_checkouts = $pending_checkouts->search(undef, {
511                 rows => $per_page,
512                 page => $page,
513             });
514         }
515
516         my @checkouts;
517         while ( my $c = $pending_checkouts->next ) {
518             # FIXME We should only retrieve what is needed in the template
519             my $issue = $c->unblessed_all_relateds;
520             delete $issue->{'more_subfields_xml'};
521             push @checkouts, $issue
522         }
523         $borrower->{'loans'}->{'loan'} = \@checkouts;
524     }
525
526     my $show_attributes = $cgi->param('show_attributes');
527     if ( $show_attributes && $show_attributes eq "1" ) {
528         # FIXME Regression expected here, we do not retrieve the same field as previously
529         # Waiting for answer on bug 14257 comment 15
530         $borrower->{'attributes'} = [
531             map {
532                 $_->type->opac_display
533                   ? {
534                     %{ $_->unblessed },
535                     %{ $_->type->unblessed },
536                     value             => $_->attribute,   # Backward compatibility
537                     value_description => $_->description, # Awkward retro-compability...
538                   }
539                   : ()
540             } $patron->extended_attributes->search
541         ];
542     }
543
544     # Add is expired information
545     $borrower->{'is_expired'} = $patron->is_expired ? 1 : 0;
546
547     return $borrower;
548 }
549
550 =head2 GetPatronStatus
551
552 Returns a patron's status information.
553
554 Parameters:
555
556   - patron_id (Required)
557     the borrower ID
558
559 =cut
560
561 sub GetPatronStatus {
562     my ($cgi) = @_;
563
564     # Get Member details
565     my $borrowernumber = $cgi->param('patron_id');
566     my $patron = Koha::Patrons->find( $borrowernumber );
567     return { code => 'PatronNotFound' } unless $patron;
568
569     # Return the results
570     return {
571         type   => $patron->categorycode,
572         status => 0, # TODO
573         expiry => $patron->dateexpiry,
574     };
575 }
576
577 =head2 GetServices
578
579 Returns information about the services available on a particular item for
580 a particular patron.
581
582 Parameters:
583
584   - patron_id (Required)
585     a borrowernumber
586   - item_id (Required)
587     an itemnumber
588
589 =cut
590
591 sub GetServices {
592     my ($cgi) = @_;
593
594     # Get the member, or return an error code if not found
595     my $borrowernumber = $cgi->param('patron_id');
596     my $patron = Koha::Patrons->find( $borrowernumber );
597     return { code => 'PatronNotFound' } unless $patron;
598
599     my $borrower = $patron->unblessed;
600     # Get the item, or return an error code if not found
601     my $itemnumber = $cgi->param('item_id');
602     my $item = Koha::Items->find($itemnumber);
603     return { code => 'RecordNotFound' } unless $item;
604
605     my @availablefor;
606
607     # Reserve level management
608     my $biblionumber = $item->biblionumber;
609     my $canbookbereserved = CanBookBeReserved( $borrower, $biblionumber );
610     if ($canbookbereserved->{status} eq 'OK') {
611         push @availablefor, 'title level hold';
612         my $canitembereserved = IsAvailableForItemLevelRequest($item, $patron);
613         if ($canitembereserved) {
614             push @availablefor, 'item level hold';
615         }
616     }
617
618     # Reserve cancellation management
619     my $holds = $patron->holds;
620     my @reserveditems;
621     while ( my $hold = $holds->next ) { # FIXME This could be improved
622         push @reserveditems, $hold->itemnumber;
623     }
624     if ( grep { $itemnumber eq $_ } @reserveditems ) {
625         push @availablefor, 'hold cancellation';
626     }
627
628     # Renewal management
629     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
630     if ( $renewal[0] ) {
631         push @availablefor, 'loan renewal';
632     }
633
634     # Issuing management
635     my $barcode = $item->barcode || '';
636     $barcode = barcodedecode($barcode) if ( $barcode && C4::Context->preference('itemBarcodeInputFilter') );
637     if ($barcode) {
638         my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $barcode );
639
640         # TODO push @availablefor, 'loan';
641     }
642
643     my $out;
644     $out->{'AvailableFor'} = \@availablefor;
645
646     return $out;
647 }
648
649 =head2 RenewLoan
650
651 Extends the due date for a borrower's existing issue.
652
653 Parameters:
654
655   - patron_id (Required)
656     a borrowernumber
657   - item_id (Required)
658     an itemnumber
659   - desired_due_date (Required)
660     the date the patron would like the item returned by
661
662 =cut
663
664 sub RenewLoan {
665     my ($cgi) = @_;
666
667     # Get borrower infos or return an error code
668     my $borrowernumber = $cgi->param('patron_id');
669     my $patron = Koha::Patrons->find( $borrowernumber );
670     return { code => 'PatronNotFound' } unless $patron;
671
672     # Get the item, or return an error code
673     my $itemnumber = $cgi->param('item_id');
674     my $item = Koha::Items->find($itemnumber);
675     return { code => 'RecordNotFound' } unless $item;
676
677     # Add renewal if possible
678     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
679     if ( $renewal[0] ) { AddRenewal( $borrowernumber, $itemnumber, undef, undef, undef, undef, 0 ); }
680
681     my $issue = $item->checkout;
682     return unless $issue; # FIXME should be handled
683
684     # Hashref building
685     my $out;
686     $out->{'renewals'} = $issue->renewals;
687     $out->{date_due}   = dt_from_string($issue->date_due)->strftime('%Y-%m-%d %H:%M');
688     $out->{'success'}  = $renewal[0];
689     $out->{'error'}    = $renewal[1];
690
691     return $out;
692 }
693
694 =head2 HoldTitle
695
696 Creates, for a borrower, a biblio-level hold reserve.
697
698 Parameters:
699
700   - patron_id (Required)
701     a borrowernumber
702   - bib_id (Required)
703     a biblionumber
704   - request_location (Required)
705     IP address where the end user request is being placed
706   - pickup_location (Optional)
707     a branch code indicating the location to which to deliver the item for pickup
708   - start_date (Optional)
709     date after which hold request is no longer needed if the document has not been made available
710   - expiry_date (Optional)
711     date after which item returned to shelf if item is not picked up
712
713 =cut
714
715 sub HoldTitle {
716     my ($cgi) = @_;
717
718     # Get the borrower or return an error code
719     my $borrowernumber = $cgi->param('patron_id');
720     my $patron = Koha::Patrons->find( $borrowernumber );
721     return { code => 'PatronNotFound' } unless $patron;
722
723     # If borrower is restricted return an error code
724     return { code => 'PatronRestricted' } if $patron->is_debarred;
725
726     # Get the biblio record, or return an error code
727     my $biblionumber = $cgi->param('bib_id');
728     my $biblio = Koha::Biblios->find( $biblionumber );
729     return { code => 'RecordNotFound' } unless $biblio;
730
731     my @hostitems = get_hostitemnumbers_of($biblionumber);
732     my @itemnumbers;
733     if (@hostitems){
734         push(@itemnumbers, @hostitems);
735     }
736
737     my $items = Koha::Items->search({ -or => { biblionumber => $biblionumber, itemnumber => { in => \@itemnumbers } } });
738
739     unless ( $items->count ) {
740         return { code => 'NoItems' };
741     }
742
743     my $title = $biblio ? $biblio->title : '';
744
745     # Check if the biblio can be reserved
746     my $code = CanBookBeReserved( $borrowernumber, $biblionumber )->{status};
747     return { code => $code } unless ( $code eq 'OK' );
748
749     my $branch;
750
751     # Pickup branch management
752     if ( $cgi->param('pickup_location') ) {
753         $branch = $cgi->param('pickup_location');
754         return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch);
755     } else { # if the request provide no branch, use the borrower's branch
756         $branch = $patron->branchcode;
757     }
758
759     my $destination = Koha::Libraries->find($branch);
760     return { code => 'libraryNotPickupLocation' } unless $destination->pickup_location;
761     return { code => 'cannotBeTransferred' } unless $biblio->can_be_transferred({ to => $destination });
762
763     my $resdate;
764     if ( $cgi->param('start_date') ) {
765         $resdate = $cgi->param('start_date');
766     }
767
768     my $expdate;
769     if ( $cgi->param('expiry_date') ) {
770         $expdate = $cgi->param('expiry_date');
771     }
772
773     # Add the reserve
774     #    $branch,    $borrowernumber, $biblionumber,
775     #    $constraint, $bibitems,  $priority, $resdate, $expdate, $notes,
776     #    $title,      $checkitem, $found
777     my $priority= C4::Reserves::CalculatePriority( $biblionumber );
778     AddReserve(
779         {
780             branchcode       => $branch,
781             borrowernumber   => $borrowernumber,
782             biblionumber     => $biblionumber,
783             priority         => $priority,
784             reservation_date => $resdate,
785             expiration_date  => $expdate,
786             title            => $title,
787         }
788     );
789
790     # Hashref building
791     my $out;
792     $out->{'title'}           = $title;
793     my $library = Koha::Libraries->find( $branch );
794     $out->{'pickup_location'} = $library ? $library->branchname : '';
795
796     # TODO $out->{'date_available'}  = '';
797
798     return $out;
799 }
800
801 =head2 HoldItem
802
803 Creates, for a borrower, an item-level hold request on a specific item of
804 a bibliographic record in Koha.
805
806 Parameters:
807
808   - patron_id (Required)
809     a borrowernumber
810   - bib_id (Required)
811     a biblionumber
812   - item_id (Required)
813     an itemnumber
814   - pickup_location (Optional)
815     a branch code indicating the location to which to deliver the item for pickup
816   - start_date (Optional)
817     date after which hold request is no longer needed if the item has not been made available
818   - expiry_date (Optional)
819     date after which item returned to shelf if item is not picked up
820
821 =cut
822
823 sub HoldItem {
824     my ($cgi) = @_;
825
826     # Get the borrower or return an error code
827     my $borrowernumber = $cgi->param('patron_id');
828     my $patron = Koha::Patrons->find( $borrowernumber );
829     return { code => 'PatronNotFound' } unless $patron;
830
831     # If borrower is restricted return an error code
832     return { code => 'PatronRestricted' } if $patron->is_debarred;
833
834     # Get the biblio or return an error code
835     my $biblionumber = $cgi->param('bib_id');
836     my $biblio = Koha::Biblios->find( $biblionumber );
837     return { code => 'RecordNotFound' } unless $biblio;
838
839     my $title = $biblio ? $biblio->title : '';
840
841     # Get the item or return an error code
842     my $itemnumber = $cgi->param('item_id');
843     my $item = Koha::Items->find($itemnumber);
844     return { code => 'RecordNotFound' } unless $item;
845
846     # If the biblio does not match the item, return an error code
847     return { code => 'RecordNotFound' } if $item->biblionumber ne $biblio->biblionumber;
848
849     # Pickup branch management
850     my $branch;
851     if ( $cgi->param('pickup_location') ) {
852         $branch = $cgi->param('pickup_location');
853         return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch);
854     } else { # if the request provide no branch, use the borrower's branch
855         $branch = $patron->branchcode;
856     }
857
858     # Check for item disponibility
859     my $canitembereserved = C4::Reserves::CanItemBeReserved( $borrowernumber, $itemnumber, $branch )->{status};
860     return { code => $canitembereserved } unless $canitembereserved eq 'OK';
861
862     my $resdate;
863     if ( $cgi->param('start_date') ) {
864         $resdate = $cgi->param('start_date');
865     }
866
867     my $expdate;
868     if ( $cgi->param('expiry_date') ) {
869         $expdate = $cgi->param('expiry_date');
870     }
871
872     # Add the reserve
873     my $priority = C4::Reserves::CalculatePriority($biblionumber);
874     AddReserve(
875         {
876             branchcode       => $branch,
877             borrowernumber   => $borrowernumber,
878             biblionumber     => $biblionumber,
879             priority         => $priority,
880             reservation_date => $resdate,
881             expiration_date  => $expdate,
882             title            => $title,
883             itemnumber       => $itemnumber,
884         }
885     );
886
887     # Hashref building
888     my $out;
889     my $library = Koha::Libraries->find( $branch );
890     $out->{'pickup_location'} = $library ? $library->branchname : '';
891
892     # TODO $out->{'date_available'} = '';
893
894     return $out;
895 }
896
897 =head2 CancelHold
898
899 Cancels an active reserve request for the borrower.
900
901 Parameters:
902
903   - patron_id (Required)
904         a borrowernumber
905   - item_id (Required)
906         a reserve_id
907
908 =cut
909
910 sub CancelHold {
911     my ($cgi) = @_;
912
913     # Get the borrower or return an error code
914     my $borrowernumber = $cgi->param('patron_id');
915     my $patron = Koha::Patrons->find( $borrowernumber );
916     return { code => 'PatronNotFound' } unless $patron;
917
918     # Get the reserve or return an error code
919     my $reserve_id = $cgi->param('item_id');
920     my $hold = Koha::Holds->find( $reserve_id );
921     return { code => 'RecordNotFound' } unless $hold;
922     return { code => 'RecordNotFound' } unless ($hold->borrowernumber == $borrowernumber);
923
924     $hold->cancel;
925
926     return { code => 'Canceled' };
927 }
928
929 =head2 _availability
930
931 Returns, for an itemnumber, an array containing availability information.
932
933  my ($biblionumber, $status, $msg, $location) = _availability($id);
934
935 =cut
936
937 sub _availability {
938     my ($itemnumber) = @_;
939     my $item = Koha::Items->find($itemnumber);
940
941     unless ( $item ) {
942         return ( undef, 'unknown', 'Error: could not retrieve availability for this ID', undef );
943     }
944
945     my $biblionumber = $item->biblioitemnumber;
946     my $library = Koha::Libraries->find( $item->holdingbranch );
947     my $location = $library ? $library->branchname : '';
948
949     if ( $item->notforloan ) {
950         return ( $biblionumber, 'not available', 'Not for loan', $location );
951     } elsif ( $item->onloan ) {
952         return ( $biblionumber, 'not available', 'Checked out', $location );
953     } elsif ( $item->itemlost ) {
954         return ( $biblionumber, 'not available', 'Item lost', $location );
955     } elsif ( $item->withdrawn ) {
956         return ( $biblionumber, 'not available', 'Item withdrawn', $location );
957     } elsif ( $item->damaged ) {
958         return ( $biblionumber, 'not available', 'Item damaged', $location );
959     } else {
960         return ( $biblionumber, 'available', undef, $location );
961     }
962 }
963
964 1;