summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/ClientAPI/Freeside.pm22
-rw-r--r--FS/FS/ClientAPI_XMLRPC.pm1
-rw-r--r--FS/FS/Conf.pm16
-rw-r--r--FS/FS/ConfDefaults.pm3
-rw-r--r--FS/FS/Cron/pay_batch.pm103
-rw-r--r--FS/FS/Misc.pm11
-rw-r--r--FS/FS/Setup.pm7
-rw-r--r--FS/FS/Template_Mixin.pm3
-rw-r--r--FS/FS/UI/Web.pm6
-rw-r--r--FS/FS/UI/Web/small_custview.pm25
-rw-r--r--FS/FS/contact.pm7
-rw-r--r--FS/FS/cust_main.pm81
-rw-r--r--FS/FS/cust_main/Billing.pm21
-rw-r--r--FS/FS/cust_pay.pm24
-rw-r--r--FS/FS/cust_pay_batch.pm1
-rw-r--r--FS/FS/cust_pkg.pm33
-rw-r--r--FS/FS/cust_pkg/Search.pm63
-rw-r--r--FS/FS/msg_template/email.pm15
-rw-r--r--FS/FS/part_export/broadband_sqlradius.pm2
-rw-r--r--FS/FS/part_export/sqlradius.pm14
-rw-r--r--FS/FS/part_pkg.pm69
-rw-r--r--FS/FS/part_pkg/voip_cdr.pm12
-rw-r--r--FS/FS/pay_batch.pm24
-rw-r--r--FS/FS/pay_batch/paymentech.pm9
-rw-r--r--FS/FS/payinfo_Mixin.pm5
-rw-r--r--FS/FS/webservice_log.pm34
-rwxr-xr-xFS/bin/freeside-upgrade8
-rwxr-xr-xFS/t/suite/15-activate_encryption.t106
-rw-r--r--fs_selfservice/FS-SelfService/cgi/view_cdr_details.html25
-rw-r--r--httemplate/edit/agent_payment_gateway.html2
-rwxr-xr-xhttemplate/edit/cust_pkg_discount.html10
-rw-r--r--httemplate/elements/tr-select-pkg-discount.html4
-rwxr-xr-xhttemplate/misc/bulk_change_pkg.cgi28
-rwxr-xr-xhttemplate/misc/bulk_pkg_increment_bill.cgi28
-rwxr-xr-xhttemplate/misc/process/bulk_change_pkg.cgi39
-rwxr-xr-xhttemplate/misc/process/bulk_pkg_increment_bill.cgi40
-rwxr-xr-xhttemplate/misc/process/cancel_pkg.html2
-rw-r--r--httemplate/search/cust_credit_bill_pkg.html2
-rw-r--r--httemplate/search/cust_msg.html5
-rwxr-xr-xhttemplate/search/cust_pkg.cgi2
-rw-r--r--httemplate/search/elements/grouped-search/core2
-rw-r--r--httemplate/search/elements/grouped-search/html10
-rwxr-xr-xhttemplate/search/report_cust_pkg.html62
-rw-r--r--httemplate/view/cust_main/packages/package.html2
-rwxr-xr-xhttemplate/view/cust_msg.html4
-rw-r--r--httemplate/view/directions.html3
-rw-r--r--ng_selfservice/elements/add_password_validation.php51
-rw-r--r--ng_selfservice/images/error.pngbin0 -> 666 bytes
-rw-r--r--ng_selfservice/images/tick.pngbin0 -> 537 bytes
-rw-r--r--ng_selfservice/password.php89
-rw-r--r--ng_selfservice/xmlrpc_validate_passwd.php15
51 files changed, 961 insertions, 189 deletions
diff --git a/FS/FS/ClientAPI/Freeside.pm b/FS/FS/ClientAPI/Freeside.pm
index 8aa61e6..42b9c42 100644
--- a/FS/FS/ClientAPI/Freeside.pm
+++ b/FS/FS/ClientAPI/Freeside.pm
@@ -44,16 +44,34 @@ sub freesideinc_service {
return { 'error' => 'bad support-key' };
}
+ my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+ my $custnum = $cust_pkg->custnum;
+
+ my $quantity = $packet->{'quantity'} || 1;
+
+ #false laziness w/webservice_log.pm
+ my $color = 1.10;
+ my $page = 0.10;
+
#XXX check if some customers can use some API calls, rate-limiting, etc.
# but for now, everybody can use everything
+ if ( $packet->{method} eq 'print' ) {
+ my $avail_credit = $cust_pkg->cust_main->credit_limit
+ - $color - $quantity * $page
+ - FS::webservice_log->price_print(
+ 'custnum' => $custnum,
+ );
+
+ return { 'error' => 'Over credit limit' }
+ if $avail_credit <= 0;
+ }
#record it happened
- my $custnum = $svc_acct->cust_svc->cust_pkg->custnum;
my $webservice_log = new FS::webservice_log {
'custnum' => $custnum,
'svcnum' => $svc_acct->svcnum,
'method' => $packet->{'method'},
- 'quantity' => $packet->{'quantity'} || 1,
+ 'quantity' => $quantity,
};
my $error = $webservice_log->insert;
return { 'error' => $error } if $error;
diff --git a/FS/FS/ClientAPI_XMLRPC.pm b/FS/FS/ClientAPI_XMLRPC.pm
index 3167aa0..e69a06e 100644
--- a/FS/FS/ClientAPI_XMLRPC.pm
+++ b/FS/FS/ClientAPI_XMLRPC.pm
@@ -181,6 +181,7 @@ sub ss2clientapi {
'reset_passwd' => 'MyAccount/reset_passwd',
'check_reset_passwd' => 'MyAccount/check_reset_passwd',
'process_reset_passwd' => 'MyAccount/process_reset_passwd',
+ 'validate_passwd' => 'MyAccount/validate_passwd',
'list_tickets' => 'MyAccount/list_tickets',
'create_ticket' => 'MyAccount/create_ticket',
'get_ticket' => 'MyAccount/get_ticket',
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index ec317ba..0d561a2 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -669,6 +669,7 @@ my %batch_gateway_options = (
);
map { $_->gatewaynum, $_->label } @gateways;
},
+ 'per_agent' => 1,
);
my %invoice_mode_options = (
@@ -1565,6 +1566,14 @@ and customer address. Include units.',
},
{
+ 'key' => 'invoice_omit_due_date',
+ 'section' => 'invoice_balances',
+ 'description' => 'Omit the "Please pay by (date)" from invoices.',
+ 'type' => 'checkbox',
+ 'per_agent' => 1,
+ },
+
+ {
'key' => 'invoice_sections',
'section' => 'invoicing',
'description' => 'Split invoice into sections and label according to package category when enabled.',
@@ -2909,6 +2918,13 @@ and customer address. Include units.',
},
{
+ 'key' => 'selfservice-password_reset_hours',
+ 'section' => 'self-service',
+ 'description' => 'Numbers of hours an email password reset is valid. Defaults to 24.',
+ 'type' => 'text',
+ },
+
+ {
'key' => 'selfservice-password_reset_msgnum',
'section' => 'self-service',
'description' => 'Template to use for password reset emails.',
diff --git a/FS/FS/ConfDefaults.pm b/FS/FS/ConfDefaults.pm
index 2fa8344..2c24b13 100644
--- a/FS/FS/ConfDefaults.pm
+++ b/FS/FS/ConfDefaults.pm
@@ -33,6 +33,9 @@ sub cust_fields_avail { (
'Cust# | Cust. Status | Customer' =>
'custnum | Status | Last, First or Company (Last, First)',
+ 'Agent | Agent Cust# or Cust# | Cust. Status | Customer' =>
+ 'Agent | Agent Cust# | Status | Last, First or Company (Last, First)',
+
'Customer | Day phone | Night phone | Mobile phone | Fax number' =>
'Customer | (all phones)',
'Cust# | Customer | Day phone | Night phone | Mobile phone | Fax number' =>
diff --git a/FS/FS/Cron/pay_batch.pm b/FS/FS/Cron/pay_batch.pm
index 9791749..1e110f2 100644
--- a/FS/FS/Cron/pay_batch.pm
+++ b/FS/FS/Cron/pay_batch.pm
@@ -22,6 +22,38 @@ $me = '[FS::Cron::pay_batch]';
# -r: Multi-process mode dry run option
# -a: Only process customers with the specified agentnum
+sub batch_gateways {
+ my $conf = FS::Conf->new;
+ # returns a list of arrayrefs: [ gateway, payby, agentnum ]
+ my %opt = @_;
+ my @agentnums;
+ if ( $conf->exists('batch-spoolagent') ) {
+ if ( $opt{a} ) {
+ @agentnums = split(',', $opt{a});
+ } else {
+ @agentnums = map { $_->agentnum } qsearch('agent');
+ }
+ } else {
+ @agentnums = ('');
+ if ( $opt{a} ) {
+ warn "Payment batch processing skipped in per-agent mode.\n" if $DEBUG;
+ return;
+ }
+ }
+ my @return;
+ foreach my $agentnum (@agentnums) {
+ my %gateways;
+ foreach my $payby ('CARD', 'CHEK') {
+ my $gatewaynum = $conf->config("batch-gateway-$payby", $agentnum);
+ next if !$gatewaynum;
+ my $gateway = FS::payment_gateway->by_key($gatewaynum)
+ or die "payment_gateway '$gatewaynum' not found\n";
+ push @return, [ $gateway, $payby, $agentnum ];
+ }
+ }
+ @return;
+}
+
sub pay_batch_submit {
my %opt = @_;
local $DEBUG = ($opt{l} || 1) if $opt{v};
@@ -31,25 +63,14 @@ sub pay_batch_submit {
my $dbh = dbh;
warn "$me batch_submit\n" if $DEBUG;
- my $conf = FS::Conf->new;
-
- # need to respect -a somehow, but for now none of this is per-agent
- if ( $opt{a} ) {
- warn "Payment batch processing skipped in per-agent mode.\n" if $DEBUG;
- return;
- }
- my %gateways;
- foreach my $payby ('CARD', 'CHEK') {
- my $gatewaynum = $conf->config("batch-gateway-$payby");
- next if !$gatewaynum;
- my $gateway = FS::payment_gateway->by_key($gatewaynum)
- or die "payment_gateway '$gatewaynum' not found\n";
-
+ foreach my $config (batch_gateways(%opt)) {
+ my ($gateway, $payby, $agentnum) = @$config;
if ( $gateway->batch_processor->can('default_transport') ) {
- foreach my $pay_batch (
- qsearch('pay_batch', { status => 'O', payby => $payby })
- ) {
+ my $search = { status => 'O', payby => $payby };
+ $search->{agentnum} = $agentnum if $agentnum;
+
+ foreach my $pay_batch ( qsearch('pay_batch', $search) ) {
warn "Exporting batch ".$pay_batch->batchnum."\n" if $DEBUG;
eval { $pay_batch->export_to_gateway( $gateway, debug => $DEBUG ); };
@@ -80,38 +101,28 @@ sub pay_batch_receive {
my $error;
warn "$me batch_receive\n" if $DEBUG;
- my $conf = FS::Conf->new;
- # need to respect -a somehow, but for now none of this is per-agent
- if ( $opt{a} ) {
- warn "Payment batch processing skipped in per-agent mode.\n" if $DEBUG;
- return;
- }
- my %gateways;
- foreach my $payby ('CARD', 'CHEK') {
- my $gatewaynum = $conf->config("batch-gateway-$payby");
- next if !$gatewaynum;
- # If the same gateway is selected for both paybys, only import it once
- $gateways{$gatewaynum} = FS::payment_gateway->by_key($gatewaynum);
- if ( !$gateways{$gatewaynum} ) {
+ my %gateway_done;
+ # If a gateway is selected for more than one payby+agentnum, still
+ # only import from it once; we expect it will send back multiple
+ # result batches.
+ foreach my $config (batch_gateways(%opt)) {
+ my ($gateway, $payby, $agentnum) = @$config;
+ next if $gateway_done{$gateway->gatewaynum};
+ next unless $gateway->batch_processor->can('default_transport');
+ # already warned about this above
+ warn "Importing results from '".$gateway->label."'\n" if $DEBUG;
+ # Note that import_from_gateway is not agent-limited; if a gateway
+ # returns results for batches not associated with this agent, we will
+ # still accept them. Well-behaved gateways will not do that.
+ $error = eval {
+ FS::pay_batch->import_from_gateway( gateway =>$gateway, debug => $DEBUG )
+ } || $@;
+ if ( $error ) {
+ # this we can roll back
$dbh->rollback;
- die "batch-gateway-$payby gateway $gatewaynum not found\n";
+ die "error receiving from gateway '".$gateway->label."':\n$error\n";
}
- }
-
- foreach my $gateway (values %gateways) {
- if ( $gateway->batch_processor->can('default_transport') ) {
- warn "Importing results from '".$gateway->label."'\n" if $DEBUG;
- $error = eval {
- FS::pay_batch->import_from_gateway( gateway =>$gateway, debug => $DEBUG )
- } || $@;
- if ( $error ) {
- # this we can roll back
- $dbh->rollback;
- die "error receiving from gateway '".$gateway->label."':\n$error\n";
- }
- }
- # else we already warned about it above
} #$gateway
# resolve batches if we can
diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm
index 9a43180..a2d1b3e 100644
--- a/FS/FS/Misc.pm
+++ b/FS/FS/Misc.pm
@@ -256,10 +256,17 @@ sub send_email {
}
push @to, $options{bcc} if defined($options{bcc});
+ # fully unpack all addresses found in @to (including Bcc) to make the
+ # envelope list
+ my @env_to;
+ foreach my $dest (@to) {
+ push @env_to, map { $_->address } Email::Address->parse($dest);
+ }
+
local $@; # just in case
eval { sendmail($message, { transport => $transport,
from => $from,
- to => \@to }) };
+ to => \@env_to }) };
my $error = '';
if(ref($@) and $@->isa('Email::Sender::Failure')) {
@@ -274,7 +281,7 @@ sub send_email {
if ( $conf->exists('log_sent_mail') ) {
my $cust_msg = FS::cust_msg->new({
'env_from' => $options{'from'},
- 'env_to' => join(', ', @to),
+ 'env_to' => join(', ', @env_to),
'header' => $message->header_as_string,
'body' => $message->body_as_string,
'_date' => $time,
diff --git a/FS/FS/Setup.pm b/FS/FS/Setup.pm
index 0c3226a..f005a36 100644
--- a/FS/FS/Setup.pm
+++ b/FS/FS/Setup.pm
@@ -7,7 +7,6 @@ use vars qw( @EXPORT_OK );
use Tie::IxHash;
use Crypt::OpenSSL::RSA;
use FS::UID qw( dbh driver_name );
-#use FS::Record;
use FS::svc_domain;
$FS::svc_domain::whois_hack = 1;
@@ -99,6 +98,12 @@ sub enable_encryption {
$conf->set('encryptionpublickey', $rsa->get_public_key_string );
$conf->set('encryptionprivatekey', $rsa->get_private_key_string );
+ # reload Record globals, false laziness with FS::Record
+ $FS::Record::conf_encryption = $conf->exists('encryption');
+ $FS::Record::conf_encryptionmodule = $conf->config('encryptionmodule');
+ $FS::Record::conf_encryptionpublickey = join("\n",$conf->config('encryptionpublickey'));
+ $FS::Record::conf_encryptionprivatekey = join("\n",$conf->config('encryptionprivatekey'));
+
}
sub enable_banned_pay_pad {
diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm
index caa31f7..d7add71 100644
--- a/FS/FS/Template_Mixin.pm
+++ b/FS/FS/Template_Mixin.pm
@@ -1952,7 +1952,8 @@ sub balance_due_msg {
# (yes, or if invoice_sections is enabled; this is just for compatibility)
if ( $self->due_date ) {
$msg .= ' - ' . $self->mt('Please pay by'). ' '.
- $self->due_date2str('short');
+ $self->due_date2str('short')
+ unless $self->conf->config_bool('invoice_omit_due_date');
} elsif ( $self->terms ) {
$msg .= ' - '. $self->mt($self->terms);
}
diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm
index 04aeda1..6cc04b9 100644
--- a/FS/FS/UI/Web.pm
+++ b/FS/FS/UI/Web.pm
@@ -300,7 +300,7 @@ sub cust_header {
my %header2method = (
'Customer' => 'name',
'Cust. Status' => 'cust_status_label',
- 'Cust#' => 'custnum',
+ 'Cust#' => 'display_custnum',
'Name' => 'contact',
'Company' => 'company',
@@ -347,6 +347,8 @@ sub cust_header {
# 'Payment Type' => 'cust_payby',
'Current Balance' => 'current_balance',
'Agent Cust#' => 'agent_custid',
+ 'Agent' => 'agent_name',
+ 'Agent Cust# or Cust#' => 'display_custnum',
'Advertising Source' => 'referral',
);
$header2method{'Cust#'} = 'display_custnum'
@@ -450,6 +452,8 @@ sub cust_sql_fields {
}
push @fields, 'agent_custid';
+ push @fields, 'agentnum' if grep { $_ eq 'agent_name' } @cust_fields;
+
my @extra_fields = ();
if (grep { $_ eq 'current_balance' } @cust_fields) {
push @extra_fields, FS::cust_main->balance_sql . " AS current_balance";
diff --git a/FS/FS/UI/Web/small_custview.pm b/FS/FS/UI/Web/small_custview.pm
index e82e332..85bee7d 100644
--- a/FS/FS/UI/Web/small_custview.pm
+++ b/FS/FS/UI/Web/small_custview.pm
@@ -142,16 +142,25 @@ sub small_custview {
}
$html .= '</TD></TR><TR><TD></TD><TD BGCOLOR="#ffffff">';
- if ( $cust_main->daytime && $cust_main->night ) {
- $html .= ( FS::Msgcat::_gettext('daytime') || 'Day' ).
- ' '. $cust_main->daytime.
- '<BR>'. ( FS::Msgcat::_gettext('night') || 'Night' ).
- ' '. $cust_main->night;
- } elsif ( $cust_main->daytime || $cust_main->night ) {
- $html .= $cust_main->daytime || $cust_main->night;
+
+ my $num_numbers = 0;
+ $num_numbers++ foreach grep $cust_main->$_(), qw( daytime night mobile );
+ if ( $num_numbers > 1 ) {
+ $html .= ucfirst( FS::Msgcat::_gettext('daytime') ).
+ ' '. $cust_main->daytime. '<BR>'
+ if $cust_main->daytime;
+ $html .= ucfirst( FS::Msgcat::_gettext('night') ).
+ ' '. $cust_main->night. '<BR>'
+ if $cust_main->night;
+ $html .= ucfirst( FS::Msgcat::_gettext('mobile') ).
+ ' '. $cust_main->mobile. '<BR>'
+ if $cust_main->night;
+ } elsif ( $num_numbers ) { # == 1 ) {
+ $html .= ( $cust_main->daytime || $cust_main->night || $cust_main->mobile ).
+ '<BR>';
}
if ( $cust_main->fax ) {
- $html .= '<BR>Fax '. $cust_main->fax;
+ $html .= 'Fax '. $cust_main->fax;
}
$html .= '</TD></TR></TABLE></TD>';
diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm
index a8aa43b..e49f6df 100644
--- a/FS/FS/contact.pm
+++ b/FS/FS/contact.pm
@@ -873,7 +873,10 @@ sub send_reset_email {
'svcnum' => $opt{'svcnum'},
};
- my $timeout = '24 hours'; #?
+
+ my $conf = new FS::Conf;
+ my $timeout =
+ ($conf->config('selfservice-password_reset_hours') || 24 ). ' hours';
my $reset_session_id;
do {
@@ -885,8 +888,6 @@ sub send_reset_email {
#email it
- my $conf = new FS::Conf;
-
my $cust_main = '';
my @cust_contact = grep $_->selfservice_access, $self->cust_contact;
$cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 71552b0..4bd3f26 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -5784,13 +5784,94 @@ sub _upgrade_data { #class method
$class->_upgrade_otaker(%opts);
+ # turn on encryption as part of regular upgrade, so all new records are immediately encrypted
+ # existing records will be encrypted in queueable_upgrade (below)
+ unless ($conf->exists('encryptionpublickey') || $conf->exists('encryptionprivatekey')) {
+ eval "use FS::Setup";
+ die $@ if $@;
+ FS::Setup::enable_encryption();
+ }
+
}
sub queueable_upgrade {
my $class = shift;
+
+ ### encryption gets turned on in _upgrade_data, above
+
+ eval "use FS::upgrade_journal";
+ die $@ if $@;
+
+ # prior to 2013 (commit f16665c9) payinfo was stored in history if not encrypted,
+ # clear that out before encrypting/tokenizing anything else
+ if (!FS::upgrade_journal->is_done('clear_payinfo_history')) {
+ foreach my $table ('cust_payby','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') {
+ my $sql = 'UPDATE h_'.$table.' SET payinfo = NULL WHERE payinfo IS NOT NULL';
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+ }
+ FS::upgrade_journal->set_done('clear_payinfo_history');
+ }
+
+ # encrypt old records
+ if ($conf->exists('encryption') && !FS::upgrade_journal->is_done('encryption_check')) {
+
+ # allow replacement of closed cust_pay/cust_refund records
+ local $FS::payinfo_Mixin::allow_closed_replace = 1;
+
+ # because it looks like nothing's changing
+ local $FS::Record::no_update_diff = 1;
+
+ # commit everything immediately
+ local $FS::UID::AutoCommit = 1;
+
+ # encrypt what's there
+ foreach my $table ('cust_payby','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') {
+ my $tclass = 'FS::'.$table;
+ my $lastrecnum = 0;
+ my @recnums = ();
+ while (my $recnum = _upgrade_next_recnum(dbh,$table,\$lastrecnum,\@recnums)) {
+ my $record = $tclass->by_key($recnum);
+ next unless $record; # small chance it's been deleted, that's ok
+ next unless grep { $record->payby eq $_ } @FS::Record::encrypt_payby;
+ # window for possible conflict is practically nonexistant,
+ # but just in case...
+ $record = $record->select_for_update;
+ my $error = $record->replace;
+ die $error if $error;
+ }
+ }
+
+ FS::upgrade_journal->set_done('encryption_check');
+ }
+
+ # now that everything's encrypted, tokenize...
FS::cust_main::Billing_Realtime::token_check(@_);
}
+# not entirely false laziness w/ Billing_Realtime::_token_check_next_recnum
+# cust_payby might get deleted while this runs
+# not a method!
+sub _upgrade_next_recnum {
+ my ($dbh,$table,$lastrecnum,$recnums) = @_;
+ my $recnum = shift @$recnums;
+ return $recnum if $recnum;
+ my $tclass = 'FS::'.$table;
+ my $sql = 'SELECT '.$tclass->primary_key.
+ ' FROM '.$table.
+ ' WHERE '.$tclass->primary_key.' > '.$$lastrecnum.
+ ' ORDER BY '.$tclass->primary_key.' LIMIT 500';;
+ my $sth = $dbh->prepare($sql) or die $dbh->errstr;
+ $sth->execute() or die $sth->errstr;
+ my @recnums;
+ while (my $rec = $sth->fetchrow_hashref) {
+ push @$recnums, $rec->{$tclass->primary_key};
+ }
+ $sth->finish();
+ $$lastrecnum = $$recnums[-1];
+ return shift @$recnums;
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm
index 4821ce5..6932647 100644
--- a/FS/FS/cust_main/Billing.pm
+++ b/FS/FS/cust_main/Billing.pm
@@ -544,14 +544,19 @@ sub bill {
foreach my $part_pkg ( @part_pkg ) {
- $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
+ my $this_cust_pkg = $cust_pkg;
+ # for add-on packages, copy the object to avoid leaking changes back to
+ # the caller if pkg_list is in use; see RT#73607
+ if ( $part_pkg->get('pkgpart') != $real_pkgpart ) {
+ $this_cust_pkg = FS::cust_pkg->new({ %hash });
+ }
my $pass = '';
- if ( $cust_pkg->separate_bill ) {
+ if ( $this_cust_pkg->separate_bill ) {
# if no_auto is also set, that's fine. we just need to not have
# invoices that are both auto and no_auto, and since the package
# gets an invoice all to itself, it will only be one or the other.
- $pass = $cust_pkg->pkgnum;
+ $pass = $this_cust_pkg->pkgnum;
if (!exists $cust_bill_pkg{$pass}) { # it may not exist yet
push @passes, $pass;
$total_setup{$pass} = do { my $z = 0; \$z };
@@ -565,17 +570,17 @@ sub bill {
);
$cust_bill_pkg{$pass} = [];
}
- } elsif ( ($cust_pkg->no_auto || $part_pkg->no_auto) ) {
+ } elsif ( ($this_cust_pkg->no_auto || $part_pkg->no_auto) ) {
$pass = 'no_auto';
}
- my $next_bill = $cust_pkg->getfield('bill') || 0;
+ my $next_bill = $this_cust_pkg->getfield('bill') || 0;
my $error;
# let this run once if this is the last bill upon cancellation
while ( $next_bill <= $cmp_time or $options{cancel} ) {
$error =
$self->_make_lines( 'part_pkg' => $part_pkg,
- 'cust_pkg' => $cust_pkg,
+ 'cust_pkg' => $this_cust_pkg,
'precommit_hooks' => \@precommit_hooks,
'line_items' => $cust_bill_pkg{$pass},
'setup' => $total_setup{$pass},
@@ -590,12 +595,12 @@ sub bill {
last if $error;
# or if we're not incrementing the bill date.
- last if ($cust_pkg->getfield('bill') || 0) == $next_bill;
+ last if ($this_cust_pkg->getfield('bill') || 0) == $next_bill;
# or if we're letting it run only once
last if $options{cancel};
- $next_bill = $cust_pkg->getfield('bill') || 0;
+ $next_bill = $this_cust_pkg->getfield('bill') || 0;
#stop if -o was passed to freeside-daily
last if $options{'one_recur'};
diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm
index b15920b..64ce7ec 100644
--- a/FS/FS/cust_pay.pm
+++ b/FS/FS/cust_pay.pm
@@ -1216,6 +1216,30 @@ sub _upgrade_data { #class method
###
$class->upgrade_set_cardtype;
+ # for batch payments, make sure paymask is set
+ do {
+ local $FS::payinfo_Mixin::allow_closed_replace = 1;
+ local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
+
+ my $cursor = FS::Cursor->new({
+ table => 'cust_pay',
+ extra_sql => ' WHERE paymask IS NULL AND payinfo IS NOT NULL
+ AND payby IN(\'CARD\', \'CHEK\')
+ AND batchnum IS NOT NULL',
+ });
+
+ # records from cursors for some reason don't decrypt payinfo, so
+ # call replace_old to fetch the record "normally"
+ while (my $cust_pay = $cursor->fetch) {
+ $cust_pay = $cust_pay->replace_old;
+ $cust_pay->set('paymask', $cust_pay->mask_payinfo);
+ my $error = $cust_pay->replace;
+ if ($error) {
+ die "$error (setting masked payinfo on payment#". $cust_pay->paynum.
+ ")\n"
+ }
+ }
+ };
}
sub process_upgrade_paybatch {
diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm
index 8127c6a..d29c6d0 100644
--- a/FS/FS/cust_pay_batch.pm
+++ b/FS/FS/cust_pay_batch.pm
@@ -297,6 +297,7 @@ sub approve {
'custnum' => $new->custnum,
'payby' => $new->payby,
'payinfo' => $new->payinfo || $old->payinfo,
+ 'paymask' => $new->mask_payinfo,
'paid' => $new->paid,
'_date' => $new->_date,
'usernum' => $new->usernum,
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index bcb5176..b491f91 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -38,6 +38,8 @@ use FS::sales;
# for modify_charge
use FS::cust_credit;
+use Data::Dumper;
+
# temporary fix; remove this once (un)suspend admin notices are cleaned up
use FS::Misc qw(send_email);
@@ -3017,7 +3019,7 @@ sub modify_charge {
$pkg_opt_modified = 1;
}
}
- $pkg_opt_modified = 1 if (scalar(@old_additional) - 1) != $i;
+ $pkg_opt_modified = 1 if scalar(@old_additional) != $i;
$pkg_opt{'additional_count'} = $i if $i > 0;
my $old_classnum;
@@ -3171,9 +3173,6 @@ sub modify_charge {
'';
}
-
-
-use Data::Dumper;
sub process_bulk_cust_pkg {
my $job = shift;
my $param = shift;
@@ -5582,6 +5581,32 @@ sub _upgrade_data { # class method
my $error = $part_pkg_link->remove_linked;
die $error if $error;
}
+
+ # RT#73607: canceling a package with billing addons sometimes changes its
+ # pkgpart.
+ # Find records where the last replace_new record for the package before it
+ # was canceled has a different pkgpart from the package itself.
+ my @cust_pkg = qsearch({
+ 'table' => 'cust_pkg',
+ 'select' => 'cust_pkg.*, h_cust_pkg.pkgpart AS h_pkgpart',
+ 'addl_from' => ' JOIN (
+ SELECT pkgnum, MAX(historynum) AS historynum FROM h_cust_pkg
+ WHERE cancel IS NULL
+ AND history_action = \'replace_new\'
+ GROUP BY pkgnum
+ ) AS last_history USING (pkgnum)
+ JOIN h_cust_pkg USING (historynum)',
+ 'extra_sql' => ' WHERE cust_pkg.cancel is not null
+ AND cust_pkg.pkgpart != h_cust_pkg.pkgpart'
+ });
+ foreach my $cust_pkg ( @cust_pkg ) {
+ my $pkgnum = $cust_pkg->pkgnum;
+ warn "fixing pkgpart on canceled pkg#$pkgnum\n";
+ $cust_pkg->set('pkgpart', $cust_pkg->h_pkgpart);
+ my $error = $cust_pkg->replace;
+ die $error if $error;
+ }
+
}
=back
diff --git a/FS/FS/cust_pkg/Search.pm b/FS/FS/cust_pkg/Search.pm
index 89809de..311dbdb 100644
--- a/FS/FS/cust_pkg/Search.pm
+++ b/FS/FS/cust_pkg/Search.pm
@@ -281,7 +281,7 @@ sub search {
}
###
- # parse refnum (advertising source)
+ # parse (customer) refnum (advertising source)
###
if ( exists($params->{'refnum'}) ) {
@@ -292,7 +292,7 @@ sub search {
@refnum = ( $params->{'refnum'} );
}
my $in = join(',', grep /^\d+$/, @refnum);
- push @where, "refnum IN($in)" if length $in;
+ push @where, "cust_main.refnum IN($in)" if length $in;
}
###
@@ -450,7 +450,8 @@ sub search {
'' => {},
);
- if( exists($params->{'active'} ) ) {
+ if ( exists($params->{'active'} ) ) {
+
# This overrides all the other date-related fields, and includes packages
# that were active at some time during the interval. It excludes:
# - packages that were set up after the end of the interval
@@ -464,40 +465,51 @@ sub search {
"(cust_pkg.cancel IS NULL OR cust_pkg.cancel >= $beginning )",
"(cust_pkg.susp IS NULL OR cust_pkg.susp >= $beginning )",
"NOT (".FS::cust_pkg->onetime_sql . ")";
- }
- else {
+
+ } else {
+
my $exclude_change_from = 0;
my $exclude_change_to = 0;
foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel )) {
- next unless exists($params->{$field});
+ if ( $params->{$field.'_null'} ) {
+
+ push @where, "cust_pkg.$field IS NULL";
+ # this should surely be obsoleted by now: OR cust_pkg.$field == 0 )
- my($beginning, $ending) = @{$params->{$field}};
+ } else {
- next if $beginning == 0 && $ending == 4294967295;
+ next unless exists($params->{$field});
+
+ my($beginning, $ending) = @{$params->{$field}};
+
+ next if $beginning == 0 && $ending == 4294967295;
+
+ push @where,
+ "cust_pkg.$field IS NOT NULL",
+ "cust_pkg.$field >= $beginning",
+ "cust_pkg.$field <= $ending";
+
+ $orderby ||= "ORDER BY cust_pkg.$field";
+
+ if ( $field eq 'setup' ) {
+ $exclude_change_from = 1;
+ } elsif ( $field eq 'cancel' ) {
+ $exclude_change_to = 1;
+ } elsif ( $field eq 'change_date' ) {
+ # if we are given setup and change_date ranges, and the setup date
+ # falls in _both_ ranges, then include the package whether it was
+ # a change or not
+ $exclude_change_from = 0;
+ }
- push @where,
- "cust_pkg.$field IS NOT NULL",
- "cust_pkg.$field >= $beginning",
- "cust_pkg.$field <= $ending";
-
- $orderby ||= "ORDER BY cust_pkg.$field";
-
- if ( $field eq 'setup' ) {
- $exclude_change_from = 1;
- } elsif ( $field eq 'cancel' ) {
- $exclude_change_to = 1;
- } elsif ( $field eq 'change_date' ) {
- # if we are given setup and change_date ranges, and the setup date
- # falls in _both_ ranges, then include the package whether it was
- # a change or not
- $exclude_change_from = 0;
}
+
}
if ($exclude_change_from) {
- push @where, "change_pkgnum IS NULL";
+ push @where, "cust_pkg.change_pkgnum IS NULL";
}
if ($exclude_change_to) {
# a join might be more efficient here
@@ -506,6 +518,7 @@ sub search {
WHERE cust_pkg.pkgnum = changed_to_pkg.change_pkgnum
)";
}
+
}
$orderby ||= 'ORDER BY bill';
diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm
index 5abbaca..63c860f 100644
--- a/FS/FS/msg_template/email.pm
+++ b/FS/FS/msg_template/email.pm
@@ -290,7 +290,7 @@ sub prepare {
my @to;
if ( exists($opt{'to'}) ) {
- @to = split(/\s*,\s*/, $opt{'to'});
+ @to = map { $_->format } Email::Address->parse($opt{'to'});
} elsif ( $cust_main ) {
@@ -393,14 +393,17 @@ sub prepare {
# effective To: address (not in headers)
push @to, $self->bcc_addr if $self->bcc_addr;
- my $env_to = join(', ', @to);
+ my @env_to;
+ foreach my $dest (@to) {
+ push @env_to, map { $_->address } Email::Address->parse($dest);
+ }
my $cust_msg = FS::cust_msg->new({
'custnum' => $cust_main ? $cust_main->custnum : '',
'msgnum' => $self->msgnum,
'_date' => $time,
'env_from' => $env_from,
- 'env_to' => $env_to,
+ 'env_to' => join(',', @env_to),
'header' => $message->header_as_string,
'body' => $message->body_as_string,
'error' => '',
@@ -507,7 +510,9 @@ sub send_prepared {
$domain = $1;
}
- my @to = split(/\s*,\s*/, $cust_msg->env_to);
+ # in principle should already be a list of bare addresses, but run it
+ # through Email::Address to make sure
+ my @env_to = map { $_->address } Email::Address->parse($cust_msg->env_to);
my %smtp_opt = ( 'host' => $conf->config('smtpmachine'),
'helo' => $domain );
@@ -533,7 +538,7 @@ sub send_prepared {
eval {
sendmail( $message, { transport => $transport,
from => $cust_msg->env_from,
- to => \@to })
+ to => \@env_to })
};
my $error = '';
if(ref($@) and $@->isa('Email::Sender::Failure')) {
diff --git a/FS/FS/part_export/broadband_sqlradius.pm b/FS/FS/part_export/broadband_sqlradius.pm
index e58c641..2d6681e 100644
--- a/FS/FS/part_export/broadband_sqlradius.pm
+++ b/FS/FS/part_export/broadband_sqlradius.pm
@@ -133,6 +133,8 @@ sub radius_check_suspended {
sub _export_suspend {
my( $self, $svc_broadband ) = (shift, shift);
+ return '' if $self->option('skip_provisioning');
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm
index f0ef3fc..9e65e51 100644
--- a/FS/FS/part_export/sqlradius.pm
+++ b/FS/FS/part_export/sqlradius.pm
@@ -26,6 +26,10 @@ tie %options, 'Tie::IxHash',
type => 'select',
options => [qw( usergroup radusergroup ) ],
},
+ 'skip_provisioning' => {
+ type => 'checkbox',
+ label => 'Skip provisioning records to this database'
+ },
'ignore_accounting' => {
type => 'checkbox',
label => 'Ignore accounting records from this database'
@@ -154,6 +158,8 @@ sub radius_check { #override for other svcdb
sub _export_insert {
my($self, $svc_x) = (shift, shift);
+ return '' if $self->option('skip_provisioning');
+
foreach my $table (qw(reply check)) {
my $method = "radius_$table";
my %attrib = $self->$method($svc_x);
@@ -179,6 +185,8 @@ sub _export_insert {
sub _export_replace {
my( $self, $new, $old ) = (shift, shift, shift);
+ return '' if $self->option('skip_provisioning');
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
@@ -289,6 +297,8 @@ sub _export_replace {
sub _export_suspend {
my( $self, $svc_acct ) = (shift, shift);
+ return '' if $self->option('skip_provisioning');
+
my $new = $svc_acct->clone_suspended;
local $SIG{HUP} = 'IGNORE';
@@ -360,6 +370,8 @@ sub _export_suspend {
sub _export_unsuspend {
my( $self, $svc_x ) = (shift, shift);
+ return '' if $self->option('skip_provisioning');
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
@@ -399,6 +411,8 @@ sub _export_unsuspend {
sub _export_delete {
my( $self, $svc_x ) = (shift, shift);
+ return '' if $self->option('skip_provisioning');
+
my $jobnum = '';
my $usergroup = $self->option('usergroup') || 'usergroup';
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 35f178e..bb8c6bc 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -401,19 +401,20 @@ I<bulk_skip>, I<provision_hold> and I<options>
If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as
values, the appropriate FS::pkg_svc records will be replaced. I<hidden_svc>
-can be set to a hashref of svcparts and flag values ('Y' or '') to set the
-'hidden' field in these records. I<bulk_skip> and I<provision_hold> can be set
-to a hashref of svcparts and flag values ('Y' or '') to set the respective field
-in those records.
+can be set to a hashref of svcparts and flag values ('Y' or '') to set the
+'hidden' field in these records. I<bulk_skip> and I<provision_hold> can be
+set to a hashref of svcparts and flag values ('Y' or '') to set the
+respective field in those records.
-If I<primary_svc> is set to the svcpart of the primary service, the appropriate
-FS::pkg_svc record will be updated.
+If I<primary_svc> is set to the svcpart of the primary service, the
+appropriate FS::pkg_svc record will be updated.
-If I<options> is set to a hashref, the appropriate FS::part_pkg_option records
-will be replaced.
+If I<options> is set to a hashref, the appropriate FS::part_pkg_option
+records will be replaced.
If I<part_pkg_currency> is set to a hashref of options (with the keys as
-option_CURRENCY), appropriate FS::part_pkg::currency records will be replaced.
+option_CURRENCY), appropriate FS::part_pkg::currency records will be
+replaced.
=cut
@@ -2345,6 +2346,56 @@ sub queueable_upgrade {
die $error if $error;
}
}
+
+ # remove custom flag from one-time charge packages that were accidentally
+ # flagged as custom
+ $search = FS::Cursor->new({
+ 'table' => 'part_pkg',
+ 'hashref' => { 'freq' => '0',
+ 'custom' => 'Y',
+ 'family_pkgpart' => { op => '!=', value => '' },
+ },
+ 'addl_from' => ' JOIN
+ (select pkgpart from cust_pkg group by pkgpart having count(*) = 1)
+ AS singular_pkg USING (pkgpart)',
+ });
+ my @fields = grep { $_ ne 'pkgpart'
+ and $_ ne 'custom'
+ and $_ ne 'disabled' } FS::part_pkg->fields;
+ PKGPART: while (my $part_pkg = $search->fetch) {
+ # can't merge the package back into its parent (too late for that)
+ # but we can remove the custom flag if it's not actually customized,
+ # i.e. nothing has been changed.
+
+ my $family_pkgpart = $part_pkg->family_pkgpart;
+ next PKGPART if $family_pkgpart == $part_pkg->pkgpart;
+ my $parent_pkg = FS::part_pkg->by_key($family_pkgpart);
+ foreach my $field (@fields) {
+ if ($part_pkg->get($field) ne $parent_pkg->get($field)) {
+ next PKGPART;
+ }
+ }
+ # options have to be identical too
+ # but links, FCC options, discount plans, and usage packages can't be
+ # changed through the "modify charge" UI, so skip them
+ my %newopt = $part_pkg->options;
+ my %oldopt = $parent_pkg->options;
+ OPTION: foreach my $option (keys %newopt) {
+ if (delete $newopt{$option} ne delete $oldopt{$option}) {
+ next PKGPART;
+ }
+ }
+ if (keys(%newopt) or keys(%oldopt)) {
+ next PKGPART;
+ }
+ # okay, now replace it
+ warn "Removing custom flag from part_pkg#".$part_pkg->pkgpart."\n";
+ $part_pkg->set('custom', '');
+ my $error = $part_pkg->replace;
+ die $error if $error;
+ } # $search->fetch
+
+ return;
}
=item curuser_pkgs_sql
diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm
index df97286..d96c472 100644
--- a/FS/FS/part_pkg/voip_cdr.pm
+++ b/FS/FS/part_pkg/voip_cdr.pm
@@ -191,6 +191,9 @@ tie my %accountcode_tollfree_field, 'Tie::IxHash',
'skip_dcontext' => { 'name' => 'Do not charge for CDRs where dcontext is set to any of these (comma-separated) values: ',
},
+ 'skip_dcontext_prefix' => { 'name' => 'Do not charge for CDRs where dcontext starts with: ',
+ },
+
'skip_dcontext_suffix' => { 'name' => 'Do not charge for CDRs where dcontext ends with: ',
},
@@ -336,7 +339,8 @@ tie my %accountcode_tollfree_field, 'Tie::IxHash',
use_cdrtypenum ignore_cdrtypenum
use_calltypenum ignore_calltypenum
ignore_disposition disposition_in disposition_prefix
- skip_dcontext skip_dcontext_suffix skip_dst_prefix
+ skip_dcontext skip_dcontext_prefix skip_dcontext_suffix
+ skip_dst_prefix
skip_dstchannel_prefix skip_src_length_more
noskip_src_length_accountcode_tollfree
accountcode_tollfree_ratenum accountcode_tollfree_field
@@ -608,6 +612,12 @@ sub check_chargable {
if $self->option_cacheable('skip_dcontext') =~ /\S/
&& grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext'));
+ my $len_dcontext_prefix =
+ length($self->option_cacheable('skip_dcontext_prefix'));
+ return "dcontext starts with ". $self->option_cacheable('skip_dcontext_prefix')
+ if $len_dcontext_prefix
+ && substr($cdr->dcontext,0,$len_dcontext_prefix) eq $self->option_cacheable('skip_dcontext_prefix');
+
my $len_suffix = length($self->option_cacheable('skip_dcontext_suffix'));
return "dcontext ends with ". $self->option_cacheable('skip_dcontext_suffix')
if $len_suffix
diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm
index 1049751..4aeb331 100644
--- a/FS/FS/pay_batch.pm
+++ b/FS/FS/pay_batch.pm
@@ -14,6 +14,7 @@ use FS::Record qw( dbh qsearch qsearchs );
use FS::Conf;
use FS::cust_pay;
use FS::Log;
+use Try::Tiny;
=head1 NAME
@@ -1086,16 +1087,21 @@ sub export_to_gateway {
return '';
}
- my $batch = Business::BatchPayment->create(Batch =>
- batch_id => $self->batchnum,
- items => \@items
- );
- $processor->submit($batch);
+ try {
+ my $batch = Business::BatchPayment->create(Batch =>
+ batch_id => $self->batchnum,
+ items => \@items
+ );
+ $processor->submit($batch);
- if ($batch->processor_id) {
- $self->set('processor_id',$batch->processor_id);
- $self->replace;
- }
+ if ($batch->processor_id) {
+ $self->set('processor_id',$batch->processor_id);
+ $self->replace;
+ }
+ } catch {
+ $dbh->rollback if $oldAutoCommit;
+ die $_;
+ };
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
diff --git a/FS/FS/pay_batch/paymentech.pm b/FS/FS/pay_batch/paymentech.pm
index 1282507..3cf3134 100644
--- a/FS/FS/pay_batch/paymentech.pm
+++ b/FS/FS/pay_batch/paymentech.pm
@@ -175,7 +175,14 @@ sub _upgrade_gateway {
my $conf = FS::Conf->new;
my @batchconfig = $conf->config('batchconfig-paymentech');
my %options;
- @options{ qw(bin terminalID merchantID login password ) } = @batchconfig;
+ @options{ qw(
+ bin
+ terminalID
+ merchantID
+ login
+ password
+ with_recurringInd
+ ) } = @batchconfig;
$options{'industryType'} = 'EC';
( 'Paymentech', %options );
}
diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm
index 3a51022..1c45720 100644
--- a/FS/FS/payinfo_Mixin.pm
+++ b/FS/FS/payinfo_Mixin.pm
@@ -307,13 +307,12 @@ sub payinfo_used {
my $payinfo = shift || $self->payinfo;
my %hash = (
'custnum' => $self->custnum,
- 'payby' => 'CARD',
+ 'payby' => $self->payby,
);
return 1
if qsearch('cust_pay', { %hash, 'payinfo' => $payinfo } )
- || qsearch('cust_pay',
- { %hash, 'paymask' => $self->mask_payinfo('CARD', $payinfo) } )
+ || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo } )
;
return 0;
diff --git a/FS/FS/webservice_log.pm b/FS/FS/webservice_log.pm
index 7e320c2..1dfabe6 100644
--- a/FS/FS/webservice_log.pm
+++ b/FS/FS/webservice_log.pm
@@ -125,6 +125,40 @@ sub check {
=back
+=head1 CLASS METHODS
+
+=over 4
+
+=item price_print
+
+Calculates cost of printing unbilled print jobs for this customer.
+
+=cut
+
+sub price_print {
+ my( $class, %opt ) = @_;
+
+# $opt{'beginning'} ||= 0;
+# $opt{'ending'} ||= 4294967295;
+
+ #false laziness w/ClientAPI/Freeside.pm
+ my $color = 1.10;
+ my $page = 0.10;
+
+ $class->scalar_sql("
+ SELECT SUM( $color + quantity * $page )
+ FROM webservice_log
+ WHERE custnum = $opt{custnum}
+ AND method = 'print'
+ AND status IS NULL
+ ");
+# AND _date >= $opt{beginning}
+# AND _date < $opt{ending}
+
+}
+
+=back
+
=head1 BUGS
=head1 SEE ALSO
diff --git a/FS/bin/freeside-upgrade b/FS/bin/freeside-upgrade
index b8a8fbd..1684408 100755
--- a/FS/bin/freeside-upgrade
+++ b/FS/bin/freeside-upgrade
@@ -473,11 +473,9 @@ Also performs other upgrade functions:
[ -r ]: Skip sqlradius updates. Useful for occassions where the sqlradius
databases may be inaccessible.
- [ -j ]: Run certain upgrades asychronously from the job queue. Currently
- used only for the 2.x -> 3.x cust_location, cust_pay and part_pkg
- upgrades. This may cause odd behavior before the upgrade is
- complete, so it's recommended only for very large cust_main, cust_pay
- and/or part_pkg tables that take too long to upgrade.
+ [ -j ]: Run certain upgrades asychronously from the job queue. Recommended
+ for very large cust_main or part_pkg tables that take too long to
+ upgrade.
[ -a ]: Run schema changes in parallel (Pg only). DBIx::DBSchema minimum
version 0.41 recommended. Recommended only for large databases and
diff --git a/FS/t/suite/15-activate_encryption.t b/FS/t/suite/15-activate_encryption.t
new file mode 100755
index 0000000..e5732f7
--- /dev/null
+++ b/FS/t/suite/15-activate_encryption.t
@@ -0,0 +1,106 @@
+#!/usr/bin/perl
+
+use strict;
+use FS::Test;
+use Test::More tests => 13;
+use FS::Conf;
+use FS::UID qw( dbh );
+use DateTime;
+use FS::cust_main; # to load all other tables
+
+my $fs = FS::Test->new( user => 'admin' );
+my $conf = FS::Conf->new;
+my $err;
+my @tables = qw(cust_payby cust_pay_pending cust_pay cust_pay_void cust_refund);
+
+### can only run on test database (company name "Freeside Test")
+like( $conf->config('company_name'), qr/^Freeside Test/, 'using test database' ) or BAIL_OUT('');
+
+### we need to unencrypt our test db before we can test turning it on
+
+# temporarily load all payinfo into memory
+my %payinfo = ();
+foreach my $table (@tables) {
+ $payinfo{$table} = {};
+ foreach my $record ($fs->qsearch({ table => $table })) {
+ next unless grep { $record->payby eq $_ } @FS::Record::encrypt_payby;
+ $payinfo{$table}{$record->get($record->primary_key)} = $record->get('payinfo');
+ }
+}
+
+# turn off encryption
+foreach my $config ( qw(encryption encryptionmodule encryptionpublickey encryptionprivatekey) ) {
+ $conf->delete($config);
+ ok( !$conf->exists($config), "deleted $config" ) or BAIL_OUT('');
+}
+$FS::Record::conf_encryption = $conf->exists('encryption');
+$FS::Record::conf_encryptionmodule = $conf->config('encryptionmodule');
+$FS::Record::conf_encryptionpublickey = join("\n",$conf->config('encryptionpublickey'));
+$FS::Record::conf_encryptionprivatekey = join("\n",$conf->config('encryptionprivatekey'));
+
+# save unencrypted values
+foreach my $table (@tables) {
+ local $FS::payinfo_Mixin::allow_closed_replace = 1;
+ local $FS::Record::no_update_diff = 1;
+ local $FS::UID::AutoCommit = 1;
+ my $tclass = 'FS::'.$table;
+ foreach my $key (keys %{$payinfo{$table}}) {
+ my $record = $tclass->by_key($key);
+ $record->payinfo($payinfo{$table}{$key});
+ $err = $record->replace;
+ last if $err;
+ }
+}
+ok( !$err, "save unencrypted values" ) or BAIL_OUT($err);
+
+# make sure it worked
+CHECKDECRYPT:
+foreach my $table (@tables) {
+ my $tclass = 'FS::'.$table;
+ foreach my $key (sort {$a <=> $b} keys %{$payinfo{$table}}) {
+ my $sql = 'SELECT * FROM '.$table.
+ ' WHERE payinfo LIKE \'M%\''.
+ ' AND char_length(payinfo) > 80'.
+ ' AND '.$tclass->primary_key.' = '.$key;
+ my $sth = dbh->prepare($sql) or BAIL_OUT(dbh->errstr);
+ $sth->execute or BAIL_OUT($sth->errstr);
+ if (my $hashrec = $sth->fetchrow_hashref) {
+ $err = $table.' '.$key.' encrypted';
+ last CHECKDECRYPT;
+ }
+ }
+}
+ok( !$err, "all values unencrypted" ) or BAIL_OUT($err);
+
+### now, run upgrade
+$err = system('freeside-upgrade','admin');
+ok( !$err, 'upgrade ran' ) or BAIL_OUT('Error string: '.$!);
+
+# check that confs got set
+foreach my $config ( qw(encryption encryptionmodule encryptionpublickey encryptionprivatekey) ) {
+ ok( $conf->exists($config), "$config was set" ) or BAIL_OUT('');
+}
+
+# check that known records got encrypted
+CHECKENCRYPT:
+foreach my $table (@tables) {
+ my $tclass = 'FS::'.$table;
+ foreach my $key (sort {$a <=> $b} keys %{$payinfo{$table}}) {
+ my $sql = 'SELECT * FROM '.$table.
+ ' WHERE payinfo LIKE \'M%\''.
+ ' AND char_length(payinfo) > 80'.
+ ' AND '.$tclass->primary_key.' = '.$key;
+ my $sth = dbh->prepare($sql) or BAIL_OUT(dbh->errstr);
+ $sth->execute or BAIL_OUT($sth->errstr);
+ unless ($sth->fetchrow_hashref) {
+ $err = $table.' '.$key.' not encrypted';
+ last CHECKENCRYPT;
+ }
+ }
+}
+ok( !$err, "all values encrypted" ) or BAIL_OUT($err);
+
+exit;
+
+1;
+
diff --git a/fs_selfservice/FS-SelfService/cgi/view_cdr_details.html b/fs_selfservice/FS-SelfService/cgi/view_cdr_details.html
index f396682..40eed80 100644
--- a/fs_selfservice/FS-SelfService/cgi/view_cdr_details.html
+++ b/fs_selfservice/FS-SelfService/cgi/view_cdr_details.html
@@ -13,20 +13,25 @@
<TABLE WIDTH="100%">
<TR>
<TD WIDTH="50%">
-<%= if ($previous < $beginning) {
- $OUT .= qq!<A HREF="${url}view_cdr_details;svcnum=$svcnum;beginning=!;
- $OUT .= qq!$previous;ending=$beginning">Previous period</A>!;
- }else{
+<%=
+ $ahref = qq!<A HREF="${url}view_cdr_details;svcnum=$svcnum;!;
+ $ahref = qq!inbound=1;! if $inbound;
+ if ($previous < $beginning) {
+ $OUT .= $ahref.
+ qq!beginning=$previous;ending=$beginning">Previous period</A>!;
+ } else {
'';
- } %>
+ }
+%>
</TD>
<TD WIDTH="50%" ALIGN="right">
-<%= if ($next > $ending) {
- $OUT .= qq!<A HREF="${url}view_cdr_details;svcnum=$svcnum;beginning=!;
- $OUT .= qq!$ending;ending=$next">Next period</A>!;
- }else{
+<%=
+ if ($next > $ending) {
+ $OUT .= $ahref. qq!beginning=$ending;ending=$next">Next period</A>!;
+ } else {
'';
- }%>
+ }
+%>
</TD>
</TR>
</TABLE>
diff --git a/httemplate/edit/agent_payment_gateway.html b/httemplate/edit/agent_payment_gateway.html
index 753bc76..6d15164 100644
--- a/httemplate/edit/agent_payment_gateway.html
+++ b/httemplate/edit/agent_payment_gateway.html
@@ -12,6 +12,8 @@ Use gateway <SELECT NAME="gatewaynum">
% foreach my $payment_gateway (
% qsearch('payment_gateway', { 'disabled' => '' } )
% ) {
+% # don't let these be selected as agent overrides; there's a different mechanism
+% next if $payment_gateway->gateway_namespace eq 'Business::BatchPayment';
%
<OPTION VALUE="<% $payment_gateway->gatewaynum %>"><% $payment_gateway->gateway_module %> (<% $payment_gateway->gateway_username %>)
diff --git a/httemplate/edit/cust_pkg_discount.html b/httemplate/edit/cust_pkg_discount.html
index e1e3dae..79c3478 100755
--- a/httemplate/edit/cust_pkg_discount.html
+++ b/httemplate/edit/cust_pkg_discount.html
@@ -1,13 +1,17 @@
<& /elements/header-popup.html, "Discount Package" &>
<& /elements/error.html &>
-<FORM NAME="DiscountPkgForm" ACTION="<% $p %>edit/process/cust_pkg_discount.html" METHOD=POST>
+<FORM NAME = "DiscountPkgForm"
+ ACTION = "<% $p %>edit/process/cust_pkg_discount.html"
+ METHOD = POST
+ onSubmit = "document.DiscountPkgForm.submit.disabled=true;"
+>
<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
<% ntable('#cccccc') %>
<TR>
- <TH ALIGN="right">Current package&nbsp;</TH>
+ <TH ALIGN="right">Package&nbsp;</TH>
<TD COLSPAN=7>
<% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B> - <% $part_pkg->comment |h %>
</TD>
@@ -18,6 +22,8 @@
curr_value_recur => $recur_discountnum,
disable_setup => $disable_setup,
disable_recur => $disable_recur,
+ setup_label => emt('Setup fee discount'),
+ recur_label => emt('Recurring fee discount'),
&>
</TABLE>
diff --git a/httemplate/elements/tr-select-pkg-discount.html b/httemplate/elements/tr-select-pkg-discount.html
index dc38cff..0c57fd8 100644
--- a/httemplate/elements/tr-select-pkg-discount.html
+++ b/httemplate/elements/tr-select-pkg-discount.html
@@ -34,7 +34,7 @@ description if curr_value_setup is set. Likewise "disable_recur".
% if ( $curuser->access_right('Waive setup fee') ) {
% push @$pre_options, -2 => 'Waive setup fee';
% }
-<& tr-td-label.html, label => emt('Setup fee') &>
+<& tr-td-label.html, label => $opt{setup_label} || emt('Setup fee') &>
<td>
<& select-discount.html,
field => 'setup_discountnum',
@@ -97,7 +97,7 @@ description if curr_value_setup is set. Likewise "disable_recur".
% if ( $curuser->access_right('Discount customer package')
% and !$opt{disable_recur} ) {
-<& tr-td-label.html, label => emt('Recurring fee') &>
+<& tr-td-label.html, label => $opt{recur_label} || emt('Recurring fee') &>
<td>
<& select-discount.html,
field => 'recur_discountnum',
diff --git a/httemplate/misc/bulk_change_pkg.cgi b/httemplate/misc/bulk_change_pkg.cgi
index 4964e59..6ed272f 100755
--- a/httemplate/misc/bulk_change_pkg.cgi
+++ b/httemplate/misc/bulk_change_pkg.cgi
@@ -10,16 +10,32 @@
%# some false laziness w/search/cust_pkg.cgi
<INPUT TYPE="hidden" NAME="query" VALUE="<% $cgi->keywords |h %>">
-% for my $param (qw(agentnum custnum magic status classnum custom censustract)) {
-<INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $cgi->param($param) |h %>">
-% }
+% for my $param (
+% qw(
+% agentnum cust_status cust_main_salesnum salesnum custnum magic status
+% custom pkgbatch zip
+% 477part 477rownum date
+% report_option
+% ),
+% grep { /^location_\w+$/ || /^report_option_any/ } $cgi->param
+% ) {
+ <INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $cgi->param($param) |h %>">
+% }
+%
+% for my $param (qw( censustract censustract2 ) ) {
+% next unless grep { $_ eq $param } $cgi->param;
+ <INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $cgi->param($param) |h %>">
+% }
%
-% foreach my $pkgpart ($cgi->param('pkgpart')) {
-<INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $pkgpart |h %>">
+% for my $param (qw( pkgpart classnum refnum towernum )) {
+% foreach my $value ($cgi->param($param)) {
+ <INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $value |h %>">
+% }
% }
%
-% foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
+% foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel active )) {
%
+ <INPUT TYPE="hidden" NAME="<% $field %>_null" VALUE="<% $cgi->param("${field}_null") |h %>">
<INPUT TYPE="hidden" NAME="<% $field %>begin" VALUE="<% $cgi->param("${field}.begin") |h %>">
<INPUT TYPE="hidden" NAME="<% $field %>beginning" VALUE="<% $cgi->param("${field}beginning") |h %>">
<INPUT TYPE="hidden" NAME="<% $field %>end" VALUE="<% $cgi->param("${field}.end") |h %>">
diff --git a/httemplate/misc/bulk_pkg_increment_bill.cgi b/httemplate/misc/bulk_pkg_increment_bill.cgi
index d594b55..fc9bbc8 100755
--- a/httemplate/misc/bulk_pkg_increment_bill.cgi
+++ b/httemplate/misc/bulk_pkg_increment_bill.cgi
@@ -10,16 +10,32 @@
%# some false laziness w/search/cust_pkg.cgi
<INPUT TYPE="hidden" NAME="query" VALUE="<% $cgi->keywords |h %>">
-% for my $param (qw(agentnum custnum magic status classnum custom censustract)) {
-<INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $cgi->param($param) |h %>">
-% }
+% for my $param (
+% qw(
+% agentnum cust_status cust_main_salesnum salesnum custnum magic status
+% custom pkgbatch zip
+% 477part 477rownum date
+% report_option
+% ),
+% grep { /^location_\w+$/ || /^report_option_any/ } $cgi->param
+% ) {
+ <INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $cgi->param($param) |h %>">
+% }
+%
+% for my $param (qw( censustract censustract2 ) ) {
+% next unless grep { $_ eq $param } $cgi->param;
+ <INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $cgi->param($param) |h %>">
+% }
%
-% foreach my $pkgpart ($cgi->param('pkgpart')) {
-<INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $pkgpart |h %>">
+% for my $param (qw( pkgpart classnum refnum towernum )) {
+% foreach my $value ($cgi->param($param)) {
+ <INPUT TYPE="hidden" NAME="<% $param %>" VALUE="<% $value |h %>">
+% }
% }
%
-% foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
+% foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel active )) {
%
+ <INPUT TYPE="hidden" NAME="<% $field %>_null" VALUE="<% $cgi->param("${field}_null") |h %>">
<INPUT TYPE="hidden" NAME="<% $field %>begin" VALUE="<% $cgi->param("${field}.begin") |h %>">
<INPUT TYPE="hidden" NAME="<% $field %>beginning" VALUE="<% $cgi->param("${field}beginning") |h %>">
<INPUT TYPE="hidden" NAME="<% $field %>end" VALUE="<% $cgi->param("${field}.end") |h %>">
diff --git a/httemplate/misc/process/bulk_change_pkg.cgi b/httemplate/misc/process/bulk_change_pkg.cgi
index e22dafe..2432f3c 100755
--- a/httemplate/misc/process/bulk_change_pkg.cgi
+++ b/httemplate/misc/process/bulk_change_pkg.cgi
@@ -11,16 +11,43 @@ my %search_hash = ();
$search_hash{'query'} = $cgi->param('query');
-for my $param (qw(agentnum magic status classnum pkgpart)) {
- $search_hash{$param} = $cgi->param($param)
- if $cgi->param($param);
+#scalars
+for (qw( agentnum cust_status cust_main_salesnum salesnum custnum magic status
+ custom cust_fields pkgbatch zip
+ 477part 477rownum date
+ ))
+{
+ $search_hash{$_} = $cgi->param($_) if length($cgi->param($_));
+}
+
+#arrays
+for my $param (qw( pkgpart classnum refnum towernum )) {
+ $search_hash{$param} = [ $cgi->param($param) ]
+ if grep { $_ eq $param } $cgi->param;
+}
+
+#scalars that need to be passed if empty
+for my $param (qw( censustract censustract2 )) {
+ $search_hash{$param} = $cgi->param($param) || ''
+ if grep { $_ eq $param } $cgi->param;
+}
+
+#location flags (checkboxes)
+my @loc = grep /^\w+$/, $cgi->param('loc');
+$search_hash{"location_$_"} = 1 foreach @loc;
+
+my $report_option = $cgi->param('report_option');
+$search_hash{report_option} = $report_option if $report_option;
+
+for my $param (grep /^report_option_any/, $cgi->param) {
+ $search_hash{$param} = $cgi->param($param);
}
###
# parse dates
###
-#false laziness w/report_cust_pkg.html
+#false laziness w/report_cust_pkg.html and bulk_pkg_increment_bill.cgi
my %disable = (
'all' => {},
'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'adjourn'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, },
@@ -30,7 +57,9 @@ my %disable = (
'' => {},
);
-foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
+foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel active )) {
+
+ $search_hash{$field.'_null'} = scalar( $cgi->param($field.'_null') );
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field);
diff --git a/httemplate/misc/process/bulk_pkg_increment_bill.cgi b/httemplate/misc/process/bulk_pkg_increment_bill.cgi
index d89f491..48c9de7 100755
--- a/httemplate/misc/process/bulk_pkg_increment_bill.cgi
+++ b/httemplate/misc/process/bulk_pkg_increment_bill.cgi
@@ -25,17 +25,43 @@ my %search_hash = ();
$search_hash{'query'} = $cgi->param('query');
-for my $param (qw(agentnum magic status classnum pkgpart)) {
- $search_hash{$param} = $cgi->param($param)
- if $cgi->param($param);
+#scalars
+for (qw( agentnum cust_status cust_main_salesnum salesnum custnum magic status
+ custom cust_fields pkgbatch zip
+ 477part 477rownum date
+ ))
+{
+ $search_hash{$_} = $cgi->param($_) if length($cgi->param($_));
+}
+
+#arrays
+for my $param (qw( pkgpart classnum refnum towernum )) {
+ $search_hash{$param} = [ $cgi->param($param) ]
+ if grep { $_ eq $param } $cgi->param;
+}
+
+#scalars that need to be passed if empty
+for my $param (qw( censustract censustract2 )) {
+ $search_hash{$param} = $cgi->param($param) || ''
+ if grep { $_ eq $param } $cgi->param;
+}
+
+#location flags (checkboxes)
+my @loc = grep /^\w+$/, $cgi->param('loc');
+$search_hash{"location_$_"} = 1 foreach @loc;
+
+my $report_option = $cgi->param('report_option');
+$search_hash{report_option} = $report_option if $report_option;
+
+for my $param (grep /^report_option_any/, $cgi->param) {
+ $search_hash{$param} = $cgi->param($param);
}
###
# parse dates
###
-#false laziness w/report_cust_pkg.html
-# and, now, w/bulk_change_pkg.cgi
+#false laziness w/report_cust_pkg.html and bulk_change_pkg.cgi
my %disable = (
'all' => {},
'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'adjourn'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, },
@@ -45,7 +71,9 @@ my %disable = (
'' => {},
);
-foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
+foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel active )) {
+
+ $search_hash{$field.'_null'} = scalar( $cgi->param($field.'_null') );
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field);
diff --git a/httemplate/misc/process/cancel_pkg.html b/httemplate/misc/process/cancel_pkg.html
index 7e33e15..cb20712 100755
--- a/httemplate/misc/process/cancel_pkg.html
+++ b/httemplate/misc/process/cancel_pkg.html
@@ -1,4 +1,4 @@
-<& /elements/popup-topreload.html, et("Package $past_method") &>
+<& /elements/popup-topreload.html, emt("Package $past_method") &>
<%once>
my %past = ( 'cancel' => 'cancelled',
diff --git a/httemplate/search/cust_credit_bill_pkg.html b/httemplate/search/cust_credit_bill_pkg.html
index 0cdd8de..4a14893 100644
--- a/httemplate/search/cust_credit_bill_pkg.html
+++ b/httemplate/search/cust_credit_bill_pkg.html
@@ -457,6 +457,8 @@ if ( $cgi->param('nottax') ) {
push @where, "billpkgtaxratelocationnum IS NULL";
}
+ $join_pkg .= ' LEFT JOIN cust_pkg USING ( pkgnum ) ';
+
$join_pkg .= ' LEFT JOIN tax_rate_location USING ( taxratelocationnum ) ';
} elsif ( $conf->exists('tax-pkg_address') ) {
diff --git a/httemplate/search/cust_msg.html b/httemplate/search/cust_msg.html
index 33e1815..65460f7 100644
--- a/httemplate/search/cust_msg.html
+++ b/httemplate/search/cust_msg.html
@@ -19,7 +19,10 @@
ucfirst($_[0]->msgtype) || $_[0]->msgname
},
sub {
- join('<BR>', split(/,\s*/, $_[0]->env_to) )
+ join('<BR>',
+ map { encode_entities($_->format) }
+ Email::Address->parse($_[0]->env_to)
+ )
},
'status',
sub { encode_entities($_[0]->error) },
diff --git a/httemplate/search/cust_pkg.cgi b/httemplate/search/cust_pkg.cgi
index dbd346d..df1d7e5 100755
--- a/httemplate/search/cust_pkg.cgi
+++ b/httemplate/search/cust_pkg.cgi
@@ -203,6 +203,8 @@ my %disable = (
foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel active )) {
+ $search_hash{$field.'_null'} = scalar( $cgi->param($field.'_null') );
+
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field);
next if $beginning == 0 && $ending == 4294967295
diff --git a/httemplate/search/elements/grouped-search/core b/httemplate/search/elements/grouped-search/core
index 3d38a8c..b15fe86 100644
--- a/httemplate/search/elements/grouped-search/core
+++ b/httemplate/search/elements/grouped-search/core
@@ -110,7 +110,7 @@ for my $i (0 .. scalar(@groups) - 1) {
push @group_labels, $label;
my @footer;
- if ($opt{'subtotal_row'}) {
+ if ($opt{'subtotal_row'} and @groups > 1) {
for( my $col = 0;
exists($opt{'subtotal_row'}[$col]) or exists($opt{'header'}[$col]);
$col++
diff --git a/httemplate/search/elements/grouped-search/html b/httemplate/search/elements/grouped-search/html
index 9c2418a..28d0040 100644
--- a/httemplate/search/elements/grouped-search/html
+++ b/httemplate/search/elements/grouped-search/html
@@ -106,14 +106,18 @@ if ($group_info->{num} > 1) {
&>
<DIV CLASS="fstabcontainer">
+% if ( $group->num_rows > 0 ) {
+<P><% emt('[quant,_1,_2]', $group->num_rows, $opt{name_singular}) %>
+</P>
%# download links
-<P><% emt('Download full results') %><BR>
+<P><% emt('Download results:') %>
% $cgi->param('type', 'xls');
-<A HREF="<% $cgi->self_url %>"><% emt('as Excel spreadsheet') %></A><BR>
+<A HREF="<% $cgi->self_url %>"><% emt('Spreadsheet') %></A>&nbsp;|&nbsp;
% $cgi->param('type', 'html-print');
-<A HREF="<% $cgi->self_url %>"><% emt('as printable copy') %></A><BR>
+<A HREF="<% $cgi->self_url %>"><% emt('webpage') %></A>
% $cgi->delete('type');
</P>
+% }
<% $pager %>
diff --git a/httemplate/search/report_cust_pkg.html b/httemplate/search/report_cust_pkg.html
index 27aecec..556177c 100755
--- a/httemplate/search/report_cust_pkg.html
+++ b/httemplate/search/report_cust_pkg.html
@@ -80,6 +80,7 @@
what.form.<% $field %>_beginning_text.disabled = true;
what.form.<% $field %>_ending_text.disabled = true;
+ what.form.<% $field %>_null.disabled = true;
what.form.<% $field %>_beginning_text.style.backgroundColor = '#dddddd';
what.form.<% $field %>_ending_text.style.backgroundColor = '#dddddd';
@@ -90,15 +91,21 @@
% } else {
- what.form.<% $field %>_beginning_text.disabled = false;
- what.form.<% $field %>_ending_text.disabled = false;
- what.form.<% $field %>_beginning_text.style.backgroundColor = '#ffffff';
- what.form.<% $field %>_ending_text.style.backgroundColor = '#ffffff';
+ what.form.<% $field %>_null.disabled = false;
- what.form.<% $field %>_beginning_button.style.display = '';
- what.form.<% $field %>_ending_button.style.display = '';
- what.form.<% $field %>_beginning_disabled.style.display = 'none';
- what.form.<% $field %>_ending_disabled.style.display = 'none';
+ if ( ! what.form.<% $field %>_null.checked ) {
+
+ what.form.<% $field %>_beginning_text.disabled = false;
+ what.form.<% $field %>_ending_text.disabled = false;
+ what.form.<% $field %>_beginning_text.style.backgroundColor = '#ffffff';
+ what.form.<% $field %>_ending_text.style.backgroundColor = '#ffffff';
+
+ what.form.<% $field %>_beginning_button.style.display = '';
+ what.form.<% $field %>_ending_button.style.display = '';
+ what.form.<% $field %>_beginning_disabled.style.display = 'none';
+ what.form.<% $field %>_ending_disabled.style.display = 'none';
+
+ }
% }
% }
@@ -109,6 +116,37 @@
}
+% foreach my $field (@date_fields) {
+
+ function <% $field %>_null_changed(what) {
+
+ if ( what.checked ) {
+ what.form.<% $field %>_beginning_text.disabled = true;
+ what.form.<% $field %>_ending_text.disabled = true;
+ what.form.<% $field %>_beginning_text.style.backgroundColor = '#dddddd';
+ what.form.<% $field %>_ending_text.style.backgroundColor = '#dddddd';
+ what.form.<% $field %>_beginning_button.style.display = 'none';
+ what.form.<% $field %>_ending_button.style.display = 'none';
+ what.form.<% $field %>_beginning_disabled.style.display = '';
+ what.form.<% $field %>_ending_disabled.style.display = '';
+
+ } else {
+ what.form.<% $field %>_beginning_text.disabled = false;
+ what.form.<% $field %>_ending_text.disabled = false;
+ what.form.<% $field %>_beginning_text.style.backgroundColor = '#ffffff';
+ what.form.<% $field %>_ending_text.style.backgroundColor = '#ffffff';
+
+ what.form.<% $field %>_beginning_button.style.display = '';
+ what.form.<% $field %>_ending_button.style.display = '';
+ what.form.<% $field %>_beginning_disabled.style.display = 'none';
+ what.form.<% $field %>_ending_disabled.style.display = 'none';
+
+ }
+
+ }
+
+% }
+
</SCRIPT>
<& /elements/tr-select-pkg_class.html,
@@ -135,6 +173,7 @@
<TD></TD>
<TD>From date <i>(m/d/y)</i></TD>
<TD>To date <i>(m/d/y)</i></TD>
+ <TD>Empty date</TD>
</TR>
% my $noinit = 0;
% foreach my $field (@date_fields) {
@@ -152,6 +191,13 @@
</TD>
% $noinit = 1;
% }
+ <TD ALIGN="center">
+ <& /elements/checkbox.html,
+ 'field' => $field.'_null',
+ 'value' => 'Y',
+ 'onchange' => $field.'_null_changed',
+ &>
+ </TD>
</TR>
% } #foreach $field
</TABLE>
diff --git a/httemplate/view/cust_main/packages/package.html b/httemplate/view/cust_main/packages/package.html
index 14f7fb0..dd15c7b 100644
--- a/httemplate/view/cust_main/packages/package.html
+++ b/httemplate/view/cust_main/packages/package.html
@@ -268,7 +268,7 @@
&& ! $cust_pkg->get('cancel')
&& $can_discount_pkg
},
- popup => "edit/cust_pkg_discount.html?$plink".
+ popup => "edit/cust_pkg_discount.html?$plink",
actionlabel => emt('Discount package'),
width => 616,
},
diff --git a/httemplate/view/cust_msg.html b/httemplate/view/cust_msg.html
index 91a08eb..d2b043c 100755
--- a/httemplate/view/cust_msg.html
+++ b/httemplate/view/cust_msg.html
@@ -61,7 +61,9 @@ $custmsgnum =~ /^(\d+)$/ or die "illegal custmsgnum";
my $cust_msg = qsearchs('cust_msg', { 'custmsgnum' => $custmsgnum });
my $date = '';
$date = time2str('%Y-%m-%d %T', $cust_msg->_date) if ( $cust_msg->_date );
-my $env_to = join('</TD></TR><TR><TD></TD><TD>', split(',', $cust_msg->env_to));
+my @to = map { encode_entities($_->format) }
+ Email::Address->parse($cust_msg->env_to);
+my $env_to = join('</TD></TR><TR><TD></TD><TD>', @to);
my %label = (
'sent' => 'Sent:',
diff --git a/httemplate/view/directions.html b/httemplate/view/directions.html
index 8377d12..1c99cda 100644
--- a/httemplate/view/directions.html
+++ b/httemplate/view/directions.html
@@ -62,7 +62,8 @@ function show_route() {
if ( status == google.maps.DirectionsStatus.OK ) {
directionsDisplay.setDirections(result);
} else {
- document.body.innerHTML = ('<P STYLE="color: red;">Directions lookup failed with the following error: '+status+'</P>');
+ document.body.innerHTML = ('<P STYLE="color: red;">Directions lookup failed with the following error: '+status+'</P>')
+ + <% include('/elements/google_maps_api_key.html' ) |js_string%>;
}
});
}
diff --git a/ng_selfservice/elements/add_password_validation.php b/ng_selfservice/elements/add_password_validation.php
new file mode 100644
index 0000000..6938437
--- /dev/null
+++ b/ng_selfservice/elements/add_password_validation.php
@@ -0,0 +1,51 @@
+<SCRIPT>
+function add_password_validation (fieldid,nologin) {
+ var inputfield = document.getElementById(fieldid);
+ inputfield.onchange = function () {
+ var fieldid = this.id+'_result';
+ var resultfield = document.getElementById(fieldid);
+ var svcnum = '';
+ var svcfield = document.getElementById(this.id+'_svcnum');
+ if (svcfield) {
+ svcnum = svcfield.options[svcfield.selectedIndex].value;
+ }
+ if (this.value) {
+ resultfield.innerHTML = '<SPAN STYLE="color: blue;">Validating password...</SPAN>';
+ var validate_data = {
+ fieldid: fieldid,
+ check_password: this.value,
+ };
+ if (!nologin) {
+ validate_data['svcnum'] = svcnum;
+ }
+ $.ajax({
+ url: 'xmlrpc_validate_passwd.php',
+ data: validate_data,
+ method: 'POST',
+ success: function ( result ) {
+ result = JSON.parse(result);
+ var resultfield = document.getElementById(fieldid);
+ if (resultfield) {
+ var errorimg = '<IMG SRC="images/error.png" style="width: 1em; display: inline-block; padding-right: .5em">';
+ var validimg = '<IMG SRC="images/tick.png" style="width: 1em; display: inline-block; padding-right: .5em">';
+ if (result.password_valid) {
+ resultfield.innerHTML = validimg+'<SPAN STYLE="color: green;">Password valid!</SPAN>';
+ } else if (result.password_invalid) {
+ resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.password_invalid+'</SPAN>';
+ } else {
+ resultfield.innerHTML = '';
+ }
+ }
+ },
+ error: function ( jqXHR, textStatus, errorThrown ) {
+ var resultfield = document.getElementById(fieldid);
+ console.log('ajax error: '+textStatus+'+'+errorThrown);
+ if (resultfield) {
+ resultfield.innerHTML = '';
+ }
+ },
+ });
+ }
+ };
+}
+</SCRIPT>
diff --git a/ng_selfservice/images/error.png b/ng_selfservice/images/error.png
new file mode 100644
index 0000000..628cf2d
--- /dev/null
+++ b/ng_selfservice/images/error.png
Binary files differ
diff --git a/ng_selfservice/images/tick.png b/ng_selfservice/images/tick.png
new file mode 100644
index 0000000..a9925a0
--- /dev/null
+++ b/ng_selfservice/images/tick.png
Binary files differ
diff --git a/ng_selfservice/password.php b/ng_selfservice/password.php
index 41296ed..a6e6795 100644
--- a/ng_selfservice/password.php
+++ b/ng_selfservice/password.php
@@ -1,5 +1,92 @@
<? $title ='Change Password'; include('elements/header.php'); ?>
<? $current_menu = 'password.php'; include('elements/menu.php'); ?>
-Chagne password
+<?
+$error = '';
+$pwd_change_success = false;
+if ( isset($_POST['svcnum']) ) {
+
+ $pwd_change_result = $freeside->myaccount_passwd(array(
+ 'session_id' => $_COOKIE['session_id'],
+ 'svcnum' => $_POST['svcnum'],
+ 'new_password' => $_POST['new_password'],
+ 'new_password2' => $_POST['new_password2'],
+ ));
+
+ if ($pwd_change_result['error']) {
+ $error = $pwd_change_result['error'];
+ } else {
+ $pwd_change_success = true;
+ }
+}
+
+if ($pwd_change_success) {
+?>
+
+<P>Password changed for <? echo $pwd_change_result['value'],' ',$pwd_change_result['label'] ?>.</P>
+
+<?
+} else {
+ $pwd_change_svcs = $freeside->list_svcs(array(
+ 'session_id' => $_COOKIE['session_id'],
+ 'svcdb' => 'svc_acct',
+ ));
+ if (isset($pwd_change_svcs['error'])) {
+ $error = $error || $pwd_change_svcs['error'];
+ }
+ if (!isset($pwd_change_svcs['svcs'])) {
+ $pwd_change_svcs['svcs'] = $pwd_change_svcs['svcs'];
+ $error = $error || 'Unknown error loading services';
+ }
+ if ($error) {
+ include('elements/error.php');
+ }
+?>
+
+<FORM METHOD="POST">
+<TABLE BGCOLOR="#cccccc">
+ <TR>
+ <TH ALIGN="right">Change password for account: </TH>
+ <TD>
+ <SELECT ID="new_password_svcnum" NAME="svcnum">
+<?
+ $selected_svcnum = isset($_POST['svcnum']) ? $_POST['svcnum'] : $pwd_change_svcs['svcnum'];
+ foreach ($pwd_change_svcs['svcs'] as $svc) {
+?>
+ <OPTION VALUE="<? echo $svc['svcnum'] ?>"<? echo $selected_svcnum == $svc['svcnum'] ? ' SELECTED' : '' ?>>
+ <? echo $svc['label'],': ',$svc['value'] ?>
+ </OPTION>
+<?
+ }
+?>
+ </SELECT>
+ </TD>
+ </TR>
+
+ <TR>
+ <TH ALIGN="right">New password: </TH>
+ <TD>
+ <INPUT ID="new_password" TYPE="password" NAME="new_password" SIZE="18">
+ <DIV ID="new_password_result"></DIV>
+<? include('elements/add_password_validation.php'); ?>
+ <SCRIPT>add_password_validation('new_password');</SCRIPT>
+ </TD>
+ </TR>
+
+ <TR>
+ <TH ALIGN="right">Re-enter new password: </TH>
+ <TD><INPUT TYPE="password" NAME="new_password2" SIZE="18"></TD>
+ </TR>
+
+</TABLE>
+<BR>
+
+<INPUT TYPE="submit" VALUE="Change password">
+
+</FORM>
+
+<?
+} // end if $pwd_change_show_form
+?>
+
<? include('elements/menu_footer.php'); ?>
<? include('elements/footer.php'); ?>
diff --git a/ng_selfservice/xmlrpc_validate_passwd.php b/ng_selfservice/xmlrpc_validate_passwd.php
new file mode 100644
index 0000000..5632dc3
--- /dev/null
+++ b/ng_selfservice/xmlrpc_validate_passwd.php
@@ -0,0 +1,15 @@
+<?
+
+require_once('elements/session.php');
+
+$xmlrpc_args = array(
+ fieldid => $_POST['fieldid'],
+ check_password => $_POST['check_password'],
+ svcnum => $_POST['svcnum'],
+ session_id => $_COOKIE['session_id']
+);
+
+$result = $freeside->validate_passwd($xmlrpc_args);
+echo json_encode($result);
+
+?>