Bug 27342: Fix C4::ILSDI::Services::AuthenticatePatron
[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 qw( get_hostitemnumbers_of );
25 use C4::Circulation qw( CanBookBeRenewed barcodedecode CanBookBeIssued AddRenewal );
26 use C4::Accounts;
27 use C4::Reserves qw( CanBookBeReserved IsAvailableForItemLevelRequest CalculatePriority AddReserve CanItemBeReserved CanReserveBeCanceledFromOpac );
28 use C4::Context;
29 use C4::Auth;
30 use CGI qw ( -utf8 );
31 use DateTime;
32 use C4::Auth;
33 use Koha::DateUtils qw( dt_from_string );
34 use C4::AuthoritiesMarc qw( GetAuthorityXML );
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, $itemcallnumber ) = _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             if ($itemcallnumber) { $out .= "          <dlf:itemcallnumber>" . $itemcallnumber. "</dlf:itemcallnumber>\n"; }
133             $out .= "        </dlf:simpleavailability>\n";
134             $out .= "      </dlf:item>\n";
135             $out .= "    </dlf:items>\n";
136             $out .= "  </dlf:record>\n";
137         } else {
138             my $status;
139             my $msg;
140             my $items = Koha::Items->search({ biblionumber => $id });
141             if ($items->count) {
142                 # Open XML
143                 $out .= "  <dlf:record>\n";
144                 $out .= "    <dlf:bibliographic id=\"" .$id. "\" />\n";
145                 $out .= "    <dlf:items>\n";
146                 # We loop over the items to clean them
147                 while ( my $item = $items->next ) {
148                     my $itemnumber = $item->itemnumber;
149                     my ( $biblionumber, $status, $msg, $location, $itemcallnumber ) = _availability($itemnumber);
150                     $out .= "      <dlf:item id=\"" . $itemnumber . "\">\n";
151                     $out .= "        <dlf:simpleavailability>\n";
152                     $out .= "          <dlf:identifier>" . $itemnumber . "</dlf:identifier>\n";
153                     $out .= "          <dlf:availabilitystatus>" . $status . "</dlf:availabilitystatus>\n";
154                     if ($msg)      { $out .= "          <dlf:availabilitymsg>" . $msg . "</dlf:availabilitymsg>\n"; }
155                     if ($location) { $out .= "          <dlf:location>" . $location . "</dlf:location>\n"; }
156                     if ($itemcallnumber) { $out .= "          <dlf:itemcallnumber>" . $itemcallnumber. "</dlf:itemcallnumber>\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 $record = $biblio->metadata->record({ embed_items => 1 });
221         if ($record) {
222             $biblioitem->{marcxml} = $record->as_xml_record();
223         }
224
225         # Get most of the needed data
226         my $biblioitemnumber = $biblioitem->{'biblioitemnumber'};
227         my $checkouts = Koha::Checkouts->search(
228             { biblionumber => $biblionumber },
229             {
230                 join => 'item',
231                 '+select' => ['item.barcode'],
232                 '+as'     => ['barcode'],
233             }
234         )->unblessed;
235         foreach my $checkout (@$checkouts) {
236             delete $checkout->{'borrowernumber'};
237         }
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( $username, $password );
397     if ( $status == 1 ) {
398         # Track the login
399         C4::Auth::track_login_daily( $userid );
400         # Get the borrower
401         my $patron = Koha::Patrons->find( { userid => $userid } );
402         return { id => $patron->borrowernumber };
403     }
404     elsif ( $status == -2 ){
405         return { code => 'PasswordExpired' };
406     }
407     else {
408         return { code => 'PatronNotFound' };
409     }
410 }
411
412 =head2 GetPatronInfo
413
414 Returns specified information about the patron, based on options in the
415 request. This function can optionally return patron's contact information,
416 fine information, hold request information, and loan information.
417
418 Parameters:
419
420   - patron_id (Required)
421     the borrowernumber
422   - show_contact (Optional, default 1)
423     whether or not to return patron's contact information in the response
424   - show_fines (Optional, default 0)
425     whether or not to return fine information in the response
426   - show_holds (Optional, default 0)
427     whether or not to return hold request information in the response
428   - show_loans (Optional, default 0)
429     whether or not to return loan information request information in the response
430   - show_attributes (Optional, default 0)
431     whether or not to return additional patron attributes, when enabled the attributes
432     are limited to those marked as opac visible only.
433
434 =cut
435
436 sub GetPatronInfo {
437     my ($cgi) = @_;
438
439     # Get Member details
440     my $borrowernumber = $cgi->param('patron_id');
441     my $patron = Koha::Patrons->find( $borrowernumber );
442     return { code => 'PatronNotFound' } unless $patron;
443
444     # Cleaning the borrower hashref
445     my $borrower = $patron->unblessed;
446     $borrower->{charges} = sprintf "%.02f", $patron->account->non_issues_charges; # FIXME Formatting should not be done here
447     my $library = Koha::Libraries->find( $borrower->{branchcode} );
448     $borrower->{'branchname'} = $library ? $library->branchname : '';
449     delete $borrower->{'userid'};
450     delete $borrower->{'password'};
451
452     # Contact fields management
453     if ( defined $cgi->param('show_contact') && $cgi->param('show_contact') eq "0" ) {
454
455         # Define contact fields
456         my @contactfields = (
457             'email',              'emailpro',           'fax',                 'mobile',          'phone',             'phonepro',
458             'streetnumber',       'zipcode',            'city',                'streettype',      'B_address',         'B_city',
459             'B_email',            'B_phone',            'B_zipcode',           'address',         'address2',          'altcontactaddress1',
460             'altcontactaddress2', 'altcontactaddress3', 'altcontactfirstname', 'altcontactphone', 'altcontactsurname', 'altcontactzipcode'
461         );
462
463         # and delete them
464         foreach my $field (@contactfields) {
465             delete $borrower->{$field};
466         }
467     }
468
469     # Fines management
470     if ( $cgi->param('show_fines') && $cgi->param('show_fines') eq "1" ) {
471         $borrower->{fines}{fine} = $patron->account->lines->unblessed;
472     }
473
474     # Reserves management
475     if ( $cgi->param('show_holds') && $cgi->param('show_holds') eq "1" ) {
476
477         # Get borrower's reserves
478         my $holds = $patron->holds;
479         while ( my $hold = $holds->next ) {
480
481             my ( $item, $biblio, $biblioitem ) = ( {}, {}, {} );
482             # Get additional informations
483             if ( $hold->itemnumber ) {    # item level holds
484                 $item       = Koha::Items->find( $hold->itemnumber );
485                 $biblio     = $item->biblio;
486                 $biblioitem = $biblio->biblioitem;
487
488                 # Remove unwanted fields
489                 $item = $item->unblessed;
490                 delete $item->{more_subfields_xml};
491                 $biblio     = $biblio->unblessed;
492                 $biblioitem = $biblioitem->unblessed;
493             }
494
495             # Add additional fields
496             my $unblessed_hold = $hold->unblessed;
497             $unblessed_hold->{item}       = { %$item, %$biblio, %$biblioitem };
498             my $library = Koha::Libraries->find( $hold->branchcode );
499             my $branchname = $library ? $library->branchname : '';
500             $unblessed_hold->{branchname} = $branchname;
501             $biblio = Koha::Biblios->find( $hold->biblionumber ); # Should be $hold->get_biblio
502             $unblessed_hold->{title} = $biblio ? $biblio->title : ''; # Just in case, but should not be needed
503
504             push @{ $borrower->{holds}{hold} }, $unblessed_hold;
505
506         }
507     }
508
509     # Issues management
510     if ( $cgi->param('show_loans') && $cgi->param('show_loans') eq "1" ) {
511         my $per_page = $cgi->param('loans_per_page');
512         my $page = $cgi->param('loans_page');
513
514         my $pending_checkouts = $patron->pending_checkouts;
515
516         if ($page || $per_page) {
517             $page ||= 1;
518             $per_page ||= 10;
519             $borrower->{total_loans} = $pending_checkouts->count();
520             $pending_checkouts = $pending_checkouts->search(undef, {
521                 rows => $per_page,
522                 page => $page,
523             });
524         }
525
526         my @checkouts;
527         while ( my $c = $pending_checkouts->next ) {
528             # FIXME We should only retrieve what is needed in the template
529             my $issue = $c->unblessed_all_relateds;
530             delete $issue->{'more_subfields_xml'};
531             push @checkouts, $issue
532         }
533         $borrower->{'loans'}->{'loan'} = \@checkouts;
534     }
535
536     my $show_attributes = $cgi->param('show_attributes');
537     if ( $show_attributes && $show_attributes eq "1" ) {
538         # FIXME Regression expected here, we do not retrieve the same field as previously
539         # Waiting for answer on bug 14257 comment 15
540         $borrower->{'attributes'} = [
541             map {
542                 $_->type->opac_display
543                   ? {
544                     %{ $_->unblessed },
545                     %{ $_->type->unblessed },
546                     value             => $_->attribute,   # Backward compatibility
547                     value_description => $_->description, # Awkward retro-compability...
548                   }
549                   : ()
550             } $patron->extended_attributes->search->as_list
551         ];
552     }
553
554     # Add is expired information
555     $borrower->{'is_expired'} = $patron->is_expired ? 1 : 0;
556
557     return $borrower;
558 }
559
560 =head2 GetPatronStatus
561
562 Returns a patron's status information.
563
564 Parameters:
565
566   - patron_id (Required)
567     the borrower ID
568
569 =cut
570
571 sub GetPatronStatus {
572     my ($cgi) = @_;
573
574     # Get Member details
575     my $borrowernumber = $cgi->param('patron_id');
576     my $patron = Koha::Patrons->find( $borrowernumber );
577     return { code => 'PatronNotFound' } unless $patron;
578
579     # Return the results
580     return {
581         type   => $patron->categorycode,
582         status => 0, # TODO
583         expiry => $patron->dateexpiry,
584     };
585 }
586
587 =head2 GetServices
588
589 Returns information about the services available on a particular item for
590 a particular patron.
591
592 Parameters:
593
594   - patron_id (Required)
595     a borrowernumber
596   - item_id (Required)
597     an itemnumber
598
599 =cut
600
601 sub GetServices {
602     my ($cgi) = @_;
603
604     # Get the member, or return an error code if not found
605     my $borrowernumber = $cgi->param('patron_id');
606     my $patron = Koha::Patrons->find( $borrowernumber );
607     return { code => 'PatronNotFound' } unless $patron;
608
609     my $borrower = $patron->unblessed;
610     # Get the item, or return an error code if not found
611     my $itemnumber = $cgi->param('item_id');
612     my $item = Koha::Items->find($itemnumber);
613     return { code => 'RecordNotFound' } unless $item;
614
615     my @availablefor;
616
617     # Reserve level management
618     my $biblionumber = $item->biblionumber;
619     my $canbookbereserved = CanBookBeReserved( $borrower, $biblionumber );
620     if ($canbookbereserved->{status} eq 'OK') {
621         push @availablefor, 'title level hold';
622         my $canitembereserved = IsAvailableForItemLevelRequest($item, $patron);
623         if ($canitembereserved) {
624             push @availablefor, 'item level hold';
625         }
626     }
627
628     # Reserve cancellation management
629     my $holds = $patron->holds;
630     my @reserveditems;
631     while ( my $hold = $holds->next ) { # FIXME This could be improved
632         push @reserveditems, $hold->itemnumber;
633     }
634     if ( grep { $itemnumber eq $_ } @reserveditems ) {
635         push @availablefor, 'hold cancellation';
636     }
637
638     # Renewal management
639     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
640     if ( $renewal[0] ) {
641         push @availablefor, 'loan renewal';
642     }
643
644     # Issuing management
645     my $barcode = $item->barcode || '';
646     $barcode = barcodedecode($barcode) if $barcode;
647     if ($barcode) {
648         my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $barcode );
649
650         # TODO push @availablefor, 'loan';
651     }
652
653     my $out;
654     $out->{'AvailableFor'} = \@availablefor;
655
656     return $out;
657 }
658
659 =head2 RenewLoan
660
661 Extends the due date for a borrower's existing issue.
662
663 Parameters:
664
665   - patron_id (Required)
666     a borrowernumber
667   - item_id (Required)
668     an itemnumber
669   - desired_due_date (Required)
670     the date the patron would like the item returned by
671
672 =cut
673
674 sub RenewLoan {
675     my ($cgi) = @_;
676
677     # Get borrower infos or return an error code
678     my $borrowernumber = $cgi->param('patron_id');
679     my $patron = Koha::Patrons->find( $borrowernumber );
680     return { code => 'PatronNotFound' } unless $patron;
681
682     # Get the item, or return an error code
683     my $itemnumber = $cgi->param('item_id');
684     my $item = Koha::Items->find($itemnumber);
685     return { code => 'RecordNotFound' } unless $item;
686
687     # Add renewal if possible
688     my @renewal = CanBookBeRenewed( $borrowernumber, $itemnumber );
689     if ( $renewal[0] ) { AddRenewal( $borrowernumber, $itemnumber, undef, undef, undef, undef, 0 ); }
690
691     my $issue = $item->checkout;
692     return unless $issue; # FIXME should be handled
693
694     # Hashref building
695     my $out;
696     $out->{'renewals'} = $issue->renewals_count;
697     $out->{date_due}   = dt_from_string($issue->date_due)->strftime('%Y-%m-%d %H:%M');
698     $out->{'success'}  = $renewal[0];
699     $out->{'error'}    = $renewal[1];
700
701     return $out;
702 }
703
704 =head2 HoldTitle
705
706 Creates, for a borrower, a biblio-level hold reserve.
707
708 Parameters:
709
710   - patron_id (Required)
711     a borrowernumber
712   - bib_id (Required)
713     a biblionumber
714   - request_location (Required)
715     IP address where the end user request is being placed
716   - pickup_location (Optional)
717     a branch code indicating the location to which to deliver the item for pickup
718   - start_date (Optional)
719     date after which hold request is no longer needed if the document has not been made available
720   - expiry_date (Optional)
721     date after which item returned to shelf if item is not picked up
722
723 =cut
724
725 sub HoldTitle {
726     my ($cgi) = @_;
727
728     # Get the borrower or return an error code
729     my $borrowernumber = $cgi->param('patron_id');
730     my $patron = Koha::Patrons->find( $borrowernumber );
731     return { code => 'PatronNotFound' } unless $patron;
732
733
734     # If borrower is restricted return an error code
735     return { code => 'PatronRestricted' } if $patron->is_debarred;
736
737     # Check for patron expired, category and syspref settings
738     return { code => 'PatronExpired' } if ($patron->category->effective_BlockExpiredPatronOpacActions && $patron->is_expired);
739
740     # Get the biblio record, or return an error code
741     my $biblionumber = $cgi->param('bib_id');
742     my $biblio = Koha::Biblios->find( $biblionumber );
743     return { code => 'RecordNotFound' } unless $biblio;
744
745     my @hostitems = get_hostitemnumbers_of($biblionumber);
746     my @itemnumbers;
747     if (@hostitems){
748         push(@itemnumbers, @hostitems);
749     }
750
751     my $items = Koha::Items->search({ -or => { biblionumber => $biblionumber, itemnumber => { in => \@itemnumbers } } });
752
753     unless ( $items->count ) {
754         return { code => 'NoItems' };
755     }
756
757     my $title = $biblio ? $biblio->title : '';
758
759     # Check if the biblio can be reserved
760     my $code = CanBookBeReserved( $borrowernumber, $biblionumber )->{status};
761     return { code => $code } unless ( $code eq 'OK' );
762
763     my $branch;
764
765     # Pickup branch management
766     if ( $cgi->param('pickup_location') ) {
767         $branch = $cgi->param('pickup_location');
768         return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch);
769     } else { # if the request provide no branch, use the borrower's branch
770         $branch = $patron->branchcode;
771     }
772
773     my $destination = Koha::Libraries->find($branch);
774     return { code => 'libraryNotPickupLocation' } unless $destination->pickup_location;
775     return { code => 'cannotBeTransferred' } unless $biblio->can_be_transferred({ to => $destination });
776
777     my $resdate;
778     if ( $cgi->param('start_date') ) {
779         $resdate = $cgi->param('start_date');
780     }
781
782     my $expdate;
783     if ( $cgi->param('expiry_date') ) {
784         $expdate = $cgi->param('expiry_date');
785     }
786
787     # Add the reserve
788     #    $branch,    $borrowernumber, $biblionumber,
789     #    $constraint, $bibitems,  $priority, $resdate, $expdate, $notes,
790     #    $title,      $checkitem, $found
791     my $priority= C4::Reserves::CalculatePriority( $biblionumber );
792     AddReserve(
793         {
794             branchcode       => $branch,
795             borrowernumber   => $borrowernumber,
796             biblionumber     => $biblionumber,
797             priority         => $priority,
798             reservation_date => $resdate,
799             expiration_date  => $expdate,
800             title            => $title,
801         }
802     );
803
804     # Hashref building
805     my $out;
806     $out->{'title'}           = $title;
807     my $library = Koha::Libraries->find( $branch );
808     $out->{'pickup_location'} = $library ? $library->branchname : '';
809
810     # TODO $out->{'date_available'}  = '';
811
812     return $out;
813 }
814
815 =head2 HoldItem
816
817 Creates, for a borrower, an item-level hold request on a specific item of
818 a bibliographic record in Koha.
819
820 Parameters:
821
822   - patron_id (Required)
823     a borrowernumber
824   - bib_id (Required)
825     a biblionumber
826   - item_id (Required)
827     an itemnumber
828   - pickup_location (Optional)
829     a branch code indicating the location to which to deliver the item for pickup
830   - start_date (Optional)
831     date after which hold request is no longer needed if the item has not been made available
832   - expiry_date (Optional)
833     date after which item returned to shelf if item is not picked up
834
835 =cut
836
837 sub HoldItem {
838     my ($cgi) = @_;
839
840     # Get the borrower or return an error code
841     my $borrowernumber = $cgi->param('patron_id');
842     my $patron = Koha::Patrons->find( $borrowernumber );
843     return { code => 'PatronNotFound' } unless $patron;
844
845     # If borrower is restricted return an error code
846     return { code => 'PatronRestricted' } if $patron->is_debarred;
847
848     # Check for patron expired, category and syspref settings
849     return { code => 'PatronExpired' } if ($patron->category->effective_BlockExpiredPatronOpacActions && $patron->is_expired);
850
851     # Get the biblio or return an error code
852     my $biblionumber = $cgi->param('bib_id');
853     my $biblio = Koha::Biblios->find( $biblionumber );
854     return { code => 'RecordNotFound' } unless $biblio;
855
856     my $title = $biblio ? $biblio->title : '';
857
858     # Get the item or return an error code
859     my $itemnumber = $cgi->param('item_id');
860     my $item = Koha::Items->find($itemnumber);
861     return { code => 'RecordNotFound' } unless $item;
862
863     # If the biblio does not match the item, return an error code
864     return { code => 'RecordNotFound' } if $item->biblionumber ne $biblio->biblionumber;
865
866     # Pickup branch management
867     my $branch;
868     if ( $cgi->param('pickup_location') ) {
869         $branch = $cgi->param('pickup_location');
870         return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch);
871     } else { # if the request provide no branch, use the borrower's branch
872         $branch = $patron->branchcode;
873     }
874
875     # Check for item disponibility
876     my $canitembereserved = C4::Reserves::CanItemBeReserved( $patron, $item, $branch )->{status};
877     return { code => $canitembereserved } unless $canitembereserved eq 'OK';
878
879     my $resdate;
880     if ( $cgi->param('start_date') ) {
881         $resdate = $cgi->param('start_date');
882     }
883
884     my $expdate;
885     if ( $cgi->param('expiry_date') ) {
886         $expdate = $cgi->param('expiry_date');
887     }
888
889     # Add the reserve
890     my $priority = C4::Reserves::CalculatePriority($biblionumber);
891     AddReserve(
892         {
893             branchcode       => $branch,
894             borrowernumber   => $borrowernumber,
895             biblionumber     => $biblionumber,
896             priority         => $priority,
897             reservation_date => $resdate,
898             expiration_date  => $expdate,
899             title            => $title,
900             itemnumber       => $itemnumber,
901         }
902     );
903
904     # Hashref building
905     my $out;
906     my $library = Koha::Libraries->find( $branch );
907     $out->{'pickup_location'} = $library ? $library->branchname : '';
908
909     # TODO $out->{'date_available'} = '';
910
911     return $out;
912 }
913
914 =head2 CancelHold
915
916 Cancels an active reserve request for the borrower.
917
918 Parameters:
919
920   - patron_id (Required)
921         a borrowernumber
922   - item_id (Required)
923         a reserve_id
924
925 =cut
926
927 sub CancelHold {
928     my ($cgi) = @_;
929
930     # Get the borrower or return an error code
931     my $borrowernumber = $cgi->param('patron_id');
932     my $patron = Koha::Patrons->find( $borrowernumber );
933     return { code => 'PatronNotFound' } unless $patron;
934
935     # Get the reserve or return an error code
936     my $reserve_id = $cgi->param('item_id');
937     my $hold = Koha::Holds->find( $reserve_id );
938     return { code => 'RecordNotFound' } unless $hold;
939
940     # Check if reserve belongs to the borrower and if it is in a state which allows cancellation
941     return { code => 'BorrowerCannotCancelHold' } unless CanReserveBeCanceledFromOpac( $reserve_id, $borrowernumber );
942
943     $hold->cancel;
944
945     return { code => 'Canceled' };
946 }
947
948 =head2 _availability
949
950 Returns, for an itemnumber, an array containing availability information.
951
952  my ($biblionumber, $status, $msg, $location) = _availability($id);
953
954 =cut
955
956 sub _availability {
957     my ($itemnumber) = @_;
958     my $item = Koha::Items->find($itemnumber);
959
960     use Koha::I18N;
961
962     unless ( $item ) {
963         return ( undef, __('unknown'), __('Error: could not retrieve availability for this ID'), undef );
964     }
965
966     my $biblionumber = $item->biblioitemnumber;
967     my $library = Koha::Libraries->find( $item->holdingbranch );
968     my $location = $library ? $library->branchname : '';
969     my $itemcallnumber = $item->itemcallnumber;
970
971     if ( $item->is_notforloan ) {
972         return ( $biblionumber, __('not available'), __('Not for loan'), $location, $itemcallnumber );
973     } elsif ( $item->onloan ) {
974         return ( $biblionumber, __('not available'), __('Checked out'), $location, $itemcallnumber );
975     } elsif ( $item->itemlost ) {
976         return ( $biblionumber, __('not available'), __('Item lost'), $location, $itemcallnumber );
977     } elsif ( $item->withdrawn ) {
978         return ( $biblionumber, __('not available'), __('Item withdrawn'), $location, $itemcallnumber );
979     } elsif ( $item->damaged ) {
980         return ( $biblionumber, __('not available'), __('Item damaged'), $location, $itemcallnumber );
981     } else {
982         return ( $biblionumber, __('available'), undef, $location, $itemcallnumber );
983     }
984 }
985
986 1;