3 # Copyright 2000-2002 Katipo Communications
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use Carp qw( carp croak );
24 use Module::Load::Conditional qw( can_load );
26 use Try::Tiny qw( catch try );
29 use C4::Log qw( logaction );
32 use Koha::DateUtils qw( dt_from_string output_pref );
33 use Koha::SMS::Providers;
36 use Koha::Notice::Messages;
37 use Koha::Notice::Templates;
38 use Koha::DateUtils qw( dt_from_string output_pref );
40 use Koha::SMTP::Servers;
41 use Koha::Subscriptions;
43 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 my $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 $module = $action eq 'ACQUISITION ORDER' ? 'ACQUISITIONS' : 'CLAIMS';
525 . join( ',', @email )
530 ) if C4::Context->preference("ClaimsLog");
533 # If we come here, return an OK status
537 =head2 GetPreparedLetter( %params )
540 module => letter module, mandatory
541 letter_code => letter code, mandatory
542 branchcode => for letter selection, if missing default system letter taken
543 tables => a hashref with table names as keys. Values are either:
544 - a scalar - primary key value
545 - an arrayref - primary key values
546 - a hashref - full record
547 substitute => custom substitution key/value pairs
548 repeat => records to be substituted on consecutive lines:
549 - an arrayref - tries to guess what needs substituting by
550 taking remaining << >> tokensr; not recommended
551 - a hashref token => @tables - replaces <token> << >> << >> </token>
552 subtemplate for each @tables row; table is a hashref as above
553 want_librarian => boolean, if set to true triggers librarian details
554 substitution from the userenv
556 letter fields hashref (title & content useful)
560 sub GetPreparedLetter {
563 my $letter = $params{letter};
564 my $lang = $params{lang} || 'default';
567 my $module = $params{module} or croak "No module";
568 my $letter_code = $params{letter_code} or croak "No letter_code";
569 my $branchcode = $params{branchcode} || '';
570 my $mtt = $params{message_transport_type} || 'email';
572 my $template = Koha::Notice::Templates->find_effective_template(
575 code => $letter_code,
576 branchcode => $branchcode,
577 message_transport_type => $mtt,
582 unless ( $template ) {
583 warn( "No $module $letter_code letter transported by " . $mtt );
587 $letter = $template->unblessed;
588 $letter->{'content-type'} = 'text/html; charset="UTF-8"' if $letter->{is_html};
591 my $objects = $params{objects} || {};
592 my $tables = $params{tables} || {};
593 my $substitute = $params{substitute} || {};
594 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
595 my $repeat = $params{repeat};
596 %$tables || %$substitute || $repeat || %$loops || %$objects
597 or carp( "ERROR: nothing to substitute - all of 'objects', 'tables', 'loops' and 'substitute' are empty" ),
599 my $want_librarian = $params{want_librarian};
602 while ( my ($token, $val) = each %$substitute ) {
604 if ( $token eq 'items.content' ) {
605 $val =~ s|\n|<br/>|g if $letter->{is_html};
608 $letter->{title} =~ s/<<$token>>/$val/g;
609 $letter->{content} =~ s/<<$token>>/$val/g;
613 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
614 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
616 if ($want_librarian) {
617 # parsing librarian name
618 my $userenv = C4::Context->userenv;
619 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
620 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
621 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
624 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
627 if (ref ($repeat) eq 'ARRAY' ) {
628 $repeat_no_enclosing_tags = $repeat;
630 $repeat_enclosing_tags = $repeat;
634 if ($repeat_enclosing_tags) {
635 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
636 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
639 my %subletter = ( title => '', content => $subcontent );
640 _substitute_tables( \%subletter, $_ );
643 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
649 _substitute_tables( $letter, $tables );
652 if ($repeat_no_enclosing_tags) {
653 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
658 $c =~ s/<<count>>/$i/go;
659 foreach my $field ( keys %{$_} ) {
660 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
664 } @$repeat_no_enclosing_tags;
666 my $replaceby = join( "\n", @lines );
667 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
671 $letter->{content} = _process_tt(
673 content => $letter->{content},
677 substitute => $substitute,
682 $letter->{title} = _process_tt(
684 content => $letter->{title},
688 substitute => $substitute,
693 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
698 sub _substitute_tables {
699 my ( $letter, $tables ) = @_;
700 while ( my ($table, $param) = each %$tables ) {
703 my $ref = ref $param;
706 if ($ref && $ref eq 'HASH') {
710 my $sth = _parseletter_sth($table);
712 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
715 $sth->execute( $ref ? @$param : $param );
717 $values = $sth->fetchrow_hashref;
721 _parseletter ( $letter, $table, $values );
725 sub _parseletter_sth {
729 carp "ERROR: _parseletter_sth() called without argument (table)";
732 # NOTE: we used to check whether we had a statement handle cached in
733 # a %handles module-level variable. This was a dumb move and
734 # broke things for the rest of us. prepare_cached is a better
735 # way to cache statement handles anyway.
737 ($table eq 'accountlines' ) ? "SELECT * FROM $table WHERE accountlines_id = ?" :
738 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
739 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
740 ($table eq 'credits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
741 ($table eq 'debits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
742 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
743 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
744 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE issue_id = ?" :
745 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
746 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
747 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
748 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
749 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
750 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
751 ($table eq 'aqbasket' ) ? "SELECT * FROM $table WHERE basketno = ?" :
752 ($table eq 'illrequests' ) ? "SELECT * FROM $table WHERE illrequest_id = ?" :
753 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
754 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
755 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
756 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
757 ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" :
758 ($table eq 'additional_contents' || $table eq 'opac_news') ? "SELECT * FROM additional_contents WHERE idnew = ?" :
759 ($table eq 'recalls') ? "SELECT * FROM $table WHERE recall_id = ?" :
762 warn "ERROR: No _parseletter_sth query for table '$table'";
763 return; # nothing to get
765 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
766 warn "ERROR: Failed to prepare query: '$query'";
769 return $sth; # now cache is populated for that $table
772 =head2 _parseletter($letter, $table, $values)
775 - $letter : a hash to letter fields (title & content useful)
776 - $table : the Koha table to parse.
777 - $values_in : table record hashref
778 parse all fields from a table, and replace values in title & content with the appropriate value
779 (not exported sub, used only internally)
784 my ( $letter, $table, $values_in ) = @_;
786 # Work on a local copy of $values_in (passed by reference) to avoid side effects
787 # in callers ( by changing / formatting values )
788 my $values = $values_in ? { %$values_in } : {};
790 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
791 $values->{'dateexpiry'} = output_pref({ dt => dt_from_string( $values->{'dateexpiry'} ), dateonly => 1 });
794 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
795 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
798 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
799 my $todaysdate = output_pref( dt_from_string() );
800 $letter->{content} =~ s/<<today>>/$todaysdate/go;
803 while ( my ($field, $val) = each %$values ) {
804 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
805 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
806 #Therefore adding the test on biblio. This includes biblioitems,
807 #but excludes items. Removed unneeded global and lookahead.
809 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
810 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
811 $val = $av->count ? $av->next->lib : '';
815 my $replacedby = defined ($val) ? $val : '';
817 and not $replacedby =~ m|9999-12-31|
818 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
820 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
821 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
822 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
824 for my $letter_field ( qw( title content ) ) {
825 my $filter_string_used = q{};
826 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
827 # We overwrite $dateonly if the filter exists and we have a time in the datetime
828 $filter_string_used = $1 || q{};
829 $dateonly = $1 unless $dateonly;
831 my $replacedby_date = eval {
832 output_pref({ dt => scalar dt_from_string( $replacedby ), dateonly => $dateonly });
834 $replacedby_date //= q{};
836 if ( $letter->{ $letter_field } ) {
837 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
838 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
842 # Other fields replacement
844 for my $letter_field ( qw( title content ) ) {
845 if ( $letter->{ $letter_field } ) {
846 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
847 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
853 if ($table eq 'borrowers' && $letter->{content}) {
854 my $patron = Koha::Patrons->find( $values->{borrowernumber} );
856 my $attributes = $patron->extended_attributes;
858 while ( my $attribute = $attributes->next ) {
859 my $code = $attribute->code;
860 my $val = $attribute->description; # FIXME - we always display intranet description here!
861 $val =~ s/\p{P}(?=$)//g if $val;
862 next unless $val gt '';
864 push @{ $attr{$code} }, $val;
866 while ( my ($code, $val_ar) = each %attr ) {
867 my $replacefield = "<<borrower-attribute:$code>>";
868 my $replacedby = join ',', @$val_ar;
869 $letter->{content} =~ s/$replacefield/$replacedby/g;
878 my $success = EnqueueLetter( { letter => $letter,
879 borrowernumber => '12', message_transport_type => 'email' } )
881 Places a letter in the message_queue database table, which will
882 eventually get processed (sent) by the process_message_queue.pl
883 cronjob when it calls SendQueuedMessages.
885 Return message_id on success
888 * letter - required; A letter hashref as returned from GetPreparedLetter
889 * message_transport_type - required; One of the available mtts
890 * borrowernumber - optional if 'to_address' is passed; The borrowernumber of the patron we enqueuing the notice for
891 * to_address - optional if 'borrowernumber' is passed; The destination email address for the notice (defaults to patron->notice_email_address)
892 * from_address - optional; The from address for the notice, defaults to patron->library->from_email_address
893 * reply_address - optional; The reply address for the notice, defaults to patron->library->reply_to
898 my $params = shift or return;
900 return unless exists $params->{'letter'};
901 # return unless exists $params->{'borrowernumber'};
902 return unless exists $params->{'message_transport_type'};
904 my $content = $params->{letter}->{content};
905 $content =~ s/\s+//g if(defined $content);
906 if ( not defined $content or $content eq '' ) {
907 Koha::Logger->get->info("Trying to add an empty message to the message queue");
911 # If we have any attachments we should encode then into the body.
912 if ( $params->{'attachments'} ) {
913 $params->{'letter'} = _add_attachments(
914 { letter => $params->{'letter'},
915 attachments => $params->{'attachments'},
920 my $dbh = C4::Context->dbh();
921 my $statement = << 'ENDSQL';
922 INSERT INTO message_queue
923 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, reply_address, content_type, failure_code )
925 ( ?, ?, ?, ?, ?, ?, ?, CAST(NOW() AS DATETIME), ?, ?, ?, ?, ? )
928 my $sth = $dbh->prepare($statement);
929 my $result = $sth->execute(
930 $params->{'borrowernumber'}, # borrowernumber
931 $params->{'letter'}->{'title'}, # subject
932 $params->{'letter'}->{'content'}, # content
933 $params->{'letter'}->{'metadata'} || '', # metadata
934 $params->{'letter'}->{'code'} || '', # letter_code
935 $params->{'message_transport_type'}, # message_transport_type
937 $params->{'to_address'}, # to_address
938 $params->{'from_address'}, # from_address
939 $params->{'reply_address'}, # reply_address
940 $params->{'letter'}->{'content-type'}, # content_type
941 $params->{'failure_code'} || '', # failure_code
943 return $dbh->last_insert_id(undef,undef,'message_queue', undef);
946 =head2 SendQueuedMessages ([$hashref])
948 my $sent = SendQueuedMessages({
949 letter_code => $letter_code,
950 borrowernumber => $who_letter_is_for,
956 Sends all of the 'pending' items in the message queue, unless
957 parameters are passed.
959 The letter_code, borrowernumber and limit parameters are used
960 to build a parameter set for _get_unsent_messages, thus limiting
961 which pending messages will be processed. They are all optional.
963 The verbose parameter can be used to generate debugging output.
966 Returns number of messages sent.
970 sub SendQueuedMessages {
973 my $which_unsent_messages = {
974 'message_id' => $params->{'message_id'},
975 'limit' => $params->{'limit'} // 0,
976 'borrowernumber' => $params->{'borrowernumber'} // q{},
977 'letter_code' => $params->{'letter_code'} // q{},
978 'type' => $params->{'type'} // q{},
980 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
981 MESSAGE: foreach my $message ( @$unsent_messages ) {
982 my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
983 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
984 $message_object->make_column_dirty('status');
985 return unless $message_object->store;
987 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
988 warn sprintf( 'sending %s message to patron: %s',
989 $message->{'message_transport_type'},
990 $message->{'borrowernumber'} || 'Admin' )
991 if $params->{'verbose'};
992 # This is just begging for subclassing
993 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
994 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
995 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
997 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
998 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
999 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1000 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1001 unless ( $sms_provider ) {
1002 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1003 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1006 unless ( $patron->smsalertnumber ) {
1007 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1008 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1011 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1012 $message->{to_address} .= '@' . $sms_provider->domain();
1014 # Check for possible from_address override
1015 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1016 if ($from_address && $message->{from_address} ne $from_address) {
1017 $message->{from_address} = $from_address;
1018 _update_message_from_address($message->{'message_id'}, $message->{from_address});
1021 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1022 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1024 _send_message_by_sms( $message );
1028 return scalar( @$unsent_messages );
1031 =head2 GetRSSMessages
1033 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1035 returns a listref of all queued RSS messages for a particular person.
1039 sub GetRSSMessages {
1042 return unless $params;
1043 return unless ref $params;
1044 return unless $params->{'borrowernumber'};
1046 return _get_unsent_messages( { message_transport_type => 'rss',
1047 limit => $params->{'limit'},
1048 borrowernumber => $params->{'borrowernumber'}, } );
1051 =head2 GetPrintMessages
1053 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1055 Returns a arrayref of all queued print messages (optionally, for a particular
1060 sub GetPrintMessages {
1061 my $params = shift || {};
1063 return _get_unsent_messages( { message_transport_type => 'print',
1064 borrowernumber => $params->{'borrowernumber'},
1068 =head2 GetQueuedMessages ([$hashref])
1070 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1072 Fetches a list of messages from the message queue optionally filtered by borrowernumber
1073 and limited to specified limit.
1075 Return is an arrayref of hashes, each has represents a message in the message queue.
1079 sub GetQueuedMessages {
1082 my $dbh = C4::Context->dbh();
1083 my $statement = << 'ENDSQL';
1084 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued, updated_on, failure_code
1090 if ( exists $params->{'borrowernumber'} ) {
1091 push @whereclauses, ' borrowernumber = ? ';
1092 push @query_params, $params->{'borrowernumber'};
1095 if ( @whereclauses ) {
1096 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1099 if ( defined $params->{'limit'} ) {
1100 $statement .= ' LIMIT ? ';
1101 push @query_params, $params->{'limit'};
1104 my $sth = $dbh->prepare( $statement );
1105 my $result = $sth->execute( @query_params );
1106 return $sth->fetchall_arrayref({});
1109 =head2 GetMessageTransportTypes
1111 my @mtt = GetMessageTransportTypes();
1113 returns an arrayref of transport types
1117 sub GetMessageTransportTypes {
1118 my $dbh = C4::Context->dbh();
1119 my $mtts = $dbh->selectcol_arrayref("
1120 SELECT message_transport_type
1121 FROM message_transport_types
1122 ORDER BY message_transport_type
1129 my $message = C4::Letters::Message($message_id);
1134 my ( $message_id ) = @_;
1135 return unless $message_id;
1136 my $dbh = C4::Context->dbh;
1137 return $dbh->selectrow_hashref(q|
1138 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
1140 WHERE message_id = ?
1141 |, {}, $message_id );
1144 =head2 ResendMessage
1146 Attempt to resend a message which has failed previously.
1148 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1150 Updates the message to 'pending' status so that
1151 it will be resent later on.
1153 returns 1 on success, 0 on failure, undef if no message was found
1158 my $message_id = shift;
1159 return unless $message_id;
1161 my $message = GetMessage( $message_id );
1162 return unless $message;
1164 if ( $message->{status} ne 'pending' ) {
1165 $rv = C4::Letters::_set_message_status({
1166 message_id => $message_id,
1167 status => 'pending',
1169 $rv = $rv > 0? 1: 0;
1170 # Clear destination email address to force address update
1171 _update_message_to_address( $message_id, undef ) if $rv &&
1172 $message->{message_transport_type} eq 'email';
1177 =head2 _add_attachements
1179 _add_attachments({ letter => $letter, attachments => $attachments });
1182 letter - the standard letter hashref
1183 attachments - listref of attachments. each attachment is a hashref of:
1184 type - the mime type, like 'text/plain'
1185 content - the actual attachment
1186 filename - the name of the attachment.
1188 returns your letter object, with the content updated.
1189 This routine picks the I<content> of I<letter> and generates a MIME
1190 email, attaching the passed I<attachments> using Koha::Email. The
1191 content is replaced by the string representation of the MIME object,
1192 and the content-type is updated for later handling.
1196 sub _add_attachments {
1199 my $letter = $params->{letter};
1200 my $attachments = $params->{attachments};
1201 return $letter unless @$attachments;
1203 my $message = Koha::Email->new;
1205 if ( $letter->{is_html} ) {
1206 $message->html_body( _wrap_html( $letter->{content}, $letter->{title} ) );
1209 $message->text_body( $letter->{content} );
1212 foreach my $attachment ( @$attachments ) {
1214 Encode::encode( "UTF-8", $attachment->{content} ),
1215 content_type => $attachment->{type} || 'application/octet-stream',
1216 name => $attachment->{filename},
1217 disposition => 'attachment',
1221 $letter->{'content-type'} = SERIALIZED_EMAIL_CONTENT_TYPE;
1222 $letter->{content} = $message->as_string;
1228 =head2 _get_unsent_messages
1230 This function's parameter hash reference takes the following
1231 optional named parameters:
1232 message_transport_type: method of message sending (e.g. email, sms, etc.)
1233 borrowernumber : who the message is to be sent
1234 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1235 message_id : the message_id of the message. In that case the sub will return only 1 result
1236 limit : maximum number of messages to send
1238 This function returns an array of matching hash referenced rows from
1239 message_queue with some borrower information added.
1243 sub _get_unsent_messages {
1246 my $dbh = C4::Context->dbh();
1248 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
1249 FROM message_queue mq
1250 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1254 my @query_params = ('pending');
1255 if ( ref $params ) {
1256 if ( $params->{'message_transport_type'} ) {
1257 $statement .= ' AND mq.message_transport_type = ? ';
1258 push @query_params, $params->{'message_transport_type'};
1260 if ( $params->{'borrowernumber'} ) {
1261 $statement .= ' AND mq.borrowernumber = ? ';
1262 push @query_params, $params->{'borrowernumber'};
1264 if ( $params->{'letter_code'} ) {
1265 $statement .= ' AND mq.letter_code = ? ';
1266 push @query_params, $params->{'letter_code'};
1268 if ( $params->{'type'} ) {
1269 $statement .= ' AND message_transport_type = ? ';
1270 push @query_params, $params->{'type'};
1272 if ( $params->{message_id} ) {
1273 $statement .= ' AND message_id = ?';
1274 push @query_params, $params->{message_id};
1276 if ( $params->{'limit'} ) {
1277 $statement .= ' limit ? ';
1278 push @query_params, $params->{'limit'};
1282 my $sth = $dbh->prepare( $statement );
1283 my $result = $sth->execute( @query_params );
1284 return $sth->fetchall_arrayref({});
1287 sub _send_message_by_email {
1288 my $message = shift or return;
1289 my ($username, $password, $method) = @_;
1291 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1292 my $to_address = $message->{'to_address'};
1293 unless ($to_address) {
1295 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1296 _set_message_status(
1298 message_id => $message->{'message_id'},
1300 failure_code => 'INVALID_BORNUMBER'
1305 $to_address = $patron->notice_email_address;
1306 unless ($to_address) {
1307 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1308 # warning too verbose for this more common case?
1309 _set_message_status(
1311 message_id => $message->{'message_id'},
1313 failure_code => 'NO_EMAIL'
1320 my $subject = $message->{'subject'};
1322 my $content = $message->{'content'};
1323 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1324 my $is_html = $content_type =~ m/html/io;
1326 my $branch_email = undef;
1327 my $branch_replyto = undef;
1328 my $branch_returnpath = undef;
1332 $library = $patron->library;
1333 $branch_email = $library->from_email_address;
1334 $branch_replyto = $library->branchreplyto;
1335 $branch_returnpath = $library->branchreturnpath;
1338 # NOTE: Patron may not be defined above so branch_email may be undefined still
1339 # so we need to fallback to KohaAdminEmailAddress as a last resort.
1341 $message->{'from_address'}
1343 || C4::Context->preference('KohaAdminEmailAddress');
1344 if( !$from_address ) {
1345 _set_message_status(
1347 message_id => $message->{'message_id'},
1349 failure_code => 'NO_FROM',
1361 C4::Context->preference('NoticeBcc')
1362 ? ( bcc => C4::Context->preference('NoticeBcc') )
1365 from => $from_address,
1366 reply_to => $message->{'reply_address'} || $branch_replyto,
1367 sender => $branch_returnpath,
1368 subject => "" . $message->{subject}
1371 if ( $message->{'content_type'} && $message->{'content_type'} eq SERIALIZED_EMAIL_CONTENT_TYPE ) {
1373 # The message has been previously composed as a valid MIME object
1374 # and serialized as a string on the DB
1375 $email = Koha::Email->new_from_string($content);
1376 $email->create($params);
1378 $email = Koha::Email->create($params);
1380 $email->html_body( _wrap_html( $content, $subject ) );
1382 $email->text_body($content);
1387 if ( ref($_) eq 'Koha::Exceptions::BadParameter' ) {
1388 _set_message_status(
1390 message_id => $message->{'message_id'},
1392 failure_code => "INVALID_EMAIL:".$_->parameter
1396 _set_message_status(
1398 message_id => $message->{'message_id'},
1400 failure_code => 'UNKNOWN_ERROR'
1406 return unless $email;
1410 $smtp_server = $library->smtp_server;
1413 $smtp_server = Koha::SMTP::Servers->get_default;
1419 sasl_username => $username,
1420 sasl_password => $password,
1425 # if initial message address was empty, coming here means that a to address was found and
1426 # queue should be updated; same if to address was overriden by Koha::Email->create
1427 _update_message_to_address( $message->{'message_id'}, $email->email->header('To') )
1428 if !$message->{to_address}
1429 || $message->{to_address} ne $email->email->header('To');
1432 $email->send_or_die({ transport => $smtp_server->transport });
1434 _set_message_status(
1436 message_id => $message->{'message_id'},
1444 _set_message_status(
1446 message_id => $message->{'message_id'},
1448 failure_code => 'SENDMAIL'
1452 carp "$Mail::Sendmail::error";
1458 my ($content, $title) = @_;
1460 my $css = C4::Context->preference("NoticeCSS") || '';
1461 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1463 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1464 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1465 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1467 <title>$title</title>
1468 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1479 my ( $message ) = @_;
1480 my $dbh = C4::Context->dbh;
1481 my $count = $dbh->selectrow_array(q|
1484 WHERE message_transport_type = ?
1485 AND borrowernumber = ?
1487 AND CAST(updated_on AS date) = CAST(NOW() AS date)
1490 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1494 sub _send_message_by_sms {
1495 my $message = shift or return;
1496 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1498 unless ( $patron and $patron->smsalertnumber ) {
1499 _set_message_status( { message_id => $message->{'message_id'},
1501 failure_code => 'MISSING_SMS' } );
1505 if ( _is_duplicate( $message ) ) {
1506 _set_message_status(
1508 message_id => $message->{'message_id'},
1510 failure_code => 'DUPLICATE_MESSAGE'
1516 my $success = C4::SMS->send_sms(
1518 destination => $patron->smsalertnumber,
1519 message => $message->{'content'},
1524 _set_message_status(
1526 message_id => $message->{'message_id'},
1533 _set_message_status(
1535 message_id => $message->{'message_id'},
1537 failure_code => 'NO_NOTES'
1545 sub _update_message_to_address {
1547 my $dbh = C4::Context->dbh();
1548 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1551 sub _update_message_from_address {
1552 my ($message_id, $from_address) = @_;
1553 my $dbh = C4::Context->dbh();
1554 $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1557 sub _set_message_status {
1558 my $params = shift or return;
1560 foreach my $required_parameter ( qw( message_id status ) ) {
1561 return unless exists $params->{ $required_parameter };
1564 my $dbh = C4::Context->dbh();
1565 my $statement = 'UPDATE message_queue SET status= ?, failure_code= ? WHERE message_id = ?';
1566 my $sth = $dbh->prepare( $statement );
1567 my $result = $sth->execute( $params->{'status'},
1568 $params->{'failure_code'} || '',
1569 $params->{'message_id'} );
1574 my ( $params ) = @_;
1576 my $content = $params->{content};
1577 my $tables = $params->{tables};
1578 my $loops = $params->{loops};
1579 my $objects = $params->{objects};
1580 my $substitute = $params->{substitute} || {};
1581 my $lang = defined($params->{lang}) && $params->{lang} ne 'default' ? $params->{lang} : 'en';
1582 my ($theme, $availablethemes);
1584 my $htdocs = C4::Context->config('intrahtdocs');
1585 ($theme, $lang, $availablethemes)= C4::Templates::availablethemes( $htdocs, 'about.tt', 'intranet', $lang);
1587 foreach (@$availablethemes) {
1588 push @includes, "$htdocs/$_/$lang/includes";
1589 push @includes, "$htdocs/$_/en/includes" unless $lang eq 'en';
1592 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1593 my $template = Template->new(
1597 PLUGIN_BASE => 'Koha::Template::Plugin',
1598 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1599 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1600 INCLUDE_PATH => \@includes,
1602 ENCODING => 'UTF-8',
1604 ) or die Template->error();
1606 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute, %$objects };
1608 $content = add_tt_filters( $content );
1609 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1612 my $schema = Koha::Database->new->schema;
1614 $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1615 $schema->txn_rollback;
1620 sub _get_tt_params {
1621 my ($tables, $is_a_loop) = @_;
1627 article_requests => {
1628 module => 'Koha::ArticleRequests',
1629 singular => 'article_request',
1630 plural => 'article_requests',
1634 module => 'Koha::Acquisition::Baskets',
1635 singular => 'basket',
1636 plural => 'baskets',
1640 module => 'Koha::Biblios',
1641 singular => 'biblio',
1642 plural => 'biblios',
1643 pk => 'biblionumber',
1646 module => 'Koha::Biblioitems',
1647 singular => 'biblioitem',
1648 plural => 'biblioitems',
1649 pk => 'biblioitemnumber',
1652 module => 'Koha::Patrons',
1653 singular => 'borrower',
1654 plural => 'borrowers',
1655 pk => 'borrowernumber',
1658 module => 'Koha::Libraries',
1659 singular => 'branch',
1660 plural => 'branches',
1664 module => 'Koha::Account::Lines',
1665 singular => 'credit',
1666 plural => 'credits',
1667 pk => 'accountlines_id',
1670 module => 'Koha::Account::Lines',
1671 singular => 'debit',
1673 pk => 'accountlines_id',
1676 module => 'Koha::Items',
1681 additional_contents => {
1682 module => 'Koha::AdditionalContents',
1683 singular => 'additional_content',
1684 plural => 'additional_contents',
1688 module => 'Koha::AdditionalContents',
1694 module => 'Koha::Acquisition::Orders',
1695 singular => 'order',
1697 pk => 'ordernumber',
1700 module => 'Koha::Holds',
1706 module => 'Koha::Serials',
1707 singular => 'serial',
1708 plural => 'serials',
1712 module => 'Koha::Subscriptions',
1713 singular => 'subscription',
1714 plural => 'subscriptions',
1715 pk => 'subscriptionid',
1718 module => 'Koha::Suggestions',
1719 singular => 'suggestion',
1720 plural => 'suggestions',
1721 pk => 'suggestionid',
1724 module => 'Koha::Checkouts',
1725 singular => 'checkout',
1726 plural => 'checkouts',
1730 module => 'Koha::Old::Checkouts',
1731 singular => 'old_checkout',
1732 plural => 'old_checkouts',
1736 module => 'Koha::Checkouts',
1737 singular => 'overdue',
1738 plural => 'overdues',
1741 borrower_modifications => {
1742 module => 'Koha::Patron::Modifications',
1743 singular => 'patron_modification',
1744 plural => 'patron_modifications',
1745 fk => 'verification_token',
1748 module => 'Koha::Illrequests',
1749 singular => 'illrequest',
1750 plural => 'illrequests',
1751 pk => 'illrequest_id'
1755 foreach my $table ( keys %$tables ) {
1756 next unless $config->{$table};
1758 my $ref = ref( $tables->{$table} ) || q{};
1759 my $module = $config->{$table}->{module};
1761 if ( can_load( modules => { $module => undef } ) ) {
1762 my $pk = $config->{$table}->{pk};
1763 my $fk = $config->{$table}->{fk};
1766 my $values = $tables->{$table} || [];
1767 unless ( ref( $values ) eq 'ARRAY' ) {
1768 croak "ERROR processing table $table. Wrong API call.";
1770 my $key = $pk ? $pk : $fk;
1771 # $key does not come from user input
1772 my $objects = $module->search(
1773 { $key => $values },
1775 # We want to retrieve the data in the same order
1777 # field is a MySQLism, but they are no other way to do it
1778 # To be generic we could do it in perl, but we will need to fetch
1779 # all the data then order them
1780 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1783 $params->{ $config->{$table}->{plural} } = $objects;
1785 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1786 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1788 if ( $fk ) { # Using a foreign key for lookup
1789 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1791 foreach my $key ( @$fk ) {
1792 $search->{$key} = $id->{$key};
1794 $object = $module->search( $search )->last();
1795 } else { # Foreign key is single column
1796 $object = $module->search( { $fk => $id } )->last();
1798 } else { # using the table's primary key for lookup
1799 $object = $module->find($id);
1801 $params->{ $config->{$table}->{singular} } = $object;
1803 else { # $ref eq 'ARRAY'
1805 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1806 $object = $module->search( { $pk => $tables->{$table} } )->last();
1808 else { # Params are mutliple foreign keys
1809 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1811 $params->{ $config->{$table}->{singular} } = $object;
1815 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1819 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1824 =head3 add_tt_filters
1826 $content = add_tt_filters( $content );
1828 Add TT filters to some specific fields if needed.
1830 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1834 sub add_tt_filters {
1835 my ( $content ) = @_;
1836 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1837 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1841 =head2 get_item_content
1843 my $item = Koha::Items->find(...)->unblessed;
1844 my @item_content_fields = qw( date_due title barcode author itemnumber );
1845 my $item_content = C4::Letters::get_item_content({
1847 item_content_fields => \@item_content_fields
1850 This function generates a tab-separated list of values for the passed item. Dates
1851 are formatted following the current setup.
1855 sub get_item_content {
1856 my ( $params ) = @_;
1857 my $item = $params->{item};
1858 my $dateonly = $params->{dateonly} || 0;
1859 my $item_content_fields = $params->{item_content_fields} || [];
1861 return unless $item;
1863 my @item_info = map {
1867 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1871 } @$item_content_fields;
1872 return join( "\t", @item_info ) . "\n";