diff options
27 files changed, 517 insertions, 314 deletions
@@ -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 diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 903b42e96..41d6db691 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -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', diff --git a/FS/FS/Password_Mixin.pm b/FS/FS/Password_Mixin.pm index ee038db5a..bdfab1881 100644 --- a/FS/FS/Password_Mixin.pm +++ b/FS/FS/Password_Mixin.pm @@ -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 diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index 4daa6372e..76cadc396 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -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', diff --git a/FS/FS/cdr/amcom.pm b/FS/FS/cdr/amcom.pm index 697a6827f..4fb94ee7f 100644 --- a/FS/FS/cdr/amcom.pm +++ b/FS/FS/cdr/amcom.pm @@ -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 diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 67c7d91f7..77b914f2f 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -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, diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index daa3353c1..b1373849f 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -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 index 000000000..d360564c4 --- /dev/null +++ b/FS/FS/part_event/Condition/cust_pay_payby.pm @@ -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 index 000000000..0487ae539 --- /dev/null +++ b/FS/bin/freeside-cdr-evariste-import @@ -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; + + + diff --git a/bin/freeside-debian-releases.sh b/bin/freeside-debian-releases.sh index 82465266f..e832d9176 100644 --- a/bin/freeside-debian-releases.sh +++ b/bin/freeside-debian-releases.sh @@ -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 diff --git a/bin/svc.replace b/bin/svc.replace index ad9cc43a2..a9ceadab2 100644 --- a/bin/svc.replace +++ b/bin/svc.replace @@ -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; diff --git a/debian/changelog b/debian/changelog index eb34e5239..893d60abe 100644 --- a/debian/changelog +++ b/debian/changelog @@ -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. diff --git a/debian/control b/debian/control index efd6234db..c8bffae1b 100644 --- a/debian/control +++ b/debian/control @@ -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 diff --git a/httemplate/docs/about.html b/httemplate/docs/about.html index 8ed4089db..651918017 100644 --- a/httemplate/docs/about.html +++ b/httemplate/docs/about.html @@ -28,7 +28,7 @@ % } else { <FONT SIZE="-1"> % } -© 2015 Freeside Internet Services, Inc.<BR> +© 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> diff --git a/httemplate/edit/cust_main/billing.html b/httemplate/edit/cust_main/billing.html index c4b66c8d5..519f2f8f2 100644 --- a/httemplate/edit/cust_main/billing.html +++ b/httemplate/edit/cust_main/billing.html @@ -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 %>"> diff --git a/httemplate/edit/svc_acct.cgi b/httemplate/edit/svc_acct.cgi index ff8e31615..42660462d 100755 --- a/httemplate/edit/svc_acct.cgi +++ b/httemplate/edit/svc_acct.cgi @@ -52,9 +52,10 @@ <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> diff --git a/httemplate/elements/validate_password.html b/httemplate/elements/validate_password.html index a488c4f16..f067ad8fc 100644 --- a/httemplate/elements/validate_password.html +++ b/httemplate/elements/validate_password.html @@ -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); diff --git a/httemplate/misc/confirm-address_standardize.html b/httemplate/misc/confirm-address_standardize.html index 0a05c70bd..9d1a5c135 100644 --- a/httemplate/misc/confirm-address_standardize.html +++ b/httemplate/misc/confirm-address_standardize.html @@ -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 diff --git a/httemplate/misc/confirm-censustract.html b/httemplate/misc/confirm-censustract.html index 024bc17c4..10ae91812 100644 --- a/httemplate/misc/confirm-censustract.html +++ b/httemplate/misc/confirm-censustract.html @@ -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 %>" diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html index 09ff93cca..cd4c92f23 100644 --- a/httemplate/misc/email-customers.html +++ b/httemplate/misc/email-customers.html @@ -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, ) %> diff --git a/httemplate/misc/xmlhttp-validate_password.html b/httemplate/misc/xmlhttp-validate_password.html index 28dbf6460..1efb4aaa3 100644 --- a/httemplate/misc/xmlhttp-validate_password.html +++ b/httemplate/misc/xmlhttp-validate_password.html @@ -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'}; diff --git a/httemplate/search/cust_main.cgi b/httemplate/search/cust_main.cgi index ba80f0275..38ec4f453 100755 --- a/httemplate/search/cust_main.cgi +++ b/httemplate/search/cust_main.cgi @@ -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, diff --git a/httemplate/search/cust_pkg.cgi b/httemplate/search/cust_pkg.cgi index f1e686a83..dbd346dba 100755 --- a/httemplate/search/cust_pkg.cgi +++ b/httemplate/search/cust_pkg.cgi @@ -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); }, diff --git a/httemplate/search/customer_accounting_summary.html b/httemplate/search/customer_accounting_summary.html index 744b313f9..a8e503324 100644 --- a/httemplate/search/customer_accounting_summary.html +++ b/httemplate/search/customer_accounting_summary.html @@ -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, diff --git a/httemplate/search/customer_cdr_profit.html b/httemplate/search/customer_cdr_profit.html index 8dc06636a..c5351094f 100644 --- a/httemplate/search/customer_cdr_profit.html +++ b/httemplate/search/customer_cdr_profit.html @@ -1,121 +1,11 @@ -% 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 index 000000000..98e81785f --- /dev/null +++ b/httemplate/search/elements/grid-report.html @@ -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> diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html index 39f032499..3d0983e67 100644 --- a/httemplate/view/cust_main/billing.html +++ b/httemplate/view/cust_main/billing.html @@ -385,9 +385,9 @@ </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> % } |