Bug 21908: (QA follow-up) Remove useless parentheses in query
[koha.git] / C4 / Letters.pm
blob5382819b8f840f466ae5f8695673be5686092675
1 package C4::Letters;
3 # Copyright 2000-2002 Katipo Communications
5 # This file is part of Koha.
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.
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.
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>.
20 use Modern::Perl;
22 use MIME::Lite;
23 use Mail::Sendmail;
24 use Date::Calc qw( Add_Delta_Days );
25 use Encode;
26 use Carp;
27 use Template;
28 use Module::Load::Conditional qw(can_load);
30 use C4::Members;
31 use C4::Members::Attributes qw(GetBorrowerAttributes);
32 use C4::Log;
33 use C4::SMS;
34 use C4::Debug;
35 use Koha::DateUtils;
36 use Koha::SMS::Providers;
38 use Koha::Email;
39 use Koha::Notice::Messages;
40 use Koha::DateUtils qw( format_sqldatetime dt_from_string );
41 use Koha::Patrons;
42 use Koha::Subscriptions;
44 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
46 BEGIN {
47 require Exporter;
48 @ISA = qw(Exporter);
49 @EXPORT = qw(
50 &GetLetters &GetLettersAvailableForALibrary &GetLetterTemplates &DelLetter &GetPreparedLetter &GetWrappedLetter &SendAlerts &GetPrintMessages &GetMessageTransportTypes
54 =head1 NAME
56 C4::Letters - Give functions for Letters management
58 =head1 SYNOPSIS
60 use C4::Letters;
62 =head1 DESCRIPTION
64 "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
65 late issues, as well as other tasks like sending a mail to users that have subscribed to a "serial issue alert" (= being warned every time a new issue has arrived at the library)
67 Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
69 =head2 GetLetters([$module])
71 $letters = &GetLetters($module);
72 returns informations about letters.
73 if needed, $module filters for letters given module
75 DEPRECATED - You must use Koha::Notice::Templates instead
76 The group by clause is confusing and can lead to issues
78 =cut
80 sub GetLetters {
81 my ($filters) = @_;
82 my $module = $filters->{module};
83 my $code = $filters->{code};
84 my $branchcode = $filters->{branchcode};
85 my $dbh = C4::Context->dbh;
86 my $letters = $dbh->selectall_arrayref(
88 SELECT code, module, name
89 FROM letter
90 WHERE 1
92 . ( $module ? q| AND module = ?| : q|| )
93 . ( $code ? q| AND code = ?| : q|| )
94 . ( defined $branchcode ? q| AND branchcode = ?| : q|| )
95 . q| GROUP BY code, module, name ORDER BY name|, { Slice => {} }
96 , ( $module ? $module : () )
97 , ( $code ? $code : () )
98 , ( defined $branchcode ? $branchcode : () )
101 return $letters;
104 =head2 GetLetterTemplates
106 my $letter_templates = GetLetterTemplates(
108 module => 'circulation',
109 code => 'my code',
110 branchcode => 'CPL', # '' for default,
114 Return a hashref of letter templates.
116 =cut
118 sub GetLetterTemplates {
119 my ( $params ) = @_;
121 my $module = $params->{module};
122 my $code = $params->{code};
123 my $branchcode = $params->{branchcode} // '';
124 my $dbh = C4::Context->dbh;
125 my $letters = $dbh->selectall_arrayref(
127 SELECT module, code, branchcode, name, is_html, title, content, message_transport_type, lang
128 FROM letter
129 WHERE module = ?
130 AND code = ?
131 and branchcode = ?
133 , { Slice => {} }
134 , $module, $code, $branchcode
137 return $letters;
140 =head2 GetLettersAvailableForALibrary
142 my $letters = GetLettersAvailableForALibrary(
144 branchcode => 'CPL', # '' for default
145 module => 'circulation',
149 Return an arrayref of letters, sorted by name.
150 If a specific letter exist for the given branchcode, it will be retrieve.
151 Otherwise the default letter will be.
153 =cut
155 sub GetLettersAvailableForALibrary {
156 my ($filters) = @_;
157 my $branchcode = $filters->{branchcode};
158 my $module = $filters->{module};
160 croak "module should be provided" unless $module;
162 my $dbh = C4::Context->dbh;
163 my $default_letters = $dbh->selectall_arrayref(
165 SELECT module, code, branchcode, name
166 FROM letter
167 WHERE 1
169 . q| AND branchcode = ''|
170 . ( $module ? q| AND module = ?| : q|| )
171 . q| ORDER BY name|, { Slice => {} }
172 , ( $module ? $module : () )
175 my $specific_letters;
176 if ($branchcode) {
177 $specific_letters = $dbh->selectall_arrayref(
179 SELECT module, code, branchcode, name
180 FROM letter
181 WHERE 1
183 . q| AND branchcode = ?|
184 . ( $module ? q| AND module = ?| : q|| )
185 . q| ORDER BY name|, { Slice => {} }
186 , $branchcode
187 , ( $module ? $module : () )
191 my %letters;
192 for my $l (@$default_letters) {
193 $letters{ $l->{code} } = $l;
195 for my $l (@$specific_letters) {
196 # Overwrite the default letter with the specific one.
197 $letters{ $l->{code} } = $l;
200 return [ map { $letters{$_} }
201 sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
202 keys %letters ];
206 sub getletter {
207 my ( $module, $code, $branchcode, $message_transport_type, $lang) = @_;
208 $message_transport_type //= '%';
209 $lang = 'default' unless( $lang && C4::Context->preference('TranslateNotices') );
212 my $only_my_library = C4::Context->only_my_library;
213 if ( $only_my_library and $branchcode ) {
214 $branchcode = C4::Context::mybranch();
216 $branchcode //= '';
218 my $dbh = C4::Context->dbh;
219 my $sth = $dbh->prepare(q{
220 SELECT *
221 FROM letter
222 WHERE module=? AND code=? AND (branchcode = ? OR branchcode = '')
223 AND message_transport_type LIKE ?
224 AND lang =?
225 ORDER BY branchcode DESC LIMIT 1
227 $sth->execute( $module, $code, $branchcode, $message_transport_type, $lang );
228 my $line = $sth->fetchrow_hashref
229 or return;
230 $line->{'content-type'} = 'text/html; charset="UTF-8"' if $line->{is_html};
231 return { %$line };
235 =head2 DelLetter
237 DelLetter(
239 branchcode => 'CPL',
240 module => 'circulation',
241 code => 'my code',
242 [ mtt => 'email', ]
246 Delete the letter. The mtt parameter is facultative.
247 If not given, all templates mathing the other parameters will be removed.
249 =cut
251 sub DelLetter {
252 my ($params) = @_;
253 my $branchcode = $params->{branchcode};
254 my $module = $params->{module};
255 my $code = $params->{code};
256 my $mtt = $params->{mtt};
257 my $lang = $params->{lang};
258 my $dbh = C4::Context->dbh;
259 $dbh->do(q|
260 DELETE FROM letter
261 WHERE branchcode = ?
262 AND module = ?
263 AND code = ?
265 . ( $mtt ? q| AND message_transport_type = ?| : q|| )
266 . ( $lang? q| AND lang = ?| : q|| )
267 , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
270 =head2 SendAlerts
272 my $err = &SendAlerts($type, $externalid, $letter_code);
274 Parameters:
275 - $type : the type of alert
276 - $externalid : the id of the "object" to query
277 - $letter_code : the notice template to use
279 C<&SendAlerts> sends an email notice directly to a patron or a vendor.
281 Currently it supports ($type):
282 - claim serial issues (claimissues)
283 - claim acquisition orders (claimacquisition)
284 - send acquisition orders to the vendor (orderacquisition)
285 - notify patrons about newly received serial issues (issue)
286 - notify patrons when their account is created (members)
288 Returns undef or { error => 'message } on failure.
289 Returns true on success.
291 =cut
293 sub SendAlerts {
294 my ( $type, $externalid, $letter_code ) = @_;
295 my $dbh = C4::Context->dbh;
296 if ( $type eq 'issue' ) {
298 # prepare the letter...
299 # search the subscriptionid
300 my $sth =
301 $dbh->prepare(
302 "SELECT subscriptionid FROM serial WHERE serialid=?");
303 $sth->execute($externalid);
304 my ($subscriptionid) = $sth->fetchrow
305 or warn( "No subscription for '$externalid'" ),
306 return;
308 # search the biblionumber
309 $sth =
310 $dbh->prepare(
311 "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
312 $sth->execute($subscriptionid);
313 my ($biblionumber) = $sth->fetchrow
314 or warn( "No biblionumber for '$subscriptionid'" ),
315 return;
317 my %letter;
318 # find the list of subscribers to notify
319 my $subscription = Koha::Subscriptions->find( $subscriptionid );
320 my $subscribers = $subscription->subscribers;
321 while ( my $patron = $subscribers->next ) {
322 my $email = $patron->email or next;
324 # warn "sending issues...";
325 my $userenv = C4::Context->userenv;
326 my $library = $patron->library;
327 my $letter = GetPreparedLetter (
328 module => 'serial',
329 letter_code => $letter_code,
330 branchcode => $userenv->{branch},
331 tables => {
332 'branches' => $library->branchcode,
333 'biblio' => $biblionumber,
334 'biblioitems' => $biblionumber,
335 'borrowers' => $patron->unblessed,
336 'subscription' => $subscriptionid,
337 'serial' => $externalid,
339 want_librarian => 1,
340 ) or return;
342 # ... then send mail
343 my $message = Koha::Email->new();
344 my %mail = $message->create_message_headers(
346 to => $email,
347 from => $library->branchemail,
348 replyto => $library->branchreplyto,
349 sender => $library->branchreturnpath,
350 subject => Encode::encode( "UTF-8", "" . $letter->{title} ),
351 message => $letter->{'is_html'}
352 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
353 Encode::encode( "UTF-8", "" . $letter->{'title'} ))
354 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
355 contenttype => $letter->{'is_html'}
356 ? 'text/html; charset="utf-8"'
357 : 'text/plain; charset="utf-8"',
360 unless( Mail::Sendmail::sendmail(%mail) ) {
361 carp $Mail::Sendmail::error;
362 return { error => $Mail::Sendmail::error };
366 elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
368 # prepare the letter...
369 my $strsth;
370 my $sthorders;
371 my $dataorders;
372 my $action;
373 if ( $type eq 'claimacquisition') {
374 $strsth = qq{
375 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
376 FROM aqorders
377 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
378 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
379 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
380 WHERE aqorders.ordernumber IN (
383 if (!@$externalid){
384 carp "No order selected";
385 return { error => "no_order_selected" };
387 $strsth .= join( ",", ('?') x @$externalid ) . ")";
388 $action = "ACQUISITION CLAIM";
389 $sthorders = $dbh->prepare($strsth);
390 $sthorders->execute( @$externalid );
391 $dataorders = $sthorders->fetchall_arrayref( {} );
394 if ($type eq 'claimissues') {
395 $strsth = qq{
396 SELECT serial.*,subscription.*, biblio.*, biblioitems.*, aqbooksellers.*,
397 aqbooksellers.id AS booksellerid
398 FROM serial
399 LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
400 LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
401 LEFT JOIN biblioitems ON serial.biblionumber = biblioitems.biblionumber
402 LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
403 WHERE serial.serialid IN (
406 if (!@$externalid){
407 carp "No issues selected";
408 return { error => "no_issues_selected" };
411 $strsth .= join( ",", ('?') x @$externalid ) . ")";
412 $action = "SERIAL CLAIM";
413 $sthorders = $dbh->prepare($strsth);
414 $sthorders->execute( @$externalid );
415 $dataorders = $sthorders->fetchall_arrayref( {} );
418 if ( $type eq 'orderacquisition') {
419 $strsth = qq{
420 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
421 FROM aqorders
422 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
423 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
424 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
425 WHERE aqbasket.basketno = ?
426 AND orderstatus IN ('new','ordered')
429 if (!$externalid){
430 carp "No basketnumber given";
431 return { error => "no_basketno" };
433 $action = "ACQUISITION ORDER";
434 $sthorders = $dbh->prepare($strsth);
435 $sthorders->execute($externalid);
436 $dataorders = $sthorders->fetchall_arrayref( {} );
439 my $sthbookseller =
440 $dbh->prepare("select * from aqbooksellers where id=?");
441 $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
442 my $databookseller = $sthbookseller->fetchrow_hashref;
444 my $addressee = $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
446 my $sthcontact =
447 $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
448 $sthcontact->execute( $dataorders->[0]->{booksellerid} );
449 my $datacontact = $sthcontact->fetchrow_hashref;
451 my @email;
452 my @cc;
453 push @email, $databookseller->{bookselleremail} if $databookseller->{bookselleremail};
454 push @email, $datacontact->{email} if ( $datacontact && $datacontact->{email} );
455 unless (@email) {
456 warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
457 return { error => "no_email" };
459 my $addlcontact;
460 while ($addlcontact = $sthcontact->fetchrow_hashref) {
461 push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
464 my $userenv = C4::Context->userenv;
465 my $letter = GetPreparedLetter (
466 module => $type,
467 letter_code => $letter_code,
468 branchcode => $userenv->{branch},
469 tables => {
470 'branches' => $userenv->{branch},
471 'aqbooksellers' => $databookseller,
472 'aqcontacts' => $datacontact,
474 repeat => $dataorders,
475 want_librarian => 1,
476 ) or return { error => "no_letter" };
478 # Remove the order tag
479 $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
481 # ... then send mail
482 my $library = Koha::Libraries->find( $userenv->{branch} );
483 my %mail = (
484 To => join( ',', @email),
485 Cc => join( ',', @cc),
486 From => $library->branchemail || C4::Context->preference('KohaAdminEmailAddress'),
487 Subject => Encode::encode( "UTF-8", "" . $letter->{title} ),
488 Message => $letter->{'is_html'}
489 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
490 Encode::encode( "UTF-8", "" . $letter->{'title'} ))
491 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
492 'Content-Type' => $letter->{'is_html'}
493 ? 'text/html; charset="utf-8"'
494 : 'text/plain; charset="utf-8"',
497 if ($type eq 'claimacquisition' || $type eq 'claimissues' ) {
498 $mail{'Reply-to'} = C4::Context->preference('ReplytoDefault')
499 if C4::Context->preference('ReplytoDefault');
500 $mail{'Sender'} = C4::Context->preference('ReturnpathDefault')
501 if C4::Context->preference('ReturnpathDefault');
502 $mail{'Bcc'} = $userenv->{emailaddress}
503 if C4::Context->preference("ClaimsBccCopy");
506 unless ( Mail::Sendmail::sendmail(%mail) ) {
507 carp $Mail::Sendmail::error;
508 return { error => $Mail::Sendmail::error };
511 logaction(
512 "ACQUISITION",
513 $action,
514 undef,
515 "To="
516 . join( ',', @email )
517 . " Title="
518 . $letter->{title}
519 . " Content="
520 . $letter->{content}
521 ) if C4::Context->preference("LetterLog");
523 # send an "account details" notice to a newly created user
524 elsif ( $type eq 'members' ) {
525 my $library = Koha::Libraries->find( $externalid->{branchcode} )->unblessed;
526 my $letter = GetPreparedLetter (
527 module => 'members',
528 letter_code => $letter_code,
529 branchcode => $externalid->{'branchcode'},
530 tables => {
531 'branches' => $library,
532 'borrowers' => $externalid->{'borrowernumber'},
534 substitute => { 'borrowers.password' => $externalid->{'password'} },
535 want_librarian => 1,
536 ) or return;
537 return { error => "no_email" } unless $externalid->{'emailaddr'};
538 my $email = Koha::Email->new();
539 my %mail = $email->create_message_headers(
541 to => $externalid->{'emailaddr'},
542 from => $library->{branchemail},
543 replyto => $library->{branchreplyto},
544 sender => $library->{branchreturnpath},
545 subject => Encode::encode( "UTF-8", "" . $letter->{'title'} ),
546 message => $letter->{'is_html'}
547 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
548 Encode::encode( "UTF-8", "" . $letter->{'title'} ) )
549 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
550 contenttype => $letter->{'is_html'}
551 ? 'text/html; charset="utf-8"'
552 : 'text/plain; charset="utf-8"',
555 unless( Mail::Sendmail::sendmail(%mail) ) {
556 carp $Mail::Sendmail::error;
557 return { error => $Mail::Sendmail::error };
561 # If we come here, return an OK status
562 return 1;
565 =head2 GetPreparedLetter( %params )
567 %params hash:
568 module => letter module, mandatory
569 letter_code => letter code, mandatory
570 branchcode => for letter selection, if missing default system letter taken
571 tables => a hashref with table names as keys. Values are either:
572 - a scalar - primary key value
573 - an arrayref - primary key values
574 - a hashref - full record
575 substitute => custom substitution key/value pairs
576 repeat => records to be substituted on consecutive lines:
577 - an arrayref - tries to guess what needs substituting by
578 taking remaining << >> tokensr; not recommended
579 - a hashref token => @tables - replaces <token> << >> << >> </token>
580 subtemplate for each @tables row; table is a hashref as above
581 want_librarian => boolean, if set to true triggers librarian details
582 substitution from the userenv
583 Return value:
584 letter fields hashref (title & content useful)
586 =cut
588 sub GetPreparedLetter {
589 my %params = @_;
591 my $letter = $params{letter};
593 unless ( $letter ) {
594 my $module = $params{module} or croak "No module";
595 my $letter_code = $params{letter_code} or croak "No letter_code";
596 my $branchcode = $params{branchcode} || '';
597 my $mtt = $params{message_transport_type} || 'email';
598 my $lang = $params{lang} || 'default';
600 $letter = getletter( $module, $letter_code, $branchcode, $mtt, $lang );
602 unless ( $letter ) {
603 $letter = getletter( $module, $letter_code, $branchcode, $mtt, 'default' )
604 or warn( "No $module $letter_code letter transported by " . $mtt ),
605 return;
609 my $tables = $params{tables} || {};
610 my $substitute = $params{substitute} || {};
611 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
612 my $repeat = $params{repeat};
613 %$tables || %$substitute || $repeat || %$loops
614 or carp( "ERROR: nothing to substitute - both 'tables', 'loops' and 'substitute' are empty" ),
615 return;
616 my $want_librarian = $params{want_librarian};
618 if (%$substitute) {
619 while ( my ($token, $val) = each %$substitute ) {
620 if ( $token eq 'items.content' ) {
621 $val =~ s|\n|<br/>|g if $letter->{is_html};
624 $letter->{title} =~ s/<<$token>>/$val/g;
625 $letter->{content} =~ s/<<$token>>/$val/g;
629 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
630 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
632 if ($want_librarian) {
633 # parsing librarian name
634 my $userenv = C4::Context->userenv;
635 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
636 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
637 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
640 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
642 if ($repeat) {
643 if (ref ($repeat) eq 'ARRAY' ) {
644 $repeat_no_enclosing_tags = $repeat;
645 } else {
646 $repeat_enclosing_tags = $repeat;
650 if ($repeat_enclosing_tags) {
651 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
652 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
653 my $subcontent = $1;
654 my @lines = map {
655 my %subletter = ( title => '', content => $subcontent );
656 _substitute_tables( \%subletter, $_ );
657 $subletter{content};
658 } @$tag_tables;
659 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
664 if (%$tables) {
665 _substitute_tables( $letter, $tables );
668 if ($repeat_no_enclosing_tags) {
669 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
670 my $line = $&;
671 my $i = 1;
672 my @lines = map {
673 my $c = $line;
674 $c =~ s/<<count>>/$i/go;
675 foreach my $field ( keys %{$_} ) {
676 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
678 $i++;
680 } @$repeat_no_enclosing_tags;
682 my $replaceby = join( "\n", @lines );
683 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
687 $letter->{content} = _process_tt(
689 content => $letter->{content},
690 tables => $tables,
691 loops => $loops,
692 substitute => $substitute,
696 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
698 return $letter;
701 sub _substitute_tables {
702 my ( $letter, $tables ) = @_;
703 while ( my ($table, $param) = each %$tables ) {
704 next unless $param;
706 my $ref = ref $param;
708 my $values;
709 if ($ref && $ref eq 'HASH') {
710 $values = $param;
712 else {
713 my $sth = _parseletter_sth($table);
714 unless ($sth) {
715 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
716 return;
718 $sth->execute( $ref ? @$param : $param );
720 $values = $sth->fetchrow_hashref;
721 $sth->finish();
724 _parseletter ( $letter, $table, $values );
728 sub _parseletter_sth {
729 my $table = shift;
730 my $sth;
731 unless ($table) {
732 carp "ERROR: _parseletter_sth() called without argument (table)";
733 return;
735 # NOTE: we used to check whether we had a statement handle cached in
736 # a %handles module-level variable. This was a dumb move and
737 # broke things for the rest of us. prepare_cached is a better
738 # way to cache statement handles anyway.
739 my $query =
740 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
741 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
742 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
743 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
744 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE itemnumber = ? ORDER BY timestamp DESC LIMIT 1" :
745 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
746 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
747 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
748 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
749 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
750 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
751 ($table eq 'opac_news' ) ? "SELECT * FROM $table WHERE idnew = ?" :
752 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
753 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
754 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
755 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
756 undef ;
757 unless ($query) {
758 warn "ERROR: No _parseletter_sth query for table '$table'";
759 return; # nothing to get
761 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
762 warn "ERROR: Failed to prepare query: '$query'";
763 return;
765 return $sth; # now cache is populated for that $table
768 =head2 _parseletter($letter, $table, $values)
770 parameters :
771 - $letter : a hash to letter fields (title & content useful)
772 - $table : the Koha table to parse.
773 - $values_in : table record hashref
774 parse all fields from a table, and replace values in title & content with the appropriate value
775 (not exported sub, used only internally)
777 =cut
779 sub _parseletter {
780 my ( $letter, $table, $values_in ) = @_;
782 # Work on a local copy of $values_in (passed by reference) to avoid side effects
783 # in callers ( by changing / formatting values )
784 my $values = $values_in ? { %$values_in } : {};
786 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
787 $values->{'dateexpiry'} = output_pref({ str => $values->{dateexpiry}, dateonly => 1 });
790 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
791 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
794 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
795 my $todaysdate = output_pref( DateTime->now() );
796 $letter->{content} =~ s/<<today>>/$todaysdate/go;
799 while ( my ($field, $val) = each %$values ) {
800 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
801 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
802 #Therefore adding the test on biblio. This includes biblioitems,
803 #but excludes items. Removed unneeded global and lookahead.
805 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
806 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
807 $val = $av->count ? $av->next->lib : '';
810 # Dates replacement
811 my $replacedby = defined ($val) ? $val : '';
812 if ( $replacedby
813 and not $replacedby =~ m|0000-00-00|
814 and not $replacedby =~ m|9999-12-31|
815 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
817 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
818 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
819 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
821 for my $letter_field ( qw( title content ) ) {
822 my $filter_string_used = q{};
823 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
824 # We overwrite $dateonly if the filter exists and we have a time in the datetime
825 $filter_string_used = $1 || q{};
826 $dateonly = $1 unless $dateonly;
828 my $replacedby_date = eval {
829 output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
832 if ( $letter->{ $letter_field } ) {
833 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
834 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
838 # Other fields replacement
839 else {
840 for my $letter_field ( qw( title content ) ) {
841 if ( $letter->{ $letter_field } ) {
842 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
843 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
849 if ($table eq 'borrowers' && $letter->{content}) {
850 if ( my $attributes = GetBorrowerAttributes($values->{borrowernumber}) ) {
851 my %attr;
852 foreach (@$attributes) {
853 my $code = $_->{code};
854 my $val = $_->{value_description} || $_->{value};
855 $val =~ s/\p{P}(?=$)//g if $val;
856 next unless $val gt '';
857 $attr{$code} ||= [];
858 push @{ $attr{$code} }, $val;
860 while ( my ($code, $val_ar) = each %attr ) {
861 my $replacefield = "<<borrower-attribute:$code>>";
862 my $replacedby = join ',', @$val_ar;
863 $letter->{content} =~ s/$replacefield/$replacedby/g;
867 return $letter;
870 =head2 EnqueueLetter
872 my $success = EnqueueLetter( { letter => $letter,
873 borrowernumber => '12', message_transport_type => 'email' } )
875 places a letter in the message_queue database table, which will
876 eventually get processed (sent) by the process_message_queue.pl
877 cronjob when it calls SendQueuedMessages.
879 return message_id on success
881 =cut
883 sub EnqueueLetter {
884 my $params = shift or return;
886 return unless exists $params->{'letter'};
887 # return unless exists $params->{'borrowernumber'};
888 return unless exists $params->{'message_transport_type'};
890 my $content = $params->{letter}->{content};
891 $content =~ s/\s+//g if(defined $content);
892 if ( not defined $content or $content eq '' ) {
893 warn "Trying to add an empty message to the message queue" if $debug;
894 return;
897 # If we have any attachments we should encode then into the body.
898 if ( $params->{'attachments'} ) {
899 $params->{'letter'} = _add_attachments(
900 { letter => $params->{'letter'},
901 attachments => $params->{'attachments'},
902 message => MIME::Lite->new( Type => 'multipart/mixed' ),
907 my $dbh = C4::Context->dbh();
908 my $statement = << 'ENDSQL';
909 INSERT INTO message_queue
910 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type )
911 VALUES
912 ( ?, ?, ?, ?, ?, ?, ?, NOW(), ?, ?, ? )
913 ENDSQL
915 my $sth = $dbh->prepare($statement);
916 my $result = $sth->execute(
917 $params->{'borrowernumber'}, # borrowernumber
918 $params->{'letter'}->{'title'}, # subject
919 $params->{'letter'}->{'content'}, # content
920 $params->{'letter'}->{'metadata'} || '', # metadata
921 $params->{'letter'}->{'code'} || '', # letter_code
922 $params->{'message_transport_type'}, # message_transport_type
923 'pending', # status
924 $params->{'to_address'}, # to_address
925 $params->{'from_address'}, # from_address
926 $params->{'letter'}->{'content-type'}, # content_type
928 return $dbh->last_insert_id(undef,undef,'message_queue', undef);
931 =head2 SendQueuedMessages ([$hashref])
933 my $sent = SendQueuedMessages({
934 letter_code => $letter_code,
935 borrowernumber => $who_letter_is_for,
936 limit => 50,
937 verbose => 1,
938 type => 'sms',
941 Sends all of the 'pending' items in the message queue, unless
942 parameters are passed.
944 The letter_code, borrowernumber and limit parameters are used
945 to build a parameter set for _get_unsent_messages, thus limiting
946 which pending messages will be processed. They are all optional.
948 The verbose parameter can be used to generate debugging output.
949 It is also optional.
951 Returns number of messages sent.
953 =cut
955 sub SendQueuedMessages {
956 my $params = shift;
958 my $which_unsent_messages = {
959 'limit' => $params->{'limit'} // 0,
960 'borrowernumber' => $params->{'borrowernumber'} // q{},
961 'letter_code' => $params->{'letter_code'} // q{},
962 'type' => $params->{'type'} // q{},
964 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
965 MESSAGE: foreach my $message ( @$unsent_messages ) {
966 my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
967 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
968 $message_object->make_column_dirty('status');
969 return unless $message_object->store;
971 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
972 warn sprintf( 'sending %s message to patron: %s',
973 $message->{'message_transport_type'},
974 $message->{'borrowernumber'} || 'Admin' )
975 if $params->{'verbose'} or $debug;
976 # This is just begging for subclassing
977 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
978 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
979 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
981 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
982 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
983 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
984 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
985 unless ( $sms_provider ) {
986 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
987 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
988 next MESSAGE;
990 unless ( $patron->smsalertnumber ) {
991 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
992 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
993 next MESSAGE;
995 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
996 $message->{to_address} .= '@' . $sms_provider->domain();
998 # Check for possible from_address override
999 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1000 if ($from_address && $message->{from_address} ne $from_address) {
1001 $message->{from_address} = $from_address;
1002 _update_message_from_address($message->{'message_id'}, $message->{from_address});
1005 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1006 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1007 } else {
1008 _send_message_by_sms( $message );
1012 return scalar( @$unsent_messages );
1015 =head2 GetRSSMessages
1017 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1019 returns a listref of all queued RSS messages for a particular person.
1021 =cut
1023 sub GetRSSMessages {
1024 my $params = shift;
1026 return unless $params;
1027 return unless ref $params;
1028 return unless $params->{'borrowernumber'};
1030 return _get_unsent_messages( { message_transport_type => 'rss',
1031 limit => $params->{'limit'},
1032 borrowernumber => $params->{'borrowernumber'}, } );
1035 =head2 GetPrintMessages
1037 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1039 Returns a arrayref of all queued print messages (optionally, for a particular
1040 person).
1042 =cut
1044 sub GetPrintMessages {
1045 my $params = shift || {};
1047 return _get_unsent_messages( { message_transport_type => 'print',
1048 borrowernumber => $params->{'borrowernumber'},
1049 } );
1052 =head2 GetQueuedMessages ([$hashref])
1054 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1056 fetches messages out of the message queue.
1058 returns:
1059 list of hashes, each has represents a message in the message queue.
1061 =cut
1063 sub GetQueuedMessages {
1064 my $params = shift;
1066 my $dbh = C4::Context->dbh();
1067 my $statement = << 'ENDSQL';
1068 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued
1069 FROM message_queue
1070 ENDSQL
1072 my @query_params;
1073 my @whereclauses;
1074 if ( exists $params->{'borrowernumber'} ) {
1075 push @whereclauses, ' borrowernumber = ? ';
1076 push @query_params, $params->{'borrowernumber'};
1079 if ( @whereclauses ) {
1080 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1083 if ( defined $params->{'limit'} ) {
1084 $statement .= ' LIMIT ? ';
1085 push @query_params, $params->{'limit'};
1088 my $sth = $dbh->prepare( $statement );
1089 my $result = $sth->execute( @query_params );
1090 return $sth->fetchall_arrayref({});
1093 =head2 GetMessageTransportTypes
1095 my @mtt = GetMessageTransportTypes();
1097 returns an arrayref of transport types
1099 =cut
1101 sub GetMessageTransportTypes {
1102 my $dbh = C4::Context->dbh();
1103 my $mtts = $dbh->selectcol_arrayref("
1104 SELECT message_transport_type
1105 FROM message_transport_types
1106 ORDER BY message_transport_type
1108 return $mtts;
1111 =head2 GetMessage
1113 my $message = C4::Letters::Message($message_id);
1115 =cut
1117 sub GetMessage {
1118 my ( $message_id ) = @_;
1119 return unless $message_id;
1120 my $dbh = C4::Context->dbh;
1121 return $dbh->selectrow_hashref(q|
1122 SELECT message_id, borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type
1123 FROM message_queue
1124 WHERE message_id = ?
1125 |, {}, $message_id );
1128 =head2 ResendMessage
1130 Attempt to resend a message which has failed previously.
1132 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1134 Updates the message to 'pending' status so that
1135 it will be resent later on.
1137 returns 1 on success, 0 on failure, undef if no message was found
1139 =cut
1141 sub ResendMessage {
1142 my $message_id = shift;
1143 return unless $message_id;
1145 my $message = GetMessage( $message_id );
1146 return unless $message;
1147 my $rv = 0;
1148 if ( $message->{status} ne 'pending' ) {
1149 $rv = C4::Letters::_set_message_status({
1150 message_id => $message_id,
1151 status => 'pending',
1153 $rv = $rv > 0? 1: 0;
1154 # Clear destination email address to force address update
1155 _update_message_to_address( $message_id, undef ) if $rv &&
1156 $message->{message_transport_type} eq 'email';
1158 return $rv;
1161 =head2 _add_attachements
1163 named parameters:
1164 letter - the standard letter hashref
1165 attachments - listref of attachments. each attachment is a hashref of:
1166 type - the mime type, like 'text/plain'
1167 content - the actual attachment
1168 filename - the name of the attachment.
1169 message - a MIME::Lite object to attach these to.
1171 returns your letter object, with the content updated.
1173 =cut
1175 sub _add_attachments {
1176 my $params = shift;
1178 my $letter = $params->{'letter'};
1179 my $attachments = $params->{'attachments'};
1180 return $letter unless @$attachments;
1181 my $message = $params->{'message'};
1183 # First, we have to put the body in as the first attachment
1184 $message->attach(
1185 Type => $letter->{'content-type'} || 'TEXT',
1186 Data => $letter->{'is_html'}
1187 ? _wrap_html($letter->{'content'}, $letter->{'title'})
1188 : $letter->{'content'},
1191 foreach my $attachment ( @$attachments ) {
1192 $message->attach(
1193 Type => $attachment->{'type'},
1194 Data => $attachment->{'content'},
1195 Filename => $attachment->{'filename'},
1198 # we're forcing list context here to get the header, not the count back from grep.
1199 ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1200 $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1201 $letter->{'content'} = $message->body_as_string;
1203 return $letter;
1207 =head2 _get_unsent_messages
1209 This function's parameter hash reference takes the following
1210 optional named parameters:
1211 message_transport_type: method of message sending (e.g. email, sms, etc.)
1212 borrowernumber : who the message is to be sent
1213 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1214 limit : maximum number of messages to send
1216 This function returns an array of matching hash referenced rows from
1217 message_queue with some borrower information added.
1219 =cut
1221 sub _get_unsent_messages {
1222 my $params = shift;
1224 my $dbh = C4::Context->dbh();
1225 my $statement = qq{
1226 SELECT mq.message_id, mq.borrowernumber, mq.subject, mq.content, mq.message_transport_type, mq.status, mq.time_queued, mq.from_address, mq.to_address, mq.content_type, b.branchcode, mq.letter_code
1227 FROM message_queue mq
1228 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1229 WHERE status = ?
1232 my @query_params = ('pending');
1233 if ( ref $params ) {
1234 if ( $params->{'message_transport_type'} ) {
1235 $statement .= ' AND mq.message_transport_type = ? ';
1236 push @query_params, $params->{'message_transport_type'};
1238 if ( $params->{'borrowernumber'} ) {
1239 $statement .= ' AND mq.borrowernumber = ? ';
1240 push @query_params, $params->{'borrowernumber'};
1242 if ( $params->{'letter_code'} ) {
1243 $statement .= ' AND mq.letter_code = ? ';
1244 push @query_params, $params->{'letter_code'};
1246 if ( $params->{'type'} ) {
1247 $statement .= ' AND message_transport_type = ? ';
1248 push @query_params, $params->{'type'};
1250 if ( $params->{'limit'} ) {
1251 $statement .= ' limit ? ';
1252 push @query_params, $params->{'limit'};
1256 $debug and warn "_get_unsent_messages SQL: $statement";
1257 $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1258 my $sth = $dbh->prepare( $statement );
1259 my $result = $sth->execute( @query_params );
1260 return $sth->fetchall_arrayref({});
1263 sub _send_message_by_email {
1264 my $message = shift or return;
1265 my ($username, $password, $method) = @_;
1267 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1268 my $to_address = $message->{'to_address'};
1269 unless ($to_address) {
1270 unless ($patron) {
1271 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1272 _set_message_status( { message_id => $message->{'message_id'},
1273 status => 'failed' } );
1274 return;
1276 $to_address = $patron->notice_email_address;
1277 unless ($to_address) {
1278 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1279 # warning too verbose for this more common case?
1280 _set_message_status( { message_id => $message->{'message_id'},
1281 status => 'failed' } );
1282 return;
1286 my $utf8 = decode('MIME-Header', $message->{'subject'} );
1287 $message->{subject}= encode('MIME-Header', $utf8);
1288 my $subject = encode('UTF-8', $message->{'subject'});
1289 my $content = encode('UTF-8', $message->{'content'});
1290 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1291 my $is_html = $content_type =~ m/html/io;
1292 my $branch_email = undef;
1293 my $branch_replyto = undef;
1294 my $branch_returnpath = undef;
1295 if ($patron) {
1296 my $library = $patron->library;
1297 $branch_email = $library->branchemail;
1298 $branch_replyto = $library->branchreplyto;
1299 $branch_returnpath = $library->branchreturnpath;
1301 my $email = Koha::Email->new();
1302 my %sendmail_params = $email->create_message_headers(
1304 to => $to_address,
1305 from => $message->{'from_address'} || $branch_email,
1306 replyto => $branch_replyto,
1307 sender => $branch_returnpath,
1308 subject => $subject,
1309 message => $is_html ? _wrap_html( $content, $subject ) : $content,
1310 contenttype => $content_type
1314 $sendmail_params{'Auth'} = {user => $username, pass => $password, method => $method} if $username;
1315 if ( my $bcc = C4::Context->preference('NoticeBcc') ) {
1316 $sendmail_params{ Bcc } = $bcc;
1319 _update_message_to_address($message->{'message_id'},$to_address) unless $message->{to_address}; #if initial message address was empty, coming here means that a to address was found and queue should be updated
1321 if ( Mail::Sendmail::sendmail( %sendmail_params ) ) {
1322 _set_message_status( { message_id => $message->{'message_id'},
1323 status => 'sent' } );
1324 return 1;
1325 } else {
1326 _set_message_status( { message_id => $message->{'message_id'},
1327 status => 'failed' } );
1328 carp $Mail::Sendmail::error;
1329 return;
1333 sub _wrap_html {
1334 my ($content, $title) = @_;
1336 my $css = C4::Context->preference("NoticeCSS") || '';
1337 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1338 return <<EOS;
1339 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1340 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1341 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1342 <head>
1343 <title>$title</title>
1344 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1345 $css
1346 </head>
1347 <body>
1348 $content
1349 </body>
1350 </html>
1354 sub _is_duplicate {
1355 my ( $message ) = @_;
1356 my $dbh = C4::Context->dbh;
1357 my $count = $dbh->selectrow_array(q|
1358 SELECT COUNT(*)
1359 FROM message_queue
1360 WHERE message_transport_type = ?
1361 AND borrowernumber = ?
1362 AND letter_code = ?
1363 AND CAST(time_queued AS date) = CAST(NOW() AS date)
1364 AND status="sent"
1365 AND content = ?
1366 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1367 return $count;
1370 sub _send_message_by_sms {
1371 my $message = shift or return;
1372 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1374 unless ( $patron and $patron->smsalertnumber ) {
1375 _set_message_status( { message_id => $message->{'message_id'},
1376 status => 'failed' } );
1377 return;
1380 if ( _is_duplicate( $message ) ) {
1381 _set_message_status( { message_id => $message->{'message_id'},
1382 status => 'failed' } );
1383 return;
1386 my $success = C4::SMS->send_sms( { destination => $patron->smsalertnumber,
1387 message => $message->{'content'},
1388 } );
1389 _set_message_status( { message_id => $message->{'message_id'},
1390 status => ($success ? 'sent' : 'failed') } );
1391 return $success;
1394 sub _update_message_to_address {
1395 my ($id, $to)= @_;
1396 my $dbh = C4::Context->dbh();
1397 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1400 sub _update_message_from_address {
1401 my ($message_id, $from_address) = @_;
1402 my $dbh = C4::Context->dbh();
1403 $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1406 sub _set_message_status {
1407 my $params = shift or return;
1409 foreach my $required_parameter ( qw( message_id status ) ) {
1410 return unless exists $params->{ $required_parameter };
1413 my $dbh = C4::Context->dbh();
1414 my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1415 my $sth = $dbh->prepare( $statement );
1416 my $result = $sth->execute( $params->{'status'},
1417 $params->{'message_id'} );
1418 return $result;
1421 sub _process_tt {
1422 my ( $params ) = @_;
1424 my $content = $params->{content};
1425 my $tables = $params->{tables};
1426 my $loops = $params->{loops};
1427 my $substitute = $params->{substitute} || {};
1429 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1430 my $template = Template->new(
1432 EVAL_PERL => 1,
1433 ABSOLUTE => 1,
1434 PLUGIN_BASE => 'Koha::Template::Plugin',
1435 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1436 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1437 FILTERS => {},
1438 ENCODING => 'UTF-8',
1440 ) or die Template->error();
1442 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
1444 $content = add_tt_filters( $content );
1445 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1447 my $output;
1448 $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1450 return $output;
1453 sub _get_tt_params {
1454 my ($tables, $is_a_loop) = @_;
1456 my $params;
1457 $is_a_loop ||= 0;
1459 my $config = {
1460 article_requests => {
1461 module => 'Koha::ArticleRequests',
1462 singular => 'article_request',
1463 plural => 'article_requests',
1464 pk => 'id',
1466 biblio => {
1467 module => 'Koha::Biblios',
1468 singular => 'biblio',
1469 plural => 'biblios',
1470 pk => 'biblionumber',
1472 biblioitems => {
1473 module => 'Koha::Biblioitems',
1474 singular => 'biblioitem',
1475 plural => 'biblioitems',
1476 pk => 'biblioitemnumber',
1478 borrowers => {
1479 module => 'Koha::Patrons',
1480 singular => 'borrower',
1481 plural => 'borrowers',
1482 pk => 'borrowernumber',
1484 branches => {
1485 module => 'Koha::Libraries',
1486 singular => 'branch',
1487 plural => 'branches',
1488 pk => 'branchcode',
1490 items => {
1491 module => 'Koha::Items',
1492 singular => 'item',
1493 plural => 'items',
1494 pk => 'itemnumber',
1496 opac_news => {
1497 module => 'Koha::News',
1498 singular => 'news',
1499 plural => 'news',
1500 pk => 'idnew',
1502 aqorders => {
1503 module => 'Koha::Acquisition::Orders',
1504 singular => 'order',
1505 plural => 'orders',
1506 pk => 'ordernumber',
1508 reserves => {
1509 module => 'Koha::Holds',
1510 singular => 'hold',
1511 plural => 'holds',
1512 fk => [ 'borrowernumber', 'biblionumber' ],
1514 serial => {
1515 module => 'Koha::Serials',
1516 singular => 'serial',
1517 plural => 'serials',
1518 pk => 'serialid',
1520 subscription => {
1521 module => 'Koha::Subscriptions',
1522 singular => 'subscription',
1523 plural => 'subscriptions',
1524 pk => 'subscriptionid',
1526 suggestions => {
1527 module => 'Koha::Suggestions',
1528 singular => 'suggestion',
1529 plural => 'suggestions',
1530 pk => 'suggestionid',
1532 issues => {
1533 module => 'Koha::Checkouts',
1534 singular => 'checkout',
1535 plural => 'checkouts',
1536 fk => 'itemnumber',
1538 old_issues => {
1539 module => 'Koha::Old::Checkouts',
1540 singular => 'old_checkout',
1541 plural => 'old_checkouts',
1542 fk => 'itemnumber',
1544 overdues => {
1545 module => 'Koha::Checkouts',
1546 singular => 'overdue',
1547 plural => 'overdues',
1548 fk => 'itemnumber',
1550 borrower_modifications => {
1551 module => 'Koha::Patron::Modifications',
1552 singular => 'patron_modification',
1553 plural => 'patron_modifications',
1554 fk => 'verification_token',
1558 foreach my $table ( keys %$tables ) {
1559 next unless $config->{$table};
1561 my $ref = ref( $tables->{$table} ) || q{};
1562 my $module = $config->{$table}->{module};
1564 if ( can_load( modules => { $module => undef } ) ) {
1565 my $pk = $config->{$table}->{pk};
1566 my $fk = $config->{$table}->{fk};
1568 if ( $is_a_loop ) {
1569 my $values = $tables->{$table} || [];
1570 unless ( ref( $values ) eq 'ARRAY' ) {
1571 croak "ERROR processing table $table. Wrong API call.";
1573 my $key = $pk ? $pk : $fk;
1574 # $key does not come from user input
1575 my $objects = $module->search(
1576 { $key => $values },
1578 # We want to retrieve the data in the same order
1579 # FIXME MySQLism
1580 # field is a MySQLism, but they are no other way to do it
1581 # To be generic we could do it in perl, but we will need to fetch
1582 # all the data then order them
1583 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1586 $params->{ $config->{$table}->{plural} } = $objects;
1588 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1589 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1590 my $object;
1591 if ( $fk ) { # Using a foreign key for lookup
1592 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1593 my $search;
1594 foreach my $key ( @$fk ) {
1595 $search->{$key} = $id->{$key};
1597 $object = $module->search( $search )->last();
1598 } else { # Foreign key is single column
1599 $object = $module->search( { $fk => $id } )->last();
1601 } else { # using the table's primary key for lookup
1602 $object = $module->find($id);
1604 $params->{ $config->{$table}->{singular} } = $object;
1606 else { # $ref eq 'ARRAY'
1607 my $object;
1608 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1609 $object = $module->search( { $pk => $tables->{$table} } )->last();
1611 else { # Params are mutliple foreign keys
1612 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1614 $params->{ $config->{$table}->{singular} } = $object;
1617 else {
1618 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1622 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1624 return $params;
1627 =head3 add_tt_filters
1629 $content = add_tt_filters( $content );
1631 Add TT filters to some specific fields if needed.
1633 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1635 =cut
1637 sub add_tt_filters {
1638 my ( $content ) = @_;
1639 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1640 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1641 return $content;
1644 =head2 get_item_content
1646 my $item = Koha::Items->find(...)->unblessed;
1647 my @item_content_fields = qw( date_due title barcode author itemnumber );
1648 my $item_content = C4::Letters::get_item_content({
1649 item => $item,
1650 item_content_fields => \@item_content_fields
1653 This function generates a tab-separated list of values for the passed item. Dates
1654 are formatted following the current setup.
1656 =cut
1658 sub get_item_content {
1659 my ( $params ) = @_;
1660 my $item = $params->{item};
1661 my $dateonly = $params->{dateonly} || 0;
1662 my $item_content_fields = $params->{item_content_fields} || [];
1664 return unless $item;
1666 my @item_info = map {
1667 $_ =~ /^date|date$/
1668 ? eval {
1669 output_pref(
1670 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1672 : $item->{$_}
1673 || ''
1674 } @$item_content_fields;
1675 return join( "\t", @item_info ) . "\n";
1679 __END__