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>.
22 use Carp qw( carp croak );
24 use Module::Load::Conditional qw( can_load );
28 use C4::Log qw( logaction );
32 use Koha::Auth::TwoFactorAuth;
33 use Koha::DateUtils qw( dt_from_string output_pref );
36 use Koha::Notice::Messages;
37 use Koha::Notice::Templates;
38 use Koha::Notice::Util;
40 use Koha::SMS::Providers;
41 use Koha::SMTP::Servers;
42 use Koha::Subscriptions;
45 use constant SERIALIZED_EMAIL_CONTENT_TYPE => 'message/rfc822';
47 our (@ISA, @EXPORT_OK);
53 GetLettersAvailableForALibrary
62 GetMessageTransportTypes
70 our $domain_limits = {};
74 C4::Letters - Give functions for Letters management
82 "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
83 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)
85 Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
87 =head2 GetLetters([$module])
89 $letters = &GetLetters($module);
90 returns informations about letters.
91 if needed, $module filters for letters given module
93 DEPRECATED - You must use Koha::Notice::Templates instead
94 The group by clause is confusing and can lead to issues
100 my $module = $filters->{module};
101 my $code = $filters->{code};
102 my $branchcode = $filters->{branchcode};
103 my $dbh = C4::Context->dbh;
104 my $letters = $dbh->selectall_arrayref(
106 SELECT code, module, name
110 . ( $module ? q| AND module = ?| : q|| )
111 . ( $code ? q| AND code = ?| : q|| )
112 . ( defined $branchcode ? q| AND branchcode = ?| : q|| )
113 . q| GROUP BY code, module, name ORDER BY name|, { Slice => {} }
114 , ( $module ? $module : () )
115 , ( $code ? $code : () )
116 , ( defined $branchcode ? $branchcode : () )
122 =head2 GetLetterTemplates
124 my $letter_templates = GetLetterTemplates(
126 module => 'circulation',
128 branchcode => 'CPL', # '' for default,
132 Return a hashref of letter templates.
136 sub GetLetterTemplates {
139 my $module = $params->{module};
140 my $code = $params->{code};
141 my $branchcode = $params->{branchcode} // '';
142 my $dbh = C4::Context->dbh;
143 return Koha::Notice::Templates->search(
147 branchcode => $branchcode,
149 C4::Context->preference('TranslateNotices')
151 : ( lang => 'default' )
157 =head2 GetLettersAvailableForALibrary
159 my $letters = GetLettersAvailableForALibrary(
161 branchcode => 'CPL', # '' for default
162 module => 'circulation',
166 Return an arrayref of letters, sorted by name.
167 If a specific letter exist for the given branchcode, it will be retrieve.
168 Otherwise the default letter will be.
172 sub GetLettersAvailableForALibrary {
174 my $branchcode = $filters->{branchcode};
175 my $module = $filters->{module};
177 croak "module should be provided" unless $module;
179 my $dbh = C4::Context->dbh;
180 my $default_letters = $dbh->selectall_arrayref(
182 SELECT module, code, branchcode, name
186 . q| AND branchcode = ''|
187 . ( $module ? q| AND module = ?| : q|| )
188 . q| ORDER BY name|, { Slice => {} }
189 , ( $module ? $module : () )
192 my $specific_letters;
194 $specific_letters = $dbh->selectall_arrayref(
196 SELECT module, code, branchcode, name
200 . q| AND branchcode = ?|
201 . ( $module ? q| AND module = ?| : q|| )
202 . q| ORDER BY name|, { Slice => {} }
204 , ( $module ? $module : () )
209 for my $l (@$default_letters) {
210 $letters{ $l->{code} } = $l;
212 for my $l (@$specific_letters) {
213 # Overwrite the default letter with the specific one.
214 $letters{ $l->{code} } = $l;
217 return [ map { $letters{$_} }
218 sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
228 module => 'circulation',
234 Delete the letter. The mtt parameter is facultative.
235 If not given, all templates mathing the other parameters will be removed.
241 my $branchcode = $params->{branchcode};
242 my $module = $params->{module};
243 my $code = $params->{code};
244 my $mtt = $params->{mtt};
245 my $lang = $params->{lang};
246 my $dbh = C4::Context->dbh;
253 . ( $mtt ? q| AND message_transport_type = ?| : q|| )
254 . ( $lang? q| AND lang = ?| : q|| )
255 , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
260 my $err = &SendAlerts($type, $externalid, $letter_code);
263 - $type : the type of alert
264 - $externalid : the id of the "object" to query
265 - $letter_code : the notice template to use
267 C<&SendAlerts> sends an email notice directly to a patron or a vendor.
269 Currently it supports ($type):
270 - claim serial issues (claimissues)
271 - claim acquisition orders (claimacquisition)
272 - send acquisition orders to the vendor (orderacquisition)
273 - notify patrons about newly received serial issues (issue)
274 - notify patrons when their account is created (members)
276 Returns undef or { error => 'message } on failure.
277 Returns true on success.
282 my ( $type, $externalid, $letter_code ) = @_;
283 my $dbh = C4::Context->dbh;
286 if ( $type eq 'issue' ) {
288 # prepare the letter...
289 # search the subscriptionid
292 "SELECT subscriptionid FROM serial WHERE serialid=?");
293 $sth->execute($externalid);
294 my ($subscriptionid) = $sth->fetchrow
295 or warn( "No subscription for '$externalid'" ),
298 # search the biblionumber
301 "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
302 $sth->execute($subscriptionid);
303 my ($biblionumber) = $sth->fetchrow
304 or warn( "No biblionumber for '$subscriptionid'" ),
307 # find the list of subscribers to notify
308 my $subscription = Koha::Subscriptions->find( $subscriptionid );
309 my $subscribers = $subscription->subscribers;
310 while ( my $patron = $subscribers->next ) {
311 my $email = $patron->email or next;
313 # warn "sending issues...";
314 my $userenv = C4::Context->userenv;
315 my $library = $patron->library;
316 my $letter = GetPreparedLetter (
318 letter_code => $letter_code,
319 branchcode => $userenv->{branch},
321 'branches' => $library->branchcode,
322 'biblio' => $biblionumber,
323 'biblioitems' => $biblionumber,
324 'borrowers' => $patron->unblessed,
325 'subscription' => $subscriptionid,
326 'serial' => $externalid,
331 # FIXME: This 'default' behaviour should be moved to Koha::Email
332 my $mail = Koha::Email->create(
335 from => $library->branchemail,
336 reply_to => $library->branchreplyto,
337 sender => $library->branchreturnpath,
338 subject => "" . $letter->{title},
342 if ( $letter->{is_html} ) {
343 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
346 $mail->text_body( $letter->{content} );
350 $mail->send_or_die({ transport => $library->smtp_server->transport });
353 # We expect ref($_) eq 'Email::Sender::Failure'
354 $error = $_->message;
360 return { error => $error }
364 elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
366 # prepare the letter...
373 if ( $type eq 'claimacquisition') {
375 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
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 (
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') {
396 SELECT serial.*,subscription.*, biblio.*, biblioitems.*, aqbooksellers.*,
397 aqbooksellers.id AS booksellerid
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 (
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 $basketno = $externalid;
421 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
423 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
424 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
425 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
426 WHERE aqbasket.basketno = ?
427 AND orderstatus IN ('new','ordered')
430 unless ( $basketno ) {
431 carp "No basketnumber given";
432 return { error => "no_basketno" };
434 $action = "ACQUISITION ORDER";
435 $sthorders = $dbh->prepare($strsth);
436 $sthorders->execute($basketno);
437 $dataorders = $sthorders->fetchall_arrayref( {} );
439 aqorders => [ map { $_->{ordernumber} } @$dataorders ]
443 my $booksellerid = $dataorders->[0]->{booksellerid};
444 my $addressee = $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
447 $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
448 $sthcontact->execute( $booksellerid );
449 my $datacontact = $sthcontact->fetchrow_hashref;
453 push @email, $datacontact->{email} if ( $datacontact && $datacontact->{email} );
455 warn "Bookseller $booksellerid without emails";
456 return { error => "no_email" };
459 while ($addlcontact = $sthcontact->fetchrow_hashref) {
460 push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
463 my $userenv = C4::Context->userenv;
464 my $letter = GetPreparedLetter (
466 letter_code => $letter_code,
467 branchcode => $userenv->{branch},
469 'branches' => $userenv->{branch},
470 'aqbooksellers' => $booksellerid,
471 'aqcontacts' => $datacontact,
472 'aqbasket' => $basketno,
474 repeat => $dataorders,
477 ) or return { error => "no_letter" };
479 # Remove the order tag
480 $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
483 my $library = Koha::Libraries->find( $userenv->{branch} );
484 my $mail = Koha::Email->create(
486 to => join( ',', @email ),
487 cc => join( ',', @cc ),
490 C4::Context->preference("ClaimsBccCopy")
491 && ( $type eq 'claimacquisition'
492 || $type eq 'claimissues' )
494 ? ( bcc => $userenv->{emailaddress} )
497 from => $library->branchemail
498 || C4::Context->preference('KohaAdminEmailAddress'),
499 subject => "" . $letter->{title},
503 if ( $letter->{is_html} ) {
504 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
507 $mail->text_body( "" . $letter->{content} );
511 $mail->send_or_die({ transport => $library->smtp_server->transport });
514 # We expect ref($_) eq 'Email::Sender::Failure'
515 $error = $_->message;
521 return { error => $error }
524 my $log_object = $action eq 'ACQUISITION ORDER' ? $externalid : undef;
525 my $module = $action eq 'ACQUISITION ORDER' ? 'ACQUISITIONS' : 'CLAIMS';
531 . join( ',', @email )
536 ) if C4::Context->preference("ClaimsLog");
539 # If we come here, return an OK status
543 =head2 GetPreparedLetter( %params )
546 module => letter module, mandatory
547 letter_code => letter code, mandatory
548 branchcode => for letter selection, if missing default system letter taken
549 tables => a hashref with table names as keys. Values are either:
550 - a scalar - primary key value
551 - an arrayref - primary key values
552 - a hashref - full record
553 substitute => custom substitution key/value pairs
554 repeat => records to be substituted on consecutive lines:
555 - an arrayref - tries to guess what needs substituting by
556 taking remaining << >> tokensr; not recommended
557 - a hashref token => @tables - replaces <token> << >> << >> </token>
558 subtemplate for each @tables row; table is a hashref as above
559 want_librarian => boolean, if set to true triggers librarian details
560 substitution from the userenv
562 letter fields hashref (title & content useful)
566 sub GetPreparedLetter {
569 my $letter = $params{letter};
570 my $lang = $params{lang} || 'default';
573 my $module = $params{module} or croak "No module";
574 my $letter_code = $params{letter_code} or croak "No letter_code";
575 my $branchcode = $params{branchcode} || '';
576 my $mtt = $params{message_transport_type} || 'email';
578 my $template = Koha::Notice::Templates->find_effective_template(
581 code => $letter_code,
582 branchcode => $branchcode,
583 message_transport_type => $mtt,
588 unless ( $template ) {
589 warn( "No $module $letter_code letter transported by " . $mtt );
593 $letter = $template->unblessed;
594 $letter->{'content-type'} = 'text/html; charset="UTF-8"' if $letter->{is_html};
597 my $objects = $params{objects} || {};
598 my $tables = $params{tables} || {};
599 my $substitute = $params{substitute} || {};
600 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
601 my $repeat = $params{repeat};
602 %$tables || %$substitute || $repeat || %$loops || %$objects
603 or carp( "ERROR: nothing to substitute - all of 'objects', 'tables', 'loops' and 'substitute' are empty" ),
605 my $want_librarian = $params{want_librarian};
608 while ( my ($token, $val) = each %$substitute ) {
610 if ( $token eq 'items.content' ) {
611 $val =~ s|\n|<br/>|g if $letter->{is_html};
614 $letter->{title} =~ s/<<$token>>/$val/g;
615 $letter->{content} =~ s/<<$token>>/$val/g;
619 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
620 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
622 if ($want_librarian) {
623 # parsing librarian name
624 my $userenv = C4::Context->userenv;
625 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
626 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
627 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
630 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
633 if (ref ($repeat) eq 'ARRAY' ) {
634 $repeat_no_enclosing_tags = $repeat;
636 $repeat_enclosing_tags = $repeat;
640 if ($repeat_enclosing_tags) {
641 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
642 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
645 my %subletter = ( title => '', content => $subcontent );
646 _substitute_tables( \%subletter, $_ );
649 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
655 _substitute_tables( $letter, $tables );
658 if ($repeat_no_enclosing_tags) {
659 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
664 $c =~ s/<<count>>/$i/go;
665 foreach my $field ( keys %{$_} ) {
666 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
670 } @$repeat_no_enclosing_tags;
672 my $replaceby = join( "\n", @lines );
673 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
677 $letter->{content} = _process_tt(
679 content => $letter->{content},
683 substitute => $substitute,
688 $letter->{title} = _process_tt(
690 content => $letter->{title},
694 substitute => $substitute,
699 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
704 sub _substitute_tables {
705 my ( $letter, $tables ) = @_;
706 while ( my ($table, $param) = each %$tables ) {
709 my $ref = ref $param;
712 if ($ref && $ref eq 'HASH') {
716 my $sth = _parseletter_sth($table);
718 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
721 $sth->execute( $ref ? @$param : $param );
723 $values = $sth->fetchrow_hashref;
727 _parseletter ( $letter, $table, $values );
731 sub _parseletter_sth {
735 carp "ERROR: _parseletter_sth() called without argument (table)";
738 # NOTE: we used to check whether we had a statement handle cached in
739 # a %handles module-level variable. This was a dumb move and
740 # broke things for the rest of us. prepare_cached is a better
741 # way to cache statement handles anyway.
743 ($table eq 'accountlines' ) ? "SELECT * FROM $table WHERE accountlines_id = ?" :
744 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
745 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
746 ($table eq 'tickets' ) ? "SELECT * FROM $table WHERE id = ?" :
747 ($table eq 'ticket_updates' ) ? "SELECT * FROM $table WHERE id = ?" :
748 ($table eq 'credits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
749 ($table eq 'debits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
750 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
751 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
752 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE issue_id = ?" :
753 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
754 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
755 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
756 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
757 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
758 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
759 ($table eq 'aqbasket' ) ? "SELECT * FROM $table WHERE basketno = ?" :
760 ($table eq 'illrequests' ) ? "SELECT * FROM $table WHERE illrequest_id = ?" :
761 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
762 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
763 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
764 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
765 ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" :
766 ($table eq 'additional_contents' || $table eq 'opac_news') ? "SELECT * FROM additional_contents WHERE idnew = ?" :
767 ($table eq 'recalls') ? "SELECT * FROM $table WHERE recall_id = ?" :
770 warn "ERROR: No _parseletter_sth query for table '$table'";
771 return; # nothing to get
773 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
774 warn "ERROR: Failed to prepare query: '$query'";
777 return $sth; # now cache is populated for that $table
780 =head2 _parseletter($letter, $table, $values)
783 - $letter : a hash to letter fields (title & content useful)
784 - $table : the Koha table to parse.
785 - $values_in : table record hashref
786 parse all fields from a table, and replace values in title & content with the appropriate value
787 (not exported sub, used only internally)
792 my ( $letter, $table, $values_in ) = @_;
794 # Work on a local copy of $values_in (passed by reference) to avoid side effects
795 # in callers ( by changing / formatting values )
796 my $values = $values_in ? { %$values_in } : {};
798 # FIXME Dates formatting must be done in notice's templates
799 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
800 $values->{'dateexpiry'} = output_pref({ dt => dt_from_string( $values->{'dateexpiry'} ), dateonly => 1 });
803 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
804 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
807 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
808 my $todaysdate = output_pref( dt_from_string() );
809 $letter->{content} =~ s/<<today>>/$todaysdate/go;
812 while ( my ($field, $val) = each %$values ) {
813 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
814 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
815 #Therefore adding the test on biblio. This includes biblioitems,
816 #but excludes items. Removed unneeded global and lookahead.
818 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
819 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
820 $val = $av->count ? $av->next->lib : '';
824 my $replacedby = defined ($val) ? $val : '';
826 and not $replacedby =~ m|9999-12-31|
827 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
829 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
830 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
831 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
833 for my $letter_field ( qw( title content ) ) {
834 my $filter_string_used = q{};
835 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
836 # We overwrite $dateonly if the filter exists and we have a time in the datetime
837 $filter_string_used = $1 || q{};
838 $dateonly = $1 unless $dateonly;
840 my $replacedby_date = eval {
841 output_pref({ dt => scalar dt_from_string( $replacedby ), dateonly => $dateonly });
843 $replacedby_date //= q{};
845 if ( $letter->{ $letter_field } ) {
846 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
847 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
851 # Other fields replacement
853 for my $letter_field ( qw( title content ) ) {
854 if ( $letter->{ $letter_field } ) {
855 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
856 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
862 if ($table eq 'borrowers' && $letter->{content}) {
863 my $patron = Koha::Patrons->find( $values->{borrowernumber} );
865 my $attributes = $patron->extended_attributes;
867 while ( my $attribute = $attributes->next ) {
868 my $code = $attribute->code;
869 my $val = $attribute->description; # FIXME - we always display intranet description here!
870 $val =~ s/\p{P}(?=$)//g if $val;
871 next unless $val gt '';
873 push @{ $attr{$code} }, $val;
875 while ( my ($code, $val_ar) = each %attr ) {
876 my $replacefield = "<<borrower-attribute:$code>>";
877 my $replacedby = join ',', @$val_ar;
878 $letter->{content} =~ s/$replacefield/$replacedby/g;
887 my $success = EnqueueLetter( { letter => $letter,
888 borrowernumber => '12', message_transport_type => 'email' } )
890 Places a letter in the message_queue database table, which will
891 eventually get processed (sent) by the process_message_queue.pl
892 cronjob when it calls SendQueuedMessages.
894 Return message_id on success
897 * letter - required; A letter hashref as returned from GetPreparedLetter
898 * message_transport_type - required; One of the available mtts
899 * borrowernumber - optional if 'to_address' is passed; The borrowernumber of the patron we enqueuing the notice for
900 * to_address - optional if 'borrowernumber' is passed; The destination email address for the notice (defaults to patron->notice_email_address)
901 * from_address - optional; The from address for the notice, defaults to patron->library->from_email_address
902 * reply_address - optional; The reply address for the notice, defaults to patron->library->reply_to
907 my $params = shift or return;
909 return unless exists $params->{'letter'};
910 # return unless exists $params->{'borrowernumber'};
911 return unless exists $params->{'message_transport_type'};
913 my $content = $params->{letter}->{content};
914 $content =~ s/\s+//g if(defined $content);
915 if ( not defined $content or $content eq '' ) {
916 Koha::Logger->get->info("Trying to add an empty message to the message queue");
920 # If we have any attachments we should encode then into the body.
921 if ( $params->{'attachments'} ) {
922 $params->{'letter'} = _add_attachments(
923 { letter => $params->{'letter'},
924 attachments => $params->{'attachments'},
929 my $message = Koha::Notice::Message->new(
931 letter_id => $params->{letter}->{id} || undef,
932 borrowernumber => $params->{borrowernumber},
933 subject => $params->{letter}->{title},
934 content => $params->{letter}->{content},
935 metadata => $params->{letter}->{metadata} || q{},
936 letter_code => $params->{letter}->{code} || q{},
937 message_transport_type => $params->{message_transport_type},
939 time_queued => dt_from_string(),
940 to_address => $params->{to_address},
941 from_address => $params->{from_address},
942 reply_address => $params->{reply_address},
943 content_type => $params->{letter}->{'content-type'},
944 failure_code => $params->{failure_code} || q{},
950 =head2 SendQueuedMessages ([$hashref])
952 my $sent = SendQueuedMessages({
954 borrowernumber => $who_letter_is_for,
955 letter_code => $letter_code, # can be scalar or arrayref
956 type => $type, # can be scalar or arrayref
962 Sends 'pending' messages from the queue, based on parameters.
964 The (optional) message_id, borrowernumber, letter_code, type and where
965 parameter are used to select which pending messages will be processed. The
966 limit parameter determines the volume of results, i.e. sent messages.
968 The optional verbose parameter can be used to generate debugging output.
970 Returns number of messages sent.
974 sub SendQueuedMessages {
976 my $limit = $params->{limit};
977 my $where = $params->{where};
979 Koha::Exceptions::BadParameter->throw("Parameter message_id cannot be empty if passed.")
980 if ( exists( $params->{message_id} ) && !$params->{message_id} );
982 my $smtp_transports = {};
984 my $count_messages = 0;
985 my $unsent_messages = Koha::Notice::Messages->search({
987 $params->{message_id} ? ( message_id => $params->{message_id} ) : (),
988 $params->{borrowernumber} ? ( borrowernumber => $params->{borrowernumber} ) : (),
989 # Check for scalar or array in letter_code and type
990 ref($params->{letter_code}) && @{$params->{letter_code}} ? ( letter_code => $params->{letter_code} ) : (),
991 !ref($params->{letter_code}) && $params->{letter_code} ? ( letter_code => $params->{letter_code} ) : (),
992 ref($params->{type}) && @{$params->{type}} ? ( message_transport_type => $params->{type} ) : (), #TODO Existing inconsistency
993 !ref($params->{type}) && $params->{type} ? ( message_transport_type => $params->{type} ) : (), #TODO Existing inconsistency
995 $unsent_messages = $unsent_messages->search( \$where ) if $where;
997 $domain_limits = Koha::Notice::Util->load_domain_limits; # (re)initialize per run
998 while( ( my $message_object = $unsent_messages->next ) && ( !$limit || $count_messages < $limit ) ) {
999 my $message = $message_object->unblessed;
1001 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
1002 $message_object->make_column_dirty('status');
1003 return unless $message_object->store;
1005 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
1006 warn sprintf( 'Processing %s message to patron: %s',
1007 $message->{'message_transport_type'},
1008 $message->{'borrowernumber'} || 'Admin' )
1009 if $params->{'verbose'};
1010 # This is just begging for subclassing
1011 next if ( lc($message->{'message_transport_type'}) eq 'rss' );
1012 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
1013 my $rv = _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'}, $smtp_transports );
1014 $count_messages++ if $rv;
1016 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
1017 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
1018 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1019 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1020 unless ( $sms_provider ) {
1021 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1022 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1025 unless ( $patron->smsalertnumber ) {
1026 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1027 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1030 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1031 $message->{to_address} .= '@' . $sms_provider->domain();
1033 # Check for possible from_address override
1034 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1035 if ($from_address && $message->{from_address} ne $from_address) {
1036 $message->{from_address} = $from_address;
1037 _update_message_from_address($message->{'message_id'}, $message->{from_address});
1040 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1041 my $rv = _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1042 $count_messages++ if $rv;
1044 my $rv = _send_message_by_sms( $message );
1045 $count_messages++ if $rv;
1049 return $count_messages;
1052 =head2 GetRSSMessages
1054 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1056 returns a listref of all queued RSS messages for a particular person.
1060 sub GetRSSMessages {
1063 return unless $params;
1064 return unless ref $params;
1065 return unless $params->{'borrowernumber'};
1067 return _get_unsent_messages( { message_transport_type => 'rss',
1068 limit => $params->{'limit'},
1069 borrowernumber => $params->{'borrowernumber'}, } );
1072 =head2 GetPrintMessages
1074 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1076 Returns a arrayref of all queued print messages (optionally, for a particular
1081 sub GetPrintMessages {
1082 my $params = shift || {};
1084 return _get_unsent_messages( { message_transport_type => 'print',
1085 borrowernumber => $params->{'borrowernumber'},
1089 =head2 GetQueuedMessages ([$hashref])
1091 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1093 Fetches a list of messages from the message queue optionally filtered by borrowernumber
1094 and limited to specified limit.
1096 Return is an arrayref of hashes, each has represents a message in the message queue.
1100 sub GetQueuedMessages {
1103 my $dbh = C4::Context->dbh();
1104 my $statement = << 'ENDSQL';
1105 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued, updated_on, failure_code
1111 if ( exists $params->{'borrowernumber'} ) {
1112 push @whereclauses, ' borrowernumber = ? ';
1113 push @query_params, $params->{'borrowernumber'};
1116 if ( @whereclauses ) {
1117 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1120 if ( defined $params->{'limit'} ) {
1121 $statement .= ' LIMIT ? ';
1122 push @query_params, $params->{'limit'};
1125 my $sth = $dbh->prepare( $statement );
1126 my $result = $sth->execute( @query_params );
1127 return $sth->fetchall_arrayref({});
1130 =head2 GetMessageTransportTypes
1132 my @mtt = GetMessageTransportTypes();
1134 returns an arrayref of transport types
1138 sub GetMessageTransportTypes {
1139 my $dbh = C4::Context->dbh();
1140 my $mtts = $dbh->selectcol_arrayref("
1141 SELECT message_transport_type
1142 FROM message_transport_types
1143 ORDER BY message_transport_type
1150 my $message = C4::Letters::Message($message_id);
1155 my ( $message_id ) = @_;
1156 return unless $message_id;
1157 my $dbh = C4::Context->dbh;
1158 return $dbh->selectrow_hashref(q|
1159 SELECT message_id, borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, updated_on, to_address, from_address, reply_address, content_type, failure_code
1161 WHERE message_id = ?
1162 |, {}, $message_id );
1165 =head2 ResendMessage
1167 Attempt to resend a message which has failed previously.
1169 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1171 Updates the message to 'pending' status so that
1172 it will be resent later on.
1174 returns 1 on success, 0 on failure, undef if no message was found
1179 my $message_id = shift;
1180 return unless $message_id;
1182 my $message = GetMessage( $message_id );
1183 return unless $message;
1185 if ( $message->{status} ne 'pending' ) {
1186 $rv = C4::Letters::_set_message_status({
1187 message_id => $message_id,
1188 status => 'pending',
1190 $rv = $rv > 0? 1: 0;
1191 # Clear destination email address to force address update
1192 _update_message_to_address( $message_id, undef ) if $rv &&
1193 $message->{message_transport_type} eq 'email';
1198 =head2 _add_attachements
1200 _add_attachments({ letter => $letter, attachments => $attachments });
1203 letter - the standard letter hashref
1204 attachments - listref of attachments. each attachment is a hashref of:
1205 type - the mime type, like 'text/plain'
1206 content - the actual attachment
1207 filename - the name of the attachment.
1209 returns your letter object, with the content updated.
1210 This routine picks the I<content> of I<letter> and generates a MIME
1211 email, attaching the passed I<attachments> using Koha::Email. The
1212 content is replaced by the string representation of the MIME object,
1213 and the content-type is updated for later handling.
1217 sub _add_attachments {
1220 my $letter = $params->{letter};
1221 my $attachments = $params->{attachments};
1222 return $letter unless @$attachments;
1224 my $message = Koha::Email->new;
1226 if ( $letter->{is_html} ) {
1227 $message->html_body( _wrap_html( $letter->{content}, $letter->{title} ) );
1230 $message->text_body( $letter->{content} );
1233 foreach my $attachment ( @$attachments ) {
1235 Encode::encode( "UTF-8", $attachment->{content} ),
1236 content_type => $attachment->{type} || 'application/octet-stream',
1237 name => $attachment->{filename},
1238 disposition => 'attachment',
1242 $letter->{'content-type'} = SERIALIZED_EMAIL_CONTENT_TYPE;
1243 $letter->{content} = $message->as_string;
1249 =head2 _get_unsent_messages
1251 This function's parameter hash reference takes the following
1252 optional named parameters:
1253 message_transport_type: method of message sending (e.g. email, sms, etc.)
1254 Can be a single string, or an arrayref of strings
1255 borrowernumber : who the message is to be sent
1256 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1257 Can be a single string, or an arrayref of strings
1258 message_id : the message_id of the message. In that case the sub will return only 1 result
1259 limit : maximum number of messages to send
1261 This function returns an array of matching hash referenced rows from
1262 message_queue with some borrower information added.
1266 sub _get_unsent_messages {
1269 my $dbh = C4::Context->dbh();
1271 SELECT mq.message_id, mq.borrowernumber, mq.subject, mq.content, mq.message_transport_type, mq.status, mq.time_queued, mq.from_address, mq.reply_address, mq.to_address, mq.content_type, b.branchcode, mq.letter_code, mq.failure_code
1272 FROM message_queue mq
1273 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1277 my @query_params = ('pending');
1278 if ( ref $params ) {
1279 if ( $params->{'borrowernumber'} ) {
1280 $statement .= ' AND mq.borrowernumber = ? ';
1281 push @query_params, $params->{'borrowernumber'};
1283 if ( $params->{'letter_code'} ) {
1284 my @letter_codes = ref $params->{'letter_code'} eq "ARRAY" ? @{$params->{'letter_code'}} : $params->{'letter_code'};
1285 if ( @letter_codes ) {
1286 my $q = join( ",", ("?") x @letter_codes );
1287 $statement .= " AND mq.letter_code IN ( $q ) ";
1288 push @query_params, @letter_codes;
1291 if ( $params->{'message_transport_type'} ) {
1292 my @types = ref $params->{'message_transport_type'} eq "ARRAY" ? @{$params->{'message_transport_type'}} : $params->{'message_transport_type'};
1294 my $q = join( ",", ("?") x @types );
1295 $statement .= " AND message_transport_type IN ( $q ) ";
1296 push @query_params, @types;
1299 if ( $params->{message_id} ) {
1300 $statement .= ' AND message_id = ?';
1301 push @query_params, $params->{message_id};
1303 if ( $params->{where} ) {
1304 $statement .= " AND $params->{where} ";
1306 if ( $params->{'limit'} ) {
1307 $statement .= ' limit ? ';
1308 push @query_params, $params->{'limit'};
1312 my $sth = $dbh->prepare( $statement );
1313 my $result = $sth->execute( @query_params );
1314 return $sth->fetchall_arrayref({});
1317 sub _send_message_by_email {
1318 my $message = shift or return;
1319 my ( $username, $password, $method, $smtp_transports ) = @_;
1321 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1322 my $to_address = $message->{'to_address'};
1324 my @guarantor_address;
1325 my $count_guarantor_address;
1326 if (C4::Context->preference('RedirectGuaranteeEmail') eq 'yes' && $patron) {
1327 #Get guanrantor adresses
1328 my $guarantor_relationships = $patron->guarantor_relationships;
1329 my @guarantors = $guarantor_relationships->guarantors->as_list;
1330 foreach my $guarantor (@guarantors) {
1331 my $address = $guarantor->notice_email_address;
1332 push( @guarantor_address, $address ) if $address;
1334 $count_guarantor_address = scalar @guarantor_address;
1336 unless ($to_address) {
1337 if (!$patron && !$count_guarantor_address) {
1338 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1339 _set_message_status(
1341 message_id => $message->{'message_id'},
1343 failure_code => 'INVALID_BORNUMBER'
1349 $to_address = $patron->notice_email_address;
1351 if (!$to_address && !$count_guarantor_address) {
1352 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1353 # warning too verbose for this more common case?
1354 _set_message_status(
1356 message_id => $message->{'message_id'},
1358 failure_code => 'NO_EMAIL'
1363 if (!$to_address && $count_guarantor_address) {
1364 $to_address = shift @guarantor_address;
1368 $cc_address = join( ',', @guarantor_address );
1369 # Skip this message if we exceed domain limits in this run
1370 if( Koha::Notice::Util->exceeds_limit({ to => $to_address, limits => $domain_limits }) ) {
1371 # Save the to_address if you delay the message so that we dont need to look it up again
1372 _update_message_to_address( $message->{'message_id'}, $to_address )
1373 if !$message->{to_address};
1377 my $subject = $message->{'subject'};
1379 my $content = $message->{'content'};
1380 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1381 my $is_html = $content_type =~ m/html/io;
1383 my $branch_email = undef;
1384 my $branch_replyto = undef;
1385 my $branch_returnpath = undef;
1388 $patron //= Koha::Patrons->find( $message->{borrowernumber} ); # we might already found him
1390 $library = $patron->library;
1391 $branch_email = $library->from_email_address;
1392 $branch_replyto = $library->branchreplyto;
1393 $branch_returnpath = $library->branchreturnpath;
1396 # NOTE: Patron may not be defined above so branch_email may be undefined still
1397 # so we need to fallback to KohaAdminEmailAddress as a last resort.
1399 $message->{'from_address'}
1401 || C4::Context->preference('KohaAdminEmailAddress');
1402 if( !$from_address ) {
1403 _set_message_status(
1405 message_id => $message->{'message_id'},
1407 failure_code => 'NO_FROM',
1419 C4::Context->preference('NoticeBcc')
1420 ? ( bcc => C4::Context->preference('NoticeBcc') )
1425 ? ( cc => $cc_address )
1428 from => $from_address,
1429 reply_to => $message->{'reply_address'} || $branch_replyto,
1430 sender => $branch_returnpath,
1431 subject => "" . $message->{subject}
1434 if ( $message->{'content_type'} && $message->{'content_type'} eq SERIALIZED_EMAIL_CONTENT_TYPE ) {
1436 # The message has been previously composed as a valid MIME object
1437 # and serialized as a string on the DB
1438 $email = Koha::Email->new_from_string($content);
1439 $email->create($params);
1441 $email = Koha::Email->create($params);
1443 $email->html_body( _wrap_html( $content, $subject ) );
1445 $email->text_body($content);
1450 if ( ref($_) eq 'Koha::Exceptions::BadParameter' ) {
1451 _set_message_status(
1453 message_id => $message->{'message_id'},
1455 failure_code => "INVALID_EMAIL:".$_->parameter
1459 _set_message_status(
1461 message_id => $message->{'message_id'},
1463 failure_code => 'UNKNOWN_ERROR'
1469 return unless $email;
1473 $smtp_server = $library->smtp_server;
1476 $smtp_server = Koha::SMTP::Servers->get_default;
1482 sasl_username => $username,
1483 sasl_password => $password,
1488 # if initial message address was empty, coming here means that a to address was found and
1489 # queue should be updated; same if to address was overriden by Koha::Email->create
1490 _update_message_to_address( $message->{'message_id'}, $email->email->header('To') )
1491 if !$message->{to_address}
1492 || $message->{to_address} ne $email->email->header('To');
1494 $smtp_transports->{ $smtp_server->id // 'default' } ||= $smtp_server->transport;
1495 my $smtp_transport = $smtp_transports->{ $smtp_server->id // 'default' };
1497 _update_message_from_address($message->{'message_id'},$email->email->header('From') )
1498 if !$message->{from_address}
1499 || $message->{from_address} ne $email->email->header('From');
1502 $email->send_or_die({ transport => $smtp_transport });
1504 _set_message_status(
1506 message_id => $message->{'message_id'},
1514 _set_message_status(
1516 message_id => $message->{'message_id'},
1518 failure_code => 'SENDMAIL'
1522 carp "$Mail::Sendmail::error";
1528 my ($content, $title) = @_;
1530 my $css = C4::Context->preference("NoticeCSS") || '';
1531 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1533 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1534 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1535 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1537 <title>$title</title>
1538 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1549 my ( $message ) = @_;
1550 my $dbh = C4::Context->dbh;
1551 my $count = $dbh->selectrow_array(q|
1554 WHERE message_transport_type = ?
1555 AND borrowernumber = ?
1557 AND CAST(updated_on AS date) = CAST(NOW() AS date)
1560 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1564 sub _send_message_by_sms {
1565 my $message = shift or return;
1566 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1567 _update_message_to_address($message->{message_id}, $patron->smsalertnumber) if $patron;
1569 unless ( $patron and $patron->smsalertnumber ) {
1570 _set_message_status( { message_id => $message->{'message_id'},
1572 failure_code => 'MISSING_SMS' } );
1576 if ( _is_duplicate( $message ) ) {
1577 _set_message_status(
1579 message_id => $message->{'message_id'},
1581 failure_code => 'DUPLICATE_MESSAGE'
1587 my $success = C4::SMS->send_sms(
1589 destination => $patron->smsalertnumber,
1590 message => $message->{'content'},
1595 _set_message_status(
1597 message_id => $message->{'message_id'},
1604 _set_message_status(
1606 message_id => $message->{'message_id'},
1608 failure_code => 'NO_NOTES'
1616 sub _update_message_to_address {
1618 my $dbh = C4::Context->dbh();
1619 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1622 sub _update_message_from_address {
1623 my ($message_id, $from_address) = @_;
1624 my $dbh = C4::Context->dbh();
1625 $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1628 sub _set_message_status {
1629 my $params = shift or return;
1631 foreach my $required_parameter ( qw( message_id status ) ) {
1632 return unless exists $params->{ $required_parameter };
1635 my $dbh = C4::Context->dbh();
1636 my $statement = 'UPDATE message_queue SET status= ?, failure_code= ? WHERE message_id = ?';
1637 my $sth = $dbh->prepare( $statement );
1638 my $result = $sth->execute( $params->{'status'},
1639 $params->{'failure_code'} || '',
1640 $params->{'message_id'} );
1645 my ( $params ) = @_;
1647 my $content = $params->{content};
1648 my $tables = $params->{tables};
1649 my $loops = $params->{loops};
1650 my $objects = $params->{objects} || {};
1651 my $substitute = $params->{substitute} || {};
1652 my $lang = defined($params->{lang}) && $params->{lang} ne 'default' ? $params->{lang} : 'en';
1653 my ($theme, $availablethemes);
1655 my $htdocs = C4::Context->config('intrahtdocs');
1656 ($theme, $lang, $availablethemes)= C4::Templates::availablethemes( $htdocs, 'about.tt', 'intranet', $lang);
1658 foreach (@$availablethemes) {
1659 push @includes, "$htdocs/$_/$lang/includes";
1660 push @includes, "$htdocs/$_/en/includes" unless $lang eq 'en';
1663 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1664 my $template = Template->new(
1668 PLUGIN_BASE => 'Koha::Template::Plugin',
1669 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1670 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1671 INCLUDE_PATH => \@includes,
1673 ENCODING => 'UTF-8',
1675 ) or die Template->error();
1677 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute, %$objects };
1679 $content = add_tt_filters( $content );
1680 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1683 my $schema = Koha::Database->new->schema;
1685 my $processed = try {
1686 $template->process( \$content, $tt_params, \$output );
1689 $schema->txn_rollback;
1691 croak "ERROR PROCESSING TEMPLATE: " . $template->error() unless $processed;
1696 sub _get_tt_params {
1697 my ($tables, $is_a_loop) = @_;
1703 article_requests => {
1704 module => 'Koha::ArticleRequests',
1705 singular => 'article_request',
1706 plural => 'article_requests',
1710 module => 'Koha::Acquisition::Baskets',
1711 singular => 'basket',
1712 plural => 'baskets',
1716 module => 'Koha::Acquisition::Booksellers',
1717 singular => 'bookseller',
1718 plural => 'booksellers',
1722 module => 'Koha::Biblios',
1723 singular => 'biblio',
1724 plural => 'biblios',
1725 pk => 'biblionumber',
1728 module => 'Koha::Biblioitems',
1729 singular => 'biblioitem',
1730 plural => 'biblioitems',
1731 pk => 'biblioitemnumber',
1734 module => 'Koha::Patrons',
1735 singular => 'borrower',
1736 plural => 'borrowers',
1737 pk => 'borrowernumber',
1740 module => 'Koha::Libraries',
1741 singular => 'branch',
1742 plural => 'branches',
1746 module => 'Koha::Account::Lines',
1747 singular => 'credit',
1748 plural => 'credits',
1749 pk => 'accountlines_id',
1752 module => 'Koha::Account::Lines',
1753 singular => 'debit',
1755 pk => 'accountlines_id',
1758 module => 'Koha::Items',
1763 additional_contents => {
1764 module => 'Koha::AdditionalContents',
1765 singular => 'additional_content',
1766 plural => 'additional_contents',
1770 module => 'Koha::AdditionalContents',
1776 module => 'Koha::Acquisition::Orders',
1777 singular => 'order',
1779 pk => 'ordernumber',
1782 module => 'Koha::Holds',
1788 module => 'Koha::Serials',
1789 singular => 'serial',
1790 plural => 'serials',
1794 module => 'Koha::Subscriptions',
1795 singular => 'subscription',
1796 plural => 'subscriptions',
1797 pk => 'subscriptionid',
1800 module => 'Koha::Suggestions',
1801 singular => 'suggestion',
1802 plural => 'suggestions',
1803 pk => 'suggestionid',
1806 module => 'Koha::Tickets',
1807 singular => 'ticket',
1808 plural => 'tickets',
1812 module => 'Koha::Ticket::Updates',
1813 singular => 'ticket_update',
1814 plural => 'ticket_updates',
1818 module => 'Koha::Checkouts',
1819 singular => 'checkout',
1820 plural => 'checkouts',
1824 module => 'Koha::Old::Checkouts',
1825 singular => 'old_checkout',
1826 plural => 'old_checkouts',
1830 module => 'Koha::Checkouts',
1831 singular => 'overdue',
1832 plural => 'overdues',
1835 borrower_modifications => {
1836 module => 'Koha::Patron::Modifications',
1837 singular => 'patron_modification',
1838 plural => 'patron_modifications',
1839 fk => 'verification_token',
1842 module => 'Koha::Illrequests',
1843 singular => 'illrequest',
1844 plural => 'illrequests',
1845 pk => 'illrequest_id'
1849 foreach my $table ( keys %$tables ) {
1850 next unless $config->{$table};
1852 my $ref = ref( $tables->{$table} ) || q{};
1853 my $module = $config->{$table}->{module};
1855 if ( can_load( modules => { $module => undef } ) ) {
1856 my $pk = $config->{$table}->{pk};
1857 my $fk = $config->{$table}->{fk};
1860 my $values = $tables->{$table} || [];
1861 unless ( ref( $values ) eq 'ARRAY' ) {
1862 croak "ERROR processing table $table. Wrong API call.";
1864 my $key = $pk ? $pk : $fk;
1865 # $key does not come from user input
1866 my $objects = $module->search(
1867 { $key => $values },
1869 # We want to retrieve the data in the same order
1871 # field is a MySQLism, but they are no other way to do it
1872 # To be generic we could do it in perl, but we will need to fetch
1873 # all the data then order them
1874 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1877 $params->{ $config->{$table}->{plural} } = $objects;
1879 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1880 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1882 if ( $fk ) { # Using a foreign key for lookup
1883 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1885 foreach my $key ( @$fk ) {
1886 $search->{$key} = $id->{$key};
1888 $object = $module->search( $search )->last();
1889 } else { # Foreign key is single column
1890 $object = $module->search( { $fk => $id } )->last();
1892 } else { # using the table's primary key for lookup
1893 $object = $module->find($id);
1895 $params->{ $config->{$table}->{singular} } = $object;
1897 else { # $ref eq 'ARRAY'
1899 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1900 $object = $module->search( { $pk => $tables->{$table} } )->last();
1902 else { # Params are mutliple foreign keys
1903 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1905 $params->{ $config->{$table}->{singular} } = $object;
1909 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1913 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1918 =head3 add_tt_filters
1920 $content = add_tt_filters( $content );
1922 Add TT filters to some specific fields if needed.
1924 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1928 sub add_tt_filters {
1929 my ( $content ) = @_;
1930 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1931 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1935 =head2 get_item_content
1937 my $item = Koha::Items->find(...)->unblessed;
1938 my @item_content_fields = qw( date_due title barcode author itemnumber );
1939 my $item_content = C4::Letters::get_item_content({
1941 item_content_fields => \@item_content_fields
1944 This function generates a tab-separated list of values for the passed item. Dates
1945 are formatted following the current setup.
1949 sub get_item_content {
1950 my ( $params ) = @_;
1951 my $item = $params->{item};
1952 my $dateonly = $params->{dateonly} || 0;
1953 my $item_content_fields = $params->{item_content_fields} || [];
1955 return unless $item;
1957 my @item_info = map {
1961 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1965 } @$item_content_fields;
1966 return join( "\t", @item_info ) . "\n";