Bug 20516: Show patron's library in pending discharges table
[koha.git] / C4 / Letters.pm
blobd4664d03098d7af992a42044046f5955661ac2a7
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::DateUtils qw( format_sqldatetime dt_from_string );
40 use Koha::Patrons;
42 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
44 BEGIN {
45 require Exporter;
46 @ISA = qw(Exporter);
47 @EXPORT = qw(
48 &GetLetters &GetLettersAvailableForALibrary &GetLetterTemplates &DelLetter &GetPreparedLetter &GetWrappedLetter &addalert &getalert &delalert &findrelatedto &SendAlerts &GetPrintMessages &GetMessageTransportTypes
52 =head1 NAME
54 C4::Letters - Give functions for Letters management
56 =head1 SYNOPSIS
58 use C4::Letters;
60 =head1 DESCRIPTION
62 "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
63 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)
65 Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
67 =head2 GetLetters([$module])
69 $letters = &GetLetters($module);
70 returns informations about letters.
71 if needed, $module filters for letters given module
73 DEPRECATED - You must use Koha::Notice::Templates instead
74 The group by clause is confusing and can lead to issues
76 =cut
78 sub GetLetters {
79 my ($filters) = @_;
80 my $module = $filters->{module};
81 my $code = $filters->{code};
82 my $branchcode = $filters->{branchcode};
83 my $dbh = C4::Context->dbh;
84 my $letters = $dbh->selectall_arrayref(
86 SELECT code, module, name
87 FROM letter
88 WHERE 1
90 . ( $module ? q| AND module = ?| : q|| )
91 . ( $code ? q| AND code = ?| : q|| )
92 . ( defined $branchcode ? q| AND branchcode = ?| : q|| )
93 . q| GROUP BY code, module, name ORDER BY name|, { Slice => {} }
94 , ( $module ? $module : () )
95 , ( $code ? $code : () )
96 , ( defined $branchcode ? $branchcode : () )
99 return $letters;
102 =head2 GetLetterTemplates
104 my $letter_templates = GetLetterTemplates(
106 module => 'circulation',
107 code => 'my code',
108 branchcode => 'CPL', # '' for default,
112 Return a hashref of letter templates.
114 =cut
116 sub GetLetterTemplates {
117 my ( $params ) = @_;
119 my $module = $params->{module};
120 my $code = $params->{code};
121 my $branchcode = $params->{branchcode} // '';
122 my $dbh = C4::Context->dbh;
123 my $letters = $dbh->selectall_arrayref(
125 SELECT module, code, branchcode, name, is_html, title, content, message_transport_type, lang
126 FROM letter
127 WHERE module = ?
128 AND code = ?
129 and branchcode = ?
131 , { Slice => {} }
132 , $module, $code, $branchcode
135 return $letters;
138 =head2 GetLettersAvailableForALibrary
140 my $letters = GetLettersAvailableForALibrary(
142 branchcode => 'CPL', # '' for default
143 module => 'circulation',
147 Return an arrayref of letters, sorted by name.
148 If a specific letter exist for the given branchcode, it will be retrieve.
149 Otherwise the default letter will be.
151 =cut
153 sub GetLettersAvailableForALibrary {
154 my ($filters) = @_;
155 my $branchcode = $filters->{branchcode};
156 my $module = $filters->{module};
158 croak "module should be provided" unless $module;
160 my $dbh = C4::Context->dbh;
161 my $default_letters = $dbh->selectall_arrayref(
163 SELECT module, code, branchcode, name
164 FROM letter
165 WHERE 1
167 . q| AND branchcode = ''|
168 . ( $module ? q| AND module = ?| : q|| )
169 . q| ORDER BY name|, { Slice => {} }
170 , ( $module ? $module : () )
173 my $specific_letters;
174 if ($branchcode) {
175 $specific_letters = $dbh->selectall_arrayref(
177 SELECT module, code, branchcode, name
178 FROM letter
179 WHERE 1
181 . q| AND branchcode = ?|
182 . ( $module ? q| AND module = ?| : q|| )
183 . q| ORDER BY name|, { Slice => {} }
184 , $branchcode
185 , ( $module ? $module : () )
189 my %letters;
190 for my $l (@$default_letters) {
191 $letters{ $l->{code} } = $l;
193 for my $l (@$specific_letters) {
194 # Overwrite the default letter with the specific one.
195 $letters{ $l->{code} } = $l;
198 return [ map { $letters{$_} }
199 sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
200 keys %letters ];
204 sub getletter {
205 my ( $module, $code, $branchcode, $message_transport_type, $lang) = @_;
206 $message_transport_type //= '%';
207 $lang = 'default' unless( $lang && C4::Context->preference('TranslateNotices') );
210 my $only_my_library = C4::Context->only_my_library;
211 if ( $only_my_library and $branchcode ) {
212 $branchcode = C4::Context::mybranch();
214 $branchcode //= '';
216 my $dbh = C4::Context->dbh;
217 my $sth = $dbh->prepare(q{
218 SELECT *
219 FROM letter
220 WHERE module=? AND code=? AND (branchcode = ? OR branchcode = '')
221 AND message_transport_type LIKE ?
222 AND lang =?
223 ORDER BY branchcode DESC LIMIT 1
225 $sth->execute( $module, $code, $branchcode, $message_transport_type, $lang );
226 my $line = $sth->fetchrow_hashref
227 or return;
228 $line->{'content-type'} = 'text/html; charset="UTF-8"' if $line->{is_html};
229 return { %$line };
233 =head2 DelLetter
235 DelLetter(
237 branchcode => 'CPL',
238 module => 'circulation',
239 code => 'my code',
240 [ mtt => 'email', ]
244 Delete the letter. The mtt parameter is facultative.
245 If not given, all templates mathing the other parameters will be removed.
247 =cut
249 sub DelLetter {
250 my ($params) = @_;
251 my $branchcode = $params->{branchcode};
252 my $module = $params->{module};
253 my $code = $params->{code};
254 my $mtt = $params->{mtt};
255 my $lang = $params->{lang};
256 my $dbh = C4::Context->dbh;
257 $dbh->do(q|
258 DELETE FROM letter
259 WHERE branchcode = ?
260 AND module = ?
261 AND code = ?
263 . ( $mtt ? q| AND message_transport_type = ?| : q|| )
264 . ( $lang? q| AND lang = ?| : q|| )
265 , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
268 =head2 addalert ($borrowernumber, $type, $externalid)
270 parameters :
271 - $borrowernumber : the number of the borrower subscribing to the alert
272 - $type : the type of alert.
273 - $externalid : the primary key of the object to put alert on. For issues, the alert is made on subscriptionid.
275 create an alert and return the alertid (primary key)
277 =cut
279 sub addalert {
280 my ( $borrowernumber, $type, $externalid ) = @_;
281 my $dbh = C4::Context->dbh;
282 my $sth =
283 $dbh->prepare(
284 "insert into alert (borrowernumber, type, externalid) values (?,?,?)");
285 $sth->execute( $borrowernumber, $type, $externalid );
287 # get the alert number newly created and return it
288 my $alertid = $dbh->{'mysql_insertid'};
289 return $alertid;
292 =head2 delalert ($alertid)
294 parameters :
295 - alertid : the alert id
296 deletes the alert
298 =cut
300 sub delalert {
301 my $alertid = shift or die "delalert() called without valid argument (alertid)"; # it's gonna die anyway.
302 $debug and warn "delalert: deleting alertid $alertid";
303 my $sth = C4::Context->dbh->prepare("delete from alert where alertid=?");
304 $sth->execute($alertid);
307 =head2 getalert ([$borrowernumber], [$type], [$externalid])
309 parameters :
310 - $borrowernumber : the number of the borrower subscribing to the alert
311 - $type : the type of alert.
312 - $externalid : the primary key of the object to put alert on. For issues, the alert is made on subscriptionid.
313 all parameters NON mandatory. If a parameter is omitted, the query is done without the corresponding parameter. For example, without $externalid, returns all alerts for a borrower on a topic.
315 =cut
317 sub getalert {
318 my ( $borrowernumber, $type, $externalid ) = @_;
319 my $dbh = C4::Context->dbh;
320 my $query = "SELECT a.*, b.branchcode FROM alert a JOIN borrowers b USING(borrowernumber) WHERE 1";
321 my @bind;
322 if ($borrowernumber and $borrowernumber =~ /^\d+$/) {
323 $query .= " AND borrowernumber=?";
324 push @bind, $borrowernumber;
326 if ($type) {
327 $query .= " AND type=?";
328 push @bind, $type;
330 if ($externalid) {
331 $query .= " AND externalid=?";
332 push @bind, $externalid;
334 my $sth = $dbh->prepare($query);
335 $sth->execute(@bind);
336 return $sth->fetchall_arrayref({});
339 =head2 findrelatedto($type, $externalid)
341 parameters :
342 - $type : the type of alert
343 - $externalid : the id of the "object" to query
345 In the table alert, a "id" is stored in the externalid field. This "id" is related to another table, depending on the type of the alert.
346 When type=issue, the id is related to a subscriptionid and this sub returns the name of the biblio.
348 =cut
350 # outmoded POD:
351 # When type=virtual, the id is related to a virtual shelf and this sub returns the name of the sub
353 sub findrelatedto {
354 my $type = shift or return;
355 my $externalid = shift or return;
356 my $q = ($type eq 'issue' ) ?
357 "select title as result from subscription left join biblio on subscription.biblionumber=biblio.biblionumber where subscriptionid=?" :
358 ($type eq 'borrower') ?
359 "select concat(firstname,' ',surname) from borrowers where borrowernumber=?" : undef;
360 unless ($q) {
361 warn "findrelatedto(): Illegal type '$type'";
362 return;
364 my $sth = C4::Context->dbh->prepare($q);
365 $sth->execute($externalid);
366 my ($result) = $sth->fetchrow;
367 return $result;
370 =head2 SendAlerts
372 my $err = &SendAlerts($type, $externalid, $letter_code);
374 Parameters:
375 - $type : the type of alert
376 - $externalid : the id of the "object" to query
377 - $letter_code : the notice template to use
379 C<&SendAlerts> sends an email notice directly to a patron or a vendor.
381 Currently it supports ($type):
382 - claim serial issues (claimissues)
383 - claim acquisition orders (claimacquisition)
384 - send acquisition orders to the vendor (orderacquisition)
385 - notify patrons about newly received serial issues (issue)
386 - notify patrons when their account is created (members)
388 Returns undef or { error => 'message } on failure.
389 Returns true on success.
391 =cut
393 sub SendAlerts {
394 my ( $type, $externalid, $letter_code ) = @_;
395 my $dbh = C4::Context->dbh;
396 if ( $type eq 'issue' ) {
398 # prepare the letter...
399 # search the subscriptionid
400 my $sth =
401 $dbh->prepare(
402 "SELECT subscriptionid FROM serial WHERE serialid=?");
403 $sth->execute($externalid);
404 my ($subscriptionid) = $sth->fetchrow
405 or warn( "No subscription for '$externalid'" ),
406 return;
408 # search the biblionumber
409 $sth =
410 $dbh->prepare(
411 "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
412 $sth->execute($subscriptionid);
413 my ($biblionumber) = $sth->fetchrow
414 or warn( "No biblionumber for '$subscriptionid'" ),
415 return;
417 my %letter;
418 # find the list of borrowers to alert
419 my $alerts = getalert( '', 'issue', $subscriptionid );
420 foreach (@$alerts) {
421 my $patron = Koha::Patrons->find( $_->{borrowernumber} );
422 next unless $patron; # Just in case
423 my $email = $patron->email or next;
425 # warn "sending issues...";
426 my $userenv = C4::Context->userenv;
427 my $library = Koha::Libraries->find( $_->{branchcode} );
428 my $letter = GetPreparedLetter (
429 module => 'serial',
430 letter_code => $letter_code,
431 branchcode => $userenv->{branch},
432 tables => {
433 'branches' => $_->{branchcode},
434 'biblio' => $biblionumber,
435 'biblioitems' => $biblionumber,
436 'borrowers' => $patron->unblessed,
437 'subscription' => $subscriptionid,
438 'serial' => $externalid,
440 want_librarian => 1,
441 ) or return;
443 # ... then send mail
444 my $message = Koha::Email->new();
445 my %mail = $message->create_message_headers(
447 to => $email,
448 from => $library->branchemail,
449 replyto => $library->branchreplyto,
450 sender => $library->branchreturnpath,
451 subject => Encode::encode( "UTF-8", "" . $letter->{title} ),
452 message => $letter->{'is_html'}
453 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
454 Encode::encode( "UTF-8", "" . $letter->{'title'} ))
455 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
456 contenttype => $letter->{'is_html'}
457 ? 'text/html; charset="utf-8"'
458 : 'text/plain; charset="utf-8"',
461 unless( Mail::Sendmail::sendmail(%mail) ) {
462 carp $Mail::Sendmail::error;
463 return { error => $Mail::Sendmail::error };
467 elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
469 # prepare the letter...
470 my $strsth;
471 my $sthorders;
472 my $dataorders;
473 my $action;
474 if ( $type eq 'claimacquisition') {
475 $strsth = qq{
476 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
477 FROM aqorders
478 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
479 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
480 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
481 WHERE aqorders.ordernumber IN (
484 if (!@$externalid){
485 carp "No order selected";
486 return { error => "no_order_selected" };
488 $strsth .= join( ",", ('?') x @$externalid ) . ")";
489 $action = "ACQUISITION CLAIM";
490 $sthorders = $dbh->prepare($strsth);
491 $sthorders->execute( @$externalid );
492 $dataorders = $sthorders->fetchall_arrayref( {} );
495 if ($type eq 'claimissues') {
496 $strsth = qq{
497 SELECT serial.*,subscription.*, biblio.*, aqbooksellers.*,
498 aqbooksellers.id AS booksellerid
499 FROM serial
500 LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
501 LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
502 LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
503 WHERE serial.serialid IN (
506 if (!@$externalid){
507 carp "No Order selected";
508 return { error => "no_order_selected" };
511 $strsth .= join( ",", ('?') x @$externalid ) . ")";
512 $action = "CLAIM ISSUE";
513 $sthorders = $dbh->prepare($strsth);
514 $sthorders->execute( @$externalid );
515 $dataorders = $sthorders->fetchall_arrayref( {} );
518 if ( $type eq 'orderacquisition') {
519 $strsth = qq{
520 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
521 FROM aqorders
522 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
523 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
524 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
525 WHERE aqbasket.basketno = ?
526 AND orderstatus IN ('new','ordered')
529 if (!$externalid){
530 carp "No basketnumber given";
531 return { error => "no_basketno" };
533 $action = "ACQUISITION ORDER";
534 $sthorders = $dbh->prepare($strsth);
535 $sthorders->execute($externalid);
536 $dataorders = $sthorders->fetchall_arrayref( {} );
539 my $sthbookseller =
540 $dbh->prepare("select * from aqbooksellers where id=?");
541 $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
542 my $databookseller = $sthbookseller->fetchrow_hashref;
544 my $addressee = $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
546 my $sthcontact =
547 $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
548 $sthcontact->execute( $dataorders->[0]->{booksellerid} );
549 my $datacontact = $sthcontact->fetchrow_hashref;
551 my @email;
552 my @cc;
553 push @email, $databookseller->{bookselleremail} if $databookseller->{bookselleremail};
554 push @email, $datacontact->{email} if ( $datacontact && $datacontact->{email} );
555 unless (@email) {
556 warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
557 return { error => "no_email" };
559 my $addlcontact;
560 while ($addlcontact = $sthcontact->fetchrow_hashref) {
561 push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
564 my $userenv = C4::Context->userenv;
565 my $letter = GetPreparedLetter (
566 module => $type,
567 letter_code => $letter_code,
568 branchcode => $userenv->{branch},
569 tables => {
570 'branches' => $userenv->{branch},
571 'aqbooksellers' => $databookseller,
572 'aqcontacts' => $datacontact,
574 repeat => $dataorders,
575 want_librarian => 1,
576 ) or return { error => "no_letter" };
578 # Remove the order tag
579 $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
581 # ... then send mail
582 my $library = Koha::Libraries->find( $userenv->{branch} );
583 my %mail = (
584 To => join( ',', @email),
585 Cc => join( ',', @cc),
586 From => $library->branchemail || C4::Context->preference('KohaAdminEmailAddress'),
587 Subject => Encode::encode( "UTF-8", "" . $letter->{title} ),
588 Message => $letter->{'is_html'}
589 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
590 Encode::encode( "UTF-8", "" . $letter->{'title'} ))
591 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
592 'Content-Type' => $letter->{'is_html'}
593 ? 'text/html; charset="utf-8"'
594 : 'text/plain; charset="utf-8"',
597 if ($type eq 'claimacquisition' || $type eq 'claimissues' ) {
598 $mail{'Reply-to'} = C4::Context->preference('ReplytoDefault')
599 if C4::Context->preference('ReplytoDefault');
600 $mail{'Sender'} = C4::Context->preference('ReturnpathDefault')
601 if C4::Context->preference('ReturnpathDefault');
602 $mail{'Bcc'} = $userenv->{emailaddress}
603 if C4::Context->preference("ClaimsBccCopy");
606 unless ( Mail::Sendmail::sendmail(%mail) ) {
607 carp $Mail::Sendmail::error;
608 return { error => $Mail::Sendmail::error };
611 logaction(
612 "ACQUISITION",
613 $action,
614 undef,
615 "To="
616 . join( ',', @email )
617 . " Title="
618 . $letter->{title}
619 . " Content="
620 . $letter->{content}
621 ) if C4::Context->preference("LetterLog");
623 # send an "account details" notice to a newly created user
624 elsif ( $type eq 'members' ) {
625 my $library = Koha::Libraries->find( $externalid->{branchcode} )->unblessed;
626 my $letter = GetPreparedLetter (
627 module => 'members',
628 letter_code => $letter_code,
629 branchcode => $externalid->{'branchcode'},
630 tables => {
631 'branches' => $library,
632 'borrowers' => $externalid->{'borrowernumber'},
634 substitute => { 'borrowers.password' => $externalid->{'password'} },
635 want_librarian => 1,
636 ) or return;
637 return { error => "no_email" } unless $externalid->{'emailaddr'};
638 my $email = Koha::Email->new();
639 my %mail = $email->create_message_headers(
641 to => $externalid->{'emailaddr'},
642 from => $library->{branchemail},
643 replyto => $library->{branchreplyto},
644 sender => $library->{branchreturnpath},
645 subject => Encode::encode( "UTF-8", "" . $letter->{'title'} ),
646 message => $letter->{'is_html'}
647 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
648 Encode::encode( "UTF-8", "" . $letter->{'title'} ) )
649 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
650 contenttype => $letter->{'is_html'}
651 ? 'text/html; charset="utf-8"'
652 : 'text/plain; charset="utf-8"',
655 unless( Mail::Sendmail::sendmail(%mail) ) {
656 carp $Mail::Sendmail::error;
657 return { error => $Mail::Sendmail::error };
661 # If we come here, return an OK status
662 return 1;
665 =head2 GetPreparedLetter( %params )
667 %params hash:
668 module => letter module, mandatory
669 letter_code => letter code, mandatory
670 branchcode => for letter selection, if missing default system letter taken
671 tables => a hashref with table names as keys. Values are either:
672 - a scalar - primary key value
673 - an arrayref - primary key values
674 - a hashref - full record
675 substitute => custom substitution key/value pairs
676 repeat => records to be substituted on consecutive lines:
677 - an arrayref - tries to guess what needs substituting by
678 taking remaining << >> tokensr; not recommended
679 - a hashref token => @tables - replaces <token> << >> << >> </token>
680 subtemplate for each @tables row; table is a hashref as above
681 want_librarian => boolean, if set to true triggers librarian details
682 substitution from the userenv
683 Return value:
684 letter fields hashref (title & content useful)
686 =cut
688 sub GetPreparedLetter {
689 my %params = @_;
691 my $module = $params{module} or croak "No module";
692 my $letter_code = $params{letter_code} or croak "No letter_code";
693 my $branchcode = $params{branchcode} || '';
694 my $mtt = $params{message_transport_type} || 'email';
695 my $lang = $params{lang} || 'default';
697 my $letter = getletter( $module, $letter_code, $branchcode, $mtt, $lang );
699 unless ( $letter ) {
700 $letter = getletter( $module, $letter_code, $branchcode, $mtt, 'default' )
701 or warn( "No $module $letter_code letter transported by " . $mtt ),
702 return;
705 my $tables = $params{tables} || {};
706 my $substitute = $params{substitute} || {};
707 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
708 my $repeat = $params{repeat};
709 %$tables || %$substitute || $repeat || %$loops
710 or carp( "ERROR: nothing to substitute - both 'tables', 'loops' and 'substitute' are empty" ),
711 return;
712 my $want_librarian = $params{want_librarian};
714 if (%$substitute) {
715 while ( my ($token, $val) = each %$substitute ) {
716 if ( $token eq 'items.content' ) {
717 $val =~ s|\n|<br/>|g if $letter->{is_html};
720 $letter->{title} =~ s/<<$token>>/$val/g;
721 $letter->{content} =~ s/<<$token>>/$val/g;
725 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
726 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
728 if ($want_librarian) {
729 # parsing librarian name
730 my $userenv = C4::Context->userenv;
731 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
732 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
733 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
736 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
738 if ($repeat) {
739 if (ref ($repeat) eq 'ARRAY' ) {
740 $repeat_no_enclosing_tags = $repeat;
741 } else {
742 $repeat_enclosing_tags = $repeat;
746 if ($repeat_enclosing_tags) {
747 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
748 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
749 my $subcontent = $1;
750 my @lines = map {
751 my %subletter = ( title => '', content => $subcontent );
752 _substitute_tables( \%subletter, $_ );
753 $subletter{content};
754 } @$tag_tables;
755 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
760 if (%$tables) {
761 _substitute_tables( $letter, $tables );
764 if ($repeat_no_enclosing_tags) {
765 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
766 my $line = $&;
767 my $i = 1;
768 my @lines = map {
769 my $c = $line;
770 $c =~ s/<<count>>/$i/go;
771 foreach my $field ( keys %{$_} ) {
772 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
774 $i++;
776 } @$repeat_no_enclosing_tags;
778 my $replaceby = join( "\n", @lines );
779 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
783 $letter->{content} = _process_tt(
785 content => $letter->{content},
786 tables => $tables,
787 loops => $loops,
788 substitute => $substitute,
792 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
794 return $letter;
797 sub _substitute_tables {
798 my ( $letter, $tables ) = @_;
799 while ( my ($table, $param) = each %$tables ) {
800 next unless $param;
802 my $ref = ref $param;
804 my $values;
805 if ($ref && $ref eq 'HASH') {
806 $values = $param;
808 else {
809 my $sth = _parseletter_sth($table);
810 unless ($sth) {
811 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
812 return;
814 $sth->execute( $ref ? @$param : $param );
816 $values = $sth->fetchrow_hashref;
817 $sth->finish();
820 _parseletter ( $letter, $table, $values );
824 sub _parseletter_sth {
825 my $table = shift;
826 my $sth;
827 unless ($table) {
828 carp "ERROR: _parseletter_sth() called without argument (table)";
829 return;
831 # NOTE: we used to check whether we had a statement handle cached in
832 # a %handles module-level variable. This was a dumb move and
833 # broke things for the rest of us. prepare_cached is a better
834 # way to cache statement handles anyway.
835 my $query =
836 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
837 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
838 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
839 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
840 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE itemnumber = ? ORDER BY timestamp DESC LIMIT 1" :
841 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
842 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
843 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
844 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
845 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
846 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
847 ($table eq 'opac_news' ) ? "SELECT * FROM $table WHERE idnew = ?" :
848 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
849 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
850 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
851 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
852 undef ;
853 unless ($query) {
854 warn "ERROR: No _parseletter_sth query for table '$table'";
855 return; # nothing to get
857 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
858 warn "ERROR: Failed to prepare query: '$query'";
859 return;
861 return $sth; # now cache is populated for that $table
864 =head2 _parseletter($letter, $table, $values)
866 parameters :
867 - $letter : a hash to letter fields (title & content useful)
868 - $table : the Koha table to parse.
869 - $values_in : table record hashref
870 parse all fields from a table, and replace values in title & content with the appropriate value
871 (not exported sub, used only internally)
873 =cut
875 sub _parseletter {
876 my ( $letter, $table, $values_in ) = @_;
878 # Work on a local copy of $values_in (passed by reference) to avoid side effects
879 # in callers ( by changing / formatting values )
880 my $values = $values_in ? { %$values_in } : {};
882 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
883 $values->{'dateexpiry'} = format_sqldatetime( $values->{'dateexpiry'} );
886 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
887 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
890 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
891 my $todaysdate = output_pref( DateTime->now() );
892 $letter->{content} =~ s/<<today>>/$todaysdate/go;
895 while ( my ($field, $val) = each %$values ) {
896 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
897 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
898 #Therefore adding the test on biblio. This includes biblioitems,
899 #but excludes items. Removed unneeded global and lookahead.
901 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
902 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
903 $val = $av->count ? $av->next->lib : '';
906 # Dates replacement
907 my $replacedby = defined ($val) ? $val : '';
908 if ( $replacedby
909 and not $replacedby =~ m|0000-00-00|
910 and not $replacedby =~ m|9999-12-31|
911 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
913 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
914 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
915 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
917 for my $letter_field ( qw( title content ) ) {
918 my $filter_string_used = q{};
919 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
920 # We overwrite $dateonly if the filter exists and we have a time in the datetime
921 $filter_string_used = $1 || q{};
922 $dateonly = $1 unless $dateonly;
924 my $replacedby_date = eval {
925 output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
928 if ( $letter->{ $letter_field } ) {
929 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
930 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
934 # Other fields replacement
935 else {
936 for my $letter_field ( qw( title content ) ) {
937 if ( $letter->{ $letter_field } ) {
938 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
939 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
945 if ($table eq 'borrowers' && $letter->{content}) {
946 if ( my $attributes = GetBorrowerAttributes($values->{borrowernumber}) ) {
947 my %attr;
948 foreach (@$attributes) {
949 my $code = $_->{code};
950 my $val = $_->{value_description} || $_->{value};
951 $val =~ s/\p{P}(?=$)//g if $val;
952 next unless $val gt '';
953 $attr{$code} ||= [];
954 push @{ $attr{$code} }, $val;
956 while ( my ($code, $val_ar) = each %attr ) {
957 my $replacefield = "<<borrower-attribute:$code>>";
958 my $replacedby = join ',', @$val_ar;
959 $letter->{content} =~ s/$replacefield/$replacedby/g;
963 return $letter;
966 =head2 EnqueueLetter
968 my $success = EnqueueLetter( { letter => $letter,
969 borrowernumber => '12', message_transport_type => 'email' } )
971 places a letter in the message_queue database table, which will
972 eventually get processed (sent) by the process_message_queue.pl
973 cronjob when it calls SendQueuedMessages.
975 return message_id on success
977 =cut
979 sub EnqueueLetter {
980 my $params = shift or return;
982 return unless exists $params->{'letter'};
983 # return unless exists $params->{'borrowernumber'};
984 return unless exists $params->{'message_transport_type'};
986 my $content = $params->{letter}->{content};
987 $content =~ s/\s+//g if(defined $content);
988 if ( not defined $content or $content eq '' ) {
989 warn "Trying to add an empty message to the message queue" if $debug;
990 return;
993 # If we have any attachments we should encode then into the body.
994 if ( $params->{'attachments'} ) {
995 $params->{'letter'} = _add_attachments(
996 { letter => $params->{'letter'},
997 attachments => $params->{'attachments'},
998 message => MIME::Lite->new( Type => 'multipart/mixed' ),
1003 my $dbh = C4::Context->dbh();
1004 my $statement = << 'ENDSQL';
1005 INSERT INTO message_queue
1006 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type )
1007 VALUES
1008 ( ?, ?, ?, ?, ?, ?, ?, NOW(), ?, ?, ? )
1009 ENDSQL
1011 my $sth = $dbh->prepare($statement);
1012 my $result = $sth->execute(
1013 $params->{'borrowernumber'}, # borrowernumber
1014 $params->{'letter'}->{'title'}, # subject
1015 $params->{'letter'}->{'content'}, # content
1016 $params->{'letter'}->{'metadata'} || '', # metadata
1017 $params->{'letter'}->{'code'} || '', # letter_code
1018 $params->{'message_transport_type'}, # message_transport_type
1019 'pending', # status
1020 $params->{'to_address'}, # to_address
1021 $params->{'from_address'}, # from_address
1022 $params->{'letter'}->{'content-type'}, # content_type
1024 return $dbh->last_insert_id(undef,undef,'message_queue', undef);
1027 =head2 SendQueuedMessages ([$hashref])
1029 my $sent = SendQueuedMessages({
1030 letter_code => $letter_code,
1031 borrowernumber => $who_letter_is_for,
1032 limit => 50,
1033 verbose => 1,
1034 type => 'sms',
1037 Sends all of the 'pending' items in the message queue, unless
1038 parameters are passed.
1040 The letter_code, borrowernumber and limit parameters are used
1041 to build a parameter set for _get_unsent_messages, thus limiting
1042 which pending messages will be processed. They are all optional.
1044 The verbose parameter can be used to generate debugging output.
1045 It is also optional.
1047 Returns number of messages sent.
1049 =cut
1051 sub SendQueuedMessages {
1052 my $params = shift;
1054 my $which_unsent_messages = {
1055 'limit' => $params->{'limit'} // 0,
1056 'borrowernumber' => $params->{'borrowernumber'} // q{},
1057 'letter_code' => $params->{'letter_code'} // q{},
1058 'type' => $params->{'type'} // q{},
1060 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
1061 MESSAGE: foreach my $message ( @$unsent_messages ) {
1062 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
1063 warn sprintf( 'sending %s message to patron: %s',
1064 $message->{'message_transport_type'},
1065 $message->{'borrowernumber'} || 'Admin' )
1066 if $params->{'verbose'} or $debug;
1067 # This is just begging for subclassing
1068 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
1069 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
1070 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1072 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
1073 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
1074 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1075 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1076 unless ( $sms_provider ) {
1077 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1078 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1079 next MESSAGE;
1081 unless ( $patron->smsalertnumber ) {
1082 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1083 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1084 next MESSAGE;
1086 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1087 $message->{to_address} .= '@' . $sms_provider->domain();
1088 _update_message_to_address($message->{'message_id'},$message->{to_address});
1089 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1090 } else {
1091 _send_message_by_sms( $message );
1095 return scalar( @$unsent_messages );
1098 =head2 GetRSSMessages
1100 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1102 returns a listref of all queued RSS messages for a particular person.
1104 =cut
1106 sub GetRSSMessages {
1107 my $params = shift;
1109 return unless $params;
1110 return unless ref $params;
1111 return unless $params->{'borrowernumber'};
1113 return _get_unsent_messages( { message_transport_type => 'rss',
1114 limit => $params->{'limit'},
1115 borrowernumber => $params->{'borrowernumber'}, } );
1118 =head2 GetPrintMessages
1120 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1122 Returns a arrayref of all queued print messages (optionally, for a particular
1123 person).
1125 =cut
1127 sub GetPrintMessages {
1128 my $params = shift || {};
1130 return _get_unsent_messages( { message_transport_type => 'print',
1131 borrowernumber => $params->{'borrowernumber'},
1132 } );
1135 =head2 GetQueuedMessages ([$hashref])
1137 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1139 fetches messages out of the message queue.
1141 returns:
1142 list of hashes, each has represents a message in the message queue.
1144 =cut
1146 sub GetQueuedMessages {
1147 my $params = shift;
1149 my $dbh = C4::Context->dbh();
1150 my $statement = << 'ENDSQL';
1151 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued
1152 FROM message_queue
1153 ENDSQL
1155 my @query_params;
1156 my @whereclauses;
1157 if ( exists $params->{'borrowernumber'} ) {
1158 push @whereclauses, ' borrowernumber = ? ';
1159 push @query_params, $params->{'borrowernumber'};
1162 if ( @whereclauses ) {
1163 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1166 if ( defined $params->{'limit'} ) {
1167 $statement .= ' LIMIT ? ';
1168 push @query_params, $params->{'limit'};
1171 my $sth = $dbh->prepare( $statement );
1172 my $result = $sth->execute( @query_params );
1173 return $sth->fetchall_arrayref({});
1176 =head2 GetMessageTransportTypes
1178 my @mtt = GetMessageTransportTypes();
1180 returns an arrayref of transport types
1182 =cut
1184 sub GetMessageTransportTypes {
1185 my $dbh = C4::Context->dbh();
1186 my $mtts = $dbh->selectcol_arrayref("
1187 SELECT message_transport_type
1188 FROM message_transport_types
1189 ORDER BY message_transport_type
1191 return $mtts;
1194 =head2 GetMessage
1196 my $message = C4::Letters::Message($message_id);
1198 =cut
1200 sub GetMessage {
1201 my ( $message_id ) = @_;
1202 return unless $message_id;
1203 my $dbh = C4::Context->dbh;
1204 return $dbh->selectrow_hashref(q|
1205 SELECT message_id, borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type
1206 FROM message_queue
1207 WHERE message_id = ?
1208 |, {}, $message_id );
1211 =head2 ResendMessage
1213 Attempt to resend a message which has failed previously.
1215 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1217 Updates the message to 'pending' status so that
1218 it will be resent later on.
1220 returns 1 on success, 0 on failure, undef if no message was found
1222 =cut
1224 sub ResendMessage {
1225 my $message_id = shift;
1226 return unless $message_id;
1228 my $message = GetMessage( $message_id );
1229 return unless $message;
1230 my $rv = 0;
1231 if ( $message->{status} ne 'pending' ) {
1232 $rv = C4::Letters::_set_message_status({
1233 message_id => $message_id,
1234 status => 'pending',
1236 $rv = $rv > 0? 1: 0;
1237 # Clear destination email address to force address update
1238 _update_message_to_address( $message_id, undef ) if $rv &&
1239 $message->{message_transport_type} eq 'email';
1241 return $rv;
1244 =head2 _add_attachements
1246 named parameters:
1247 letter - the standard letter hashref
1248 attachments - listref of attachments. each attachment is a hashref of:
1249 type - the mime type, like 'text/plain'
1250 content - the actual attachment
1251 filename - the name of the attachment.
1252 message - a MIME::Lite object to attach these to.
1254 returns your letter object, with the content updated.
1256 =cut
1258 sub _add_attachments {
1259 my $params = shift;
1261 my $letter = $params->{'letter'};
1262 my $attachments = $params->{'attachments'};
1263 return $letter unless @$attachments;
1264 my $message = $params->{'message'};
1266 # First, we have to put the body in as the first attachment
1267 $message->attach(
1268 Type => $letter->{'content-type'} || 'TEXT',
1269 Data => $letter->{'is_html'}
1270 ? _wrap_html($letter->{'content'}, $letter->{'title'})
1271 : $letter->{'content'},
1274 foreach my $attachment ( @$attachments ) {
1275 $message->attach(
1276 Type => $attachment->{'type'},
1277 Data => $attachment->{'content'},
1278 Filename => $attachment->{'filename'},
1281 # we're forcing list context here to get the header, not the count back from grep.
1282 ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1283 $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1284 $letter->{'content'} = $message->body_as_string;
1286 return $letter;
1290 =head2 _get_unsent_messages
1292 This function's parameter hash reference takes the following
1293 optional named parameters:
1294 message_transport_type: method of message sending (e.g. email, sms, etc.)
1295 borrowernumber : who the message is to be sent
1296 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1297 limit : maximum number of messages to send
1299 This function returns an array of matching hash referenced rows from
1300 message_queue with some borrower information added.
1302 =cut
1304 sub _get_unsent_messages {
1305 my $params = shift;
1307 my $dbh = C4::Context->dbh();
1308 my $statement = qq{
1309 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
1310 FROM message_queue mq
1311 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1312 WHERE status = ?
1315 my @query_params = ('pending');
1316 if ( ref $params ) {
1317 if ( $params->{'message_transport_type'} ) {
1318 $statement .= ' AND mq.message_transport_type = ? ';
1319 push @query_params, $params->{'message_transport_type'};
1321 if ( $params->{'borrowernumber'} ) {
1322 $statement .= ' AND mq.borrowernumber = ? ';
1323 push @query_params, $params->{'borrowernumber'};
1325 if ( $params->{'letter_code'} ) {
1326 $statement .= ' AND mq.letter_code = ? ';
1327 push @query_params, $params->{'letter_code'};
1329 if ( $params->{'type'} ) {
1330 $statement .= ' AND message_transport_type = ? ';
1331 push @query_params, $params->{'type'};
1333 if ( $params->{'limit'} ) {
1334 $statement .= ' limit ? ';
1335 push @query_params, $params->{'limit'};
1339 $debug and warn "_get_unsent_messages SQL: $statement";
1340 $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1341 my $sth = $dbh->prepare( $statement );
1342 my $result = $sth->execute( @query_params );
1343 return $sth->fetchall_arrayref({});
1346 sub _send_message_by_email {
1347 my $message = shift or return;
1348 my ($username, $password, $method) = @_;
1350 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1351 my $to_address = $message->{'to_address'};
1352 unless ($to_address) {
1353 unless ($patron) {
1354 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1355 _set_message_status( { message_id => $message->{'message_id'},
1356 status => 'failed' } );
1357 return;
1359 $to_address = $patron->notice_email_address;
1360 unless ($to_address) {
1361 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1362 # warning too verbose for this more common case?
1363 _set_message_status( { message_id => $message->{'message_id'},
1364 status => 'failed' } );
1365 return;
1369 my $utf8 = decode('MIME-Header', $message->{'subject'} );
1370 $message->{subject}= encode('MIME-Header', $utf8);
1371 my $subject = encode('UTF-8', $message->{'subject'});
1372 my $content = encode('UTF-8', $message->{'content'});
1373 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1374 my $is_html = $content_type =~ m/html/io;
1375 my $branch_email = undef;
1376 my $branch_replyto = undef;
1377 my $branch_returnpath = undef;
1378 if ($patron) {
1379 my $library = $patron->library;
1380 $branch_email = $library->branchemail;
1381 $branch_replyto = $library->branchreplyto;
1382 $branch_returnpath = $library->branchreturnpath;
1384 my $email = Koha::Email->new();
1385 my %sendmail_params = $email->create_message_headers(
1387 to => $to_address,
1388 from => $message->{'from_address'} || $branch_email,
1389 replyto => $branch_replyto,
1390 sender => $branch_returnpath,
1391 subject => $subject,
1392 message => $is_html ? _wrap_html( $content, $subject ) : $content,
1393 contenttype => $content_type
1397 $sendmail_params{'Auth'} = {user => $username, pass => $password, method => $method} if $username;
1398 if ( my $bcc = C4::Context->preference('NoticeBcc') ) {
1399 $sendmail_params{ Bcc } = $bcc;
1402 _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
1404 if ( Mail::Sendmail::sendmail( %sendmail_params ) ) {
1405 _set_message_status( { message_id => $message->{'message_id'},
1406 status => 'sent' } );
1407 return 1;
1408 } else {
1409 _set_message_status( { message_id => $message->{'message_id'},
1410 status => 'failed' } );
1411 carp $Mail::Sendmail::error;
1412 return;
1416 sub _wrap_html {
1417 my ($content, $title) = @_;
1419 my $css = C4::Context->preference("NoticeCSS") || '';
1420 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1421 return <<EOS;
1422 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1423 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1424 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1425 <head>
1426 <title>$title</title>
1427 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1428 $css
1429 </head>
1430 <body>
1431 $content
1432 </body>
1433 </html>
1437 sub _is_duplicate {
1438 my ( $message ) = @_;
1439 my $dbh = C4::Context->dbh;
1440 my $count = $dbh->selectrow_array(q|
1441 SELECT COUNT(*)
1442 FROM message_queue
1443 WHERE message_transport_type = ?
1444 AND borrowernumber = ?
1445 AND letter_code = ?
1446 AND CAST(time_queued AS date) = CAST(NOW() AS date)
1447 AND status="sent"
1448 AND content = ?
1449 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1450 return $count;
1453 sub _send_message_by_sms {
1454 my $message = shift or return;
1455 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1457 unless ( $patron and $patron->smsalertnumber ) {
1458 _set_message_status( { message_id => $message->{'message_id'},
1459 status => 'failed' } );
1460 return;
1463 if ( _is_duplicate( $message ) ) {
1464 _set_message_status( { message_id => $message->{'message_id'},
1465 status => 'failed' } );
1466 return;
1469 my $success = C4::SMS->send_sms( { destination => $patron->smsalertnumber,
1470 message => $message->{'content'},
1471 } );
1472 _set_message_status( { message_id => $message->{'message_id'},
1473 status => ($success ? 'sent' : 'failed') } );
1474 return $success;
1477 sub _update_message_to_address {
1478 my ($id, $to)= @_;
1479 my $dbh = C4::Context->dbh();
1480 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1483 sub _set_message_status {
1484 my $params = shift or return;
1486 foreach my $required_parameter ( qw( message_id status ) ) {
1487 return unless exists $params->{ $required_parameter };
1490 my $dbh = C4::Context->dbh();
1491 my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1492 my $sth = $dbh->prepare( $statement );
1493 my $result = $sth->execute( $params->{'status'},
1494 $params->{'message_id'} );
1495 return $result;
1498 sub _process_tt {
1499 my ( $params ) = @_;
1501 my $content = $params->{content};
1502 my $tables = $params->{tables};
1503 my $loops = $params->{loops};
1504 my $substitute = $params->{substitute} || {};
1506 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1507 my $template = Template->new(
1509 EVAL_PERL => 1,
1510 ABSOLUTE => 1,
1511 PLUGIN_BASE => 'Koha::Template::Plugin',
1512 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1513 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1514 FILTERS => {},
1515 ENCODING => 'UTF-8',
1517 ) or die Template->error();
1519 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
1521 $content = add_tt_filters( $content );
1522 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1524 my $output;
1525 $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1527 return $output;
1530 sub _get_tt_params {
1531 my ($tables, $is_a_loop) = @_;
1533 my $params;
1534 $is_a_loop ||= 0;
1536 my $config = {
1537 article_requests => {
1538 module => 'Koha::ArticleRequests',
1539 singular => 'article_request',
1540 plural => 'article_requests',
1541 pk => 'id',
1543 biblio => {
1544 module => 'Koha::Biblios',
1545 singular => 'biblio',
1546 plural => 'biblios',
1547 pk => 'biblionumber',
1549 biblioitems => {
1550 module => 'Koha::Biblioitems',
1551 singular => 'biblioitem',
1552 plural => 'biblioitems',
1553 pk => 'biblioitemnumber',
1555 borrowers => {
1556 module => 'Koha::Patrons',
1557 singular => 'borrower',
1558 plural => 'borrowers',
1559 pk => 'borrowernumber',
1561 branches => {
1562 module => 'Koha::Libraries',
1563 singular => 'branch',
1564 plural => 'branches',
1565 pk => 'branchcode',
1567 items => {
1568 module => 'Koha::Items',
1569 singular => 'item',
1570 plural => 'items',
1571 pk => 'itemnumber',
1573 opac_news => {
1574 module => 'Koha::News',
1575 singular => 'news',
1576 plural => 'news',
1577 pk => 'idnew',
1579 aqorders => {
1580 module => 'Koha::Acquisition::Orders',
1581 singular => 'order',
1582 plural => 'orders',
1583 pk => 'ordernumber',
1585 reserves => {
1586 module => 'Koha::Holds',
1587 singular => 'hold',
1588 plural => 'holds',
1589 fk => [ 'borrowernumber', 'biblionumber' ],
1591 serial => {
1592 module => 'Koha::Serials',
1593 singular => 'serial',
1594 plural => 'serials',
1595 pk => 'serialid',
1597 subscription => {
1598 module => 'Koha::Subscriptions',
1599 singular => 'subscription',
1600 plural => 'subscriptions',
1601 pk => 'subscriptionid',
1603 suggestions => {
1604 module => 'Koha::Suggestions',
1605 singular => 'suggestion',
1606 plural => 'suggestions',
1607 pk => 'suggestionid',
1609 issues => {
1610 module => 'Koha::Checkouts',
1611 singular => 'checkout',
1612 plural => 'checkouts',
1613 fk => 'itemnumber',
1615 old_issues => {
1616 module => 'Koha::Old::Checkouts',
1617 singular => 'old_checkout',
1618 plural => 'old_checkouts',
1619 fk => 'itemnumber',
1621 overdues => {
1622 module => 'Koha::Checkouts',
1623 singular => 'overdue',
1624 plural => 'overdues',
1625 fk => 'itemnumber',
1627 borrower_modifications => {
1628 module => 'Koha::Patron::Modifications',
1629 singular => 'patron_modification',
1630 plural => 'patron_modifications',
1631 fk => 'verification_token',
1635 foreach my $table ( keys %$tables ) {
1636 next unless $config->{$table};
1638 my $ref = ref( $tables->{$table} ) || q{};
1639 my $module = $config->{$table}->{module};
1641 if ( can_load( modules => { $module => undef } ) ) {
1642 my $pk = $config->{$table}->{pk};
1643 my $fk = $config->{$table}->{fk};
1645 if ( $is_a_loop ) {
1646 my $values = $tables->{$table} || [];
1647 unless ( ref( $values ) eq 'ARRAY' ) {
1648 croak "ERROR processing table $table. Wrong API call.";
1650 my $key = $pk ? $pk : $fk;
1651 # $key does not come from user input
1652 my $objects = $module->search(
1653 { $key => $values },
1655 # We want to retrieve the data in the same order
1656 # FIXME MySQLism
1657 # field is a MySQLism, but they are no other way to do it
1658 # To be generic we could do it in perl, but we will need to fetch
1659 # all the data then order them
1660 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1663 $params->{ $config->{$table}->{plural} } = $objects;
1665 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1666 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1667 my $object;
1668 if ( $fk ) { # Using a foreign key for lookup
1669 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1670 my $search;
1671 foreach my $key ( @$fk ) {
1672 $search->{$key} = $id->{$key};
1674 $object = $module->search( $search )->last();
1675 } else { # Foreign key is single column
1676 $object = $module->search( { $fk => $id } )->last();
1678 } else { # using the table's primary key for lookup
1679 $object = $module->find($id);
1681 $params->{ $config->{$table}->{singular} } = $object;
1683 else { # $ref eq 'ARRAY'
1684 my $object;
1685 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1686 $object = $module->search( { $pk => $tables->{$table} } )->last();
1688 else { # Params are mutliple foreign keys
1689 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1691 $params->{ $config->{$table}->{singular} } = $object;
1694 else {
1695 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1699 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1701 return $params;
1704 =head3 add_tt_filters
1706 $content = add_tt_filters( $content );
1708 Add TT filters to some specific fields if needed.
1710 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1712 =cut
1714 sub add_tt_filters {
1715 my ( $content ) = @_;
1716 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1717 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1718 return $content;
1721 =head2 get_item_content
1723 my $item = Koha::Items->find(...)->unblessed;
1724 my @item_content_fields = qw( date_due title barcode author itemnumber );
1725 my $item_content = C4::Letters::get_item_content({
1726 item => $item,
1727 item_content_fields => \@item_content_fields
1730 This function generates a tab-separated list of values for the passed item. Dates
1731 are formatted following the current setup.
1733 =cut
1735 sub get_item_content {
1736 my ( $params ) = @_;
1737 my $item = $params->{item};
1738 my $dateonly = $params->{dateonly} || 0;
1739 my $item_content_fields = $params->{item_content_fields} || [];
1741 return unless $item;
1743 my @item_info = map {
1744 $_ =~ /^date|date$/
1745 ? eval {
1746 output_pref(
1747 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1749 : $item->{$_}
1750 || ''
1751 } @$item_content_fields;
1752 return join( "\t", @item_info ) . "\n";
1756 __END__