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