[freeside-commits] branch FREESIDE_3_BRANCH updated. 58b18f705e1be83a81c028f18908c1de2aae858e

Jonathan Prykop jonathan at 420.am
Fri Jul 3 19:33:12 PDT 2015


The branch, FREESIDE_3_BRANCH has been updated
       via  58b18f705e1be83a81c028f18908c1de2aae858e (commit)
       via  bbac8cacba3eca0712ffdf9012f9b1e3732642ec (commit)
       via  c112a7e56557acce5a54d3b57e4cdbab7c43ecc5 (commit)
       via  0be0b02db98ed06dabe51805fe45b2e9704327b8 (commit)
      from  e7c1b3f96941e30898bf0e8b4dd25abc1859757c (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 58b18f705e1be83a81c028f18908c1de2aae858e
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Fri Jul 3 21:30:13 2015 -0500

    RT#34078: Payment History Report / Statement

diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html
index d79d843..e5bcc5c 100644
--- a/httemplate/view/cust_main/payment_history.html
+++ b/httemplate/view/cust_main/payment_history.html
@@ -151,6 +151,13 @@
 
 %# invoice reports, combined statement
 % if ( $curuser->access_right('List invoices') ) { 
+%   if ( $curuser->access_right('Bulk send customer notices')
+%          && $cust_main->invoicing_list_emailonly ) {
+
+  <A HREF="<% $p %>misc/email-customer-statement.html?table=cust_main;agent_virt_agentnum=<% $cust_main->agentnum %>;custnum=<% $custnum %>"><% mt('Email statement to this customer') |h %></A>
+  <BR>
+
+%   }
 %   if ( $num_cust_bill > 0 ) {
   <A HREF="<% $p %>view/cust_main_statement-pdf.cgi?<% $custnum %>"><%
   mt('Download typeset statement PDF') |h %></A>

commit bbac8cacba3eca0712ffdf9012f9b1e3732642ec
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Fri Jul 3 19:46:00 2015 -0500

    RT#34078: Payment History Report / Statement

diff --git a/FS/FS/ClientAPI/MasonComponent.pm b/FS/FS/ClientAPI/MasonComponent.pm
index 50597e2..3c3bf4c 100644
--- a/FS/FS/ClientAPI/MasonComponent.pm
+++ b/FS/FS/ClientAPI/MasonComponent.pm
@@ -14,6 +14,7 @@ $DEBUG = 0;
 $me = '[FS::ClientAPI::MasonComponent]';
 
 my %allowed_comps = map { $_=>1 } qw(
+  /elements/customer-statement.html
   /elements/select-did.html
   /misc/areacodes.cgi
   /misc/exchanges.cgi
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 01a47d0..10df0fc 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -4517,7 +4517,7 @@ sub payment_history {
         'amount'      => sprintf('%.2f', $_->setup + $_->recur ),
         'charged'     => sprintf('%.2f', $_->setup + $_->recur ),
         'date'        => $cust_bill->_date,
-        'date_pretty' =>  time2str('%m/%d/%Y', $cust_bill->_date ),
+        'date_pretty' => $self->time2str_local('short', $cust_bill->_date ),
       }
         foreach $cust_bill->cust_bill_pkg;
 
@@ -4531,7 +4531,7 @@ sub payment_history {
                      'amount'      => sprintf('%.2f', $_->charged ),
                      'charged'     => sprintf('%.2f', $_->charged ),
                      'date'        => $_->_date,
-                     'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                     'date_pretty' => $self->time2str_local('short', $_->_date ),
                    }
       foreach $self->cust_bill;
 
@@ -4543,7 +4543,7 @@ sub payment_history {
                    'amount'      => sprintf('%.2f', 0 - $_->paid ),
                    'paid'        => sprintf('%.2f', $_->paid ),
                    'date'        => $_->_date,
-                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                   'date_pretty' => $self->time2str_local('short', $_->_date ),
                  }
     foreach $self->cust_pay;
 
@@ -4553,7 +4553,7 @@ sub payment_history {
                    'amount'      => sprintf('%.2f', 0 -$_->amount ),
                    'credit'      => sprintf('%.2f', $_->amount ),
                    'date'        => $_->_date,
-                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                   'date_pretty' => $self->time2str_local('short', $_->_date ),
                  }
     foreach $self->cust_credit;
 
@@ -4563,7 +4563,7 @@ sub payment_history {
                    'amount'      => $_->refund,
                    'refund'      => $_->refund,
                    'date'        => $_->_date,
-                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                   'date_pretty' => $self->time2str_local('short', $_->_date ),
                  }
     foreach $self->cust_refund;
 
diff --git a/fs_selfservice/FS-SelfService/cgi/history.html b/fs_selfservice/FS-SelfService/cgi/history.html
index 605bc1c..0b6f9ce 100644
--- a/fs_selfservice/FS-SelfService/cgi/history.html
+++ b/fs_selfservice/FS-SelfService/cgi/history.html
@@ -1,39 +1,14 @@
 <%= include('header', 'Payment History') %>
 
 <%=
-my $balance = 0;
-my $style      = 'text-align: left; margin: 0; padding: 0 1em 0 0;';
-my $moneystyle = 'text-align: right; margin: 0; padding: 0 1em 0 0;';
-my $col1 = "#ffffff";
-my $col2 = "#dddddd";
-my $col = $col1;
-foreach my $item (@history) {
-  $balance += $$item{'amount'};
-  $$item{'amount'} =~ s/^(-?)/$1$money_char/;
-  $out .= <<EOF;
-  <TR>
-    <TD style="$style background: $col;">$$item{'date_pretty'}</TD>
-    <TD style="$style background: $col;">$$item{'description'}</TD>
-    <TD style="$moneystyle background: $col;">$$item{'amount'}</TD>
-  </TR>
-EOF
-  $col = $col eq $col1 ? $col2 : $col1;
-}
-$balance = sprintf('%.2f',$balance);
-$balance =~ s/^(-?)/$1$money_char/;
-$out = <<EOF;
-<P>Balance: <B>$balance</B></P>
-<TABLE style="margin: 0;" CELLSPACING="0">
-  <TR>
-    <TH style="$style background: #ff9999;">Date</TH>
-    <TH style="$style background: #ff9999;">Description</TH>
-    <TH style="$moneystyle background: #ff9999;">Amount</TH>
-  </TR>
-$out
-</TABLE>
-EOF
+my $out = mason_comp(
+  'session_id' => $session_id,
+  'comp'       => '/elements/customer-statement.html',
+  'args'       => [
+    'history' => \@history,
+  ]
+);
+$out->{'output'} || $out->{'error'};
 %>
 
-</TABLE>
-
 <%= include('footer') %>

commit c112a7e56557acce5a54d3b57e4cdbab7c43ecc5
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Thu Jun 25 00:51:02 2015 -0500

    RT#34078: Payment History Report / Statement

diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index c17eb4a..c5c0e46 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -2688,13 +2688,6 @@ and customer address. Include units.',
   },
 
   {
-    'key'         => 'payment_history_msgnum',
-    'section'     => 'notification',
-    'description' => 'Template to use for sending payment history to customer',
-    %msg_template_options,
-  },
-
-  {
     'key'         => 'payby',
     'section'     => 'billing',
     'description' => 'Available payment types.',
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index d17a636..01a47d0 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -4488,7 +4488,7 @@ I<conf> - optional already-loaded FS::Conf object.
 =cut
 
 # Caution: this gets used by FS::ClientAPI::MyAccount::billing_history,
-# and also payment_history_text, which should both be kept customer-friendly.
+# and also for sending customer statements, which should both be kept customer-friendly.
 # If you add anything that shouldn't be passed on through the API or exposed 
 # to customers, add a new option to include it, don't include it by default
 sub payment_history {
@@ -4610,31 +4610,6 @@ sub payment_history {
   return @out;
 }
 
-=item payment_history_text
-
-Accepts the same options as L</payment_history> and returns those
-results as a string table with fixed-width columns, max width 80 char.
-
-=cut
-
-sub payment_history_text {
-  my $self = shift;
-  my $opt = ref($_[0]) ? $_[0] : { @_ };
-  my $out = sprintf("%-12s",'Date');
-  $out .= sprintf("%11s",'Amount') . '  ';
-  $out .= sprintf("%11s",'Balance') . '  ';
-  $out .= 'Description'; #don't need to pad with spaces
-  $out .= "\n";
-  foreach my $item ($self->payment_history($opt)) {
-    $out .= sprintf("%-10.10s",$$item{'date_pretty'}) . '  ';   #12 width
-    $out .= sprintf("%11.11s",$$item{'amount_pretty'}) . '  ';  #13 width
-    $out .= sprintf("%11.11s",$$item{'balance_pretty'}) . '  '; #13 width
-    $out .= sprintf("%.42s",$$item{'description'});             #max 42 width
-    $out .= "\n";
-  }
-  return $out;
-}
-
 =back
 
 =head1 CLASS METHODS
diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm
index 211dc32..40c0ae9 100644
--- a/FS/FS/cust_main_Mixin.pm
+++ b/FS/FS/cust_main_Mixin.pm
@@ -380,11 +380,6 @@ HTML body
 
 Text body
 
-=item sub_param
-
-Optional list of parameter hashrefs to be passed
-along to L<FS::msg_template/prepare>.
-
 =back
 
 Returns an error message, or false for success.
@@ -461,8 +456,6 @@ sub email_search_result {
         'cust_main' => $cust_main,
         'object'    => $obj,
       );
-      $message{'sub_param'} = $param->{'sub_param'}
-        if $param->{'sub_param'};
     }
     else {
       my @to = $cust_main->invoicing_list_emailonly;
@@ -540,9 +533,7 @@ sub process_email_search_result {
 
   $param->{'search'} = thaw(decode_base64($param->{'search'}))
     or die "process_email_search_result requires search params.\n";
-  $param->{'sub_param'} = thaw(decode_base64($param->{'sub_param'}))
-    or die "process_email_search_result error decoding sub_param\n"
-      if $param->{'sub_param'};
+
 #  $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ]
 #    unless ref($param->{'payby'});
 
diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm
index 644663e..cb13696 100644
--- a/FS/FS/msg_template.pm
+++ b/FS/FS/msg_template.pm
@@ -268,19 +268,7 @@ invoicing_list addresses.  Multiple addresses may be comma-separated.
 
 =item substitutions
 
-A hash reference of additional string substitutions
-
-=item sub_param
-
-A hash reference, keys are the names of existing substitutions,
-values are an addition parameter object to pass to the subroutine
-for that substitution, e.g.
-
-	'sub_param' => {
-	  'payment_history' => {
-	    'start_date' => 1434764295,
-	  },
-	},
+A hash reference of additional substitutions
 
 =back
 
@@ -336,10 +324,7 @@ sub prepare {
       }
       elsif( ref($name) eq 'ARRAY' ) {
         # [ foo => sub { ... } ]
-        my @subparam = ();
-        push(@subparam, $opt{'sub_param'}->{$name->[0]})
-          if $opt{'sub_param'} && $opt{'sub_param'}->{$name->[0]};
-        $hash{$prefix.($name->[0])} = $name->[1]->($obj, at subparam);
+        $hash{$prefix.($name->[0])} = $name->[1]->($obj);
       }
       else {
         warn "bad msg_template substitution: '$name'\n";
@@ -352,10 +337,7 @@ sub prepare {
     $hash{$_} = $opt{substitutions}->{$_} foreach keys %{$opt{substitutions}};
   }
 
-  foreach my $key (keys %hash) {
-    next if $self->no_encode($key);
-    $hash{$key} = encode_entities($_ || '');
-  };
+  $_ = encode_entities($_ || '') foreach values(%hash);
 
   ###
   # clean up template
@@ -522,13 +504,6 @@ my $usage_warning = sub {
 
 #my $conf = new FS::Conf;
 
-# for substitutions that handle their own encoding
-sub no_encode {
-  my $self = shift;
-  my $field = shift;
-  return ($field eq 'payment_history');
-}
-
 #return contexts and fill-in values
 # If you add anything, be sure to add a description in 
 # httemplate/edit/msg_template.html.
@@ -587,12 +562,6 @@ sub substitutions {
       [ selfservice_server_base_url => sub { 
           $conf->config('selfservice_server-base_url') #, shift->agentnum) 
         } ],
-      [ payment_history => sub {
-          my $cust_main = shift;
-          my $param = shift || {};
-          #html works, see no_encode method
-          return '<PRE>' . encode_entities($cust_main->payment_history_text($param)) . '</PRE>';
-        } ],
     ],
     # next_bill_date
     'cust_pkg'  => [qw( 
diff --git a/FS/FS/msg_template/InitialData.pm b/FS/FS/msg_template/InitialData.pm
index 87c407c..a4e27fd 100644
--- a/FS/FS/msg_template/InitialData.pm
+++ b/FS/FS/msg_template/InitialData.pm
@@ -21,15 +21,6 @@ If you did not request this password reset, you may safely ignore and delete thi
 END
                       ],
     },
-    { msgname   => 'payment_history_template',
-      mime_type => 'text/html',
-      _conf        => 'payment_history_msgnum',
-      _insert_args => [ subject => '{ $company_name } payment history',
-                        body    => <<'END',
-{ $payment_history }
-END
-                      ],
-    },
   ];
 }
 
diff --git a/httemplate/edit/msg_template.html b/httemplate/edit/msg_template.html
index a1026fe..c6b2a7d 100644
--- a/httemplate/edit/msg_template.html
+++ b/httemplate/edit/msg_template.html
@@ -210,7 +210,6 @@ my %substitutions = (
     '$company_address'=> 'Our company address',
     '$company_phonenum' => 'Our phone number',
     '$selfservice_server_base_url' => 'Base URL of customer self-service',
-    '$payment_history' => 'List of invoices/payments/credits/refunds',
   ],
   'contact' => [ # duplicate this for shipping
     '$name'           => 'Company and contact name',
@@ -323,7 +322,7 @@ my $widget = new HTML::Widgets::SelectLayers(
     my @hints = @{ $substitutions{$section} };
     while(@hints) {
       my $key = shift @hints;
-      $html .= qq!\n<TR><TD STYLE="padding-right: .25em;"><A href="javascript:insertHtml('{$key}')">$key</A></TD>!;
+      $html .= qq!\n<TR><TD><A href="javascript:insertHtml('{$key}')">$key</A></TD>!;
       $html .= "\n<TD>".shift(@hints).'</TD></TR>';
     }
     $html .= "\n</TABLE>";
diff --git a/httemplate/elements/customer-statement.html b/httemplate/elements/customer-statement.html
new file mode 100644
index 0000000..63c21cb
--- /dev/null
+++ b/httemplate/elements/customer-statement.html
@@ -0,0 +1,45 @@
+<%doc>
+
+Formats customer payment history into a table.
+
+  include('/elements/customer-statement.html',
+    'history' => \@history
+  );
+
+Option 'history' should be of the form returned by $cust_main->payment_history.
+This element might be used directly by selfservice, so it does not (and should not)
+pull data from the database.
+
+</%doc>
+
+% my $style      = 'text-align: left; margin: 0; padding: 0 1em 0 0;';
+% my $moneystyle = 'text-align: right; margin: 0; padding: 0 1em 0 0;';
+
+<TABLE STYLE="margin: 0;" CELLSPACING="0">
+  <TR>
+    <TH STYLE="<% $style %> background: #ff9999;">Date</TH>
+    <TH STYLE="<% $style %> background: #ff9999;">Description</TH>
+    <TH STYLE="<% $moneystyle %> background: #ff9999;">Amount</TH>
+    <TH STYLE="<% $moneystyle %> background: #ff9999;">Balance</TH>
+  </TR>
+
+% my $col1 = "#ffffff";
+% my $col2 = "#dddddd";
+% my $col = $col1;
+% foreach my $item (@{$opt{'history'}}) {
+  <TR>
+    <TD STYLE="<% $style %> background: <% $col %>;"><% $$item{'date_pretty'} %></TD>
+    <TD STYLE="<% $style %> background: <% $col %>;"><% $$item{'description'} %></TD>
+    <TD STYLE="<% $moneystyle %> background: <% $col %>;"><% $$item{'amount_pretty'} %></TD>
+    <TD STYLE="<% $moneystyle %> background: <% $col %>;"><% $$item{'balance_pretty'} %></TD>
+  </TR>
+%   $col = $col eq $col1 ? $col2 : $col1;
+% }
+
+</TABLE>
+
+<%init>
+my %opt = @_;
+
+die "Invalid type for history" unless ref($opt{'history'}) eq 'ARRAY';
+</%init>
diff --git a/httemplate/misc/email-customer-statement.html b/httemplate/misc/email-customer-statement.html
new file mode 100644
index 0000000..65660f1
--- /dev/null
+++ b/httemplate/misc/email-customer-statement.html
@@ -0,0 +1,91 @@
+
+ <% include('email-customers.html',
+      'form_action'       => 'email-customer-statement.html',
+      'title'             => 'Send statement to customer',
+      'no_search_fields'  => [ 'start_date', 'end_date' ],
+      'alternate_form'    => $alternate_form,
+      'post_search_hook'  => $post_search_hook,
+    )
+ %>
+
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('View invoices');
+
+my $alternate_form = sub {
+  # this could maaaybe be a separate element, for cleanliness
+  # but it's really only for use by this page, and it's not overly complicated
+  my $noinit = 0;
+  return join("\n",
+    '<TABLE BORDER="0">',
+    (
+      map {
+        my $label = ucfirst($_);
+        $label =~ s/_/ /;
+        include('/elements/tr-input-date-field.html',{
+          'name' => $_,
+          'value' => $cgi->param($_) || '',
+          'label' => $label,
+          'noinit' => $noinit++
+        });
+      }
+      qw( start_date end_date )
+    ),
+    '</TABLE>',
+    '<INPUT TYPE="hidden" NAME="action" VALUE="preview">',
+    '<INPUT TYPE="submit" VALUE="Preview notice">',
+  );
+};
+
+my $post_search_hook = sub {
+  my %opt = @_;
+  return unless $cgi->param('action') eq 'preview';
+  my $cust_main = qsearchs('cust_main',$opt{'search'})
+    or die "Could not find customer";
+
+  # so that the statement indicates the latest date
+  my $date_format = $opt{'conf'}->config('date_format') || '%m/%d/%Y';
+  $cgi->param('end_date', time2str($date_format, time))
+    unless $cgi->param('end_date');
+
+  # set from/subject/html_body based on date range
+
+  $cgi->param('from',
+    $opt{'conf'}->config('invoice_from')
+  );
+
+  # shortcut for common text
+  my $summary_text  = $cust_main->name_short .
+    ($cgi->param('start_date') ? ' from ' : '') .
+    $cgi->param('start_date') .
+    ($cgi->param('end_date') ? ' through ' : '') .
+    $cgi->param('end_date');
+
+  $cgi->param('subject',
+    $opt{'conf'}->config('company_name') . 
+    ' statement for ' .
+    $summary_text
+  );
+
+  $cgi->param('html_body',
+    '<P>' .
+    $opt{'conf'}->config('company_name') . 
+    ' statement of charges and payments for ' .
+    $summary_text . 
+    "</P>" .
+    include('/elements/customer-statement.html',
+      'history' => [ 
+        $cust_main->payment_history(
+          map {
+            $_ => parse_datetime($cgi->param($_))
+          }
+          qw( start_date end_date ),
+        ),
+      ],
+    )
+  );
+};
+
+</%init>
+
diff --git a/httemplate/misc/email-customers-history.html b/httemplate/misc/email-customers-history.html
deleted file mode 100644
index 2f9a38d..0000000
--- a/httemplate/misc/email-customers-history.html
+++ /dev/null
@@ -1,51 +0,0 @@
-
- <% include('email-customers.html',
-      'form_action'       => 'email-customers-history.html',
-      'sub_param_process' => $sub_param_process,
-      'alternate_form'    => $alternate_form,
-      'title'             => 'Send payment history',
-    )
- %>
-
-<%init>
-
-my $sub_param_process = sub {
-  my $conf = shift;
-  my %sub_param;
-  foreach my $field ( qw( start_date end_date ) ) {
-    $sub_param{'payment_history'}->{$field} = parse_datetime($cgi->param($field));
-    $cgi->delete($field);
-  }
-  $cgi->param('msgnum',$conf->config('payment_history_msgnum'));
-  return %sub_param;
-};
-
-my $alternate_form = sub {
-  my %sub_param = @_;
-  # this could maaaybe be a separate element, for cleanliness
-  # but it's really only for use by this page, and it's not overly complicated
-  my $noinit = 0;
-  return join("\n",
-    '<TABLE BORDER="0">',
-    (
-      map {
-        my $label = ucfirst($_);
-        $label =~ s/_/ /;
-        include('/elements/tr-input-date-field.html',{
-          'name' => $_,
-          'value' => $sub_param{'payment_history'}->{$_} || '',
-          'label' => $label,
-          'noinit' => $noinit++
-        });
-      }
-      qw( start_date end_date )
-    ),
-    '</TABLE>',
-    '<INPUT TYPE="hidden" NAME="msgnum" VALUE="' . $cgi->param('msgnum') . '">',
-    '<INPUT TYPE="hidden" NAME="action" VALUE="preview">',
-    '<INPUT TYPE="submit" VALUE="Preview notice">',
-  );
-};
-
-</%init>
-
diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html
index d1d5ac7..3327303 100644
--- a/httemplate/misc/email-customers.html
+++ b/httemplate/misc/email-customers.html
@@ -6,18 +6,23 @@ frozen hash in the 'search' cgi param.  Form allows selecting an existing msg_te
 or creating a custom message, and shows a preview of the message before sending.
 If linked to as a popup, include the cgi parameter 'popup' for proper header handling.
 
-This may also be used as an element in other pages, enabling you to pass along
-additional substitution parameters to a message template, with the following options:
+This may also be used as an element in other pages, enabling you to provide an
+alternate initial form while using this for search freezing/thawing and 
+preview/send actions, with the following options:
 
 form_action - the URL to submit the form to
 
-sub_param_process - subroutine to override cgi param values (such as msgnum) 
-and parse/delete additional form fields from the cgi;  should return a %sub_param 
-hash to be passed along for message substitution
+title - the title of the page
 
-alternate_form - an alternate form for template selection/message creation
+no_search_fields - arrayref of additional fields that are not search parameters
 
-title - the title of the page
+alternate_form - subroutine that returns alternate html for the initial form,
+replaces msgnum/from/subject/html_body/action inputs and submit button,
+not used if an action is specified
+
+post_search_hook - sub hook for additional processing after search has been processed from cgi,
+gets passed options 'conf' and 'search' (a reference to the unfrozen %search hash),
+should be used to set msgnum or from/subject/html_body cgi params
 
 </%doc>
 
@@ -35,9 +40,6 @@ title - the title of the page
 %# multi-valued search params.  We are no longer in search context, so we 
 %# pack the search into a Storable string for later use.
 <INPUT TYPE="hidden" NAME="search" VALUE="<% encode_base64(nfreeze(\%search)) %>">
-% if (%sub_param) {
-<INPUT TYPE="hidden" NAME="sub_param" VALUE="<% encode_base64(nfreeze(\%sub_param)) %>">
-% }
 <INPUT TYPE="hidden" NAME="popup" VALUE="<% $popup %>">
 <INPUT TYPE="hidden" NAME="url" VALUE="<% $url | h %>">
 
@@ -47,7 +49,7 @@ title - the title of the page
 
     <% include('/elements/progress-init.html',
                  'OneTrueForm',
-                 [ qw( search table from subject html_body text_body msgnum sub_param ) ],
+                 [ qw( search table from subject html_body text_body msgnum ) ],
                  'process/email-customers.html',
                  $pdest,
               )
@@ -105,7 +107,7 @@ title - the title of the page
 
     </TABLE>
 
-% if ( $cgi->param('action') eq 'preview' ) {
+%   if ( $cgi->param('action') eq 'preview' ) {
 
       <SCRIPT>
         function areyousure(href) {
@@ -119,9 +121,9 @@ title - the title of the page
     
 %   }
 
-% } elsif ($alternate_form) {
+% } elsif ($opt{'alternate_form'}) {
 
-<% $alternate_form %>
+<% &{$opt{'alternate_form'}}() %>
 
 % } else {
 
@@ -174,7 +176,7 @@ Template:
     <INPUT TYPE="hidden" NAME="action" VALUE="preview">
     <INPUT TYPE="submit" VALUE="Preview notice">
 
-% } #end not preview or alternate form
+% } #end not action or alternate form
 
 </FORM>
 
@@ -194,11 +196,11 @@ die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices');
 
 my $conf = FS::Conf->new;
+my @no_search_fields = qw( action table from subject html_body text_body popup url );
 
 my $form_action = $opt{'form_action'} || 'email-customers.html';
-my %sub_param = $opt{'sub_param_process'} ? &{$opt{'sub_param_process'}}($conf) : ();
-my $alternate_form = $opt{'alternate_form'} ? &{$opt{'alternate_form'}}(%sub_param) : ();
 my $title = $opt{'title'} || 'Send customer notices';
+push( @no_search_fields, @{$opt{'no_search_fields'}} ) if $opt{'no_search_fields'};
 
 my $table = $cgi->param('table') or die "'table' required";
 my $agent_virt_agentnum = $cgi->param('agent_virt_agentnum') || '';
@@ -214,13 +216,18 @@ if ( $cgi->param('search') ) {
 }
 else {
   %search = $cgi->Vars;
-  delete $search{$_} for qw( action table from subject html_body text_body popup url sub_param );
+  delete $search{$_} for @no_search_fields;
   # FS::$table->search is expected to know which parameters might be 
   # multi-valued, and to accept scalar values for them also.  No good 
   # solution to this since CGI can't tell whether a parameter _might_
   # have had multiple values, only whether it does.
   @search{keys %search} = map { /\0/ ? [ split /\0/, $_ ] : $_ } values %search;
-} 
+}
+
+&{$opt{'post_search_hook'}}(
+  'conf'   => $conf,
+  'search' => \%search,
+) if $opt{'post_search_hook'};
 
 my $num_cust;
 my $from = '';
@@ -238,6 +245,7 @@ my $html_body = $cgi->param('html_body') || '';
 my $msg_template = '';
 
 if ( $cgi->param('action') eq 'preview' ) {
+
   my $sql_query = "FS::$table"->search(\%search);
   my $count_query = delete($sql_query->{'count_query'});
   my $count_sth = dbh->prepare($count_query)
@@ -260,7 +268,6 @@ if ( $cgi->param('action') eq 'preview' ) {
       'cust_main' => $cust,
       'object' => $object,
     );
-    $msgopts{'sub_param'} = \%sub_param if %sub_param; 
     my %message = $msg_template->prepare(%msgopts);
     ($from, $subject, $html_body) = @message{'from', 'subject', 'html_body'};
   }

commit 0be0b02db98ed06dabe51805fe45b2e9704327b8
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Mon Jun 22 18:34:27 2015 -0500

    RT#34078: Payment History Report / Statement

diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index 4097ff8..804c851 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -662,73 +662,15 @@ sub billing_history {
     $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} )
                             : '(none)';
 
-  my @history = ();
-
   my $conf = new FS::Conf;
 
-  if ( $conf->exists('selfservice-billing_history-line_items') ) {
-
-    foreach my $cust_bill ( $cust_main->cust_bill ) {
-
-      push @history, {
-        'type'        => 'Line item',
-        'description' => $_->desc( $cust_main->locale ).
-                           ( $_->sdate && $_->edate
-                               ? ' '. time2str('%d-%b-%Y', $_->sdate).
-                                 ' To '. time2str('%d-%b-%Y', $_->edate)
-                               : ''
-                           ),
-        'amount'      => sprintf('%.2f', $_->setup + $_->recur ),
-        'date'        => $cust_bill->_date,
-        'date_pretty' =>  time2str('%m/%d/%Y', $cust_bill->_date ),
-      }
-        foreach $cust_bill->cust_bill_pkg;
-
-    }
-
-  } else {
+  $return{'history'} = [
+    $cust_main->payment_history(
+      'line_items' => $conf->exists('selfservice-billing_history-line_items'),
+      'reverse_sort' => 1,
+    )
+  ];
 
-    push @history, {
-                     'type'        => 'Invoice',
-                     'description' => 'Invoice #'. $_->display_invnum,
-                     'amount'      => sprintf('%.2f', $_->charged ),
-                     'date'        => $_->_date,
-                     'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
-                   }
-      foreach $cust_main->cust_bill;
-
-  }
-
-  push @history, {
-                   'type'        => 'Payment',
-                   'description' => 'Payment', #XXX type
-                   'amount'      => sprintf('%.2f', 0 - $_->paid ),
-                   'date'        => $_->_date,
-                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
-                 }
-    foreach $cust_main->cust_pay;
-
-  push @history, {
-                   'type'        => 'Credit',
-                   'description' => 'Credit', #more info?
-                   'amount'      => sprintf('%.2f', 0 -$_->amount ),
-                   'date'        => $_->_date,
-                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
-                 }
-    foreach $cust_main->cust_credit;
-
-  push @history, {
-                   'type'        => 'Refund',
-                   'description' => 'Refund', #more info?  type, like payment?
-                   'amount'      => $_->refund,
-                   'date'        => $_->_date,
-                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
-                 }
-    foreach $cust_main->cust_refund;
-
-  @history = sort { $b->{'date'} <=> $a->{'date'} } @history;
-
-  $return{'history'} = \@history;
   $return{'money_char'} = $conf->config("money_char") || '$',
 
   return \%return;
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index c5c0e46..c17eb4a 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -2688,6 +2688,13 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'payment_history_msgnum',
+    'section'     => 'notification',
+    'description' => 'Template to use for sending payment history to customer',
+    %msg_template_options,
+  },
+
+  {
     'key'         => 'payby',
     'section'     => 'billing',
     'description' => 'Available payment types.',
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 6aaeac6..d17a636 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -4437,6 +4437,204 @@ my ($self,$field) = @_;
 
 }
 
+=item payment_history
+
+Returns an array of hashrefs standardizing information from cust_bill, cust_pay,
+cust_credit and cust_refund objects.  Each hashref has the following fields:
+
+I<type> - one of 'Line item', 'Invoice', 'Payment', 'Credit', 'Refund' or 'Previous'
+
+I<date> - value of _date field, unix timestamp
+
+I<date_pretty> - user-friendly date
+
+I<description> - user-friendly description of item
+
+I<amount> - impact of item on user's balance 
+(positive for Invoice/Refund/Line item, negative for Payment/Credit.)
+Not to be confused with the native 'amount' field in cust_credit, see below.
+
+I<amount_pretty> - includes money char
+
+I<balance> - customer balance, chronologically as of this item
+
+I<balance_pretty> - includes money char
+
+I<charged> - amount charged for cust_bill (Invoice or Line item) records, undef for other types
+
+I<paid> - amount paid for cust_pay records, undef for other types
+
+I<credit> - amount credited for cust_credit records, undef for other types.
+Literally the 'amount' field from cust_credit, renamed here to avoid confusion.
+
+I<refund> - amount refunded for cust_refund records, undef for other types
+
+The four table-specific keys always have positive values, whether they reflect charges or payments.
+
+The following options may be passed to this method:
+
+I<line_items> - if true, returns charges ('Line item') rather than invoices
+
+I<start_date> - unix timestamp, only include records on or after.
+If specified, an item of type 'Previous' will also be included.
+It does not have table-specific fields.
+
+I<end_date> - unix timestamp, only include records before
+
+I<reverse_sort> - order from newest to oldest (default is oldest to newest)
+
+I<conf> - optional already-loaded FS::Conf object.
+
+=cut
+
+# Caution: this gets used by FS::ClientAPI::MyAccount::billing_history,
+# and also payment_history_text, which should both be kept customer-friendly.
+# If you add anything that shouldn't be passed on through the API or exposed 
+# to customers, add a new option to include it, don't include it by default
+sub payment_history {
+  my $self = shift;
+  my $opt = ref($_[0]) ? $_[0] : { @_ };
+
+  my $conf = $$opt{'conf'} || new FS::Conf;
+  my $money_char = $conf->config("money_char") || '$',
+
+  #first load entire history, 
+  #need previous to calculate previous balance
+  #loading after end_date shouldn't hurt too much?
+  my @history = ();
+  if ( $$opt{'line_items'} ) {
+
+    foreach my $cust_bill ( $self->cust_bill ) {
+
+      push @history, {
+        'type'        => 'Line item',
+        'description' => $_->desc( $self->locale ).
+                           ( $_->sdate && $_->edate
+                               ? ' '. time2str('%d-%b-%Y', $_->sdate).
+                                 ' To '. time2str('%d-%b-%Y', $_->edate)
+                               : ''
+                           ),
+        'amount'      => sprintf('%.2f', $_->setup + $_->recur ),
+        'charged'     => sprintf('%.2f', $_->setup + $_->recur ),
+        'date'        => $cust_bill->_date,
+        'date_pretty' =>  time2str('%m/%d/%Y', $cust_bill->_date ),
+      }
+        foreach $cust_bill->cust_bill_pkg;
+
+    }
+
+  } else {
+
+    push @history, {
+                     'type'        => 'Invoice',
+                     'description' => 'Invoice #'. $_->display_invnum,
+                     'amount'      => sprintf('%.2f', $_->charged ),
+                     'charged'     => sprintf('%.2f', $_->charged ),
+                     'date'        => $_->_date,
+                     'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                   }
+      foreach $self->cust_bill;
+
+  }
+
+  push @history, {
+                   'type'        => 'Payment',
+                   'description' => 'Payment', #XXX type
+                   'amount'      => sprintf('%.2f', 0 - $_->paid ),
+                   'paid'        => sprintf('%.2f', $_->paid ),
+                   'date'        => $_->_date,
+                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                 }
+    foreach $self->cust_pay;
+
+  push @history, {
+                   'type'        => 'Credit',
+                   'description' => 'Credit', #more info?
+                   'amount'      => sprintf('%.2f', 0 -$_->amount ),
+                   'credit'      => sprintf('%.2f', $_->amount ),
+                   'date'        => $_->_date,
+                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                 }
+    foreach $self->cust_credit;
+
+  push @history, {
+                   'type'        => 'Refund',
+                   'description' => 'Refund', #more info?  type, like payment?
+                   'amount'      => $_->refund,
+                   'refund'      => $_->refund,
+                   'date'        => $_->_date,
+                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                 }
+    foreach $self->cust_refund;
+
+  #put it all in chronological order
+  @history = sort { $a->{'date'} <=> $b->{'date'} } @history;
+
+  #calculate balance, filter items outside date range
+  my $previous = 0;
+  my $balance = 0;
+  my @out = ();
+  foreach my $item (@history) {
+    last if $$opt{'end_date'} && ($$item{'date'} >= $$opt{'end_date'});
+    $balance += $$item{'amount'};
+    if ($$opt{'start_date'} && ($$item{'date'} < $$opt{'start_date'})) {
+      $previous += $$item{'amount'};
+      next;
+    }
+    $$item{'balance'} = sprintf("%.2f",$balance);
+    foreach my $key ( qw(amount balance) ) {
+      $$item{$key.'_pretty'} = $$item{$key};
+      $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/;
+    }
+    push(@out,$item);
+  }
+
+  # start with previous balance, if there was one
+  if ($previous) {
+    my $item = {
+      'type'        => 'Previous',
+      'description' => 'Previous balance',
+      'amount'      => sprintf("%.2f",$previous),
+      'balance'     => sprintf("%.2f",$previous),
+    };
+    #false laziness with above
+    foreach my $key ( qw(amount balance) ) {
+      $$item{$key.'_pretty'} = $$item{$key};
+      $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/;
+    }
+    unshift(@out,$item);
+  }
+
+  @out = reverse @history if $$opt{'reverse_sort'};
+
+  return @out;
+}
+
+=item payment_history_text
+
+Accepts the same options as L</payment_history> and returns those
+results as a string table with fixed-width columns, max width 80 char.
+
+=cut
+
+sub payment_history_text {
+  my $self = shift;
+  my $opt = ref($_[0]) ? $_[0] : { @_ };
+  my $out = sprintf("%-12s",'Date');
+  $out .= sprintf("%11s",'Amount') . '  ';
+  $out .= sprintf("%11s",'Balance') . '  ';
+  $out .= 'Description'; #don't need to pad with spaces
+  $out .= "\n";
+  foreach my $item ($self->payment_history($opt)) {
+    $out .= sprintf("%-10.10s",$$item{'date_pretty'}) . '  ';   #12 width
+    $out .= sprintf("%11.11s",$$item{'amount_pretty'}) . '  ';  #13 width
+    $out .= sprintf("%11.11s",$$item{'balance_pretty'}) . '  '; #13 width
+    $out .= sprintf("%.42s",$$item{'description'});             #max 42 width
+    $out .= "\n";
+  }
+  return $out;
+}
+
 =back
 
 =head1 CLASS METHODS
diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm
index 40c0ae9..211dc32 100644
--- a/FS/FS/cust_main_Mixin.pm
+++ b/FS/FS/cust_main_Mixin.pm
@@ -380,6 +380,11 @@ HTML body
 
 Text body
 
+=item sub_param
+
+Optional list of parameter hashrefs to be passed
+along to L<FS::msg_template/prepare>.
+
 =back
 
 Returns an error message, or false for success.
@@ -456,6 +461,8 @@ sub email_search_result {
         'cust_main' => $cust_main,
         'object'    => $obj,
       );
+      $message{'sub_param'} = $param->{'sub_param'}
+        if $param->{'sub_param'};
     }
     else {
       my @to = $cust_main->invoicing_list_emailonly;
@@ -533,7 +540,9 @@ sub process_email_search_result {
 
   $param->{'search'} = thaw(decode_base64($param->{'search'}))
     or die "process_email_search_result requires search params.\n";
-
+  $param->{'sub_param'} = thaw(decode_base64($param->{'sub_param'}))
+    or die "process_email_search_result error decoding sub_param\n"
+      if $param->{'sub_param'};
 #  $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ]
 #    unless ref($param->{'payby'});
 
diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm
index cb13696..644663e 100644
--- a/FS/FS/msg_template.pm
+++ b/FS/FS/msg_template.pm
@@ -268,7 +268,19 @@ invoicing_list addresses.  Multiple addresses may be comma-separated.
 
 =item substitutions
 
-A hash reference of additional substitutions
+A hash reference of additional string substitutions
+
+=item sub_param
+
+A hash reference, keys are the names of existing substitutions,
+values are an addition parameter object to pass to the subroutine
+for that substitution, e.g.
+
+	'sub_param' => {
+	  'payment_history' => {
+	    'start_date' => 1434764295,
+	  },
+	},
 
 =back
 
@@ -324,7 +336,10 @@ sub prepare {
       }
       elsif( ref($name) eq 'ARRAY' ) {
         # [ foo => sub { ... } ]
-        $hash{$prefix.($name->[0])} = $name->[1]->($obj);
+        my @subparam = ();
+        push(@subparam, $opt{'sub_param'}->{$name->[0]})
+          if $opt{'sub_param'} && $opt{'sub_param'}->{$name->[0]};
+        $hash{$prefix.($name->[0])} = $name->[1]->($obj, at subparam);
       }
       else {
         warn "bad msg_template substitution: '$name'\n";
@@ -337,7 +352,10 @@ sub prepare {
     $hash{$_} = $opt{substitutions}->{$_} foreach keys %{$opt{substitutions}};
   }
 
-  $_ = encode_entities($_ || '') foreach values(%hash);
+  foreach my $key (keys %hash) {
+    next if $self->no_encode($key);
+    $hash{$key} = encode_entities($_ || '');
+  };
 
   ###
   # clean up template
@@ -504,6 +522,13 @@ my $usage_warning = sub {
 
 #my $conf = new FS::Conf;
 
+# for substitutions that handle their own encoding
+sub no_encode {
+  my $self = shift;
+  my $field = shift;
+  return ($field eq 'payment_history');
+}
+
 #return contexts and fill-in values
 # If you add anything, be sure to add a description in 
 # httemplate/edit/msg_template.html.
@@ -562,6 +587,12 @@ sub substitutions {
       [ selfservice_server_base_url => sub { 
           $conf->config('selfservice_server-base_url') #, shift->agentnum) 
         } ],
+      [ payment_history => sub {
+          my $cust_main = shift;
+          my $param = shift || {};
+          #html works, see no_encode method
+          return '<PRE>' . encode_entities($cust_main->payment_history_text($param)) . '</PRE>';
+        } ],
     ],
     # next_bill_date
     'cust_pkg'  => [qw( 
diff --git a/FS/FS/msg_template/InitialData.pm b/FS/FS/msg_template/InitialData.pm
index a4e27fd..87c407c 100644
--- a/FS/FS/msg_template/InitialData.pm
+++ b/FS/FS/msg_template/InitialData.pm
@@ -21,6 +21,15 @@ If you did not request this password reset, you may safely ignore and delete thi
 END
                       ],
     },
+    { msgname   => 'payment_history_template',
+      mime_type => 'text/html',
+      _conf        => 'payment_history_msgnum',
+      _insert_args => [ subject => '{ $company_name } payment history',
+                        body    => <<'END',
+{ $payment_history }
+END
+                      ],
+    },
   ];
 }
 
diff --git a/httemplate/edit/msg_template.html b/httemplate/edit/msg_template.html
index c6b2a7d..a1026fe 100644
--- a/httemplate/edit/msg_template.html
+++ b/httemplate/edit/msg_template.html
@@ -210,6 +210,7 @@ my %substitutions = (
     '$company_address'=> 'Our company address',
     '$company_phonenum' => 'Our phone number',
     '$selfservice_server_base_url' => 'Base URL of customer self-service',
+    '$payment_history' => 'List of invoices/payments/credits/refunds',
   ],
   'contact' => [ # duplicate this for shipping
     '$name'           => 'Company and contact name',
@@ -322,7 +323,7 @@ my $widget = new HTML::Widgets::SelectLayers(
     my @hints = @{ $substitutions{$section} };
     while(@hints) {
       my $key = shift @hints;
-      $html .= qq!\n<TR><TD><A href="javascript:insertHtml('{$key}')">$key</A></TD>!;
+      $html .= qq!\n<TR><TD STYLE="padding-right: .25em;"><A href="javascript:insertHtml('{$key}')">$key</A></TD>!;
       $html .= "\n<TD>".shift(@hints).'</TD></TR>';
     }
     $html .= "\n</TABLE>";
diff --git a/httemplate/misc/email-customers-history.html b/httemplate/misc/email-customers-history.html
new file mode 100644
index 0000000..2f9a38d
--- /dev/null
+++ b/httemplate/misc/email-customers-history.html
@@ -0,0 +1,51 @@
+
+ <% include('email-customers.html',
+      'form_action'       => 'email-customers-history.html',
+      'sub_param_process' => $sub_param_process,
+      'alternate_form'    => $alternate_form,
+      'title'             => 'Send payment history',
+    )
+ %>
+
+<%init>
+
+my $sub_param_process = sub {
+  my $conf = shift;
+  my %sub_param;
+  foreach my $field ( qw( start_date end_date ) ) {
+    $sub_param{'payment_history'}->{$field} = parse_datetime($cgi->param($field));
+    $cgi->delete($field);
+  }
+  $cgi->param('msgnum',$conf->config('payment_history_msgnum'));
+  return %sub_param;
+};
+
+my $alternate_form = sub {
+  my %sub_param = @_;
+  # this could maaaybe be a separate element, for cleanliness
+  # but it's really only for use by this page, and it's not overly complicated
+  my $noinit = 0;
+  return join("\n",
+    '<TABLE BORDER="0">',
+    (
+      map {
+        my $label = ucfirst($_);
+        $label =~ s/_/ /;
+        include('/elements/tr-input-date-field.html',{
+          'name' => $_,
+          'value' => $sub_param{'payment_history'}->{$_} || '',
+          'label' => $label,
+          'noinit' => $noinit++
+        });
+      }
+      qw( start_date end_date )
+    ),
+    '</TABLE>',
+    '<INPUT TYPE="hidden" NAME="msgnum" VALUE="' . $cgi->param('msgnum') . '">',
+    '<INPUT TYPE="hidden" NAME="action" VALUE="preview">',
+    '<INPUT TYPE="submit" VALUE="Preview notice">',
+  );
+};
+
+</%init>
+
diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html
index 83e8615..d1d5ac7 100644
--- a/httemplate/misc/email-customers.html
+++ b/httemplate/misc/email-customers.html
@@ -1,3 +1,26 @@
+<%doc>
+
+Allows emailing one or more customers, based on a search for customers.  Search can
+be specified either through cust_main fields as cgi params, or through a base64 encoded
+frozen hash in the 'search' cgi param.  Form allows selecting an existing msg_template,
+or creating a custom message, and shows a preview of the message before sending.
+If linked to as a popup, include the cgi parameter 'popup' for proper header handling.
+
+This may also be used as an element in other pages, enabling you to pass along
+additional substitution parameters to a message template, with the following options:
+
+form_action - the URL to submit the form to
+
+sub_param_process - subroutine to override cgi param values (such as msgnum) 
+and parse/delete additional form fields from the cgi;  should return a %sub_param 
+hash to be passed along for message substitution
+
+alternate_form - an alternate form for template selection/message creation
+
+title - the title of the page
+
+</%doc>
+
 % if ($popup) {
 <% include('/elements/header-popup.html', $title) %>
 % } else {
@@ -5,13 +28,16 @@
 % }
 
 
-<FORM NAME="OneTrueForm" ACTION="email-customers.html" METHOD="POST">
+<FORM NAME="OneTrueForm" ACTION="<% $form_action %>" METHOD="POST">
 <INPUT TYPE="hidden" NAME="table" VALUE="<% $table %>">
 %# Mixing search params with from address, subject, etc. required special-case
 %# handling of those, risked name conflicts, and caused massive problems with 
 %# multi-valued search params.  We are no longer in search context, so we 
 %# pack the search into a Storable string for later use.
 <INPUT TYPE="hidden" NAME="search" VALUE="<% encode_base64(nfreeze(\%search)) %>">
+% if (%sub_param) {
+<INPUT TYPE="hidden" NAME="sub_param" VALUE="<% encode_base64(nfreeze(\%sub_param)) %>">
+% }
 <INPUT TYPE="hidden" NAME="popup" VALUE="<% $popup %>">
 <INPUT TYPE="hidden" NAME="url" VALUE="<% $url | h %>">
 
@@ -21,7 +47,7 @@
 
     <% include('/elements/progress-init.html',
                  'OneTrueForm',
-                 [ qw( search table from subject html_body text_body msgnum ) ],
+                 [ qw( search table from subject html_body text_body msgnum sub_param ) ],
                  'process/email-customers.html',
                  $pdest,
               )
@@ -93,6 +119,10 @@
     
 %   }
 
+% } elsif ($alternate_form) {
+
+<% $alternate_form %>
+
 % } else {
 
 <SCRIPT TYPE="text/javascript">
@@ -144,7 +174,7 @@ Template:
     <INPUT TYPE="hidden" NAME="action" VALUE="preview">
     <INPUT TYPE="submit" VALUE="Preview notice">
 
-% }
+% } #end not preview or alternate form
 
 </FORM>
 
@@ -158,11 +188,18 @@ Template:
 
 <%init>
 
+my %opt = @_;
+
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices');
 
 my $conf = FS::Conf->new;
 
+my $form_action = $opt{'form_action'} || 'email-customers.html';
+my %sub_param = $opt{'sub_param_process'} ? &{$opt{'sub_param_process'}}($conf) : ();
+my $alternate_form = $opt{'alternate_form'} ? &{$opt{'alternate_form'}}(%sub_param) : ();
+my $title = $opt{'title'} || 'Send customer notices';
+
 my $table = $cgi->param('table') or die "'table' required";
 my $agent_virt_agentnum = $cgi->param('agent_virt_agentnum') || '';
 
@@ -177,7 +214,7 @@ if ( $cgi->param('search') ) {
 }
 else {
   %search = $cgi->Vars;
-  delete $search{$_} for qw( action table from subject html_body text_body popup url );
+  delete $search{$_} for qw( action table from subject html_body text_body popup url sub_param );
   # FS::$table->search is expected to know which parameters might be 
   # multi-valued, and to accept scalar values for them also.  No good 
   # solution to this since CGI can't tell whether a parameter _might_
@@ -185,8 +222,6 @@ else {
   @search{keys %search} = map { /\0/ ? [ split /\0/, $_ ] : $_ } values %search;
 } 
 
-my $title = 'Send customer notices';
-
 my $num_cust;
 my $from = '';
 if ( $cgi->param('from') ) {
@@ -221,10 +256,12 @@ if ( $cgi->param('action') eq 'preview' ) {
     $sql_query->{'order_by'} = '';
     my $object = qsearchs($sql_query);
     my $cust = $object->cust_main;
-    my %message = $msg_template->prepare(
+    my %msgopts = (
       'cust_main' => $cust,
-      'object' => $object
+      'object' => $object,
     );
+    $msgopts{'sub_param'} = \%sub_param if %sub_param; 
+    my %message = $msg_template->prepare(%msgopts);
     ($from, $subject, $html_body) = @message{'from', 'subject', 'html_body'};
   }
 }

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/ClientAPI/MasonComponent.pm              |    1 +
 FS/FS/ClientAPI/MyAccount.pm                   |   70 +---------
 FS/FS/cust_main.pm                             |  173 ++++++++++++++++++++++++
 fs_selfservice/FS-SelfService/cgi/history.html |   41 ++----
 httemplate/elements/customer-statement.html    |   45 ++++++
 httemplate/misc/email-customer-statement.html  |   91 +++++++++++++
 httemplate/misc/email-customers.html           |   60 ++++++--
 httemplate/view/cust_main/payment_history.html |    7 +
 8 files changed, 383 insertions(+), 105 deletions(-)
 create mode 100644 httemplate/elements/customer-statement.html
 create mode 100644 httemplate/misc/email-customer-statement.html




More information about the freeside-commits mailing list