Bug 7317: Interlibrary loans framework for Koha.
[koha.git] / Koha / Illrequest.pm
1 package Koha::Illrequest;
2
3 # Copyright PTFS Europe 2016
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 3 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
15 # details.
16 #
17 # You should have received a copy of the GNU General Public License along with
18 # Koha; if not, write to the Free Software Foundation, Inc., 51 Franklin
19 # Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21 # use Modern::Perl;
22
23 use Clone 'clone';
24 use File::Basename qw/basename/;
25 use Koha::Database;
26 use Koha::Email;
27 use Koha::Illrequest;
28 use Koha::Illrequestattributes;
29 use Koha::Patron;
30 use Mail::Sendmail;
31 use Try::Tiny;
32
33 use base qw(Koha::Object);
34
35 =head1 NAME
36
37 Koha::Illrequest - Koha Illrequest Object class
38
39 =head1 (Re)Design
40
41 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
42 of related Illrequestattributes.
43
44 The former encapsulates the basic necessary information that any ILL requires
45 to be usable in Koha.  The latter is a set of additional properties used by
46 one of the backends.
47
48 The former subsumes the legacy "Status" object.  The latter remains
49 encapsulated in the "Record" object.
50
51 TODO:
52
53 - Anything invoking the ->status method; annotated with:
54   + # Old use of ->status !
55
56 =head1 API
57
58 =head2 Backend API Response Principles
59
60 All methods should return a hashref in the following format:
61
62 =item * error
63
64 This should be set to 1 if an error was encountered.
65
66 =item * status
67
68 The status should be a string from the list of statuses detailed below.
69
70 =item * message
71
72 The message is a free text field that can be passed on to the end user.
73
74 =item * value
75
76 The value returned by the method.
77
78 =over
79
80 =head2 Interface Status Messages
81
82 =over
83
84 =item * branch_address_incomplete
85
86 An interface request has determined branch address details are incomplete.
87
88 =item * cancel_success
89
90 The interface's cancel_request method was successful in cancelling the
91 Illrequest using the API.
92
93 =item * cancel_fail
94
95 The interface's cancel_request method failed to cancel the Illrequest using
96 the API.
97
98 =item * unavailable
99
100 The interface's request method returned saying that the desired item is not
101 available for request.
102
103 =head2 Class Methods
104
105 =cut
106
107 =head3 type
108
109 =cut
110
111 sub _type {
112     return 'Illrequest';
113 }
114
115 sub illrequestattributes {
116     my ( $self ) = @_;
117     return Koha::Illrequestattributes->_new_from_dbic(
118         scalar $self->_result->illrequestattributes
119     );
120 }
121
122 sub patron {
123     my ( $self ) = @_;
124     return Koha::Patron->_new_from_dbic(
125         scalar $self->_result->borrowernumber
126     );
127 }
128
129 sub load_backend {
130     my ( $self, $backend_id ) = @_;
131
132     my @raw = qw/Koha Illbackends/; # Base Path
133
134     my $backend_name = $backend_id || $self->backend;
135     $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
136     $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
137     require $location;
138     $self->{_my_backend} = $backend_class->new({ config => $self->_config });
139     return $self;
140 }
141
142 =head3 _backend
143
144     my $backend = $abstract->_backend($new_backend);
145     my $backend = $abstract->_backend;
146
147 Getter/Setter for our API object.
148
149 =cut
150
151 sub _backend {
152     my ( $self, $backend ) = @_;
153     $self->{_my_backend} = $backend if ( $backend );
154     # Dynamically load our backend object, as late as possible.
155     $self->load_backend unless ( $self->{_my_backend} );
156     return $self->{_my_backend};
157 }
158
159 =head3 _backend_capability
160
161     my $backend_capability_result = $self->_backend_capability($name, $args);
162
163 This is a helper method to invoke optional capabilities in the backend.  If
164 the capability named by $name is not supported, return 0, else invoke it,
165 passing $args along with the invocation, and return its return value.
166
167 NOTE: this module suffers from a confusion in termninology:
168
169 in _backend_capability, the notion of capability refers to an optional feature
170 that is implemented in core, but might not be supported by a given backend.
171
172 in capabilities & custom_capability, capability refers to entries in the
173 status_graph (after union between backend and core).
174
175 The easiest way to fix this would be to fix the terminology in
176 capabilities & custom_capability and their callers.
177
178 =cut
179
180 sub _backend_capability {
181     my ( $self, $name, $args ) = @_;
182     my $capability = 0;
183     try {
184         $capability = $self->_backend->capabilities($name);
185     } catch {
186         return 0;
187     };
188     if ( $capability ) {
189         return &{$capability}($args);
190     } else {
191         return 0;
192     }
193 }
194
195 =head3 _config
196
197     my $config = $abstract->_config($config);
198     my $config = $abstract->_config;
199
200 Getter/Setter for our config object.
201
202 =cut
203
204 sub _config {
205     my ( $self, $config ) = @_;
206     $self->{_my_config} = $config if ( $config );
207     # Load our config object, as late as possible.
208     unless ( $self->{_my_config} ) {
209         $self->{_my_config} = Koha::Illrequest::Config->new;
210     }
211     return $self->{_my_config};
212 }
213
214 =head3 metadata
215
216 =cut
217
218 sub metadata {
219     my ( $self ) = @_;
220     return $self->_backend->metadata($self);
221 }
222
223 =head3 _core_status_graph
224
225     my $core_status_graph = $illrequest->_core_status_graph;
226
227 Returns ILL module's default status graph.  A status graph defines the list of
228 available actions at any stage in the ILL workflow.  This is for instance used
229 by the perl script & template to generate the correct buttons to display to
230 the end user at any given point.
231
232 =cut
233
234 sub _core_status_graph {
235     my ( $self ) = @_;
236     return {
237         NEW => {
238             prev_actions => [ ],                           # Actions containing buttons
239                                                            # leading to this status
240             id             => 'NEW',                       # ID of this status
241             name           => 'New request',               # UI name of this status
242             ui_method_name => 'New request',               # UI name of method leading
243                                                            # to this status
244             method         => 'create',                    # method to this status
245             next_actions   => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
246                                                            # requests with this status
247             ui_method_icon => 'fa-plus',                   # UI Style class
248         },
249         REQ => {
250             prev_actions   => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
251             id             => 'REQ',
252             name           => 'Requested',
253             ui_method_name => 'Confirm request',
254             method         => 'confirm',
255             next_actions   => [ 'REQREV', 'COMP' ],
256             ui_method_icon => 'fa-check',
257         },
258         GENREQ => {
259             prev_actions   => [ 'NEW', 'REQREV' ],
260             id             => 'GENREQ',
261             name           => 'Requested from partners',
262             ui_method_name => 'Place request with partners',
263             method         => 'generic_confirm',
264             next_actions   => [ 'COMP' ],
265             ui_method_icon => 'fa-send-o',
266         },
267         REQREV => {
268             prev_actions   => [ 'REQ' ],
269             id             => 'REQREV',
270             name           => 'Request reverted',
271             ui_method_name => 'Revert Request',
272             method         => 'cancel',
273             next_actions   => [ 'REQ', 'GENREQ', 'KILL' ],
274             ui_method_icon => 'fa-times',
275         },
276         QUEUED => {
277             prev_actions   => [ ],
278             id             => 'QUEUED',
279             name           => 'Queued request',
280             ui_method_name => 0,
281             method         => 0,
282             next_actions   => [ 'REQ', 'KILL' ],
283             ui_method_icon => 0,
284         },
285         CANCREQ => {
286             prev_actions   => [ 'NEW' ],
287             id             => 'CANCREQ',
288             name           => 'Cancellation requested',
289             ui_method_name => 0,
290             method         => 0,
291             next_actions   => [ 'KILL', 'REQ' ],
292             ui_method_icon => 0,
293         },
294         COMP => {
295             prev_actions   => [ 'REQ' ],
296             id             => 'COMP',
297             name           => 'Completed',
298             ui_method_name => 'Mark completed',
299             method         => 'mark_completed',
300             next_actions   => [ ],
301             ui_method_icon => 'fa-check',
302         },
303         KILL => {
304             prev_actions   => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
305             id             => 'KILL',
306             name           => 0,
307             ui_method_name => 'Delete request',
308             method         => 'delete',
309             next_actions   => [ ],
310             ui_method_icon => 'fa-trash',
311         },
312     };
313 }
314
315 =head3 _core_status_graph
316
317     my $status_graph = $illrequest->_core_status_graph($origin, $new_graph);
318
319 Return a new status_graph, the result of merging $origin & new_graph.  This is
320 operation is a union over the sets defied by the two graphs.
321
322 Each entry in $new_graph is added to $origin.  We do not provide a syntax for
323 'subtraction' of entries from $origin.
324
325 Whilst it is not intended that this works, you can override entries in $origin
326 with entries with the same key in $new_graph.  This can lead to problematic
327 behaviour when $new_graph adds an entry, which modifies a dependent entry in
328 $origin, only for the entry in $origin to be replaced later with a new entry
329 from $new_graph.
330
331 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
332 i.e. each of the graphs need to be correct at the outset of the operation.
333
334 =cut
335
336 sub _status_graph_union {
337     my ( $self, $core_status_graph, $backend_status_graph ) = @_;
338     # Create new status graph with:
339     # - all core_status_graph
340     # - for-each each backend_status_graph
341     #   + add to new status graph
342     #   + for each core prev_action:
343     #     * locate core_status
344     #     * update next_actions with additional next action.
345     #   + for each core next_action:
346     #     * locate core_status
347     #     * update prev_actions with additional prev action
348
349     my @core_status_ids = keys %{$core_status_graph};
350     my $status_graph = clone($core_status_graph);
351
352     foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
353         $backend_status = $backend_status_graph->{$backend_status_key};
354         # Add to new status graph
355         $status_graph->{$backend_status_key} = $backend_status;
356         # Update all core methods' next_actions.
357         foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
358             if ( grep $prev_action, @core_status_ids ) {
359                 my @next_actions =
360                      @{$status_graph->{$prev_action}->{next_actions}};
361                 push @next_actions, $backend_status_key;
362                 $status_graph->{$prev_action}->{next_actions}
363                     = \@next_actions;
364             }
365         }
366         # Update all core methods' prev_actions
367         foreach my $next_action ( @{$backend_status->{next_actions}} ) {
368             if ( grep $next_action, @core_status_ids ) {
369                 my @prev_actions =
370                      @{$status_graph->{$next_action}->{prev_actions}};
371                 push @prev_actions, $backend_status_key;
372                 $status_graph->{$next_action}->{prev_actions}
373                     = \@prev_actions;
374             }
375         }
376     }
377
378     return $status_graph;
379 }
380
381 ### Core API methods
382
383 =head3 capabilities
384
385     my $capabilities = $illrequest->capabilities;
386
387 Return a hashref mapping methods to operation names supported by the queried
388 backend.
389
390 Example return value:
391
392     { create => "Create Request", confirm => "Progress Request" }
393
394 NOTE: this module suffers from a confusion in termninology:
395
396 in _backend_capability, the notion of capability refers to an optional feature
397 that is implemented in core, but might not be supported by a given backend.
398
399 in capabilities & custom_capability, capability refers to entries in the
400 status_graph (after union between backend and core).
401
402 The easiest way to fix this would be to fix the terminology in
403 capabilities & custom_capability and their callers.
404
405 =cut
406
407 sub capabilities {
408     my ( $self, $status ) = @_;
409     # Generate up to date status_graph
410     my $status_graph = $self->_status_graph_union(
411         $self->_core_status_graph,
412         $self->_backend->status_graph({
413             request => $self,
414             other   => {}
415         })
416     );
417     # Extract available actions from graph.
418     return $status_graph->{$status} if $status;
419     # Or return entire graph.
420     return $status_graph;
421 }
422
423 =head3 custom_capability
424
425 Return the result of invoking $CANDIDATE on this request's backend with
426 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
427
428 NOTE: this module suffers from a confusion in termninology:
429
430 in _backend_capability, the notion of capability refers to an optional feature
431 that is implemented in core, but might not be supported by a given backend.
432
433 in capabilities & custom_capability, capability refers to entries in the
434 status_graph (after union between backend and core).
435
436 The easiest way to fix this would be to fix the terminology in
437 capabilities & custom_capability and their callers.
438
439 =cut
440
441 sub custom_capability {
442     my ( $self, $candidate, $params ) = @_;
443     foreach my $capability ( values %{$self->capabilities} ) {
444         if ( $candidate eq $capability->{method} ) {
445             my $response =
446                 $self->_backend->$candidate({
447                     request    => $self,
448                     other      => $params,
449                 });
450             return $self->expandTemplate($response);
451         }
452     }
453     return 0;
454 }
455
456 sub available_backends {
457     my ( $self ) = @_;
458     my $backend_dir = $self->_config->backend_dir;
459     my @backends = ();
460     @backends = <$backend_dir/*> if ( $backend_dir );
461     @backends = map { basename($_) } @backends;
462     return \@backends;
463 }
464
465 sub available_actions {
466     my ( $self ) = @_;
467     my $current_action = $self->capabilities($self->status);
468     my @available_actions = map { $self->capabilities($_) }
469         @{$current_action->{next_actions}};
470     return \@available_actions;
471 }
472
473 sub mark_completed {
474     my ( $self ) = @_;
475     $self->status('COMP')->store;
476     return {
477         error   => 0,
478         status  => '',
479         message => '',
480         method  => 'mark_completed',
481         stage   => 'commit',
482         next    => 'illview',
483     };
484 }
485
486 sub backend_confirm {
487     my ( $self, $params ) = @_;
488
489     # The backend handles setting of mandatory fields in the commit stage:
490     # - orderid
491     # - accessurl, cost (if available).
492     my $response = $self->_backend->confirm({
493             request    => $self,
494             other      => $params,
495         });
496     return $self->expandTemplate($response);
497 }
498
499 sub backend_update_status {
500     my ( $self, $params ) = @_;
501     return $self->expandTemplate($self->_backend->update_status($params));
502 }
503
504 =head3 backend_cancel
505
506     my $ILLResponse = $illRequest->backend_cancel;
507
508 The standard interface method allowing for request cancellation.
509
510 =cut
511
512 sub backend_cancel {
513     my ( $self, $params ) = @_;
514
515     my $result = $self->_backend->cancel({
516         request => $self,
517         other => $params
518     });
519
520     return $self->expandTemplate($result);
521 }
522
523 =head3 backend_renew
524
525     my $renew_response = $illRequest->backend_renew;
526
527 The standard interface method allowing for request renewal queries.
528
529 =cut
530
531 sub backend_renew {
532     my ( $self ) = @_;
533     return $self->expandTemplate(
534         $self->_backend->renew({
535             request    => $self,
536         })
537     );
538 }
539
540 =head3 backend_create
541
542     my $create_response = $abstractILL->backend_create($params);
543
544 Return an array of Record objects created by querying our backend with
545 a Search query.
546
547 In the context of the other ILL methods, this is a special method: we only
548 pass it $params, as it does not yet have any other data associated with it.
549
550 =cut
551
552 sub backend_create {
553     my ( $self, $params ) = @_;
554
555     # Establish whether we need to do a generic copyright clearance.
556     if ( ( !$params->{stage} || $params->{stage} eq 'init' )
557              && C4::Context->preference("ILLModuleCopyrightClearance") ) {
558         return {
559             error   => 0,
560             status  => '',
561             message => '',
562             method  => 'create',
563             stage   => 'copyrightclearance',
564             value   => {
565                 backend => $self->_backend->name
566             }
567         };
568     } elsif ( $params->{stage} eq 'copyrightclearance' ) {
569         $params->{stage} = 'init';
570     }
571
572     # First perform API action, then...
573     my $args = {
574         request => $self,
575         other   => $params,
576     };
577     my $result = $self->_backend->create($args);
578
579     # ... simple case: we're not at 'commit' stage.
580     my $stage = $result->{stage};
581     return $self->expandTemplate($result)
582         unless ( 'commit' eq $stage );
583
584     # ... complex case: commit!
585
586     # Do we still have space for an ILL or should we queue?
587     my $permitted = $self->check_limits(
588         { patron => $self->patron }, { librarycode => $self->branchcode }
589     );
590
591     # Now augment our committed request.
592
593     $result->{permitted} = $permitted;             # Queue request?
594
595     # This involves...
596
597     # ...Updating status!
598     $self->status('QUEUED')->store unless ( $permitted );
599
600     return $self->expandTemplate($result);
601 }
602
603 =head3 expandTemplate
604
605     my $params = $abstract->expandTemplate($params);
606
607 Return a version of $PARAMS augmented with our required template path.
608
609 =cut
610
611 sub expandTemplate {
612     my ( $self, $params ) = @_;
613     my $backend = $self->_backend->name;
614     # Generate path to file to load
615     my $backend_dir = $self->_config->backend_dir;
616     my $backend_tmpl = join "/", $backend_dir, $backend;
617     my $intra_tmpl =  join "/", $backend_tmpl, "intra-includes",
618         $params->{method} . ".inc";
619     my $opac_tmpl =  join "/", $backend_tmpl, "opac-includes",
620         $params->{method} . ".inc";
621     # Set files to load
622     $params->{template} = $intra_tmpl;
623     $params->{opac_template} = $opac_tmpl;
624     return $params;
625 }
626
627 #### Abstract Imports
628
629 =head3 getLimits
630
631     my $limit_rules = $abstract->getLimits( {
632         type  => 'brw_cat' | 'branch',
633         value => $value
634     } );
635
636 Return the ILL limit rules for the supplied combination of type / value.
637
638 As the config may have no rules for this particular type / value combination,
639 or for the default, we must define fall-back values here.
640
641 =cut
642
643 sub getLimits {
644     my ( $self, $params ) = @_;
645     my $limits = $self->_config->getLimitRules($params->{type});
646
647     return $limits->{$params->{value}}
648         || $limits->{default}
649         || { count => -1, method => 'active' };
650 }
651
652 =head3 getPrefix
653
654     my $prefix = $abstract->getPrefix( {
655         brw_cat => $brw_cat,
656         branch  => $branch_code,
657     } );
658
659 Return the ILL prefix as defined by our $params: either per borrower category,
660 per branch or the default.
661
662 =cut
663
664 sub getPrefix {
665     my ( $self, $params ) = @_;
666     my $brn_prefixes = $self->_config->getPrefixes('branch');
667     my $brw_prefixes = $self->_config->getPrefixes('brw_cat');
668
669     return $brw_prefixes->{$params->{brw_cat}}
670         || $brn_prefixes->{$params->{branch}}
671         || $brw_prefixes->{default}
672         || "";                  # "the empty prefix"
673 }
674
675 #### Illrequests Imports
676
677 =head3 check_limits
678
679     my $ok = $illRequests->check_limits( {
680         borrower   => $borrower,
681         branchcode => 'branchcode' | undef,
682     } );
683
684 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
685 see whether we are still able to place ILLs.
686
687 LimitRules are derived from koha-conf.xml:
688  + default limit counts, and counting method
689  + branch specific limit counts & counting method
690  + borrower category specific limit counts & counting method
691  + err on the side of caution: a counting fail will cause fail, even if
692    the other counts passes.
693
694 =cut
695
696 sub check_limits {
697     my ( $self, $params ) = @_;
698     my $patron     = $params->{patron};
699     my $branchcode = $params->{librarycode} || $patron->branchcode;
700
701     # Establish maximum number of allowed requests
702     my ( $branch_rules, $brw_rules ) = (
703         $self->getLimits( {
704             type => 'branch',
705             value => $branchcode
706         } ),
707         $self->getLimits( {
708             type => 'brw_cat',
709             value => $patron->categorycode,
710         } ),
711     );
712     my ( $branch_limit, $brw_limit )
713         = ( $branch_rules->{count}, $brw_rules->{count} );
714     # Establish currently existing requests
715     my ( $branch_count, $brw_count ) = (
716         $self->_limit_counter(
717             $branch_rules->{method}, { branchcode => $branchcode }
718         ),
719         $self->_limit_counter(
720             $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
721         ),
722     );
723
724     # Compare and return
725     # A limit of -1 means no limit exists.
726     # We return blocked if either branch limit or brw limit is reached.
727     if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
728              || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
729         return 0;
730     } else {
731         return 1;
732     }
733 }
734
735 sub _limit_counter {
736     my ( $self, $method, $target ) = @_;
737
738     # Establish parameters of counts
739     my $resultset;
740     if ($method && $method eq 'annual') {
741         $resultset = Koha::Illrequests->search({
742             -and => [
743                 %{$target},
744                 \"YEAR(placed) = YEAR(NOW())"
745             ]
746         });
747     } else {                    # assume 'active'
748         # XXX: This status list is ugly. There should be a method in config
749         # to return these.
750         $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
751         $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
752     }
753
754     # Fetch counts
755     return $resultset->count;
756 }
757
758 =head3 requires_moderation
759
760     my $status = $illRequest->requires_moderation;
761
762 Return the name of the status if moderation by staff is required; or 0
763 otherwise.
764
765 =cut
766
767 sub requires_moderation {
768     my ( $self ) = @_;
769     my $require_moderation = {
770         'CANCREQ' => 'CANCREQ',
771     };
772     return $require_moderation->{$self->status};
773 }
774
775 =head3 generic_confirm
776
777     my $stage_summary = $illRequest->generic_confirm;
778
779 Handle the generic_confirm extended method.  The first stage involves creating
780 a template email for the end user to edit in the browser.  The second stage
781 attempts to submit the email.
782
783 =cut
784
785 sub generic_confirm {
786     my ( $self, $params ) = @_;
787     my $branch = Koha::Libraries->find($params->{current_branchcode})
788         || die "Invalid current branchcode. Are you logged in as the database user?";
789     if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
790         my $draft->{subject} = "ILL Request";
791         $draft->{body} = <<EOF;
792 Dear Sir/Madam,
793
794     We would like to request an interlibrary loan for a title matching the
795 following description:
796
797 EOF
798
799         my $details = $self->metadata;
800         while (my ($title, $value) = each %{$details}) {
801             $draft->{body} .= "  - " . $title . ": " . $value . "\n"
802                 if $value;
803         }
804         $draft->{body} .= <<EOF;
805
806 Please let us know if you are able to supply this to us.
807
808 Kind Regards
809
810 EOF
811
812         my @address = map { $branch->$_ }
813             qw/ branchname branchaddress1 branchaddress2 branchaddress3
814                 branchzip branchcity branchstate branchcountry branchphone
815                 branchemail /;
816         my $address = "";
817         foreach my $line ( @address ) {
818             $address .= $line . "\n" if $line;
819         }
820
821         $draft->{body} .= $address;
822
823         my $partners = Koha::Patrons->search({
824             categorycode => $self->_config->partner_code
825         });
826         return {
827             error   => 0,
828             status  => '',
829             message => '',
830             method  => 'generic_confirm',
831             stage   => 'draft',
832             value   => {
833                 draft    => $draft,
834                 partners => $partners,
835             }
836         };
837
838     } elsif ( 'draft' eq $params->{stage} ) {
839         # Create the to header
840         my $to = $params->{partners};
841         $to =~ s/^\x00//;       # Strip leading NULLs
842         $to =~ s/\x00/; /;      # Replace others with '; '
843         die "No target email addresses found. Either select at least one partner or check your ILL partner library records." if ( !$to );
844         # Create the from, replyto and sender headers
845         my $from = $branch->branchemail;
846         my $replyto = $branch->branchreplyto || $from;
847         die "Your branch has no email address. Please set it."
848             if ( !$from );
849         # Create the email
850         my $message = Koha::Email->new;
851         my %mail = $message->create_message_headers(
852             {
853                 to          => $to,
854                 from        => $from,
855                 replyto     => $replyto,
856                 subject     => Encode::encode( "utf8", $params->{subject} ),
857                 message     => Encode::encode( "utf8", $params->{body} ),
858                 contenttype => 'text/plain',
859             }
860         );
861         # Send it
862         my $result = sendmail(%mail);
863         if ( $result ) {
864             $self->status("GENREQ")->store;
865             return {
866                 error   => 0,
867                 status  => '',
868                 message => '',
869                 method  => 'generic_confirm',
870                 stage   => 'commit',
871                 next    => 'illview',
872             };
873         } else {
874             return {
875                 error   => 1,
876                 status  => 'email_failed',
877                 message => $Mail::Sendmail::error,
878                 method  => 'generic_confirm',
879                 stage   => 'draft',
880             };
881         }
882     } else {
883         die "Unknown stage, should not have happened."
884     }
885 }
886
887 =head3 id_prefix
888
889     my $prefix = $record->id_prefix;
890
891 Return the prefix appropriate for the current Illrequest as derived from the
892 borrower and branch associated with this request's Status, and the config
893 file.
894
895 =cut
896
897 sub id_prefix {
898     my ( $self ) = @_;
899     my $brw = $self->patron;
900     my $brw_cat = "dummy";
901     $brw_cat = $brw->categorycode
902         unless ( 'HASH' eq ref($brw) && $brw->{deleted} );
903     my $prefix = $self->getPrefix( {
904         brw_cat => $brw_cat,
905         branch  => $self->branchcode,
906     } );
907     $prefix .= "-" if ( $prefix );
908     return $prefix;
909 }
910
911 =head3 _censor
912
913     my $params = $illRequest->_censor($params);
914
915 Return $params, modified to reflect our censorship requirements.
916
917 =cut
918
919 sub _censor {
920     my ( $self, $params ) = @_;
921     my $censorship = $self->_config->censorship;
922     $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
923         if ( $params->{opac} );
924     $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
925
926     return $params;
927 }
928
929 =head1 AUTHOR
930
931 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
932
933 =cut
934
935 1;