3 Allows emailing one or more customers, based on a search for customers.
4 Search can be specified either through cust_main fields as cgi params, or
5 through a base64 encoded frozen hash in the 'search' cgi param. Form allows
6 selecting an existing msg_template, or creating a custom message, and shows a
7 preview of the message before sending. If linked to as a popup, include the
8 cgi parameter 'popup' for proper header handling.
10 This may also be used as an element in other pages, enabling you to provide an
11 alternate initial form while using this for search freezing/thawing and
12 preview/send actions, with the following options:
14 acl - the access right to use (defaults to 'Bulk send customer notices')
16 form_action - the URL to submit the form to
18 process_url - the URL for starting the JSRPC process
20 title - the title of the page
22 no_search_fields - arrayref of additional fields that are not search parameters
24 alternate_form - subroutine that returns alternate html for the initial form,
25 replaces msgnum/from/subject/body/action inputs and submit button, not
26 used if an action is specified
28 post_search_hook - sub hook for additional processing after search has been
29 processed from cgi, gets passed options 'conf' and 'search' (a reference to
30 the unfrozen %search hash), should be used to set msgnum or
31 from/subject/body cgi params
35 <& /elements/header-popup.html, $title &>
37 <& /elements/header.html, $title &>
40 <& /elements/error.html &>
42 <FORM NAME="OneTrueForm" ACTION="<% $form_action %>" METHOD="POST">
43 <INPUT TYPE="hidden" NAME="table" VALUE="<% $table %>">
44 %# Mixing search params with from address, subject, etc. required special-case
45 %# handling of those, risked name conflicts, and caused massive problems with
46 %# multi-valued search params. We are no longer in search context, so we
47 %# pack the search into a Storable string for later use.
48 <INPUT TYPE="hidden" NAME="search" VALUE="<% encode_base64(nfreeze(\%search)) %>">
49 <INPUT TYPE="hidden" NAME="popup" VALUE="<% $popup %>">
50 <INPUT TYPE="hidden" NAME="url" VALUE="<% $url | h %>">
52 % if ( $cgi->param('preview') ) {
53 % # preview mode: at this point we have a msg_template (either "real" or
54 % # draft) and $html_body and $text_body contain the preview message.
55 % # give the user a chance to back out (by going back to edit mode).
57 <FONT SIZE="+2">Preview notice</FONT>
58 <& /elements/progress-init.html,
60 [ qw( search table msgnum ) ],
65 <TABLE CLASS="fsinnerbox">
66 <INPUT TYPE="hidden" NAME="msgnum" VALUE="<% $msg_template->msgnum %>">
67 % # kludge these through hidden inputs because they're not really part
68 % # of the template, but should be sticky during draft editing
69 <INPUT TYPE="hidden" NAME="from_name" VALUE="<% $cgi->param('from_name') %>">
70 <INPUT TYPE="hidden" NAME="from_addr" VALUE="<% $cgi->param('from_addr') %>">
72 % if ( !$msg_template->disabled ) {
73 <& /elements/tr-td-label.html, 'label' => 'Template:' &>
74 <td><% $msg_template->msgname |h %></td>
78 <& /elements/tr-td-label.html, 'label' => 'From:' &>
79 <td><% $from |h %></td>
82 <& /elements/tr-td-label.html, 'label' => 'Subject:' &>
83 <td><% $subject |h %></td>
86 <TR><TD COLSPAN=2> </TD></TR>
88 <TH ALIGN="right" VALIGN="top">Message (HTML display): </TD>
89 <TD CLASS="background" ALIGN="left"><% $html_body %></TD>
92 % my $text_body = HTML::FormatText->new(leftmargin=>0)->format(
93 % HTML::TreeBuilder->new_from_content(
97 <TR><TD COLSPAN=2> </TD></TR>
99 <TH ALIGN="right" VALIGN="top">Message (Text display): </TD>
100 <TD CLASS="background" ALIGN="left">
101 <a href="javascript:void(0)" ID="email-message-text-view" style="color:#666666" onclick="showtext()">(view)</a>
102 <a href="javascript:void(0)" ID="email-message-text-hide" style="color:#666666; display: none;" onclick="hidetext()">(hide)</a>
103 <PRE id="email-message-text" style="display: none;"><% $text_body %></PRE>
111 function showtext() {
112 $('#email-message-text-view').css('display','none');
113 $('#email-message-text-hide').css('display','');
114 $('#email-message-text').slideDown();
117 function hidetext() {
118 $('#email-message-text-view').css('display','');
119 $('#email-message-text-hide').css('display','none');
120 $('#email-message-text').slideUp();
123 function areyousure(href) {
124 if (confirm("Send this notice to <% ($num_cust > 1) ? "$num_cust customers" : '1 customer' %> ?")) {
131 <INPUT TYPE="submit" NAME="edit" VALUE="Edit">
132 <INPUT TYPE="button" VALUE="Send notice" onClick="areyousure()">
134 % } elsif ($opt{'alternate_form'}) {
136 <% &{$opt{'alternate_form'}}() %>
141 <SCRIPT TYPE="text/javascript">
142 function toggle(obj) {
143 document.getElementById('table_no_template').style.display = (obj.value == 0) ? '' : 'none';
147 % if ( $msg_template and $msg_template->disabled ) {
148 % # if we've already established a draft template, don't let msgnum be changed
149 <& /elements/hidden.html,
151 curr_value => ($cgi->param('msgnum') || ''),
155 <& /elements/select-msg_template.html,
156 onchange => 'toggle(this)',
157 curr_value => ($cgi->param('msgnum') || ''),
161 <TABLE BGCOLOR="#cccccc" CELLSPACING=0 WIDTH="100%" id="table_no_template">
162 <& /elements/tr-td-label.html, 'label' => 'From:' &>
163 <TD><& /elements/input-text.html,
164 'field' => 'from_name',
165 'value' => $conf->config('invoice_from_name', $agent_virt_agentnum) ||
166 $conf->config('company_name', $agent_virt_agentnum), #?
168 'curr_value' => $cgi->param('from_name'),
170 <& /elements/input-text.html,
171 'field' => 'from_addr',
172 'type' => 'email', # HTML5, woot
173 'value' => $conf->config('invoice_from', $agent_virt_agentnum),
175 'curr_value' => $cgi->param('from_addr'),
178 <& /elements/tr-input-text.html,
179 'field' => 'subject',
180 'label' => 'Subject:',
182 'curr_value' => $subject,
186 <TD ALIGN="right" VALIGN="top" STYLE="padding-top:3px">Message: </TD>
187 <TD><& /elements/htmlarea.html,
190 'curr_value' => $body,
197 <INPUT TYPE="submit" NAME="preview" VALUE="Preview notice">
199 % } #end not action or alternate form
203 <& /elements/footer.html &>
209 $opt{'acl'} ||= 'Bulk send customer notices';
212 unless $FS::CurrentUser::CurrentUser->access_right($opt{'acl'});
214 my $conf = FS::Conf->new;
215 my @no_search_fields = qw( table from subject html_body text_body popup url );
217 my $form_action = $opt{'form_action'} || 'email-customers.html';
218 my $process_url = $opt{'process_url'} || 'process/email-customers.html';
219 my $title = $opt{'title'} || 'Send customer notices';
220 push( @no_search_fields, @{$opt{'no_search_fields'}} ) if $opt{'no_search_fields'};
222 my $table = $cgi->param('table') or die "'table' required";
223 my $agent_virt_agentnum = $cgi->param('agent_virt_agentnum') || '';
225 my $popup = $cgi->param('popup');
226 my $url = $cgi->param('url');
227 my $pdest = { 'message' => "Notice sent" };
228 $pdest->{'url'} = $cgi->param('url') if $url;
231 if ( $cgi->param('search') ) {
232 %search = %{ thaw(decode_base64( $cgi->param('search') )) };
235 %search = $cgi->Vars;
236 delete $search{$_} for @no_search_fields;
237 # FS::$table->search is expected to know which parameters might be
238 # multi-valued, and to accept scalar values for them also. No good
239 # solution to this since CGI can't tell whether a parameter _might_
240 # have had multiple values, only whether it does.
241 @search{keys %search} = map { /\0/ ? [ split /\0/, $_ ] : $_ } values %search;
244 &{$opt{'post_search_hook'}}(
246 'search' => \%search,
247 ) if $opt{'post_search_hook'};
251 if ( $cgi->param('from') ) {
252 $from = $cgi->param('from');
253 } elsif ( $cgi->param('from_name') ) {
254 $from = ($cgi->param('from_name') . ' <' . $cgi->param('from_addr') . '>');
255 } elsif ( $cgi->param('from_addr') ) {
256 $from = $cgi->param('from_addr');
259 my $msg_template = '';
260 if ( $cgi->param('msgnum') =~ /^(\d+)$/ ) {
261 $msg_template = FS::msg_template->by_key($1)
262 or die "template not found: ".$cgi->param('msgnum');
265 my $subject = $cgi->param('subject');
266 my $body = $cgi->param('body');
267 my ($html_body, $text_body);
269 if ( !$cgi->param('preview') ) {
271 # edit mode: initialize the fields from the saved draft, if there is one
272 if ( $msg_template and $msg_template->disabled eq 'D' ) {
273 my $content = $msg_template->content(''); # no localization on these yet
274 $subject ||= $content->subject;
275 $body ||= $content->body;
280 my $sql_query = "FS::$table"->search(\%search);
281 my $count_query = delete($sql_query->{'count_query'});
282 my $count_sth = dbh->prepare($count_query)
283 or die "Error preparing $count_query: ". dbh->errstr;
285 or die "Error executing $count_query: ". $count_sth->errstr;
286 my $count_arrayref = $count_sth->fetchrow_arrayref;
287 $num_cust = $count_arrayref->[0];
289 if ( !$msg_template or $msg_template->disabled eq 'D' ) {
290 # then this is a one-off template; edit it in place
291 my $subject = $cgi->param('subject') || '';
292 my $body = $cgi->param('body') || '';
294 # create a draft template
295 $msg_template ||= FS::msg_template->new({
299 # anyone have a better idea for msgname?
300 $msg_template->set('msgname' => "Notice " . DateTime->now->iso8601);
301 $msg_template->set('from_addr' => $from);
307 if ( $msg_template->msgnum ) {
308 $error = $msg_template->replace(%content);
310 $error = $msg_template->insert(%content);
314 $cgi->param('error', $error);
315 $cgi->delete('preview'); # don't go on to preview stage yet
319 # unless creating the msg_template failed, we now have one, so construct a
320 # preview message from the first customer/whatever in the search results
322 if ( $msg_template ) {
323 $sql_query->{'extra_sql'} .= ' LIMIT 1';
324 $sql_query->{'select'} = "$table.*";
325 $sql_query->{'order_by'} = '';
326 my $object = qsearchs($sql_query);
327 my $cust = $object->cust_main;
329 'cust_main' => $cust,
333 my $cust_msg = $msg_template->prepare(%msgopts);
334 $from = $cust_msg->env_from;
335 $html_body = $cust_msg->preview;
336 if ( $cust_msg->header =~ /^subject: (.*)/mi ) {