Bug 22071: (follow-up) Add POD for validate_query_parameters
[koha.git] / C4 / Letters.pm
blob0f2378d6603d82d47405e00620055955788841c1
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 lang => $externalid->{lang} || 'default',
531 tables => {
532 'branches' => $library,
533 'borrowers' => $externalid->{'borrowernumber'},
535 substitute => { 'borrowers.password' => $externalid->{'password'} },
536 want_librarian => 1,
537 ) or return;
538 return { error => "no_email" } unless $externalid->{'emailaddr'};
539 my $email = Koha::Email->new();
540 my %mail = $email->create_message_headers(
542 to => $externalid->{'emailaddr'},
543 from => $library->{branchemail},
544 replyto => $library->{branchreplyto},
545 sender => $library->{branchreturnpath},
546 subject => Encode::encode( "UTF-8", "" . $letter->{'title'} ),
547 message => $letter->{'is_html'}
548 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
549 Encode::encode( "UTF-8", "" . $letter->{'title'} ) )
550 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
551 contenttype => $letter->{'is_html'}
552 ? 'text/html; charset="utf-8"'
553 : 'text/plain; charset="utf-8"',
556 unless( Mail::Sendmail::sendmail(%mail) ) {
557 carp $Mail::Sendmail::error;
558 return { error => $Mail::Sendmail::error };
562 # If we come here, return an OK status
563 return 1;
566 =head2 GetPreparedLetter( %params )
568 %params hash:
569 module => letter module, mandatory
570 letter_code => letter code, mandatory
571 branchcode => for letter selection, if missing default system letter taken
572 tables => a hashref with table names as keys. Values are either:
573 - a scalar - primary key value
574 - an arrayref - primary key values
575 - a hashref - full record
576 substitute => custom substitution key/value pairs
577 repeat => records to be substituted on consecutive lines:
578 - an arrayref - tries to guess what needs substituting by
579 taking remaining << >> tokensr; not recommended
580 - a hashref token => @tables - replaces <token> << >> << >> </token>
581 subtemplate for each @tables row; table is a hashref as above
582 want_librarian => boolean, if set to true triggers librarian details
583 substitution from the userenv
584 Return value:
585 letter fields hashref (title & content useful)
587 =cut
589 sub GetPreparedLetter {
590 my %params = @_;
592 my $letter = $params{letter};
594 unless ( $letter ) {
595 my $module = $params{module} or croak "No module";
596 my $letter_code = $params{letter_code} or croak "No letter_code";
597 my $branchcode = $params{branchcode} || '';
598 my $mtt = $params{message_transport_type} || 'email';
599 my $lang = $params{lang} || 'default';
601 $letter = getletter( $module, $letter_code, $branchcode, $mtt, $lang );
603 unless ( $letter ) {
604 $letter = getletter( $module, $letter_code, $branchcode, $mtt, 'default' )
605 or warn( "No $module $letter_code letter transported by " . $mtt ),
606 return;
610 my $tables = $params{tables} || {};
611 my $substitute = $params{substitute} || {};
612 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
613 my $repeat = $params{repeat};
614 %$tables || %$substitute || $repeat || %$loops
615 or carp( "ERROR: nothing to substitute - both 'tables', 'loops' and 'substitute' are empty" ),
616 return;
617 my $want_librarian = $params{want_librarian};
619 if (%$substitute) {
620 while ( my ($token, $val) = each %$substitute ) {
621 if ( $token eq 'items.content' ) {
622 $val =~ s|\n|<br/>|g if $letter->{is_html};
625 $letter->{title} =~ s/<<$token>>/$val/g;
626 $letter->{content} =~ s/<<$token>>/$val/g;
630 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
631 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
633 if ($want_librarian) {
634 # parsing librarian name
635 my $userenv = C4::Context->userenv;
636 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
637 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
638 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
641 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
643 if ($repeat) {
644 if (ref ($repeat) eq 'ARRAY' ) {
645 $repeat_no_enclosing_tags = $repeat;
646 } else {
647 $repeat_enclosing_tags = $repeat;
651 if ($repeat_enclosing_tags) {
652 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
653 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
654 my $subcontent = $1;
655 my @lines = map {
656 my %subletter = ( title => '', content => $subcontent );
657 _substitute_tables( \%subletter, $_ );
658 $subletter{content};
659 } @$tag_tables;
660 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
665 if (%$tables) {
666 _substitute_tables( $letter, $tables );
669 if ($repeat_no_enclosing_tags) {
670 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
671 my $line = $&;
672 my $i = 1;
673 my @lines = map {
674 my $c = $line;
675 $c =~ s/<<count>>/$i/go;
676 foreach my $field ( keys %{$_} ) {
677 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
679 $i++;
681 } @$repeat_no_enclosing_tags;
683 my $replaceby = join( "\n", @lines );
684 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
688 $letter->{content} = _process_tt(
690 content => $letter->{content},
691 tables => $tables,
692 loops => $loops,
693 substitute => $substitute,
697 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
699 return $letter;
702 sub _substitute_tables {
703 my ( $letter, $tables ) = @_;
704 while ( my ($table, $param) = each %$tables ) {
705 next unless $param;
707 my $ref = ref $param;
709 my $values;
710 if ($ref && $ref eq 'HASH') {
711 $values = $param;
713 else {
714 my $sth = _parseletter_sth($table);
715 unless ($sth) {
716 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
717 return;
719 $sth->execute( $ref ? @$param : $param );
721 $values = $sth->fetchrow_hashref;
722 $sth->finish();
725 _parseletter ( $letter, $table, $values );
729 sub _parseletter_sth {
730 my $table = shift;
731 my $sth;
732 unless ($table) {
733 carp "ERROR: _parseletter_sth() called without argument (table)";
734 return;
736 # NOTE: we used to check whether we had a statement handle cached in
737 # a %handles module-level variable. This was a dumb move and
738 # broke things for the rest of us. prepare_cached is a better
739 # way to cache statement handles anyway.
740 my $query =
741 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
742 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
743 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
744 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
745 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE itemnumber = ? ORDER BY timestamp DESC LIMIT 1" :
746 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
747 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
748 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
749 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
750 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
751 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
752 ($table eq 'opac_news' ) ? "SELECT * FROM $table WHERE idnew = ?" :
753 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
754 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
755 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
756 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
757 undef ;
758 unless ($query) {
759 warn "ERROR: No _parseletter_sth query for table '$table'";
760 return; # nothing to get
762 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
763 warn "ERROR: Failed to prepare query: '$query'";
764 return;
766 return $sth; # now cache is populated for that $table
769 =head2 _parseletter($letter, $table, $values)
771 parameters :
772 - $letter : a hash to letter fields (title & content useful)
773 - $table : the Koha table to parse.
774 - $values_in : table record hashref
775 parse all fields from a table, and replace values in title & content with the appropriate value
776 (not exported sub, used only internally)
778 =cut
780 sub _parseletter {
781 my ( $letter, $table, $values_in ) = @_;
783 # Work on a local copy of $values_in (passed by reference) to avoid side effects
784 # in callers ( by changing / formatting values )
785 my $values = $values_in ? { %$values_in } : {};
787 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
788 $values->{'dateexpiry'} = output_pref({ str => $values->{dateexpiry}, dateonly => 1 });
791 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
792 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
795 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
796 my $todaysdate = output_pref( DateTime->now() );
797 $letter->{content} =~ s/<<today>>/$todaysdate/go;
800 while ( my ($field, $val) = each %$values ) {
801 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
802 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
803 #Therefore adding the test on biblio. This includes biblioitems,
804 #but excludes items. Removed unneeded global and lookahead.
806 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
807 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
808 $val = $av->count ? $av->next->lib : '';
811 # Dates replacement
812 my $replacedby = defined ($val) ? $val : '';
813 if ( $replacedby
814 and not $replacedby =~ m|0000-00-00|
815 and not $replacedby =~ m|9999-12-31|
816 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
818 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
819 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
820 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
822 for my $letter_field ( qw( title content ) ) {
823 my $filter_string_used = q{};
824 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
825 # We overwrite $dateonly if the filter exists and we have a time in the datetime
826 $filter_string_used = $1 || q{};
827 $dateonly = $1 unless $dateonly;
829 my $replacedby_date = eval {
830 output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
833 if ( $letter->{ $letter_field } ) {
834 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
835 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
839 # Other fields replacement
840 else {
841 for my $letter_field ( qw( title content ) ) {
842 if ( $letter->{ $letter_field } ) {
843 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
844 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
850 if ($table eq 'borrowers' && $letter->{content}) {
851 if ( my $attributes = GetBorrowerAttributes($values->{borrowernumber}) ) {
852 my %attr;
853 foreach (@$attributes) {
854 my $code = $_->{code};
855 my $val = $_->{value_description} || $_->{value};
856 $val =~ s/\p{P}(?=$)//g if $val;
857 next unless $val gt '';
858 $attr{$code} ||= [];
859 push @{ $attr{$code} }, $val;
861 while ( my ($code, $val_ar) = each %attr ) {
862 my $replacefield = "<<borrower-attribute:$code>>";
863 my $replacedby = join ',', @$val_ar;
864 $letter->{content} =~ s/$replacefield/$replacedby/g;
868 return $letter;
871 =head2 EnqueueLetter
873 my $success = EnqueueLetter( { letter => $letter,
874 borrowernumber => '12', message_transport_type => 'email' } )
876 places a letter in the message_queue database table, which will
877 eventually get processed (sent) by the process_message_queue.pl
878 cronjob when it calls SendQueuedMessages.
880 return message_id on success
882 =cut
884 sub EnqueueLetter {
885 my $params = shift or return;
887 return unless exists $params->{'letter'};
888 # return unless exists $params->{'borrowernumber'};
889 return unless exists $params->{'message_transport_type'};
891 my $content = $params->{letter}->{content};
892 $content =~ s/\s+//g if(defined $content);
893 if ( not defined $content or $content eq '' ) {
894 warn "Trying to add an empty message to the message queue" if $debug;
895 return;
898 # If we have any attachments we should encode then into the body.
899 if ( $params->{'attachments'} ) {
900 $params->{'letter'} = _add_attachments(
901 { letter => $params->{'letter'},
902 attachments => $params->{'attachments'},
903 message => MIME::Lite->new( Type => 'multipart/mixed' ),
908 my $dbh = C4::Context->dbh();
909 my $statement = << 'ENDSQL';
910 INSERT INTO message_queue
911 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type )
912 VALUES
913 ( ?, ?, ?, ?, ?, ?, ?, NOW(), ?, ?, ? )
914 ENDSQL
916 my $sth = $dbh->prepare($statement);
917 my $result = $sth->execute(
918 $params->{'borrowernumber'}, # borrowernumber
919 $params->{'letter'}->{'title'}, # subject
920 $params->{'letter'}->{'content'}, # content
921 $params->{'letter'}->{'metadata'} || '', # metadata
922 $params->{'letter'}->{'code'} || '', # letter_code
923 $params->{'message_transport_type'}, # message_transport_type
924 'pending', # status
925 $params->{'to_address'}, # to_address
926 $params->{'from_address'}, # from_address
927 $params->{'letter'}->{'content-type'}, # content_type
929 return $dbh->last_insert_id(undef,undef,'message_queue', undef);
932 =head2 SendQueuedMessages ([$hashref])
934 my $sent = SendQueuedMessages({
935 letter_code => $letter_code,
936 borrowernumber => $who_letter_is_for,
937 limit => 50,
938 verbose => 1,
939 type => 'sms',
942 Sends all of the 'pending' items in the message queue, unless
943 parameters are passed.
945 The letter_code, borrowernumber and limit parameters are used
946 to build a parameter set for _get_unsent_messages, thus limiting
947 which pending messages will be processed. They are all optional.
949 The verbose parameter can be used to generate debugging output.
950 It is also optional.
952 Returns number of messages sent.
954 =cut
956 sub SendQueuedMessages {
957 my $params = shift;
959 my $which_unsent_messages = {
960 'limit' => $params->{'limit'} // 0,
961 'borrowernumber' => $params->{'borrowernumber'} // q{},
962 'letter_code' => $params->{'letter_code'} // q{},
963 'type' => $params->{'type'} // q{},
965 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
966 MESSAGE: foreach my $message ( @$unsent_messages ) {
967 my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
968 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
969 $message_object->make_column_dirty('status');
970 return unless $message_object->store;
972 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
973 warn sprintf( 'sending %s message to patron: %s',
974 $message->{'message_transport_type'},
975 $message->{'borrowernumber'} || 'Admin' )
976 if $params->{'verbose'} or $debug;
977 # This is just begging for subclassing
978 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
979 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
980 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
982 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
983 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
984 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
985 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
986 unless ( $sms_provider ) {
987 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
988 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
989 next MESSAGE;
991 unless ( $patron->smsalertnumber ) {
992 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
993 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
994 next MESSAGE;
996 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
997 $message->{to_address} .= '@' . $sms_provider->domain();
999 # Check for possible from_address override
1000 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1001 if ($from_address && $message->{from_address} ne $from_address) {
1002 $message->{from_address} = $from_address;
1003 _update_message_from_address($message->{'message_id'}, $message->{from_address});
1006 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1007 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1008 } else {
1009 _send_message_by_sms( $message );
1013 return scalar( @$unsent_messages );
1016 =head2 GetRSSMessages
1018 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1020 returns a listref of all queued RSS messages for a particular person.
1022 =cut
1024 sub GetRSSMessages {
1025 my $params = shift;
1027 return unless $params;
1028 return unless ref $params;
1029 return unless $params->{'borrowernumber'};
1031 return _get_unsent_messages( { message_transport_type => 'rss',
1032 limit => $params->{'limit'},
1033 borrowernumber => $params->{'borrowernumber'}, } );
1036 =head2 GetPrintMessages
1038 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1040 Returns a arrayref of all queued print messages (optionally, for a particular
1041 person).
1043 =cut
1045 sub GetPrintMessages {
1046 my $params = shift || {};
1048 return _get_unsent_messages( { message_transport_type => 'print',
1049 borrowernumber => $params->{'borrowernumber'},
1050 } );
1053 =head2 GetQueuedMessages ([$hashref])
1055 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1057 fetches messages out of the message queue.
1059 returns:
1060 list of hashes, each has represents a message in the message queue.
1062 =cut
1064 sub GetQueuedMessages {
1065 my $params = shift;
1067 my $dbh = C4::Context->dbh();
1068 my $statement = << 'ENDSQL';
1069 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued
1070 FROM message_queue
1071 ENDSQL
1073 my @query_params;
1074 my @whereclauses;
1075 if ( exists $params->{'borrowernumber'} ) {
1076 push @whereclauses, ' borrowernumber = ? ';
1077 push @query_params, $params->{'borrowernumber'};
1080 if ( @whereclauses ) {
1081 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1084 if ( defined $params->{'limit'} ) {
1085 $statement .= ' LIMIT ? ';
1086 push @query_params, $params->{'limit'};
1089 my $sth = $dbh->prepare( $statement );
1090 my $result = $sth->execute( @query_params );
1091 return $sth->fetchall_arrayref({});
1094 =head2 GetMessageTransportTypes
1096 my @mtt = GetMessageTransportTypes();
1098 returns an arrayref of transport types
1100 =cut
1102 sub GetMessageTransportTypes {
1103 my $dbh = C4::Context->dbh();
1104 my $mtts = $dbh->selectcol_arrayref("
1105 SELECT message_transport_type
1106 FROM message_transport_types
1107 ORDER BY message_transport_type
1109 return $mtts;
1112 =head2 GetMessage
1114 my $message = C4::Letters::Message($message_id);
1116 =cut
1118 sub GetMessage {
1119 my ( $message_id ) = @_;
1120 return unless $message_id;
1121 my $dbh = C4::Context->dbh;
1122 return $dbh->selectrow_hashref(q|
1123 SELECT message_id, borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type
1124 FROM message_queue
1125 WHERE message_id = ?
1126 |, {}, $message_id );
1129 =head2 ResendMessage
1131 Attempt to resend a message which has failed previously.
1133 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1135 Updates the message to 'pending' status so that
1136 it will be resent later on.
1138 returns 1 on success, 0 on failure, undef if no message was found
1140 =cut
1142 sub ResendMessage {
1143 my $message_id = shift;
1144 return unless $message_id;
1146 my $message = GetMessage( $message_id );
1147 return unless $message;
1148 my $rv = 0;
1149 if ( $message->{status} ne 'pending' ) {
1150 $rv = C4::Letters::_set_message_status({
1151 message_id => $message_id,
1152 status => 'pending',
1154 $rv = $rv > 0? 1: 0;
1155 # Clear destination email address to force address update
1156 _update_message_to_address( $message_id, undef ) if $rv &&
1157 $message->{message_transport_type} eq 'email';
1159 return $rv;
1162 =head2 _add_attachements
1164 named parameters:
1165 letter - the standard letter hashref
1166 attachments - listref of attachments. each attachment is a hashref of:
1167 type - the mime type, like 'text/plain'
1168 content - the actual attachment
1169 filename - the name of the attachment.
1170 message - a MIME::Lite object to attach these to.
1172 returns your letter object, with the content updated.
1174 =cut
1176 sub _add_attachments {
1177 my $params = shift;
1179 my $letter = $params->{'letter'};
1180 my $attachments = $params->{'attachments'};
1181 return $letter unless @$attachments;
1182 my $message = $params->{'message'};
1184 # First, we have to put the body in as the first attachment
1185 $message->attach(
1186 Type => $letter->{'content-type'} || 'TEXT',
1187 Data => $letter->{'is_html'}
1188 ? _wrap_html($letter->{'content'}, $letter->{'title'})
1189 : $letter->{'content'},
1192 foreach my $attachment ( @$attachments ) {
1193 $message->attach(
1194 Type => $attachment->{'type'},
1195 Data => $attachment->{'content'},
1196 Filename => $attachment->{'filename'},
1199 # we're forcing list context here to get the header, not the count back from grep.
1200 ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1201 $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1202 $letter->{'content'} = $message->body_as_string;
1204 return $letter;
1208 =head2 _get_unsent_messages
1210 This function's parameter hash reference takes the following
1211 optional named parameters:
1212 message_transport_type: method of message sending (e.g. email, sms, etc.)
1213 borrowernumber : who the message is to be sent
1214 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1215 limit : maximum number of messages to send
1217 This function returns an array of matching hash referenced rows from
1218 message_queue with some borrower information added.
1220 =cut
1222 sub _get_unsent_messages {
1223 my $params = shift;
1225 my $dbh = C4::Context->dbh();
1226 my $statement = qq{
1227 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
1228 FROM message_queue mq
1229 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1230 WHERE status = ?
1233 my @query_params = ('pending');
1234 if ( ref $params ) {
1235 if ( $params->{'message_transport_type'} ) {
1236 $statement .= ' AND mq.message_transport_type = ? ';
1237 push @query_params, $params->{'message_transport_type'};
1239 if ( $params->{'borrowernumber'} ) {
1240 $statement .= ' AND mq.borrowernumber = ? ';
1241 push @query_params, $params->{'borrowernumber'};
1243 if ( $params->{'letter_code'} ) {
1244 $statement .= ' AND mq.letter_code = ? ';
1245 push @query_params, $params->{'letter_code'};
1247 if ( $params->{'type'} ) {
1248 $statement .= ' AND message_transport_type = ? ';
1249 push @query_params, $params->{'type'};
1251 if ( $params->{'limit'} ) {
1252 $statement .= ' limit ? ';
1253 push @query_params, $params->{'limit'};
1257 $debug and warn "_get_unsent_messages SQL: $statement";
1258 $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1259 my $sth = $dbh->prepare( $statement );
1260 my $result = $sth->execute( @query_params );
1261 return $sth->fetchall_arrayref({});
1264 sub _send_message_by_email {
1265 my $message = shift or return;
1266 my ($username, $password, $method) = @_;
1268 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1269 my $to_address = $message->{'to_address'};
1270 unless ($to_address) {
1271 unless ($patron) {
1272 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1273 _set_message_status( { message_id => $message->{'message_id'},
1274 status => 'failed' } );
1275 return;
1277 $to_address = $patron->notice_email_address;
1278 unless ($to_address) {
1279 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1280 # warning too verbose for this more common case?
1281 _set_message_status( { message_id => $message->{'message_id'},
1282 status => 'failed' } );
1283 return;
1287 my $utf8 = decode('MIME-Header', $message->{'subject'} );
1288 $message->{subject}= encode('MIME-Header', $utf8);
1289 my $subject = encode('UTF-8', $message->{'subject'});
1290 my $content = encode('UTF-8', $message->{'content'});
1291 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1292 my $is_html = $content_type =~ m/html/io;
1293 my $branch_email = undef;
1294 my $branch_replyto = undef;
1295 my $branch_returnpath = undef;
1296 if ($patron) {
1297 my $library = $patron->library;
1298 $branch_email = $library->branchemail;
1299 $branch_replyto = $library->branchreplyto;
1300 $branch_returnpath = $library->branchreturnpath;
1302 my $email = Koha::Email->new();
1303 my %sendmail_params = $email->create_message_headers(
1305 to => $to_address,
1306 from => $message->{'from_address'} || $branch_email,
1307 replyto => $branch_replyto,
1308 sender => $branch_returnpath,
1309 subject => $subject,
1310 message => $is_html ? _wrap_html( $content, $subject ) : $content,
1311 contenttype => $content_type
1315 $sendmail_params{'Auth'} = {user => $username, pass => $password, method => $method} if $username;
1316 if ( my $bcc = C4::Context->preference('NoticeBcc') ) {
1317 $sendmail_params{ Bcc } = $bcc;
1320 _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
1322 if ( Mail::Sendmail::sendmail( %sendmail_params ) ) {
1323 _set_message_status( { message_id => $message->{'message_id'},
1324 status => 'sent' } );
1325 return 1;
1326 } else {
1327 _set_message_status( { message_id => $message->{'message_id'},
1328 status => 'failed' } );
1329 carp $Mail::Sendmail::error;
1330 return;
1334 sub _wrap_html {
1335 my ($content, $title) = @_;
1337 my $css = C4::Context->preference("NoticeCSS") || '';
1338 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1339 return <<EOS;
1340 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1341 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1342 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1343 <head>
1344 <title>$title</title>
1345 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1346 $css
1347 </head>
1348 <body>
1349 $content
1350 </body>
1351 </html>
1355 sub _is_duplicate {
1356 my ( $message ) = @_;
1357 my $dbh = C4::Context->dbh;
1358 my $count = $dbh->selectrow_array(q|
1359 SELECT COUNT(*)
1360 FROM message_queue
1361 WHERE message_transport_type = ?
1362 AND borrowernumber = ?
1363 AND letter_code = ?
1364 AND CAST(time_queued AS date) = CAST(NOW() AS date)
1365 AND status="sent"
1366 AND content = ?
1367 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1368 return $count;
1371 sub _send_message_by_sms {
1372 my $message = shift or return;
1373 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1375 unless ( $patron and $patron->smsalertnumber ) {
1376 _set_message_status( { message_id => $message->{'message_id'},
1377 status => 'failed' } );
1378 return;
1381 if ( _is_duplicate( $message ) ) {
1382 _set_message_status( { message_id => $message->{'message_id'},
1383 status => 'failed' } );
1384 return;
1387 my $success = C4::SMS->send_sms( { destination => $patron->smsalertnumber,
1388 message => $message->{'content'},
1389 } );
1390 _set_message_status( { message_id => $message->{'message_id'},
1391 status => ($success ? 'sent' : 'failed') } );
1392 return $success;
1395 sub _update_message_to_address {
1396 my ($id, $to)= @_;
1397 my $dbh = C4::Context->dbh();
1398 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1401 sub _update_message_from_address {
1402 my ($message_id, $from_address) = @_;
1403 my $dbh = C4::Context->dbh();
1404 $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1407 sub _set_message_status {
1408 my $params = shift or return;
1410 foreach my $required_parameter ( qw( message_id status ) ) {
1411 return unless exists $params->{ $required_parameter };
1414 my $dbh = C4::Context->dbh();
1415 my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1416 my $sth = $dbh->prepare( $statement );
1417 my $result = $sth->execute( $params->{'status'},
1418 $params->{'message_id'} );
1419 return $result;
1422 sub _process_tt {
1423 my ( $params ) = @_;
1425 my $content = $params->{content};
1426 my $tables = $params->{tables};
1427 my $loops = $params->{loops};
1428 my $substitute = $params->{substitute} || {};
1430 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1431 my $template = Template->new(
1433 EVAL_PERL => 1,
1434 ABSOLUTE => 1,
1435 PLUGIN_BASE => 'Koha::Template::Plugin',
1436 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1437 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1438 FILTERS => {},
1439 ENCODING => 'UTF-8',
1441 ) or die Template->error();
1443 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
1445 $content = add_tt_filters( $content );
1446 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1448 my $output;
1449 $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1451 return $output;
1454 sub _get_tt_params {
1455 my ($tables, $is_a_loop) = @_;
1457 my $params;
1458 $is_a_loop ||= 0;
1460 my $config = {
1461 article_requests => {
1462 module => 'Koha::ArticleRequests',
1463 singular => 'article_request',
1464 plural => 'article_requests',
1465 pk => 'id',
1467 biblio => {
1468 module => 'Koha::Biblios',
1469 singular => 'biblio',
1470 plural => 'biblios',
1471 pk => 'biblionumber',
1473 biblioitems => {
1474 module => 'Koha::Biblioitems',
1475 singular => 'biblioitem',
1476 plural => 'biblioitems',
1477 pk => 'biblioitemnumber',
1479 borrowers => {
1480 module => 'Koha::Patrons',
1481 singular => 'borrower',
1482 plural => 'borrowers',
1483 pk => 'borrowernumber',
1485 branches => {
1486 module => 'Koha::Libraries',
1487 singular => 'branch',
1488 plural => 'branches',
1489 pk => 'branchcode',
1491 items => {
1492 module => 'Koha::Items',
1493 singular => 'item',
1494 plural => 'items',
1495 pk => 'itemnumber',
1497 opac_news => {
1498 module => 'Koha::News',
1499 singular => 'news',
1500 plural => 'news',
1501 pk => 'idnew',
1503 aqorders => {
1504 module => 'Koha::Acquisition::Orders',
1505 singular => 'order',
1506 plural => 'orders',
1507 pk => 'ordernumber',
1509 reserves => {
1510 module => 'Koha::Holds',
1511 singular => 'hold',
1512 plural => 'holds',
1513 fk => [ 'borrowernumber', 'biblionumber' ],
1515 serial => {
1516 module => 'Koha::Serials',
1517 singular => 'serial',
1518 plural => 'serials',
1519 pk => 'serialid',
1521 subscription => {
1522 module => 'Koha::Subscriptions',
1523 singular => 'subscription',
1524 plural => 'subscriptions',
1525 pk => 'subscriptionid',
1527 suggestions => {
1528 module => 'Koha::Suggestions',
1529 singular => 'suggestion',
1530 plural => 'suggestions',
1531 pk => 'suggestionid',
1533 issues => {
1534 module => 'Koha::Checkouts',
1535 singular => 'checkout',
1536 plural => 'checkouts',
1537 fk => 'itemnumber',
1539 old_issues => {
1540 module => 'Koha::Old::Checkouts',
1541 singular => 'old_checkout',
1542 plural => 'old_checkouts',
1543 fk => 'itemnumber',
1545 overdues => {
1546 module => 'Koha::Checkouts',
1547 singular => 'overdue',
1548 plural => 'overdues',
1549 fk => 'itemnumber',
1551 borrower_modifications => {
1552 module => 'Koha::Patron::Modifications',
1553 singular => 'patron_modification',
1554 plural => 'patron_modifications',
1555 fk => 'verification_token',
1559 foreach my $table ( keys %$tables ) {
1560 next unless $config->{$table};
1562 my $ref = ref( $tables->{$table} ) || q{};
1563 my $module = $config->{$table}->{module};
1565 if ( can_load( modules => { $module => undef } ) ) {
1566 my $pk = $config->{$table}->{pk};
1567 my $fk = $config->{$table}->{fk};
1569 if ( $is_a_loop ) {
1570 my $values = $tables->{$table} || [];
1571 unless ( ref( $values ) eq 'ARRAY' ) {
1572 croak "ERROR processing table $table. Wrong API call.";
1574 my $key = $pk ? $pk : $fk;
1575 # $key does not come from user input
1576 my $objects = $module->search(
1577 { $key => $values },
1579 # We want to retrieve the data in the same order
1580 # FIXME MySQLism
1581 # field is a MySQLism, but they are no other way to do it
1582 # To be generic we could do it in perl, but we will need to fetch
1583 # all the data then order them
1584 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1587 $params->{ $config->{$table}->{plural} } = $objects;
1589 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1590 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1591 my $object;
1592 if ( $fk ) { # Using a foreign key for lookup
1593 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1594 my $search;
1595 foreach my $key ( @$fk ) {
1596 $search->{$key} = $id->{$key};
1598 $object = $module->search( $search )->last();
1599 } else { # Foreign key is single column
1600 $object = $module->search( { $fk => $id } )->last();
1602 } else { # using the table's primary key for lookup
1603 $object = $module->find($id);
1605 $params->{ $config->{$table}->{singular} } = $object;
1607 else { # $ref eq 'ARRAY'
1608 my $object;
1609 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1610 $object = $module->search( { $pk => $tables->{$table} } )->last();
1612 else { # Params are mutliple foreign keys
1613 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1615 $params->{ $config->{$table}->{singular} } = $object;
1618 else {
1619 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1623 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1625 return $params;
1628 =head3 add_tt_filters
1630 $content = add_tt_filters( $content );
1632 Add TT filters to some specific fields if needed.
1634 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1636 =cut
1638 sub add_tt_filters {
1639 my ( $content ) = @_;
1640 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1641 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1642 return $content;
1645 =head2 get_item_content
1647 my $item = Koha::Items->find(...)->unblessed;
1648 my @item_content_fields = qw( date_due title barcode author itemnumber );
1649 my $item_content = C4::Letters::get_item_content({
1650 item => $item,
1651 item_content_fields => \@item_content_fields
1654 This function generates a tab-separated list of values for the passed item. Dates
1655 are formatted following the current setup.
1657 =cut
1659 sub get_item_content {
1660 my ( $params ) = @_;
1661 my $item = $params->{item};
1662 my $dateonly = $params->{dateonly} || 0;
1663 my $item_content_fields = $params->{item_content_fields} || [];
1665 return unless $item;
1667 my @item_info = map {
1668 $_ =~ /^date|date$/
1669 ? eval {
1670 output_pref(
1671 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1673 : $item->{$_}
1674 || ''
1675 } @$item_content_fields;
1676 return join( "\t", @item_info ) . "\n";
1680 __END__