Bug 24244: Send null instead of empty string if branch not set
[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 (suggestions.branchcode='' OR suggestions.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     delete $suggestion->{branchcode} if $suggestion->{branchcode} eq '';
452
453     my $suggestion_object = Koha::Suggestion->new( $suggestion )->store;
454     my $suggestion_id = $suggestion_object->suggestionid;
455
456     my $emailpurchasesuggestions = C4::Context->preference("EmailPurchaseSuggestions");
457     if ($emailpurchasesuggestions) {
458         my $full_suggestion = GetSuggestion( $suggestion_id); # We should not need to refetch it!
459         if (
460             my $letter = C4::Letters::GetPreparedLetter(
461                 module      => 'suggestions',
462                 letter_code => 'NEW_SUGGESTION',
463                 tables      => {
464                     'branches'    => $full_suggestion->{branchcode},
465                     'borrowers'   => $full_suggestion->{suggestedby},
466                     'suggestions' => $full_suggestion,
467                 },
468             )
469         ){
470
471             my $toaddress;
472             if ( $emailpurchasesuggestions eq "BranchEmailAddress" ) {
473                 my $library =
474                   Koha::Libraries->find( $full_suggestion->{branchcode} );
475                 $toaddress =
476                      $library->branchreplyto
477                   || $library->branchemail
478                   || C4::Context->preference('ReplytoDefault')
479                   || C4::Context->preference('KohaAdminEmailAddress');
480             }
481             elsif ( $emailpurchasesuggestions eq "KohaAdminEmailAddress" ) {
482                 $toaddress = C4::Context->preference('ReplytoDefault')
483                   || C4::Context->preference('KohaAdminEmailAddress');
484             }
485             else {
486                 $toaddress =
487                      C4::Context->preference($emailpurchasesuggestions)
488                   || C4::Context->preference('ReplytoDefault')
489                   || C4::Context->preference('KohaAdminEmailAddress');
490             }
491
492             C4::Letters::EnqueueLetter(
493                 {
494                     letter         => $letter,
495                     borrowernumber => $full_suggestion->{suggestedby},
496                     suggestionid   => $full_suggestion->{suggestionid},
497                     to_address     => $toaddress,
498                     message_transport_type => 'email',
499                 }
500             ) or warn "can't enqueue letter $letter";
501         }
502     }
503
504     return $suggestion_id;
505 }
506
507 =head2 ModSuggestion
508
509 &ModSuggestion($suggestion)
510
511 Modify the suggestion according to the hash passed by ref.
512 The hash HAS to contain suggestionid
513 Data not defined is not updated unless it is a note or sort1
514 Send a mail to notify the user that did the suggestion.
515
516 Note that there is no function to modify a suggestion.
517
518 =cut
519
520 sub ModSuggestion {
521     my ($suggestion) = @_;
522     return unless( $suggestion and defined($suggestion->{suggestionid}) );
523
524     my $suggestion_object = Koha::Suggestions->find( $suggestion->{suggestionid} );
525     eval { # FIXME Must raise an exception instead
526         $suggestion_object->set($suggestion)->store;
527     };
528     return 0 if $@;
529
530     if ( $suggestion->{STATUS} ) {
531
532         # fetch the entire updated suggestion so that we can populate the letter
533         my $full_suggestion = GetSuggestion( $suggestion->{suggestionid} );
534         my $patron = Koha::Patrons->find( $full_suggestion->{suggestedby} );
535
536         my $transport = (C4::Context->preference("FallbackToSMSIfNoEmail")) && ($patron->smsalertnumber) && (!$patron->email) ? 'sms' : 'email';
537
538         if (
539             my $letter = C4::Letters::GetPreparedLetter(
540                 module      => 'suggestions',
541                 letter_code => $full_suggestion->{STATUS},
542                 branchcode  => $full_suggestion->{branchcode},
543                 lang        => $patron->lang,
544                 tables      => {
545                     'branches'    => $full_suggestion->{branchcode},
546                     'borrowers'   => $full_suggestion->{suggestedby},
547                     'suggestions' => $full_suggestion,
548                     'biblio'      => $full_suggestion->{biblionumber},
549                 },
550             )
551           )
552         {
553             C4::Letters::EnqueueLetter(
554                 {
555                     letter         => $letter,
556                     borrowernumber => $full_suggestion->{suggestedby},
557                     suggestionid   => $full_suggestion->{suggestionid},
558                     LibraryName    => C4::Context->preference("LibraryName"),
559                     message_transport_type => $transport,
560                 }
561             ) or warn "can't enqueue letter $letter";
562         }
563     }
564     return 1; # No useful if the exception is raised earlier
565 }
566
567 =head2 ConnectSuggestionAndBiblio
568
569 &ConnectSuggestionAndBiblio($ordernumber,$biblionumber)
570
571 connect a suggestion to an existing biblio
572
573 =cut
574
575 sub ConnectSuggestionAndBiblio {
576     my ( $suggestionid, $biblionumber ) = @_;
577     my $dbh   = C4::Context->dbh;
578     my $query = q{
579         UPDATE suggestions
580         SET    biblionumber=?
581         WHERE  suggestionid=?
582     };
583     my $sth = $dbh->prepare($query);
584     $sth->execute( $biblionumber, $suggestionid );
585 }
586
587 =head2 DelSuggestion
588
589 &DelSuggestion($borrowernumber,$ordernumber)
590
591 Delete a suggestion. A borrower can delete a suggestion only if they are its owner.
592
593 =cut
594
595 sub DelSuggestion {
596     my ( $borrowernumber, $suggestionid, $type ) = @_;
597     my $dbh = C4::Context->dbh;
598
599     # check that the suggestion comes from the suggestor
600     my $query = q{
601         SELECT suggestedby
602         FROM   suggestions
603         WHERE  suggestionid=?
604     };
605     my $sth = $dbh->prepare($query);
606     $sth->execute($suggestionid);
607     my ($suggestedby) = $sth->fetchrow;
608     if ( $type eq 'intranet' || $suggestedby eq $borrowernumber ) {
609         my $queryDelete = q{
610             DELETE FROM suggestions
611             WHERE suggestionid=?
612         };
613         $sth = $dbh->prepare($queryDelete);
614         my $suggestiondeleted = $sth->execute($suggestionid);
615         return $suggestiondeleted;
616     }
617 }
618
619 =head2 DelSuggestionsOlderThan
620     &DelSuggestionsOlderThan($days)
621
622     Delete all suggestions older than TODAY-$days , that have be accepted or rejected.
623     We do now allow a negative number. If you want to delete all suggestions, just use Koha::Suggestions->delete or so.
624
625 =cut
626
627 sub DelSuggestionsOlderThan {
628     my ($days) = @_;
629     return unless $days && $days > 0;
630     my $dbh = C4::Context->dbh;
631     my $sth = $dbh->prepare(
632         q{
633         DELETE FROM suggestions
634         WHERE STATUS<>'ASKED'
635             AND date < ADDDATE(NOW(), ?)
636     }
637     );
638     $sth->execute("-$days");
639 }
640
641 sub GetUnprocessedSuggestions {
642     my ( $number_of_days_since_the_last_modification ) = @_;
643
644     $number_of_days_since_the_last_modification ||= 0;
645
646     my $dbh = C4::Context->dbh;
647
648     my $s = $dbh->selectall_arrayref(q|
649         SELECT *
650         FROM suggestions
651         WHERE STATUS = 'ASKED'
652             AND budgetid IS NOT NULL
653             AND CAST(NOW() AS DATE) - INTERVAL ? DAY = CAST(suggesteddate AS DATE)
654     |, { Slice => {} }, $number_of_days_since_the_last_modification );
655     return $s;
656 }
657
658 1;
659 __END__
660
661
662 =head1 AUTHOR
663
664 Koha Development Team <http://koha-community.org/>
665
666 =cut
667