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>.
23 use Date::Calc qw( Add_Delta_Days );
27 use Module::Load::Conditional qw(can_load);
37 use Koha::SMS::Providers;
40 use Koha::Notice::Messages;
41 use Koha::Notice::Templates;
42 use Koha::DateUtils qw( format_sqldatetime dt_from_string );
44 use Koha::SMTP::Servers;
45 use Koha::Subscriptions;
47 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
53 &EnqueueLetter &GetLetters &GetLettersAvailableForALibrary &GetLetterTemplates &DelLetter &GetPreparedLetter &GetWrappedLetter &SendAlerts &GetPrintMessages &GetMessageTransportTypes
59 C4::Letters - Give functions for Letters management
67 "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
68 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)
70 Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
72 =head2 GetLetters([$module])
74 $letters = &GetLetters($module);
75 returns informations about letters.
76 if needed, $module filters for letters given module
78 DEPRECATED - You must use Koha::Notice::Templates instead
79 The group by clause is confusing and can lead to issues
85 my $module = $filters->{module};
86 my $code = $filters->{code};
87 my $branchcode = $filters->{branchcode};
88 my $dbh = C4::Context->dbh;
89 my $letters = $dbh->selectall_arrayref(
91 SELECT code, module, name
95 . ( $module ? q| AND module = ?| : q|| )
96 . ( $code ? q| AND code = ?| : q|| )
97 . ( defined $branchcode ? q| AND branchcode = ?| : q|| )
98 . q| GROUP BY code, module, name ORDER BY name|, { Slice => {} }
99 , ( $module ? $module : () )
100 , ( $code ? $code : () )
101 , ( defined $branchcode ? $branchcode : () )
107 =head2 GetLetterTemplates
109 my $letter_templates = GetLetterTemplates(
111 module => 'circulation',
113 branchcode => 'CPL', # '' for default,
117 Return a hashref of letter templates.
121 sub GetLetterTemplates {
124 my $module = $params->{module};
125 my $code = $params->{code};
126 my $branchcode = $params->{branchcode} // '';
127 my $dbh = C4::Context->dbh;
128 return Koha::Notice::Templates->search(
132 branchcode => $branchcode,
134 C4::Context->preference('TranslateNotices')
136 : ( lang => 'default' )
142 =head2 GetLettersAvailableForALibrary
144 my $letters = GetLettersAvailableForALibrary(
146 branchcode => 'CPL', # '' for default
147 module => 'circulation',
151 Return an arrayref of letters, sorted by name.
152 If a specific letter exist for the given branchcode, it will be retrieve.
153 Otherwise the default letter will be.
157 sub GetLettersAvailableForALibrary {
159 my $branchcode = $filters->{branchcode};
160 my $module = $filters->{module};
162 croak "module should be provided" unless $module;
164 my $dbh = C4::Context->dbh;
165 my $default_letters = $dbh->selectall_arrayref(
167 SELECT module, code, branchcode, name
171 . q| AND branchcode = ''|
172 . ( $module ? q| AND module = ?| : q|| )
173 . q| ORDER BY name|, { Slice => {} }
174 , ( $module ? $module : () )
177 my $specific_letters;
179 $specific_letters = $dbh->selectall_arrayref(
181 SELECT module, code, branchcode, name
185 . q| AND branchcode = ?|
186 . ( $module ? q| AND module = ?| : q|| )
187 . q| ORDER BY name|, { Slice => {} }
189 , ( $module ? $module : () )
194 for my $l (@$default_letters) {
195 $letters{ $l->{code} } = $l;
197 for my $l (@$specific_letters) {
198 # Overwrite the default letter with the specific one.
199 $letters{ $l->{code} } = $l;
202 return [ map { $letters{$_} }
203 sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
209 my ( $module, $code, $branchcode, $message_transport_type, $lang) = @_;
210 $message_transport_type //= '%';
211 $lang = 'default' unless( $lang && C4::Context->preference('TranslateNotices') );
214 my $only_my_library = C4::Context->only_my_library;
215 if ( $only_my_library and $branchcode ) {
216 $branchcode = C4::Context::mybranch();
220 my $dbh = C4::Context->dbh;
221 my $sth = $dbh->prepare(q{
224 WHERE module=? AND code=? AND (branchcode = ? OR branchcode = '')
225 AND message_transport_type LIKE ?
227 ORDER BY branchcode DESC LIMIT 1
229 $sth->execute( $module, $code, $branchcode, $message_transport_type, $lang );
230 my $line = $sth->fetchrow_hashref
232 $line->{'content-type'} = 'text/html; charset="UTF-8"' if $line->{is_html};
242 module => 'circulation',
248 Delete the letter. The mtt parameter is facultative.
249 If not given, all templates mathing the other parameters will be removed.
255 my $branchcode = $params->{branchcode};
256 my $module = $params->{module};
257 my $code = $params->{code};
258 my $mtt = $params->{mtt};
259 my $lang = $params->{lang};
260 my $dbh = C4::Context->dbh;
267 . ( $mtt ? q| AND message_transport_type = ?| : q|| )
268 . ( $lang? q| AND lang = ?| : q|| )
269 , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
274 my $err = &SendAlerts($type, $externalid, $letter_code);
277 - $type : the type of alert
278 - $externalid : the id of the "object" to query
279 - $letter_code : the notice template to use
281 C<&SendAlerts> sends an email notice directly to a patron or a vendor.
283 Currently it supports ($type):
284 - claim serial issues (claimissues)
285 - claim acquisition orders (claimacquisition)
286 - send acquisition orders to the vendor (orderacquisition)
287 - notify patrons about newly received serial issues (issue)
288 - notify patrons when their account is created (members)
290 Returns undef or { error => 'message } on failure.
291 Returns true on success.
296 my ( $type, $externalid, $letter_code ) = @_;
297 my $dbh = C4::Context->dbh;
300 if ( $type eq 'issue' ) {
302 # prepare the letter...
303 # search the subscriptionid
306 "SELECT subscriptionid FROM serial WHERE serialid=?");
307 $sth->execute($externalid);
308 my ($subscriptionid) = $sth->fetchrow
309 or warn( "No subscription for '$externalid'" ),
312 # search the biblionumber
315 "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
316 $sth->execute($subscriptionid);
317 my ($biblionumber) = $sth->fetchrow
318 or warn( "No biblionumber for '$subscriptionid'" ),
321 # find the list of subscribers to notify
322 my $subscription = Koha::Subscriptions->find( $subscriptionid );
323 my $subscribers = $subscription->subscribers;
324 while ( my $patron = $subscribers->next ) {
325 my $email = $patron->email or next;
327 # warn "sending issues...";
328 my $userenv = C4::Context->userenv;
329 my $library = $patron->library;
330 my $letter = GetPreparedLetter (
332 letter_code => $letter_code,
333 branchcode => $userenv->{branch},
335 'branches' => $library->branchcode,
336 'biblio' => $biblionumber,
337 'biblioitems' => $biblionumber,
338 'borrowers' => $patron->unblessed,
339 'subscription' => $subscriptionid,
340 'serial' => $externalid,
345 # FIXME: This 'default' behaviour should be moved to Koha::Email
346 my $mail = Koha::Email->create(
349 from => $library->branchemail,
350 reply_to => $library->branchreplyto,
351 sender => $library->branchreturnpath,
352 subject => "" . $letter->{title},
356 if ( $letter->{is_html} ) {
357 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
360 $mail->text_body( $letter->{content} );
364 $mail->send_or_die({ transport => $library->smtp_server->transport });
367 # We expect ref($_) eq 'Email::Sender::Failure'
368 $error = $_->message;
374 return { error => $error }
378 elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
380 # prepare the letter...
386 if ( $type eq 'claimacquisition') {
388 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
390 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
391 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
392 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
393 WHERE aqorders.ordernumber IN (
397 carp "No order selected";
398 return { error => "no_order_selected" };
400 $strsth .= join( ",", ('?') x @$externalid ) . ")";
401 $action = "ACQUISITION CLAIM";
402 $sthorders = $dbh->prepare($strsth);
403 $sthorders->execute( @$externalid );
404 $dataorders = $sthorders->fetchall_arrayref( {} );
407 if ($type eq 'claimissues') {
409 SELECT serial.*,subscription.*, biblio.*, biblioitems.*, aqbooksellers.*,
410 aqbooksellers.id AS booksellerid
412 LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
413 LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
414 LEFT JOIN biblioitems ON serial.biblionumber = biblioitems.biblionumber
415 LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
416 WHERE serial.serialid IN (
420 carp "No issues selected";
421 return { error => "no_issues_selected" };
424 $strsth .= join( ",", ('?') x @$externalid ) . ")";
425 $action = "SERIAL CLAIM";
426 $sthorders = $dbh->prepare($strsth);
427 $sthorders->execute( @$externalid );
428 $dataorders = $sthorders->fetchall_arrayref( {} );
431 if ( $type eq 'orderacquisition') {
432 my $basketno = $externalid;
434 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
436 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
437 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
438 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
439 WHERE aqbasket.basketno = ?
440 AND orderstatus IN ('new','ordered')
443 unless ( $basketno ) {
444 carp "No basketnumber given";
445 return { error => "no_basketno" };
447 $action = "ACQUISITION ORDER";
448 $sthorders = $dbh->prepare($strsth);
449 $sthorders->execute($basketno);
450 $dataorders = $sthorders->fetchall_arrayref( {} );
454 $dbh->prepare("select * from aqbooksellers where id=?");
455 $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
456 my $databookseller = $sthbookseller->fetchrow_hashref;
458 my $addressee = $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
461 $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
462 $sthcontact->execute( $dataorders->[0]->{booksellerid} );
463 my $datacontact = $sthcontact->fetchrow_hashref;
467 push @email, $datacontact->{email} if ( $datacontact && $datacontact->{email} );
469 warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
470 return { error => "no_email" };
473 while ($addlcontact = $sthcontact->fetchrow_hashref) {
474 push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
477 my $userenv = C4::Context->userenv;
478 my $letter = GetPreparedLetter (
480 letter_code => $letter_code,
481 branchcode => $userenv->{branch},
483 'branches' => $userenv->{branch},
484 'aqbooksellers' => $databookseller,
485 'aqcontacts' => $datacontact,
486 'aqbasket' => $basketno,
488 repeat => $dataorders,
490 ) or return { error => "no_letter" };
492 # Remove the order tag
493 $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
496 my $library = Koha::Libraries->find( $userenv->{branch} );
497 my $mail = Koha::Email->create(
499 to => join( ',', @email ),
500 cc => join( ',', @cc ),
503 C4::Context->preference("ClaimsBccCopy")
504 && ( $type eq 'claimacquisition'
505 || $type eq 'claimissues' )
507 ? ( bcc => $userenv->{emailaddress} )
510 from => $library->branchemail
511 || C4::Context->preference('KohaAdminEmailAddress'),
512 subject => "" . $letter->{title},
516 if ( $letter->{is_html} ) {
517 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
520 $mail->text_body( "" . $letter->{content} );
524 $mail->send_or_die({ transport => $library->smtp_server->transport });
527 # We expect ref($_) eq 'Email::Sender::Failure'
528 $error = $_->message;
534 return { error => $error }
542 . join( ',', @email )
547 ) if C4::Context->preference("LetterLog");
549 # send an "account details" notice to a newly created user
550 elsif ( $type eq 'members' ) {
551 my $library = Koha::Libraries->find( $externalid->{branchcode} );
552 my $letter = GetPreparedLetter (
554 letter_code => $letter_code,
555 branchcode => $externalid->{'branchcode'},
556 lang => $externalid->{lang} || 'default',
558 'branches' => $library->unblessed,
559 'borrowers' => $externalid->{'borrowernumber'},
561 substitute => { 'borrowers.password' => $externalid->{'password'} },
564 return { error => "no_email" } unless $externalid->{'emailaddr'};
568 # FIXME: This 'default' behaviour should be moved to Koha::Email
569 my $mail = Koha::Email->create(
571 to => $externalid->{'emailaddr'},
572 from => $library->branchemail,
573 reply_to => $library->branchreplyto,
574 sender => $library->branchreturnpath,
575 subject => "" . $letter->{'title'},
579 if ( $letter->{is_html} ) {
580 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
583 $mail->text_body( $letter->{content} );
586 $mail->send_or_die({ transport => $library->smtp_server->transport });
589 # We expect ref($_) eq 'Email::Sender::Failure'
590 $error = $_->message;
596 return { error => $error }
600 # If we come here, return an OK status
604 =head2 GetPreparedLetter( %params )
607 module => letter module, mandatory
608 letter_code => letter code, mandatory
609 branchcode => for letter selection, if missing default system letter taken
610 tables => a hashref with table names as keys. Values are either:
611 - a scalar - primary key value
612 - an arrayref - primary key values
613 - a hashref - full record
614 substitute => custom substitution key/value pairs
615 repeat => records to be substituted on consecutive lines:
616 - an arrayref - tries to guess what needs substituting by
617 taking remaining << >> tokensr; not recommended
618 - a hashref token => @tables - replaces <token> << >> << >> </token>
619 subtemplate for each @tables row; table is a hashref as above
620 want_librarian => boolean, if set to true triggers librarian details
621 substitution from the userenv
623 letter fields hashref (title & content useful)
627 sub GetPreparedLetter {
630 my $letter = $params{letter};
631 my $lang = $params{lang} || 'default';
634 my $module = $params{module} or croak "No module";
635 my $letter_code = $params{letter_code} or croak "No letter_code";
636 my $branchcode = $params{branchcode} || '';
637 my $mtt = $params{message_transport_type} || 'email';
639 $letter = getletter( $module, $letter_code, $branchcode, $mtt, $lang );
642 $letter = getletter( $module, $letter_code, $branchcode, $mtt, 'default' )
643 or warn( "No $module $letter_code letter transported by " . $mtt ),
648 my $tables = $params{tables} || {};
649 my $substitute = $params{substitute} || {};
650 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
651 my $repeat = $params{repeat};
652 %$tables || %$substitute || $repeat || %$loops
653 or carp( "ERROR: nothing to substitute - both 'tables', 'loops' and 'substitute' are empty" ),
655 my $want_librarian = $params{want_librarian};
658 while ( my ($token, $val) = each %$substitute ) {
659 if ( $token eq 'items.content' ) {
660 $val =~ s|\n|<br/>|g if $letter->{is_html};
663 $letter->{title} =~ s/<<$token>>/$val/g;
664 $letter->{content} =~ s/<<$token>>/$val/g;
668 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
669 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
671 if ($want_librarian) {
672 # parsing librarian name
673 my $userenv = C4::Context->userenv;
674 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
675 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
676 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
679 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
682 if (ref ($repeat) eq 'ARRAY' ) {
683 $repeat_no_enclosing_tags = $repeat;
685 $repeat_enclosing_tags = $repeat;
689 if ($repeat_enclosing_tags) {
690 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
691 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
694 my %subletter = ( title => '', content => $subcontent );
695 _substitute_tables( \%subletter, $_ );
698 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
704 _substitute_tables( $letter, $tables );
707 if ($repeat_no_enclosing_tags) {
708 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
713 $c =~ s/<<count>>/$i/go;
714 foreach my $field ( keys %{$_} ) {
715 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
719 } @$repeat_no_enclosing_tags;
721 my $replaceby = join( "\n", @lines );
722 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
726 $letter->{content} = _process_tt(
728 content => $letter->{content},
731 substitute => $substitute,
736 $letter->{title} = _process_tt(
738 content => $letter->{title},
741 substitute => $substitute,
745 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
750 sub _substitute_tables {
751 my ( $letter, $tables ) = @_;
752 while ( my ($table, $param) = each %$tables ) {
755 my $ref = ref $param;
758 if ($ref && $ref eq 'HASH') {
762 my $sth = _parseletter_sth($table);
764 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
767 $sth->execute( $ref ? @$param : $param );
769 $values = $sth->fetchrow_hashref;
773 _parseletter ( $letter, $table, $values );
777 sub _parseletter_sth {
781 carp "ERROR: _parseletter_sth() called without argument (table)";
784 # NOTE: we used to check whether we had a statement handle cached in
785 # a %handles module-level variable. This was a dumb move and
786 # broke things for the rest of us. prepare_cached is a better
787 # way to cache statement handles anyway.
789 ($table eq 'accountlines' ) ? "SELECT * FROM $table WHERE accountlines_id = ?" :
790 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
791 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
792 ($table eq 'credits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
793 ($table eq 'debits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
794 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
795 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
796 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE itemnumber = ? ORDER BY timestamp DESC LIMIT 1" :
797 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
798 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
799 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
800 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
801 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
802 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
803 ($table eq 'aqbasket' ) ? "SELECT * FROM $table WHERE basketno = ?" :
804 ($table eq 'illrequests' ) ? "SELECT * FROM $table WHERE illrequest_id = ?" :
805 ($table eq 'opac_news' ) ? "SELECT * FROM $table WHERE idnew = ?" :
806 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
807 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
808 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
809 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
810 ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" :
813 warn "ERROR: No _parseletter_sth query for table '$table'";
814 return; # nothing to get
816 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
817 warn "ERROR: Failed to prepare query: '$query'";
820 return $sth; # now cache is populated for that $table
823 =head2 _parseletter($letter, $table, $values)
826 - $letter : a hash to letter fields (title & content useful)
827 - $table : the Koha table to parse.
828 - $values_in : table record hashref
829 parse all fields from a table, and replace values in title & content with the appropriate value
830 (not exported sub, used only internally)
835 my ( $letter, $table, $values_in ) = @_;
837 # Work on a local copy of $values_in (passed by reference) to avoid side effects
838 # in callers ( by changing / formatting values )
839 my $values = $values_in ? { %$values_in } : {};
841 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
842 $values->{'dateexpiry'} = output_pref({ dt => dt_from_string( $values->{'dateexpiry'} ), dateonly => 1 });
845 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
846 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
849 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
850 my $todaysdate = output_pref( dt_from_string() );
851 $letter->{content} =~ s/<<today>>/$todaysdate/go;
854 while ( my ($field, $val) = each %$values ) {
855 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
856 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
857 #Therefore adding the test on biblio. This includes biblioitems,
858 #but excludes items. Removed unneeded global and lookahead.
860 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
861 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
862 $val = $av->count ? $av->next->lib : '';
866 my $replacedby = defined ($val) ? $val : '';
868 and not $replacedby =~ m|9999-12-31|
869 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
871 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
872 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
873 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
875 for my $letter_field ( qw( title content ) ) {
876 my $filter_string_used = q{};
877 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
878 # We overwrite $dateonly if the filter exists and we have a time in the datetime
879 $filter_string_used = $1 || q{};
880 $dateonly = $1 unless $dateonly;
882 my $replacedby_date = eval {
883 output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
886 if ( $letter->{ $letter_field } ) {
887 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
888 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
892 # Other fields replacement
894 for my $letter_field ( qw( title content ) ) {
895 if ( $letter->{ $letter_field } ) {
896 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
897 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
903 if ($table eq 'borrowers' && $letter->{content}) {
904 my $patron = Koha::Patrons->find( $values->{borrowernumber} );
906 my $attributes = $patron->extended_attributes;
908 while ( my $attribute = $attributes->next ) {
909 my $code = $attribute->code;
910 my $val = $attribute->description; # FIXME - we always display intranet description here!
911 $val =~ s/\p{P}(?=$)//g if $val;
912 next unless $val gt '';
914 push @{ $attr{$code} }, $val;
916 while ( my ($code, $val_ar) = each %attr ) {
917 my $replacefield = "<<borrower-attribute:$code>>";
918 my $replacedby = join ',', @$val_ar;
919 $letter->{content} =~ s/$replacefield/$replacedby/g;
928 my $success = EnqueueLetter( { letter => $letter,
929 borrowernumber => '12', message_transport_type => 'email' } )
931 places a letter in the message_queue database table, which will
932 eventually get processed (sent) by the process_message_queue.pl
933 cronjob when it calls SendQueuedMessages.
935 return message_id on success
940 my $params = shift or return;
942 return unless exists $params->{'letter'};
943 # return unless exists $params->{'borrowernumber'};
944 return unless exists $params->{'message_transport_type'};
946 my $content = $params->{letter}->{content};
947 $content =~ s/\s+//g if(defined $content);
948 if ( not defined $content or $content eq '' ) {
949 warn "Trying to add an empty message to the message queue" if $debug;
953 # If we have any attachments we should encode then into the body.
954 if ( $params->{'attachments'} ) {
955 $params->{'letter'} = _add_attachments(
956 { letter => $params->{'letter'},
957 attachments => $params->{'attachments'},
958 message => MIME::Lite->new( Type => 'multipart/mixed' ),
963 my $dbh = C4::Context->dbh();
964 my $statement = << 'ENDSQL';
965 INSERT INTO message_queue
966 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, reply_address, content_type )
968 ( ?, ?, ?, ?, ?, ?, ?, NOW(), ?, ?, ?, ? )
971 my $sth = $dbh->prepare($statement);
972 my $result = $sth->execute(
973 $params->{'borrowernumber'}, # borrowernumber
974 $params->{'letter'}->{'title'}, # subject
975 $params->{'letter'}->{'content'}, # content
976 $params->{'letter'}->{'metadata'} || '', # metadata
977 $params->{'letter'}->{'code'} || '', # letter_code
978 $params->{'message_transport_type'}, # message_transport_type
980 $params->{'to_address'}, # to_address
981 $params->{'from_address'}, # from_address
982 $params->{'reply_address'}, # reply_address
983 $params->{'letter'}->{'content-type'}, # content_type
985 return $dbh->last_insert_id(undef,undef,'message_queue', undef);
988 =head2 SendQueuedMessages ([$hashref])
990 my $sent = SendQueuedMessages({
991 letter_code => $letter_code,
992 borrowernumber => $who_letter_is_for,
998 Sends all of the 'pending' items in the message queue, unless
999 parameters are passed.
1001 The letter_code, borrowernumber and limit parameters are used
1002 to build a parameter set for _get_unsent_messages, thus limiting
1003 which pending messages will be processed. They are all optional.
1005 The verbose parameter can be used to generate debugging output.
1006 It is also optional.
1008 Returns number of messages sent.
1012 sub SendQueuedMessages {
1015 my $which_unsent_messages = {
1016 'message_id' => $params->{'message_id'},
1017 'limit' => $params->{'limit'} // 0,
1018 'borrowernumber' => $params->{'borrowernumber'} // q{},
1019 'letter_code' => $params->{'letter_code'} // q{},
1020 'type' => $params->{'type'} // q{},
1022 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
1023 MESSAGE: foreach my $message ( @$unsent_messages ) {
1024 my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
1025 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
1026 $message_object->make_column_dirty('status');
1027 return unless $message_object->store;
1029 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
1030 warn sprintf( 'sending %s message to patron: %s',
1031 $message->{'message_transport_type'},
1032 $message->{'borrowernumber'} || 'Admin' )
1033 if $params->{'verbose'} or $debug;
1034 # This is just begging for subclassing
1035 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
1036 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
1037 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1039 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
1040 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
1041 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1042 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1043 unless ( $sms_provider ) {
1044 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1045 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1048 unless ( $patron->smsalertnumber ) {
1049 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1050 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1053 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1054 $message->{to_address} .= '@' . $sms_provider->domain();
1056 # Check for possible from_address override
1057 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1058 if ($from_address && $message->{from_address} ne $from_address) {
1059 $message->{from_address} = $from_address;
1060 _update_message_from_address($message->{'message_id'}, $message->{from_address});
1063 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1064 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1066 _send_message_by_sms( $message );
1070 return scalar( @$unsent_messages );
1073 =head2 GetRSSMessages
1075 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1077 returns a listref of all queued RSS messages for a particular person.
1081 sub GetRSSMessages {
1084 return unless $params;
1085 return unless ref $params;
1086 return unless $params->{'borrowernumber'};
1088 return _get_unsent_messages( { message_transport_type => 'rss',
1089 limit => $params->{'limit'},
1090 borrowernumber => $params->{'borrowernumber'}, } );
1093 =head2 GetPrintMessages
1095 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1097 Returns a arrayref of all queued print messages (optionally, for a particular
1102 sub GetPrintMessages {
1103 my $params = shift || {};
1105 return _get_unsent_messages( { message_transport_type => 'print',
1106 borrowernumber => $params->{'borrowernumber'},
1110 =head2 GetQueuedMessages ([$hashref])
1112 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1114 Fetches a list of messages from the message queue optionally filtered by borrowernumber
1115 and limited to specified limit.
1117 Return is an arrayref of hashes, each has represents a message in the message queue.
1121 sub GetQueuedMessages {
1124 my $dbh = C4::Context->dbh();
1125 my $statement = << 'ENDSQL';
1126 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued, updated_on
1132 if ( exists $params->{'borrowernumber'} ) {
1133 push @whereclauses, ' borrowernumber = ? ';
1134 push @query_params, $params->{'borrowernumber'};
1137 if ( @whereclauses ) {
1138 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1141 if ( defined $params->{'limit'} ) {
1142 $statement .= ' LIMIT ? ';
1143 push @query_params, $params->{'limit'};
1146 my $sth = $dbh->prepare( $statement );
1147 my $result = $sth->execute( @query_params );
1148 return $sth->fetchall_arrayref({});
1151 =head2 GetMessageTransportTypes
1153 my @mtt = GetMessageTransportTypes();
1155 returns an arrayref of transport types
1159 sub GetMessageTransportTypes {
1160 my $dbh = C4::Context->dbh();
1161 my $mtts = $dbh->selectcol_arrayref("
1162 SELECT message_transport_type
1163 FROM message_transport_types
1164 ORDER BY message_transport_type
1171 my $message = C4::Letters::Message($message_id);
1176 my ( $message_id ) = @_;
1177 return unless $message_id;
1178 my $dbh = C4::Context->dbh;
1179 return $dbh->selectrow_hashref(q|
1180 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
1182 WHERE message_id = ?
1183 |, {}, $message_id );
1186 =head2 ResendMessage
1188 Attempt to resend a message which has failed previously.
1190 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1192 Updates the message to 'pending' status so that
1193 it will be resent later on.
1195 returns 1 on success, 0 on failure, undef if no message was found
1200 my $message_id = shift;
1201 return unless $message_id;
1203 my $message = GetMessage( $message_id );
1204 return unless $message;
1206 if ( $message->{status} ne 'pending' ) {
1207 $rv = C4::Letters::_set_message_status({
1208 message_id => $message_id,
1209 status => 'pending',
1211 $rv = $rv > 0? 1: 0;
1212 # Clear destination email address to force address update
1213 _update_message_to_address( $message_id, undef ) if $rv &&
1214 $message->{message_transport_type} eq 'email';
1219 =head2 _add_attachements
1222 letter - the standard letter hashref
1223 attachments - listref of attachments. each attachment is a hashref of:
1224 type - the mime type, like 'text/plain'
1225 content - the actual attachment
1226 filename - the name of the attachment.
1227 message - a MIME::Lite object to attach these to.
1229 returns your letter object, with the content updated.
1233 sub _add_attachments {
1236 my $letter = $params->{'letter'};
1237 my $attachments = $params->{'attachments'};
1238 return $letter unless @$attachments;
1239 my $message = $params->{'message'};
1241 # First, we have to put the body in as the first attachment
1243 Type => $letter->{'content-type'} || 'TEXT',
1244 Data => $letter->{'is_html'}
1245 ? _wrap_html($letter->{'content'}, $letter->{'title'})
1246 : $letter->{'content'},
1249 foreach my $attachment ( @$attachments ) {
1251 Type => $attachment->{'type'},
1252 Data => $attachment->{'content'},
1253 Filename => $attachment->{'filename'},
1256 # we're forcing list context here to get the header, not the count back from grep.
1257 ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1258 $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1259 $letter->{'content'} = $message->body_as_string;
1265 =head2 _get_unsent_messages
1267 This function's parameter hash reference takes the following
1268 optional named parameters:
1269 message_transport_type: method of message sending (e.g. email, sms, etc.)
1270 borrowernumber : who the message is to be sent
1271 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1272 message_id : the message_id of the message. In that case the sub will return only 1 result
1273 limit : maximum number of messages to send
1275 This function returns an array of matching hash referenced rows from
1276 message_queue with some borrower information added.
1280 sub _get_unsent_messages {
1283 my $dbh = C4::Context->dbh();
1285 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
1286 FROM message_queue mq
1287 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1291 my @query_params = ('pending');
1292 if ( ref $params ) {
1293 if ( $params->{'message_transport_type'} ) {
1294 $statement .= ' AND mq.message_transport_type = ? ';
1295 push @query_params, $params->{'message_transport_type'};
1297 if ( $params->{'borrowernumber'} ) {
1298 $statement .= ' AND mq.borrowernumber = ? ';
1299 push @query_params, $params->{'borrowernumber'};
1301 if ( $params->{'letter_code'} ) {
1302 $statement .= ' AND mq.letter_code = ? ';
1303 push @query_params, $params->{'letter_code'};
1305 if ( $params->{'type'} ) {
1306 $statement .= ' AND message_transport_type = ? ';
1307 push @query_params, $params->{'type'};
1309 if ( $params->{message_id} ) {
1310 $statement .= ' AND message_id = ?';
1311 push @query_params, $params->{message_id};
1313 if ( $params->{'limit'} ) {
1314 $statement .= ' limit ? ';
1315 push @query_params, $params->{'limit'};
1319 $debug and warn "_get_unsent_messages SQL: $statement";
1320 $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1321 my $sth = $dbh->prepare( $statement );
1322 my $result = $sth->execute( @query_params );
1323 return $sth->fetchall_arrayref({});
1326 sub _send_message_by_email {
1327 my $message = shift or return;
1328 my ($username, $password, $method) = @_;
1330 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1331 my $to_address = $message->{'to_address'};
1332 unless ($to_address) {
1334 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1335 _set_message_status( { message_id => $message->{'message_id'},
1336 status => 'failed' } );
1339 $to_address = $patron->notice_email_address;
1340 unless ($to_address) {
1341 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1342 # warning too verbose for this more common case?
1343 _set_message_status( { message_id => $message->{'message_id'},
1344 status => 'failed' } );
1349 my $subject = $message->{'subject'};
1351 my $content = $message->{'content'};
1352 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1353 my $is_html = $content_type =~ m/html/io;
1355 my $branch_email = undef;
1356 my $branch_replyto = undef;
1357 my $branch_returnpath = undef;
1361 $library = $patron->library;
1362 $branch_email = $library->branchemail;
1363 $branch_replyto = $library->branchreplyto;
1364 $branch_returnpath = $library->branchreturnpath;
1367 my $email = Koha::Email->create(
1371 C4::Context->preference('NoticeBcc')
1372 ? ( bcc => C4::Context->preference('NoticeBcc') )
1375 from => $message->{'from_address'} || $branch_email,
1376 reply_to => $message->{'reply_address'} || $branch_replyto,
1377 sender => $branch_returnpath,
1378 subject => "" . $message->{subject}
1384 _wrap_html( $content, $subject )
1388 $email->text_body( $content );
1393 $smtp_server = $library->smtp_server;
1396 $smtp_server = Koha::SMTP::Servers->get_default;
1402 sasl_username => $username,
1403 sasl_password => $password,
1408 # if initial message address was empty, coming here means that a to address was found and
1409 # queue should be updated; same if to address was overriden by Koha::Email->create
1410 _update_message_to_address( $message->{'message_id'}, $email->email->header('To') )
1411 if !$message->{to_address}
1412 || $message->{to_address} ne $email->email->header('To');
1415 $email->send_or_die({ transport => $smtp_server->transport });
1417 _set_message_status(
1419 message_id => $message->{'message_id'},
1426 _set_message_status(
1428 message_id => $message->{'message_id'},
1438 my ($content, $title) = @_;
1440 my $css = C4::Context->preference("NoticeCSS") || '';
1441 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1443 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1444 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1445 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1447 <title>$title</title>
1448 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1459 my ( $message ) = @_;
1460 my $dbh = C4::Context->dbh;
1461 my $count = $dbh->selectrow_array(q|
1464 WHERE message_transport_type = ?
1465 AND borrowernumber = ?
1467 AND CAST(updated_on AS date) = CAST(NOW() AS date)
1470 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1474 sub _send_message_by_sms {
1475 my $message = shift or return;
1476 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1478 unless ( $patron and $patron->smsalertnumber ) {
1479 _set_message_status( { message_id => $message->{'message_id'},
1480 status => 'failed' } );
1484 if ( _is_duplicate( $message ) ) {
1485 _set_message_status( { message_id => $message->{'message_id'},
1486 status => 'failed' } );
1490 my $success = C4::SMS->send_sms( { destination => $patron->smsalertnumber,
1491 message => $message->{'content'},
1493 _set_message_status( { message_id => $message->{'message_id'},
1494 status => ($success ? 'sent' : 'failed') } );
1498 sub _update_message_to_address {
1500 my $dbh = C4::Context->dbh();
1501 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1504 sub _update_message_from_address {
1505 my ($message_id, $from_address) = @_;
1506 my $dbh = C4::Context->dbh();
1507 $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1510 sub _set_message_status {
1511 my $params = shift or return;
1513 foreach my $required_parameter ( qw( message_id status ) ) {
1514 return unless exists $params->{ $required_parameter };
1517 my $dbh = C4::Context->dbh();
1518 my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1519 my $sth = $dbh->prepare( $statement );
1520 my $result = $sth->execute( $params->{'status'},
1521 $params->{'message_id'} );
1526 my ( $params ) = @_;
1528 my $content = $params->{content};
1529 my $tables = $params->{tables};
1530 my $loops = $params->{loops};
1531 my $substitute = $params->{substitute} || {};
1532 my $lang = defined($params->{lang}) && $params->{lang} ne 'default' ? $params->{lang} : 'en';
1533 my ($theme, $activethemes);
1535 my $htdocs = C4::Context->config('intrahtdocs');
1536 ($theme, $lang, $activethemes)= C4::Templates::activethemes( $htdocs, 'about.tt', 'intranet', $lang);
1538 foreach (@$activethemes) {
1539 push @includes, "$htdocs/$_/$lang/includes";
1540 push @includes, "$htdocs/$_/en/includes" unless $lang eq 'en';
1543 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1544 my $template = Template->new(
1548 PLUGIN_BASE => 'Koha::Template::Plugin',
1549 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1550 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1551 INCLUDE_PATH => \@includes,
1553 ENCODING => 'UTF-8',
1555 ) or die Template->error();
1557 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
1559 $content = add_tt_filters( $content );
1560 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1563 $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1568 sub _get_tt_params {
1569 my ($tables, $is_a_loop) = @_;
1575 article_requests => {
1576 module => 'Koha::ArticleRequests',
1577 singular => 'article_request',
1578 plural => 'article_requests',
1582 module => 'Koha::Acquisition::Baskets',
1583 singular => 'basket',
1584 plural => 'baskets',
1588 module => 'Koha::Biblios',
1589 singular => 'biblio',
1590 plural => 'biblios',
1591 pk => 'biblionumber',
1594 module => 'Koha::Biblioitems',
1595 singular => 'biblioitem',
1596 plural => 'biblioitems',
1597 pk => 'biblioitemnumber',
1600 module => 'Koha::Patrons',
1601 singular => 'borrower',
1602 plural => 'borrowers',
1603 pk => 'borrowernumber',
1606 module => 'Koha::Libraries',
1607 singular => 'branch',
1608 plural => 'branches',
1612 module => 'Koha::Account::Lines',
1613 singular => 'credit',
1614 plural => 'credits',
1615 pk => 'accountlines_id',
1618 module => 'Koha::Account::Lines',
1619 singular => 'debit',
1621 pk => 'accountlines_id',
1624 module => 'Koha::Items',
1630 module => 'Koha::News',
1636 module => 'Koha::Acquisition::Orders',
1637 singular => 'order',
1639 pk => 'ordernumber',
1642 module => 'Koha::Holds',
1648 module => 'Koha::Serials',
1649 singular => 'serial',
1650 plural => 'serials',
1654 module => 'Koha::Subscriptions',
1655 singular => 'subscription',
1656 plural => 'subscriptions',
1657 pk => 'subscriptionid',
1660 module => 'Koha::Suggestions',
1661 singular => 'suggestion',
1662 plural => 'suggestions',
1663 pk => 'suggestionid',
1666 module => 'Koha::Checkouts',
1667 singular => 'checkout',
1668 plural => 'checkouts',
1672 module => 'Koha::Old::Checkouts',
1673 singular => 'old_checkout',
1674 plural => 'old_checkouts',
1678 module => 'Koha::Checkouts',
1679 singular => 'overdue',
1680 plural => 'overdues',
1683 borrower_modifications => {
1684 module => 'Koha::Patron::Modifications',
1685 singular => 'patron_modification',
1686 plural => 'patron_modifications',
1687 fk => 'verification_token',
1690 module => 'Koha::Illrequests',
1691 singular => 'illrequest',
1692 plural => 'illrequests',
1693 pk => 'illrequest_id'
1697 foreach my $table ( keys %$tables ) {
1698 next unless $config->{$table};
1700 my $ref = ref( $tables->{$table} ) || q{};
1701 my $module = $config->{$table}->{module};
1703 if ( can_load( modules => { $module => undef } ) ) {
1704 my $pk = $config->{$table}->{pk};
1705 my $fk = $config->{$table}->{fk};
1708 my $values = $tables->{$table} || [];
1709 unless ( ref( $values ) eq 'ARRAY' ) {
1710 croak "ERROR processing table $table. Wrong API call.";
1712 my $key = $pk ? $pk : $fk;
1713 # $key does not come from user input
1714 my $objects = $module->search(
1715 { $key => $values },
1717 # We want to retrieve the data in the same order
1719 # field is a MySQLism, but they are no other way to do it
1720 # To be generic we could do it in perl, but we will need to fetch
1721 # all the data then order them
1722 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1725 $params->{ $config->{$table}->{plural} } = $objects;
1727 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1728 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1730 if ( $fk ) { # Using a foreign key for lookup
1731 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1733 foreach my $key ( @$fk ) {
1734 $search->{$key} = $id->{$key};
1736 $object = $module->search( $search )->last();
1737 } else { # Foreign key is single column
1738 $object = $module->search( { $fk => $id } )->last();
1740 } else { # using the table's primary key for lookup
1741 $object = $module->find($id);
1743 $params->{ $config->{$table}->{singular} } = $object;
1745 else { # $ref eq 'ARRAY'
1747 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1748 $object = $module->search( { $pk => $tables->{$table} } )->last();
1750 else { # Params are mutliple foreign keys
1751 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1753 $params->{ $config->{$table}->{singular} } = $object;
1757 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1761 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1766 =head3 add_tt_filters
1768 $content = add_tt_filters( $content );
1770 Add TT filters to some specific fields if needed.
1772 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1776 sub add_tt_filters {
1777 my ( $content ) = @_;
1778 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1779 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1783 =head2 get_item_content
1785 my $item = Koha::Items->find(...)->unblessed;
1786 my @item_content_fields = qw( date_due title barcode author itemnumber );
1787 my $item_content = C4::Letters::get_item_content({
1789 item_content_fields => \@item_content_fields
1792 This function generates a tab-separated list of values for the passed item. Dates
1793 are formatted following the current setup.
1797 sub get_item_content {
1798 my ( $params ) = @_;
1799 my $item = $params->{item};
1800 my $dateonly = $params->{dateonly} || 0;
1801 my $item_content_fields = $params->{item_content_fields} || [];
1803 return unless $item;
1805 my @item_info = map {
1809 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1813 } @$item_content_fields;
1814 return join( "\t", @item_info ) . "\n";