Merge branch 'FREESIDE_3_BRANCH_34237' into FREESIDE_3_BRANCH
authorJonathan Prykop <jonathan@freeside.biz>
Fri, 19 Feb 2016 08:52:01 +0000 (02:52 -0600)
committerJonathan Prykop <jonathan@freeside.biz>
Fri, 19 Feb 2016 08:52:01 +0000 (02:52 -0600)
27 files changed:
FS/FS.pm
FS/FS/Conf.pm
FS/FS/Password_Mixin.pm
FS/FS/Template_Mixin.pm
FS/FS/cdr/amcom.pm
FS/FS/cust_credit.pm
FS/FS/cust_pkg.pm
FS/FS/part_event/Condition/cust_pay_payby.pm [new file with mode: 0644]
FS/bin/freeside-cdr-evariste-import [new file with mode: 0755]
bin/freeside-debian-releases.sh
bin/svc.replace
debian/changelog
debian/control
httemplate/docs/about.html
httemplate/edit/cust_main/billing.html
httemplate/edit/svc_acct.cgi
httemplate/elements/validate_password.html
httemplate/misc/confirm-address_standardize.html
httemplate/misc/confirm-censustract.html
httemplate/misc/email-customers.html
httemplate/misc/xmlhttp-validate_password.html
httemplate/search/cust_main.cgi
httemplate/search/cust_pkg.cgi
httemplate/search/customer_accounting_summary.html
httemplate/search/customer_cdr_profit.html
httemplate/search/elements/grid-report.html [new file with mode: 0644]
httemplate/view/cust_main/billing.html

index ff46d60..c17cd93 100644 (file)
--- a/FS/FS.pm
+++ b/FS/FS.pm
@@ -3,7 +3,7 @@ package FS;
 use strict;
 use vars qw($VERSION);
 
-$VERSION = '3.9git';
+$VERSION = '3.91~git';
 
 #find missing entries in this file with:
 # for a in `ls *pm | cut -d. -f1`; do grep 'L<FS::'$a'>' ../FS.pm >/dev/null || echo "missing $a" ; done
