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 $tables = $params{tables} || {};
592 my $substitute = $params{substitute} || {};
593 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
594 my $repeat = $params{repeat};
595 %$tables || %$substitute || $repeat || %$loops
596 or carp( "ERROR: nothing to substitute - both 'tables', 'loops' and 'substitute' are empty" ),
598 my $want_librarian = $params{want_librarian};
601 while ( my ($token, $val) = each %$substitute ) {
602 if ( $token eq 'items.content' ) {
603 $val =~ s|\n|<br/>|g if $letter->{is_html};
606 $letter->{title} =~ s/<<$token>>/$val/g;
607 $letter->{content} =~ s/<<$token>>/$val/g;
611 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
612 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
614 if ($want_librarian) {
615 # parsing librarian name
616 my $userenv = C4::Context->userenv;
617 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
618 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
619 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
622 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
625 if (ref ($repeat) eq 'ARRAY' ) {
626 $repeat_no_enclosing_tags = $repeat;
628 $repeat_enclosing_tags = $repeat;
632 if ($repeat_enclosing_tags) {
633 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
634 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
637 my %subletter = ( title => '', content => $subcontent );
638 _substitute_tables( \%subletter, $_ );
641 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
647 _substitute_tables( $letter, $tables );
650 if ($repeat_no_enclosing_tags) {
651 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
656 $c =~ s/<<count>>/$i/go;
657 foreach my $field ( keys %{$_} ) {
658 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
662 } @$repeat_no_enclosing_tags;
664 my $replaceby = join( "\n", @lines );
665 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
669 $letter->{content} = _process_tt(
671 content => $letter->{content},
674 substitute => $substitute,
679 $letter->{title} = _process_tt(
681 content => $letter->{title},
684 substitute => $substitute,
688 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
693 sub _substitute_tables {
694 my ( $letter, $tables ) = @_;
695 while ( my ($table, $param) = each %$tables ) {
698 my $ref = ref $param;
701 if ($ref && $ref eq 'HASH') {
705 my $sth = _parseletter_sth($table);
707 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
710 $sth->execute( $ref ? @$param : $param );
712 $values = $sth->fetchrow_hashref;
716 _parseletter ( $letter, $table, $values );
720 sub _parseletter_sth {
724 carp "ERROR: _parseletter_sth() called without argument (table)";
727 # NOTE: we used to check whether we had a statement handle cached in
728 # a %handles module-level variable. This was a dumb move and
729 # broke things for the rest of us. prepare_cached is a better
730 # way to cache statement handles anyway.
732 ($table eq 'accountlines' ) ? "SELECT * FROM $table WHERE accountlines_id = ?" :
733 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
734 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
735 ($table eq 'credits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
736 ($table eq 'debits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
737 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
738 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
739 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE issue_id = ?" :
740 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
741 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
742 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
743 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
744 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
745 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
746 ($table eq 'aqbasket' ) ? "SELECT * FROM $table WHERE basketno = ?" :
747 ($table eq 'illrequests' ) ? "SELECT * FROM $table WHERE illrequest_id = ?" :
748 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
749 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
750 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
751 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
752 ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" :
753 ($table eq 'additional_contents' || $table eq 'opac_news') ? "SELECT * FROM additional_contents WHERE idnew = ?" :
756 warn "ERROR: No _parseletter_sth query for table '$table'";
757 return; # nothing to get
759 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
760 warn "ERROR: Failed to prepare query: '$query'";
763 return $sth; # now cache is populated for that $table
766 =head2 _parseletter($letter, $table, $values)
769 - $letter : a hash to letter fields (title & content useful)
770 - $table : the Koha table to parse.
771 - $values_in : table record hashref
772 parse all fields from a table, and replace values in title & content with the appropriate value
773 (not exported sub, used only internally)
778 my ( $letter, $table, $values_in ) = @_;
780 # Work on a local copy of $values_in (passed by reference) to avoid side effects
781 # in callers ( by changing / formatting values )
782 my $values = $values_in ? { %$values_in } : {};
784 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
785 $values->{'dateexpiry'} = output_pref({ dt => dt_from_string( $values->{'dateexpiry'} ), dateonly => 1 });
788 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
789 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
792 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
793 my $todaysdate = output_pref( dt_from_string() );
794 $letter->{content} =~ s/<<today>>/$todaysdate/go;
797 while ( my ($field, $val) = each %$values ) {
798 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
799 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
800 #Therefore adding the test on biblio. This includes biblioitems,
801 #but excludes items. Removed unneeded global and lookahead.
803 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
804 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
805 $val = $av->count ? $av->next->lib : '';
809 my $replacedby = defined ($val) ? $val : '';
811 and not $replacedby =~ m|9999-12-31|
812 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
814 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
815 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
816 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
818 for my $letter_field ( qw( title content ) ) {
819 my $filter_string_used = q{};
820 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
821 # We overwrite $dateonly if the filter exists and we have a time in the datetime
822 $filter_string_used = $1 || q{};
823 $dateonly = $1 unless $dateonly;
825 my $replacedby_date = eval {
826 output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
829 if ( $letter->{ $letter_field } ) {
830 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
831 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
835 # Other fields replacement
837 for my $letter_field ( qw( title content ) ) {
838 if ( $letter->{ $letter_field } ) {
839 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
840 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
846 if ($table eq 'borrowers' && $letter->{content}) {
847 my $patron = Koha::Patrons->find( $values->{borrowernumber} );
849 my $attributes = $patron->extended_attributes;
851 while ( my $attribute = $attributes->next ) {
852 my $code = $attribute->code;
853 my $val = $attribute->description; # FIXME - we always display intranet description here!
854 $val =~ s/\p{P}(?=$)//g if $val;
855 next unless $val gt '';
857 push @{ $attr{$code} }, $val;
859 while ( my ($code, $val_ar) = each %attr ) {
860 my $replacefield = "<<borrower-attribute:$code>>";
861 my $replacedby = join ',', @$val_ar;
862 $letter->{content} =~ s/$replacefield/$replacedby/g;
871 my $success = EnqueueLetter( { letter => $letter,
872 borrowernumber => '12', message_transport_type => 'email' } )
874 Places a letter in the message_queue database table, which will
875 eventually get processed (sent) by the process_message_queue.pl
876 cronjob when it calls SendQueuedMessages.
878 Return message_id on success
881 * letter - required; A letter hashref as returned from GetPreparedLetter
882 * message_transport_type - required; One of the available mtts
883 * borrowernumber - optional if 'to_address' is passed; The borrowernumber of the patron we enqueuing the notice for
884 * to_address - optional if 'borrowernumber' is passed; The destination email address for the notice (defaults to patron->notice_email_address)
885 * from_address - optional; The from address for the notice, defaults to patron->library->from_email_address
886 * reply_address - optional; The reply address for the notice, defaults to patron->library->reply_to
891 my $params = shift or return;
893 return unless exists $params->{'letter'};
894 # return unless exists $params->{'borrowernumber'};
895 return unless exists $params->{'message_transport_type'};
897 my $content = $params->{letter}->{content};
898 $content =~ s/\s+//g if(defined $content);
899 if ( not defined $content or $content eq '' ) {
900 Koha::Logger->get->info("Trying to add an empty message to the message queue");
904 # If we have any attachments we should encode then into the body.
905 if ( $params->{'attachments'} ) {
906 $params->{'letter'} = _add_attachments(
907 { letter => $params->{'letter'},
908 attachments => $params->{'attachments'},
913 my $dbh = C4::Context->dbh();
914 my $statement = << 'ENDSQL';
915 INSERT INTO message_queue
916 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, reply_address, content_type, failure_code )
918 ( ?, ?, ?, ?, ?, ?, ?, CAST(NOW() AS DATETIME), ?, ?, ?, ?, ? )
921 my $sth = $dbh->prepare($statement);
922 my $result = $sth->execute(
923 $params->{'borrowernumber'}, # borrowernumber
924 $params->{'letter'}->{'title'}, # subject
925 $params->{'letter'}->{'content'}, # content
926 $params->{'letter'}->{'metadata'} || '', # metadata
927 $params->{'letter'}->{'code'} || '', # letter_code
928 $params->{'message_transport_type'}, # message_transport_type
930 $params->{'to_address'}, # to_address
931 $params->{'from_address'}, # from_address
932 $params->{'reply_address'}, # reply_address
933 $params->{'letter'}->{'content-type'}, # content_type
934 $params->{'failure_code'} || '', # failure_code
936 return $dbh->last_insert_id(undef,undef,'message_queue', undef);
939 =head2 SendQueuedMessages ([$hashref])
941 my $sent = SendQueuedMessages({
942 letter_code => $letter_code,
943 borrowernumber => $who_letter_is_for,
949 Sends all of the 'pending' items in the message queue, unless
950 parameters are passed.
952 The letter_code, borrowernumber and limit parameters are used
953 to build a parameter set for _get_unsent_messages, thus limiting
954 which pending messages will be processed. They are all optional.
956 The verbose parameter can be used to generate debugging output.
959 Returns number of messages sent.
963 sub SendQueuedMessages {
966 my $which_unsent_messages = {
967 'message_id' => $params->{'message_id'},
968 'limit' => $params->{'limit'} // 0,
969 'borrowernumber' => $params->{'borrowernumber'} // q{},
970 'letter_code' => $params->{'letter_code'} // q{},
971 'type' => $params->{'type'} // q{},
973 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
974 MESSAGE: foreach my $message ( @$unsent_messages ) {
975 my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
976 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
977 $message_object->make_column_dirty('status');
978 return unless $message_object->store;
980 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
981 warn sprintf( 'sending %s message to patron: %s',
982 $message->{'message_transport_type'},
983 $message->{'borrowernumber'} || 'Admin' )
984 if $params->{'verbose'};
985 # This is just begging for subclassing
986 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
987 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
988 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
990 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
991 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
992 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
993 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
994 unless ( $sms_provider ) {
995 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'};
996 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
999 unless ( $patron->smsalertnumber ) {
1000 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1001 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1004 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1005 $message->{to_address} .= '@' . $sms_provider->domain();
1007 # Check for possible from_address override
1008 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1009 if ($from_address && $message->{from_address} ne $from_address) {
1010 $message->{from_address} = $from_address;
1011 _update_message_from_address($message->{'message_id'}, $message->{from_address});
1014 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1015 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1017 _send_message_by_sms( $message );
1021 return scalar( @$unsent_messages );
1024 =head2 GetRSSMessages
1026 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1028 returns a listref of all queued RSS messages for a particular person.
1032 sub GetRSSMessages {
1035 return unless $params;
1036 return unless ref $params;
1037 return unless $params->{'borrowernumber'};
1039 return _get_unsent_messages( { message_transport_type => 'rss',
1040 limit => $params->{'limit'},
1041 borrowernumber => $params->{'borrowernumber'}, } );
1044 =head2 GetPrintMessages
1046 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1048 Returns a arrayref of all queued print messages (optionally, for a particular
1053 sub GetPrintMessages {
1054 my $params = shift || {};
1056 return _get_unsent_messages( { message_transport_type => 'print',
1057 borrowernumber => $params->{'borrowernumber'},
1061 =head2 GetQueuedMessages ([$hashref])
1063 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1065 Fetches a list of messages from the message queue optionally filtered by borrowernumber
1066 and limited to specified limit.
1068 Return is an arrayref of hashes, each has represents a message in the message queue.
1072 sub GetQueuedMessages {
1075 my $dbh = C4::Context->dbh();
1076 my $statement = << 'ENDSQL';
1077 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued, updated_on, failure_code
1083 if ( exists $params->{'borrowernumber'} ) {
1084 push @whereclauses, ' borrowernumber = ? ';
1085 push @query_params, $params->{'borrowernumber'};
1088 if ( @whereclauses ) {
1089 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1092 if ( defined $params->{'limit'} ) {
1093 $statement .= ' LIMIT ? ';
1094 push @query_params, $params->{'limit'};
1097 my $sth = $dbh->prepare( $statement );
1098 my $result = $sth->execute( @query_params );
1099 return $sth->fetchall_arrayref({});
1102 =head2 GetMessageTransportTypes
1104 my @mtt = GetMessageTransportTypes();
1106 returns an arrayref of transport types
1110 sub GetMessageTransportTypes {
1111 my $dbh = C4::Context->dbh();
1112 my $mtts = $dbh->selectcol_arrayref("
1113 SELECT message_transport_type
1114 FROM message_transport_types
1115 ORDER BY message_transport_type
1122 my $message = C4::Letters::Message($message_id);
1127 my ( $message_id ) = @_;
1128 return unless $message_id;
1129 my $dbh = C4::Context->dbh;
1130 return $dbh->selectrow_hashref(q|
1131 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
1133 WHERE message_id = ?
1134 |, {}, $message_id );
1137 =head2 ResendMessage
1139 Attempt to resend a message which has failed previously.
1141 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1143 Updates the message to 'pending' status so that
1144 it will be resent later on.
1146 returns 1 on success, 0 on failure, undef if no message was found
1151 my $message_id = shift;
1152 return unless $message_id;
1154 my $message = GetMessage( $message_id );
1155 return unless $message;
1157 if ( $message->{status} ne 'pending' ) {
1158 $rv = C4::Letters::_set_message_status({
1159 message_id => $message_id,
1160 status => 'pending',
1162 $rv = $rv > 0? 1: 0;
1163 # Clear destination email address to force address update
1164 _update_message_to_address( $message_id, undef ) if $rv &&
1165 $message->{message_transport_type} eq 'email';
1170 =head2 _add_attachements
1172 _add_attachments({ letter => $letter, attachments => $attachments });
1175 letter - the standard letter hashref
1176 attachments - listref of attachments. each attachment is a hashref of:
1177 type - the mime type, like 'text/plain'
1178 content - the actual attachment
1179 filename - the name of the attachment.
1181 returns your letter object, with the content updated.
1182 This routine picks the I<content> of I<letter> and generates a MIME
1183 email, attaching the passed I<attachments> using Koha::Email. The
1184 content is replaced by the string representation of the MIME object,
1185 and the content-type is updated for later handling.
1189 sub _add_attachments {
1192 my $letter = $params->{letter};
1193 my $attachments = $params->{attachments};
1194 return $letter unless @$attachments;
1196 my $message = Koha::Email->new;
1198 if ( $letter->{is_html} ) {
1199 $message->html_body( _wrap_html( $letter->{content}, $letter->{title} ) );
1202 $message->text_body( $letter->{content} );
1205 foreach my $attachment ( @$attachments ) {
1207 Encode::encode( "UTF-8", $attachment->{content} ),
1208 content_type => $attachment->{type} || 'application/octet-stream',
1209 name => $attachment->{filename},
1210 disposition => 'attachment',
1214 $letter->{'content-type'} = SERIALIZED_EMAIL_CONTENT_TYPE;
1215 $letter->{content} = $message->as_string;
1221 =head2 _get_unsent_messages
1223 This function's parameter hash reference takes the following
1224 optional named parameters:
1225 message_transport_type: method of message sending (e.g. email, sms, etc.)
1226 borrowernumber : who the message is to be sent
1227 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1228 message_id : the message_id of the message. In that case the sub will return only 1 result
1229 limit : maximum number of messages to send
1231 This function returns an array of matching hash referenced rows from
1232 message_queue with some borrower information added.
1236 sub _get_unsent_messages {
1239 my $dbh = C4::Context->dbh();
1241 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
1242 FROM message_queue mq
1243 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1247 my @query_params = ('pending');
1248 if ( ref $params ) {
1249 if ( $params->{'message_transport_type'} ) {
1250 $statement .= ' AND mq.message_transport_type = ? ';
1251 push @query_params, $params->{'message_transport_type'};
1253 if ( $params->{'borrowernumber'} ) {
1254 $statement .= ' AND mq.borrowernumber = ? ';
1255 push @query_params, $params->{'borrowernumber'};
1257 if ( $params->{'letter_code'} ) {
1258 $statement .= ' AND mq.letter_code = ? ';
1259 push @query_params, $params->{'letter_code'};
1261 if ( $params->{'type'} ) {
1262 $statement .= ' AND message_transport_type = ? ';
1263 push @query_params, $params->{'type'};
1265 if ( $params->{message_id} ) {
1266 $statement .= ' AND message_id = ?';
1267 push @query_params, $params->{message_id};
1269 if ( $params->{'limit'} ) {
1270 $statement .= ' limit ? ';
1271 push @query_params, $params->{'limit'};
1275 my $sth = $dbh->prepare( $statement );
1276 my $result = $sth->execute( @query_params );
1277 return $sth->fetchall_arrayref({});
1280 sub _send_message_by_email {
1281 my $message = shift or return;
1282 my ($username, $password, $method) = @_;
1284 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1285 my $to_address = $message->{'to_address'};
1286 unless ($to_address) {
1288 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1289 _set_message_status(
1291 message_id => $message->{'message_id'},
1293 failure_code => 'INVALID_BORNUMBER'
1298 $to_address = $patron->notice_email_address;
1299 unless ($to_address) {
1300 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1301 # warning too verbose for this more common case?
1302 _set_message_status(
1304 message_id => $message->{'message_id'},
1306 failure_code => 'NO_EMAIL'
1313 my $subject = $message->{'subject'};
1315 my $content = $message->{'content'};
1316 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1317 my $is_html = $content_type =~ m/html/io;
1319 my $branch_email = undef;
1320 my $branch_replyto = undef;
1321 my $branch_returnpath = undef;
1325 $library = $patron->library;
1326 $branch_email = $library->from_email_address;
1327 $branch_replyto = $library->branchreplyto;
1328 $branch_returnpath = $library->branchreturnpath;
1331 # NOTE: Patron may not be defined above so branch_email may be undefined still
1332 # so we need to fallback to KohaAdminEmailAddress as a last resort.
1334 $message->{'from_address'}
1336 || C4::Context->preference('KohaAdminEmailAddress');
1337 if( !$from_address ) {
1338 _set_message_status(
1340 message_id => $message->{'message_id'},
1342 failure_code => 'NO_FROM',
1354 C4::Context->preference('NoticeBcc')
1355 ? ( bcc => C4::Context->preference('NoticeBcc') )
1358 from => $from_address,
1359 reply_to => $message->{'reply_address'} || $branch_replyto,
1360 sender => $branch_returnpath,
1361 subject => "" . $message->{subject}
1364 if ( $message->{'content_type'} && $message->{'content_type'} eq SERIALIZED_EMAIL_CONTENT_TYPE ) {
1366 # The message has been previously composed as a valid MIME object
1367 # and serialized as a string on the DB
1368 $email = Koha::Email->new_from_string($content);
1369 $email->create($params);
1371 $email = Koha::Email->create($params);
1373 $email->html_body( _wrap_html( $content, $subject ) );
1375 $email->text_body($content);
1380 if ( ref($_) eq 'Koha::Exceptions::BadParameter' ) {
1381 _set_message_status(
1383 message_id => $message->{'message_id'},
1385 failure_code => "INVALID_EMAIL:".$_->parameter
1389 _set_message_status(
1391 message_id => $message->{'message_id'},
1393 failure_code => 'UNKNOWN_ERROR'
1399 return unless $email;
1403 $smtp_server = $library->smtp_server;
1406 $smtp_server = Koha::SMTP::Servers->get_default;
1412 sasl_username => $username,
1413 sasl_password => $password,
1418 # if initial message address was empty, coming here means that a to address was found and
1419 # queue should be updated; same if to address was overriden by Koha::Email->create
1420 _update_message_to_address( $message->{'message_id'}, $email->email->header('To') )
1421 if !$message->{to_address}
1422 || $message->{to_address} ne $email->email->header('To');
1425 $email->send_or_die({ transport => $smtp_server->transport });
1427 _set_message_status(
1429 message_id => $message->{'message_id'},
1437 _set_message_status(
1439 message_id => $message->{'message_id'},
1441 failure_code => 'SENDMAIL'
1445 carp "$Mail::Sendmail::error";
1451 my ($content, $title) = @_;
1453 my $css = C4::Context->preference("NoticeCSS") || '';
1454 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1456 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1457 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1458 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1460 <title>$title</title>
1461 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1472 my ( $message ) = @_;
1473 my $dbh = C4::Context->dbh;
1474 my $count = $dbh->selectrow_array(q|
1477 WHERE message_transport_type = ?
1478 AND borrowernumber = ?
1480 AND CAST(updated_on AS date) = CAST(NOW() AS date)
1483 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1487 sub _send_message_by_sms {
1488 my $message = shift or return;
1489 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1491 unless ( $patron and $patron->smsalertnumber ) {
1492 _set_message_status( { message_id => $message->{'message_id'},
1494 failure_code => 'MISSING_SMS' } );
1498 if ( _is_duplicate( $message ) ) {
1499 _set_message_status(
1501 message_id => $message->{'message_id'},
1503 failure_code => 'DUPLICATE_MESSAGE'
1509 my $success = C4::SMS->send_sms(
1511 destination => $patron->smsalertnumber,
1512 message => $message->{'content'},
1517 _set_message_status(
1519 message_id => $message->{'message_id'},
1526 _set_message_status(
1528 message_id => $message->{'message_id'},
1530 failure_code => 'NO_NOTES'
1538 sub _update_message_to_address {
1540 my $dbh = C4::Context->dbh();
1541 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1544 sub _update_message_from_address {
1545 my ($message_id, $from_address) = @_;
1546 my $dbh = C4::Context->dbh();
1547 $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1550 sub _set_message_status {
1551 my $params = shift or return;
1553 foreach my $required_parameter ( qw( message_id status ) ) {
1554 return unless exists $params->{ $required_parameter };
1557 my $dbh = C4::Context->dbh();
1558 my $statement = 'UPDATE message_queue SET status= ?, failure_code= ? WHERE message_id = ?';
1559 my $sth = $dbh->prepare( $statement );
1560 my $result = $sth->execute( $params->{'status'},
1561 $params->{'failure_code'} || '',
1562 $params->{'message_id'} );
1567 my ( $params ) = @_;
1569 my $content = $params->{content};
1570 my $tables = $params->{tables};
1571 my $loops = $params->{loops};
1572 my $substitute = $params->{substitute} || {};
1573 my $lang = defined($params->{lang}) && $params->{lang} ne 'default' ? $params->{lang} : 'en';
1574 my ($theme, $availablethemes);
1576 my $htdocs = C4::Context->config('intrahtdocs');
1577 ($theme, $lang, $availablethemes)= C4::Templates::availablethemes( $htdocs, 'about.tt', 'intranet', $lang);
1579 foreach (@$availablethemes) {
1580 push @includes, "$htdocs/$_/$lang/includes";
1581 push @includes, "$htdocs/$_/en/includes" unless $lang eq 'en';
1584 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1585 my $template = Template->new(
1589 PLUGIN_BASE => 'Koha::Template::Plugin',
1590 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1591 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1592 INCLUDE_PATH => \@includes,
1594 ENCODING => 'UTF-8',
1596 ) or die Template->error();
1598 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
1600 $content = add_tt_filters( $content );
1601 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1604 $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1609 sub _get_tt_params {
1610 my ($tables, $is_a_loop) = @_;
1616 article_requests => {
1617 module => 'Koha::ArticleRequests',
1618 singular => 'article_request',
1619 plural => 'article_requests',
1623 module => 'Koha::Acquisition::Baskets',
1624 singular => 'basket',
1625 plural => 'baskets',
1629 module => 'Koha::Biblios',
1630 singular => 'biblio',
1631 plural => 'biblios',
1632 pk => 'biblionumber',
1635 module => 'Koha::Biblioitems',
1636 singular => 'biblioitem',
1637 plural => 'biblioitems',
1638 pk => 'biblioitemnumber',
1641 module => 'Koha::Patrons',
1642 singular => 'borrower',
1643 plural => 'borrowers',
1644 pk => 'borrowernumber',
1647 module => 'Koha::Libraries',
1648 singular => 'branch',
1649 plural => 'branches',
1653 module => 'Koha::Account::Lines',
1654 singular => 'credit',
1655 plural => 'credits',
1656 pk => 'accountlines_id',
1659 module => 'Koha::Account::Lines',
1660 singular => 'debit',
1662 pk => 'accountlines_id',
1665 module => 'Koha::Items',
1670 additional_contents => {
1671 module => 'Koha::AdditionalContents',
1672 singular => 'additional_content',
1673 plural => 'additional_contents',
1677 module => 'Koha::AdditionalContents',
1683 module => 'Koha::Acquisition::Orders',
1684 singular => 'order',
1686 pk => 'ordernumber',
1689 module => 'Koha::Holds',
1695 module => 'Koha::Serials',
1696 singular => 'serial',
1697 plural => 'serials',
1701 module => 'Koha::Subscriptions',
1702 singular => 'subscription',
1703 plural => 'subscriptions',
1704 pk => 'subscriptionid',
1707 module => 'Koha::Suggestions',
1708 singular => 'suggestion',
1709 plural => 'suggestions',
1710 pk => 'suggestionid',
1713 module => 'Koha::Checkouts',
1714 singular => 'checkout',
1715 plural => 'checkouts',
1719 module => 'Koha::Old::Checkouts',
1720 singular => 'old_checkout',
1721 plural => 'old_checkouts',
1725 module => 'Koha::Checkouts',
1726 singular => 'overdue',
1727 plural => 'overdues',
1730 borrower_modifications => {
1731 module => 'Koha::Patron::Modifications',
1732 singular => 'patron_modification',
1733 plural => 'patron_modifications',
1734 fk => 'verification_token',
1737 module => 'Koha::Illrequests',
1738 singular => 'illrequest',
1739 plural => 'illrequests',
1740 pk => 'illrequest_id'
1744 foreach my $table ( keys %$tables ) {
1745 next unless $config->{$table};
1747 my $ref = ref( $tables->{$table} ) || q{};
1748 my $module = $config->{$table}->{module};
1750 if ( can_load( modules => { $module => undef } ) ) {
1751 my $pk = $config->{$table}->{pk};
1752 my $fk = $config->{$table}->{fk};
1755 my $values = $tables->{$table} || [];
1756 unless ( ref( $values ) eq 'ARRAY' ) {
1757 croak "ERROR processing table $table. Wrong API call.";
1759 my $key = $pk ? $pk : $fk;
1760 # $key does not come from user input
1761 my $objects = $module->search(
1762 { $key => $values },
1764 # We want to retrieve the data in the same order
1766 # field is a MySQLism, but they are no other way to do it
1767 # To be generic we could do it in perl, but we will need to fetch
1768 # all the data then order them
1769 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1772 $params->{ $config->{$table}->{plural} } = $objects;
1774 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1775 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1777 if ( $fk ) { # Using a foreign key for lookup
1778 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1780 foreach my $key ( @$fk ) {
1781 $search->{$key} = $id->{$key};
1783 $object = $module->search( $search )->last();
1784 } else { # Foreign key is single column
1785 $object = $module->search( { $fk => $id } )->last();
1787 } else { # using the table's primary key for lookup
1788 $object = $module->find($id);
1790 $params->{ $config->{$table}->{singular} } = $object;
1792 else { # $ref eq 'ARRAY'
1794 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1795 $object = $module->search( { $pk => $tables->{$table} } )->last();
1797 else { # Params are mutliple foreign keys
1798 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1800 $params->{ $config->{$table}->{singular} } = $object;
1804 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1808 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1813 =head3 add_tt_filters
1815 $content = add_tt_filters( $content );
1817 Add TT filters to some specific fields if needed.
1819 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1823 sub add_tt_filters {
1824 my ( $content ) = @_;
1825 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1826 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1830 =head2 get_item_content
1832 my $item = Koha::Items->find(...)->unblessed;
1833 my @item_content_fields = qw( date_due title barcode author itemnumber );
1834 my $item_content = C4::Letters::get_item_content({
1836 item_content_fields => \@item_content_fields
1839 This function generates a tab-separated list of values for the passed item. Dates
1840 are formatted following the current setup.
1844 sub get_item_content {
1845 my ( $params ) = @_;
1846 my $item = $params->{item};
1847 my $dateonly = $params->{dateonly} || 0;
1848 my $item_content_fields = $params->{item_content_fields} || [];
1850 return unless $item;
1852 my @item_info = map {
1856 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1860 } @$item_content_fields;
1861 return join( "\t", @item_info ) . "\n";