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 );
26 use Try::Tiny qw( catch try );
29 use C4::Log qw( logaction );
32 use Koha::DateUtils qw( dt_from_string output_pref );
33 use Koha::SMS::Providers;
36 use Koha::Notice::Messages;
37 use Koha::Notice::Templates;
38 use Koha::DateUtils qw( dt_from_string output_pref );
40 use Koha::SMTP::Servers;
41 use Koha::Subscriptions;
43 our (@ISA, @EXPORT_OK);
49 GetLettersAvailableForALibrary
58 GetMessageTransportTypes
68 C4::Letters - Give functions for Letters management
76 "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
77 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)
79 Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
81 =head2 GetLetters([$module])
83 $letters = &GetLetters($module);
84 returns informations about letters.
85 if needed, $module filters for letters given module
87 DEPRECATED - You must use Koha::Notice::Templates instead
88 The group by clause is confusing and can lead to issues
94 my $module = $filters->{module};
95 my $code = $filters->{code};
96 my $branchcode = $filters->{branchcode};
97 my $dbh = C4::Context->dbh;
98 my $letters = $dbh->selectall_arrayref(
100 SELECT code, module, name
104 . ( $module ? q| AND module = ?| : q|| )
105 . ( $code ? q| AND code = ?| : q|| )
106 . ( defined $branchcode ? q| AND branchcode = ?| : q|| )
107 . q| GROUP BY code, module, name ORDER BY name|, { Slice => {} }
108 , ( $module ? $module : () )
109 , ( $code ? $code : () )
110 , ( defined $branchcode ? $branchcode : () )
116 =head2 GetLetterTemplates
118 my $letter_templates = GetLetterTemplates(
120 module => 'circulation',
122 branchcode => 'CPL', # '' for default,
126 Return a hashref of letter templates.
130 sub GetLetterTemplates {
133 my $module = $params->{module};
134 my $code = $params->{code};
135 my $branchcode = $params->{branchcode} // '';
136 my $dbh = C4::Context->dbh;
137 return Koha::Notice::Templates->search(
141 branchcode => $branchcode,
143 C4::Context->preference('TranslateNotices')
145 : ( lang => 'default' )
151 =head2 GetLettersAvailableForALibrary
153 my $letters = GetLettersAvailableForALibrary(
155 branchcode => 'CPL', # '' for default
156 module => 'circulation',
160 Return an arrayref of letters, sorted by name.
161 If a specific letter exist for the given branchcode, it will be retrieve.
162 Otherwise the default letter will be.
166 sub GetLettersAvailableForALibrary {
168 my $branchcode = $filters->{branchcode};
169 my $module = $filters->{module};
171 croak "module should be provided" unless $module;
173 my $dbh = C4::Context->dbh;
174 my $default_letters = $dbh->selectall_arrayref(
176 SELECT module, code, branchcode, name
180 . q| AND branchcode = ''|
181 . ( $module ? q| AND module = ?| : q|| )
182 . q| ORDER BY name|, { Slice => {} }
183 , ( $module ? $module : () )
186 my $specific_letters;
188 $specific_letters = $dbh->selectall_arrayref(
190 SELECT module, code, branchcode, name
194 . q| AND branchcode = ?|
195 . ( $module ? q| AND module = ?| : q|| )
196 . q| ORDER BY name|, { Slice => {} }
198 , ( $module ? $module : () )
203 for my $l (@$default_letters) {
204 $letters{ $l->{code} } = $l;
206 for my $l (@$specific_letters) {
207 # Overwrite the default letter with the specific one.
208 $letters{ $l->{code} } = $l;
211 return [ map { $letters{$_} }
212 sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
222 module => 'circulation',
228 Delete the letter. The mtt parameter is facultative.
229 If not given, all templates mathing the other parameters will be removed.
235 my $branchcode = $params->{branchcode};
236 my $module = $params->{module};
237 my $code = $params->{code};
238 my $mtt = $params->{mtt};
239 my $lang = $params->{lang};
240 my $dbh = C4::Context->dbh;
247 . ( $mtt ? q| AND message_transport_type = ?| : q|| )
248 . ( $lang? q| AND lang = ?| : q|| )
249 , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
254 my $err = &SendAlerts($type, $externalid, $letter_code);
257 - $type : the type of alert
258 - $externalid : the id of the "object" to query
259 - $letter_code : the notice template to use
261 C<&SendAlerts> sends an email notice directly to a patron or a vendor.
263 Currently it supports ($type):
264 - claim serial issues (claimissues)
265 - claim acquisition orders (claimacquisition)
266 - send acquisition orders to the vendor (orderacquisition)
267 - notify patrons about newly received serial issues (issue)
268 - notify patrons when their account is created (members)
270 Returns undef or { error => 'message } on failure.
271 Returns true on success.
276 my ( $type, $externalid, $letter_code ) = @_;
277 my $dbh = C4::Context->dbh;
280 if ( $type eq 'issue' ) {
282 # prepare the letter...
283 # search the subscriptionid
286 "SELECT subscriptionid FROM serial WHERE serialid=?");
287 $sth->execute($externalid);
288 my ($subscriptionid) = $sth->fetchrow
289 or warn( "No subscription for '$externalid'" ),
292 # search the biblionumber
295 "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
296 $sth->execute($subscriptionid);
297 my ($biblionumber) = $sth->fetchrow
298 or warn( "No biblionumber for '$subscriptionid'" ),
301 # find the list of subscribers to notify
302 my $subscription = Koha::Subscriptions->find( $subscriptionid );
303 my $subscribers = $subscription->subscribers;
304 while ( my $patron = $subscribers->next ) {
305 my $email = $patron->email or next;
307 # warn "sending issues...";
308 my $userenv = C4::Context->userenv;
309 my $library = $patron->library;
310 my $letter = GetPreparedLetter (
312 letter_code => $letter_code,
313 branchcode => $userenv->{branch},
315 'branches' => $library->branchcode,
316 'biblio' => $biblionumber,
317 'biblioitems' => $biblionumber,
318 'borrowers' => $patron->unblessed,
319 'subscription' => $subscriptionid,
320 'serial' => $externalid,
325 # FIXME: This 'default' behaviour should be moved to Koha::Email
326 my $mail = Koha::Email->create(
329 from => $library->branchemail,
330 reply_to => $library->branchreplyto,
331 sender => $library->branchreturnpath,
332 subject => "" . $letter->{title},
336 if ( $letter->{is_html} ) {
337 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
340 $mail->text_body( $letter->{content} );
344 $mail->send_or_die({ transport => $library->smtp_server->transport });
347 # We expect ref($_) eq 'Email::Sender::Failure'
348 $error = $_->message;
354 return { error => $error }
358 elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
360 # prepare the letter...
366 if ( $type eq 'claimacquisition') {
368 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
370 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
371 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
372 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
373 WHERE aqorders.ordernumber IN (
377 carp "No order selected";
378 return { error => "no_order_selected" };
380 $strsth .= join( ",", ('?') x @$externalid ) . ")";
381 $action = "ACQUISITION CLAIM";
382 $sthorders = $dbh->prepare($strsth);
383 $sthorders->execute( @$externalid );
384 $dataorders = $sthorders->fetchall_arrayref( {} );
387 if ($type eq 'claimissues') {
389 SELECT serial.*,subscription.*, biblio.*, biblioitems.*, aqbooksellers.*,
390 aqbooksellers.id AS booksellerid
392 LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
393 LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
394 LEFT JOIN biblioitems ON serial.biblionumber = biblioitems.biblionumber
395 LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
396 WHERE serial.serialid IN (
400 carp "No issues selected";
401 return { error => "no_issues_selected" };
404 $strsth .= join( ",", ('?') x @$externalid ) . ")";
405 $action = "SERIAL CLAIM";
406 $sthorders = $dbh->prepare($strsth);
407 $sthorders->execute( @$externalid );
408 $dataorders = $sthorders->fetchall_arrayref( {} );
411 if ( $type eq 'orderacquisition') {
412 my $basketno = $externalid;
414 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
416 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
417 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
418 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
419 WHERE aqbasket.basketno = ?
420 AND orderstatus IN ('new','ordered')
423 unless ( $basketno ) {
424 carp "No basketnumber given";
425 return { error => "no_basketno" };
427 $action = "ACQUISITION ORDER";
428 $sthorders = $dbh->prepare($strsth);
429 $sthorders->execute($basketno);
430 $dataorders = $sthorders->fetchall_arrayref( {} );
434 $dbh->prepare("select * from aqbooksellers where id=?");
435 $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
436 my $databookseller = $sthbookseller->fetchrow_hashref;
438 my $addressee = $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
441 $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
442 $sthcontact->execute( $dataorders->[0]->{booksellerid} );
443 my $datacontact = $sthcontact->fetchrow_hashref;
447 push @email, $datacontact->{email} if ( $datacontact && $datacontact->{email} );
449 warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
450 return { error => "no_email" };
453 while ($addlcontact = $sthcontact->fetchrow_hashref) {
454 push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
457 my $userenv = C4::Context->userenv;
458 my $letter = GetPreparedLetter (
460 letter_code => $letter_code,
461 branchcode => $userenv->{branch},
463 'branches' => $userenv->{branch},
464 'aqbooksellers' => $databookseller,
465 'aqcontacts' => $datacontact,
466 'aqbasket' => $basketno,
468 repeat => $dataorders,
470 ) or return { error => "no_letter" };
472 # Remove the order tag
473 $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
476 my $library = Koha::Libraries->find( $userenv->{branch} );
477 my $mail = Koha::Email->create(
479 to => join( ',', @email ),
480 cc => join( ',', @cc ),
483 C4::Context->preference("ClaimsBccCopy")
484 && ( $type eq 'claimacquisition'
485 || $type eq 'claimissues' )
487 ? ( bcc => $userenv->{emailaddress} )
490 from => $library->branchemail
491 || C4::Context->preference('KohaAdminEmailAddress'),
492 subject => "" . $letter->{title},
496 if ( $letter->{is_html} ) {
497 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
500 $mail->text_body( "" . $letter->{content} );
504 $mail->send_or_die({ transport => $library->smtp_server->transport });
507 # We expect ref($_) eq 'Email::Sender::Failure'
508 $error = $_->message;
514 return { error => $error }
517 my $module = $action eq 'ACQUISITION ORDER' ? 'ACQUISITIONS' : 'CLAIMS';
523 . join( ',', @email )
528 ) if C4::Context->preference("ClaimsLog");
530 # send an "account details" notice to a newly created user
531 elsif ( $type eq 'members' ) {
532 my $library = Koha::Libraries->find( $externalid->{branchcode} );
533 my $letter = GetPreparedLetter (
535 letter_code => $letter_code,
536 branchcode => $externalid->{'branchcode'},
537 lang => $externalid->{lang} || 'default',
539 'branches' => $library->unblessed,
540 'borrowers' => $externalid->{'borrowernumber'},
542 substitute => { 'borrowers.password' => $externalid->{'password'} },
545 return { error => "no_email" } unless $externalid->{'emailaddr'};
549 # FIXME: This 'default' behaviour should be moved to Koha::Email
550 my $mail = Koha::Email->create(
552 to => $externalid->{'emailaddr'},
553 from => $library->branchemail,
554 reply_to => $library->branchreplyto,
555 sender => $library->branchreturnpath,
556 subject => "" . $letter->{'title'},
560 if ( $letter->{is_html} ) {
561 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
564 $mail->text_body( $letter->{content} );
567 $mail->send_or_die({ transport => $library->smtp_server->transport });
570 # We expect ref($_) eq 'Email::Sender::Failure'
571 $error = $_->message;
577 return { error => $error }
581 # If we come here, return an OK status
585 =head2 GetPreparedLetter( %params )
588 module => letter module, mandatory
589 letter_code => letter code, mandatory
590 branchcode => for letter selection, if missing default system letter taken
591 tables => a hashref with table names as keys. Values are either:
592 - a scalar - primary key value
593 - an arrayref - primary key values
594 - a hashref - full record
595 substitute => custom substitution key/value pairs
596 repeat => records to be substituted on consecutive lines:
597 - an arrayref - tries to guess what needs substituting by
598 taking remaining << >> tokensr; not recommended
599 - a hashref token => @tables - replaces <token> << >> << >> </token>
600 subtemplate for each @tables row; table is a hashref as above
601 want_librarian => boolean, if set to true triggers librarian details
602 substitution from the userenv
604 letter fields hashref (title & content useful)
608 sub GetPreparedLetter {
611 my $letter = $params{letter};
612 my $lang = $params{lang} || 'default';
615 my $module = $params{module} or croak "No module";
616 my $letter_code = $params{letter_code} or croak "No letter_code";
617 my $branchcode = $params{branchcode} || '';
618 my $mtt = $params{message_transport_type} || 'email';
620 my $template = Koha::Notice::Templates->find_effective_template(
623 code => $letter_code,
624 branchcode => $branchcode,
625 message_transport_type => $mtt,
630 unless ( $template ) {
631 warn( "No $module $letter_code letter transported by " . $mtt );
635 $letter = $template->unblessed;
636 $letter->{'content-type'} = 'text/html; charset="UTF-8"' if $letter->{is_html};
639 my $tables = $params{tables} || {};
640 my $substitute = $params{substitute} || {};
641 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
642 my $repeat = $params{repeat};
643 %$tables || %$substitute || $repeat || %$loops
644 or carp( "ERROR: nothing to substitute - both 'tables', 'loops' and 'substitute' are empty" ),
646 my $want_librarian = $params{want_librarian};
649 while ( my ($token, $val) = each %$substitute ) {
650 if ( $token eq 'items.content' ) {
651 $val =~ s|\n|<br/>|g if $letter->{is_html};
654 $letter->{title} =~ s/<<$token>>/$val/g;
655 $letter->{content} =~ s/<<$token>>/$val/g;
659 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
660 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
662 if ($want_librarian) {
663 # parsing librarian name
664 my $userenv = C4::Context->userenv;
665 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
666 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
667 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
670 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
673 if (ref ($repeat) eq 'ARRAY' ) {
674 $repeat_no_enclosing_tags = $repeat;
676 $repeat_enclosing_tags = $repeat;
680 if ($repeat_enclosing_tags) {
681 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
682 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
685 my %subletter = ( title => '', content => $subcontent );
686 _substitute_tables( \%subletter, $_ );
689 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
695 _substitute_tables( $letter, $tables );
698 if ($repeat_no_enclosing_tags) {
699 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
704 $c =~ s/<<count>>/$i/go;
705 foreach my $field ( keys %{$_} ) {
706 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
710 } @$repeat_no_enclosing_tags;
712 my $replaceby = join( "\n", @lines );
713 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
717 $letter->{content} = _process_tt(
719 content => $letter->{content},
722 substitute => $substitute,
727 $letter->{title} = _process_tt(
729 content => $letter->{title},
732 substitute => $substitute,
736 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
741 sub _substitute_tables {
742 my ( $letter, $tables ) = @_;
743 while ( my ($table, $param) = each %$tables ) {
746 my $ref = ref $param;
749 if ($ref && $ref eq 'HASH') {
753 my $sth = _parseletter_sth($table);
755 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
758 $sth->execute( $ref ? @$param : $param );
760 $values = $sth->fetchrow_hashref;
764 _parseletter ( $letter, $table, $values );
768 sub _parseletter_sth {
772 carp "ERROR: _parseletter_sth() called without argument (table)";
775 # NOTE: we used to check whether we had a statement handle cached in
776 # a %handles module-level variable. This was a dumb move and
777 # broke things for the rest of us. prepare_cached is a better
778 # way to cache statement handles anyway.
780 ($table eq 'accountlines' ) ? "SELECT * FROM $table WHERE accountlines_id = ?" :
781 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
782 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
783 ($table eq 'credits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
784 ($table eq 'debits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
785 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
786 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
787 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE issue_id = ?" :
788 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
789 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
790 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
791 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
792 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
793 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
794 ($table eq 'aqbasket' ) ? "SELECT * FROM $table WHERE basketno = ?" :
795 ($table eq 'illrequests' ) ? "SELECT * FROM $table WHERE illrequest_id = ?" :
796 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
797 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
798 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
799 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
800 ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" :
801 ($table eq 'additional_contents' || $table eq 'opac_news') ? "SELECT * FROM additional_contents WHERE idnew = ?" :
804 warn "ERROR: No _parseletter_sth query for table '$table'";
805 return; # nothing to get
807 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
808 warn "ERROR: Failed to prepare query: '$query'";
811 return $sth; # now cache is populated for that $table
814 =head2 _parseletter($letter, $table, $values)
817 - $letter : a hash to letter fields (title & content useful)
818 - $table : the Koha table to parse.
819 - $values_in : table record hashref
820 parse all fields from a table, and replace values in title & content with the appropriate value
821 (not exported sub, used only internally)
826 my ( $letter, $table, $values_in ) = @_;
828 # Work on a local copy of $values_in (passed by reference) to avoid side effects
829 # in callers ( by changing / formatting values )
830 my $values = $values_in ? { %$values_in } : {};
832 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
833 $values->{'dateexpiry'} = output_pref({ dt => dt_from_string( $values->{'dateexpiry'} ), dateonly => 1 });
836 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
837 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
840 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
841 my $todaysdate = output_pref( dt_from_string() );
842 $letter->{content} =~ s/<<today>>/$todaysdate/go;
845 while ( my ($field, $val) = each %$values ) {
846 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
847 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
848 #Therefore adding the test on biblio. This includes biblioitems,
849 #but excludes items. Removed unneeded global and lookahead.
851 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
852 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
853 $val = $av->count ? $av->next->lib : '';
857 my $replacedby = defined ($val) ? $val : '';
859 and not $replacedby =~ m|9999-12-31|
860 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
862 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
863 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
864 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
866 for my $letter_field ( qw( title content ) ) {
867 my $filter_string_used = q{};
868 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
869 # We overwrite $dateonly if the filter exists and we have a time in the datetime
870 $filter_string_used = $1 || q{};
871 $dateonly = $1 unless $dateonly;
873 my $replacedby_date = eval {
874 output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
877 if ( $letter->{ $letter_field } ) {
878 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
879 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
883 # Other fields replacement
885 for my $letter_field ( qw( title content ) ) {
886 if ( $letter->{ $letter_field } ) {
887 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
888 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
894 if ($table eq 'borrowers' && $letter->{content}) {
895 my $patron = Koha::Patrons->find( $values->{borrowernumber} );
897 my $attributes = $patron->extended_attributes;
899 while ( my $attribute = $attributes->next ) {
900 my $code = $attribute->code;
901 my $val = $attribute->description; # FIXME - we always display intranet description here!
902 $val =~ s/\p{P}(?=$)//g if $val;
903 next unless $val gt '';
905 push @{ $attr{$code} }, $val;
907 while ( my ($code, $val_ar) = each %attr ) {
908 my $replacefield = "<<borrower-attribute:$code>>";
909 my $replacedby = join ',', @$val_ar;
910 $letter->{content} =~ s/$replacefield/$replacedby/g;
919 my $success = EnqueueLetter( { letter => $letter,
920 borrowernumber => '12', message_transport_type => 'email' } )
922 Places a letter in the message_queue database table, which will
923 eventually get processed (sent) by the process_message_queue.pl
924 cronjob when it calls SendQueuedMessages.
926 Return message_id on success
929 * letter - required; A letter hashref as returned from GetPreparedLetter
930 * message_transport_type - required; One of the available mtts
931 * borrowernumber - optional if 'to_address' is passed; The borrowernumber of the patron we enqueuing the notice for
932 * to_address - optional if 'borrowernumber' is passed; The destination email address for the notice (defaults to patron->notice_email_address)
933 * from_address - optional; The from address for the notice, defaults to patron->library->from_email_address
934 * reply_address - optional; The reply address for the notice, defaults to patron->library->reply_to
939 my $params = shift or return;
941 return unless exists $params->{'letter'};
942 # return unless exists $params->{'borrowernumber'};
943 return unless exists $params->{'message_transport_type'};
945 my $content = $params->{letter}->{content};
946 $content =~ s/\s+//g if(defined $content);
947 if ( not defined $content or $content eq '' ) {
948 Koha::Logger->get->info("Trying to add an empty message to the message queue");
952 # If we have any attachments we should encode then into the body.
953 if ( $params->{'attachments'} ) {
954 $params->{'letter'} = _add_attachments(
955 { letter => $params->{'letter'},
956 attachments => $params->{'attachments'},
961 my $dbh = C4::Context->dbh();
962 my $statement = << 'ENDSQL';
963 INSERT INTO message_queue
964 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, reply_address, content_type, failure_code )
966 ( ?, ?, ?, ?, ?, ?, ?, CAST(NOW() AS DATETIME), ?, ?, ?, ?, ? )
969 my $sth = $dbh->prepare($statement);
970 my $result = $sth->execute(
971 $params->{'borrowernumber'}, # borrowernumber
972 $params->{'letter'}->{'title'}, # subject
973 $params->{'letter'}->{'content'}, # content
974 $params->{'letter'}->{'metadata'} || '', # metadata
975 $params->{'letter'}->{'code'} || '', # letter_code
976 $params->{'message_transport_type'}, # message_transport_type
978 $params->{'to_address'}, # to_address
979 $params->{'from_address'}, # from_address
980 $params->{'reply_address'}, # reply_address
981 $params->{'letter'}->{'content-type'}, # content_type
982 $params->{'failure_code'} || '', # failure_code
984 return $dbh->last_insert_id(undef,undef,'message_queue', undef);
987 =head2 SendQueuedMessages ([$hashref])
989 my $sent = SendQueuedMessages({
990 letter_code => $letter_code,
991 borrowernumber => $who_letter_is_for,
997 Sends all of the 'pending' items in the message queue, unless
998 parameters are passed.
1000 The letter_code, borrowernumber and limit parameters are used
1001 to build a parameter set for _get_unsent_messages, thus limiting
1002 which pending messages will be processed. They are all optional.
1004 The verbose parameter can be used to generate debugging output.
1005 It is also optional.
1007 Returns number of messages sent.
1011 sub SendQueuedMessages {
1014 my $which_unsent_messages = {
1015 'message_id' => $params->{'message_id'},
1016 'limit' => $params->{'limit'} // 0,
1017 'borrowernumber' => $params->{'borrowernumber'} // q{},
1018 'letter_code' => $params->{'letter_code'} // q{},
1019 'type' => $params->{'type'} // q{},
1021 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
1022 MESSAGE: foreach my $message ( @$unsent_messages ) {
1023 my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
1024 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
1025 $message_object->make_column_dirty('status');
1026 return unless $message_object->store;
1028 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
1029 warn sprintf( 'sending %s message to patron: %s',
1030 $message->{'message_transport_type'},
1031 $message->{'borrowernumber'} || 'Admin' )
1032 if $params->{'verbose'};
1033 # This is just begging for subclassing
1034 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
1035 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
1036 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1038 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
1039 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
1040 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1041 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1042 unless ( $sms_provider ) {
1043 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1044 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1047 unless ( $patron->smsalertnumber ) {
1048 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1049 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1052 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1053 $message->{to_address} .= '@' . $sms_provider->domain();
1055 # Check for possible from_address override
1056 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1057 if ($from_address && $message->{from_address} ne $from_address) {
1058 $message->{from_address} = $from_address;
1059 _update_message_from_address($message->{'message_id'}, $message->{from_address});
1062 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1063 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1065 _send_message_by_sms( $message );
1069 return scalar( @$unsent_messages );
1072 =head2 GetRSSMessages
1074 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1076 returns a listref of all queued RSS messages for a particular person.
1080 sub GetRSSMessages {
1083 return unless $params;
1084 return unless ref $params;
1085 return unless $params->{'borrowernumber'};
1087 return _get_unsent_messages( { message_transport_type => 'rss',
1088 limit => $params->{'limit'},
1089 borrowernumber => $params->{'borrowernumber'}, } );
1092 =head2 GetPrintMessages
1094 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1096 Returns a arrayref of all queued print messages (optionally, for a particular
1101 sub GetPrintMessages {
1102 my $params = shift || {};
1104 return _get_unsent_messages( { message_transport_type => 'print',
1105 borrowernumber => $params->{'borrowernumber'},
1109 =head2 GetQueuedMessages ([$hashref])
1111 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1113 Fetches a list of messages from the message queue optionally filtered by borrowernumber
1114 and limited to specified limit.
1116 Return is an arrayref of hashes, each has represents a message in the message queue.
1120 sub GetQueuedMessages {
1123 my $dbh = C4::Context->dbh();
1124 my $statement = << 'ENDSQL';
1125 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued, updated_on, failure_code
1131 if ( exists $params->{'borrowernumber'} ) {
1132 push @whereclauses, ' borrowernumber = ? ';
1133 push @query_params, $params->{'borrowernumber'};
1136 if ( @whereclauses ) {
1137 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1140 if ( defined $params->{'limit'} ) {
1141 $statement .= ' LIMIT ? ';
1142 push @query_params, $params->{'limit'};
1145 my $sth = $dbh->prepare( $statement );
1146 my $result = $sth->execute( @query_params );
1147 return $sth->fetchall_arrayref({});
1150 =head2 GetMessageTransportTypes
1152 my @mtt = GetMessageTransportTypes();
1154 returns an arrayref of transport types
1158 sub GetMessageTransportTypes {
1159 my $dbh = C4::Context->dbh();
1160 my $mtts = $dbh->selectcol_arrayref("
1161 SELECT message_transport_type
1162 FROM message_transport_types
1163 ORDER BY message_transport_type
1170 my $message = C4::Letters::Message($message_id);
1175 my ( $message_id ) = @_;
1176 return unless $message_id;
1177 my $dbh = C4::Context->dbh;
1178 return $dbh->selectrow_hashref(q|
1179 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
1181 WHERE message_id = ?
1182 |, {}, $message_id );
1185 =head2 ResendMessage
1187 Attempt to resend a message which has failed previously.
1189 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1191 Updates the message to 'pending' status so that
1192 it will be resent later on.
1194 returns 1 on success, 0 on failure, undef if no message was found
1199 my $message_id = shift;
1200 return unless $message_id;
1202 my $message = GetMessage( $message_id );
1203 return unless $message;
1205 if ( $message->{status} ne 'pending' ) {
1206 $rv = C4::Letters::_set_message_status({
1207 message_id => $message_id,
1208 status => 'pending',
1210 $rv = $rv > 0? 1: 0;
1211 # Clear destination email address to force address update
1212 _update_message_to_address( $message_id, undef ) if $rv &&
1213 $message->{message_transport_type} eq 'email';
1218 =head2 _add_attachements
1220 _add_attachments({ letter => $letter, attachments => $attachments });
1223 letter - the standard letter hashref
1224 attachments - listref of attachments. each attachment is a hashref of:
1225 type - the mime type, like 'text/plain'
1226 content - the actual attachment
1227 filename - the name of the attachment.
1229 returns your letter object, with the content updated.
1230 This routine picks the I<content> of I<letter> and generates a MIME
1231 email, attaching the passed I<attachments> using Koha::Email. The
1232 content is replaced by the string representation of the MIME object,
1233 and the content-type is updated to B<MIME> for later handling.
1237 sub _add_attachments {
1240 my $letter = $params->{letter};
1241 my $attachments = $params->{attachments};
1242 return $letter unless @$attachments;
1244 my $message = Koha::Email->new;
1246 if ( $letter->{is_html} ) {
1247 $message->html_body( _wrap_html( $letter->{content}, $letter->{title} ) );
1250 $message->text_body( $letter->{content} );
1253 foreach my $attachment ( @$attachments ) {
1255 Encode::encode( "UTF-8", $attachment->{content} ),
1256 content_type => 'application/octet-stream',
1257 name => $attachment->{filename},
1258 disposition => 'attachment',
1262 $letter->{'content-type'} = 'MIME';
1263 $letter->{content} = $message->as_string;
1269 =head2 _get_unsent_messages
1271 This function's parameter hash reference takes the following
1272 optional named parameters:
1273 message_transport_type: method of message sending (e.g. email, sms, etc.)
1274 borrowernumber : who the message is to be sent
1275 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1276 message_id : the message_id of the message. In that case the sub will return only 1 result
1277 limit : maximum number of messages to send
1279 This function returns an array of matching hash referenced rows from
1280 message_queue with some borrower information added.
1284 sub _get_unsent_messages {
1287 my $dbh = C4::Context->dbh();
1289 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
1290 FROM message_queue mq
1291 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1295 my @query_params = ('pending');
1296 if ( ref $params ) {
1297 if ( $params->{'message_transport_type'} ) {
1298 $statement .= ' AND mq.message_transport_type = ? ';
1299 push @query_params, $params->{'message_transport_type'};
1301 if ( $params->{'borrowernumber'} ) {
1302 $statement .= ' AND mq.borrowernumber = ? ';
1303 push @query_params, $params->{'borrowernumber'};
1305 if ( $params->{'letter_code'} ) {
1306 $statement .= ' AND mq.letter_code = ? ';
1307 push @query_params, $params->{'letter_code'};
1309 if ( $params->{'type'} ) {
1310 $statement .= ' AND message_transport_type = ? ';
1311 push @query_params, $params->{'type'};
1313 if ( $params->{message_id} ) {
1314 $statement .= ' AND message_id = ?';
1315 push @query_params, $params->{message_id};
1317 if ( $params->{'limit'} ) {
1318 $statement .= ' limit ? ';
1319 push @query_params, $params->{'limit'};
1323 my $sth = $dbh->prepare( $statement );
1324 my $result = $sth->execute( @query_params );
1325 return $sth->fetchall_arrayref({});
1328 sub _send_message_by_email {
1329 my $message = shift or return;
1330 my ($username, $password, $method) = @_;
1332 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1333 my $to_address = $message->{'to_address'};
1334 unless ($to_address) {
1336 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1337 _set_message_status(
1339 message_id => $message->{'message_id'},
1341 failure_code => 'INVALID_BORNUMBER'
1346 $to_address = $patron->notice_email_address;
1347 unless ($to_address) {
1348 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1349 # warning too verbose for this more common case?
1350 _set_message_status(
1352 message_id => $message->{'message_id'},
1354 failure_code => 'NO_EMAIL'
1361 my $subject = $message->{'subject'};
1363 my $content = $message->{'content'};
1364 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1365 my $is_html = $content_type =~ m/html/io;
1367 my $branch_email = undef;
1368 my $branch_replyto = undef;
1369 my $branch_returnpath = undef;
1373 $library = $patron->library;
1374 $branch_email = $library->from_email_address;
1375 $branch_replyto = $library->branchreplyto;
1376 $branch_returnpath = $library->branchreturnpath;
1379 # NOTE: Patron may not be defined above so branch_email may be undefined still
1380 # so we need to fallback to KohaAdminEmailAddress as a last resort.
1382 $message->{'from_address'}
1384 || C4::Context->preference('KohaAdminEmailAddress');
1385 if( !$from_address ) {
1386 _set_message_status(
1388 message_id => $message->{'message_id'},
1390 failure_code => 'NO_FROM',
1402 C4::Context->preference('NoticeBcc')
1403 ? ( bcc => C4::Context->preference('NoticeBcc') )
1406 from => $from_address,
1407 reply_to => $message->{'reply_address'} || $branch_replyto,
1408 sender => $branch_returnpath,
1409 subject => "" . $message->{subject}
1412 if ( $message->{'content_type'} && $message->{'content_type'} eq 'MIME' ) {
1414 # The message has been previously composed as a valid MIME object
1415 # and serialized as a string on the DB
1416 $email = Koha::Email->new_from_string($content);
1417 $email->create($params);
1419 $email = Koha::Email->create($params);
1421 $email->html_body( _wrap_html( $content, $subject ) );
1423 $email->text_body($content);
1428 if ( ref($_) eq 'Koha::Exceptions::BadParameter' ) {
1429 _set_message_status(
1431 message_id => $message->{'message_id'},
1433 failure_code => "INVALID_EMAIL:".$_->parameter
1437 _set_message_status(
1439 message_id => $message->{'message_id'},
1441 failure_code => 'UNKNOWN_ERROR'
1447 return unless $email;
1451 $smtp_server = $library->smtp_server;
1454 $smtp_server = Koha::SMTP::Servers->get_default;
1460 sasl_username => $username,
1461 sasl_password => $password,
1466 # if initial message address was empty, coming here means that a to address was found and
1467 # queue should be updated; same if to address was overriden by Koha::Email->create
1468 _update_message_to_address( $message->{'message_id'}, $email->email->header('To') )
1469 if !$message->{to_address}
1470 || $message->{to_address} ne $email->email->header('To');
1473 $email->send_or_die({ transport => $smtp_server->transport });
1475 _set_message_status(
1477 message_id => $message->{'message_id'},
1485 _set_message_status(
1487 message_id => $message->{'message_id'},
1489 failure_code => 'SENDMAIL'
1493 carp "$Mail::Sendmail::error";
1499 my ($content, $title) = @_;
1501 my $css = C4::Context->preference("NoticeCSS") || '';
1502 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1504 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1505 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1506 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1508 <title>$title</title>
1509 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1520 my ( $message ) = @_;
1521 my $dbh = C4::Context->dbh;
1522 my $count = $dbh->selectrow_array(q|
1525 WHERE message_transport_type = ?
1526 AND borrowernumber = ?
1528 AND CAST(updated_on AS date) = CAST(NOW() AS date)
1531 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1535 sub _send_message_by_sms {
1536 my $message = shift or return;
1537 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1539 unless ( $patron and $patron->smsalertnumber ) {
1540 _set_message_status( { message_id => $message->{'message_id'},
1542 failure_code => 'MISSING_SMS' } );
1546 if ( _is_duplicate( $message ) ) {
1547 _set_message_status(
1549 message_id => $message->{'message_id'},
1551 failure_code => 'DUPLICATE_MESSAGE'
1557 my $success = C4::SMS->send_sms(
1559 destination => $patron->smsalertnumber,
1560 message => $message->{'content'},
1565 _set_message_status(
1567 message_id => $message->{'message_id'},
1574 _set_message_status(
1576 message_id => $message->{'message_id'},
1578 failure_code => 'NO_NOTES'
1586 sub _update_message_to_address {
1588 my $dbh = C4::Context->dbh();
1589 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1592 sub _update_message_from_address {
1593 my ($message_id, $from_address) = @_;
1594 my $dbh = C4::Context->dbh();
1595 $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1598 sub _set_message_status {
1599 my $params = shift or return;
1601 foreach my $required_parameter ( qw( message_id status ) ) {
1602 return unless exists $params->{ $required_parameter };
1605 my $dbh = C4::Context->dbh();
1606 my $statement = 'UPDATE message_queue SET status= ?, failure_code= ? WHERE message_id = ?';
1607 my $sth = $dbh->prepare( $statement );
1608 my $result = $sth->execute( $params->{'status'},
1609 $params->{'failure_code'} || '',
1610 $params->{'message_id'} );
1615 my ( $params ) = @_;
1617 my $content = $params->{content};
1618 my $tables = $params->{tables};
1619 my $loops = $params->{loops};
1620 my $substitute = $params->{substitute} || {};
1621 my $lang = defined($params->{lang}) && $params->{lang} ne 'default' ? $params->{lang} : 'en';
1622 my ($theme, $availablethemes);
1624 my $htdocs = C4::Context->config('intrahtdocs');
1625 ($theme, $lang, $availablethemes)= C4::Templates::availablethemes( $htdocs, 'about.tt', 'intranet', $lang);
1627 foreach (@$availablethemes) {
1628 push @includes, "$htdocs/$_/$lang/includes";
1629 push @includes, "$htdocs/$_/en/includes" unless $lang eq 'en';
1632 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1633 my $template = Template->new(
1637 PLUGIN_BASE => 'Koha::Template::Plugin',
1638 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1639 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1640 INCLUDE_PATH => \@includes,
1642 ENCODING => 'UTF-8',
1644 ) or die Template->error();
1646 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
1648 $content = add_tt_filters( $content );
1649 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1652 $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1657 sub _get_tt_params {
1658 my ($tables, $is_a_loop) = @_;
1664 article_requests => {
1665 module => 'Koha::ArticleRequests',
1666 singular => 'article_request',
1667 plural => 'article_requests',
1671 module => 'Koha::Acquisition::Baskets',
1672 singular => 'basket',
1673 plural => 'baskets',
1677 module => 'Koha::Biblios',
1678 singular => 'biblio',
1679 plural => 'biblios',
1680 pk => 'biblionumber',
1683 module => 'Koha::Biblioitems',
1684 singular => 'biblioitem',
1685 plural => 'biblioitems',
1686 pk => 'biblioitemnumber',
1689 module => 'Koha::Patrons',
1690 singular => 'borrower',
1691 plural => 'borrowers',
1692 pk => 'borrowernumber',
1695 module => 'Koha::Libraries',
1696 singular => 'branch',
1697 plural => 'branches',
1701 module => 'Koha::Account::Lines',
1702 singular => 'credit',
1703 plural => 'credits',
1704 pk => 'accountlines_id',
1707 module => 'Koha::Account::Lines',
1708 singular => 'debit',
1710 pk => 'accountlines_id',
1713 module => 'Koha::Items',
1718 additional_contents => {
1719 module => 'Koha::AdditionalContents',
1720 singular => 'additional_content',
1721 plural => 'additional_contents',
1725 module => 'Koha::AdditionalContents',
1731 module => 'Koha::Acquisition::Orders',
1732 singular => 'order',
1734 pk => 'ordernumber',
1737 module => 'Koha::Holds',
1743 module => 'Koha::Serials',
1744 singular => 'serial',
1745 plural => 'serials',
1749 module => 'Koha::Subscriptions',
1750 singular => 'subscription',
1751 plural => 'subscriptions',
1752 pk => 'subscriptionid',
1755 module => 'Koha::Suggestions',
1756 singular => 'suggestion',
1757 plural => 'suggestions',
1758 pk => 'suggestionid',
1761 module => 'Koha::Checkouts',
1762 singular => 'checkout',
1763 plural => 'checkouts',
1767 module => 'Koha::Old::Checkouts',
1768 singular => 'old_checkout',
1769 plural => 'old_checkouts',
1773 module => 'Koha::Checkouts',
1774 singular => 'overdue',
1775 plural => 'overdues',
1778 borrower_modifications => {
1779 module => 'Koha::Patron::Modifications',
1780 singular => 'patron_modification',
1781 plural => 'patron_modifications',
1782 fk => 'verification_token',
1785 module => 'Koha::Illrequests',
1786 singular => 'illrequest',
1787 plural => 'illrequests',
1788 pk => 'illrequest_id'
1792 foreach my $table ( keys %$tables ) {
1793 next unless $config->{$table};
1795 my $ref = ref( $tables->{$table} ) || q{};
1796 my $module = $config->{$table}->{module};
1798 if ( can_load( modules => { $module => undef } ) ) {
1799 my $pk = $config->{$table}->{pk};
1800 my $fk = $config->{$table}->{fk};
1803 my $values = $tables->{$table} || [];
1804 unless ( ref( $values ) eq 'ARRAY' ) {
1805 croak "ERROR processing table $table. Wrong API call.";
1807 my $key = $pk ? $pk : $fk;
1808 # $key does not come from user input
1809 my $objects = $module->search(
1810 { $key => $values },
1812 # We want to retrieve the data in the same order
1814 # field is a MySQLism, but they are no other way to do it
1815 # To be generic we could do it in perl, but we will need to fetch
1816 # all the data then order them
1817 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1820 $params->{ $config->{$table}->{plural} } = $objects;
1822 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1823 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1825 if ( $fk ) { # Using a foreign key for lookup
1826 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1828 foreach my $key ( @$fk ) {
1829 $search->{$key} = $id->{$key};
1831 $object = $module->search( $search )->last();
1832 } else { # Foreign key is single column
1833 $object = $module->search( { $fk => $id } )->last();
1835 } else { # using the table's primary key for lookup
1836 $object = $module->find($id);
1838 $params->{ $config->{$table}->{singular} } = $object;
1840 else { # $ref eq 'ARRAY'
1842 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1843 $object = $module->search( { $pk => $tables->{$table} } )->last();
1845 else { # Params are mutliple foreign keys
1846 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1848 $params->{ $config->{$table}->{singular} } = $object;
1852 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1856 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1861 =head3 add_tt_filters
1863 $content = add_tt_filters( $content );
1865 Add TT filters to some specific fields if needed.
1867 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1871 sub add_tt_filters {
1872 my ( $content ) = @_;
1873 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1874 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1878 =head2 get_item_content
1880 my $item = Koha::Items->find(...)->unblessed;
1881 my @item_content_fields = qw( date_due title barcode author itemnumber );
1882 my $item_content = C4::Letters::get_item_content({
1884 item_content_fields => \@item_content_fields
1887 This function generates a tab-separated list of values for the passed item. Dates
1888 are formatted following the current setup.
1892 sub get_item_content {
1893 my ( $params ) = @_;
1894 my $item = $params->{item};
1895 my $dateonly = $params->{dateonly} || 0;
1896 my $item_content_fields = $params->{item_content_fields} || [];
1898 return unless $item;
1900 my @item_info = map {
1904 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1908 } @$item_content_fields;
1909 return join( "\t", @item_info ) . "\n";