index 903b42e..41d6db6 100644 (file)
@@ -4307,6 +4307,14 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'password-insecure',
+    'section'     => 'password',
+    'description' => 'Disable all password security checks and allow entry of insecure passwords.  NOT RECOMMENDED.',
+    'type'        => 'checkbox',
+    'per_agent'   => 1,
+  },
+
+  {
     'key'         => 'datavolume-forcemegabytes',
     'section'     => 'UI',
     'description' => 'All data volumes are expressed in megabytes',
index ee038db..bdfab18 100644 (file)
@@ -45,6 +45,16 @@ sub is_password_allowed {
   my $self = shift;
   my $password = shift;
 
+  my $cust_main = $self->cust_main;
+
+  # workaround for non-inserted services
+  if ( !$cust_main and $self->get('pkgnum') ) {
+    my $cust_pkg = FS::cust_pkg->by_key($self->get('pkgnum'));
+    $cust_main = $cust_pkg->cust_main if $cust_pkg;
+  }
+  warn "is_password_allowed: no customer could be identified" if !$cust_main;
+  return '' if $cust_main && $conf->config_bool('password-insecure', $cust_main->agentnum);
+
   # basic checks using Data::Password;
   # options for Data::Password
   $DICTIONARY = 4;   # minimum length of disallowed words
@@ -70,7 +80,6 @@ sub is_password_allowed {
   return '' unless $self->get($self->primary_key); # for validating new passwords pre-insert
 
   #check against customer fields
-  my $cust_main = $self->cust_main;
   if ($cust_main) {
     my @words;
     # words from cust_main
index 4daa637..76cadc3 100644 (file)
@@ -2251,7 +2251,7 @@ sub generate_email {
   my @otherparts = ();
   if ( ref($self) eq 'FS::cust_bill' && $cust_main->email_csv_cdr ) {
 
-    if ( $conf->exists('voip-cust_email_csv_cdr_zip') ) {
+    if ( $conf->config('voip-cdr_email_attach') eq 'zip' ) {
 
       my $data = join('', map "$_\n",
                    $self->call_details(prepend_billed_number=>1)
@@ -2267,13 +2267,14 @@ sub generate_email {
       die "Error zipping CDR attachment: $!" unless $status == AZ_OK;
 
       push @otherparts, build MIME::Entity
-        'Type'       => 'application/zip',
-        'Encoding'   => 'base64',
-        'Data'       => $zipdata,
+        'Type'        => 'application/zip',
+        'Encoding'    => 'base64',
+        'Data'        => $zipdata,
+        'Disposition' => 'attachment',
         'Filename'    => 'usage-'. $self->invnum. '.zip',
       ;
 
-    } else {
+    } else { # } elsif ( $conf->config('voip-cdr_email_attach') eq 'csv' ) {
  
       push @otherparts, build MIME::Entity
         'Type'        => 'text/csv',
index 697a682..4fb94ee 100644 (file)
@@ -2,9 +2,9 @@ package FS::cdr::amcom;
 
 use strict;
 use base qw( FS::cdr );
-use vars qw( %info %cdrtypes);
+use vars qw( %info );
 use DateTime;
-use FS::Record qw( qsearch );
+use FS::Record qw( qsearchs );
 use FS::cdr_type;
 
 my ($tmp_mday, $tmp_mon, $tmp_year);
@@ -16,12 +16,6 @@ my ($tmp_mday, $tmp_mon, $tmp_year);
   'type'          => 'csv',
   'sep_char'      => ',',
   'disabled'      => 0,
-  'header_buffer' => sub {
-
-       %cdrtypes = ( map { $_->cdrtypename => $_->cdrtypenum }
-                             qsearch('cdr_type', {})
-            );
-    },
 
   #listref of what to do with each field from the CDR, in order
   'import_fields' => [
@@ -43,7 +37,9 @@ my ($tmp_mday, $tmp_mon, $tmp_year);
     sub {         # 5. Call Category (LOCAL, NATIONAL, FREECALL, MOBILE)
       my ($cdr, $data) = @_;
       $data ||= 'none';
-      $cdr->cdrtypenum($cdrtypes{$data} || '');
+
+      my $cdr_type = qsearchs('cdr_type', { 'cdrtypename' => $data } );
+      $cdr->set('cdrtypenum', $cdr_type->cdrtypenum) if $cdr_type;      
       $cdr->set('dcontext', $data);  
     },
     sub {         # 6. Start Date (DDMMYYYY
index 67c7d91..77b914f 100644 (file)
@@ -739,19 +739,31 @@ sub calculate_tax_adjustment {
     $cust_bill{ $invnum}->custnum == $arg{custnum}
       or die "lineitem #$billpkgnum not found\n";
 
-    # calculate credit ratio.
-    # (First deduct any existing credits applied to this line item, to avoid
-    # rounding errors.)
-    my $charged = $cust_bill_pkg->get($setuprecur);
-    my $previously_credited =
-      $cust_bill_pkg->credited( '', '', setuprecur => $setuprecur) || 0;
-
-    $charged -= $previously_credited;
+    # tax_Xlocation records don't distinguish setup and recur, so calculate
+    # the fraction of setup+recur (after deducting credits) that's setup. This
+    # will also be the fraction of tax (after deducting credits) that's tax on
+    # setup.
+    my ($setup, $recur);
+    $setup = $cust_bill_pkg->get('setup') || 0;
+    if ($setup) {
+      $setup -= $cust_bill_pkg->credited('', '', setuprecur => 'setup') || 0;
+    }
+    $recur = $cust_bill_pkg->get('recur') || 0;
+    if ($recur) {
+      $recur -= $cust_bill_pkg->credited('', '', setuprecur => 'recur') || 0;
+    }
+    my $setup_ratio = $setup / ($setup + $recur);
+
+    # Calculate the fraction of tax to credit: it's the fraction of this charge
+    # (either setup or recur) that's being credited.
+    my $charged = ($setuprecur eq 'setup') ? $setup : $recur;
+    next if $charged == 0; # shouldn't happen, but still...
+
     if ($charged < $amount) {
       $error = "invoice #$invnum: tried to credit $amount, but only $charged was charged";
       last;
     }
-    my $ratio = $amount / $charged;
+    my $credit_ratio = $amount / $charged;
 
     # gather taxes that apply to the selected item
     foreach my $table (
@@ -766,7 +778,16 @@ sub calculate_tax_adjustment {
         foreach ($tax_link->cust_credit_bill_pkg) {
           $tax_amount -= $_->amount;
         }
-        my $tax_credit = sprintf('%.2f', $tax_amount * $ratio);
+        # split tax amount based on setuprecur
+        # (this method ensures that, if you credit both setup and recur tax,
+        # it always equals the entire tax despite any rounding)
+        my $setup_tax = sprintf('%.2f', $tax_amount * $setup_ratio);
+        if ( $setuprecur eq 'setup' ) {
+          $tax_amount = $setup_tax;
+        } else {
+          $tax_amount = $tax_amount - $setup_tax;
+        }
+        my $tax_credit = sprintf('%.2f', $tax_amount * $credit_ratio);
         my $pkey = $tax_link->get($tax_link->primary_key);
         push @taxlines, {
           table   => $table,
index daa3353..b137384 100644 (file)
@@ -56,9 +56,11 @@ $disable_agentcheck = 0;
 
 $upgrade = 0; #go away after setup+start dates cleaned up for old customers
 
+our $cache_enabled = 0;
+
 sub _simplecache {
   my( $self, $hashref ) = @_;
-  if ( $hashref->{'pkg'} ) {
+  if ( $cache_enabled && $hashref->{'pkg'} && $hashref->{'plan'} ) {
     $self->{'_pkgpart'} = FS::part_pkg->new($hashref);
   }
 }
diff --git a/FS/FS/part_event/Condition/cust_pay_payby.pm b/FS/FS/part_event/Condition/cust_pay_payby.pm
new file mode 100644 (file)
index 0000000..d360564
--- /dev/null
@@ -0,0 +1,40 @@
+package FS::part_event::Condition::cust_pay_payby;
+
+use strict;
+use base qw( FS::part_event::Condition );
+use FS::payby;
+use FS::Record qw( qsearchs );
+use FS::cust_pay;
+
+sub description { 'Type of most recent payment'; }
+
+tie my %payby, 'Tie::IxHash', FS::payby->payment_payby2payname;
+
+sub option_fields {
+  (
+    'payby' => {
+                 label         => 'Payment type',
+                 type          => 'checkbox-multiple',
+                 options       => [ keys %payby ],
+                 option_labels => \%payby,
+               },
+  );
+}
+
+sub condition {
+  my($self, $object) = @_;
+
+  my $cust_main = $self->cust_main($object);
+
+  my $cust_pay = qsearchs({ 'table'    => 'cust_pay',
+                            'hashref'  => { 'custnum'=>$cust_main->custnum },
+                            'order_by' => 'ORDER BY _date DESC LIMIT 1',
+                         })
+    or return 0;
+
+  my $payby = $self->option('payby') || {};
+  $payby->{ $cust_pay->payby };
+
+}
+
+1;
diff --git a/FS/bin/freeside-cdr-evariste-import b/FS/bin/freeside-cdr-evariste-import
new file mode 100755 (executable)
index 0000000..0487ae5
--- /dev/null
@@ -0,0 +1,129 @@
+#!/usr/bin/perl
+
+use strict;
+
+use DBI;
+use Date::Format 'time2str';
+use Date::Parse 'str2time';
+use Getopt::Long;
+
+use FS::Record qw(qsearchs dbh);
+use FS::UID qw(adminsuidsetup);
+use FS::cdr;
+use FS::cdr_batch;
+use Time::Local;
+
+sub usage {
+  "Import cdrs from an Evariste CSRP postgres database.
+
+Date range defaults from the enddate of the last evariste import 
+batch to the most recent midnight.  Imports cdrs for calls that 
+ended on or after startdate, before enddate.
+
+Usage:
+freeside-cdr-evariste -d database -h host -u dbusername -p dbpass
+  [-s startdate] [-e enddate] freesideuser
+";
+}
+
+my ($db,$host,$username,$password,$startdate,$enddate,$verbose);
+GetOptions(
+  "db=s"        => \$db,
+  "enddate=s"   => \$enddate,
+  "host=s"      => \$host,
+  "password=s"  => \$password,
+  "startdate=s" => \$startdate,
+  "username=s"  => \$username
+);
+
+my $fsuser = $ARGV[-1];
+
+die usage() unless $db && $host && $password && $username && $fsuser;
+
+adminsuidsetup($fsuser);
+
+if ($startdate) {
+  $startdate = str2time($startdate) or die "Can't parse startdate $startdate";
+  $startdate = time2str("%Y-%m-%d %H:%M:%S",$startdate);
+}
+unless ($startdate) {
+  my $lastbatch = qsearchs({
+    'table'     => 'cdr_batch',
+    'hashref'   => { 'cdrbatch' => {op=>'like', value=>"evariste-import-$host-$db\%"}},
+    'order_by'  => 'ORDER BY _date DESC LIMIT 1',
+  });
+  $startdate = time2str("%Y-%m-%d %H:%M:%S", $lastbatch->_date) if $lastbatch;
+}
+$startdate ||= '2010-01-01 00:00:00'; #seems decently in the past
+
+my @now = localtime();
+my $now = timelocal(0,0,0,$now[3],$now[4],$now[5]); #most recent midnight
+if ($enddate) {
+  $enddate = str2time($enddate) or die "Can't parse enddate $enddate";
+  $now = $enddate;
+  $enddate = time2str("%Y-%m-%d %H:%M:%S",$enddate);
+}
+$enddate ||= time2str("%Y-%m-%d %H:%M:%S",$now);
+
+my $cdbh = DBI->connect("dbi:Pg:database=$db;host=$host", $username, $password) 
+  or die $DBI::errstr;
+
+# selecting by end_time rather than start_time 
+# so we don't lose records between batches
+my $csth = $cdbh->prepare('SELECT c.*, cp.* FROM cdr c
+LEFT JOIN cdr_rate_postproc cp ON cp.cdr_id = c.id
+WHERE end_time >= ? AND end_time < ?')
+  or die $cdbh->errstr;
+
+$csth->execute($startdate,$enddate)
+  or die $csth->errstr;
+
+$FS::UID::AutoCommit = 0;
+
+my $cdrbatchname = "evariste-import-$host-$db-". time2str('%Y/%m/%d-%T',$now);
+die "Batch $cdrbatchname already exists, please specify a different end date. \n\n" . usage()
+  if FS::cdr_batch->row_exists('cdrbatch = ?', $cdrbatchname);
+my $cdr_batch = new FS::cdr_batch({ 
+  'cdrbatch' => $cdrbatchname,
+  '_date'    => $now,
+});
+my $error = $cdr_batch->insert;
+if ($error) {
+  dbh->rollback;
+  die "Error creating batch: $error";
+}
+
+while (my $row = $csth->fetchrow_hashref) {
+  next if FS::cdr->row_exists('uniqueid = ?', $row->{'id'});
+  my $cdr = FS::cdr->new ({
+    # from cdr table
+    'cdrbatchnum'             => $cdr_batch->cdrbatchnum,
+    'uniqueid'                => $row->{'id'},
+    'src'                     => $row->{'src'},
+    'dst'                     => $row->{'dest'},
+    'startdate'               => int(str2time($row->{'start_time'})),
+    'answerdate'              => int(str2time($row->{'answer_time'})),
+    'enddate'                 => int(str2time($row->{'end_time'})),
+    'duration'                => $row->{'duration_sec'},
+    'accountcode'             => $row->{'customer_id'},
+    'src_ip_addr'             => $row->{'src_ip'},
+    'dst_ip_addr'             => $row->{'dest_ip'},
+    # from cdr_rate_postproc table
+    'billsec'                 => $row->{'rate_bill_sec'},
+    'upstream_price'          => $row->{'rate_cost_net'},
+  });
+  $error = $cdr->insert;
+  if ($error) {
+    dbh->rollback or die dbh->errstr;
+    die "Error inserting cdr: $error";
+  }
+}
+
+$csth->finish;
+
+dbh->commit or die dbh->errstr;
+
+exit;
+
+
+
index 8246526..e832d91 100644 (file)
@@ -38,7 +38,7 @@ fi
 
 # Add the build information to changelog
 
-dch -b --newversion $GIT_VERSION~$DATE "Auto-Build"
+dch -b --newversion $GIT_VERSION-$DATE "Auto-Build"
 
 # Using pbuilder and pdebuild in chroot instead of building directly : dpkg-buildpackage -b -rfakeroot -uc -us
 
index ad9cc43..a9ceada 100644 (file)
@@ -6,7 +6,7 @@ use FS::Record qw( qsearch );
 use FS::cust_svc;
 
 my $user = shift or die &usage;
-adminsuidsetup;
+adminsuidsetup $user;
 
 my $svcpart = shift or die &usage;
 
index eb34e52..893d60a 100644 (file)
@@ -1,3 +1,15 @@
+freeside (3.91git~20160205-1) UNRELEASED; urgency=low
+
+  * Testing version of freeside.
+
+ -- Jeremy Davis <jeremyd-debian@freeside.biz>  Fri, 05 Feb 2016 17:01:21 -0500
+
+freeside (3.9) UNRELEASED; urgency=low
+
+  * Stable version of freeside.
+
+ -- Jeremy Davis <jeremyd-debian@freeside.biz>  Fri, 05 Feb 2016 02:07:21 -0500
+
 freeside (3.9git~20160120-1) UNRELEASED; urgency=low
 
   * Testing version of freeside.
index efd6234..c8bffae 100644 (file)
@@ -17,7 +17,7 @@ Architecture: all
 Pre-Depends: freeside-lib
 # dbconfig-common
 Depends: ${perl:Depends}, ${shlibs:Depends}, ${misc:Depends}, freeside-webui,
- debconf, cron, openbsd-inetd, tcpd, undersmtpd, ssmtp, freeside-lib (>= 3.8git~20151123)
+ debconf, cron, openbsd-inetd, tcpd, undersmtpd, ssmtp, freeside-lib (>= 3.8)
 Description: Billing and trouble ticketing for service providers
  Freeside is a web-based billing, trouble ticketing and network monitoring
  application.  It includes features for ISPs and WISPs, hosting providers and
index 8ed4089..6519180 100644 (file)
@@ -28,7 +28,7 @@
 % } else {
   <FONT SIZE="-1">
 % }
-&copy; 2015 Freeside Internet Services, Inc.<BR>
+&copy; 2016 Freeside Internet Services, Inc.<BR>
 All rights reserved.<BR>
 Licensed under the terms of the<BR>
 GNU <b>Affero</b> General Public License.<BR>
index c4b66c8..519f2f8 100644 (file)
@@ -597,9 +597,9 @@ function toggle(obj) {
       <INPUT TYPE="hidden" NAME="squelch_cdr" VALUE="<% $cust_main->squelch_cdr %>">
 % }
 
-% if ( $conf->config('voip-cdr_email_attach') ) { 
+% if ( my $attach = $conf->config('voip-cdr_email_attach') ) {
       <TR>
-       <TD COLSPAN="2"><INPUT TYPE="checkbox" NAME="email_csv_cdr" VALUE="Y" <% $cust_main->email_csv_cdr eq "Y" ? 'CHECKED' : '' %>> <% mt('Attach CDRs as CSV to emailed invoices') |h %></TD>
+       <TD COLSPAN="2"><INPUT TYPE="checkbox" NAME="email_csv_cdr" VALUE="Y" <% $cust_main->email_csv_cdr eq "Y" ? 'CHECKED' : '' %>> <% mt('Attach CDRs as '. uc($attach). ' to emailed invoices') |h %></TD>
       </TR>
 % } else { 
       <INPUT TYPE="hidden" NAME="email_csv_cdr" VALUE="<% $cust_main->email_csv_cdr %>">
index ff8e316..4266046 100755 (executable)
     <INPUT TYPE="text" ID="clear_password" NAME="clear_password" VALUE="<% $password %>" SIZE=<% $pmax2 %> MAXLENGTH=<% $pmax %>>
     <& /elements/random_pass.html, 'clear_password' &><BR>
     <DIV ID="clear_password_result" STYLE="font-size: smaller"></DIV>
-    <& '/elements/validate_password.html', 
-         'fieldid' => 'clear_password',
-         'svcnum' => $svcnum 
+    <& /elements/validate_password.html, 
+        'fieldid' => 'clear_password',
+        'svcnum' => $svcnum ,
+        'pkgnum' => $pkgnum,
     &>
   </TD>
 </TR>
index a488c4f..f067ad8 100644 (file)
@@ -5,8 +5,9 @@ To validate passwords via javascript/xmlhttp:
   <INPUT ID="password_field" TYPE="text">
   <DIV ID="password_field_result">
   <& '/elements/validate_password.html', 
-     fieldid  => 'password_field',
-     svcnum   => $svcnum
+      fieldid  => 'password_field',
+      svcnum   => $svcnum,
+      pkgnum   => $pkgnum, # used if the service doesn't exist yet
   &>
 
 The ID of the input field can be anything;  the ID of the DIV in which to display results
@@ -27,7 +28,10 @@ function add_password_validation (fieldid) {
     var resultfield = document.getElementById(fieldid);
     if (this.value) {
       resultfield.innerHTML = '<SPAN STYLE="color: blue;">Validating password...</SPAN>';
-      validate_password('fieldid',fieldid,'svcnum','<% $opt{'svcnum'} %>','password',this.value,
+      validate_password('fieldid',fieldid,
+                        'svcnum',<% $opt{'svcnum'} |js_string %>,
+                        'pkgnum',<% $opt{'pkgnum'} |js_string %>,
+                        'password',this.value,
         function (result) {
           result = JSON.parse(result);
           var resultfield = document.getElementById(result.fieldid);
index 0a05c70..9d1a5c1 100644 (file)
@@ -34,18 +34,18 @@ Confirm address standardization
   </TR>
   <TR>
 %     if ( $old{$pre.'company'} ) {
-    <TD><% $old{$pre.'company'} %></TD>
+    <TD><% $old{$pre.'company'} |h %></TD>
 %     }
   </TR>
   <TR>
-    <TD><% $old{$pre.'address1'} %></TD>
+    <TD><% $old{$pre.'address1'} |h %></TD>
     <TD ROWSPAN=3><FONT COLOR="#ff0000"><B><% $new{$pre.'error'} %></B></FONT></TD>
   </TR>
   <TR>
-    <TD><% $old{$pre.'address2'} %></TD>
+    <TD><% $old{$pre.'address2'} |h %></TD>
   </TR>
   <TR>
-    <TD><% $old{$pre.'city'} %>, <% $old{$pre.'state'} %>  <% $old{$pre.'zip'} %></TD>
+    <TD><% $old{$pre.'city'} |h %>, <% $old{$pre.'state'} |h %>  <% $old{$pre.'zip'} |h %></TD>
   </TR>
 %   } else { # not an error
 %     $rows++ if !$new{$pre.'addr_clean'};
@@ -68,21 +68,21 @@ Confirm address standardization
   <TR>
 %     if ( $old{$pre.'company'} ) {
   <TR>
-    <TD><% $old{$pre.'company'} %></TD>
-    <TD><% $new{$pre.'company'} %></TD>
+    <TD><% $old{$pre.'company'} |h %></TD>
+    <TD><% $new{$pre.'company'} |h %></TD>
   </TR>
 %     }
   <TR>
-    <TD><% $old{$pre.'address1'} %></TD>
-    <TD><% $new{$pre.'address1'} %></TD>
+    <TD><% $old{$pre.'address1'} |h %></TD>
+    <TD><% $new{$pre.'address1'} |h %></TD>
   </TR>
   <TR>
-    <TD><% $old{$pre.'address2'} %></TD>
-    <TD><% $new{$pre.'address2'} %></TD>
+    <TD><% $old{$pre.'address2'} |h %></TD>
+    <TD><% $new{$pre.'address2'} |h %></TD>
   </TR>
   <TR>
-    <TD><% $old{$pre.'city'} %>, <% $old{$pre.'state'} %>  <% $old{$pre.'zip'} %></TD>
-    <TD><% $new{$pre.'city'} %>, <% $new{$pre.'state'} %>  <% $new{$pre.'zip'} %></TD>
+    <TD><% $old{$pre.'city'} |h %>, <% $old{$pre.'state'} |h %>  <% $old{$pre.'zip'} |h %></TD>
+    <TD><% $new{$pre.'city'} |h %>, <% $new{$pre.'state'} |h %>  <% $new{$pre.'zip'} |h %></TD>
   </TR>
 
 %   } # if error
index 024bc17..10ae918 100644 (file)
@@ -13,8 +13,8 @@ Census tract error
 Confirm census tract
 % }
 </B><BR>
-<% $location{address1} %> <% $location{address2} %><BR>
-<% $location{city} %>, <% $location{state} %> <% $location{zip} %><BR>
+<% $location{address1} |h %> <% $location{address2} |h %><BR>
+<% $location{city} |h %>, <% $location{state} |h %> <% $location{zip} |h %><BR>
 <BR>
 % my $querystring = "census_year=$year&latitude=".$cache->get('latitude').'&longitude='.$cache->get('longitude');
 <A HREF="http://maps.ffiec.gov/FFIECMapper/TGMapSrv.aspx?<% $querystring %>"
index 09ff93c..cd4c92f 100644 (file)
@@ -80,14 +80,14 @@ should be used to set msgnum or from/subject/html_body cgi params
       <% include('/elements/tr-fixed.html',
                    'field'      => 'from',
                    'label'      => 'From:',
-                   'value' => scalar( $from ),
+                   'value'      => $from,
                 )
       %>
 
       <% include('/elements/tr-fixed.html',
                    'field'      => 'subject',
                    'label'      => 'Subject:',
-                   'value' => scalar( $subject ),
+                   'value'      => $subject,
                 )
       %>
 
index 28dbf64..1efb4aa 100644 (file)
@@ -1,13 +1,14 @@
 <%doc>
-Requires cgi params 'password' (plaintext) and 'sub' ('validate_password' is only 
-acceptable value.)  Also accepts 'svcnum' (for svc_acct, will otherwise create an
-empty dummy svc_acct) and 'fieldid' (for html post-processing, passed along in 
-results for convenience.)
-
-Returns a json-encoded hashref with keys of 'valid' (set to 1 if object is valid),
-'error' (error text if password is invalid) or 'syserror' (error text if password
-could not be validated.)  Only one of these keys will be set.  Will also set
-'fieldid' if it was passed.
+Requires cgi params 'password' (plaintext) and 'sub' ('validate_password' is
+only acceptable value.)  Also accepts 'svcnum' (for svc_acct, will otherwise
+create an empty dummy svc_acct), 'pkgnum' (for when the svc_acct isn't yet
+inserted), and 'fieldid' (for html post-processing, passed along in results
+for convenience.)
+
+Returns a json-encoded hashref with keys of 'valid' (set to 1 if object is
+valid), 'error' (error text if password is invalid) or 'syserror' (error text
+if password could not be validated.)  Only one of these keys will be set.
+Will also set 'fieldid' if it was passed.
 </%doc>
 
 <% encode_json($result) %>
@@ -32,9 +33,13 @@ my $validate_password = sub {
   $result{'syserror'} = 'Invalid svcnum' unless $svcnum =~ /^\d*$/;
   return \%result if $result{'syserror'};
 
+  my $pkgnum = $arg{'pkgnum'};
+  $result{'syserror'} = 'Invalid pkgnum' unless $pkgnum =~ /^\d*$/;
+  return \%result if $result{'syserror'};
+
   my $svc_acct = $svcnum 
     ? qsearchs('svc_acct',{'svcnum' => $svcnum})
-    : (new FS::svc_acct {});
+    : FS::svc_acct->new({ 'pkgnum' => $pkgnum });
   $result{'syserror'} = 'Could not find service' unless $svc_acct;
   return \%result if $result{'syserror'};
 
index ba80f02..38ec4f4 100755 (executable)
@@ -501,8 +501,13 @@ my $pkgs_method = $conf->exists('hidecancelledpackages')
                     : 'all_pkgs';
 
 #false laziness w/httemplate/view/cust_main/packages.html
-my $select = '*, setup_option.optionvalue AS _opt_setup_fee, '.
-                'recur_option.optionvalue AS _opt_recur_fee',
+my $select = join(',',
+               'cust_pkg.*',
+               'part_pkg.*',
+               'setup_option.optionvalue AS _opt_setup_fee',
+               'recur_option.optionvalue AS _opt_recur_fee',
+             );
+
 my $addl_from = qq{
     LEFT JOIN part_pkg USING ( pkgpart )
     LEFT JOIN part_pkg_option AS setup_option
@@ -513,6 +518,7 @@ my $addl_from = qq{
            AND recur_option.optionname = 'recur_fee' )
 };
 
+local($FS::cust_pkg::cache_enabled) = 1; #for $cust_pkg->part_pkg
 my %all_pkgs = map { $_->custnum =>
                        [ $_->$pkgs_method({ select    => $select,
                                             addl_from => $addl_from,
index f1e686a..dbd346d 100755 (executable)
@@ -44,7 +44,7 @@
                         },
                     sub { my $c = shift;
                           sprintf( $money_char.'%.2f',
-                                   $c->part_pkg->base_recur($c)
+                                   $c->base_recur
                                  );
                         },
                     sub { FS::part_pkg::freq_pretty(shift); },
index 744b313..a8e5033 100644 (file)
@@ -1,120 +1,8 @@
-% if ( $cgi->param('_type') =~ /(xls)$/ ) {
-<%perl>
-  # egregious false laziness w/ search/report_tax-xls.cgi
-  my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format;
-  my $filename = $cgi->url(-relative => 1);
-  $filename =~ s/\.html$//;
-  $filename .= $format->{extension};
-  http_header('Content-Type' => $format->{mime_type});
-  http_header('Content-Disposition' => qq!attachment;filename="$filename"!);
-
-  my $output = '';
-  my $XLS = IO::String->new($output);
-  my $workbook = $format->{class}->new($XLS)
-    or die "Error opening .xls file: $!";
-
-  my $worksheet = $workbook->add_worksheet('Summary');
-
-  my %format = (
-    header => {
-      size      => 11,
-      bold      => 1,
-      align     => 'center',
-      valign    => 'vcenter',
-      text_wrap => 1,
-    },
-    money => {
-      size      => 11,
-      align     => 'right',
-      valign    => 'bottom',
-      num_format=> 8,
-    },
-    '' => {},
-  );
-  my %default = (
-      font      => 'Calibri',
-      border    => 1,
-  );
-  foreach (keys %format) {
-    my %f = (%default, %{$format{$_}});
-    $format{$_} = $workbook->add_format(%f);
-    $format{"m_$_"} = $workbook->add_format(%f);
-  }
-
-  my ($r, $c) = (0, 0);
-  for my $row (@rows) {
-    $c = 0;
-    my $thisrow = shift @cells;
-    for my $cell (@$thisrow) {
-      if (!ref($cell)) {
-        # placeholder, so increment $c so that we write to the correct place
-        $c++;
-        next;
-      }
-      # format name
-      my $f = '';
-      $f = 'header' if $row->{header} or $cell->{header};
-      $f = 'money' if $cell->{format} eq 'money';
-      if ( $cell->{rowspan} > 1 or $cell->{colspan} > 1 ) {
-        my $range = xl_range_formula(
-          'Summary',
-          $r, $r - 1 + ($cell->{rowspan} || 1),
-          $c, $c - 1 + ($cell->{colspan} || 1)
-        );
-        #warn "merging $range\n";
-        $worksheet->merge_range($range, $cell->{value}, $format{"m_$f"});
-      } else {
-      #warn "writing ".xl_rowcol_to_cell($r, $c)."\n";
-        $worksheet->write( $r, $c, $cell->{value}, $format{$f} );
-      }
-      $c++;
-    } #$cell
-  $r++;
-  } #$row
-  $workbook->close;
-
-  http_header('Content-Length' => length($output));
-  $m->print($output);
-</%perl>
-% } else {
-<& /elements/header.html, $title &>
-% my $myself = $cgi->self_url;
-<P ALIGN="right" CLASS="noprint">
-Download full reports<BR>
-as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR>
-% # as <A HREF="<% "$myself;_type=csv" %>">CSV file</A> # is this still needed?
-</P>
-<style type="text/css">
-.report * {
-  background-color: #f8f8f8;
-  border: 1px solid #999999;
-  padding: 2px;
-}
-.report td {
-  text-align: right;
-}
-.total * { background-color: #f5f6be; }
-.shaded * { background-color: #c8c8c8; }
-.totalshaded * { background-color: #bfc094; }
-</style>
-<table class="report" width="100%" cellspacing=0>
-% foreach my $rowinfo (@rows) {
-  <tr<% $rowinfo->{class} ? ' class="'.$rowinfo->{class}.'"' : ''%>>
-%   my $thisrow = shift @cells;
-%   foreach my $cell (@$thisrow) {
-%     next if !ref($cell); # placeholders
-%     my $td = $cell->{header} ? 'th' : 'td';
-%     my $style = '';
-%     $style .= " rowspan=".$cell->{rowspan} if $cell->{rowspan} > 1;
-%     $style .= " colspan=".$cell->{colspan} if $cell->{colspan} > 1;
-      <<%$td%><%$style%>><% $cell->{value} |h %></<%$td%>>
-%   }
-  </tr>
-% }
-</table>
-
-<& /elements/footer.html &>
-% }
+<& elements/grid-report.html,
+  title => $title,
+  rows  => \@rows,
+  cells => \@cells,
+&>
 <%init>
 
 die "access denied"
@@ -224,17 +112,14 @@ my @cells; # arrayrefs of cell info
 $rows[0] = {};
 $cells[0] = [
   { header => 1, rowspan => 2, colspan => ($setuprecur ? 4 : 3) },
-  ($setuprecur ? '' : ()),
   map {
     { header => 1, colspan => ($grossdiscount ? 3 : 2), value => time2str('%b %Y', $_) },
-    ''
   } @{ $data->{speriod} }
 ];
 my $ncols = scalar(@{ $data->{speriod} });
 
 $rows[1] = {};
-$cells[1] = [ '',
-  ($setuprecur ? '' : ()),
+$cells[1] = [
   map { 
   ( ($grossdiscount 
       ? (
@@ -270,8 +155,6 @@ foreach my $cust_main (@cust_main) { # correspond to cross_params
           rowspan => ($setuprecur ? 2 : 1),
         },
       ;
-    } else {
-      push @thisrow, '';
     }
     if ( $setuprecur ) {
       # subheading
@@ -310,8 +193,6 @@ for my $subrow (0..($setuprecur ? 1 : 0)) {
         header => 1,
         colspan => 3,
         rowspan => ($setuprecur ? 2 : 1), };
-  } else {
-    push @thisrow, '';
   }
   if ( $setuprecur ) {
     push @thisrow,
index 8dc0663..c535109 100644 (file)
-% if ( $cgi->param('_type') =~ /(xls)$/ ) {
-<%perl>
-  # egregious false laziness w/ search/report_tax-xls.cgi
-  my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format;
-  my $filename = $cgi->url(-relative => 1);
-  $filename =~ s/\.html$//;
-  $filename .= $format->{extension};
-  http_header('Content-Type' => $format->{mime_type});
-  http_header('Content-Disposition' => qq!attachment;filename="$filename"!);
-
-  my $output = '';
-  my $XLS = IO::String->new($output);
-  my $workbook = $format->{class}->new($XLS)
-    or die "Error opening .xls file: $!";
-
-  my $worksheet = $workbook->add_worksheet('Summary');
-
-  my %format = (
-    header => {
-      size      => 11,
-      bold      => 1,
-      align     => 'center',
-      valign    => 'vcenter',
-      text_wrap => 1,
-    },
-    money => {
-      size      => 11,
-      align     => 'right',
-      valign    => 'bottom',
-      num_format=> 8,
-    },
-    '' => {},
-  );
-  my %default = (
-      font      => 'Calibri',
-      border    => 1,
-  );
-  foreach (keys %format) {
-    my %f = (%default, %{$format{$_}});
-    $format{$_} = $workbook->add_format(%f);
-    $format{"m_$_"} = $workbook->add_format(%f);
-  }
-
-  my ($r, $c) = (0, 0);
-  for my $row (@rows) {
-    $c = 0;
-    my $thisrow = shift @cells;
-    for my $cell (@$thisrow) {
-      if (!ref($cell)) {
-        # placeholder, so increment $c so that we write to the correct place
-        $c++;
-        next;
-      }
-      # format name
-      my $f = '';
-      $f = 'header' if $row->{header} or $cell->{header};
-      $f = 'money' if $cell->{format} eq 'money';
-      if ( $cell->{rowspan} > 1 or $cell->{colspan} > 1 ) {
-        my $range = xl_range_formula(
-          'Summary',
-          $r, $r - 1 + ($cell->{rowspan} || 1),
-          $c, $c - 1 + ($cell->{colspan} || 1)
-        );
-        #warn "merging $range\n";
-        $worksheet->merge_range($range, $cell->{value}, $format{"m_$f"});
-      } else {
-      #warn "writing ".xl_rowcol_to_cell($r, $c)."\n";
-        $worksheet->write( $r, $c, $cell->{value}, $format{$f} );
-      }
-      $c += $cell->{colspan} || 1;
-    } #$cell
-  $r++;
-  } #$row
-  $workbook->close;
-
-  http_header('Content-Length' => length($output));
-  $m->print($output);
-</%perl>
-% } else {
-<& /elements/header.html, $title &>
-% my $myself = $cgi->self_url;
-<P ALIGN="right" CLASS="noprint">
-Download full reports<BR>
-as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A>
-</P>
-<style type="text/css">
-.report * {
-  background-color: #f8f8f8;
-  border: 1px solid #999999;
-  padding: 2px;
-}
-.report td {
-  text-align: right;
-}
-.total { background-color: #f5f6be; }
-.shaded { background-color: #c8c8c8; }
-.totalshaded { background-color: #bfc094; }
-</style>
-<table class="report" width="100%" cellspacing=0>
-% foreach my $rowinfo (@rows) {
-  <tr<% $rowinfo->{class} ? ' class="'.$rowinfo->{class}.'"' : ''%>>
-%   my $thisrow = shift @cells;
-%   foreach my $cell (@$thisrow) {
-%     next if !ref($cell); # placeholders
-%     my $td = $cell->{header} ? 'th' : 'td';
-%     my $style = '';
-%     $style .= ' class="'.$cell->{class}.'"' if $cell->{class};
-%     $style .= " rowspan=".$cell->{rowspan} if $cell->{rowspan} > 1;
-%     $style .= " colspan=".$cell->{colspan} if $cell->{colspan} > 1;
-%     $style .= ' style="color: red"' if $cell->{value} < 0;
-      <<%$td%><%$style%>><% $cell->{value} |h %></<%$td%>>
-%   }
-  </tr>
-% }
-</table>
-
-<& /elements/footer.html &>
-% }
+<& elements/grid-report.html,
+  title => $title,
+  rows  => \@rows,
+  cells => \@cells,
+  head  => $head,
+  # would be better handled with Mason inheritance? consider this. easy enough
+  # to change it at this point.
+&>
 <%init>
 
 die "access denied"
@@ -213,7 +103,11 @@ foreach my $cust_main (@cust_main) { # correspond to cross_params
     for my $item (0..3) { # recur/recur_cost/usage/usage_cost
       my $value = $data->{data}[$item][$col][$row];
       $skip = 0 if abs($value) > 0.005;
-      push @thisrow, { value => sprintf('%0.2f', $value), format => 'money' };
+      push @thisrow, {
+        value => sprintf('%0.2f', $value),
+        format => 'money',
+        class => ($value < 0 ? 'negative' : ''),
+      };
       $total[$col * 5 + $item] += $value;
       $profit += (($item % 2) ? -1 : 1) * $value;
     } #item
@@ -250,4 +144,9 @@ for my $col (0..($ncols * 5)-1) { # month and recur/recur_cost/usage/usage_cost/
 }
 push @cells, \@thisrow;
 
+my $head = q[
+<style>
+  .negative { color: red }
+</style>
+];
 </%init>
diff --git a/httemplate/search/elements/grid-report.html b/httemplate/search/elements/grid-report.html
new file mode 100644 (file)
index 0000000..98e8178
--- /dev/null
@@ -0,0 +1,189 @@
+<%doc>
+
+Simple display front-end for reports that produce some kind of data table,
+which the user can request as an Excel spreadsheet. /elements/header.html
+and /elements/footer.html are included automatically, so don't include them
+again.
+
+This element defines "total", "shaded", and "totalshaded" CSS classes. For
+anything else, insert a <style> element via the 'head' argument.
+
+Usage:
+
+<& elements/grid-report.html,
+  title => 'My Report',
+  rows => [
+    { header => 1, },
+    ...
+  ],
+  cells => [
+    [ # row 0
+      { value => '123.45',
+        # optional
+        format => 'money',
+        header => 1,
+        rowspan => 2,
+        colspan => 3,
+        class => 'shaded',
+      },
+      ...
+    ],
+  ],
+  head => q[<div>Thing to insert before the table</div>],
+  foot => q[<span>That's all folks!</span>].
+&>
+</%doc>
+% if ( $cgi->param('_type') =~ /(xls)$/ ) {
+<%perl>
+  # egregious false laziness w/ search/report_tax-xls.cgi
+  # and search/customer_cdr_profit.html
+  my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format;
+  my $filename = $cgi->url(-relative => 1);
+  $filename =~ s/\.html$//;
+  $filename .= $format->{extension};
+  http_header('Content-Type' => $format->{mime_type});
+  http_header('Content-Disposition' => qq!attachment;filename="$filename"!);
+
+  my $output = '';
+  my $XLS = IO::String->new($output);
+  my $workbook = $format->{class}->new($XLS)
+    or die "Error opening .xls file: $!";
+
+  my $worksheet = $workbook->add_worksheet('Summary');
+
+  my %format = (
+    header => {
+      size      => 11,
+      bold      => 1,
+      align     => 'center',
+      valign    => 'vcenter',
+      text_wrap => 1,
+    },
+    money => {
+      size      => 11,
+      align     => 'right',
+      valign    => 'bottom',
+      num_format=> 8,
+    },
+    '' => {},
+  );
+  my %default = (
+      font      => 'Calibri',
+      border    => 1,
+  );
+  foreach (keys %format) {
+    my %f = (%default, %{$format{$_}});
+    $format{$_} = $workbook->add_format(%f);
+    $format{"m_$_"} = $workbook->add_format(%f);
+  }
+
+  my ($r, $c) = (0, 0);
+  # indices in these correspond to column positions
+  my @rowspans;
+  my @widths;
+  
+  for my $row (@rows) {
+    $c = 0;
+    my $thisrow = shift @cells;
+    for my $cell (@$thisrow) {
+      # skip over cells that are occupied by rowspans above them
+      while ($rowspans[$c]) {
+        $rowspans[$c]--;
+        $c++;
+      }
+
+      # skip this cell if it's empty, also
+      next if !ref($cell);
+      # format name
+      my $f = '';
+      $f = 'header' if $row->{header} or $cell->{header};
+      $f = 'money' if $cell->{format} eq 'money';
+      if ( $cell->{rowspan} > 1 or $cell->{colspan} > 1 ) {
+        my $range = xl_range_formula(
+          'Summary',
+          $r, $r - 1 + ($cell->{rowspan} || 1),
+          $c, $c - 1 + ($cell->{colspan} || 1)
+        );
+        #warn "merging $range\n";
+        $worksheet->merge_range($range, $cell->{value}, $format{"m_$f"});
+      } else {
+      #warn "writing ".xl_rowcol_to_cell($r, $c)."\n";
+        $worksheet->write( $r, $c, $cell->{value}, $format{$f} );
+      }
+
+      # estimate column width, as in search-xls, but without date formats
+      my $width = length($cell->{value}) / ($cell->{colspan} || 1);
+      $width *= 1.1 if $f eq 'header';
+      $width++ if $f eq 'money'; # for money symbol
+      $width += 2; # pad it
+
+      for (1 .. ($cell->{colspan} || 1)) {
+        # adjust minimum widths to allow for this cell's contents
+        $widths[$c] = $width if $width > ($widths[$c] || 0);
+
+        # and if this cell has a rowspan, block off that many rows below it
+        if ( $cell->{rowspan} > 1 ) {
+          $rowspans[$c] = $cell->{rowspan} - 1;
+        }
+        $c++;
+      }
+    } #$cell
+  $r++;
+  } #$row
+
+  $c = 0;
+  for my $c (0 .. scalar(@widths) - 1) {
+    $worksheet->set_column($c, $c, $widths[$c]);
+  }
+  $workbook->close;
+
+  http_header('Content-Length' => length($output));
+  $m->print($output);
+</%perl>
+% } else {
+<& /elements/header.html, $title &>
+<% $head %>
+% my $myself = $cgi->self_url;
+<P ALIGN="right" CLASS="noprint">
+Download full reports<BR>
+as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR>
+</P>
+<style type="text/css">
+.report * {
+  background-color: #f8f8f8;
+  border: 1px solid #999999;
+  padding: 2px;
+}
+.report td {
+  text-align: right;
+}
+.total { background-color: #f5f6be; }
+.shaded { background-color: #c8c8c8; }
+.totalshaded { background-color: #bfc094; }
+</style>
+<table class="report" width="100%" cellspacing=0>
+% foreach my $rowinfo (@rows) {
+  <tr<% $rowinfo->{class} ? ' class="'.$rowinfo->{class}.'"' : ''%>>
+%   my $thisrow = shift @cells;
+%   foreach my $cell (@$thisrow) {
+%     next if !ref($cell); # placeholders
+%     my $td = $cell->{header} ? 'th' : 'td';
+%     my $style = '';
+%     $style .= " rowspan=".$cell->{rowspan} if $cell->{rowspan} > 1;
+%     $style .= " colspan=".$cell->{colspan} if $cell->{colspan} > 1;
+%     $style .= ' class="' . $cell->{class} . '"' if $cell->{class};
+      <<%$td%><%$style%>><% $cell->{value} |h %></<%$td%>>
+%   }
+  </tr>
+% }
+</table>
+<% $foot %>
+<& /elements/footer.html &>
+% }
+<%args>
+$title
+@rows
+@cells
+$head => ''
+$foot => ''
+</%args>
index 39f0324..3d0983e 100644 (file)
    </TR>
 % }
 
-% if ( $conf->config('voip-cdr_email_attach') ) { 
+% if ( my $attach = $conf->config('voip-cdr_email_attach') ) { 
   <TR>
-    <TD ALIGN="right"><% mt('Email CDRs as CSV') |h %></TD>
+    <TD ALIGN="right"><% mt('Email CDRs as '.uc($attach)) |h %></TD>
     <TD BGCOLOR="#ffffff"><% $cust_main->email_csv_cdr ? $yes : $no %></TD>
   </TR>
 % }