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 );
29 use C4::Log qw( logaction );
32 use Koha::SMS::Providers;
35 use Koha::Notice::Messages;
36 use Koha::Notice::Templates;
37 use Koha::DateUtils qw( dt_from_string output_pref );
38 use Koha::Auth::TwoFactorAuth;
40 use Koha::SMTP::Servers;
41 use Koha::Subscriptions;
43 use constant SERIALIZED_EMAIL_CONTENT_TYPE => 'message/rfc822';
45 our (@ISA, @EXPORT_OK);
51 GetLettersAvailableForALibrary
60 GetMessageTransportTypes
70 C4::Letters - Give functions for Letters management
78 "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
79 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)
81 Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
83 =head2 GetLetters([$module])
85 $letters = &GetLetters($module);
86 returns informations about letters.
87 if needed, $module filters for letters given module
89 DEPRECATED - You must use Koha::Notice::Templates instead
90 The group by clause is confusing and can lead to issues
96 my $module = $filters->{module};
97 my $code = $filters->{code};
98 my $branchcode = $filters->{branchcode};
99 my $dbh = C4::Context->dbh;
100 my $letters = $dbh->selectall_arrayref(
102 SELECT code, module, name
106 . ( $module ? q| AND module = ?| : q|| )
107 . ( $code ? q| AND code = ?| : q|| )
108 . ( defined $branchcode ? q| AND branchcode = ?| : q|| )
109 . q| GROUP BY code, module, name ORDER BY name|, { Slice => {} }
110 , ( $module ? $module : () )
111 , ( $code ? $code : () )
112 , ( defined $branchcode ? $branchcode : () )
118 =head2 GetLetterTemplates
120 my $letter_templates = GetLetterTemplates(
122 module => 'circulation',
124 branchcode => 'CPL', # '' for default,
128 Return a hashref of letter templates.
132 sub GetLetterTemplates {
135 my $module = $params->{module};
136 my $code = $params->{code};
137 my $branchcode = $params->{branchcode} // '';
138 my $dbh = C4::Context->dbh;
139 return Koha::Notice::Templates->search(
143 branchcode => $branchcode,
145 C4::Context->preference('TranslateNotices')
147 : ( lang => 'default' )
153 =head2 GetLettersAvailableForALibrary
155 my $letters = GetLettersAvailableForALibrary(
157 branchcode => 'CPL', # '' for default
158 module => 'circulation',
162 Return an arrayref of letters, sorted by name.
163 If a specific letter exist for the given branchcode, it will be retrieve.
164 Otherwise the default letter will be.
168 sub GetLettersAvailableForALibrary {
170 my $branchcode = $filters->{branchcode};
171 my $module = $filters->{module};
173 croak "module should be provided" unless $module;
175 my $dbh = C4::Context->dbh;
176 my $default_letters = $dbh->selectall_arrayref(
178 SELECT module, code, branchcode, name
182 . q| AND branchcode = ''|
183 . ( $module ? q| AND module = ?| : q|| )
184 . q| ORDER BY name|, { Slice => {} }
185 , ( $module ? $module : () )
188 my $specific_letters;
190 $specific_letters = $dbh->selectall_arrayref(
192 SELECT module, code, branchcode, name
196 . q| AND branchcode = ?|
197 . ( $module ? q| AND module = ?| : q|| )
198 . q| ORDER BY name|, { Slice => {} }
200 , ( $module ? $module : () )
205 for my $l (@$default_letters) {
206 $letters{ $l->{code} } = $l;
208 for my $l (@$specific_letters) {
209 # Overwrite the default letter with the specific one.
210 $letters{ $l->{code} } = $l;
213 return [ map { $letters{$_} }
214 sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
224 module => 'circulation',
230 Delete the letter. The mtt parameter is facultative.
231 If not given, all templates mathing the other parameters will be removed.
237 my $branchcode = $params->{branchcode};
238 my $module = $params->{module};
239 my $code = $params->{code};
240 my $mtt = $params->{mtt};
241 my $lang = $params->{lang};
242 my $dbh = C4::Context->dbh;
249 . ( $mtt ? q| AND message_transport_type = ?| : q|| )
250 . ( $lang? q| AND lang = ?| : q|| )
251 , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
256 my $err = &SendAlerts($type, $externalid, $letter_code);
259 - $type : the type of alert
260 - $externalid : the id of the "object" to query
261 - $letter_code : the notice template to use
263 C<&SendAlerts> sends an email notice directly to a patron or a vendor.
265 Currently it supports ($type):
266 - claim serial issues (claimissues)
267 - claim acquisition orders (claimacquisition)
268 - send acquisition orders to the vendor (orderacquisition)
269 - notify patrons about newly received serial issues (issue)
270 - notify patrons when their account is created (members)
272 Returns undef or { error => 'message } on failure.
273 Returns true on success.
278 my ( $type, $externalid, $letter_code ) = @_;
279 my $dbh = C4::Context->dbh;
282 if ( $type eq 'issue' ) {
284 # prepare the letter...
285 # search the subscriptionid
288 "SELECT subscriptionid FROM serial WHERE serialid=?");
289 $sth->execute($externalid);
290 my ($subscriptionid) = $sth->fetchrow
291 or warn( "No subscription for '$externalid'" ),
294 # search the biblionumber
297 "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
298 $sth->execute($subscriptionid);
299 my ($biblionumber) = $sth->fetchrow
300 or warn( "No biblionumber for '$subscriptionid'" ),
303 # find the list of subscribers to notify
304 my $subscription = Koha::Subscriptions->find( $subscriptionid );
305 my $subscribers = $subscription->subscribers;
306 while ( my $patron = $subscribers->next ) {
307 my $email = $patron->email or next;
309 # warn "sending issues...";
310 my $userenv = C4::Context->userenv;
311 my $library = $patron->library;
312 my $letter = GetPreparedLetter (
314 letter_code => $letter_code,
315 branchcode => $userenv->{branch},
317 'branches' => $library->branchcode,
318 'biblio' => $biblionumber,
319 'biblioitems' => $biblionumber,
320 'borrowers' => $patron->unblessed,
321 'subscription' => $subscriptionid,
322 'serial' => $externalid,
327 # FIXME: This 'default' behaviour should be moved to Koha::Email
328 my $mail = Koha::Email->create(
331 from => $library->branchemail,
332 reply_to => $library->branchreplyto,
333 sender => $library->branchreturnpath,
334 subject => "" . $letter->{title},
338 if ( $letter->{is_html} ) {
339 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
342 $mail->text_body( $letter->{content} );
346 $mail->send_or_die({ transport => $library->smtp_server->transport });
349 # We expect ref($_) eq 'Email::Sender::Failure'
350 $error = $_->message;
356 return { error => $error }
360 elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
362 # prepare the letter...
368 if ( $type eq 'claimacquisition') {
370 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
372 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
373 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
374 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
375 WHERE aqorders.ordernumber IN (
379 carp "No order selected";
380 return { error => "no_order_selected" };
382 $strsth .= join( ",", ('?') x @$externalid ) . ")";
383 $action = "ACQUISITION CLAIM";
384 $sthorders = $dbh->prepare($strsth);
385 $sthorders->execute( @$externalid );
386 $dataorders = $sthorders->fetchall_arrayref( {} );
389 if ($type eq 'claimissues') {
391 SELECT serial.*,subscription.*, biblio.*, biblioitems.*, aqbooksellers.*,
392 aqbooksellers.id AS booksellerid
394 LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
395 LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
396 LEFT JOIN biblioitems ON serial.biblionumber = biblioitems.biblionumber
397 LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
398 WHERE serial.serialid IN (
402 carp "No issues selected";
403 return { error => "no_issues_selected" };
406 $strsth .= join( ",", ('?') x @$externalid ) . ")";
407 $action = "SERIAL CLAIM";
408 $sthorders = $dbh->prepare($strsth);
409 $sthorders->execute( @$externalid );
410 $dataorders = $sthorders->fetchall_arrayref( {} );
413 if ( $type eq 'orderacquisition') {
414 $basketno = $externalid;
416 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
418 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
419 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
420 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
421 WHERE aqbasket.basketno = ?
422 AND orderstatus IN ('new','ordered')
425 unless ( $basketno ) {
426 carp "No basketnumber given";
427 return { error => "no_basketno" };
429 $action = "ACQUISITION ORDER";
430 $sthorders = $dbh->prepare($strsth);
431 $sthorders->execute($basketno);
432 $dataorders = $sthorders->fetchall_arrayref( {} );
436 $dbh->prepare("select * from aqbooksellers where id=?");
437 $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
438 my $databookseller = $sthbookseller->fetchrow_hashref;
440 my $addressee = $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
443 $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
444 $sthcontact->execute( $dataorders->[0]->{booksellerid} );
445 my $datacontact = $sthcontact->fetchrow_hashref;
449 push @email, $datacontact->{email} if ( $datacontact && $datacontact->{email} );
451 warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
452 return { error => "no_email" };
455 while ($addlcontact = $sthcontact->fetchrow_hashref) {
456 push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
459 my $userenv = C4::Context->userenv;
460 my $letter = GetPreparedLetter (
462 letter_code => $letter_code,
463 branchcode => $userenv->{branch},
465 'branches' => $userenv->{branch},
466 'aqbooksellers' => $databookseller,
467 'aqcontacts' => $datacontact,
468 'aqbasket' => $basketno,
470 repeat => $dataorders,
472 ) or return { error => "no_letter" };
474 # Remove the order tag
475 $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
478 my $library = Koha::Libraries->find( $userenv->{branch} );
479 my $mail = Koha::Email->create(
481 to => join( ',', @email ),
482 cc => join( ',', @cc ),
485 C4::Context->preference("ClaimsBccCopy")
486 && ( $type eq 'claimacquisition'
487 || $type eq 'claimissues' )
489 ? ( bcc => $userenv->{emailaddress} )
492 from => $library->branchemail
493 || C4::Context->preference('KohaAdminEmailAddress'),
494 subject => "" . $letter->{title},
498 if ( $letter->{is_html} ) {
499 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
502 $mail->text_body( "" . $letter->{content} );
506 $mail->send_or_die({ transport => $library->smtp_server->transport });
509 # We expect ref($_) eq 'Email::Sender::Failure'
510 $error = $_->message;
516 return { error => $error }
519 my $log_object = $action eq 'ACQUISITION ORDER' ? $externalid : undef;
520 my $module = $action eq 'ACQUISITION ORDER' ? 'ACQUISITIONS' : 'CLAIMS';
526 . join( ',', @email )
531 ) if C4::Context->preference("ClaimsLog");
534 # If we come here, return an OK status
538 =head2 GetPreparedLetter( %params )
541 module => letter module, mandatory
542 letter_code => letter code, mandatory
543 branchcode => for letter selection, if missing default system letter taken
544 tables => a hashref with table names as keys. Values are either:
545 - a scalar - primary key value
546 - an arrayref - primary key values
547 - a hashref - full record
548 substitute => custom substitution key/value pairs
549 repeat => records to be substituted on consecutive lines:
550 - an arrayref - tries to guess what needs substituting by
551 taking remaining << >> tokensr; not recommended
552 - a hashref token => @tables - replaces <token> << >> << >> </token>
553 subtemplate for each @tables row; table is a hashref as above
554 want_librarian => boolean, if set to true triggers librarian details
555 substitution from the userenv
557 letter fields hashref (title & content useful)
561 sub GetPreparedLetter {
564 my $letter = $params{letter};
565 my $lang = $params{lang} || 'default';
568 my $module = $params{module} or croak "No module";
569 my $letter_code = $params{letter_code} or croak "No letter_code";
570 my $branchcode = $params{branchcode} || '';
571 my $mtt = $params{message_transport_type} || 'email';
573 my $template = Koha::Notice::Templates->find_effective_template(
576 code => $letter_code,
577 branchcode => $branchcode,
578 message_transport_type => $mtt,
583 unless ( $template ) {
584 warn( "No $module $letter_code letter transported by " . $mtt );
588 $letter = $template->unblessed;
589 $letter->{'content-type'} = 'text/html; charset="UTF-8"' if $letter->{is_html};
592 my $objects = $params{objects} || {};
593 my $tables = $params{tables} || {};
594 my $substitute = $params{substitute} || {};
595 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
596 my $repeat = $params{repeat};
597 %$tables || %$substitute || $repeat || %$loops || %$objects
598 or carp( "ERROR: nothing to substitute - all of 'objects', 'tables', 'loops' and 'substitute' are empty" ),
600 my $want_librarian = $params{want_librarian};
603 while ( my ($token, $val) = each %$substitute ) {
605 if ( $token eq 'items.content' ) {
606 $val =~ s|\n|<br/>|g if $letter->{is_html};
609 $letter->{title} =~ s/<<$token>>/$val/g;
610 $letter->{content} =~ s/<<$token>>/$val/g;
614 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
615 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
617 if ($want_librarian) {
618 # parsing librarian name
619 my $userenv = C4::Context->userenv;
620 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
621 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
622 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
625 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
628 if (ref ($repeat) eq 'ARRAY' ) {
629 $repeat_no_enclosing_tags = $repeat;
631 $repeat_enclosing_tags = $repeat;
635 if ($repeat_enclosing_tags) {
636 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
637 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
640 my %subletter = ( title => '', content => $subcontent );
641 _substitute_tables( \%subletter, $_ );
644 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
650 _substitute_tables( $letter, $tables );
653 if ($repeat_no_enclosing_tags) {
654 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
659 $c =~ s/<<count>>/$i/go;
660 foreach my $field ( keys %{$_} ) {
661 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
665 } @$repeat_no_enclosing_tags;
667 my $replaceby = join( "\n", @lines );
668 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
672 $letter->{content} = _process_tt(
674 content => $letter->{content},
678 substitute => $substitute,
683 $letter->{title} = _process_tt(
685 content => $letter->{title},
689 substitute => $substitute,
694 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
699 sub _substitute_tables {
700 my ( $letter, $tables ) = @_;
701 while ( my ($table, $param) = each %$tables ) {
704 my $ref = ref $param;
707 if ($ref && $ref eq 'HASH') {
711 my $sth = _parseletter_sth($table);
713 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
716 $sth->execute( $ref ? @$param : $param );
718 $values = $sth->fetchrow_hashref;
722 _parseletter ( $letter, $table, $values );
726 sub _parseletter_sth {
730 carp "ERROR: _parseletter_sth() called without argument (table)";
733 # NOTE: we used to check whether we had a statement handle cached in
734 # a %handles module-level variable. This was a dumb move and
735 # broke things for the rest of us. prepare_cached is a better
736 # way to cache statement handles anyway.
738 ($table eq 'accountlines' ) ? "SELECT * FROM $table WHERE accountlines_id = ?" :
739 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
740 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
741 ($table eq 'tickets' ) ? "SELECT * FROM $table WHERE id = ?" :
742 ($table eq 'ticket_updates' ) ? "SELECT * FROM $table WHERE id = ?" :
743 ($table eq 'credits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
744 ($table eq 'debits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
745 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
746 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
747 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE issue_id = ?" :
748 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
749 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
750 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
751 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
752 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
753 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
754 ($table eq 'aqbasket' ) ? "SELECT * FROM $table WHERE basketno = ?" :
755 ($table eq 'illrequests' ) ? "SELECT * FROM $table WHERE illrequest_id = ?" :
756 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
757 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
758 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
759 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
760 ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" :
761 ($table eq 'additional_contents' || $table eq 'opac_news') ? "SELECT * FROM additional_contents WHERE idnew = ?" :
762 ($table eq 'recalls') ? "SELECT * FROM $table WHERE recall_id = ?" :
765 warn "ERROR: No _parseletter_sth query for table '$table'";
766 return; # nothing to get
768 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
769 warn "ERROR: Failed to prepare query: '$query'";
772 return $sth; # now cache is populated for that $table
775 =head2 _parseletter($letter, $table, $values)
778 - $letter : a hash to letter fields (title & content useful)
779 - $table : the Koha table to parse.
780 - $values_in : table record hashref
781 parse all fields from a table, and replace values in title & content with the appropriate value
782 (not exported sub, used only internally)
787 my ( $letter, $table, $values_in ) = @_;
789 # Work on a local copy of $values_in (passed by reference) to avoid side effects
790 # in callers ( by changing / formatting values )
791 my $values = $values_in ? { %$values_in } : {};
793 # FIXME Dates formatting must be done in notice's templates
794 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
795 $values->{'dateexpiry'} = output_pref({ dt => dt_from_string( $values->{'dateexpiry'} ), dateonly => 1 });
798 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
799 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
802 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
803 my $todaysdate = output_pref( dt_from_string() );
804 $letter->{content} =~ s/<<today>>/$todaysdate/go;
807 while ( my ($field, $val) = each %$values ) {
808 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
809 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
810 #Therefore adding the test on biblio. This includes biblioitems,
811 #but excludes items. Removed unneeded global and lookahead.
813 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
814 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
815 $val = $av->count ? $av->next->lib : '';
819 my $replacedby = defined ($val) ? $val : '';
821 and not $replacedby =~ m|9999-12-31|
822 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
824 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
825 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
826 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
828 for my $letter_field ( qw( title content ) ) {
829 my $filter_string_used = q{};
830 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
831 # We overwrite $dateonly if the filter exists and we have a time in the datetime
832 $filter_string_used = $1 || q{};
833 $dateonly = $1 unless $dateonly;
835 my $replacedby_date = eval {
836 output_pref({ dt => scalar dt_from_string( $replacedby ), dateonly => $dateonly });
838 $replacedby_date //= q{};
840 if ( $letter->{ $letter_field } ) {
841 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
842 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
846 # Other fields replacement
848 for my $letter_field ( qw( title content ) ) {
849 if ( $letter->{ $letter_field } ) {
850 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
851 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
857 if ($table eq 'borrowers' && $letter->{content}) {
858 my $patron = Koha::Patrons->find( $values->{borrowernumber} );
860 my $attributes = $patron->extended_attributes;
862 while ( my $attribute = $attributes->next ) {
863 my $code = $attribute->code;
864 my $val = $attribute->description; # FIXME - we always display intranet description here!
865 $val =~ s/\p{P}(?=$)//g if $val;
866 next unless $val gt '';
868 push @{ $attr{$code} }, $val;
870 while ( my ($code, $val_ar) = each %attr ) {
871 my $replacefield = "<<borrower-attribute:$code>>";
872 my $replacedby = join ',', @$val_ar;
873 $letter->{content} =~ s/$replacefield/$replacedby/g;
882 my $success = EnqueueLetter( { letter => $letter,
883 borrowernumber => '12', message_transport_type => 'email' } )
885 Places a letter in the message_queue database table, which will
886 eventually get processed (sent) by the process_message_queue.pl
887 cronjob when it calls SendQueuedMessages.
889 Return message_id on success
892 * letter - required; A letter hashref as returned from GetPreparedLetter
893 * message_transport_type - required; One of the available mtts
894 * borrowernumber - optional if 'to_address' is passed; The borrowernumber of the patron we enqueuing the notice for
895 * to_address - optional if 'borrowernumber' is passed; The destination email address for the notice (defaults to patron->notice_email_address)
896 * from_address - optional; The from address for the notice, defaults to patron->library->from_email_address
897 * reply_address - optional; The reply address for the notice, defaults to patron->library->reply_to
902 my $params = shift or return;
904 return unless exists $params->{'letter'};
905 # return unless exists $params->{'borrowernumber'};
906 return unless exists $params->{'message_transport_type'};
908 my $content = $params->{letter}->{content};
909 $content =~ s/\s+//g if(defined $content);
910 if ( not defined $content or $content eq '' ) {
911 Koha::Logger->get->info("Trying to add an empty message to the message queue");
915 # If we have any attachments we should encode then into the body.
916 if ( $params->{'attachments'} ) {
917 $params->{'letter'} = _add_attachments(
918 { letter => $params->{'letter'},
919 attachments => $params->{'attachments'},
924 my $dbh = C4::Context->dbh();
925 my $statement = << 'ENDSQL';
926 INSERT INTO message_queue
927 ( letter_id, borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, reply_address, content_type, failure_code )
928 VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, CAST(NOW() AS DATETIME), ?, ?, ?, ?, ? )
931 my $sth = $dbh->prepare($statement);
932 my $result = $sth->execute(
933 $params->{letter}->{id} || undef, # letter.id
934 $params->{'borrowernumber'}, # borrowernumber
935 $params->{'letter'}->{'title'}, # subject
936 $params->{'letter'}->{'content'}, # content
937 $params->{'letter'}->{'metadata'} || '', # metadata
938 $params->{'letter'}->{'code'} || '', # letter_code
939 $params->{'message_transport_type'}, # message_transport_type
941 $params->{'to_address'}, # to_address
942 $params->{'from_address'}, # from_address
943 $params->{'reply_address'}, # reply_address
944 $params->{'letter'}->{'content-type'}, # content_type
945 $params->{'failure_code'} || '', # failure_code
947 return $dbh->last_insert_id(undef,undef,'message_queue', undef);
950 =head2 SendQueuedMessages ([$hashref])
952 my $sent = SendQueuedMessages({
953 letter_code => $letter_code,
954 borrowernumber => $who_letter_is_for,
960 Sends all of the 'pending' items in the message queue, unless
961 parameters are passed.
963 The letter_code, borrowernumber and limit parameters are used
964 to build a parameter set for _get_unsent_messages, thus limiting
965 which pending messages will be processed. They are all optional.
967 The verbose parameter can be used to generate debugging output.
970 Returns number of messages sent.
974 sub SendQueuedMessages {
977 my $which_unsent_messages = {
978 'message_id' => $params->{'message_id'},
979 'limit' => $params->{'limit'} // 0,
980 'borrowernumber' => $params->{'borrowernumber'} // q{},
981 'letter_code' => $params->{'letter_code'} // q{},
982 'message_transport_type' => $params->{'type'} // q{},
983 'where' => $params->{'where'} // q{},
985 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
986 MESSAGE: foreach my $message ( @$unsent_messages ) {
987 my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
988 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
989 $message_object->make_column_dirty('status');
990 return unless $message_object->store;
992 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
993 warn sprintf( 'sending %s message to patron: %s',
994 $message->{'message_transport_type'},
995 $message->{'borrowernumber'} || 'Admin' )
996 if $params->{'verbose'};
997 # This is just begging for subclassing
998 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
999 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
1000 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1002 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
1003 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
1004 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1005 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1006 unless ( $sms_provider ) {
1007 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1008 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1011 unless ( $patron->smsalertnumber ) {
1012 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1013 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1016 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1017 $message->{to_address} .= '@' . $sms_provider->domain();
1019 # Check for possible from_address override
1020 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1021 if ($from_address && $message->{from_address} ne $from_address) {
1022 $message->{from_address} = $from_address;
1023 _update_message_from_address($message->{'message_id'}, $message->{from_address});
1026 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1027 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1029 _send_message_by_sms( $message );
1033 return scalar( @$unsent_messages );
1036 =head2 GetRSSMessages
1038 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1040 returns a listref of all queued RSS messages for a particular person.
1044 sub GetRSSMessages {
1047 return unless $params;
1048 return unless ref $params;
1049 return unless $params->{'borrowernumber'};
1051 return _get_unsent_messages( { message_transport_type => 'rss',
1052 limit => $params->{'limit'},
1053 borrowernumber => $params->{'borrowernumber'}, } );
1056 =head2 GetPrintMessages
1058 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1060 Returns a arrayref of all queued print messages (optionally, for a particular
1065 sub GetPrintMessages {
1066 my $params = shift || {};
1068 return _get_unsent_messages( { message_transport_type => 'print',
1069 borrowernumber => $params->{'borrowernumber'},
1073 =head2 GetQueuedMessages ([$hashref])
1075 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1077 Fetches a list of messages from the message queue optionally filtered by borrowernumber
1078 and limited to specified limit.
1080 Return is an arrayref of hashes, each has represents a message in the message queue.
1084 sub GetQueuedMessages {
1087 my $dbh = C4::Context->dbh();
1088 my $statement = << 'ENDSQL';
1089 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued, updated_on, failure_code
1095 if ( exists $params->{'borrowernumber'} ) {
1096 push @whereclauses, ' borrowernumber = ? ';
1097 push @query_params, $params->{'borrowernumber'};
1100 if ( @whereclauses ) {
1101 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1104 if ( defined $params->{'limit'} ) {
1105 $statement .= ' LIMIT ? ';
1106 push @query_params, $params->{'limit'};
1109 my $sth = $dbh->prepare( $statement );
1110 my $result = $sth->execute( @query_params );
1111 return $sth->fetchall_arrayref({});
1114 =head2 GetMessageTransportTypes
1116 my @mtt = GetMessageTransportTypes();
1118 returns an arrayref of transport types
1122 sub GetMessageTransportTypes {
1123 my $dbh = C4::Context->dbh();
1124 my $mtts = $dbh->selectcol_arrayref("
1125 SELECT message_transport_type
1126 FROM message_transport_types
1127 ORDER BY message_transport_type
1134 my $message = C4::Letters::Message($message_id);
1139 my ( $message_id ) = @_;
1140 return unless $message_id;
1141 my $dbh = C4::Context->dbh;
1142 return $dbh->selectrow_hashref(q|
1143 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
1145 WHERE message_id = ?
1146 |, {}, $message_id );
1149 =head2 ResendMessage
1151 Attempt to resend a message which has failed previously.
1153 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1155 Updates the message to 'pending' status so that
1156 it will be resent later on.
1158 returns 1 on success, 0 on failure, undef if no message was found
1163 my $message_id = shift;
1164 return unless $message_id;
1166 my $message = GetMessage( $message_id );
1167 return unless $message;
1169 if ( $message->{status} ne 'pending' ) {
1170 $rv = C4::Letters::_set_message_status({
1171 message_id => $message_id,
1172 status => 'pending',
1174 $rv = $rv > 0? 1: 0;
1175 # Clear destination email address to force address update
1176 _update_message_to_address( $message_id, undef ) if $rv &&
1177 $message->{message_transport_type} eq 'email';
1182 =head2 _add_attachements
1184 _add_attachments({ letter => $letter, attachments => $attachments });
1187 letter - the standard letter hashref
1188 attachments - listref of attachments. each attachment is a hashref of:
1189 type - the mime type, like 'text/plain'
1190 content - the actual attachment
1191 filename - the name of the attachment.
1193 returns your letter object, with the content updated.
1194 This routine picks the I<content> of I<letter> and generates a MIME
1195 email, attaching the passed I<attachments> using Koha::Email. The
1196 content is replaced by the string representation of the MIME object,
1197 and the content-type is updated for later handling.
1201 sub _add_attachments {
1204 my $letter = $params->{letter};
1205 my $attachments = $params->{attachments};
1206 return $letter unless @$attachments;
1208 my $message = Koha::Email->new;
1210 if ( $letter->{is_html} ) {
1211 $message->html_body( _wrap_html( $letter->{content}, $letter->{title} ) );
1214 $message->text_body( $letter->{content} );
1217 foreach my $attachment ( @$attachments ) {
1219 Encode::encode( "UTF-8", $attachment->{content} ),
1220 content_type => $attachment->{type} || 'application/octet-stream',
1221 name => $attachment->{filename},
1222 disposition => 'attachment',
1226 $letter->{'content-type'} = SERIALIZED_EMAIL_CONTENT_TYPE;
1227 $letter->{content} = $message->as_string;
1233 =head2 _get_unsent_messages
1235 This function's parameter hash reference takes the following
1236 optional named parameters:
1237 message_transport_type: method of message sending (e.g. email, sms, etc.)
1238 Can be a single string, or an arrayref of strings
1239 borrowernumber : who the message is to be sent
1240 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1241 Can be a single string, or an arrayref of strings
1242 message_id : the message_id of the message. In that case the sub will return only 1 result
1243 limit : maximum number of messages to send
1245 This function returns an array of matching hash referenced rows from
1246 message_queue with some borrower information added.
1250 sub _get_unsent_messages {
1253 my $dbh = C4::Context->dbh();
1255 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
1256 FROM message_queue mq
1257 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1261 my @query_params = ('pending');
1262 if ( ref $params ) {
1263 if ( $params->{'borrowernumber'} ) {
1264 $statement .= ' AND mq.borrowernumber = ? ';
1265 push @query_params, $params->{'borrowernumber'};
1267 if ( $params->{'letter_code'} ) {
1268 my @letter_codes = ref $params->{'letter_code'} eq "ARRAY" ? @{$params->{'letter_code'}} : $params->{'letter_code'};
1269 if ( @letter_codes ) {
1270 my $q = join( ",", ("?") x @letter_codes );
1271 $statement .= " AND mq.letter_code IN ( $q ) ";
1272 push @query_params, @letter_codes;
1275 if ( $params->{'message_transport_type'} ) {
1276 my @types = ref $params->{'message_transport_type'} eq "ARRAY" ? @{$params->{'message_transport_type'}} : $params->{'message_transport_type'};
1278 my $q = join( ",", ("?") x @types );
1279 $statement .= " AND message_transport_type IN ( $q ) ";
1280 push @query_params, @types;
1283 if ( $params->{message_id} ) {
1284 $statement .= ' AND message_id = ?';
1285 push @query_params, $params->{message_id};
1287 if ( $params->{where} ) {
1288 $statement .= " AND $params->{where} ";
1290 if ( $params->{'limit'} ) {
1291 $statement .= ' limit ? ';
1292 push @query_params, $params->{'limit'};
1296 my $sth = $dbh->prepare( $statement );
1297 my $result = $sth->execute( @query_params );
1298 return $sth->fetchall_arrayref({});
1301 sub _send_message_by_email {
1302 my $message = shift or return;
1303 my ($username, $password, $method) = @_;
1305 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1306 my $to_address = $message->{'to_address'};
1307 unless ($to_address) {
1309 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1310 _set_message_status(
1312 message_id => $message->{'message_id'},
1314 failure_code => 'INVALID_BORNUMBER'
1319 $to_address = $patron->notice_email_address;
1320 unless ($to_address) {
1321 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1322 # warning too verbose for this more common case?
1323 _set_message_status(
1325 message_id => $message->{'message_id'},
1327 failure_code => 'NO_EMAIL'
1334 my $subject = $message->{'subject'};
1336 my $content = $message->{'content'};
1337 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1338 my $is_html = $content_type =~ m/html/io;
1340 my $branch_email = undef;
1341 my $branch_replyto = undef;
1342 my $branch_returnpath = undef;
1346 $library = $patron->library;
1347 $branch_email = $library->from_email_address;
1348 $branch_replyto = $library->branchreplyto;
1349 $branch_returnpath = $library->branchreturnpath;
1352 # NOTE: Patron may not be defined above so branch_email may be undefined still
1353 # so we need to fallback to KohaAdminEmailAddress as a last resort.
1355 $message->{'from_address'}
1357 || C4::Context->preference('KohaAdminEmailAddress');
1358 if( !$from_address ) {
1359 _set_message_status(
1361 message_id => $message->{'message_id'},
1363 failure_code => 'NO_FROM',
1375 C4::Context->preference('NoticeBcc')
1376 ? ( bcc => C4::Context->preference('NoticeBcc') )
1379 from => $from_address,
1380 reply_to => $message->{'reply_address'} || $branch_replyto,
1381 sender => $branch_returnpath,
1382 subject => "" . $message->{subject}
1385 if ( $message->{'content_type'} && $message->{'content_type'} eq SERIALIZED_EMAIL_CONTENT_TYPE ) {
1387 # The message has been previously composed as a valid MIME object
1388 # and serialized as a string on the DB
1389 $email = Koha::Email->new_from_string($content);
1390 $email->create($params);
1392 $email = Koha::Email->create($params);
1394 $email->html_body( _wrap_html( $content, $subject ) );
1396 $email->text_body($content);
1401 if ( ref($_) eq 'Koha::Exceptions::BadParameter' ) {
1402 _set_message_status(
1404 message_id => $message->{'message_id'},
1406 failure_code => "INVALID_EMAIL:".$_->parameter
1410 _set_message_status(
1412 message_id => $message->{'message_id'},
1414 failure_code => 'UNKNOWN_ERROR'
1420 return unless $email;
1424 $smtp_server = $library->smtp_server;
1427 $smtp_server = Koha::SMTP::Servers->get_default;
1433 sasl_username => $username,
1434 sasl_password => $password,
1439 # if initial message address was empty, coming here means that a to address was found and
1440 # queue should be updated; same if to address was overriden by Koha::Email->create
1441 _update_message_to_address( $message->{'message_id'}, $email->email->header('To') )
1442 if !$message->{to_address}
1443 || $message->{to_address} ne $email->email->header('To');
1446 $email->send_or_die({ transport => $smtp_server->transport });
1448 _set_message_status(
1450 message_id => $message->{'message_id'},
1458 _set_message_status(
1460 message_id => $message->{'message_id'},
1462 failure_code => 'SENDMAIL'
1466 carp "$Mail::Sendmail::error";
1472 my ($content, $title) = @_;
1474 my $css = C4::Context->preference("NoticeCSS") || '';
1475 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1477 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1478 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1479 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1481 <title>$title</title>
1482 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1493 my ( $message ) = @_;
1494 my $dbh = C4::Context->dbh;
1495 my $count = $dbh->selectrow_array(q|
1498 WHERE message_transport_type = ?
1499 AND borrowernumber = ?
1501 AND CAST(updated_on AS date) = CAST(NOW() AS date)
1504 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1508 sub _send_message_by_sms {
1509 my $message = shift or return;
1510 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1511 _update_message_to_address($message->{message_id}, $patron->smsalertnumber) if $patron;
1513 unless ( $patron and $patron->smsalertnumber ) {
1514 _set_message_status( { message_id => $message->{'message_id'},
1516 failure_code => 'MISSING_SMS' } );
1520 if ( _is_duplicate( $message ) ) {
1521 _set_message_status(
1523 message_id => $message->{'message_id'},
1525 failure_code => 'DUPLICATE_MESSAGE'
1531 my $success = C4::SMS->send_sms(
1533 destination => $patron->smsalertnumber,
1534 message => $message->{'content'},
1539 _set_message_status(
1541 message_id => $message->{'message_id'},
1548 _set_message_status(
1550 message_id => $message->{'message_id'},
1552 failure_code => 'NO_NOTES'
1560 sub _update_message_to_address {
1562 my $dbh = C4::Context->dbh();
1563 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1566 sub _update_message_from_address {
1567 my ($message_id, $from_address) = @_;
1568 my $dbh = C4::Context->dbh();
1569 $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1572 sub _set_message_status {
1573 my $params = shift or return;
1575 foreach my $required_parameter ( qw( message_id status ) ) {
1576 return unless exists $params->{ $required_parameter };
1579 my $dbh = C4::Context->dbh();
1580 my $statement = 'UPDATE message_queue SET status= ?, failure_code= ? WHERE message_id = ?';
1581 my $sth = $dbh->prepare( $statement );
1582 my $result = $sth->execute( $params->{'status'},
1583 $params->{'failure_code'} || '',
1584 $params->{'message_id'} );
1589 my ( $params ) = @_;
1591 my $content = $params->{content};
1592 my $tables = $params->{tables};
1593 my $loops = $params->{loops};
1594 my $objects = $params->{objects} || {};
1595 my $substitute = $params->{substitute} || {};
1596 my $lang = defined($params->{lang}) && $params->{lang} ne 'default' ? $params->{lang} : 'en';
1597 my ($theme, $availablethemes);
1599 my $htdocs = C4::Context->config('intrahtdocs');
1600 ($theme, $lang, $availablethemes)= C4::Templates::availablethemes( $htdocs, 'about.tt', 'intranet', $lang);
1602 foreach (@$availablethemes) {
1603 push @includes, "$htdocs/$_/$lang/includes";
1604 push @includes, "$htdocs/$_/en/includes" unless $lang eq 'en';
1607 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1608 my $template = Template->new(
1612 PLUGIN_BASE => 'Koha::Template::Plugin',
1613 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1614 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1615 INCLUDE_PATH => \@includes,
1617 ENCODING => 'UTF-8',
1619 ) or die Template->error();
1621 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute, %$objects };
1623 $content = add_tt_filters( $content );
1624 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1627 my $schema = Koha::Database->new->schema;
1629 my $processed = try {
1630 $template->process( \$content, $tt_params, \$output );
1633 $schema->txn_rollback;
1635 croak "ERROR PROCESSING TEMPLATE: " . $template->error() unless $processed;
1640 sub _get_tt_params {
1641 my ($tables, $is_a_loop) = @_;
1647 article_requests => {
1648 module => 'Koha::ArticleRequests',
1649 singular => 'article_request',
1650 plural => 'article_requests',
1654 module => 'Koha::Acquisition::Baskets',
1655 singular => 'basket',
1656 plural => 'baskets',
1660 module => 'Koha::Biblios',
1661 singular => 'biblio',
1662 plural => 'biblios',
1663 pk => 'biblionumber',
1666 module => 'Koha::Biblioitems',
1667 singular => 'biblioitem',
1668 plural => 'biblioitems',
1669 pk => 'biblioitemnumber',
1672 module => 'Koha::Patrons',
1673 singular => 'borrower',
1674 plural => 'borrowers',
1675 pk => 'borrowernumber',
1678 module => 'Koha::Libraries',
1679 singular => 'branch',
1680 plural => 'branches',
1684 module => 'Koha::Account::Lines',
1685 singular => 'credit',
1686 plural => 'credits',
1687 pk => 'accountlines_id',
1690 module => 'Koha::Account::Lines',
1691 singular => 'debit',
1693 pk => 'accountlines_id',
1696 module => 'Koha::Items',
1701 additional_contents => {
1702 module => 'Koha::AdditionalContents',
1703 singular => 'additional_content',
1704 plural => 'additional_contents',
1708 module => 'Koha::AdditionalContents',
1714 module => 'Koha::Acquisition::Orders',
1715 singular => 'order',
1717 pk => 'ordernumber',
1720 module => 'Koha::Holds',
1726 module => 'Koha::Serials',
1727 singular => 'serial',
1728 plural => 'serials',
1732 module => 'Koha::Subscriptions',
1733 singular => 'subscription',
1734 plural => 'subscriptions',
1735 pk => 'subscriptionid',
1738 module => 'Koha::Suggestions',
1739 singular => 'suggestion',
1740 plural => 'suggestions',
1741 pk => 'suggestionid',
1744 module => 'Koha::Tickets',
1745 singular => 'ticket',
1746 plural => 'tickets',
1750 module => 'Koha::Ticket::Updates',
1751 singular => 'ticket_update',
1752 plural => 'ticket_updates',
1756 module => 'Koha::Checkouts',
1757 singular => 'checkout',
1758 plural => 'checkouts',
1762 module => 'Koha::Old::Checkouts',
1763 singular => 'old_checkout',
1764 plural => 'old_checkouts',
1768 module => 'Koha::Checkouts',
1769 singular => 'overdue',
1770 plural => 'overdues',
1773 borrower_modifications => {
1774 module => 'Koha::Patron::Modifications',
1775 singular => 'patron_modification',
1776 plural => 'patron_modifications',
1777 fk => 'verification_token',
1780 module => 'Koha::Illrequests',
1781 singular => 'illrequest',
1782 plural => 'illrequests',
1783 pk => 'illrequest_id'
1787 foreach my $table ( keys %$tables ) {
1788 next unless $config->{$table};
1790 my $ref = ref( $tables->{$table} ) || q{};
1791 my $module = $config->{$table}->{module};
1793 if ( can_load( modules => { $module => undef } ) ) {
1794 my $pk = $config->{$table}->{pk};
1795 my $fk = $config->{$table}->{fk};
1798 my $values = $tables->{$table} || [];
1799 unless ( ref( $values ) eq 'ARRAY' ) {
1800 croak "ERROR processing table $table. Wrong API call.";
1802 my $key = $pk ? $pk : $fk;
1803 # $key does not come from user input
1804 my $objects = $module->search(
1805 { $key => $values },
1807 # We want to retrieve the data in the same order
1809 # field is a MySQLism, but they are no other way to do it
1810 # To be generic we could do it in perl, but we will need to fetch
1811 # all the data then order them
1812 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1815 $params->{ $config->{$table}->{plural} } = $objects;
1817 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1818 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1820 if ( $fk ) { # Using a foreign key for lookup
1821 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1823 foreach my $key ( @$fk ) {
1824 $search->{$key} = $id->{$key};
1826 $object = $module->search( $search )->last();
1827 } else { # Foreign key is single column
1828 $object = $module->search( { $fk => $id } )->last();
1830 } else { # using the table's primary key for lookup
1831 $object = $module->find($id);
1833 $params->{ $config->{$table}->{singular} } = $object;
1835 else { # $ref eq 'ARRAY'
1837 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1838 $object = $module->search( { $pk => $tables->{$table} } )->last();
1840 else { # Params are mutliple foreign keys
1841 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1843 $params->{ $config->{$table}->{singular} } = $object;
1847 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1851 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1856 =head3 add_tt_filters
1858 $content = add_tt_filters( $content );
1860 Add TT filters to some specific fields if needed.
1862 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1866 sub add_tt_filters {
1867 my ( $content ) = @_;
1868 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1869 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1873 =head2 get_item_content
1875 my $item = Koha::Items->find(...)->unblessed;
1876 my @item_content_fields = qw( date_due title barcode author itemnumber );
1877 my $item_content = C4::Letters::get_item_content({
1879 item_content_fields => \@item_content_fields
1882 This function generates a tab-separated list of values for the passed item. Dates
1883 are formatted following the current setup.
1887 sub get_item_content {
1888 my ( $params ) = @_;
1889 my $item = $params->{item};
1890 my $dateonly = $params->{dateonly} || 0;
1891 my $item_content_fields = $params->{item_content_fields} || [];
1893 return unless $item;
1895 my @item_info = map {
1899 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1903 } @$item_content_fields;
1904 return join( "\t", @item_info ) . "\n";