3 # Copyright 2000-2002 Katipo Communications
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
23 use Date::Calc qw( Add_Delta_Days );
27 use Module::Load::Conditional qw(can_load);
37 use Koha::SMS::Providers;
40 use Koha::Notice::Messages;
41 use Koha::Notice::Templates;
42 use Koha::DateUtils qw( format_sqldatetime dt_from_string );
44 use Koha::SMTP::Servers;
45 use Koha::Subscriptions;
47 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
53 &EnqueueLetter &GetLetters &GetLettersAvailableForALibrary &GetLetterTemplates &DelLetter &GetPreparedLetter &GetWrappedLetter &SendAlerts &GetPrintMessages &GetMessageTransportTypes
59 C4::Letters - Give functions for Letters management
67 "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
68 late issues, as well as other tasks like sending a mail to users that have subscribed to a "serial issue alert" (= being warned every time a new issue has arrived at the library)
70 Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
72 =head2 GetLetters([$module])
74 $letters = &GetLetters($module);
75 returns informations about letters.
76 if needed, $module filters for letters given module
78 DEPRECATED - You must use Koha::Notice::Templates instead
79 The group by clause is confusing and can lead to issues
85 my $module = $filters->{module};
86 my $code = $filters->{code};
87 my $branchcode = $filters->{branchcode};
88 my $dbh = C4::Context->dbh;
89 my $letters = $dbh->selectall_arrayref(
91 SELECT code, module, name
95 . ( $module ? q| AND module = ?| : q|| )
96 . ( $code ? q| AND code = ?| : q|| )
97 . ( defined $branchcode ? q| AND branchcode = ?| : q|| )
98 . q| GROUP BY code, module, name ORDER BY name|, { Slice => {} }
99 , ( $module ? $module : () )
100 , ( $code ? $code : () )
101 , ( defined $branchcode ? $branchcode : () )
107 =head2 GetLetterTemplates
109 my $letter_templates = GetLetterTemplates(
111 module => 'circulation',
113 branchcode => 'CPL', # '' for default,
117 Return a hashref of letter templates.
121 sub GetLetterTemplates {
124 my $module = $params->{module};
125 my $code = $params->{code};
126 my $branchcode = $params->{branchcode} // '';
127 my $dbh = C4::Context->dbh;
128 return Koha::Notice::Templates->search(
132 branchcode => $branchcode,
134 C4::Context->preference('TranslateNotices')
136 : ( lang => 'default' )
142 =head2 GetLettersAvailableForALibrary
144 my $letters = GetLettersAvailableForALibrary(
146 branchcode => 'CPL', # '' for default
147 module => 'circulation',
151 Return an arrayref of letters, sorted by name.
152 If a specific letter exist for the given branchcode, it will be retrieve.
153 Otherwise the default letter will be.
157 sub GetLettersAvailableForALibrary {
159 my $branchcode = $filters->{branchcode};
160 my $module = $filters->{module};
162 croak "module should be provided" unless $module;
164 my $dbh = C4::Context->dbh;
165 my $default_letters = $dbh->selectall_arrayref(
167 SELECT module, code, branchcode, name
171 . q| AND branchcode = ''|
172 . ( $module ? q| AND module = ?| : q|| )
173 . q| ORDER BY name|, { Slice => {} }
174 , ( $module ? $module : () )
177 my $specific_letters;
179 $specific_letters = $dbh->selectall_arrayref(
181 SELECT module, code, branchcode, name
185 . q| AND branchcode = ?|
186 . ( $module ? q| AND module = ?| : q|| )
187 . q| ORDER BY name|, { Slice => {} }
189 , ( $module ? $module : () )
194 for my $l (@$default_letters) {
195 $letters{ $l->{code} } = $l;
197 for my $l (@$specific_letters) {
198 # Overwrite the default letter with the specific one.
199 $letters{ $l->{code} } = $l;
202 return [ map { $letters{$_} }
203 sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
213 module => 'circulation',
219 Delete the letter. The mtt parameter is facultative.
220 If not given, all templates mathing the other parameters will be removed.
226 my $branchcode = $params->{branchcode};
227 my $module = $params->{module};
228 my $code = $params->{code};
229 my $mtt = $params->{mtt};
230 my $lang = $params->{lang};
231 my $dbh = C4::Context->dbh;
238 . ( $mtt ? q| AND message_transport_type = ?| : q|| )
239 . ( $lang? q| AND lang = ?| : q|| )
240 , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
245 my $err = &SendAlerts($type, $externalid, $letter_code);
248 - $type : the type of alert
249 - $externalid : the id of the "object" to query
250 - $letter_code : the notice template to use
252 C<&SendAlerts> sends an email notice directly to a patron or a vendor.
254 Currently it supports ($type):
255 - claim serial issues (claimissues)
256 - claim acquisition orders (claimacquisition)
257 - send acquisition orders to the vendor (orderacquisition)
258 - notify patrons about newly received serial issues (issue)
259 - notify patrons when their account is created (members)
261 Returns undef or { error => 'message } on failure.
262 Returns true on success.
267 my ( $type, $externalid, $letter_code ) = @_;
268 my $dbh = C4::Context->dbh;
271 if ( $type eq 'issue' ) {
273 # prepare the letter...
274 # search the subscriptionid
277 "SELECT subscriptionid FROM serial WHERE serialid=?");
278 $sth->execute($externalid);
279 my ($subscriptionid) = $sth->fetchrow
280 or warn( "No subscription for '$externalid'" ),
283 # search the biblionumber
286 "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
287 $sth->execute($subscriptionid);
288 my ($biblionumber) = $sth->fetchrow
289 or warn( "No biblionumber for '$subscriptionid'" ),
292 # find the list of subscribers to notify
293 my $subscription = Koha::Subscriptions->find( $subscriptionid );
294 my $subscribers = $subscription->subscribers;
295 while ( my $patron = $subscribers->next ) {
296 my $email = $patron->email or next;
298 # warn "sending issues...";
299 my $userenv = C4::Context->userenv;
300 my $library = $patron->library;
301 my $letter = GetPreparedLetter (
303 letter_code => $letter_code,
304 branchcode => $userenv->{branch},
306 'branches' => $library->branchcode,
307 'biblio' => $biblionumber,
308 'biblioitems' => $biblionumber,
309 'borrowers' => $patron->unblessed,
310 'subscription' => $subscriptionid,
311 'serial' => $externalid,
316 # FIXME: This 'default' behaviour should be moved to Koha::Email
317 my $mail = Koha::Email->create(
320 from => $library->branchemail,
321 reply_to => $library->branchreplyto,
322 sender => $library->branchreturnpath,
323 subject => "" . $letter->{title},
327 if ( $letter->{is_html} ) {
328 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
331 $mail->text_body( $letter->{content} );
335 $mail->send_or_die({ transport => $library->smtp_server->transport });
338 # We expect ref($_) eq 'Email::Sender::Failure'
339 $error = $_->message;
345 return { error => $error }
349 elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
351 # prepare the letter...
357 if ( $type eq 'claimacquisition') {
359 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
361 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
362 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
363 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
364 WHERE aqorders.ordernumber IN (
368 carp "No order selected";
369 return { error => "no_order_selected" };
371 $strsth .= join( ",", ('?') x @$externalid ) . ")";
372 $action = "ACQUISITION CLAIM";
373 $sthorders = $dbh->prepare($strsth);
374 $sthorders->execute( @$externalid );
375 $dataorders = $sthorders->fetchall_arrayref( {} );
378 if ($type eq 'claimissues') {
380 SELECT serial.*,subscription.*, biblio.*, biblioitems.*, aqbooksellers.*,
381 aqbooksellers.id AS booksellerid
383 LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
384 LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
385 LEFT JOIN biblioitems ON serial.biblionumber = biblioitems.biblionumber
386 LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
387 WHERE serial.serialid IN (
391 carp "No issues selected";
392 return { error => "no_issues_selected" };
395 $strsth .= join( ",", ('?') x @$externalid ) . ")";
396 $action = "SERIAL CLAIM";
397 $sthorders = $dbh->prepare($strsth);
398 $sthorders->execute( @$externalid );
399 $dataorders = $sthorders->fetchall_arrayref( {} );
402 if ( $type eq 'orderacquisition') {
403 my $basketno = $externalid;
405 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
407 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
408 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
409 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
410 WHERE aqbasket.basketno = ?
411 AND orderstatus IN ('new','ordered')
414 unless ( $basketno ) {
415 carp "No basketnumber given";
416 return { error => "no_basketno" };
418 $action = "ACQUISITION ORDER";
419 $sthorders = $dbh->prepare($strsth);
420 $sthorders->execute($basketno);
421 $dataorders = $sthorders->fetchall_arrayref( {} );
425 $dbh->prepare("select * from aqbooksellers where id=?");
426 $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
427 my $databookseller = $sthbookseller->fetchrow_hashref;
429 my $addressee = $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
432 $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
433 $sthcontact->execute( $dataorders->[0]->{booksellerid} );
434 my $datacontact = $sthcontact->fetchrow_hashref;
438 push @email, $datacontact->{email} if ( $datacontact && $datacontact->{email} );
440 warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
441 return { error => "no_email" };
444 while ($addlcontact = $sthcontact->fetchrow_hashref) {
445 push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
448 my $userenv = C4::Context->userenv;
449 my $letter = GetPreparedLetter (
451 letter_code => $letter_code,
452 branchcode => $userenv->{branch},
454 'branches' => $userenv->{branch},
455 'aqbooksellers' => $databookseller,
456 'aqcontacts' => $datacontact,
457 'aqbasket' => $basketno,
459 repeat => $dataorders,
461 ) or return { error => "no_letter" };
463 # Remove the order tag
464 $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
467 my $library = Koha::Libraries->find( $userenv->{branch} );
468 my $mail = Koha::Email->create(
470 to => join( ',', @email ),
471 cc => join( ',', @cc ),
474 C4::Context->preference("ClaimsBccCopy")
475 && ( $type eq 'claimacquisition'
476 || $type eq 'claimissues' )
478 ? ( bcc => $userenv->{emailaddress} )
481 from => $library->branchemail
482 || C4::Context->preference('KohaAdminEmailAddress'),
483 subject => "" . $letter->{title},
487 if ( $letter->{is_html} ) {
488 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
491 $mail->text_body( "" . $letter->{content} );
495 $mail->send_or_die({ transport => $library->smtp_server->transport });
498 # We expect ref($_) eq 'Email::Sender::Failure'
499 $error = $_->message;
505 return { error => $error }
508 my $module = $action eq 'ACQUISITION ORDER' ? 'ACQUISITIONS' : 'CLAIMS';
514 . join( ',', @email )
519 ) if C4::Context->preference("ClaimsLog");
521 # send an "account details" notice to a newly created user
522 elsif ( $type eq 'members' ) {
523 my $library = Koha::Libraries->find( $externalid->{branchcode} );
524 my $letter = GetPreparedLetter (
526 letter_code => $letter_code,
527 branchcode => $externalid->{'branchcode'},
528 lang => $externalid->{lang} || 'default',
530 'branches' => $library->unblessed,
531 'borrowers' => $externalid->{'borrowernumber'},
533 substitute => { 'borrowers.password' => $externalid->{'password'} },
536 return { error => "no_email" } unless $externalid->{'emailaddr'};
540 # FIXME: This 'default' behaviour should be moved to Koha::Email
541 my $mail = Koha::Email->create(
543 to => $externalid->{'emailaddr'},
544 from => $library->branchemail,
545 reply_to => $library->branchreplyto,
546 sender => $library->branchreturnpath,
547 subject => "" . $letter->{'title'},
551 if ( $letter->{is_html} ) {
552 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
555 $mail->text_body( $letter->{content} );
558 $mail->send_or_die({ transport => $library->smtp_server->transport });
561 # We expect ref($_) eq 'Email::Sender::Failure'
562 $error = $_->message;
568 return { error => $error }
572 # If we come here, return an OK status
576 =head2 GetPreparedLetter( %params )
579 module => letter module, mandatory
580 letter_code => letter code, mandatory
581 branchcode => for letter selection, if missing default system letter taken
582 tables => a hashref with table names as keys. Values are either:
583 - a scalar - primary key value
584 - an arrayref - primary key values
585 - a hashref - full record
586 substitute => custom substitution key/value pairs
587 repeat => records to be substituted on consecutive lines:
588 - an arrayref - tries to guess what needs substituting by
589 taking remaining << >> tokensr; not recommended
590 - a hashref token => @tables - replaces <token> << >> << >> </token>
591 subtemplate for each @tables row; table is a hashref as above
592 want_librarian => boolean, if set to true triggers librarian details
593 substitution from the userenv
595 letter fields hashref (title & content useful)
599 sub GetPreparedLetter {
602 my $letter = $params{letter};
603 my $lang = $params{lang} || 'default';
606 my $module = $params{module} or croak "No module";
607 my $letter_code = $params{letter_code} or croak "No letter_code";
608 my $branchcode = $params{branchcode} || '';
609 my $mtt = $params{message_transport_type} || 'email';
611 my $template = Koha::Notice::Templates->find_effective_template(
614 code => $letter_code,
615 branchcode => $branchcode,
616 message_transport_type => $mtt,
621 unless ( $template ) {
622 warn( "No $module $letter_code letter transported by " . $mtt );
626 $letter = $template->unblessed;
627 $letter->{'content-type'} = 'text/html; charset="UTF-8"' if $letter->{is_html};
630 my $tables = $params{tables} || {};
631 my $substitute = $params{substitute} || {};
632 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
633 my $repeat = $params{repeat};
634 %$tables || %$substitute || $repeat || %$loops
635 or carp( "ERROR: nothing to substitute - both 'tables', 'loops' and 'substitute' are empty" ),
637 my $want_librarian = $params{want_librarian};
640 while ( my ($token, $val) = each %$substitute ) {
641 if ( $token eq 'items.content' ) {
642 $val =~ s|\n|<br/>|g if $letter->{is_html};
645 $letter->{title} =~ s/<<$token>>/$val/g;
646 $letter->{content} =~ s/<<$token>>/$val/g;
650 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
651 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
653 if ($want_librarian) {
654 # parsing librarian name
655 my $userenv = C4::Context->userenv;
656 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
657 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
658 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
661 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
664 if (ref ($repeat) eq 'ARRAY' ) {
665 $repeat_no_enclosing_tags = $repeat;
667 $repeat_enclosing_tags = $repeat;
671 if ($repeat_enclosing_tags) {
672 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
673 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
676 my %subletter = ( title => '', content => $subcontent );
677 _substitute_tables( \%subletter, $_ );
680 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
686 _substitute_tables( $letter, $tables );
689 if ($repeat_no_enclosing_tags) {
690 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
695 $c =~ s/<<count>>/$i/go;
696 foreach my $field ( keys %{$_} ) {
697 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
701 } @$repeat_no_enclosing_tags;
703 my $replaceby = join( "\n", @lines );
704 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
708 $letter->{content} = _process_tt(
710 content => $letter->{content},
713 substitute => $substitute,
718 $letter->{title} = _process_tt(
720 content => $letter->{title},
723 substitute => $substitute,
727 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
732 sub _substitute_tables {
733 my ( $letter, $tables ) = @_;
734 while ( my ($table, $param) = each %$tables ) {
737 my $ref = ref $param;
740 if ($ref && $ref eq 'HASH') {
744 my $sth = _parseletter_sth($table);
746 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
749 $sth->execute( $ref ? @$param : $param );
751 $values = $sth->fetchrow_hashref;
755 _parseletter ( $letter, $table, $values );
759 sub _parseletter_sth {
763 carp "ERROR: _parseletter_sth() called without argument (table)";
766 # NOTE: we used to check whether we had a statement handle cached in
767 # a %handles module-level variable. This was a dumb move and
768 # broke things for the rest of us. prepare_cached is a better
769 # way to cache statement handles anyway.
771 ($table eq 'accountlines' ) ? "SELECT * FROM $table WHERE accountlines_id = ?" :
772 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
773 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
774 ($table eq 'credits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
775 ($table eq 'debits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
776 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
777 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
778 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE itemnumber = ? ORDER BY timestamp DESC LIMIT 1" :
779 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
780 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
781 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
782 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
783 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
784 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
785 ($table eq 'aqbasket' ) ? "SELECT * FROM $table WHERE basketno = ?" :
786 ($table eq 'illrequests' ) ? "SELECT * FROM $table WHERE illrequest_id = ?" :
787 ($table eq 'opac_news' ) ? "SELECT * FROM $table WHERE idnew = ?" :
788 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
789 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
790 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
791 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
792 ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" :
795 warn "ERROR: No _parseletter_sth query for table '$table'";
796 return; # nothing to get
798 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
799 warn "ERROR: Failed to prepare query: '$query'";
802 return $sth; # now cache is populated for that $table
805 =head2 _parseletter($letter, $table, $values)
808 - $letter : a hash to letter fields (title & content useful)
809 - $table : the Koha table to parse.
810 - $values_in : table record hashref
811 parse all fields from a table, and replace values in title & content with the appropriate value
812 (not exported sub, used only internally)
817 my ( $letter, $table, $values_in ) = @_;
819 # Work on a local copy of $values_in (passed by reference) to avoid side effects
820 # in callers ( by changing / formatting values )
821 my $values = $values_in ? { %$values_in } : {};
823 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
824 $values->{'dateexpiry'} = output_pref({ dt => dt_from_string( $values->{'dateexpiry'} ), dateonly => 1 });
827 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
828 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
831 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
832 my $todaysdate = output_pref( dt_from_string() );
833 $letter->{content} =~ s/<<today>>/$todaysdate/go;
836 while ( my ($field, $val) = each %$values ) {
837 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
838 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
839 #Therefore adding the test on biblio. This includes biblioitems,
840 #but excludes items. Removed unneeded global and lookahead.
842 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
843 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
844 $val = $av->count ? $av->next->lib : '';
848 my $replacedby = defined ($val) ? $val : '';
850 and not $replacedby =~ m|9999-12-31|
851 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
853 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
854 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
855 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
857 for my $letter_field ( qw( title content ) ) {
858 my $filter_string_used = q{};
859 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
860 # We overwrite $dateonly if the filter exists and we have a time in the datetime
861 $filter_string_used = $1 || q{};
862 $dateonly = $1 unless $dateonly;
864 my $replacedby_date = eval {
865 output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
868 if ( $letter->{ $letter_field } ) {
869 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
870 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
874 # Other fields replacement
876 for my $letter_field ( qw( title content ) ) {
877 if ( $letter->{ $letter_field } ) {
878 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
879 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
885 if ($table eq 'borrowers' && $letter->{content}) {
886 my $patron = Koha::Patrons->find( $values->{borrowernumber} );
888 my $attributes = $patron->extended_attributes;
890 while ( my $attribute = $attributes->next ) {
891 my $code = $attribute->code;
892 my $val = $attribute->description; # FIXME - we always display intranet description here!
893 $val =~ s/\p{P}(?=$)//g if $val;
894 next unless $val gt '';
896 push @{ $attr{$code} }, $val;
898 while ( my ($code, $val_ar) = each %attr ) {
899 my $replacefield = "<<borrower-attribute:$code>>";
900 my $replacedby = join ',', @$val_ar;
901 $letter->{content} =~ s/$replacefield/$replacedby/g;
910 my $success = EnqueueLetter( { letter => $letter,
911 borrowernumber => '12', message_transport_type => 'email' } )
913 places a letter in the message_queue database table, which will
914 eventually get processed (sent) by the process_message_queue.pl
915 cronjob when it calls SendQueuedMessages.
917 return message_id on success
922 my $params = shift or return;
924 return unless exists $params->{'letter'};
925 # return unless exists $params->{'borrowernumber'};
926 return unless exists $params->{'message_transport_type'};
928 my $content = $params->{letter}->{content};
929 $content =~ s/\s+//g if(defined $content);
930 if ( not defined $content or $content eq '' ) {
931 warn "Trying to add an empty message to the message queue" if $debug;
935 # If we have any attachments we should encode then into the body.
936 if ( $params->{'attachments'} ) {
937 $params->{'letter'} = _add_attachments(
938 { letter => $params->{'letter'},
939 attachments => $params->{'attachments'},
940 message => MIME::Lite->new( Type => 'multipart/mixed' ),
945 my $dbh = C4::Context->dbh();
946 my $statement = << 'ENDSQL';
947 INSERT INTO message_queue
948 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, reply_address, content_type, delivery_note )
950 ( ?, ?, ?, ?, ?, ?, ?, CAST(NOW() AS DATETIME), ?, ?, ?, ?, ? )
953 my $sth = $dbh->prepare($statement);
954 my $result = $sth->execute(
955 $params->{'borrowernumber'}, # borrowernumber
956 $params->{'letter'}->{'title'}, # subject
957 $params->{'letter'}->{'content'}, # content
958 $params->{'letter'}->{'metadata'} || '', # metadata
959 $params->{'letter'}->{'code'} || '', # letter_code
960 $params->{'message_transport_type'}, # message_transport_type
962 $params->{'to_address'}, # to_address
963 $params->{'from_address'}, # from_address
964 $params->{'reply_address'}, # reply_address
965 $params->{'letter'}->{'content-type'}, # content_type
966 $params->{'delivery_note'} || '', # delivery_note
968 return $dbh->last_insert_id(undef,undef,'message_queue', undef);
971 =head2 SendQueuedMessages ([$hashref])
973 my $sent = SendQueuedMessages({
974 letter_code => $letter_code,
975 borrowernumber => $who_letter_is_for,
981 Sends all of the 'pending' items in the message queue, unless
982 parameters are passed.
984 The letter_code, borrowernumber and limit parameters are used
985 to build a parameter set for _get_unsent_messages, thus limiting
986 which pending messages will be processed. They are all optional.
988 The verbose parameter can be used to generate debugging output.
991 Returns number of messages sent.
995 sub SendQueuedMessages {
998 my $which_unsent_messages = {
999 'message_id' => $params->{'message_id'},
1000 'limit' => $params->{'limit'} // 0,
1001 'borrowernumber' => $params->{'borrowernumber'} // q{},
1002 'letter_code' => $params->{'letter_code'} // q{},
1003 'type' => $params->{'type'} // q{},
1005 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
1006 MESSAGE: foreach my $message ( @$unsent_messages ) {
1007 my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
1008 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
1009 $message_object->make_column_dirty('status');
1010 return unless $message_object->store;
1012 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
1013 warn sprintf( 'sending %s message to patron: %s',
1014 $message->{'message_transport_type'},
1015 $message->{'borrowernumber'} || 'Admin' )
1016 if $params->{'verbose'} or $debug;
1017 # This is just begging for subclassing
1018 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
1019 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
1020 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1022 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
1023 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
1024 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1025 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1026 unless ( $sms_provider ) {
1027 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1028 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1031 unless ( $patron->smsalertnumber ) {
1032 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1033 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1036 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1037 $message->{to_address} .= '@' . $sms_provider->domain();
1039 # Check for possible from_address override
1040 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1041 if ($from_address && $message->{from_address} ne $from_address) {
1042 $message->{from_address} = $from_address;
1043 _update_message_from_address($message->{'message_id'}, $message->{from_address});
1046 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1047 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1049 _send_message_by_sms( $message );
1053 return scalar( @$unsent_messages );
1056 =head2 GetRSSMessages
1058 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1060 returns a listref of all queued RSS messages for a particular person.
1064 sub GetRSSMessages {
1067 return unless $params;
1068 return unless ref $params;
1069 return unless $params->{'borrowernumber'};
1071 return _get_unsent_messages( { message_transport_type => 'rss',
1072 limit => $params->{'limit'},
1073 borrowernumber => $params->{'borrowernumber'}, } );
1076 =head2 GetPrintMessages
1078 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1080 Returns a arrayref of all queued print messages (optionally, for a particular
1085 sub GetPrintMessages {
1086 my $params = shift || {};
1088 return _get_unsent_messages( { message_transport_type => 'print',
1089 borrowernumber => $params->{'borrowernumber'},
1093 =head2 GetQueuedMessages ([$hashref])
1095 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1097 Fetches a list of messages from the message queue optionally filtered by borrowernumber
1098 and limited to specified limit.
1100 Return is an arrayref of hashes, each has represents a message in the message queue.
1104 sub GetQueuedMessages {
1107 my $dbh = C4::Context->dbh();
1108 my $statement = << 'ENDSQL';
1109 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued, updated_on, delivery_note
1115 if ( exists $params->{'borrowernumber'} ) {
1116 push @whereclauses, ' borrowernumber = ? ';
1117 push @query_params, $params->{'borrowernumber'};
1120 if ( @whereclauses ) {
1121 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1124 if ( defined $params->{'limit'} ) {
1125 $statement .= ' LIMIT ? ';
1126 push @query_params, $params->{'limit'};
1129 my $sth = $dbh->prepare( $statement );
1130 my $result = $sth->execute( @query_params );
1131 return $sth->fetchall_arrayref({});
1134 =head2 GetMessageTransportTypes
1136 my @mtt = GetMessageTransportTypes();
1138 returns an arrayref of transport types
1142 sub GetMessageTransportTypes {
1143 my $dbh = C4::Context->dbh();
1144 my $mtts = $dbh->selectcol_arrayref("
1145 SELECT message_transport_type
1146 FROM message_transport_types
1147 ORDER BY message_transport_type
1154 my $message = C4::Letters::Message($message_id);
1159 my ( $message_id ) = @_;
1160 return unless $message_id;
1161 my $dbh = C4::Context->dbh;
1162 return $dbh->selectrow_hashref(q|
1163 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, delivery_note
1165 WHERE message_id = ?
1166 |, {}, $message_id );
1169 =head2 ResendMessage
1171 Attempt to resend a message which has failed previously.
1173 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1175 Updates the message to 'pending' status so that
1176 it will be resent later on.
1178 returns 1 on success, 0 on failure, undef if no message was found
1183 my $message_id = shift;
1184 return unless $message_id;
1186 my $message = GetMessage( $message_id );
1187 return unless $message;
1189 if ( $message->{status} ne 'pending' ) {
1190 $rv = C4::Letters::_set_message_status({
1191 message_id => $message_id,
1192 status => 'pending',
1194 $rv = $rv > 0? 1: 0;
1195 # Clear destination email address to force address update
1196 _update_message_to_address( $message_id, undef ) if $rv &&
1197 $message->{message_transport_type} eq 'email';
1202 =head2 _add_attachements
1205 letter - the standard letter hashref
1206 attachments - listref of attachments. each attachment is a hashref of:
1207 type - the mime type, like 'text/plain'
1208 content - the actual attachment
1209 filename - the name of the attachment.
1210 message - a MIME::Lite object to attach these to.
1212 returns your letter object, with the content updated.
1216 sub _add_attachments {
1219 my $letter = $params->{'letter'};
1220 my $attachments = $params->{'attachments'};
1221 return $letter unless @$attachments;
1222 my $message = $params->{'message'};
1224 # First, we have to put the body in as the first attachment
1226 Type => $letter->{'content-type'} || 'TEXT',
1227 Data => $letter->{'is_html'}
1228 ? _wrap_html($letter->{'content'}, $letter->{'title'})
1229 : $letter->{'content'},
1232 foreach my $attachment ( @$attachments ) {
1234 Type => $attachment->{'type'},
1235 Data => $attachment->{'content'},
1236 Filename => $attachment->{'filename'},
1239 # we're forcing list context here to get the header, not the count back from grep.
1240 ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1241 $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1242 $letter->{'content'} = $message->body_as_string;
1248 =head2 _get_unsent_messages
1250 This function's parameter hash reference takes the following
1251 optional named parameters:
1252 message_transport_type: method of message sending (e.g. email, sms, etc.)
1253 borrowernumber : who the message is to be sent
1254 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1255 message_id : the message_id of the message. In that case the sub will return only 1 result
1256 limit : maximum number of messages to send
1258 This function returns an array of matching hash referenced rows from
1259 message_queue with some borrower information added.
1263 sub _get_unsent_messages {
1266 my $dbh = C4::Context->dbh();
1268 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.delivery_note
1269 FROM message_queue mq
1270 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1274 my @query_params = ('pending');
1275 if ( ref $params ) {
1276 if ( $params->{'message_transport_type'} ) {
1277 $statement .= ' AND mq.message_transport_type = ? ';
1278 push @query_params, $params->{'message_transport_type'};
1280 if ( $params->{'borrowernumber'} ) {
1281 $statement .= ' AND mq.borrowernumber = ? ';
1282 push @query_params, $params->{'borrowernumber'};
1284 if ( $params->{'letter_code'} ) {
1285 $statement .= ' AND mq.letter_code = ? ';
1286 push @query_params, $params->{'letter_code'};
1288 if ( $params->{'type'} ) {
1289 $statement .= ' AND message_transport_type = ? ';
1290 push @query_params, $params->{'type'};
1292 if ( $params->{message_id} ) {
1293 $statement .= ' AND message_id = ?';
1294 push @query_params, $params->{message_id};
1296 if ( $params->{'limit'} ) {
1297 $statement .= ' limit ? ';
1298 push @query_params, $params->{'limit'};
1302 $debug and warn "_get_unsent_messages SQL: $statement";
1303 $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1304 my $sth = $dbh->prepare( $statement );
1305 my $result = $sth->execute( @query_params );
1306 return $sth->fetchall_arrayref({});
1309 sub _send_message_by_email {
1310 my $message = shift or return;
1311 my ($username, $password, $method) = @_;
1313 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1314 my $to_address = $message->{'to_address'};
1315 unless ($to_address) {
1317 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1318 _set_message_status( { message_id => $message->{'message_id'},
1320 delivery_note => 'Invalid borrowernumber '.$message->{borrowernumber},
1321 error_code => 'INVALID_BORNUMBER' } );
1324 $to_address = $patron->notice_email_address;
1325 unless ($to_address) {
1326 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1327 # warning too verbose for this more common case?
1328 _set_message_status( { message_id => $message->{'message_id'},
1330 delivery_note => 'Unable to find an email address for this borrower',
1331 error_code => 'NO_EMAIL' } );
1336 my $subject = $message->{'subject'};
1338 my $content = $message->{'content'};
1339 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1340 my $is_html = $content_type =~ m/html/io;
1342 my $branch_email = undef;
1343 my $branch_replyto = undef;
1344 my $branch_returnpath = undef;
1348 $library = $patron->library;
1349 $branch_email = $library->branchemail;
1350 $branch_replyto = $library->branchreplyto;
1351 $branch_returnpath = $library->branchreturnpath;
1354 my $email = Koha::Email->create(
1358 C4::Context->preference('NoticeBcc')
1359 ? ( bcc => C4::Context->preference('NoticeBcc') )
1362 from => $message->{'from_address'} || $branch_email,
1363 reply_to => $message->{'reply_address'} || $branch_replyto,
1364 sender => $branch_returnpath,
1365 subject => "" . $message->{subject}
1371 _wrap_html( $content, $subject )
1375 $email->text_body( $content );
1380 $smtp_server = $library->smtp_server;
1383 $smtp_server = Koha::SMTP::Servers->get_default;
1389 sasl_username => $username,
1390 sasl_password => $password,
1395 # if initial message address was empty, coming here means that a to address was found and
1396 # queue should be updated; same if to address was overriden by Koha::Email->create
1397 _update_message_to_address( $message->{'message_id'}, $email->email->header('To') )
1398 if !$message->{to_address}
1399 || $message->{to_address} ne $email->email->header('To');
1402 $email->send_or_die({ transport => $smtp_server->transport });
1404 _set_message_status(
1406 message_id => $message->{'message_id'},
1414 _set_message_status(
1416 message_id => $message->{'message_id'},
1418 delivery_note => $Mail::Sendmail::error
1427 my ($content, $title) = @_;
1429 my $css = C4::Context->preference("NoticeCSS") || '';
1430 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1432 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1433 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1434 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1436 <title>$title</title>
1437 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1448 my ( $message ) = @_;
1449 my $dbh = C4::Context->dbh;
1450 my $count = $dbh->selectrow_array(q|
1453 WHERE message_transport_type = ?
1454 AND borrowernumber = ?
1456 AND CAST(updated_on AS date) = CAST(NOW() AS date)
1459 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1463 sub _send_message_by_sms {
1464 my $message = shift or return;
1465 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1467 unless ( $patron and $patron->smsalertnumber ) {
1468 _set_message_status( { message_id => $message->{'message_id'},
1470 delivery_note => 'Missing SMS number',
1471 error_code => 'MISSING_SMS' } );
1475 if ( _is_duplicate( $message ) ) {
1476 _set_message_status( { message_id => $message->{'message_id'},
1478 delivery_note => 'Message is duplicate',
1479 error_code => 'DUPLICATE_MESSAGE' } );
1483 my $success = C4::SMS->send_sms( { destination => $patron->smsalertnumber,
1484 message => $message->{'content'},
1486 _set_message_status( { message_id => $message->{'message_id'},
1487 status => ($success ? 'sent' : 'failed'),
1488 delivery_note => ($success ? '' : 'No notes from SMS driver'),
1489 error_code => 'NO_NOTES' } );
1494 sub _update_message_to_address {
1496 my $dbh = C4::Context->dbh();
1497 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1500 sub _update_message_from_address {
1501 my ($message_id, $from_address) = @_;
1502 my $dbh = C4::Context->dbh();
1503 $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1506 sub _set_message_status {
1507 my $params = shift or return;
1509 foreach my $required_parameter ( qw( message_id status ) ) {
1510 return unless exists $params->{ $required_parameter };
1513 my $dbh = C4::Context->dbh();
1514 my $statement = 'UPDATE message_queue SET status= ?, delivery_note= ? WHERE message_id = ?';
1515 my $sth = $dbh->prepare( $statement );
1516 my $result = $sth->execute( $params->{'status'},
1517 $params->{'delivery_note'} || '',
1518 $params->{'message_id'} );
1523 my ( $params ) = @_;
1525 my $content = $params->{content};
1526 my $tables = $params->{tables};
1527 my $loops = $params->{loops};
1528 my $substitute = $params->{substitute} || {};
1529 my $lang = defined($params->{lang}) && $params->{lang} ne 'default' ? $params->{lang} : 'en';
1530 my ($theme, $availablethemes);
1532 my $htdocs = C4::Context->config('intrahtdocs');
1533 ($theme, $lang, $availablethemes)= C4::Templates::availablethemes( $htdocs, 'about.tt', 'intranet', $lang);
1535 foreach (@$availablethemes) {
1536 push @includes, "$htdocs/$_/$lang/includes";
1537 push @includes, "$htdocs/$_/en/includes" unless $lang eq 'en';
1540 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1541 my $template = Template->new(
1545 PLUGIN_BASE => 'Koha::Template::Plugin',
1546 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1547 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1548 INCLUDE_PATH => \@includes,
1550 ENCODING => 'UTF-8',
1552 ) or die Template->error();
1554 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
1556 $content = add_tt_filters( $content );
1557 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1560 $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1565 sub _get_tt_params {
1566 my ($tables, $is_a_loop) = @_;
1572 article_requests => {
1573 module => 'Koha::ArticleRequests',
1574 singular => 'article_request',
1575 plural => 'article_requests',
1579 module => 'Koha::Acquisition::Baskets',
1580 singular => 'basket',
1581 plural => 'baskets',
1585 module => 'Koha::Biblios',
1586 singular => 'biblio',
1587 plural => 'biblios',
1588 pk => 'biblionumber',
1591 module => 'Koha::Biblioitems',
1592 singular => 'biblioitem',
1593 plural => 'biblioitems',
1594 pk => 'biblioitemnumber',
1597 module => 'Koha::Patrons',
1598 singular => 'borrower',
1599 plural => 'borrowers',
1600 pk => 'borrowernumber',
1603 module => 'Koha::Libraries',
1604 singular => 'branch',
1605 plural => 'branches',
1609 module => 'Koha::Account::Lines',
1610 singular => 'credit',
1611 plural => 'credits',
1612 pk => 'accountlines_id',
1615 module => 'Koha::Account::Lines',
1616 singular => 'debit',
1618 pk => 'accountlines_id',
1621 module => 'Koha::Items',
1627 module => 'Koha::News',
1633 module => 'Koha::Acquisition::Orders',
1634 singular => 'order',
1636 pk => 'ordernumber',
1639 module => 'Koha::Holds',
1645 module => 'Koha::Serials',
1646 singular => 'serial',
1647 plural => 'serials',
1651 module => 'Koha::Subscriptions',
1652 singular => 'subscription',
1653 plural => 'subscriptions',
1654 pk => 'subscriptionid',
1657 module => 'Koha::Suggestions',
1658 singular => 'suggestion',
1659 plural => 'suggestions',
1660 pk => 'suggestionid',
1663 module => 'Koha::Checkouts',
1664 singular => 'checkout',
1665 plural => 'checkouts',
1669 module => 'Koha::Old::Checkouts',
1670 singular => 'old_checkout',
1671 plural => 'old_checkouts',
1675 module => 'Koha::Checkouts',
1676 singular => 'overdue',
1677 plural => 'overdues',
1680 borrower_modifications => {
1681 module => 'Koha::Patron::Modifications',
1682 singular => 'patron_modification',
1683 plural => 'patron_modifications',
1684 fk => 'verification_token',
1687 module => 'Koha::Illrequests',
1688 singular => 'illrequest',
1689 plural => 'illrequests',
1690 pk => 'illrequest_id'
1694 foreach my $table ( keys %$tables ) {
1695 next unless $config->{$table};
1697 my $ref = ref( $tables->{$table} ) || q{};
1698 my $module = $config->{$table}->{module};
1700 if ( can_load( modules => { $module => undef } ) ) {
1701 my $pk = $config->{$table}->{pk};
1702 my $fk = $config->{$table}->{fk};
1705 my $values = $tables->{$table} || [];
1706 unless ( ref( $values ) eq 'ARRAY' ) {
1707 croak "ERROR processing table $table. Wrong API call.";
1709 my $key = $pk ? $pk : $fk;
1710 # $key does not come from user input
1711 my $objects = $module->search(
1712 { $key => $values },
1714 # We want to retrieve the data in the same order
1716 # field is a MySQLism, but they are no other way to do it
1717 # To be generic we could do it in perl, but we will need to fetch
1718 # all the data then order them
1719 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1722 $params->{ $config->{$table}->{plural} } = $objects;
1724 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1725 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1727 if ( $fk ) { # Using a foreign key for lookup
1728 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1730 foreach my $key ( @$fk ) {
1731 $search->{$key} = $id->{$key};
1733 $object = $module->search( $search )->last();
1734 } else { # Foreign key is single column
1735 $object = $module->search( { $fk => $id } )->last();
1737 } else { # using the table's primary key for lookup
1738 $object = $module->find($id);
1740 $params->{ $config->{$table}->{singular} } = $object;
1742 else { # $ref eq 'ARRAY'
1744 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1745 $object = $module->search( { $pk => $tables->{$table} } )->last();
1747 else { # Params are mutliple foreign keys
1748 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1750 $params->{ $config->{$table}->{singular} } = $object;
1754 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1758 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1763 =head3 add_tt_filters
1765 $content = add_tt_filters( $content );
1767 Add TT filters to some specific fields if needed.
1769 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1773 sub add_tt_filters {
1774 my ( $content ) = @_;
1775 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1776 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1780 =head2 get_item_content
1782 my $item = Koha::Items->find(...)->unblessed;
1783 my @item_content_fields = qw( date_due title barcode author itemnumber );
1784 my $item_content = C4::Letters::get_item_content({
1786 item_content_fields => \@item_content_fields
1789 This function generates a tab-separated list of values for the passed item. Dates
1790 are formatted following the current setup.
1794 sub get_item_content {
1795 my ( $params ) = @_;
1796 my $item = $params->{item};
1797 my $dateonly = $params->{dateonly} || 0;
1798 my $item_content_fields = $params->{item_content_fields} || [];
1800 return unless $item;
1802 my @item_info = map {
1806 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1810 } @$item_content_fields;
1811 return join( "\t", @item_info ) . "\n";