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::DateUtils qw( dt_from_string output_pref );
39 use Koha::Auth::TwoFactorAuth;
41 use Koha::SMS::Providers;
42 use Koha::SMTP::Servers;
43 use Koha::Subscriptions;
45 use constant SERIALIZED_EMAIL_CONTENT_TYPE => 'message/rfc822';
47 our (@ISA, @EXPORT_OK);
53 GetLettersAvailableForALibrary
62 GetMessageTransportTypes
72 C4::Letters - Give functions for Letters management
80 "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
81 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)
83 Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
85 =head2 GetLetters([$module])
87 $letters = &GetLetters($module);
88 returns informations about letters.
89 if needed, $module filters for letters given module
91 DEPRECATED - You must use Koha::Notice::Templates instead
92 The group by clause is confusing and can lead to issues
98 my $module = $filters->{module};
99 my $code = $filters->{code};
100 my $branchcode = $filters->{branchcode};
101 my $dbh = C4::Context->dbh;
102 my $letters = $dbh->selectall_arrayref(
104 SELECT code, module, name
108 . ( $module ? q| AND module = ?| : q|| )
109 . ( $code ? q| AND code = ?| : q|| )
110 . ( defined $branchcode ? q| AND branchcode = ?| : q|| )
111 . q| GROUP BY code, module, name ORDER BY name|, { Slice => {} }
112 , ( $module ? $module : () )
113 , ( $code ? $code : () )
114 , ( defined $branchcode ? $branchcode : () )
120 =head2 GetLetterTemplates
122 my $letter_templates = GetLetterTemplates(
124 module => 'circulation',
126 branchcode => 'CPL', # '' for default,
130 Return a hashref of letter templates.
134 sub GetLetterTemplates {
137 my $module = $params->{module};
138 my $code = $params->{code};
139 my $branchcode = $params->{branchcode} // '';
140 my $dbh = C4::Context->dbh;
141 return Koha::Notice::Templates->search(
145 branchcode => $branchcode,
147 C4::Context->preference('TranslateNotices')
149 : ( lang => 'default' )
155 =head2 GetLettersAvailableForALibrary
157 my $letters = GetLettersAvailableForALibrary(
159 branchcode => 'CPL', # '' for default
160 module => 'circulation',
164 Return an arrayref of letters, sorted by name.
165 If a specific letter exist for the given branchcode, it will be retrieve.
166 Otherwise the default letter will be.
170 sub GetLettersAvailableForALibrary {
172 my $branchcode = $filters->{branchcode};
173 my $module = $filters->{module};
175 croak "module should be provided" unless $module;
177 my $dbh = C4::Context->dbh;
178 my $default_letters = $dbh->selectall_arrayref(
180 SELECT module, code, branchcode, name
184 . q| AND branchcode = ''|
185 . ( $module ? q| AND module = ?| : q|| )
186 . q| ORDER BY name|, { Slice => {} }
187 , ( $module ? $module : () )
190 my $specific_letters;
192 $specific_letters = $dbh->selectall_arrayref(
194 SELECT module, code, branchcode, name
198 . q| AND branchcode = ?|
199 . ( $module ? q| AND module = ?| : q|| )
200 . q| ORDER BY name|, { Slice => {} }
202 , ( $module ? $module : () )
207 for my $l (@$default_letters) {
208 $letters{ $l->{code} } = $l;
210 for my $l (@$specific_letters) {
211 # Overwrite the default letter with the specific one.
212 $letters{ $l->{code} } = $l;
215 return [ map { $letters{$_} }
216 sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
226 module => 'circulation',
232 Delete the letter. The mtt parameter is facultative.
233 If not given, all templates mathing the other parameters will be removed.
239 my $branchcode = $params->{branchcode};
240 my $module = $params->{module};
241 my $code = $params->{code};
242 my $mtt = $params->{mtt};
243 my $lang = $params->{lang};
244 my $dbh = C4::Context->dbh;
251 . ( $mtt ? q| AND message_transport_type = ?| : q|| )
252 . ( $lang? q| AND lang = ?| : q|| )
253 , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
258 my $err = &SendAlerts($type, $externalid, $letter_code);
261 - $type : the type of alert
262 - $externalid : the id of the "object" to query
263 - $letter_code : the notice template to use
265 C<&SendAlerts> sends an email notice directly to a patron or a vendor.
267 Currently it supports ($type):
268 - claim serial issues (claimissues)
269 - claim acquisition orders (claimacquisition)
270 - send acquisition orders to the vendor (orderacquisition)
271 - notify patrons about newly received serial issues (issue)
272 - notify patrons when their account is created (members)
274 Returns undef or { error => 'message } on failure.
275 Returns true on success.
280 my ( $type, $externalid, $letter_code ) = @_;
281 my $dbh = C4::Context->dbh;
284 if ( $type eq 'issue' ) {
286 # prepare the letter...
287 # search the subscriptionid
290 "SELECT subscriptionid FROM serial WHERE serialid=?");
291 $sth->execute($externalid);
292 my ($subscriptionid) = $sth->fetchrow
293 or warn( "No subscription for '$externalid'" ),
296 # search the biblionumber
299 "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
300 $sth->execute($subscriptionid);
301 my ($biblionumber) = $sth->fetchrow
302 or warn( "No biblionumber for '$subscriptionid'" ),
305 # find the list of subscribers to notify
306 my $subscription = Koha::Subscriptions->find( $subscriptionid );
307 my $subscribers = $subscription->subscribers;
308 while ( my $patron = $subscribers->next ) {
309 my $email = $patron->email or next;
311 # warn "sending issues...";
312 my $userenv = C4::Context->userenv;
313 my $library = $patron->library;
314 my $letter = GetPreparedLetter (
316 letter_code => $letter_code,
317 branchcode => $userenv->{branch},
319 'branches' => $library->branchcode,
320 'biblio' => $biblionumber,
321 'biblioitems' => $biblionumber,
322 'borrowers' => $patron->unblessed,
323 'subscription' => $subscriptionid,
324 'serial' => $externalid,
329 # FIXME: This 'default' behaviour should be moved to Koha::Email
330 my $mail = Koha::Email->create(
333 from => $library->branchemail,
334 reply_to => $library->branchreplyto,
335 sender => $library->branchreturnpath,
336 subject => "" . $letter->{title},
340 if ( $letter->{is_html} ) {
341 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
344 $mail->text_body( $letter->{content} );
348 $mail->send_or_die({ transport => $library->smtp_server->transport });
351 # We expect ref($_) eq 'Email::Sender::Failure'
352 $error = $_->message;
358 return { error => $error }
362 elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
364 # prepare the letter...
371 if ( $type eq 'claimacquisition') {
373 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
375 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
376 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
377 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
378 WHERE aqorders.ordernumber IN (
382 carp "No order selected";
383 return { error => "no_order_selected" };
385 $strsth .= join( ",", ('?') x @$externalid ) . ")";
386 $action = "ACQUISITION CLAIM";
387 $sthorders = $dbh->prepare($strsth);
388 $sthorders->execute( @$externalid );
389 $dataorders = $sthorders->fetchall_arrayref( {} );
392 if ($type eq 'claimissues') {
394 SELECT serial.*,subscription.*, biblio.*, biblioitems.*, aqbooksellers.*,
395 aqbooksellers.id AS booksellerid
397 LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
398 LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
399 LEFT JOIN biblioitems ON serial.biblionumber = biblioitems.biblionumber
400 LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
401 WHERE serial.serialid IN (
405 carp "No issues selected";
406 return { error => "no_issues_selected" };
409 $strsth .= join( ",", ('?') x @$externalid ) . ")";
410 $action = "SERIAL CLAIM";
411 $sthorders = $dbh->prepare($strsth);
412 $sthorders->execute( @$externalid );
413 $dataorders = $sthorders->fetchall_arrayref( {} );
416 if ( $type eq 'orderacquisition') {
417 $basketno = $externalid;
419 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
421 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
422 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
423 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
424 WHERE aqbasket.basketno = ?
425 AND orderstatus IN ('new','ordered')
428 unless ( $basketno ) {
429 carp "No basketnumber given";
430 return { error => "no_basketno" };
432 $action = "ACQUISITION ORDER";
433 $sthorders = $dbh->prepare($strsth);
434 $sthorders->execute($basketno);
435 $dataorders = $sthorders->fetchall_arrayref( {} );
437 aqorders => [ map { $_->{ordernumber} } @$dataorders ]
441 my $booksellerid = $dataorders->[0]->{booksellerid};
442 my $addressee = $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
445 $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
446 $sthcontact->execute( $booksellerid );
447 my $datacontact = $sthcontact->fetchrow_hashref;
451 push @email, $datacontact->{email} if ( $datacontact && $datacontact->{email} );
453 warn "Bookseller $booksellerid without emails";
454 return { error => "no_email" };
457 while ($addlcontact = $sthcontact->fetchrow_hashref) {
458 push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
461 my $userenv = C4::Context->userenv;
462 my $letter = GetPreparedLetter (
464 letter_code => $letter_code,
465 branchcode => $userenv->{branch},
467 'branches' => $userenv->{branch},
468 'aqbooksellers' => $booksellerid,
469 'aqcontacts' => $datacontact,
470 'aqbasket' => $basketno,
472 repeat => $dataorders,
475 ) or return { error => "no_letter" };
477 # Remove the order tag
478 $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
481 my $library = Koha::Libraries->find( $userenv->{branch} );
482 my $mail = Koha::Email->create(
484 to => join( ',', @email ),
485 cc => join( ',', @cc ),
488 C4::Context->preference("ClaimsBccCopy")
489 && ( $type eq 'claimacquisition'
490 || $type eq 'claimissues' )
492 ? ( bcc => $userenv->{emailaddress} )
495 from => $library->branchemail
496 || C4::Context->preference('KohaAdminEmailAddress'),
497 subject => "" . $letter->{title},
501 if ( $letter->{is_html} ) {
502 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
505 $mail->text_body( "" . $letter->{content} );
509 $mail->send_or_die({ transport => $library->smtp_server->transport });
512 # We expect ref($_) eq 'Email::Sender::Failure'
513 $error = $_->message;
519 return { error => $error }
522 my $log_object = $action eq 'ACQUISITION ORDER' ? $externalid : undef;
523 my $module = $action eq 'ACQUISITION ORDER' ? 'ACQUISITIONS' : 'CLAIMS';
529 . join( ',', @email )
534 ) if C4::Context->preference("ClaimsLog");
537 # If we come here, return an OK status
541 =head2 GetPreparedLetter( %params )
544 module => letter module, mandatory
545 letter_code => letter code, mandatory
546 branchcode => for letter selection, if missing default system letter taken
547 tables => a hashref with table names as keys. Values are either:
548 - a scalar - primary key value
549 - an arrayref - primary key values
550 - a hashref - full record
551 substitute => custom substitution key/value pairs
552 repeat => records to be substituted on consecutive lines:
553 - an arrayref - tries to guess what needs substituting by
554 taking remaining << >> tokensr; not recommended
555 - a hashref token => @tables - replaces <token> << >> << >> </token>
556 subtemplate for each @tables row; table is a hashref as above
557 want_librarian => boolean, if set to true triggers librarian details
558 substitution from the userenv
560 letter fields hashref (title & content useful)
564 sub GetPreparedLetter {
567 my $letter = $params{letter};
568 my $lang = $params{lang} || 'default';
571 my $module = $params{module} or croak "No module";
572 my $letter_code = $params{letter_code} or croak "No letter_code";
573 my $branchcode = $params{branchcode} || '';
574 my $mtt = $params{message_transport_type} || 'email';
576 my $template = Koha::Notice::Templates->find_effective_template(
579 code => $letter_code,
580 branchcode => $branchcode,
581 message_transport_type => $mtt,
586 unless ( $template ) {
587 warn( "No $module $letter_code letter transported by " . $mtt );
591 $letter = $template->unblessed;
592 $letter->{'content-type'} = 'text/html; charset="UTF-8"' if $letter->{is_html};
595 my $objects = $params{objects} || {};
596 my $tables = $params{tables} || {};
597 my $substitute = $params{substitute} || {};
598 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
599 my $repeat = $params{repeat};
600 %$tables || %$substitute || $repeat || %$loops || %$objects
601 or carp( "ERROR: nothing to substitute - all of 'objects', 'tables', 'loops' and 'substitute' are empty" ),
603 my $want_librarian = $params{want_librarian};
606 while ( my ($token, $val) = each %$substitute ) {
608 if ( $token eq 'items.content' ) {
609 $val =~ s|\n|<br/>|g if $letter->{is_html};
612 $letter->{title} =~ s/<<$token>>/$val/g;
613 $letter->{content} =~ s/<<$token>>/$val/g;
617 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
618 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
620 if ($want_librarian) {
621 # parsing librarian name
622 my $userenv = C4::Context->userenv;
623 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
624 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
625 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
628 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
631 if (ref ($repeat) eq 'ARRAY' ) {
632 $repeat_no_enclosing_tags = $repeat;
634 $repeat_enclosing_tags = $repeat;
638 if ($repeat_enclosing_tags) {
639 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
640 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
643 my %subletter = ( title => '', content => $subcontent );
644 _substitute_tables( \%subletter, $_ );
647 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
653 _substitute_tables( $letter, $tables );
656 if ($repeat_no_enclosing_tags) {
657 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
662 $c =~ s/<<count>>/$i/go;
663 foreach my $field ( keys %{$_} ) {
664 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
668 } @$repeat_no_enclosing_tags;
670 my $replaceby = join( "\n", @lines );
671 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
675 $letter->{content} = _process_tt(
677 content => $letter->{content},
681 substitute => $substitute,
686 $letter->{title} = _process_tt(
688 content => $letter->{title},
692 substitute => $substitute,
697 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
702 sub _substitute_tables {
703 my ( $letter, $tables ) = @_;
704 while ( my ($table, $param) = each %$tables ) {
707 my $ref = ref $param;
710 if ($ref && $ref eq 'HASH') {
714 my $sth = _parseletter_sth($table);
716 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
719 $sth->execute( $ref ? @$param : $param );
721 $values = $sth->fetchrow_hashref;
725 _parseletter ( $letter, $table, $values );
729 sub _parseletter_sth {
733 carp "ERROR: _parseletter_sth() called without argument (table)";
736 # NOTE: we used to check whether we had a statement handle cached in
737 # a %handles module-level variable. This was a dumb move and
738 # broke things for the rest of us. prepare_cached is a better
739 # way to cache statement handles anyway.
741 ($table eq 'accountlines' ) ? "SELECT * FROM $table WHERE accountlines_id = ?" :
742 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
743 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
744 ($table eq 'credits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
745 ($table eq 'debits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
746 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
747 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
748 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE issue_id = ?" :
749 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
750 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
751 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
752 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
753 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
754 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
755 ($table eq 'aqbasket' ) ? "SELECT * FROM $table WHERE basketno = ?" :
756 ($table eq 'illrequests' ) ? "SELECT * FROM $table WHERE illrequest_id = ?" :
757 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
758 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
759 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
760 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
761 ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" :
762 ($table eq 'additional_contents' || $table eq 'opac_news') ? "SELECT * FROM additional_contents WHERE idnew = ?" :
763 ($table eq 'recalls') ? "SELECT * FROM $table WHERE recall_id = ?" :
766 warn "ERROR: No _parseletter_sth query for table '$table'";
767 return; # nothing to get
769 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
770 warn "ERROR: Failed to prepare query: '$query'";
773 return $sth; # now cache is populated for that $table
776 =head2 _parseletter($letter, $table, $values)
779 - $letter : a hash to letter fields (title & content useful)
780 - $table : the Koha table to parse.
781 - $values_in : table record hashref
782 parse all fields from a table, and replace values in title & content with the appropriate value
783 (not exported sub, used only internally)
788 my ( $letter, $table, $values_in ) = @_;
790 # Work on a local copy of $values_in (passed by reference) to avoid side effects
791 # in callers ( by changing / formatting values )
792 my $values = $values_in ? { %$values_in } : {};
794 # FIXME Dates formatting must be done in notice's templates
795 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
796 $values->{'dateexpiry'} = output_pref({ dt => dt_from_string( $values->{'dateexpiry'} ), dateonly => 1 });
799 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
800 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
803 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
804 my $todaysdate = output_pref( dt_from_string() );
805 $letter->{content} =~ s/<<today>>/$todaysdate/go;
808 while ( my ($field, $val) = each %$values ) {
809 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
810 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
811 #Therefore adding the test on biblio. This includes biblioitems,
812 #but excludes items. Removed unneeded global and lookahead.
814 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
815 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
816 $val = $av->count ? $av->next->lib : '';
820 my $replacedby = defined ($val) ? $val : '';
822 and not $replacedby =~ m|9999-12-31|
823 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
825 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
826 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
827 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
829 for my $letter_field ( qw( title content ) ) {
830 my $filter_string_used = q{};
831 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
832 # We overwrite $dateonly if the filter exists and we have a time in the datetime
833 $filter_string_used = $1 || q{};
834 $dateonly = $1 unless $dateonly;
836 my $replacedby_date = eval {
837 output_pref({ dt => scalar dt_from_string( $replacedby ), dateonly => $dateonly });
839 $replacedby_date //= q{};
841 if ( $letter->{ $letter_field } ) {
842 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
843 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
847 # Other fields replacement
849 for my $letter_field ( qw( title content ) ) {
850 if ( $letter->{ $letter_field } ) {
851 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
852 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
858 if ($table eq 'borrowers' && $letter->{content}) {
859 my $patron = Koha::Patrons->find( $values->{borrowernumber} );
861 my $attributes = $patron->extended_attributes;
863 while ( my $attribute = $attributes->next ) {
864 my $code = $attribute->code;
865 my $val = $attribute->description; # FIXME - we always display intranet description here!
866 $val =~ s/\p{P}(?=$)//g if $val;
867 next unless $val gt '';
869 push @{ $attr{$code} }, $val;
871 while ( my ($code, $val_ar) = each %attr ) {
872 my $replacefield = "<<borrower-attribute:$code>>";
873 my $replacedby = join ',', @$val_ar;
874 $letter->{content} =~ s/$replacefield/$replacedby/g;
883 my $success = EnqueueLetter( { letter => $letter,
884 borrowernumber => '12', message_transport_type => 'email' } )
886 Places a letter in the message_queue database table, which will
887 eventually get processed (sent) by the process_message_queue.pl
888 cronjob when it calls SendQueuedMessages.
890 Return message_id on success
893 * letter - required; A letter hashref as returned from GetPreparedLetter
894 * message_transport_type - required; One of the available mtts
895 * borrowernumber - optional if 'to_address' is passed; The borrowernumber of the patron we enqueuing the notice for
896 * to_address - optional if 'borrowernumber' is passed; The destination email address for the notice (defaults to patron->notice_email_address)
897 * from_address - optional; The from address for the notice, defaults to patron->library->from_email_address
898 * reply_address - optional; The reply address for the notice, defaults to patron->library->reply_to
903 my $params = shift or return;
905 return unless exists $params->{'letter'};
906 # return unless exists $params->{'borrowernumber'};
907 return unless exists $params->{'message_transport_type'};
909 my $content = $params->{letter}->{content};
910 $content =~ s/\s+//g if(defined $content);
911 if ( not defined $content or $content eq '' ) {
912 Koha::Logger->get->info("Trying to add an empty message to the message queue");
916 # If we have any attachments we should encode then into the body.
917 if ( $params->{'attachments'} ) {
918 $params->{'letter'} = _add_attachments(
919 { letter => $params->{'letter'},
920 attachments => $params->{'attachments'},
925 my $message = Koha::Notice::Message->new(
927 letter_id => $params->{letter}->{id} || undef,
928 borrowernumber => $params->{borrowernumber},
929 subject => $params->{letter}->{title},
930 content => $params->{letter}->{content},
931 metadata => $params->{letter}->{metadata} || q{},
932 letter_code => $params->{letter}->{code} || q{},
933 message_transport_type => $params->{message_transport_type},
935 time_queued => dt_from_string(),
936 to_address => $params->{to_address},
937 from_address => $params->{from_address},
938 reply_address => $params->{reply_address},
939 content_type => $params->{letter}->{'content-type'},
940 failure_code => $params->{failure_code} || q{},
946 =head2 SendQueuedMessages ([$hashref])
948 my $sent = SendQueuedMessages({
949 letter_code => $letter_code,
950 borrowernumber => $who_letter_is_for,
956 Sends all of the 'pending' items in the message queue, unless
957 parameters are passed.
959 The letter_code, borrowernumber and limit parameters are used
960 to build a parameter set for _get_unsent_messages, thus limiting
961 which pending messages will be processed. They are all optional.
963 The verbose parameter can be used to generate debugging output.
966 Returns number of messages sent.
970 sub SendQueuedMessages {
973 Koha::Exceptions::BadParameter->throw("Parameter message_id cannot be empty if passed.")
974 if ( exists( $params->{message_id} ) && !$params->{message_id} );
976 my $which_unsent_messages = {
977 'message_id' => $params->{'message_id'},
978 'limit' => $params->{'limit'} // 0,
979 'borrowernumber' => $params->{'borrowernumber'} // q{},
980 'letter_code' => $params->{'letter_code'} // q{},
981 'message_transport_type' => $params->{'type'} // q{},
983 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
984 MESSAGE: foreach my $message ( @$unsent_messages ) {
985 my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
986 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
987 $message_object->make_column_dirty('status');
988 return unless $message_object->store;
990 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
991 warn sprintf( 'sending %s message to patron: %s',
992 $message->{'message_transport_type'},
993 $message->{'borrowernumber'} || 'Admin' )
994 if $params->{'verbose'};
995 # This is just begging for subclassing
996 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
997 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
998 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1000 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
1001 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
1002 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1003 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1004 unless ( $sms_provider ) {
1005 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1006 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1009 unless ( $patron->smsalertnumber ) {
1010 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1011 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1014 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1015 $message->{to_address} .= '@' . $sms_provider->domain();
1017 # Check for possible from_address override
1018 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1019 if ($from_address && $message->{from_address} ne $from_address) {
1020 $message->{from_address} = $from_address;
1021 _update_message_from_address($message->{'message_id'}, $message->{from_address});
1024 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1025 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1027 _send_message_by_sms( $message );
1031 return scalar( @$unsent_messages );
1034 =head2 GetRSSMessages
1036 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1038 returns a listref of all queued RSS messages for a particular person.
1042 sub GetRSSMessages {
1045 return unless $params;
1046 return unless ref $params;
1047 return unless $params->{'borrowernumber'};
1049 return _get_unsent_messages( { message_transport_type => 'rss',
1050 limit => $params->{'limit'},
1051 borrowernumber => $params->{'borrowernumber'}, } );
1054 =head2 GetPrintMessages
1056 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1058 Returns a arrayref of all queued print messages (optionally, for a particular
1063 sub GetPrintMessages {
1064 my $params = shift || {};
1066 return _get_unsent_messages( { message_transport_type => 'print',
1067 borrowernumber => $params->{'borrowernumber'},
1071 =head2 GetQueuedMessages ([$hashref])
1073 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1075 Fetches a list of messages from the message queue optionally filtered by borrowernumber
1076 and limited to specified limit.
1078 Return is an arrayref of hashes, each has represents a message in the message queue.
1082 sub GetQueuedMessages {
1085 my $dbh = C4::Context->dbh();
1086 my $statement = << 'ENDSQL';
1087 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued, updated_on, failure_code
1093 if ( exists $params->{'borrowernumber'} ) {
1094 push @whereclauses, ' borrowernumber = ? ';
1095 push @query_params, $params->{'borrowernumber'};
1098 if ( @whereclauses ) {
1099 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1102 if ( defined $params->{'limit'} ) {
1103 $statement .= ' LIMIT ? ';
1104 push @query_params, $params->{'limit'};
1107 my $sth = $dbh->prepare( $statement );
1108 my $result = $sth->execute( @query_params );
1109 return $sth->fetchall_arrayref({});
1112 =head2 GetMessageTransportTypes
1114 my @mtt = GetMessageTransportTypes();
1116 returns an arrayref of transport types
1120 sub GetMessageTransportTypes {
1121 my $dbh = C4::Context->dbh();
1122 my $mtts = $dbh->selectcol_arrayref("
1123 SELECT message_transport_type
1124 FROM message_transport_types
1125 ORDER BY message_transport_type
1132 my $message = C4::Letters::Message($message_id);
1137 my ( $message_id ) = @_;
1138 return unless $message_id;
1139 my $dbh = C4::Context->dbh;
1140 return $dbh->selectrow_hashref(q|
1141 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
1143 WHERE message_id = ?
1144 |, {}, $message_id );
1147 =head2 ResendMessage
1149 Attempt to resend a message which has failed previously.
1151 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1153 Updates the message to 'pending' status so that
1154 it will be resent later on.
1156 returns 1 on success, 0 on failure, undef if no message was found
1161 my $message_id = shift;
1162 return unless $message_id;
1164 my $message = GetMessage( $message_id );
1165 return unless $message;
1167 if ( $message->{status} ne 'pending' ) {
1168 $rv = C4::Letters::_set_message_status({
1169 message_id => $message_id,
1170 status => 'pending',
1172 $rv = $rv > 0? 1: 0;
1173 # Clear destination email address to force address update
1174 _update_message_to_address( $message_id, undef ) if $rv &&
1175 $message->{message_transport_type} eq 'email';
1180 =head2 _add_attachements
1182 _add_attachments({ letter => $letter, attachments => $attachments });
1185 letter - the standard letter hashref
1186 attachments - listref of attachments. each attachment is a hashref of:
1187 type - the mime type, like 'text/plain'
1188 content - the actual attachment
1189 filename - the name of the attachment.
1191 returns your letter object, with the content updated.
1192 This routine picks the I<content> of I<letter> and generates a MIME
1193 email, attaching the passed I<attachments> using Koha::Email. The
1194 content is replaced by the string representation of the MIME object,
1195 and the content-type is updated for later handling.
1199 sub _add_attachments {
1202 my $letter = $params->{letter};
1203 my $attachments = $params->{attachments};
1204 return $letter unless @$attachments;
1206 my $message = Koha::Email->new;
1208 if ( $letter->{is_html} ) {
1209 $message->html_body( _wrap_html( $letter->{content}, $letter->{title} ) );
1212 $message->text_body( $letter->{content} );
1215 foreach my $attachment ( @$attachments ) {
1217 Encode::encode( "UTF-8", $attachment->{content} ),
1218 content_type => $attachment->{type} || 'application/octet-stream',
1219 name => $attachment->{filename},
1220 disposition => 'attachment',
1224 $letter->{'content-type'} = SERIALIZED_EMAIL_CONTENT_TYPE;
1225 $letter->{content} = $message->as_string;
1231 =head2 _get_unsent_messages
1233 This function's parameter hash reference takes the following
1234 optional named parameters:
1235 message_transport_type: method of message sending (e.g. email, sms, etc.)
1236 Can be a single string, or an arrayref of strings
1237 borrowernumber : who the message is to be sent
1238 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1239 Can be a single string, or an arrayref of strings
1240 message_id : the message_id of the message. In that case the sub will return only 1 result
1241 limit : maximum number of messages to send
1243 This function returns an array of matching hash referenced rows from
1244 message_queue with some borrower information added.
1248 sub _get_unsent_messages {
1251 my $dbh = C4::Context->dbh();
1253 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
1254 FROM message_queue mq
1255 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1259 my @query_params = ('pending');
1260 if ( ref $params ) {
1261 if ( $params->{'borrowernumber'} ) {
1262 $statement .= ' AND mq.borrowernumber = ? ';
1263 push @query_params, $params->{'borrowernumber'};
1265 if ( $params->{'letter_code'} ) {
1266 my @letter_codes = ref $params->{'letter_code'} eq "ARRAY" ? @{$params->{'letter_code'}} : $params->{'letter_code'};
1267 if ( @letter_codes ) {
1268 my $q = join( ",", ("?") x @letter_codes );
1269 $statement .= " AND mq.letter_code IN ( $q ) ";
1270 push @query_params, @letter_codes;
1273 if ( $params->{'message_transport_type'} ) {
1274 my @types = ref $params->{'message_transport_type'} eq "ARRAY" ? @{$params->{'message_transport_type'}} : $params->{'message_transport_type'};
1276 my $q = join( ",", ("?") x @types );
1277 $statement .= " AND message_transport_type IN ( $q ) ";
1278 push @query_params, @types;
1281 if ( $params->{message_id} ) {
1282 $statement .= ' AND message_id = ?';
1283 push @query_params, $params->{message_id};
1285 if ( $params->{'limit'} ) {
1286 $statement .= ' limit ? ';
1287 push @query_params, $params->{'limit'};
1291 my $sth = $dbh->prepare( $statement );
1292 my $result = $sth->execute( @query_params );
1293 return $sth->fetchall_arrayref({});
1296 sub _send_message_by_email {
1297 my $message = shift or return;
1298 my ($username, $password, $method) = @_;
1300 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1301 my $to_address = $message->{'to_address'};
1302 unless ($to_address) {
1304 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1305 _set_message_status(
1307 message_id => $message->{'message_id'},
1309 failure_code => 'INVALID_BORNUMBER'
1314 $to_address = $patron->notice_email_address;
1315 unless ($to_address) {
1316 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1317 # warning too verbose for this more common case?
1318 _set_message_status(
1320 message_id => $message->{'message_id'},
1322 failure_code => 'NO_EMAIL'
1329 my $subject = $message->{'subject'};
1331 my $content = $message->{'content'};
1332 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1333 my $is_html = $content_type =~ m/html/io;
1335 my $branch_email = undef;
1336 my $branch_replyto = undef;
1337 my $branch_returnpath = undef;
1341 $library = $patron->library;
1342 $branch_email = $library->from_email_address;
1343 $branch_replyto = $library->branchreplyto;
1344 $branch_returnpath = $library->branchreturnpath;
1347 # NOTE: Patron may not be defined above so branch_email may be undefined still
1348 # so we need to fallback to KohaAdminEmailAddress as a last resort.
1350 $message->{'from_address'}
1352 || C4::Context->preference('KohaAdminEmailAddress');
1353 if( !$from_address ) {
1354 _set_message_status(
1356 message_id => $message->{'message_id'},
1358 failure_code => 'NO_FROM',
1370 C4::Context->preference('NoticeBcc')
1371 ? ( bcc => C4::Context->preference('NoticeBcc') )
1374 from => $from_address,
1375 reply_to => $message->{'reply_address'} || $branch_replyto,
1376 sender => $branch_returnpath,
1377 subject => "" . $message->{subject}
1380 if ( $message->{'content_type'} && $message->{'content_type'} eq SERIALIZED_EMAIL_CONTENT_TYPE ) {
1382 # The message has been previously composed as a valid MIME object
1383 # and serialized as a string on the DB
1384 $email = Koha::Email->new_from_string($content);
1385 $email->create($params);
1387 $email = Koha::Email->create($params);
1389 $email->html_body( _wrap_html( $content, $subject ) );
1391 $email->text_body($content);
1396 if ( ref($_) eq 'Koha::Exceptions::BadParameter' ) {
1397 _set_message_status(
1399 message_id => $message->{'message_id'},
1401 failure_code => "INVALID_EMAIL:".$_->parameter
1405 _set_message_status(
1407 message_id => $message->{'message_id'},
1409 failure_code => 'UNKNOWN_ERROR'
1415 return unless $email;
1419 $smtp_server = $library->smtp_server;
1422 $smtp_server = Koha::SMTP::Servers->get_default;
1428 sasl_username => $username,
1429 sasl_password => $password,
1434 # if initial message address was empty, coming here means that a to address was found and
1435 # queue should be updated; same if to address was overriden by Koha::Email->create
1436 _update_message_to_address( $message->{'message_id'}, $email->email->header('To') )
1437 if !$message->{to_address}
1438 || $message->{to_address} ne $email->email->header('To');
1441 $email->send_or_die({ transport => $smtp_server->transport });
1443 _set_message_status(
1445 message_id => $message->{'message_id'},
1453 _set_message_status(
1455 message_id => $message->{'message_id'},
1457 failure_code => 'SENDMAIL'
1461 carp "$Mail::Sendmail::error";
1467 my ($content, $title) = @_;
1469 my $css = C4::Context->preference("NoticeCSS") || '';
1470 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1472 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1473 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1474 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1476 <title>$title</title>
1477 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1488 my ( $message ) = @_;
1489 my $dbh = C4::Context->dbh;
1490 my $count = $dbh->selectrow_array(q|
1493 WHERE message_transport_type = ?
1494 AND borrowernumber = ?
1496 AND CAST(updated_on AS date) = CAST(NOW() AS date)
1499 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1503 sub _send_message_by_sms {
1504 my $message = shift or return;
1505 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1506 _update_message_to_address($message->{message_id}, $patron->smsalertnumber) if $patron;
1508 unless ( $patron and $patron->smsalertnumber ) {
1509 _set_message_status( { message_id => $message->{'message_id'},
1511 failure_code => 'MISSING_SMS' } );
1515 if ( _is_duplicate( $message ) ) {
1516 _set_message_status(
1518 message_id => $message->{'message_id'},
1520 failure_code => 'DUPLICATE_MESSAGE'
1526 my $success = C4::SMS->send_sms(
1528 destination => $patron->smsalertnumber,
1529 message => $message->{'content'},
1534 _set_message_status(
1536 message_id => $message->{'message_id'},
1543 _set_message_status(
1545 message_id => $message->{'message_id'},
1547 failure_code => 'NO_NOTES'
1555 sub _update_message_to_address {
1557 my $dbh = C4::Context->dbh();
1558 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1561 sub _update_message_from_address {
1562 my ($message_id, $from_address) = @_;
1563 my $dbh = C4::Context->dbh();
1564 $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1567 sub _set_message_status {
1568 my $params = shift or return;
1570 foreach my $required_parameter ( qw( message_id status ) ) {
1571 return unless exists $params->{ $required_parameter };
1574 my $dbh = C4::Context->dbh();
1575 my $statement = 'UPDATE message_queue SET status= ?, failure_code= ? WHERE message_id = ?';
1576 my $sth = $dbh->prepare( $statement );
1577 my $result = $sth->execute( $params->{'status'},
1578 $params->{'failure_code'} || '',
1579 $params->{'message_id'} );
1584 my ( $params ) = @_;
1586 my $content = $params->{content};
1587 my $tables = $params->{tables};
1588 my $loops = $params->{loops};
1589 my $objects = $params->{objects} || {};
1590 my $substitute = $params->{substitute} || {};
1591 my $lang = defined($params->{lang}) && $params->{lang} ne 'default' ? $params->{lang} : 'en';
1592 my ($theme, $availablethemes);
1594 my $htdocs = C4::Context->config('intrahtdocs');
1595 ($theme, $lang, $availablethemes)= C4::Templates::availablethemes( $htdocs, 'about.tt', 'intranet', $lang);
1597 foreach (@$availablethemes) {
1598 push @includes, "$htdocs/$_/$lang/includes";
1599 push @includes, "$htdocs/$_/en/includes" unless $lang eq 'en';
1602 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1603 my $template = Template->new(
1607 PLUGIN_BASE => 'Koha::Template::Plugin',
1608 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1609 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1610 INCLUDE_PATH => \@includes,
1612 ENCODING => 'UTF-8',
1614 ) or die Template->error();
1616 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute, %$objects };
1618 $content = add_tt_filters( $content );
1619 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1622 my $schema = Koha::Database->new->schema;
1624 my $processed = try {
1625 $template->process( \$content, $tt_params, \$output );
1628 $schema->txn_rollback;
1630 croak "ERROR PROCESSING TEMPLATE: " . $template->error() unless $processed;
1635 sub _get_tt_params {
1636 my ($tables, $is_a_loop) = @_;
1642 article_requests => {
1643 module => 'Koha::ArticleRequests',
1644 singular => 'article_request',
1645 plural => 'article_requests',
1649 module => 'Koha::Acquisition::Baskets',
1650 singular => 'basket',
1651 plural => 'baskets',
1655 module => 'Koha::Acquisition::Booksellers',
1656 singular => 'bookseller',
1657 plural => 'booksellers',
1661 module => 'Koha::Biblios',
1662 singular => 'biblio',
1663 plural => 'biblios',
1664 pk => 'biblionumber',
1667 module => 'Koha::Biblioitems',
1668 singular => 'biblioitem',
1669 plural => 'biblioitems',
1670 pk => 'biblioitemnumber',
1673 module => 'Koha::Patrons',
1674 singular => 'borrower',
1675 plural => 'borrowers',
1676 pk => 'borrowernumber',
1679 module => 'Koha::Libraries',
1680 singular => 'branch',
1681 plural => 'branches',
1685 module => 'Koha::Account::Lines',
1686 singular => 'credit',
1687 plural => 'credits',
1688 pk => 'accountlines_id',
1691 module => 'Koha::Account::Lines',
1692 singular => 'debit',
1694 pk => 'accountlines_id',
1697 module => 'Koha::Items',
1702 additional_contents => {
1703 module => 'Koha::AdditionalContents',
1704 singular => 'additional_content',
1705 plural => 'additional_contents',
1709 module => 'Koha::AdditionalContents',
1715 module => 'Koha::Acquisition::Orders',
1716 singular => 'order',
1718 pk => 'ordernumber',
1721 module => 'Koha::Holds',
1727 module => 'Koha::Serials',
1728 singular => 'serial',
1729 plural => 'serials',
1733 module => 'Koha::Subscriptions',
1734 singular => 'subscription',
1735 plural => 'subscriptions',
1736 pk => 'subscriptionid',
1739 module => 'Koha::Suggestions',
1740 singular => 'suggestion',
1741 plural => 'suggestions',
1742 pk => 'suggestionid',
1745 module => 'Koha::Checkouts',
1746 singular => 'checkout',
1747 plural => 'checkouts',
1751 module => 'Koha::Old::Checkouts',
1752 singular => 'old_checkout',
1753 plural => 'old_checkouts',
1757 module => 'Koha::Checkouts',
1758 singular => 'overdue',
1759 plural => 'overdues',
1762 borrower_modifications => {
1763 module => 'Koha::Patron::Modifications',
1764 singular => 'patron_modification',
1765 plural => 'patron_modifications',
1766 fk => 'verification_token',
1769 module => 'Koha::Illrequests',
1770 singular => 'illrequest',
1771 plural => 'illrequests',
1772 pk => 'illrequest_id'
1776 foreach my $table ( keys %$tables ) {
1777 next unless $config->{$table};
1779 my $ref = ref( $tables->{$table} ) || q{};
1780 my $module = $config->{$table}->{module};
1782 if ( can_load( modules => { $module => undef } ) ) {
1783 my $pk = $config->{$table}->{pk};
1784 my $fk = $config->{$table}->{fk};
1787 my $values = $tables->{$table} || [];
1788 unless ( ref( $values ) eq 'ARRAY' ) {
1789 croak "ERROR processing table $table. Wrong API call.";
1791 my $key = $pk ? $pk : $fk;
1792 # $key does not come from user input
1793 my $objects = $module->search(
1794 { $key => $values },
1796 # We want to retrieve the data in the same order
1798 # field is a MySQLism, but they are no other way to do it
1799 # To be generic we could do it in perl, but we will need to fetch
1800 # all the data then order them
1801 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1804 $params->{ $config->{$table}->{plural} } = $objects;
1806 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1807 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1809 if ( $fk ) { # Using a foreign key for lookup
1810 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1812 foreach my $key ( @$fk ) {
1813 $search->{$key} = $id->{$key};
1815 $object = $module->search( $search )->last();
1816 } else { # Foreign key is single column
1817 $object = $module->search( { $fk => $id } )->last();
1819 } else { # using the table's primary key for lookup
1820 $object = $module->find($id);
1822 $params->{ $config->{$table}->{singular} } = $object;
1824 else { # $ref eq 'ARRAY'
1826 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1827 $object = $module->search( { $pk => $tables->{$table} } )->last();
1829 else { # Params are mutliple foreign keys
1830 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1832 $params->{ $config->{$table}->{singular} } = $object;
1836 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1840 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1845 =head3 add_tt_filters
1847 $content = add_tt_filters( $content );
1849 Add TT filters to some specific fields if needed.
1851 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1855 sub add_tt_filters {
1856 my ( $content ) = @_;
1857 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1858 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1862 =head2 get_item_content
1864 my $item = Koha::Items->find(...)->unblessed;
1865 my @item_content_fields = qw( date_due title barcode author itemnumber );
1866 my $item_content = C4::Letters::get_item_content({
1868 item_content_fields => \@item_content_fields
1871 This function generates a tab-separated list of values for the passed item. Dates
1872 are formatted following the current setup.
1876 sub get_item_content {
1877 my ( $params ) = @_;
1878 my $item = $params->{item};
1879 my $dateonly = $params->{dateonly} || 0;
1880 my $item_content_fields = $params->{item_content_fields} || [];
1882 return unless $item;
1884 my @item_info = map {
1888 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1892 } @$item_content_fields;
1893 return join( "\t", @item_info ) . "\n";