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