Bug 18743: Fix suggestion listing when organized by library
[koha.git] / C4 / Suggestions.pm
1 package C4::Suggestions;
2
3 # Copyright 2000-2002 Katipo Communications
4 # Parts Copyright Biblibre 2011
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use strict;
22
23 #use warnings; FIXME - Bug 2505
24 use CGI qw ( -utf8 );
25
26 use C4::Context;
27 use C4::Output;
28 use C4::Debug;
29 use C4::Letters;
30 use Koha::DateUtils;
31 use Koha::Suggestions;
32
33 use List::MoreUtils qw(any);
34 use base qw(Exporter);
35
36 our @EXPORT  = qw(
37   ConnectSuggestionAndBiblio
38   CountSuggestion
39   DelSuggestion
40   GetSuggestion
41   GetSuggestionByStatus
42   GetSuggestionFromBiblionumber
43   GetSuggestionInfoFromBiblionumber
44   GetSuggestionInfo
45   ModStatus
46   ModSuggestion
47   NewSuggestion
48   SearchSuggestion
49   DelSuggestionsOlderThan
50   GetUnprocessedSuggestions
51 );
52
53 =head1 NAME
54
55 C4::Suggestions - Some useful functions for dealings with aqorders.
56
57 =head1 SYNOPSIS
58
59 use C4::Suggestions;
60
61 =head1 DESCRIPTION
62
63 The functions in this module deal with the aqorders in OPAC and in librarian interface
64
65 A suggestion is done in the OPAC. It has the status "ASKED"
66
67 When a librarian manages the suggestion, they can set the status to "REJECTED" or "ACCEPTED".
68
69 When the book is ordered, the suggestion status becomes "ORDERED"
70
71 When a book is ordered and arrived in the library, the status becomes "AVAILABLE"
72
73 All aqorders of a borrower can be seen by the borrower itself.
74 Suggestions done by other borrowers can be seen when not "AVAILABLE"
75
76 =head1 FUNCTIONS
77
78 =head2 SearchSuggestion
79
80 (\@array) = &SearchSuggestion($suggestionhashref_to_search)
81
82 searches for a suggestion
83
84 return :
85 C<\@array> : the aqorders found. Array of hash.
86 Note the status is stored twice :
87 * in the status field
88 * as parameter ( for example ASKED => 1, or REJECTED => 1) . This is for template & translation purposes.
89
90 =cut
91
92 sub SearchSuggestion {
93     my ($suggestion) = @_;
94     my $dbh = C4::Context->dbh;
95     my @sql_params;
96     my @query = (
97         q{
98         SELECT suggestions.*,
99             U1.branchcode       AS branchcodesuggestedby,
100             B1.branchname       AS branchnamesuggestedby,
101             U1.surname          AS surnamesuggestedby,
102             U1.firstname        AS firstnamesuggestedby,
103             U1.cardnumber       AS cardnumbersuggestedby,
104             U1.email            AS emailsuggestedby,
105             U1.borrowernumber   AS borrnumsuggestedby,
106             U1.categorycode     AS categorycodesuggestedby,
107             C1.description      AS categorydescriptionsuggestedby,
108             U2.surname          AS surnamemanagedby,
109             U2.firstname        AS firstnamemanagedby,
110             B2.branchname       AS branchnamesuggestedby,
111             U2.email            AS emailmanagedby,
112             U2.branchcode       AS branchcodemanagedby,
113             U2.borrowernumber   AS borrnummanagedby,
114             BU.budget_name      AS budget_name
115         FROM suggestions
116             LEFT JOIN borrowers     AS U1 ON suggestedby=U1.borrowernumber
117             LEFT JOIN branches      AS B1 ON B1.branchcode=U1.branchcode
118             LEFT JOIN categories    AS C1 ON C1.categorycode=U1.categorycode
119             LEFT JOIN borrowers     AS U2 ON managedby=U2.borrowernumber
120             LEFT JOIN branches      AS B2 ON B2.branchcode=U2.branchcode
121             LEFT JOIN categories    AS C2 ON C2.categorycode=U2.categorycode
122             LEFT JOIN aqbudgets     AS BU ON budgetid=BU.budget_id
123         WHERE 1=1
124     }
125     );
126
127     # filter on biblio informations
128     foreach my $field (
129         qw( title author isbn publishercode copyrightdate collectiontitle ))
130     {
131         if ( $suggestion->{$field} ) {
132             push @sql_params, '%' . $suggestion->{$field} . '%';
133             push @query,      qq{ AND suggestions.$field LIKE ? };
134         }
135     }
136
137     # filter on user branch
138     if (   C4::Context->preference('IndependentBranches')
139         && !C4::Context->IsSuperLibrarian() )
140     {
141         # If IndependentBranches is set and the logged in user is not superlibrarian
142         # Then we want to filter by the user's library (i.e. cannot see suggestions from other libraries)
143         my $userenv = C4::Context->userenv;
144         if ($userenv) {
145             {
146                 push @sql_params, $$userenv{branch};
147                 push @query,      q{
148                     AND (suggestions.branchcode=? OR suggestions.branchcode='')
149                 };
150             }
151         }
152     }
153     elsif (defined $suggestion->{branchcode}
154         && $suggestion->{branchcode}
155         && $suggestion->{branchcode} ne '__ANY__' )
156     {
157         # If IndependentBranches is not set OR the logged in user is not superlibrarian
158         # AND the branchcode filter is passed and not '__ANY__'
159         # Then we want to filter using this parameter
160         push @sql_params, $suggestion->{branchcode};
161         push @query,      qq{ AND suggestions.branchcode=? };
162     }
163
164     # filter on nillable fields
165     foreach my $field (
166         qw( STATUS itemtype suggestedby managedby acceptedby budgetid biblionumber )
167       )
168     {
169         if ( exists $suggestion->{$field}
170                 and defined $suggestion->{$field}
171                 and $suggestion->{$field} ne '__ANY__'
172                 and (
173                     $suggestion->{$field} ne q||
174                         or $field eq 'STATUS'
175                 )
176         ) {
177             if ( $suggestion->{$field} eq '__NONE__' ) {
178                 push @query, qq{ AND (suggestions.$field = '' OR suggestions.$field IS NULL) };
179             }
180             else {
181                 push @sql_params, $suggestion->{$field};
182                 push @query, qq{ AND suggestions.$field = ? };
183             }
184         }
185     }
186
187     # filter on date fields
188     foreach my $field (qw( suggesteddate manageddate accepteddate )) {
189         my $from = $field . "_from";
190         my $to   = $field . "_to";
191         my $from_dt;
192         $from_dt = eval { dt_from_string( $suggestion->{$from} ) } if ( $suggestion->{$from} );
193         my $from_sql = '0000-00-00';
194         $from_sql = output_pref({ dt => $from_dt, dateformat => 'iso', dateonly => 1 })
195             if ($from_dt);
196         $debug && warn "SQL for start date ($field): $from_sql";
197         if ( $suggestion->{$from} || $suggestion->{$to} ) {
198             push @query, qq{ AND suggestions.$field BETWEEN ? AND ? };
199             push @sql_params, $from_sql;
200             push @sql_params,
201               output_pref({ dt => dt_from_string( $suggestion->{$to} ), dateformat => 'iso', dateonly => 1 }) || output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
202         }
203     }
204
205     $debug && warn "@query";
206     my $sth = $dbh->prepare("@query");
207     $sth->execute(@sql_params);
208     my @results;
209
210     # add status as field
211     while ( my $data = $sth->fetchrow_hashref ) {
212         $data->{ $data->{STATUS} } = 1;
213         push( @results, $data );
214     }
215
216     return ( \@results );
217 }
218
219 =head2 GetSuggestion
220
221 \%sth = &GetSuggestion($suggestionid)
222
223 this function get the detail of the suggestion $suggestionid (input arg)
224
225 return :
226     the result of the SQL query as a hash : $sth->fetchrow_hashref.
227
228 =cut
229
230 sub GetSuggestion {
231     my ($suggestionid) = @_;
232     my $dbh           = C4::Context->dbh;
233     my $query         = q{
234         SELECT *
235         FROM   suggestions
236         WHERE  suggestionid=?
237     };
238     my $sth = $dbh->prepare($query);
239     $sth->execute($suggestionid);
240     return ( $sth->fetchrow_hashref );
241 }
242
243 =head2 GetSuggestionFromBiblionumber
244
245 $ordernumber = &GetSuggestionFromBiblionumber($biblionumber)
246
247 Get a suggestion from it's biblionumber.
248
249 return :
250 the id of the suggestion which is related to the biblionumber given on input args.
251
252 =cut
253
254 sub GetSuggestionFromBiblionumber {
255     my ($biblionumber) = @_;
256     my $query = q{
257         SELECT suggestionid
258         FROM   suggestions
259         WHERE  biblionumber=? LIMIT 1
260     };
261     my $dbh = C4::Context->dbh;
262     my $sth = $dbh->prepare($query);
263     $sth->execute($biblionumber);
264     my ($suggestionid) = $sth->fetchrow;
265     return $suggestionid;
266 }
267
268 =head2 GetSuggestionInfoFromBiblionumber
269
270 Get a suggestion and borrower's informations from it's biblionumber.
271
272 return :
273 all informations (suggestion and borrower) of the suggestion which is related to the biblionumber given.
274
275 =cut
276
277 sub GetSuggestionInfoFromBiblionumber {
278     my ($biblionumber) = @_;
279     my $query = q{
280         SELECT suggestions.*,
281             U1.surname          AS surnamesuggestedby,
282             U1.firstname        AS firstnamesuggestedby,
283             U1.borrowernumber   AS borrnumsuggestedby
284         FROM suggestions
285             LEFT JOIN borrowers AS U1 ON suggestedby=U1.borrowernumber
286         WHERE biblionumber=?
287         LIMIT 1
288     };
289     my $dbh = C4::Context->dbh;
290     my $sth = $dbh->prepare($query);
291     $sth->execute($biblionumber);
292     return $sth->fetchrow_hashref;
293 }
294
295 =head2 GetSuggestionInfo
296
297 Get a suggestion and borrower's informations from it's suggestionid
298
299 return :
300 all informations (suggestion and borrower) of the suggestion which is related to the suggestionid given.
301
302 =cut
303
304 sub GetSuggestionInfo {
305     my ($suggestionid) = @_;
306     my $query = q{
307         SELECT suggestions.*,
308             U1.surname          AS surnamesuggestedby,
309             U1.firstname        AS firstnamesuggestedby,
310             U1.borrowernumber   AS borrnumsuggestedby
311         FROM suggestions
312             LEFT JOIN borrowers AS U1 ON suggestedby=U1.borrowernumber
313         WHERE suggestionid=?
314         LIMIT 1
315     };
316     my $dbh = C4::Context->dbh;
317     my $sth = $dbh->prepare($query);
318     $sth->execute($suggestionid);
319     return $sth->fetchrow_hashref;
320 }
321
322 =head2 GetSuggestionByStatus
323
324 $aqorders = &GetSuggestionByStatus($status,[$branchcode])
325
326 Get a suggestion from it's status
327
328 return :
329 all the suggestion with C<$status>
330
331 =cut
332
333 sub GetSuggestionByStatus {
334     my $status     = shift;
335     my $branchcode = shift;
336     my $dbh        = C4::Context->dbh;
337     my @sql_params = ($status);
338     my $query      = q{
339         SELECT suggestions.*,
340             U1.surname          AS surnamesuggestedby,
341             U1.firstname        AS firstnamesuggestedby,
342             U1.branchcode       AS branchcodesuggestedby,
343             B1.branchname       AS branchnamesuggestedby,
344             U1.borrowernumber   AS borrnumsuggestedby,
345             U1.categorycode     AS categorycodesuggestedby,
346             C1.description      AS categorydescriptionsuggestedby,
347             U2.surname          AS surnamemanagedby,
348             U2.firstname        AS firstnamemanagedby,
349             U2.borrowernumber   AS borrnummanagedby
350         FROM suggestions
351             LEFT JOIN borrowers     AS U1 ON suggestedby=U1.borrowernumber
352             LEFT JOIN borrowers     AS U2 ON managedby=U2.borrowernumber
353             LEFT JOIN categories    AS C1 ON C1.categorycode=U1.categorycode
354             LEFT JOIN branches      AS B1 on B1.branchcode=U1.branchcode
355         WHERE status = ?
356     };
357
358     # filter on branch
359     if ( C4::Context->preference("IndependentBranches") || $branchcode ) {
360         my $userenv = C4::Context->userenv;
361         if ($userenv) {
362             unless ( C4::Context->IsSuperLibrarian() ) {
363                 push @sql_params, $userenv->{branch};
364                 $query .= q{ AND (U1.branchcode = ? OR U1.branchcode ='') };
365             }
366         }
367         if ($branchcode) {
368             push @sql_params, $branchcode;
369             $query .= q{ AND (U1.branchcode = ? OR U1.branchcode ='') };
370         }
371     }
372
373     my $sth = $dbh->prepare($query);
374     $sth->execute(@sql_params);
375     my $results;
376     $results = $sth->fetchall_arrayref( {} );
377     return $results;
378 }
379
380 =head2 CountSuggestion
381
382 &CountSuggestion($status)
383
384 Count the number of aqorders with the status given on input argument.
385 the arg status can be :
386
387 =over 2
388
389 =item * ASKED : asked by the user, not dealed by the librarian
390
391 =item * ACCEPTED : accepted by the librarian, but not yet ordered
392
393 =item * REJECTED : rejected by the librarian (definitive status)
394
395 =item * ORDERED : ordered by the librarian (acquisition module)
396
397 =back
398
399 return :
400 the number of suggestion with this status.
401
402 =cut
403
404 sub CountSuggestion {
405     my ($status) = @_;
406     my $dbh = C4::Context->dbh;
407     my $sth;
408     my $userenv = C4::Context->userenv;
409     if ( C4::Context->preference("IndependentBranches")
410         && !C4::Context->IsSuperLibrarian() )
411     {
412         my $query = q{
413             SELECT count(*)
414             FROM suggestions
415                 LEFT JOIN borrowers ON borrowers.borrowernumber=suggestions.suggestedby
416             WHERE STATUS=?
417                 AND (borrowers.branchcode='' OR borrowers.branchcode=?)
418         };
419         $sth = $dbh->prepare($query);
420         $sth->execute( $status, $userenv->{branch} );
421     }
422     else {
423         my $query = q{
424             SELECT count(*)
425             FROM suggestions
426             WHERE STATUS=?
427         };
428         $sth = $dbh->prepare($query);
429         $sth->execute($status);
430     }
431     my ($result) = $sth->fetchrow;
432     return $result;
433 }
434
435 =head2 NewSuggestion
436
437
438 &NewSuggestion($suggestion);
439
440 Insert a new suggestion on database with value given on input arg.
441
442 =cut
443
444 sub NewSuggestion {
445     my ($suggestion) = @_;
446
447     $suggestion->{STATUS} = "ASKED" unless $suggestion->{STATUS};
448
449     $suggestion->{suggesteddate} = dt_from_string unless $suggestion->{suggesteddate};
450
451     my $suggestion_object = Koha::Suggestion->new( $suggestion )->store;
452     my $suggestion_id = $suggestion_object->suggestionid;
453
454     my $emailpurchasesuggestions = C4::Context->preference("EmailPurchaseSuggestions");
455     if ($emailpurchasesuggestions) {
456         my $full_suggestion = GetSuggestion( $suggestion_id); # We should not need to refetch it!
457         if (
458             my $letter = C4::Letters::GetPreparedLetter(
459                 module      => 'suggestions',
460                 letter_code => 'NEW_SUGGESTION',
461                 tables      => {
462                     'branches'    => $full_suggestion->{branchcode},
463                     'borrowers'   => $full_suggestion->{suggestedby},
464                     'suggestions' => $full_suggestion,
465                 },
466             )
467         ){
468
469             my $toaddress;
470             if ( $emailpurchasesuggestions eq "BranchEmailAddress" ) {
471                 my $library =
472                   Koha::Libraries->find( $full_suggestion->{branchcode} );
473                 $toaddress =
474                      $library->branchreplyto
475                   || $library->branchemail
476                   || C4::Context->preference('ReplytoDefault')
477                   || C4::Context->preference('KohaAdminEmailAddress');
478             }
479             elsif ( $emailpurchasesuggestions eq "KohaAdminEmailAddress" ) {
480                 $toaddress = C4::Context->preference('ReplytoDefault')
481                   || C4::Context->preference('KohaAdminEmailAddress');
482             }
483             else {
484                 $toaddress =
485                      C4::Context->preference($emailpurchasesuggestions)
486                   || C4::Context->preference('ReplytoDefault')
487                   || C4::Context->preference('KohaAdminEmailAddress');
488             }
489
490             C4::Letters::EnqueueLetter(
491                 {
492                     letter         => $letter,
493                     borrowernumber => $full_suggestion->{suggestedby},
494                     suggestionid   => $full_suggestion->{suggestionid},
495                     to_address     => $toaddress,
496                     message_transport_type => 'email',
497                 }
498             ) or warn "can't enqueue letter $letter";
499         }
500     }
501
502     return $suggestion_id;
503 }
504
505 =head2 ModSuggestion
506
507 &ModSuggestion($suggestion)
508
509 Modify the suggestion according to the hash passed by ref.
510 The hash HAS to contain suggestionid
511 Data not defined is not updated unless it is a note or sort1
512 Send a mail to notify the user that did the suggestion.
513
514 Note that there is no function to modify a suggestion.
515
516 =cut
517
518 sub ModSuggestion {
519     my ($suggestion) = @_;
520     return unless( $suggestion and defined($suggestion->{suggestionid}) );
521
522     my $suggestion_object = Koha::Suggestions->find( $suggestion->{suggestionid} );
523     eval { # FIXME Must raise an exception instead
524         $suggestion_object->set($suggestion)->store;
525     };
526     return 0 if $@;
527
528     if ( $suggestion->{STATUS} ) {
529
530         # fetch the entire updated suggestion so that we can populate the letter
531         my $full_suggestion = GetSuggestion( $suggestion->{suggestionid} );
532         my $patron = Koha::Patrons->find( $full_suggestion->{suggestedby} );
533
534         my $transport = (C4::Context->preference("FallbackToSMSIfNoEmail")) && ($patron->smsalertnumber) && (!$patron->email) ? 'sms' : 'email';
535
536         if (
537             my $letter = C4::Letters::GetPreparedLetter(
538                 module      => 'suggestions',
539                 letter_code => $full_suggestion->{STATUS},
540                 branchcode  => $full_suggestion->{branchcode},
541                 lang        => $patron->lang,
542                 tables      => {
543                     'branches'    => $full_suggestion->{branchcode},
544                     'borrowers'   => $full_suggestion->{suggestedby},
545                     'suggestions' => $full_suggestion,
546                     'biblio'      => $full_suggestion->{biblionumber},
547                 },
548             )
549           )
550         {
551             C4::Letters::EnqueueLetter(
552                 {
553                     letter         => $letter,
554                     borrowernumber => $full_suggestion->{suggestedby},
555                     suggestionid   => $full_suggestion->{suggestionid},
556                     LibraryName    => C4::Context->preference("LibraryName"),
557                     message_transport_type => $transport,
558                 }
559             ) or warn "can't enqueue letter $letter";
560         }
561     }
562     return 1; # No useful if the exception is raised earlier
563 }
564
565 =head2 ConnectSuggestionAndBiblio
566
567 &ConnectSuggestionAndBiblio($ordernumber,$biblionumber)
568
569 connect a suggestion to an existing biblio
570
571 =cut
572
573 sub ConnectSuggestionAndBiblio {
574     my ( $suggestionid, $biblionumber ) = @_;
575     my $dbh   = C4::Context->dbh;
576     my $query = q{
577         UPDATE suggestions
578         SET    biblionumber=?
579         WHERE  suggestionid=?
580     };
581     my $sth = $dbh->prepare($query);
582     $sth->execute( $biblionumber, $suggestionid );
583 }
584
585 =head2 DelSuggestion
586
587 &DelSuggestion($borrowernumber,$ordernumber)
588
589 Delete a suggestion. A borrower can delete a suggestion only if they are its owner.
590
591 =cut
592
593 sub DelSuggestion {
594     my ( $borrowernumber, $suggestionid, $type ) = @_;
595     my $dbh = C4::Context->dbh;
596
597     # check that the suggestion comes from the suggestor
598     my $query = q{
599         SELECT suggestedby
600         FROM   suggestions
601         WHERE  suggestionid=?
602     };
603     my $sth = $dbh->prepare($query);
604     $sth->execute($suggestionid);
605     my ($suggestedby) = $sth->fetchrow;
606     if ( $type eq 'intranet' || $suggestedby eq $borrowernumber ) {
607         my $queryDelete = q{
608             DELETE FROM suggestions
609             WHERE suggestionid=?
610         };
611         $sth = $dbh->prepare($queryDelete);
612         my $suggestiondeleted = $sth->execute($suggestionid);
613         return $suggestiondeleted;
614     }
615 }
616
617 =head2 DelSuggestionsOlderThan
618     &DelSuggestionsOlderThan($days)
619
620     Delete all suggestions older than TODAY-$days , that have be accepted or rejected.
621     We do now allow a negative number. If you want to delete all suggestions, just use Koha::Suggestions->delete or so.
622
623 =cut
624
625 sub DelSuggestionsOlderThan {
626     my ($days) = @_;
627     return unless $days && $days > 0;
628     my $dbh = C4::Context->dbh;
629     my $sth = $dbh->prepare(
630         q{
631         DELETE FROM suggestions
632         WHERE STATUS<>'ASKED'
633             AND date < ADDDATE(NOW(), ?)
634     }
635     );
636     $sth->execute("-$days");
637 }
638
639 sub GetUnprocessedSuggestions {
640     my ( $number_of_days_since_the_last_modification ) = @_;
641
642     $number_of_days_since_the_last_modification ||= 0;
643
644     my $dbh = C4::Context->dbh;
645
646     my $s = $dbh->selectall_arrayref(q|
647         SELECT *
648         FROM suggestions
649         WHERE STATUS = 'ASKED'
650             AND budgetid IS NOT NULL
651             AND CAST(NOW() AS DATE) - INTERVAL ? DAY = CAST(suggesteddate AS DATE)
652     |, { Slice => {} }, $number_of_days_since_the_last_modification );
653     return $s;
654 }
655
656 1;
657 __END__
658
659
660 =head1 AUTHOR
661
662 Koha Development Team <http://koha-community.org/>
663
664 =cut
665