From: ivan Date: Tue, 17 Jun 2008 03:35:56 +0000 (+0000) Subject: finish adding a feature to easily list all email addresses for an agent & send them... X-Git-Tag: root_of_webpay_support~565 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=7153190ee1bfeb6d3ad9e6da270a41a949333a7e finish adding a feature to easily list all email addresses for an agent & send them email --- diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 13dbd7f5b..5621a97c5 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -100,6 +100,7 @@ tie my %rights, 'Tie::IxHash', 'Add customer note', #NEW 'Edit customer note', #NEW 'Bill customer now', #NEW + 'Bulk send customer notices', #NEW ], ### @@ -193,7 +194,7 @@ tie my %rights, 'Tie::IxHash', ### # report/listing rights... ### - 'Reprting/listing rights' => [ + 'Reporting/listing rights' => [ 'List customers', 'List zip codes', #NEW 'List invoices', diff --git a/FS/FS/ConfDefaults.pm b/FS/FS/ConfDefaults.pm index 79782590d..a3af52d30 100644 --- a/FS/FS/ConfDefaults.pm +++ b/FS/FS/ConfDefaults.pm @@ -56,6 +56,9 @@ sub cust_fields_avail { ( 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Fax number | Invoicing email(s) | Payment Type | Current Balance' => 'custnum | Status | Last, First | Company | (all address fields ) | ( all phones ) | Invoicing email(s) | Payment Type | Current Balance', + 'Invoicing email(s)' => 'Invoicing email(s)', + 'Cust# | Invoicing email(s)' => 'custnum | Invoicing email(s)', + ); } =back diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm index 1f6eece50..936f94a6e 100644 --- a/FS/FS/Misc.pm +++ b/FS/FS/Misc.pm @@ -12,7 +12,7 @@ use IPC::Run3; # for do_print... should just use IPC::Run i guess #instead @ISA = qw( Exporter ); -@EXPORT_OK = qw( send_email send_fax +@EXPORT_OK = qw( generate_email send_email send_fax states_hash counties state_label card_types generate_ps generate_pdf do_print @@ -40,29 +40,173 @@ but are collected here to elimiate code duplication. =over 4 +=item generate_email OPTION => VALUE ... + +Options: + +=item from + +Sender address, required + +=item to + +Recipient address, required + +=item subject + +email subject, required + +=item html_body + +Email body (HTML alternative). Arrayref of lines, or scalar. + +Will be placed inside an HTML tag. + +=item text_body + +Email body (Text alternative). Arrayref of lines, or scalar. + +=back + +Returns an argument list to be passsed to L. + +=cut + +#false laziness w/FS::cust_bill::generate_email + +use MIME::Entity; +use HTML::Entities; + +sub generate_email { + my %args = @_; + + my $me = '[FS::Misc::generate_email]'; + + my %return = ( + 'from' => $args{'from'}, + 'to' => $args{'to'}, + 'subject' => $args{'subject'}, + ); + + #if (ref($args{'to'}) eq 'ARRAY') { + # $return{'to'} = $args{'to'}; + #} else { + # $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ } + # $self->cust_main->invoicing_list + # ]; + #} + + warn "$me creating HTML/text multipart message" + if $DEBUG; + + $return{'nobody'} = 1; + + my $alternative = build MIME::Entity + 'Type' => 'multipart/alternative', + 'Encoding' => '7bit', + 'Disposition' => 'inline' + ; + + my $data; + if ( ref($args{'text_body'}) eq 'ARRAY' ) { + $data = $args{'text_body'}; + } else { + $data = [ split(/\n/, $args{'text_body'}) ]; + } + + $alternative->attach( + 'Type' => 'text/plain', + #'Encoding' => 'quoted-printable', + 'Encoding' => '7bit', + 'Data' => $data, + 'Disposition' => 'inline', + ); + + my @html_data; + if ( ref($args{'html_body'}) eq 'ARRAY' ) { + @html_data = @{ $args{'html_body'} }; + } else { + @html_data = split(/\n/, $args{'html_body'}); + } + + $alternative->attach( + 'Type' => 'text/html', + 'Encoding' => 'quoted-printable', + 'Data' => [ '', + ' ', + ' ', + ' '. encode_entities($return{'subject'}), + ' ', + ' ', + ' ', + @html_data, + ' ', + '', + ], + 'Disposition' => 'inline', + #'Filename' => 'invoice.pdf', + ); + + #no other attachment: + # multipart/related + # multipart/alternative + # text/plain + # text/html + + $return{'content-type'} = 'multipart/related'; + $return{'mimeparts'} = [ $alternative ]; + $return{'type'} = 'multipart/alternative'; #Content-Type of first part... + #$return{'disposition'} = 'inline'; + + %return; + +} + =item send_email OPTION => VALUE ... Options: -I - (required) +=over 4 + +=item from + +(required) + +=item to + +(required) comma-separated scalar or arrayref of recipients + +=item subject -I - (required) comma-separated scalar or arrayref of recipients +(required) -I - (required) +=item content-type -I - (optional) MIME type for the body +(optional) MIME type for the body -I - (required unless I is true) arrayref of body text lines +=item body -I - (optional, but required if I is true) arrayref of MIME::Entity->build PARAMHASH refs or MIME::Entity objects. These will be passed as arguments to MIME::Entity->attach(). +(required unless I is true) arrayref of body text lines -I - (optional) when set true, send_email will ignore the I option and simply construct a message with the given I. In this case, +=item mimeparts + +(optional, but required if I is true) arrayref of MIME::Entity->build PARAMHASH refs or MIME::Entity objects. These will be passed as arguments to MIME::Entity->attach(). + +=item nobody + +(optional) when set true, send_email will ignore the I option and simply construct a message with the given I. In this case, I, if specified, overrides the default "multipart/mixed" for the outermost MIME container. -I - (optional) when using nobody, optional top-level MIME +=item content-encoding + +(optional) when using nobody, optional top-level MIME encoding which, if specified, overrides the default "7bit". -I - (optional) type parameter for multipart/related messages +=item type + +(optional) type parameter for multipart/related messages + +=back =cut diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 33788f3b5..946495ceb 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -22,7 +22,7 @@ use Locale::Country; use Data::Dumper; use FS::UID qw( getotaker dbh driver_name ); use FS::Record qw( qsearchs qsearch dbdef ); -use FS::Misc qw( send_email generate_ps do_print ); +use FS::Misc qw( generate_email send_email generate_ps do_print ); use FS::Msgcat qw(gettext); use FS::cust_pkg; use FS::cust_svc; @@ -5470,6 +5470,122 @@ sub search_sql { } +=item email_search_sql HASHREF + +(Class method) + +Emails a notice to the specified customers. + +Valid parameters are those of the L method, plus the following: + +=over 4 + +=item from + +From: address + +=item subject + +Email Subject: + +=item html_body + +HTML body + +=item text_body + +Text body + +=item job + +Optional job queue job for status updates. + +=back + +Returns an error message, or false for success. + +If an error occurs during any email, stops the enture send and returns that +error. Presumably if you're getting SMTP errors aborting is better than +retrying everything. + +=cut + +sub email_search_sql { + my($class, $params) = @_; + + my $from = delete $params->{from}; + my $subject = delete $params->{subject}; + my $html_body = delete $params->{html_body}; + my $text_body = delete $params->{text_body}; + + my $job = delete $params->{'job'}; + + my $sql_query = $class->search_sql($params); + + my $count_query = delete($sql_query->{'count_query'}); + my $count_sth = dbh->prepare($count_query) + or die "Error preparing $count_query: ". dbh->errstr; + $count_sth->execute + or die "Error executing $count_query: ". $count_sth->errstr; + my $count_arrayref = $count_sth->fetchrow_arrayref; + my $num_cust = $count_arrayref->[0]; + + #my @extra_headers = @{ delete($sql_query->{'extra_headers'}) }; + #my @extra_fields = @{ delete($sql_query->{'extra_fields'}) }; + + + my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo + + #eventually order+limit magic to reduce memory use? + foreach my $cust_main ( qsearch($sql_query) ) { + + my $to = $cust_main->invoicing_list_emailonly_scalar; + next unless $to; + + my $error = send_email( + generate_email( + 'from' => $from, + 'to' => $to, + 'subject' => $subject, + 'html_body' => $html_body, + 'text_body' => $text_body, + ) + ); + return $error if $error; + + if ( $job ) { #progressbar foo + $num++; + if ( time - $min_sec > $last ) { + my $error = $job->update_statustext( + int( 100 * $num / $num_cust ) + ); + die $error if $error; + $last = time; + } + } + + } + + return ''; +} + +use Storable qw(thaw); +use Data::Dumper; +use MIME::Base64; +sub process_email_search_sql { + my $job = shift; + #warn "$me process_re_X $method for job $job\n" if $DEBUG; + + my $param = thaw(decode_base64(shift)); + warn Dumper($param) if $DEBUG; + + $param->{'job'} = $job; + + my $error = FS::cust_main->email_search_sql( $param ); + die $error if $error; + +} + =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ] Performs a fuzzy (approximate) search and returns the matching FS::cust_main diff --git a/htetc/handler.pl b/htetc/handler.pl index 383c5fc59..be6f2f758 100644 --- a/htetc/handler.pl +++ b/htetc/handler.pl @@ -104,8 +104,11 @@ sub handler use DateTime::Format::Strptime; use Lingua::EN::Inflect qw(PL); use Tie::IxHash; + use URI::URL; use URI::Escape; use HTML::Entities; + use HTML::TreeBuilder; + use HTML::FormatText; use JSON; use MIME::Base64; use IO::Handle; @@ -159,6 +162,8 @@ sub handler use FS::part_pkg_taxclass; use FS::cust_pkg_reason; use FS::cust_refund; + use FS::cust_credit_refund; + use FS::cust_pay_refund; use FS::cust_svc; use FS::nas; use FS::part_bill_event; diff --git a/httemplate/edit/invoice_template.html b/httemplate/edit/invoice_template.html index 851ab5ecf..9cec62c86 100644 --- a/httemplate/edit/invoice_template.html +++ b/httemplate/edit/invoice_template.html @@ -10,23 +10,12 @@ % if ( $type eq 'html' ) { -% #init - - -% #editor - + <% include('/elements/htmlarea.html', + 'field' => 'value', + 'curr_value' => $value, + 'height' => 800, + ) + %> % } else { diff --git a/httemplate/elements/htmlarea.html b/httemplate/elements/htmlarea.html new file mode 100644 index 000000000..f27c4b5e6 --- /dev/null +++ b/httemplate/elements/htmlarea.html @@ -0,0 +1,36 @@ +<%doc> + +Example: + + include('/elements/htmlarea.html', + 'field' => 'fieldname', + 'curr_value' => $curr_value, + 'height' => 800, + ); + + + +% #init + + +% #editor + + +<%init> + +my %opt = @_; + + diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html new file mode 100644 index 000000000..0d3d622c3 --- /dev/null +++ b/httemplate/misc/email-customers.html @@ -0,0 +1,145 @@ +<% include('/elements/header.html', $title) %> + +
+% foreach my $key ( keys %search ) { +% my @values = ref($search{$key}) ? @{$search{$key}} : ( $search{$key} ); +% foreach my $value ( @values ) { + +% } +% } + +% if ( $cgi->param('magic') eq 'send' ) { + + Sending notice + + <% include('/elements/progress-init.html', + 'OneTrueForm', + [ keys(%search), qw( from subject html_body text_body ) ], + 'process/email-customers.html', + { 'message' => "Notice sent" }, #would be nice to show #, but.. + ) + %> + +% } elsif ( $cgi->param('magic') eq 'preview' ) { + + Preview notice + +% } + +% if ( $cgi->param('magic') ) { + + + + <% include('/elements/tr-fixed.html', + 'field' => 'from', + 'label' => 'From:', + 'value' => scalar( $cgi->param('from') ), + ) + %> + + <% include('/elements/tr-fixed.html', + 'field' => 'subject', + 'label' => 'Subject:', + 'value' => scalar( $cgi->param('subject') ), + ) + %> + + + + + + + +% my $text_body = HTML::FormatText->new(leftmargin=>0)->format( +% HTML::TreeBuilder->new_from_content( +% $cgi->param('html_body') +% ) +% ); + + + + + + +
Message (HTML display): <% $cgi->param('html_body') %>
Message (Text display):
<% $text_body %>
+ +% if ( $cgi->param('magic') eq 'preview' ) { + + + +
+ + + +% } + +% } else { + + + + <% include('/elements/tr-input-text.html', + 'field' => 'from', + 'label' => 'From:', + ) + %> + + <% include('/elements/tr-input-text.html', + 'field' => 'subject', + 'label' => 'Subject:', + ) + %> + + + + + + +
Message: <% include('/elements/htmlarea.html', 'field'=>'html_body') %>
+ +%#Substitution vars: + +

+ + + +% } + +
+ +% if ( $cgi->param('magic') eq 'send' ) { + +% } + +<% include('/elements/footer.html') %> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices'); + +my %search = $cgi->Vars; +delete $search{$_} for qw( magic from subject html_body text_body ); +$search{$_} = [ split(/\0/, $search{$_}) ] + foreach grep $search{$_} =~ /\0/, keys %search; + +my $title = 'Bulk send customer notices'; + +my $num_cust; +if ( $cgi->param('magic') eq 'preview' ) { + my $sql_query = FS::cust_main->search_sql(\%search); + my $count_query = delete($sql_query->{'count_query'}); + my $count_sth = dbh->prepare($count_query) + or die "Error preparing $count_query: ". dbh->errstr; + $count_sth->execute + or die "Error executing $count_query: ". $count_sth->errstr; + my $count_arrayref = $count_sth->fetchrow_arrayref; + $num_cust = $count_arrayref->[0]; +} + + diff --git a/httemplate/misc/process/email-customers.html b/httemplate/misc/process/email-customers.html new file mode 100644 index 000000000..d254cfecb --- /dev/null +++ b/httemplate/misc/process/email-customers.html @@ -0,0 +1,9 @@ +<% $server->process %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices'); + +my $server = new FS::UI::Web::JSRPC 'FS::cust_main::process_email_search_sql', $cgi; + + diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html index a2ecd047c..cd5e51f25 100755 --- a/httemplate/search/cust_main.html +++ b/httemplate/search/cust_main.html @@ -1,5 +1,6 @@ <% include( 'elements/search.html', 'title' => 'Customer Search Results', + 'menubar' => $menubar, 'name' => 'customers', 'query' => $sql_query, 'count_query' => $count_query, @@ -30,11 +31,6 @@ ], ) %> -<%once> - -my $link = [ "${p}view/cust_main.cgi?", 'custnum' ]; - - <%init> die "access denied" @@ -89,4 +85,23 @@ my $count_query = delete($sql_query->{'count_query'}); my @extra_headers = @{ delete($sql_query->{'extra_headers'}) }; my @extra_fields = @{ delete($sql_query->{'extra_fields'}) }; +my $link = [ "${p}view/cust_main.cgi?", 'custnum' ]; + +### +# email links +### + +my $menubar = []; + +if ( $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices') ) { + + my $uri = new URI::URL; + $uri->query_form( \%search_hash ); + my $query = $uri->query; + + push @$menubar, 'Email a notice to these customers' => + "${p}misc/email-customers.html?$query", + +} +