add customer fields option with agent, display_custnum, status and name, RT#73721
[freeside.git] / FS / FS / report_batch.pm
1 package FS::report_batch;
2 use base qw( FS::Record );
3
4 use strict;
5 use FS::Record qw( qsearch qsearchs dbdef );
6 use FS::msg_template;
7 use FS::cust_main;
8 use FS::Misc::DateTime qw(parse_datetime);
9 use FS::Mason qw(mason_interps);
10 use URI::Escape;
11 use HTML::Defang;
12
13 our $DEBUG = 0;
14
15 =head1 NAME
16
17 FS::report_batch - Object methods for report_batch records
18
19 =head1 SYNOPSIS
20
21   use FS::report_batch;
22
23   $record = new FS::report_batch \%hash;
24   $record = new FS::report_batch { 'column' => 'value' };
25
26   $error = $record->insert;
27
28   $error = $new_record->replace($old_record);
29
30   $error = $record->delete;
31
32   $error = $record->check;
33
34 =head1 DESCRIPTION
35
36 An FS::report_batch object represents an order to send a batch of reports to
37 their respective customers or other contacts.  FS::report_batch inherits from
38 FS::Record.  The following fields are currently supported:
39
40 =over 4
41
42 =item reportbatchnum
43
44 primary key
45
46 =item reportname
47
48 The name of the report, which will be the same as the file name (minus any
49 directory names). There's an enumerated set of these; you can't use just any
50 report.
51
52 =item send_date
53
54 The date the report was sent.
55
56 =item agentnum
57
58 The agentnum to limit the report to, if any.
59
60 =item sdate
61
62 The start date of the report period.
63
64 =item edate
65
66 The end date of the report period.
67
68 =item usernum
69
70 The user who ordered the report.
71
72 =back
73
74 =head1 METHODS
75
76 =over 4
77
78 =item new HASHREF
79
80 Creates a new report batch.  To add the record to the database, see L<"insert">.
81
82 =cut
83
84 sub table { 'report_batch'; }
85
86 =item insert
87
88 Adds this record to the database.  If there is an error, returns the error,
89 otherwise returns false.
90
91 =item delete
92
93 Deletes this record from the database.
94
95 =item replace OLD_RECORD
96
97 Replaces the OLD_RECORD with this one in the database.  If there is an error,
98 returns the error, otherwise returns false.
99
100 =item check
101
102 Checks all fields to make sure this is a valid record.  If there is
103 an error, returns the error, otherwise returns false.  Called by the insert
104 and replace methods.
105
106 =cut
107
108 sub check {
109   my $self = shift;
110
111   my $error = 
112     $self->ut_numbern('reportbatchnum')
113     || $self->ut_text('reportname')
114     || $self->ut_numbern('agentnum')
115     || $self->ut_numbern('sdate')
116     || $self->ut_numbern('edate')
117     || $self->ut_numbern('usernum')
118   ;
119   return $error if $error;
120
121   $self->set('send_date', time);
122
123   $self->SUPER::check;
124 }
125
126 =back
127
128 =head1 SUBROUTINES
129
130 =over 4
131
132 =item process_send_report JOB, PARAMS
133
134 Takes a hash of PARAMS, determines all contacts who need to receive a report,
135 and sends it to them. On completion, creates and stores a report_batch record.
136 JOB is a queue job to receive status messages.
137
138 PARAMS can include:
139
140 - reportname: the name of the report (listed in the C<%sendable_reports> hash).
141 Required.
142 - msgnum: the L<FS::msg_template> to use for this report. Currently the
143 content of the template is ignored, but the subject line and From/Bcc addresses
144 are still used. Required.
145 - agentnum: the agent to limit the report to.
146 - beginning, ending: the date range to run the report, as human-readable 
147 dates (I<not> unix timestamps).
148
149 =cut
150
151 # trying to keep this data-driven, with parameters that tell how the report is
152 # to be handled rather than callbacks.
153 # - path: where under the document root the report is located
154 # - domain: which table to query for objects on which the report is run.
155 #   Each record in that table produces one report.
156 # - cust_main: the method on that object that returns its linked customer (to
157 #   which the report will be sent). If the table has a 'custnum' field, this
158 #   can be omitted.
159 our %sendable_reports = (
160   'sales_commission_pkg' => {
161     'name'      => 'Sales commission per package',
162     'path'      => '/search/sales_commission_pkg.html',
163     'domain'    => 'sales',
164     'cust_main' => 'sales_cust_main',
165   },
166 );
167
168 sub process_send_report {
169   my $job = shift;
170   my $param = shift;
171
172   my $msgnum = $param->{'msgnum'};
173   my $template = FS::msg_template->by_key($msgnum)
174     or die "msg_template $msgnum not found\n";
175
176   my $reportname = $param->{'reportname'};
177   my $info = $sendable_reports{$reportname}
178     or die "don't know how to send report '$reportname'\n";
179
180   # the most important thing: which report is it?
181   my $path = $info->{'path'};
182
183   # find all targets for the report:
184   # - those matching the agentnum if there is one.
185   # - those that aren't disabled.
186   my $domain = $info->{domain};
187   my $dbt = dbdef->table($domain);
188   my $hashref = {};
189   if ( $param->{'agentnum'} and $dbt->column('agentnum') ) {
190     $hashref->{'agentnum'} = $param->{'agentnum'};
191   }
192   if ( $dbt->column('disabled') ) {
193     $hashref->{'disabled'} = '';
194   }
195   my @records = qsearch($domain, $hashref);
196   my $num_targets = scalar(@records);
197   return if $num_targets == 0;
198   my $sent = 0;
199
200   my $outbuf;
201   my ($fs_interp) = mason_interps('standalone', 'outbuf' => \$outbuf);
202   # if generating the report fails, we want to capture the error and exit,
203   # not send it.
204   $fs_interp->error_mode('fatal');
205   $fs_interp->error_format('brief');
206
207   # we have to at least have an RT::Handle
208   require RT;
209   RT::LoadConfig();
210   RT::Init();
211
212   # hold onto all the reports until we're sure they generated correctly.
213   my %cust_main;
214   my %report_content;
215
216   # grab the stylesheet
217   ### note: if we need the ability to support different stylesheets, this
218   ### is the place to put it in
219   eval { $fs_interp->exec('/elements/freeside.css') };
220   die "couldn't load stylesheet via Mason: $@\n" if $@;
221   my $stylesheet = $outbuf;
222
223   my $pkey = $dbt->primary_key;
224   foreach my $rec (@records) {
225
226     $job->update_statustext(int( 100 * $sent / $num_targets ));
227     my $pkey_val = $rec->get($pkey); # e.g. sales.salesnum
228
229     # find the customer we're sending to, and their email
230     my $cust_main;
231     if ( $info->{'cust_main'} ) {
232       my $cust_method = $info->{'cust_main'};
233       $cust_main = $rec->$cust_method;
234     } elsif ( $rec->custnum ) {
235       $cust_main = FS::cust_main->by_key($rec->custnum);
236     } else {
237       warn "$pkey = $pkey_val has no custnum; not sending report\n";
238       next;
239     }
240     my @email = $cust_main->invoicing_list_emailonly;
241     if (!@email) {
242       warn "$pkey = $pkey_val has no email destinations\n" if $DEBUG;
243       next;
244     }
245
246     # params to send to the report (as if from the user's browser)
247     my @report_param = ( # maybe list these in $info
248       agentnum  => $param->{'agentnum'},
249       beginning => $param->{'beginning'},
250       ending    => $param->{'ending'},
251       $pkey     => $pkey_val,
252       _type     => 'html-print',
253     );
254
255     # build a query string
256     my $query_string = '';
257     while (@report_param) {
258       $query_string .= uri_escape(shift @report_param)
259                     .  '='
260                     .  uri_escape(shift @report_param);
261       $query_string .= ';' if @report_param;
262     }
263     warn "$path?$query_string\n\n" if $DEBUG;
264
265     # run the report!
266     $FS::Mason::Request::QUERY_STRING = $query_string;
267     $FS::Mason::Request::FSURL = '';
268     $outbuf = '';
269     eval { $fs_interp->exec($path) };
270     die "creating report for $pkey = $pkey_val: $@" if $@;
271
272     # make some adjustments to the report
273     my $html_defang;
274     $html_defang = HTML::Defang->new(
275       url_callback      => sub { 1 }, # strip all URLs (they're not accessible)
276       tags_to_callback  => [ 'body' ], # and after the BODY tag...
277       tags_callback     => sub {
278         my $isEndTag = $_[4];
279         $html_defang->add_to_output("\n<style>\n$stylesheet\n</style>\n")
280           unless $isEndTag;
281       },
282     );
283     $outbuf = $html_defang->defang($outbuf);
284
285     $cust_main{ $cust_main->custnum } = $cust_main;
286     $report_content{ $cust_main->custnum } = $outbuf;
287   } # foreach $rec
288
289   $job->update_statustext('Sending reports...');
290   foreach my $custnum (keys %cust_main) {
291     # create an email message with the report as body
292     # change this when backporting to 3.x
293     $template->send(
294       cust_main         => $cust_main{$custnum},
295       object            => $cust_main{$custnum},
296       msgtype           => 'report',
297       override_content  => $report_content{$custnum},
298     );
299   }
300
301   my $self = FS::report_batch->new({
302     reportname  => $param->{'reportname'},
303     agentnum    => $param->{'agentnum'},
304     sdate       => parse_datetime($param->{'beginning'}),
305     edate       => parse_datetime($param->{'ending'}),
306     usernum     => $job->usernum,
307     msgnum      => $param->{'msgnum'},
308   });
309   my $error = $self->insert;
310   warn "error recording completion of report: $error\n" if $error;
311
312 }
313
314 =head1 SEE ALSO
315
316 L<FS::Record>
317
318 =cut
319
320 1;
321