'View package definition costs', #NEWNEW
'Change package start date',
'Change package contract end date',
+ 'Unmask customer DL',
+ 'Unmask customer SSN',
],
###
=cut
1;
-
my($self, $access_user, $new_password) = @_;
# do nothing if the password is unchanged
- return if $self->authenticate( $access_user, $new_password );
+ #XXX breaks password changes in employee edit ($access_user object already
+ # has new [plaintext] password)
+ #return if $self->authenticate( $access_user, $new_password );
$self->change_password_fields( $access_user, $new_password );
'process-skip_first' => $conf->exists('selfservice_process-skip_first'),
'num_payments' => scalar($cust_main->cust_pay),
'surcharge_percentage' => scalar($conf->config('credit-card-surcharge-percentage', $cust_main->agentnum)),
+ 'surcharge_flatfee' => scalar($conf->config('credit-card-surcharge-flatfee', $cust_main->agentnum)),
);
@$argsref = ( %args );
my($context, $session, $custnum) = _custoragent_session_custnum($p);
#return { 'error' => $session } if $context eq 'error';
+ my $domain = $session->{'domain'};
+
my $agentnum = '';
if ( $context eq 'customer' && $custnum ) {
$p->{'agentnum'} = $agentnum;
my $conf = new FS::Conf;
-
+ my $timeout = $conf->config('selfservice-session_timeout') || '1 hour';
#false laziness w/Signup.pm
my $skin_info_cache_agent = _cache->get("skin_info_cache_agent$agentnum");
warn "$me populating skin info cache for agentnum $agentnum\n"
if $DEBUG > 1;
+ my $menu = $conf->config("ng_selfservice-menu", $agentnum );
+
$skin_info_cache_agent = {
'agentnum' => $agentnum,
( map { $_ => scalar( $conf->config($_, $agentnum) ) }
( map { $_ => join("\n", $conf->config("selfservice-$_", $agentnum ) ) }
qw( head body_header body_footer company_address ) ),
'money_char' => $conf->config("money_char") || '$',
- 'menu' => join("\n", $conf->config("ng_selfservice-menu", $agentnum ) ) ||
+ 'menu' => _menu($domain,$menu),
+ };
+
+ _cache->set("skin_info_cache_agent$agentnum", $skin_info_cache_agent, $timeout);
+
+ }
+
+ #{ %$skin_info_cache_agent };
+ $skin_info_cache_agent;
+
+}
+
+## checks if page is in menu listing, if not sends to main with error.
+sub check_access {
+ my $p = shift;
+ my $error;
+
+ return if $p->{'page'} eq "index.php";
+ return if $p->{'page'} eq "ip_login.php";
+
+ return if substr($p->{'page'}, 0, length("process_")) eq "process_";
+
+ my $conf = new FS::Conf;
+
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+
+ my $domain = ref($session) ? $session->{'domain'} : '';
+
+ my $agentnum = '';
+ if ( $context eq 'customer' && $custnum ) {
+
+ my $sth = dbh->prepare('SELECT agentnum FROM cust_main WHERE custnum = ?')
+ or die dbh->errstr;
+
+ $sth->execute($custnum) or die $sth->errstr;
+
+ $agentnum = $sth->fetchrow_arrayref->[0]
+ or die "no agentnum for custnum $custnum";
+
+ #} elsif ( $context eq 'agent' ) {
+ } elsif ( defined($p->{'agentnum'}) and $p->{'agentnum'} =~ /^(\d+)$/ ) {
+ $agentnum = $1;
+ }
+ $p->{'agentnum'} = $agentnum;
+
+ my $menu = $conf->config("ng_selfservice-menu", $agentnum );
+
+ my $allowed_pages = _menu($domain,$menu);
+
+ my %allowed;
+ my @lines = split /\n/, $allowed_pages;
+ foreach my $line (@lines) {
+ chomp; # remove newlines
+ $line =~ s/^\s+//; # remove leading whitespace
+ next unless length($line);
+ my (@pages) = split(/ /, $line, 2);
+ $allowed{$pages[0]} = $pages[1];
+ }
+
+ $error = "You do not have access to the page ".$allowed{$p->{page}} unless $allowed{$p->{page}};
+
+ return { 'error' => $error, };
+
+}
+
+sub _menu {
+ my $p = shift;
+ my $m = shift;
+
+ my $menu;
+
+ if ($p eq 'ip_mac') {
+ $menu = 'main.php Home
+
+ payment.php Payments
+ payment_cc.php Credit Card Payment
+ payment_ach.php Electronic Check Payment
+ payment_paypal.php PayPal Payment
+ payment_webpay.php Webpay Payments
+
+ docs.php FAQs
+
+ logout.php Logout
+ ';
+ }
+ else {
+ $menu = join("\n", $m ) ||
'main.php Home
services.php Services
docs.php FAQs
logout.php Logout
- ',
- };
+ ';
+ }
+ return $menu;
+}
- _cache->set("skin_info_cache_agent$agentnum", $skin_info_cache_agent);
+sub get_mac_address {
+ my $p = shift;
- }
+## access radius exports acct tables to get mac
+ my @part_export = ();
+ @part_export = (
+ qsearch( 'part_export', { 'exporttype' => 'sqlradius' } ),
+ qsearch( 'part_export', { 'exporttype' => 'sqlradius_withdomain' } ),
+ qsearch( 'part_export', { 'exporttype' => 'broadband_sqlradius' } ),
+ );
- #{ %$skin_info_cache_agent };
- $skin_info_cache_agent;
+ my @sessions;
+ foreach my $part_export (@part_export) {
+ push @sessions, ( @{ $part_export->usage_sessions( {
+ 'ip' => $p->{'ip'},
+ 'session_status' => 'open',
+ } ) } );
+ }
+ return { 'mac_address' => $sessions[0]->{'callingstationid'}, };
}
sub login_info {
my %info = (
%{ skin_info($p) },
- 'phone_login' => $conf->exists('selfservice_server-phone_login'),
- 'single_domain'=> scalar($conf->config('selfservice_server-single_domain')),
+ 'phone_login' => $conf->exists('selfservice_server-phone_login'),
+ 'single_domain' => scalar($conf->config('selfservice_server-single_domain')),
'banner_url' => scalar($conf->config('selfservice-login_banner_url')),
'banner_image_md5' =>
md5_hex($conf->config_binary('selfservice-login_banner_image')),
} elsif ( $p->{'domain'} eq 'ip_mac' ) {
- my $svc_broadband = qsearchs( 'svc_broadband', { 'mac_addr' => $p->{'username'} } );
- return { error => 'IP address not found' }
+ return { error => 'MAC address empty '.$p->{'username'} }
+ unless $p->{'username'};
+
+ my $mac_address = $p->{'username'};
+ $mac_address =~ s/[\:\,\-\. ]//g;
+ $mac_address =~ tr/[a-z]/[A-Z/;
+
+ my $svc_broadband = qsearchs( 'svc_broadband', { 'mac_addr' => $mac_address } );
+ return { error => 'MAC address not found '.$p->{'username'} }
unless $svc_broadband;
$svc_x = $svc_broadband;
+ $session->{'domain'} = $p->{'domain'};
+
} elsif ( $p->{email}
&& (my $contact = FS::contact->by_selfservice_email($p->{email}))
)
for (@cust_main_editable_fields) {
$return{$_} = $cust_main->get($_);
}
+ $return{$_} = $cust_main->masked($_) for qw/ss stateid/;
+
#maybe a little more expensive, but it should be cached by now
for (@location_editable_fields) {
$return{$_} = $cust_main->bill_location->get($_)
$return{paybatch} = $return{payunique}; #back compat
$return{credit_card_surcharge_percentage} = $conf->config('credit-card-surcharge-percentage', $cust_main->agentnum);
+ $return{credit_card_surcharge_flatfee} = $conf->config('credit-card-surcharge-flatfee', $cust_main->agentnum);
return { 'error' => '',
%return,
})
or return { 'error' => 'unknown custpaybynum '. $p->{'custpaybynum'} };
+ my $cust_main = qsearchs( 'cust_main', {custnum => $cust_payby->custnum} )
+ or return { 'error' => 'unknown custnum '.$cust_payby->custnum };
+
foreach my $field (
qw( weight payby payinfo paycvv paydate payname paystate paytype payip )
) {
next unless exists($p->{$field});
$cust_payby->set($field,$p->{$field});
}
+ $cust_payby->set( 'paymask' => $cust_payby->mask_payinfo );
- my $error = $cust_payby->replace;
- if ( $error ) {
- return { 'error' => $error };
- } else {
- return { 'custpaybynum' => $cust_payby->custpaybynum };
+ # Update column if given a value, and the given value wasn't
+ # the value generated by $cust_main->masked($column);
+ $cust_main->set( $_, $p->{$_} )
+ for grep{ $p->{$_} !~ /^x/i; }
+ grep{ exists $p->{$_} }
+ qw/ss stateid/;
+
+ # Perform updates within a transaction
+ local $FS::UID::AutoCommit = 0;
+
+ if ( my $error = $cust_payby->replace || $cust_main->replace ) {
+ dbh->rollback;
+ return { error => $error };
}
-
+
+ dbh->commit;
+ return { custpaybynum => $cust_payby->custpaybynum };
}
sub verify_payby {
}
1;
-
'quotation_add_pkg' => 'MyAccount/quotation/quotation_add_pkg',
'quotation_remove_pkg' => 'MyAccount/quotation/quotation_remove_pkg',
'quotation_order' => 'MyAccount/quotation/quotation_order',
+ 'get_mac_address' => 'MyAccount/get_mac_address',
+ 'check_access' => 'MyAccount/check_access',
'freesideinc_service' => 'Freeside/freesideinc_service',
};
},
{
+ 'key' => 'credit-card-surcharge-flatfee',
+ 'section' => 'credit_cards',
+ 'description' => 'Add a credit card surcharge to invoices, as a flat fee. WARNING: Although recently permitted to US merchants in general, specific consumer protection laws may prohibit or restrict this practice in California, Colorado, Connecticut, Florda, Kansas, Maine, Massachusetts, New York, Oklahome, and Texas. Surcharging is also generally prohibited in most countries outside the US, AU and UK. When allowed, typically not permitted to be above 4%.',
+ 'type' => 'text',
+ 'per_agent' => 1,
+ },
+
+ {
+ 'key' => 'credit-card-surcharge-text',
+ 'section' => 'credit_cards',
+ 'description' => 'Text for the credit card surcharge invoice line. If not set, it will default to Credit Card Surcharge.',
+ 'type' => 'text',
+ 'per_agent' => 1,
+ },
+
+ {
'key' => 'discount-show-always',
'section' => 'invoicing',
'description' => 'Generate a line item on an invoice even when a package is discounted 100%',
'section' => 'invoicing',
'description' => 'Indicates that html and latex invoices should be in summary style and make use of invoice_latexsummary.',
'type' => 'checkbox',
+ 'per_agent' => 1,
},
{
{
'key' => 'invoice_sections',
'section' => 'invoicing',
- 'description' => 'Split invoice into sections and label according to package category when enabled.',
+ 'description' => 'Split invoice into sections and label according to either package category or location when enabled.',
'type' => 'checkbox',
'per_agent' => 1,
+ 'config_bool' => 1,
+ },
+
+ {
+ 'key' => 'invoice_sections_multilocation',
+ 'section' => 'invoicing',
+ 'description' => 'Enable invoice_sections for for any bill with at least this many locations on the bill.',
+ 'type' => 'text',
+ 'per_agent' => 1,
+ 'validate' => sub { shift =~ /^\d+$/ ? undef : 'Please enter a number' },
},
{
},
{
+ 'key' => 'invoice_sections_with_taxes',
+ 'section' => 'invoicing',
+ 'description' => 'Include taxes within each section of mutli-section invoices.',
+ 'type' => 'checkbox',
+ 'per_agent' => 1,
+ 'agent_bool' => 1,
+ },
+
+ {
'key' => 'summary_subtotals_method',
'section' => 'invoicing',
'description' => 'How to group line items when calculating summary subtotals. By default, it will be the same method used for grouping invoice sections.',
'description' => 'Template to use for manual payment receipts.',
%msg_template_options,
},
+
+ {
+ 'key' => 'payment_receipt_msgnum_auto',
+ 'section' => 'notification',
+ 'description' => 'Automatic payments will cause a post-payment to use a message template for automatic payment receipts rather than a post payment statement.',
+ %msg_template_options,
+ },
{
'key' => 'payment_receipt_from',
{
'key' => 'passwordmin',
'section' => 'password',
- 'description' => 'Minimum password length (default 6)',
+ 'description' => 'Minimum password length (default 8)',
'type' => 'text',
},
{
'key' => 'unmask_ss',
- 'section' => 'e-checks',
+ 'section' => 'deprecated',
'description' => "Don't mask social security numbers in the web interface.",
'type' => 'checkbox',
},
},
{
+ 'key' => 'manual_process-single_invoice_amount',
+ 'section' => 'deprecated',
+ 'description' => 'When entering manual credit card and ACH payments, amount will not autofill if the customer has more than one open invoice',
+ 'type' => 'checkbox',
+ },
+
+ {
'key' => 'manual_process-pkgpart',
'section' => 'payments',
'description' => 'Package to add to each manual credit card and ACH payment entered by employees from the backend. WARNING: Although recently permitted to US merchants in general, specific consumer protection laws may prohibit or restrict this practice in California, Colorado, Connecticut, Florda, Kansas, Maine, Massachusetts, New York, Oklahome, and Texas. Surcharging is also generally prohibited in most countries outside the US, AU and UK.',
);
1;
-
$self->ip_addr('');
}
+ # strip user-entered leading 0's from IPv4 addresses
+ # Parsers like NetAddr::IP interpret them as octal instead of decimal
+ $self->ip_addr(
+ join( '.', (
+ map{ int($_) }
+ split( /\./, $self->ip_addr )
+ ))
+ ) if $self->ip_addr =~ /\./ && $self->ip_addr =~ /[\.^]0/;
+
if ( $self->ip_addr
and !$self->router
and $self->conf->exists('auto_router') ) {
my $self = shift;
my %opt = @_;
+ #otherwise we'll get the same assignment for concurrent identical calls
+ # this will serialize them
+ $_->lock_table foreach @subclasses;
+
my @blocks;
my $na = $self->NetAddr;
FS::router->by_key($self->routernum);
}
-=item used_addresses [ BLOCK ]
+=item used_addresses [ FS::addr_block ]
+
+Returns a list of all addresses in use within the given L<FS::addr_block>.
-Returns a list of all addresses (in BLOCK, or in all blocks)
-that are in use. If called as an instance method, excludes
-that instance from the search.
+If called as an instance method, excludes that instance from the search.
=cut
sub used_addresses {
- my $self = shift;
- my $block = shift;
- return ( map { $_->_used_addresses($block, $self) } @subclasses );
+ my ($self, $block) = @_;
+
+ (
+ $block->ip_gateway ? $block->ip_gateway : (),
+ $block->NetAddr->broadcast->addr,
+ map { $_->_used_addresses($block, $self ) } @subclasses
+ );
}
sub _used_addresses {
package FS::Misc;
use strict;
-use vars qw ( @ISA @EXPORT_OK $DEBUG );
+use vars qw ( @ISA @EXPORT_OK $DEBUG $DISABLE_ALL_NOTICES );
use Exporter;
use Carp;
use Data::Dumper;
generate_ps generate_pdf do_print
csv_from_fixed
ocr_image
- bytes_substr
money_pretty
);
called from multiple other modules. These are not OO or necessarily related,
but are collected here to eliminate code duplication.
+=head1 DISABLE ALL NOTICES
+
+Set $FS::Misc::DISABLE_ALL_NOTICES to suppress:
+
+=over 4
+
+=item FS::cust_bill::send_csv
+
+=item FS::cust_bill::spool_csv
+
+=item FS::msg_template::email::send_prepared
+
+=item FS::Misc::send_email
+
+=item FS::Misc::do_print
+
+=item FS::Misc::send_fax
+
+=item FS::Template_Mixin::postal_mail_fsinc
+
+=back
+
+=cut
+
+$DISABLE_ALL_NOTICES = 0;
+
=head1 SUBROUTINES
=over 4
sub send_email {
my(%options) = @_;
+
+ if ( $DISABLE_ALL_NOTICES ) {
+ warn 'send_email() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
+ return;
+ }
+
if ( $DEBUG ) {
my %doptions = %options;
$doptions{'body'} = '(full body not shown in debug)';
die 'HylaFAX support has not been configured.'
unless $conf->exists('hylafax');
+ if ( $DISABLE_ALL_NOTICES ) {
+ warn 'send_fax() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
+ return;
+ }
+
eval {
require Fax::Hylafax::Client;
};
sub do_print {
my( $data, %opt ) = @_;
+ if ( $DISABLE_ALL_NOTICES ) {
+ warn 'do_print() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
+ return;
+ }
+
my $lpr = ( exists($opt{'lpr'}) && $opt{'lpr'} )
? $opt{'lpr'}
: $conf->config('lpr', $opt{'agentnum'} );
=item bytes_substr STRING, OFFSET[, LENGTH[, REPLACEMENT] ]
+DEPRECATED
+ Use Unicode::Truncate truncate_egc instead
+
A replacement for "substr" that counts raw bytes rather than logical
characters. Unlike "bytes::substr", will suppress fragmented UTF-8 characters
rather than output them. Unlike real "substr", is not an lvalue.
=cut
-sub bytes_substr {
- my ($string, $offset, $length, $repl) = @_;
- my $bytes = substr(
- Encode::encode('utf8', $string),
- $offset,
- $length,
- Encode::encode('utf8', $repl)
- );
- my $chk = $DEBUG ? Encode::FB_WARN : Encode::FB_QUIET;
- return Encode::decode('utf8', $bytes, $chk);
-}
+# sub bytes_substr {
+# my ($string, $offset, $length, $repl) = @_;
+# my $bytes = substr(
+# Encode::encode('utf8', $string),
+# $offset,
+# $length,
+# Encode::encode('utf8', $repl)
+# );
+# my $chk = $DEBUG ? Encode::FB_WARN : Encode::FB_QUIET;
+# return Encode::decode('utf8', $bytes, $chk);
+# }
=item money_pretty
--- /dev/null
+package FS::Misc::FixIPFormat;
+use strict;
+use warnings;
+use FS::Record qw(dbh qsearchs);
+use FS::upgrade_journal;
+
+=head1 NAME
+
+FS::Misc::FixIPFormat - Functions to repair bad IP address input
+
+=head1 DESCRIPTION
+
+Provides functions for freeside_upgrade to check IP address storage for
+user-entered leading 0's in IP addresses. When read from database, NetAddr::IP
+would treat the number as octal isntead of decimal. If a user entered
+10.0.0.052, this may get invisibly translated to 10.0.0.42 when exported.
+Base8:52 = Base0:42
+
+Tied to freeside_upgrade with journal name TABLE__fixipformat
+
+see: RT# 80555
+
+=head1 SYNOPSIS
+
+Usage:
+
+ # require, not use - this module is only run once
+ require FS::Misc::FixIPFormat;
+
+ my $error = FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+ 'svc_broadband', 'svcnum', 'ip_addr'
+ );
+ die "oh no!" if $error;
+
+=head2 fix_bad_addresses_in_table TABLE, ID_COLUMN, IP_COLUMN
+
+$error = fix_bad_addresses_in_table( 'svc_broadband', 'svcnum', 'ip_addr' );
+
+=cut
+
+sub fix_bad_addresses_in_table {
+ my ( $table ) = @_;
+ return if FS::upgrade_journal->is_done("${table}__fixipformat");
+ for my $id ( find_bad_addresses_in_table( @_ )) {
+ if ( my $error = fix_ip_for_record( $id, @_ )) {
+ die "fix_bad_addresses_in_table(): $error";
+ }
+ }
+ FS::upgrade_journal->set_done("${table}__fixipformat");
+ 0;
+}
+
+=head2 find_bad_addresses_in_table TABLE, ID_COLUMN, IP_COLUMN
+
+@id = find_bad_addresses_in_table( 'svc_broadband', 'svcnum', 'ip_addr' );
+
+=cut
+
+sub find_bad_addresses_in_table {
+ my ( $table, $id_col, $ip_col ) = @_;
+ my @fix_ids;
+
+ # using DBI directly for performance
+ my $sql_statement = "
+ SELECT $id_col, $ip_col
+ FROM $table
+ WHERE $ip_col IS NOT NULL
+ ";
+ my $sth = dbh->prepare( $sql_statement ) || die "SQL ERROR ".dbh->errstr;
+ $sth->execute || die "SQL ERROR ".dbh->errstr;
+ while ( my $row = $sth->fetchrow_hashref ) {
+ push @fix_ids, $row->{ $id_col }
+ if $row->{ $ip_col } =~ /[\.^]0\d/;
+ }
+ @fix_ids;
+}
+
+=head2 fix_ip_for_record ID, TABLE, ID_COLUMN, IP_COLUMN
+
+Attempt to strip the leading 0 from a stored IP address record. If
+the corrected IP address would be a duplicate of another record in the
+same table, thow an exception.
+
+$error = fix_ip_for_record( 1001, 'svc_broadband', 'svcnum', 'ip_addr', );
+
+=cut
+
+sub fix_ip_for_record {
+ my ( $id, $table, $id_col, $ip_col ) = @_;
+
+ my $row = qsearchs($table, {$id_col => $id})
+ || die "Error finding $table record for id $id";
+
+ my $ip = $row->getfield( $ip_col );
+ my $fixed_ip = join( '.',
+ map{ int($_) }
+ split( /\./, $ip )
+ );
+
+ return undef unless $ip ne $fixed_ip;
+
+ if ( my $dupe_row = qsearchs( $table, {$ip_col => $fixed_ip} )) {
+ if ( $dupe_row->getfield( $id_col ) != $row->getfield( $id_col )) {
+ # Another record in the table has this IP address
+ # Eg one ip is provisioned as 10.0.0.51 and another is
+ # provisioned as 10.0.0.051. Cannot auto-correct by simply
+ # trimming leading 0. Die, let support decide how to fix.
+
+ die "Invalid IP address could not be auto-corrected - ".
+ "($table - $id_col = $id, $ip_col = $ip) ".
+ "colission with another reocrd - ".
+ "($table - $id_col = ".$dupe_row->getfield( $id_col )." ".
+ "$ip_col = ",$dupe_row->getfield( $ip_col )." ) - ".
+ "The entry must be corrected to continue";
+ }
+ }
+
+ warn "Autocorrecting IP address problem for ".
+ "($table - $id_col = $id, $ip_col = $ip) $fixed_ip\n";
+ $row->setfield( $ip_col, $fixed_ip );
+ $row->replace;
+}
+
+1;
--- /dev/null
+package FS::Misc::Savepoint;
+
+use strict;
+use warnings;
+
+use Exporter;
+use vars qw( @ISA @EXPORT @EXPORT_OK );
+@ISA = qw( Exporter );
+@EXPORT = qw( savepoint_create savepoint_release savepoint_rollback );
+
+use FS::UID qw( dbh );
+use Carp qw( croak );
+
+=head1 NAME
+
+FS::Misc::Savepoint - Provides methods for SQL Savepoints
+
+=head1 SYNOPSIS
+
+ use FS::Misc::Savepoint;
+
+ # Only valid within a transaction
+ local $FS::UID::AutoCommit = 0;
+
+ savepoint_create( 'savepoint_label' );
+
+ my $error_msg = do_some_things();
+
+ if ( $error_msg ) {
+ savepoint_rollback_and_release( 'savepoint_label' );
+ } else {
+ savepoint_release( 'savepoint_label' );
+ }
+
+
+=head1 DESCRIPTION
+
+Provides methods for SQL Savepoints
+
+Using a savepoint allows for a partial roll-back of SQL statements without
+forcing a rollback of the entire enclosing transaction.
+
+=head1 METHODS
+
+=over 4
+
+=item savepoint_create LABEL
+
+=item savepoint_create { label => LABEL, dbh => DBH }
+
+Executes SQL to create a savepoint named LABEL.
+
+Savepoints cannot work while AutoCommit is enabled.
+
+Savepoint labels must be valid sql identifiers. If your choice of label
+would not make a valid column name, it probably will not make a valid label.
+
+Savepoint labels must be unique within the transaction.
+
+=cut
+
+sub savepoint_create {
+ my %param = _parse_params( @_ );
+
+ $param{dbh}->do("SAVEPOINT $param{label}")
+ or die $param{dbh}->errstr;
+}
+
+=item savepoint_release LABEL
+
+=item savepoint_release { label => LABEL, dbh => DBH }
+
+Release the savepoint - preserves the SQL statements issued since the
+savepoint was created, but does not commit the transaction.
+
+The savepoint label is freed for future use.
+
+=cut
+
+sub savepoint_release {
+ my %param = _parse_params( @_ );
+
+ $param{dbh}->do("RELEASE SAVEPOINT $param{label}")
+ or die $param{dbh}->errstr;
+}
+
+=item savepoint_rollback LABEL
+
+=item savepoint_rollback { label => LABEL, dbh => DBH }
+
+Roll back the savepoint - forgets all SQL statements issues since the
+savepoint was created, but does not commit or roll back the transaction.
+
+The savepoint still exists. Additional statements may be executed,
+and savepoint_rollback called again.
+
+=cut
+
+sub savepoint_rollback {
+ my %param = _parse_params( @_ );
+
+ $param{dbh}->do("ROLLBACK TO SAVEPOINT $param{label}")
+ or die $param{dbh}->errstr;
+}
+
+=item savepoint_rollback_and_release LABEL
+
+=item savepoint_rollback_and_release { label => LABEL, dbh => DBH }
+
+Rollback and release the savepoint
+
+=cut
+
+sub savepoint_rollback_and_release {
+ savepoint_rollback( @_ );
+ savepoint_release( @_ );
+}
+
+=back
+
+=head1 METHODS - Internal
+
+=over 4
+
+=item _parse_params
+
+Create %params from function input
+
+Basic savepoint label validation
+
+Complain when trying to use savepoints without disabling AutoCommit
+
+=cut
+
+sub _parse_params {
+ my %param = ref $_[0] ? %{ $_[0] } : ( label => $_[0] );
+ $param{dbh} ||= dbh;
+
+ # Savepoints may be any valid SQL identifier up to 64 characters
+ $param{label} =~ /^\w+$/
+ or croak sprintf(
+ 'Invalid savepont label(%s) - use only numbers, letters, _',
+ $param{label}
+ );
+
+ croak sprintf( 'Savepoint(%s) failed - AutoCommit=1', $param{label} )
+ if $FS::UID::AutoCommit;
+
+ %param;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+=cut
+
+1;
\ No newline at end of file
sub ut_ip {
my( $self, $field ) = @_;
$self->setfield($field, '127.0.0.1') if $self->getfield($field) eq '::1';
- $self->getfield($field) =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
- or return "Illegal (IP address) $field: ". $self->getfield($field);
- for ( $1, $2, $3, $4 ) { return "Illegal (IP address) $field" if $_ > 255; }
- $self->setfield($field, "$1.$2.$3.$4");
- '';
+ return "Illegal (IP address) $field: ".$self->getfield($field)
+ unless $self->getfield($field) =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
+ $self->ut_ip46($field);
}
=item ut_ipn COLUMN
sub ut_ip46 {
my( $self, $field ) = @_;
- my $ip = NetAddr::IP->new($self->getfield($field))
+ my $ip_addr = $self->getfield( $field );
+
+ # strip user-entered leading 0's from IPv4 addresses
+ # Parsers like NetAddr::IP interpret them as octal instead of decimal
+ $ip_addr = join( '.', (
+ map{ int($_) }
+ split( /\./, $ip_addr )
+ )
+ ) if $ip_addr =~ /\./ && $ip_addr =~ /[\.^]0/;
+
+ my $ip = NetAddr::IP->new( $ip_addr )
or return "Illegal (IP address) $field: ".$self->getfield($field);
$self->setfield($field, lc($ip->addr));
return '';
: '';
}
+=item ut_date COLUMN
+
+Check/untaint a column containing a date string.
+
+Date will be normalized to YYYY-MM-DD format
+
+=cut
+
+sub ut_date {
+ my ( $self, $field ) = @_;
+ my $value = $self->getfield( $field );
+
+ my @date = split /[\-\/]/, $value;
+ if ( scalar(@date) == 3 ) {
+ @date = @date[2,0,1] if $date[2] >= 1900;
+
+ local $@;
+ my $ymd;
+ eval {
+ # DateTime will die given invalid date
+ $ymd = DateTime->new(
+ year => $date[0],
+ month => $date[1],
+ day => $date[2],
+ )->ymd('-');
+ };
+
+ unless( $@ ) {
+ $self->setfield( $field, $ymd ) unless $value eq $ymd;
+ return '';
+ }
+
+ }
+ return "Illegal (date) field $field: $value";
+}
+
+=item ut_daten COLUMN
+
+Check/untaint a column containing a date string.
+
+Column may be null.
+
+Date will be normalized to YYYY-MM-DD format
+
+=cut
+
+sub ut_daten {
+ my ( $self, $field ) = @_;
+
+ $self->getfield( $field ) =~ /^()$/
+ ? $self->setfield( $field, '' )
+ : $self->ut_date( $field );
+}
+
=item ut_flag COLUMN
Check/untaint a column if it contains either an empty string or 'Y'. This
&& driver_name eq 'Pg'
)
{
- dbh->quote($value, { pg_type => PG_BYTEA() });
+ local $@;
+
+ eval { $value = dbh->quote($value, { pg_type => PG_BYTEA() }); };
+
+ if ( $@ && $@ =~ /Wide character/i ) {
+ warn 'Correcting malformed UTF-8 string for binary quote()'
+ if $DEBUG;
+ utf8::decode($value);
+ utf8::encode($value);
+ $value = dbh->quote($value, { pg_type => PG_BYTEA() });
+ }
+
+ $value;
} else {
dbh->quote($value);
}
--- /dev/null
+package FS::Report::Queued::FutureAutobill;
+use strict;
+use warnings;
+use vars qw( $job );
+
+use FS::Conf;
+use FS::cust_main;
+use FS::cust_main::Location;
+use FS::cust_payby;
+use FS::CurrentUser;
+use FS::Log;
+use FS::Mason qw(mason_interps);
+use FS::Record qw( qsearch );
+use FS::UI::Web;
+use FS::UID qw( dbh );
+
+use DateTime;
+use File::Temp;
+use Data::Dumper;
+use HTML::Entities qw( encode_entities );
+
+=head1 NAME
+
+FS::Report::Queued::FutureAutobill - Future Auto-Bill Transactions Report
+
+=head1 DESCRIPTION
+
+Future Autobill report generated within the job queue.
+
+Report results are saved to temp storage as a Mason fragment
+that is rendered by the queued report viewer.
+
+For every customer with a valid auto-bill payment method,
+report runs bill_and_collect() for each day, from today through
+the report target date. After recording the results, all
+operations are rolled back.
+
+This report relies on the ability to safely run bill_and_collect(),
+with all exports and messaging disabled, and then to roll back the
+results.
+
+=head1 PARAMETERS
+
+C<agentnum>, C<target_date>
+
+=cut
+
+sub make_report {
+ $job = shift;
+ my $param = shift;
+ my $outbuf;
+ my $DEBUG = 0;
+
+ my $time_begin = time();
+
+ my $report_fh = File::Temp->new(
+ TEMPLATE => 'report.future_autobill.XXXXXXXX',
+ DIR => sprintf( '%s/cache.%s', $FS::Conf::base_dir, $FS::UID::datasrc ),
+ UNLINK => 0
+ ) or die "Cannot create report file: $!";
+
+ if ( $DEBUG ) {
+ warn Dumper( $job );
+ warn Dumper( $param );
+ warn $report_fh;
+ warn $report_fh->filename;
+ }
+
+ my $curuser = FS::CurrentUser->load_user( $param->{CurrentUser} )
+ or die 'Unable to set report user';
+
+ my ( $fs_interp ) = FS::Mason::mason_interps(
+ 'standalone',
+ outbuf => \$outbuf,
+ );
+ $fs_interp->error_mode('fatal');
+ $fs_interp->error_format('text');
+
+ $FS::Mason::Request::QUERY_STRING = sprintf(
+ 'target_date=%s&agentnum=%s',
+ encode_entities( $param->{target_date} ),
+ encode_entities( $param->{agentnum} || '' ),
+ );
+ $FS::Mason::Request::FSURL = $param->{RootURL};
+
+ my $mason_request = $fs_interp->make_request(
+ comp => '/search/future_autobill.html'
+ );
+
+ {
+ local $@;
+ eval{ $mason_request->exec() };
+ if ( $@ ) {
+ my $error = ref $@ eq 'HTML::Mason::Exception' ? $@->error : $@;
+
+ my $log = FS::Log->new('FS::Report::Queued::FutureAutobill');
+ $log->error(
+ "Error generating report: $FS::Mason::Request::QUERY_STRING $error"
+ );
+ die $error;
+ }
+ }
+
+ my $report_fn;
+ if ( $report_fh->filename =~ /report\.(future_autobill.+)$/ ) {
+ $report_fn = $1
+ } else {
+ die 'Error parsing report filename '.$report_fh->filename;
+ }
+
+ my $report_title = FS::cust_payby->future_autobill_report_title();
+ my $time_rendered = time() - $time_begin;
+
+ if ( $DEBUG ) {
+ warn "Generated content:\n";
+ warn $outbuf;
+ warn $report_fn;
+ warn $report_title;
+ }
+
+ print $report_fh qq{<% include("/elements/header.html", '$report_title') %>\n};
+ print $report_fh $outbuf;
+ print $report_fh qq{<!-- Time to render report $time_rendered seconds -->};
+ print $report_fh qq{<% include("/elements/footer.html") %>\n};
+
+ die sprintf
+ "<a href=%s/misc/queued_report.html?report=%s>view</a>\n",
+ $param->{RootURL},
+ $report_fn;
+}
+
+1;
}
+=item cust_bill_pkg_discount: Discounts issued
+
+Arguments: agentnum, refnum, cust_classnum
+
+=cut
+
sub cust_bill_pkg_discount {
my $self = shift;
my ($speriod, $eperiod, $agentnum, %opt) = @_;
$self->scalar_sql($total_sql);
}
+=item cust_bill_pkg_discount_or_waived: Discounts and waived fees issued
+
+Arguments: agentnum, refnum, cust_classnum
+
+=cut
+
+sub cust_bill_pkg_discount_or_waived {
+
+ my $self = shift;
+ my ($speriod, $eperiod, $agentnum, %opt) = @_;
+
+ $agentnum ||= $opt{'agentnum'};
+
+ my $total_sql = "
+ SELECT
+ COALESCE(
+ SUM(
+ COALESCE(
+ cust_bill_pkg_discount.amount,
+ CAST(( SELECT optionvalue
+ FROM part_pkg_option
+ WHERE
+ part_pkg_option.pkgpart = cust_pkg.pkgpart
+ AND optionname = 'setup_fee'
+ ) AS NUMERIC )
+ )
+ ),
+ 0
+ )
+ FROM cust_bill_pkg
+ LEFT JOIN cust_bill_pkg_discount USING (billpkgnum)
+ LEFT JOIN cust_pkg ON cust_bill_pkg.pkgnum = cust_pkg.pkgnum
+ LEFT JOIN part_pkg USING (pkgpart)
+ LEFT JOIN cust_bill USING ( invnum )
+ LEFT JOIN cust_main ON cust_pkg.custnum = cust_main.custnum
+ WHERE
+ (
+ cust_bill_pkg_discount.billpkgdiscountnum IS NOT NULL
+ OR (
+ cust_pkg.setup = cust_bill_pkg.sdate
+ AND cust_pkg.waive_setup = 'Y'
+ )
+ )
+ AND cust_bill_pkg.pkgpart_override IS NULL
+ " . join "\n",
+ map { " AND ( $_ ) " }
+ grep { $_ }
+ $self->with_classnum($opt{'classnum'}, $opt{'use_override'}),
+ $self->with_report_option(%opt),
+ $self->in_time_period_and_agent($speriod, $eperiod, $agentnum);
+
+ $self->scalar_sql($total_sql);
+}
+
sub cust_bill_pkg_taxes {
my $self = shift;
my ($speriod, $eperiod, $agentnum, %opt) = @_;
as suspended,
SUM((s_active = 0 and s_suspended > 0 and e_active > 0)::int)
as resumed,
- SUM((s_active > 0 and e_active = 0 and e_suspended = 0)::int)
+ SUM((e_active = 0 and e_cancelled > s_cancelled)::int)
as cancelled
FROM ($cust_sql) AS x
";
'country', 'char', '', 2, '', '',
'payby', 'char', '', 4, '', '',
'payinfo', 'varchar', 'NULL', 512, '', '',
+ #'paymask', 'varchar', 'NULL', $char_d, '', '',
#'exp', @date_type, '', '',
'exp', 'varchar', 'NULL', 11, '', '',
'payname', 'varchar', 'NULL', $char_d, '', '',
],
'primary_key' => 'paybatchnum',
'unique' => [],
- 'index' => [ ['batchnum'], ['invnum'], ['custnum'] ],
+ 'index' => [ ['batchnum'], ['invnum'], ['custnum'],['status'] ],
'foreign_keys' => [
{ columns => [ 'batchnum' ],
table => 'pay_batch',
'columns' => [
'pkgpart', 'serial', '', '', '', '',
'pkgpartbatch', 'varchar', 'NULL', $char_d, '', '',
- 'pkg', 'varchar', '', $char_d, '', '',
+ 'pkg', 'varchar', '', 104, '', '',
'comment', 'varchar', 'NULL', 2*$char_d, '', '',
'promo_code', 'varchar', 'NULL', $char_d, '', '',
'freq', 'varchar', '', $char_d, '', '', #billing frequency
'suid', 'int', 'NULL', '', '', '',
'shared_svcnum', 'int', 'NULL', '', '', '',
'serviceid', 'varchar', 'NULL', 64, '', '',#srvexport/reportfields
+ 'speed_test_up', 'int', 'NULL', '', '', '',
+ 'speed_test_down', 'int', 'NULL', '', '', '',
+ 'speed_test_latency', 'int', 'NULL', '', '', '',
],
'primary_key' => 'svcnum',
'unique' => [ [ 'ip_addr' ], [ 'mac_addr' ] ],
'height', 'decimal', 'NULL', '', '', '',
'veg_height', 'decimal', 'NULL', '', '', '',
'color', 'varchar', 'NULL', 6, '', '',
+ 'up_rate_limit', 'int', 'NULL', '', '', '',
+ 'down_rate_limit', 'int', 'NULL', '', '', '',
],
'primary_key' => 'towernum',
'unique' => [ [ 'towername' ] ], # , 'agentnum' ] ],
'east', 'decimal', 'NULL', '10,7', '', '',
'south', 'decimal', 'NULL', '10,7', '', '',
'north', 'decimal', 'NULL', '10,7', '', '',
-
'title', 'varchar', 'NULL', $char_d,'', '',
+ 'up_rate_limit', 'int', 'NULL', '', '', '',
+ 'down_rate_limit', 'int', 'NULL', '', '', '',
],
'primary_key' => 'sectornum',
'unique' => [ [ 'towernum', 'sectorname' ], [ 'ip_addr' ], ],
'path', 'varchar', '', 2*$char_d, '', '',
'_date', @date_type, '', '',
'render_seconds', 'int', 'NULL', '', '', '',
+ 'pid', 'int', 'NULL', '', '', '',
],
'primary_key' => 'lognum',
'unique' => [],
=cut
sub time_period_pretty {
- my( $self, $part_pkg, $agentnum ) = @_;
+ my( $self, $part_pkg, $agentnum, %opt ) = @_;
#more efficient to look some of this conf stuff up outside the
# invoice/template display loop we're called from
# (Template_Mixin::_invoice_cust_bill_pkg) and pass them in as options
- return '' if $conf->exists('disable_line_item_date_ranges')
- || $part_pkg->option('disable_line_item_date_ranges',1)
+ return '' if $opt{'disable_line_item_date_ranges'}
|| ! $self->sdate
|| ! $self->edate;
use Cwd;
use FS::UID;
use FS::Misc qw( send_email );
-use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( qsearch qsearchs dbh );
use FS::Conf;
use FS::Misc qw( generate_ps generate_pdf );
use FS::pkg_category;
=item template
-Dprecated. Used as a suffix for a configuration template. Please
+Deprecated. Used as a suffix for a configuration template. Please
don't use this, it deprecated in favor of more flexible alternatives.
=back
);
}
- if ( $conf->exists('invoice_usesummary', $agentnum) ) {
+ if ( $conf->config_bool('invoice_usesummary', $agentnum) ) {
$invoice_data{'summarypage'} = $summarypage = 1;
}
my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
my $multisection = $self->has_sections;
- $conf->exists($tc.'sections', $cust_main->agentnum) ||
- $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
- $invoice_data{'multisection'} = $multisection;
+ if ( $multisection ) {
+ $invoice_data{multisection} = $conf->config($tc.'sections_method') || 1;
+ }
my $late_sections;
my $extra_sections = [];
my $extra_lines = ();
}
} else {
# subtotal sectioning is the same as for the actual invoice sections
- @summary_subtotals = @sections;
+ @summary_subtotals = grep $_->{subtotal}, @sections;
}
# Hereafter, push sections to both @sections and @summary_subtotals
my %options = ();
$options{'section'} = $section if $multisection;
+ $options{'section_with_taxes'} = 1
+ if $multisection
+ && $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum);
$options{'format'} = $format;
$options{'escape_function'} = $escape_function;
$options{'no_usage'} = 1 unless $unsquelched;
$options{'skip_usage'} =
scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
$options{'preref_callback'} = $params{'preref_callback'};
+ $options{'disable_line_item_date_ranges'} =
+ $conf->exists('disable_line_item_date_ranges');
warn "$me searching for line items\n"
if $DEBUG > 1;
+ my %section_tax_lines;
+ my %seen_tax_lines;
foreach my $line_item ( $self->_items_pkg(%options),
$self->_items_fee(%options) ) {
+ # When bill is sectioned by location, fees may be displayed within the
+ # appropriate location section. Suppress this fee from the taxes/fees
+ # end section, so it doesn't appear to be charged twice and make the
+ # subtotals seem incorrect
+ next
+ if $line_item->{locationnum}
+ && ref $options{section}
+ && !exists $options{section}->{locationnum}
+ && $self->has_sections
+ && $conf->config($tc.'sections_method') eq 'location';
+
warn "$me adding line item ".
join(', ', map "$_=>".$line_item->{$_}, keys %$line_item). "\n"
if $DEBUG > 1;
}
$line_item->{'ext_description'} ||= [];
+ if ( $options{section_with_taxes} && ref $line_item->{pkg_tax} ) {
+ for my $line_tax ( @{$ line_item->{pkg_tax} } ) {
+
+ # It is rarely possible for the same tax record to be presented here
+ # multiple times. See cust_bill_pkg::_pkg_tax_list for more info
+ next if $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} };
+ $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} } = 1;
+
+ $section_tax_lines{ $line_tax->{taxname} } += $line_tax->{amount};
+ }
+ }
+
push @detail_items, $line_item;
}
+ # If conf flag invoice_sections_with_taxes:
+ # - Add @detail_items for taxes into each section
+ # - Update section subtotal to include taxes
+ if ( $options{section_with_taxes} && %section_tax_lines ) {
+ for my $taxname ( keys %section_tax_lines ) {
+
+ push @detail_items, {
+ section => $section,
+ amount => sprintf($money_char."%.2f",$section_tax_lines{$taxname}),
+ description => &$escape_function($taxname),
+ };
+
+ # Append taxes to total. If line format resembles "$5.00 to $12.00"
+ # append to the second value.
+
+ # $section->{subtotal} = '$5.00 to 12.00'; # for testing:
+ if ($section->{subtotal} =~ /to/) {
+ my @subtotal = split /\s/, $section->{subtotal};
+ $subtotal[2] =~ s/[^\d\.]//g;
+ $subtotal[2] = sprintf(
+ $money_char."%.2f",
+ ( $subtotal[2] + $section_tax_lines{$taxname} )
+ );
+ $section->{subtotal} = join ' ', @subtotal;
+ } else {
+ $section->{subtotal} =~ s/[^\d\.]//g;
+ $section->{subtotal} = sprintf(
+ $money_char . "%.2f",
+ ( $section->{subtotal} + $section_tax_lines{$taxname} )
+ );
+ }
+
+ }
+ }
+
if ( $section->{'description'} ) {
push @buf, ( ['','-----------'],
[ $section->{'description'}. ' sub-total',
#$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
#$tax_section->{'sort_weight'} = $tax_weight;
+ my $invoice_sections_with_taxes = $conf->config_bool(
+ 'invoice_sections_with_taxes', $cust_main->agentnum
+ );
+
foreach my $tax ( @items_tax ) {
- $taxtotal += $tax->{'amount'};
my $description = &$escape_function( $tax->{'description'} );
my $amount = sprintf( '%.2f', $tax->{'amount'} );
if ( $multisection ) {
+ if ( !$invoice_sections_with_taxes ) {
+
+ $taxtotal += $tax->{'amount'};
+
+ push @detail_items, {
+ ext_description => [],
+ ref => '',
+ quantity => '',
+ description => $description,
+ amount => $money_char. $amount,
+ product_code => '',
+ section => $tax_section,
+ };
- push @detail_items, {
- ext_description => [],
- ref => '',
- quantity => '',
- description => $description,
- amount => $money_char. $amount,
- product_code => '',
- section => $tax_section,
- };
-
+ }
} else {
+ $taxtotal += $tax->{'amount'};
+
push @total_items, {
'total_item' => $description,
'total_amount' => $other_money_char. $amount,
$other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
if ( $multisection ) {
+
+ if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) {
+ # If all tax items are displayed in location/category sections,
+ # remove the empty tax section
+ @sections = grep{ $_ ne $tax_section } @sections
+ unless grep{ $_->{section} eq $tax_section } @detail_items;
+ }
+
if ( $taxtotal > 0 ) {
# there are taxes, so prepare the section to be displayed.
# $taxtotal already includes any line items that were already in the
$tax_section->{'description'} = $self->mt($tax_description);
$tax_section->{'summarized'} = '';
- # append it if it's not already there
- if ( !grep $tax_section, @sections ) {
- push @sections, $tax_section;
- push @summary_subtotals, $tax_section;
- }
- }
+ # append tax section unless it's already there
+ push @sections, $tax_section
+ unless grep {$_ eq $tax_section} @sections;
+ push @summary_subtotals, $tax_section
+ unless grep {$_ eq $tax_section} @summary_subtotals;
+
+ }
} else {
unshift @total_items, $total;
}
my $msg = $self->mt('Balance Due');
return $msg unless $self->terms; # huh?
if ( !$self->conf->exists('invoice_show_prior_due_date')
- or $self->conf->exists('invoice_sections') ) {
+ || $self->has_sections ) {
# if enabled, the due date is shown with Total New Charges (see
# _items_total) and not here
# (yes, or if invoice_sections is enabled; this is just for compatibility)
warn "$me generating plain text invoice"
if $DEBUG;
- # 'print_text' argument is no longer used
- @text = map Encode::encode_utf8($_), $self->print_text(\%args);
+ @text = $self->print_text(\%args);
} else {
'Encoding' => 'quoted-printable',
'Charset' => 'UTF-8',
#'Encoding' => '7bit',
- 'Data' => \@text,
+ 'Data' => [
+ map
+ { Encode::encode('UTF-8', $_, Encode::FB_WARN | Encode::LEAVE_SRC ) }
+ @text
+ ],
'Disposition' => 'inline',
);
' </title>',
' </head>',
' <body bgcolor="#e8e8e8">',
- Encode::encode_utf8($html),
+ Encode::encode(
+ 'UTF-8',
+ $html,
+ Encode::FB_WARN | Encode::LEAVE_SRC
+ ),
' </body>',
'</html>',
],
sub postal_mail_fsinc {
my ( $self, %opt ) = @_;
+ if ( $FS::Misc::DISABLE_PRINT ) {
+ warn 'postal_mail_fsinc() disabled by $FS::Misc::DISABLE_PRINT' if $DEBUG;
+ return;
+ }
+
my $url = 'https://ws.freeside.biz/print';
my $cust_main = $self->cust_main;
foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
next if ( $display->summary && $opt{summary} );
- my $section = $display->section;
+ #my $section = $display->section;
+ #false laziness with the method, but for efficiency inside this loop
+ my $section = $display->get('section');
+ if ( !$section && !$cust_bill_pkg->hidden ) {
+ $section = $cust_bill_pkg->get('categoryname'); #cust_bill->cust_bill_pkg added it (XXX quotations / quotation_section)
+ }
+
my $type = $display->type;
# Set $section = undef if we're sectioning by location and this
# line item _has_ a location (i.e. isn't a fee).
my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
my $escape_function = $options{escape_function};
+ my $locale = $self->cust_main
+ ? $self->cust_main->locale
+ : $self->prospect_main->locale;
+
my @items;
foreach my $cust_bill_pkg (@cust_bill_pkg) {
# cache this, so we don't look it up again in every section
warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
next;
}
- if ( exists($options{section}) and exists($options{section}{category}) )
- {
- my $categoryname = $options{section}{category};
- # then filter for items that have that section
- if ( $part_fee->categoryname ne $categoryname ) {
- warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
- next;
- }
- } # otherwise include them all in the main section
- # XXX what to do when sectioning by location?
+
+ # If _items_fee is called while building a sectioned invoice,
+ # - invoice_sections_method: category
+ # Skip fee records that do not match the section category.
+ # - invoice_sections_method: location
+ # Skip fee records always for location sections.
+ # The fee records will be presented in the tax/fee section instead.
+ if (
+ exists( $options{section} )
+ and
+ (
+ (
+ exists( $options{section}{category} )
+ and
+ $part_fee->categoryname ne $options{section}{category}
+ )
+ or
+ exists( $options{section}{location})
+ )
+ ) {
+ warn "skipping fee '".$part_fee->itemdesc.
+ "'--not in section $options{section}{category}\n" if $DEBUG;
+ next;
+ }
my @ext_desc;
my %base_invnums; # invnum => invoice date
$self->mt('from invoice #[_1] on [_2]', $_, $base_invnums{$_})
);
}
- my $desc = $part_fee->itemdesc_locale($self->cust_main->locale);
+ my $desc = $part_fee->itemdesc_locale($locale);
# but not escape the base description line
+ my @pkg_tax = $cust_bill_pkg->_pkg_tax_list
+ if $options{section_with_taxes};
+
push @items,
{ feepart => $cust_bill_pkg->feepart,
+ billpkgnum => $cust_bill_pkg->billpkgnum,
amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
description => $desc,
- ext_description => \@ext_desc
+ pkg_tax => \@pkg_tax,
+ ext_description => \@ext_desc,
# sdate/edate?
};
}
multisection: a flag indicating that this is a multisection invoice,
which does something complicated.
+section_with_taxes: Look up and include applied taxes for each record
+
Returns a list of hashrefs, each of which may contain:
pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
+ my $agentnum = $self->agentnum;
+
# for location labels: use default location on the invoice date
my $default_locationnum;
if ( $conf->exists('invoice-all_pkg_addresses') ) {
# not normally used, but pass this to the template anyway
$classname = $part_pkg->classname;
+ my @pkg_tax = $cust_bill_pkg->_pkg_tax_list
+ if $opt{section_with_taxes};
+
if ( (!$type || $type eq 'S')
&& ( $cust_bill_pkg->setup != 0
|| $cust_bill_pkg->setup_show_zero
|| ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
|| $cust_bill_pkg->recur_show_zero;
- $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
- $self->agentnum )
+ my $disable_date_ranges =
+ $opt{disable_line_item_date_ranges}
+ || $part_pkg->option('disable_line_item_date_ranges', 1);
+
+ $description .= $cust_bill_pkg->time_period_pretty(
+ $part_pkg,
+ $agentnum,
+ disable_date_ranges => $disable_date_ranges,
+ )
if $part_pkg->is_prepaid #for prepaid, "display the validity period
# triggered by the recurring charge freq
# (RT#26274)
push @{ $s->{ext_description} }, @d;
} else {
$s = {
+ billpkgnum => $cust_bill_pkg->billpkgnum,
_is_setup => 1,
description => $description,
pkgpart => $pkgpart,
ext_description => \@d,
svc_label => ($svc_label || ''),
locationnum => $cust_pkg->locationnum, # sure, why not?
+ pkg_tax => \@pkg_tax,
};
};
$description = $self->mt('Usage charges');
}
- my $part_pkg = $cust_pkg->part_pkg;
+ my $disable_date_ranges =
+ $opt{disable_line_item_date_ranges}
+ || $part_pkg->option('disable_line_item_date_ranges', 1);
- $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
- $self->agentnum );
+ $description .= $cust_bill_pkg->time_period_pretty(
+ $part_pkg,
+ $agentnum,
+ disable_date_ranges => $disable_date_ranges,
+ );
my @d = ();
my @seconds = (); # for display of usage info
push @{ $r->{ext_description} }, @d;
} else {
$r = {
+ billpkgnum => $cust_bill_pkg->billpkgnum,
description => $description,
pkgpart => $pkgpart,
pkgnum => $cust_bill_pkg->pkgnum,
ext_description => \@d,
svc_label => ($svc_label || ''),
locationnum => $cust_pkg->locationnum,
+ pkg_tax => \@pkg_tax,
};
$r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
}
} elsif ( $amount ) {
# create a new usage line
$u = {
+ billpkgnum => $cust_bill_pkg->billpkgnum,
description => $description,
pkgpart => $pkgpart,
pkgnum => $cust_bill_pkg->pkgnum,
%item_dates,
ext_description => \@d,
locationnum => $cust_pkg->locationnum,
+ pkg_tax => \@pkg_tax,
};
} # else this has no usage, so don't create a usage section
}
}
+=item has_sections AGENTNUM
+
+Return true if invoice_sections should be enabled for this bill.
+ (Inherited by both cust_bill and cust_bill_void)
+
+Determination:
+* False if not an invoice
+* True always if conf invoice_sections is enabled
+* True always if sections_by_location is enabled
+* True if conf invoice_sections_multilocation > 1,
+ and location_count >= invoice_sections_multilocation
+* Else, False
+
+=cut
+
+sub has_sections {
+ my ($self, $agentnum) = @_;
+
+ return 0 unless $self->invnum > 0;
+
+ $agentnum ||= $self->agentnum;
+ return 1 if $self->conf->config_bool('invoice_sections', $agentnum);
+ return 1 if $self->conf->exists('sections_by_location', $agentnum);
+
+ my $location_min = $self->conf->config(
+ 'invoice_sections_multilocation', $agentnum,
+ );
+
+ return 1
+ if $location_min
+ && $self->location_count >= $location_min;
+
+ 0;
+}
+
+
+=item location_count
+
+Return the number of locations billed on this invoice
+
+=cut
+
+sub location_count {
+ my ($self) = @_;
+ return 0 unless $self->invnum;
+
+ # SELECT COUNT( DISTINCT cust_pkg.locationnum )
+ # FROM cust_bill_pkg
+ # LEFT JOIN cust_pkg USING (pkgnum)
+ # WHERE invnum = 278
+ # AND cust_bill_pkg.pkgnum > 0
+
+ my $result = qsearchs({
+ select => 'COUNT(DISTINCT cust_pkg.locationnum) as location_count',
+ table => 'cust_bill_pkg',
+ addl_from => 'LEFT JOIN cust_pkg USING (pkgnum)',
+ extra_sql => 'WHERE invnum = '.dbh->quote( $self->invnum )
+ . ' AND cust_bill_pkg.pkgnum > 0'
+ });
+ ref $result ? $result->location_count : 0;
+}
+
+
+
1;
use FS::Record qw(qsearchs);
use FS::queue;
use FS::CGI qw(rooturl);
+use FS::Report::Queued::FutureAutobill;
$DEBUG = 0;
use vars qw(
@EXPORT_OK $DEBUG $me $cgi $freeside_uid $conf_dir $cache_dir
$secrets $datasrc $db_user $db_pass $schema $dbh $driver_name
- $AutoCommit %callback @callback $callback_hack
+ $AutoCommit $ForceObeyAutoCommit %callback @callback $callback_hack
);
use subs qw( getsecrets );
use Carp qw( carp croak cluck confess );
$conf_dir = "%%%FREESIDE_CONF%%%";
$cache_dir = "%%%FREESIDE_CACHE%%%";
+# Code wanting to issue a COMMIT statement to the database is expected to
+# obey the convention of checking this flag first. Setting $AutoCommit = 0
+# should (usually) suppress COMMIT statements.
$AutoCommit = 1; #ours, not DBI
+
+# Not all methods obey $AutoCommit, by design choice. Setting
+# $ForceObeyAutoCommit = 1 will override that design choice for:
+# &FS::cust_main::Billing::collect
+# &FS::cust_main::Billing::do_cust_event
+$ForceObeyAutoCommit = 0;
+
$callback_hack = 0;
=head1 NAME
# boolean+text previous_balance-exclude_from_total is now two separate options
my $total_new_charges = $conf->config('previous_balance-exclude_from_total');
- if (length($total_new_charges) > 0) {
+ if ( defined $total_new_charges && length($total_new_charges) > 0 ) {
$conf->set('previous_balance-text-total_new_charges', $total_new_charges);
$conf->set('previous_balance-exclude_from_total', '');
}
$conf->delete('unsuspendauto');
}
- if ($conf->config('cust-fields') =~ / \| Payment Type/) {
- my $cust_fields = $conf->config('cust-fields');
+ my $cust_fields = $conf->config('cust-fields');
+ if ( defined $cust_fields && $cust_fields =~ / \| Payment Type/ ) {
# so we can potentially use 'Payment Types' or somesuch in the future
$cust_fields =~ s/ \| Payment Type( \|)/$1/;
$cust_fields =~ s/ \| Payment Type$//;
$lh->maketext($_) if length($_);
}
}
+
+ unless ( FS::upgrade_journal->is_done('deprecate_unmask_ss') ) {
+ if ( $conf->config_bool( 'unmask_ss' )) {
+ warn "'unmask_ssn' deprecated from global configuration\n";
+ for my $access_group ( qsearch( access_group => {} )) {
+ $access_group->grant_access_right( 'Unmask customer SSN' );
+ warn " - 'Unmask customer SSN' access right granted to '" .
+ $access_group->groupname . "' employee group\n";
+ }
+ }
+ FS::upgrade_journal->set_done('deprecate_unmask_ss');
+ }
+
}
sub upgrade_overlimit_groups {
});
foreach my $object ( @objects ) {
my $payinfo = $object->decrypt($object->payinfo);
- die "error decrypting payinfo" if $payinfo eq $object->payinfo;
+ if ( $payinfo eq $object->payinfo ) {
+ warn "error decrypting payinfo for $table: $payinfo\n";
+ next;
+ }
$object->payinfo($payinfo);
my $error = $object->replace;
die $error if $error;
#'compliance solutions' -> 'compliance_solutions'
'tax_rate' => [],
'tax_rate_location' => [],
+
+ #upgrade part_event_condition_option agentnum to a multiple hash value
+ 'part_event_condition_option' =>[],
+
+ #fix ip format
+ 'svc_circuit' => [],
+
+ #fix ip format
+ 'svc_hardware' => [],
+
+ #fix ip format
+ 'svc_pbx' => [],
+
+ #fix ip format
+ 'tower_sector' => [],
+
+
;
\%hash;
=cut
1;
-
use base qw( FS::m2m_Common FS::m2name_Common FS::Record );
use strict;
+use Carp qw( croak );
use FS::Record qw( qsearch qsearchs );
use FS::access_right;
);
}
+=item grant_access_right RIGHTNAME
+
+Grant the specified specified FS::access_right record to this group.
+Return the FS::access_right record.
+
+=cut
+
+sub grant_access_right {
+ my ( $self, $rightname ) = @_;
+
+ croak "grant_access_right() requires \$rightname"
+ unless $rightname;
+
+ my $access_right = $self->access_right( $rightname );
+ return $access_right if $access_right;
+
+ $access_right = FS::access_right->new({
+ righttype => 'FS::access_group',
+ rightobjnum => $self->groupnum,
+ rightname => $rightname,
+ });
+ if ( my $error = $access_right->insert ) {
+ die "grant_access_right() error: $error";
+ }
+
+ $access_right;
+}
+
+=item revoke_access_right RIGHTNAME
+
+Revoke the specified FS::access_right record from this group.
+
+=cut
+
+sub revoke_access_right {
+ my ( $self, $rightname ) = @_;
+
+ croak "revoke_access_right() requires \$rightname"
+ unless $rightname;
+
+ my $access_right = $self->access_right( $rightname )
+ or return;
+
+ if ( my $error = $access_right->delete ) {
+ die "revoke_access_right() error: $error";
+ }
+}
+
=back
=head1 BUGS
=cut
1;
-
use FS::agent;
use FS::cust_main;
use FS::sales;
+use Carp qw( croak );
$DEBUG = 0;
$me = '[FS::access_user]';
return $error;
}
+=item get_pref NAME
+
+Fetch the prefvalue column from L<FS::access_user_pref> for prefname NAME
+
+Returns undef when no value has been saved, or when record has expired
+
+=cut
+
+sub get_pref {
+ my ( $self, $prefname ) = @_;
+ croak 'prefname parameter requrired' unless $prefname;
+
+ my $pref_row = $self->get_pref_row( $prefname )
+ or return undef;
+
+ return undef
+ if $pref_row->expiration
+ && $pref_row->expiration < time();
+
+ $pref_row->prefvalue;
+}
+
+=item get_pref_row NAME
+
+Fetch the row object from L<FS::access_user_pref> for prefname NAME
+
+returns undef when no row has been created
+
+=cut
+
+sub get_pref_row {
+ my ( $self, $prefname ) = @_;
+ croak 'prefname parameter required' unless $prefname;
+
+ qsearchs(
+ access_user_pref => {
+ usernum => $self->usernum,
+ prefname => $prefname,
+ }
+ );
+}
+
+=item set_pref NAME, VALUE, [EXPIRATION_EPOCH]
+
+Add or update user preference in L<FS::access_user_pref> table
+
+Passing an undefined VALUE will delete the user preference
+
+Returns VALUE
+
+=cut
+
+sub set_pref {
+ my $self = shift;
+ my ( $prefname, $prefvalue, $expiration ) = @_;
+
+ return $self->delete_pref( $prefname )
+ unless defined $prefvalue;
+
+ if ( my $pref_row = $self->get_pref_row( $prefname )) {
+ return $prefvalue
+ if $pref_row->prefvalue eq $prefvalue;
+
+ $pref_row->prefvalue( $prefvalue );
+ $pref_row->expiration( $expiration || '');
+
+ if ( my $error = $pref_row->replace ) { croak $error }
+
+ return $prefvalue;
+ }
+
+ my $pref_row = FS::access_user_pref->new({
+ usernum => $self->usernum,
+ prefname => $prefname,
+ prefvalue => $prefvalue,
+ expiration => $expiration,
+ });
+ if ( my $error = $pref_row->insert ) { croak $error }
+
+ $prefvalue;
+}
+
+=item delete_pref NAME
+
+Delete user preference from L<FS::access_user_pref> table
+
+=cut
+
+sub delete_pref {
+ my ( $self, $prefname ) = @_;
+
+ my $pref_row = $self->get_pref_row( $prefname )
+ or return;
+
+ if ( my $error = $pref_row->delete ) { croak $error }
+}
+
=back
=head1 BUGS
=back
+=item pid
+
+=back
+
=head1 METHODS
=over 4
'path' => $path,
'_date' => time,
'render_seconds' => $render_seconds,
+ 'pid' => $$,
} );
#so we can still log pages after a transaction-aborting SQL error (and then
|| $self->ut_text('path')
|| $self->ut_number('_date')
|| $self->ut_numbern('render_seconds')
+ || $self->ut_numbern('pid')
;
return $error if $error;
$self->NetAddr->cidr;
}
+=item free_addrs
+
+Returns an aref sorted list of free addresses in the block.
+
+=cut
+
+sub free_addrs {
+ my $self = shift;
+
+ my %used_addr_map =
+ map {$_ => 1}
+ FS::IP_Mixin->used_addresses($self),
+ FS::Conf->new()->config('exclude_ip_addr');
+
+ [
+ grep { !exists $used_addr_map{$_} }
+ map { $_->addr }
+ $self->NetAddr->hostenum
+ ];
+}
+
=item next_free_addr
Returns a NetAddr::IP object corresponding to the first unassigned address
=cut
1;
-
}
}
- my $override = qsearchs('agent_payment_gateway', { agentnum => $self->agentnum } );
+ my $cardtype_search = "AND ( cardtype IS NULL OR cardtype <> 'ACH')";
+ $cardtype_search = "AND ( cardtype IS NULL OR cardtype = 'ACH' )" if $options{method} eq 'ECHECK';
+
+ my $override =
+ qsearchs({
+ "table" => 'agent_payment_gateway',
+ "hashref" => { agentnum => $self->agentnum, },
+ "extra_sql" => $cardtype_search,
+ });
my $payment_gateway = FS::payment_gateway->by_key_or_default(
gatewaynum => $override ? $override->gatewaynum : '',
my $dbd_type = $args{'dbd'} ? $args{'dbd'} : 'Pg';
my $status_column = $args{status_column} ? $args{status_column} : 'freesidestatus';
my $status_column_info = $args{status_column_info} ? $args{status_column} : 'VARCHAR(32)';
+ my $st_sql;
+ my $batch_name = $args{batch_name} ? $args{batch_name} : 'CDR_DB';
my $queries = get_queries({
'dbd' => $dbd_type,
$dbi->do( $queries->{create_statustable} )
or die $dbi->errstr;
}
+ $st_sql = "INSERT INTO $status_table ( $pkey, $status_column ) VALUES ( ?, 'done' )";
}
## check for column freeside status if not using status table and create it if not there.
else {
$dbi->do( $queries->{create_statuscolumn} )
or die $dbi->errstr;
}
+ $st_sql = "UPDATE $table SET $status_column = 'done' WHERE $pkey = ?";
}
#my @cols = values %{ $args{column_map} };
$sth->execute or die $sth->errstr. " executing $sql";
my $cdr_batch = new FS::cdr_batch({
- 'cdrbatch' => $args{batch_name} . '-import-'. time2str('%Y/%m/%d-%T',time),
+ 'cdrbatch' => $batch_name . '-import-'. time2str('%Y/%m/%d-%T',time),
});
my $error = $cdr_batch->insert;
die $error if $error;
$imported++;
- my $st_sql;
- if ( $status_table ) {
-
- $st_sql =
- 'INSERT INTO '. $status_table. " ( $pkey, $status_column ) ".
- " VALUES ( ?, 'done' )";
-
- } else {
-
- $st_sql = "UPDATE $table SET $status_column = 'done' WHERE $pkey = ?";
-
- }
-
my $updated = $dbi->do($st_sql, undef, $pkey_value );
#$updates += $updated;
die "failed to set status: ".$dbi->errstr."\n" unless $updated;
$port ||= '5000'; # check for pg default 5000 is sybase.
my %dbi_connect_types = (
- 'Sybase' => ':host='.$host.';port='.$port,
+ 'Sybase' => ':server='.$host.';port='.$port,
'Pg' => ':host='.$info->{host},
);
terminating_ocn:4:208:211
)],
'import_fields' => [
-
- sub { #call_date and time
+ sub { #call_date and time
my($cdr, $data, $conf, $param) = @_;
$data =~ /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/ or die "unparsable record_date: $data";
$cdr->set('calldate', "$2/$3/$1 $4:$5:$6");
+ $cdr->set('startdate', "$2/$3/$1 $4:$5:$6");
},
-
- 'charged_party', #bill to number
- '', #translate number
-
- 'src', #originating number
-
- '', #originating lata
- '', #originating city
- '', #originating state
- '', #originating country
-
- 'dst', #terminating number
-
- '', #terminating lata
- '', #terminating city
- '', #terminating state
- '', #terminating city code
- '', #terminating country
-
- '', #call type
- '', #call transport
- 'accountcode', #account code
- '', #info digits
- 'duration', #duration
- '', #wholesale amount
- '', #cic
- 'src_lrn', #originating lrn
- 'dst_lrn', #terminating lrn
- '', #originating ocn
- '', #terminating ocn
+ 'charged_party', #bill to number
+ '', #translate number
+ 'src', #originating number
+ '', #originating lata
+ '', #originating city
+ '', #originating state
+ '', #originating country
+ 'dst', #terminating number
+ '', #terminating lata
+ '', #terminating city
+ '', #terminating state
+ '', #terminating city code
+ '', #terminating country
+ '', #call type
+ '', #call transport
+ 'accountcode', #account code
+ '', #info digits
+ sub { #duration
+ my($cdr, $field) = @_;
+ $cdr->set(duration => $field);
+ $cdr->set(billsec => $field);
+ },
+ '', #wholesale amount
+ '', #cic
+ 'src_lrn', #originating lrn
+ 'dst_lrn', #terminating lrn
+ '', #originating ocn
+ '', #terminating ocn
],
my($cdr, $cdrtypename, $conf, $param) = @_;
return unless length($cdrtypename);
_init_cdr_types();
- die "no matching cdrtypenum for $cdrtypename"
- unless defined $CDR_TYPES->{$cdrtypename};
+ unless (defined $CDR_TYPES->{$cdrtypename}) {
+ warn "Skipping Record: CDR type name $cdrtypename does not exist!";
+ $param->{skiprow} = 1;
+ }
$cdr->cdrtypenum($CDR_TYPES->{$cdrtypename});
}, # type
_cdr_min_parser_maker('billsec'), #PriceDurationMins
}
- $error ||= $self->insert_password_history;
-
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
}
}
+ if ( $self->get('password') ) {
+ my $error = $self->is_password_allowed($self->get('password'))
+ || $self->change_password($self->get('password'));
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
$hash eq $check_hash;
- } else {
+ } else {
return 0 if $self->_password eq '';
use FS::reason;
use FS::reason_type;
use FS::L10N;
+use FS::Misc::Savepoint;
$DEBUG = 0;
$me = '[FS::cust_bill]';
sub table { 'cust_bill'; }
sub template_conf { 'invoice_'; }
-sub has_sections {
- my $self = shift;
- my $agentnum = $self->cust_main->agentnum;
- my $tc = $self->template_conf;
-
- $self->conf->exists($tc.'sections', $agentnum) ||
- $self->conf->exists($tc.'sections_by_location', $agentnum);
-}
-
# should be the ONLY occurrence of "Invoice" in invoice rendering code.
# (except email_subject and invnum_date_pretty)
sub notice_name {
sub cust_bill_pkg {
my $self = shift;
qsearch(
- { 'table' => 'cust_bill_pkg',
+ {
+ 'select' => 'cust_bill_pkg.*, pkg_category.categoryname',
+ 'table' => 'cust_bill_pkg',
+ 'addl_from' => ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN part_pkg USING ( pkgpart ) '.
+ ' LEFT JOIN pkg_class USING ( classnum ) '.
+ ' LEFT JOIN pkg_category USING ( categorynum ) ',
'hashref' => { 'invnum' => $self->invnum },
'order_by' => 'ORDER BY billpkgnum', #important? otherwise we could use
# the AUTLOADED FK search. or should
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ my $savepoint_label = 'cust_bill__apply_payments_and_credits';
+ savepoint_create( $savepoint_label );
+
$self->select_for_update; #mutex
my @payments = grep { $_->unapplied > 0 }
my $error = $app->insert(%options);
if ( $error ) {
+ savepoint_rollback_and_release( $savepoint_label );
$dbh->rollback if $oldAutoCommit;
return "Error inserting ". $app->table. " record: $error";
}
}
+ savepoint_release( $savepoint_label );
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
''; #no error
sub send_csv {
my($self, %opt) = @_;
+ if ( $FS::Misc::DISABLE_ALL_NOTICES ) {
+ warn 'send_csv() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
+ return;
+ }
+
#create file(s)
my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
sub spool_csv {
my($self, %opt) = @_;
+ if ( $FS::Misc::DISABLE_ALL_NOTICES ) {
+ warn 'spool_csv() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
+ return;
+ }
+
my $time = $opt{'time'} || time;
my $cust_main = $self->cust_main;
}
-=sub _items_usage_class_summary OPTIONS
+=item _items_usage_class_summary OPTIONS
Returns a list of detail items summarizing the usage charges on this
invoice. Each one will have 'amount', 'description' (the usage charge name),
return @l;
}
-=sub _items_previous()
+=item _items_previous()
Returns an array of hashrefs, each hashref representing a line-item on
the current bill for previous unpaid invoices.
}
-=sub _items_previous_total
+=item _items_previous_total
Return sum of amounts from all items returned by _items_previous
Results will vary based on invoicing conf flags
}
}
-=sub _items_credits()
+=item _items_credits()
Return array of hashrefs containing credits to be shown as line-items
when rendering this bill.
@return;
}
-=sub _items_credits_total
+=item _items_credits_total
Return the total of al items from _items_credits
Will vary based on invoice display conf flag
-=sub _items_credits_postbill()
+=item _items_credits_postbill()
Returns an array of hashrefs for credits where
- Credit issued after this invoice
}} @cust_credit_bill;
}
-=sub _items_payments_postbill()
+=item _items_payments_postbill()
Returns an array of hashrefs for payments where
- Payment occured after this invoice
}} @cust_bill_pay;
}
-=sub _items_payments()
+=item _items_payments()
Return array of hashrefs containing payments to be shown as line-items
when rendering this bill.
if ($self->conf->exists('previous_balance-payments_since')) {
if ($template eq 'statement') {
-print "\nCASE 3\n";
# Case 3 (see above)
# Return payments timestamped between the previous and following bills
} else {
# Case 2 (see above)
# Return payments timestamped between this and the previous bill
-print "\nCASE 2\n";
+
my $date_start = 0;
my $date_end = $self->_date;
return @{ $self->get('_items_payments') };
}
-=sub _items_payments_total
+=item _items_payments_total
Return a total of all records returned by _items_payments
Results vary based on invoicing conf flags
return @return;
}
-=sub _items_total()
+=item _items_total()
Generate the line-items to be shown on the bill in the "Totals" section
return $error;
}
+ #more efficiently than below, because there could be lots
+ $self->void_cust_bill_pkg_detail($reprocess_cdrs);
+
foreach my $table (qw(
- cust_bill_pkg_detail
cust_bill_pkg_display
cust_bill_pkg_discount
cust_bill_pkg_tax_location
cust_tax_exempt_pkg
cust_bill_pkg_fee
)) {
- my %delete_args = ();
- $delete_args{'reprocess_cdrs'} = $reprocess_cdrs
- if $table eq 'cust_bill_pkg_detail';
-
foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
my $vclass = 'FS::'.$table.'_void';
my $void = $vclass->new( {
map { $_ => $linked->get($_) } $linked->fields
});
- my $error = $void->insert || $linked->delete(%delete_args);
+ my $error = $void->insert || $linked->delete;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
}
+sub void_cust_bill_pkg_detail {
+ my( $self, $reprocess_cdrs ) = @_;
+
+ my $from_cust_bill_pkg_detail =
+ 'FROM cust_bill_pkg_detail WHERE billpkgnum = ?';
+ my $where_detailnum =
+ "WHERE detailnum IN ( SELECT detailnum $from_cust_bill_pkg_detail )";
+
+ if ( $reprocess_cdrs ) {
+ #well, technically this could have been on other invoices / termination
+ # partners... separate flag?
+ $self->scalar_sql(
+ "DELETE FROM cdr_termination
+ WHERE acctid IN ( SELECT acctid FROM cdr $where_detailnum )
+ ",
+ $self->billpkgnum
+ );
+ }
+
+ my $setstatus = $reprocess_cdrs ? ', freesidestatus = NULL' : '';
+ $self->scalar_sql(
+ "UPDATE cdr SET detailnum = NULL $setstatus $where_detailnum",
+ $self->billpkgnum
+ );
+
+ $self->scalar_sql("INSERT INTO cust_bill_pkg_detail_void
+ SELECT * $from_cust_bill_pkg_detail",
+ $self->billpkgnum
+ );
+
+ $self->scalar_sql("DELETE $from_cust_bill_pkg_detail", $self->billpkgnum);
+
+}
+
=item delete
Not recommended.
=cut
sub cust_main {
+ carp "->cust_main called" if $DEBUG;
# required for cust_main_Mixin equivalence
# and use cust_bill instead of cust_pkg because this might not have a
# cust_pkg
'';
}
+sub _pkg_tax_list {
+ # Return an array of hashrefs for each cust_bill_pkg_tax_location
+ # applied to this bill for this cust_bill_pkg.pkgnum.
+ #
+ # ! Important Note:
+ # In some situations, this list will contain more tax records than the
+ # ones directly related to $self->billpkgnum. The returned list contains
+ # all records, for this bill, charged against this billpkgnum's pkgnum.
+ #
+ # One must keep this in mind when using data returned by this method.
+ #
+ # An unaddressed deficiency in the cust_bill_pkg_tax_location model makes
+ # this necessary: When a linked-hidden package generates a tax/fee as a row
+ # in cust_bill_pkg_tax_location, there is not enough information to surmise
+ # with specificity which billpkgnum row represents the direct parent of the
+ # the linked-hidden package's tax row. The closest we can get to this
+ # backwards reassociation is to use the pkgnum. Therefore, when multiple
+ # billpkgnum's appear with the same pkgnum, this method is going to return
+ # the tax records for ALL of those billpkgnum's, not just $self->billpkgnum.
+ #
+ # This could be addressed with an update to the model, and to the billing
+ # routine that generates rows into cust_bill_pkg_tax_location. Perhaps a
+ # column, link_billpkgnum or parent_billpkgnum, recording the link. I'm not
+ # doing that now, because there would be no possible repair of data stored
+ # historically prior to such a fix. I need _pkg_tax_list() to not be
+ # broken for already-generated bills.
+ #
+ # Any code you write relying on _pkg_tax_list() MUST be aware of, and
+ # account for, the possible return of duplicated tax records returned
+ # when method is called on multiple cust_bill_pkg_tax_location rows.
+ # Duplicates can be identified by billpkgtaxlocationnum column.
+
+ my $self = shift;
+
+ my $search_selector;
+ if ( $self->pkgnum ) {
+
+ # For taxes applied to normal billing items
+ $search_selector =
+ ' cust_bill_pkg_tax_location.pkgnum = '
+ . dbh->quote( $self->pkgnum );
+
+ } elsif ( $self->feepart ) {
+
+ # For taxes applied to fees, when the fee is not attached to a package
+ # i.e. late fees, billing events fees
+ $search_selector =
+ ' cust_bill_pkg_tax_location.taxable_billpkgnum = '
+ . dbh->quote( $self->billpkgnum );
+
+ } else {
+ warn "_pkg_tax_list() unhandled case breaking taxes into sections";
+ warn "_pkg_tax_list() $_: ".$self->$_
+ for qw(pkgnum billpkgnum feepart);
+ return;
+ }
+
+ map +{
+ billpkgtaxlocationnum => $_->billpkgtaxlocationnum,
+ billpkgnum => $_->billpkgnum,
+ taxnum => $_->taxnum,
+ amount => $_->amount,
+ taxname => $_->taxname,
+ },
+ qsearch({
+ table => 'cust_bill_pkg_tax_location',
+ addl_from => '
+ LEFT JOIN cust_bill_pkg
+ ON cust_bill_pkg.billpkgnum
+ = cust_bill_pkg_tax_location.taxable_billpkgnum
+ ',
+ select => join( ', ', (qw|
+ cust_bill_pkg.billpkgnum
+ cust_bill_pkg_tax_location.billpkgtaxlocationnum
+ cust_bill_pkg_tax_location.taxnum
+ cust_bill_pkg_tax_location.amount
+ |)),
+ extra_sql =>
+ ' WHERE '.
+ ' cust_bill_pkg.invnum = ' . dbh->quote( $self->invnum ) .
+ ' AND '.
+ $search_selector
+ });
+
+}
+
sub _upgrade_data {
# Create a queue job to run upgrade_tax_location from January 1, 2012 to
# the present date.
=cut
1;
-
sub notice_name { 'VOIDED Invoice'; }
sub template_conf { 'invoice_'; }
-sub has_sections {
- my $self = shift;
- my $agentnum = $self->cust_main->agentnum;
- my $tc = $self->template_conf;
-
- $self->conf->exists($tc.'sections', $agentnum) ||
- $self->conf->exists($tc.'sections_by_location', $agentnum);
-}
-
=item insert
=cut
1;
-
"
JOIN part_event USING ( eventpart )
+
LEFT JOIN cust_bill ON ( eventtable = 'cust_bill' AND tablenum = invnum )
LEFT JOIN cust_pkg ON ( eventtable = 'cust_pkg' AND tablenum = pkgnum )
LEFT JOIN cust_pay ON ( eventtable = 'cust_pay' AND tablenum = paynum )
+ LEFT JOIN cust_pay_batch ON ( eventtable = 'cust_pay_batch' AND tablenum = paybatchnum )
+ LEFT JOIN cust_statement ON ( eventtable = 'cust_statement' AND tablenum = cust_statement.statementnum )
+
LEFT JOIN cust_svc ON ( eventtable = 'svc_acct' AND tablenum = svcnum )
LEFT JOIN cust_pkg AS cust_pkg_for_svc ON ( cust_svc.pkgnum = cust_pkg_for_svc.pkgnum )
+
LEFT JOIN cust_main ON (
( eventtable = 'cust_main' AND tablenum = cust_main.custnum )
OR ( eventtable = 'cust_bill' AND cust_bill.custnum = cust_main.custnum )
use FS::cust_payby;
use FS::contact;
use FS::reason;
+use FS::Misc::Savepoint;
# 1 is mostly method/subroutine entry and options
# 2 traces progress of some operations
my( $self, %opt ) = @_;
# we're going to cancel services, which is not reversible
+ # unless exports are suppressed
die "cancel_pkgs cannot be run inside a transaction"
- if $FS::UID::AutoCommit == 0;
+ if !$FS::UID::AutoCommit && !$FS::svc_Common::noexport_hack;
+ my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
+ savepoint_create('cancel_pkgs');
+
return ( 'access denied' )
unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
my $ban = new FS::banned_pay $cust_payby->_new_banned_pay_hashref;
my $error = $ban->insert;
if ($error) {
- dbh->rollback;
+ savepoint_rollback_and_release('cancel_pkgs');
+ dbh->rollback if $oldAutoCommit;
return ( $error );
}
'time' => $cancel_time );
if ($error) {
warn "Error billing during cancel, custnum ". $self->custnum. ": $error";
- dbh->rollback;
+ savepoint_rollback_and_release('cancel_pkgs');
+ dbh->rollback if $oldAutoCommit;
return ( "Error billing during cancellation: $error" );
}
}
- dbh->commit;
+ savepoint_release('cancel_pkgs');
+ dbh->commit if $oldAutoCommit;
my @errors;
# try to cancel each service, the same way we would for individual packages,
warn "$me removing ".scalar(@sorted_cust_svc)." service(s) for customer ".
$self->custnum."\n"
if $DEBUG;
+ my $i = 0;
foreach my $cust_svc (@sorted_cust_svc) {
+ my $savepoint = 'cancel_pkgs_'.$i++;
+ savepoint_create( $savepoint );
my $part_svc = $cust_svc->part_svc;
next if ( defined($part_svc) and $part_svc->preserve );
# immediate cancel, no date option
# transactionize individually
my $error = try { $cust_svc->cancel } catch { $_ };
if ( $error ) {
- dbh->rollback;
+ savepoint_rollback_and_release( $savepoint );
+ dbh->rollback if $oldAutoCommit;
push @errors, $error;
} else {
- dbh->commit;
+ savepoint_release( $savepoint );
+ dbh->commit if $oldAutoCommit;
}
}
if (@errors) {
@cprs = @{ delete $opt{'cust_pkg_reason'} };
}
my $null_reason;
+ $i = 0;
foreach (@pkgs) {
my %lopt = %opt;
+ my $savepoint = 'cancel_pkgs_'.$i++;
+ savepoint_create( $savepoint );
if (@cprs) {
my $cpr = shift @cprs;
if ( $cpr ) {
}
my $error = $_->cancel(%lopt);
if ( $error ) {
- dbh->rollback;
+ savepoint_rollback_and_release( $savepoint );
+ dbh->rollback if $oldAutoCommit;
push @errors, 'pkgnum '.$_->pkgnum.': '.$error;
} else {
- dbh->commit;
+ savepoint_release( $savepoint );
+ dbh->commit if $oldAutoCommit;
}
}
$name;
}
+=item batch_payment_payname
+
+Returns a name string for this customer, either "cust_batch_payment->payname" or "First Last" or "Company,
+based on if a company name exists and is the account being used a business account.
+
+=cut
+
+sub batch_payment_payname {
+ my $self = shift;
+ my $cust_pay_batch = shift;
+ my $name;
+
+ if ($cust_pay_batch->{Hash}->{payby} eq "CARD") { $name = $cust_pay_batch->payname; }
+ else { $name = $self->first .' '. $self->last; }
+
+ $name = $self->company
+ if (($cust_pay_batch->{Hash}->{paytype} eq "Business checking" || $cust_pay_batch->{Hash}->{paytype} eq "Business savings") && $self->company);
+
+ $name;
+}
+
=item service_contact
Returns the L<FS::contact> object for this customer that has the 'Service'
$cust_main->bill_and_collect( %$param );
}
+=item pending_invoice_count
+
+Return number of cust_bill with pending=Y for this customer
+
+=cut
+
+sub pending_invoice_count {
+ FS::cust_bill->count( 'custnum = '.shift->custnum."AND pending = 'Y'" );
+}
+
#starting to take quite a while for big dbs
# (JRNL: journaled so it only happens once per database)
# - seq scan of h_cust_main (yuck), but not going to index paycvv, so
package FS::cust_main::Billing;
use strict;
+use feature 'state';
use vars qw( $conf $DEBUG $me );
use Carp;
use Data::Dumper;
use FS::FeeOrigin_Mixin;
use FS::Log;
use FS::TaxEngine;
+use FS::Misc::Savepoint;
# 1 is mostly method/subroutine entry and options
# 2 traces progress of some operations
# In a batch tax environment, do not run collection if any pending
# invoices were created. Collection will run after the next tax batch.
- my $tax = FS::TaxEngine->new;
- if ( $tax->info->{batch} and
- qsearch('cust_bill', { custnum => $self->custnum, pending => 'Y' })
- )
- {
+ state $is_batch_tax = FS::TaxEngine->new->info->{batch} ? 1 : 0;
+ if ( $is_batch_tax && $self->pending_invoice_count ) {
warn "skipped collection for custnum ".$self->custnum.
" due to pending invoices\n" if $DEBUG;
} elsif ( $conf->exists('cancelled_cust-noevents')
}
}
+ $lineitems++
+ if $cust_pkg->waive_setup && $part_pkg->can('prorate_setup') && $part_pkg->prorate_setup($cust_pkg, $time);
+
if ( $cust_pkg->get('setup') ) {
# don't change it
} elsif ( $cust_pkg->get('start_date') ) {
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
#never want to roll back an event just because it returned an error
- local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
+ # unless $FS::UID::ForceObeyAutoCommit is set
+ local $FS::UID::AutoCommit = 1
+ unless !$oldAutoCommit
+ && $FS::UID::ForceObeyAutoCommit;
$self->do_cust_event(
'debug' => ( $options{'debug'} || 0 ),
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
#never want to roll back an event just because it or a different one
# returned an error
- local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
+ # unless $FS::UID::ForceObeyAutoCommit is set
+ local $FS::UID::AutoCommit = 1
+ unless !$oldAutoCommit
+ && $FS::UID::ForceObeyAutoCommit;
foreach my $cust_event ( @$due_cust_event ) {
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ my $savepoint_label = 'Billing__apply_payments_and_credits';
+ savepoint_create( $savepoint_label );
+
$self->select_for_update; #mutex
foreach my $cust_bill ( $self->open_cust_bill ) {
my $error = $cust_bill->apply_payments_and_credits(%options);
if ( $error ) {
+ savepoint_rollback_and_release( $savepoint_label );
$dbh->rollback if $oldAutoCommit;
return "Error applying: $error";
}
}
+ savepoint_release( $savepoint_label );
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
''; #no error
return;
}
- my $invnum = delete $options{invnum};
+ #my $invnum = delete $options{invnum};
+ my $invnum = $options{invnum};
#pay fields should all come from either cust_payby or options, not both
# in theory, could just pass payby, and use it to select cust_payby,
} );
foreach (qw( address1 address2 city state zip country latitude longitude
- payby payinfo paydate payname paycode ))
+ payby payinfo paydate payname paycode paytype ))
{
$options{$_} = '' unless exists($options{$_});
}
'country' => $options{country} || $loc->country,
'payby' => $options{payby} || $cust_payby->payby,
'payinfo' => $options{payinfo} || $cust_payby->payinfo,
+ 'paymask' => ( $options{payinfo}
+ ? FS::payinfo_Mixin->mask_payinfo( $options{payby},
+ $options{payinfo} )
+ : $cust_payby->paymask
+ ),
'exp' => $options{paydate} || $cust_payby->paydate,
'payname' => $options{payname} || $cust_payby->payname,
'paytype' => $options{paytype} || $cust_payby->paytype,
'amount' => $amount, # consolidating
- 'paycode' => $options{paycode} || $cust_payby->paycode,
+ 'paycode' => $options{paycode} || '',
} );
$cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
use FS::cust_refund;
use FS::banned_pay;
use FS::payment_gateway;
+use FS::Misc::Savepoint;
$realtime_bop_decline_quiet = 0;
our $BOP_TESTING = 0;
our $BOP_TESTING_SUCCESS = 1;
+our $BOP_TESTING_TIMESTAMP = '';
install_callback FS::UID sub {
$conf = new FS::Conf;
confess "Can't call realtime_bop within another transaction ".
'($FS::UID::AutoCommit is false)'
- unless $FS::UID::AutoCommit;
+ unless $FS::UID::AutoCommit || $BOP_TESTING;
local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
$options{amount} = $amount;
}
+ return '' unless $options{amount} > 0;
+
# set fields from passed cust_payby
_bop_cust_payby_options(\%options);
if $conf->config('credit-card-surcharge-percentage', $self->agentnum)
&& $options{method} eq 'CC';
+ my $cc_surcharge_flat = 0;
+ $cc_surcharge_flat = $conf->config('credit-card-surcharge-flatfee', $self->agentnum)
+ if $conf->config('credit-card-surcharge-flatfee', $self->agentnum)
+ && $options{method} eq 'CC';
+
# always add cc surcharge if called from event
- if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
- $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
+ if($options{'cc_surcharge_from_event'} && ($cc_surcharge_pct > 0 || $cc_surcharge_flat > 0)) {
+ if ($options{'amount'} > 0) {
+ $cc_surcharge = ($options{'amount'} * ($cc_surcharge_pct / 100)) + $cc_surcharge_flat;
$options{'amount'} += $cc_surcharge;
$options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
+ }
}
- elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
- # payment screen), so consider the given
- # amount as post-surcharge
- $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
+ elsif($cc_surcharge_pct > 0 || $cc_surcharge_flat > 0) {
+ # we're called not from event (i.e. from a
+ # payment screen), so consider the given
+ # amount as post-surcharge
+ $cc_surcharge = $options{'amount'} - (($options{'amount'} - $cc_surcharge_flat) / ( 1 + $cc_surcharge_pct/100 )) if $options{'amount'} > 0;
}
$cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
my $cust_pay_pending = new FS::cust_pay_pending {
'custnum' => $self->custnum,
'paid' => $options{amount},
- '_date' => '',
+ '_date' => $BOP_TESTING ? $BOP_TESTING_TIMESTAMP : '',
'payby' => $bop_method2payby{$options{method}},
'payinfo' => $options{payinfo},
'paymask' => $options{paymask},
return { reference => $cust_pay_pending->paypendingnum,
map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
- } elsif ( $transaction->is_success() && $action2 ) {
+ } elsif ( !$BOP_TESTING && $transaction->is_success() && $action2 ) {
$cust_pay_pending->status('authorized');
my $cpp_authorized_err = $cust_pay_pending->replace;
'custnum' => $self->custnum,
'invnum' => $options{'invnum'},
'paid' => $cust_pay_pending->paid,
- '_date' => '',
+ '_date' => $BOP_TESTING ? $BOP_TESTING_TIMESTAMP : '',
'payby' => $cust_pay_pending->payby,
'payinfo' => $options{'payinfo'},
'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ my $savepoint_label = '_realtime_bop_result';
+ savepoint_create( $savepoint_label );
+
#start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
if ( $error ) {
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+ savepoint_rollback( $savepoint_label );
+
$cust_pay->invnum(''); #try again with no specific invnum
$cust_pay->paynum('');
my $error2 = $cust_pay->insert( $options{'manual'} ?
if ( $error2 ) {
# gah. but at least we have a record of the state we had to abort in
# from cust_pay_pending now.
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+ savepoint_rollback_and_release( $savepoint_label );
+
my $e = "WARNING: $options{method} captured but payment not recorded -".
" error inserting payment (". $payment_gateway->gateway_module.
"): $error2".
my $jobnum = $cust_pay_pending->jobnum;
if ( $jobnum ) {
my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
-
+
unless ( $placeholder ) {
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+ savepoint_rollback_and_release( $savepoint_label );
+
my $e = "WARNING: $options{method} captured but job $jobnum not ".
"found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
warn $e;
$error = $placeholder->delete;
if ( $error ) {
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+ savepoint_rollback_and_release( $savepoint_label );
+
my $e = "WARNING: $options{method} captured but could not delete ".
"job $jobnum for paypendingnum ".
$cust_pay_pending->paypendingnum. ": $error\n";
my $cpp_done_err = $cust_pay_pending->replace;
if ( $cpp_done_err ) {
+ savepoint_rollback_and_release( $savepoint_label );
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
my $e = "WARNING: $options{method} captured but payment not recorded - ".
"error updating status for paypendingnum ".
$cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
return $e;
} else {
-
+ savepoint_release( $savepoint_label );
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
if ( $options{'apply'} ) {
}
my $cust_pkg;
+ my $cc_surcharge_text = 'Credit Card Surcharge';
+ $cc_surcharge_text = $conf->config('credit-card-surcharge-text', $self->agentnum) if $conf->exists('credit-card-surcharge-text', $self->agentnum);
my $charge_error = $self->charge({
'amount' => $options{'cc_surcharge'},
- 'pkg' => 'Credit Card Surcharge',
+ 'pkg' => $cc_surcharge_text,
'setuptax' => 'Y',
'cust_pkg_ref' => \$cust_pkg,
});
"resolved - error updating status for paypendingnum ".
$cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
warn $e;
+ #XXX internal system log $e (what's going on?)
$perror = "$e ($perror)";
}
my $payment_gateway =
$self->agent->payment_gateway( 'method' => $options{method} );
- my( $processor, $login, $password, $namespace ) =
+ ( $processor, $login, $password, $namespace ) =
map { my $method = "gateway_$_"; $payment_gateway->$method }
qw( module username password namespace );
#cust_main phone numbers and contact phone number
push @cust_main, qsearch( {
- 'table' => 'cust_main',
- 'hashref' => { %options },
+ 'select' => 'cust_main.*',
+ 'table' => 'cust_main',
+ 'addl_from' => ' left join cust_contact using (custnum) '.
+ ' left join contact_phone using (contactnum) ',
+ 'hashref' => { %options },
'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
' ( '.
join(' OR ', map "$_ = '$phonen'",
" OR phonenum = '$phonenum' ".
' ) '.
" AND $agentnums_sql", #agent virtualization
- 'addl_from' => ' left join cust_contact using (custnum) left join contact_phone using (contactnum) ',
} );
unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match
#try looking for matches with extensions unless one was specified
push @cust_main, qsearch( {
- 'table' => 'cust_main',
- 'hashref' => { %options },
+ 'table' => 'cust_main',
+ 'hashref' => { %options },
'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
' ( '.
join(' OR ', map "$_ LIKE '$phonen\%'",
if ( $search =~ /@/ ) { #email address from cust_main_invoice and contact_email
push @cust_main, qsearch( {
- 'table' => 'cust_main',
- 'hashref' => { %options },
+ 'select' => 'cust_main.*',
+ 'table' => 'cust_main',
+ 'addl_from' => ' left join cust_main_invoice using (custnum) '.
+ ' left join cust_contact using (custnum) '.
+ ' left join contact_email using (contactnum) ',
+ 'hashref' => { %options },
'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
' ( '.
join(' OR ', map "$_ = '$search'",
).
' ) '.
" AND $agentnums_sql", #agent virtualization
- 'addl_from' => ' left join cust_main_invoice using (custnum) left join cust_contact using (custnum) left join contact_email using (contactnum) ',
} );
# custnum search (also try agent_custid), with some tweaking options if your
# probably the Right Thing: return customers that have any associated
# locations matching the string, not just bill/ship location
push @cust_main, qsearch( {
+ 'select' => 'cust_main.*',
'table' => 'cust_main',
'addl_from' => ' JOIN cust_location USING (custnum) ',
'hashref' => { %options, },
#doesn't throw a wrench in the works)
push @cust_main, qsearch( {
- 'table' => 'cust_main',
- 'hashref' => { %options },
- 'extra_sql' =>
+ 'table' => 'cust_main',
+ 'hashref' => { %options },
+ 'extra_sql' =>
( keys(%options) ? ' AND ' : ' WHERE ' ).
join(' AND ',
" LOWER(first) = ". dbh->quote(lc($first)),
" LOWER(company) = ". dbh->quote(lc($company)),
$agentnums_sql,
),
- } ),
+ } );
#contacts?
# probably not necessary for the "something a browser remembered" case
#cust_main and contacts
push @cust_main, qsearch( {
+ 'select' => 'cust_main.*',
'table' => 'cust_main',
- 'select' => 'cust_main.*, cust_contact.*, contact.contactnum, contact.last as contact_last, contact.first as contact_first, contact.title',
+ 'addl_from' => ' left join cust_contact using (custnum) '.
+ ' left join contact using (contactnum) ',
'hashref' => { %options },
'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
- 'addl_from' => ' left join cust_contact on cust_main.custnum = cust_contact.custnum left join contact using (contactnum) ',
} );
# or it just be something that was typed in... (try that in a sec)
if $conf->exists('address1-search');
push @cust_main, qsearch( {
+ 'select' => 'cust_main.*',
'table' => 'cust_main',
- 'select' => 'cust_main.*, cust_contact.*, contact.contactnum, contact.last as contact_last, contact.first as contact_first, contact.title',
+ 'addl_from' => ' left join cust_contact using (custnum) '.
+ ' left join contact using (contactnum) ',
'hashref' => { %options },
'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
- 'addl_from' => 'left join cust_contact on cust_main.custnum = cust_contact.custnum left join contact using (contactnum) ',
} );
#no exact match, trying substring/fuzzy
if ( $conf->exists('address1-search') && length($value) >= $min_len ) {
push @cust_main, qsearch( {
+ select => 'cust_main.*',
table => 'cust_main',
addl_from => 'JOIN cust_location USING (custnum)',
extra_sql => 'WHERE '.
my $mask_search = FS::payinfo_Mixin->mask_payinfo('CARD', $card_search);
push @cust_main, qsearch({
+ 'select' => 'cust_main.*',
'table' => 'cust_main',
'addl_from' => ' JOIN cust_payby USING (custnum)',
'hashref' => {},
|| ! $cust_bill
)
{
- my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
+ $error = $self->send_message_receipt(
+ 'cust_main' => $cust_main,
+ 'cust_bill' => $opt->{cust_bill},
+ 'msgnum' => $conf->config('payment_receipt_msgnum', $cust_main->agentnum)
+ );
+ #not manual and no noemail flag (here or on the customer)
+ } elsif ( ! $opt->{'noemail'} && ! $cust_main->invoice_noemail ) {
+
+ # check to see if they want to send specific message template as receipt for auto payments
+ if ( $conf->config('payment_receipt_msgnum_auto', $cust_main->agentnum) ) {
+ $error = $self->send_message_receipt(
+ 'cust_main' => $cust_main,
+ 'cust_bill' => $opt->{cust_bill},
+ 'msgnum' => $conf->config('payment_receipt_msgnum_auto', $cust_main->agentnum),
+ );
+ }
+ else {
+ my $queue = new FS::queue {
+ 'job' => 'FS::cust_bill::queueable_email',
+ 'paynum' => $self->paynum,
+ 'custnum' => $cust_main->custnum,
+ };
+
+ my %opt = (
+ 'invnum' => $cust_bill->invnum,
+ 'no_coupon' => 1,
+ );
+
+ if ( my $mode = $conf->config('payment_receipt_statement_mode') ) {
+ $opt{'mode'} = $mode;
+ } else {
+ # backward compatibility, no good fix for this yet as some people may
+ # still have "invoice_latex_statement" and such options
+ $opt{'template'} = 'statement';
+ $opt{'notice_name'} = 'Statement';
+ }
+
+ $error = $queue->insert(%opt);
+ }
+
+
+
+ }
+
+ warn "send_receipt: $error\n" if $error;
+}
+
+=item send_message_receipt
+
+sends out a message receipt.
+$error = $self->send_message_receipt(
+ 'cust_main' => $cust_main,
+ 'cust_bill' => $opt->{cust_bill},
+ 'msgnum' => $conf->config('payment_receipt_msgnum', $cust_main->agentnum)
+ );
+
+=cut
+
+sub send_message_receipt {
+ my ($self, %opt) = @_;
+ my $cust_main = $opt{'cust_main'};
+ my $cust_bill = $opt{'cust_bill'};
+ my $msgnum = $opt{'msgnum'};
+ my $error = '';
+
if ( $msgnum ) {
my %substitutions = ();
- $substitutions{invnum} = $opt->{cust_bill}->invnum if $opt->{cust_bill};
+ $substitutions{invnum} = $cust_bill->invnum if $cust_bill;
my $msg_template = qsearchs('msg_template',{ msgnum => $msgnum});
unless ($msg_template) {
$error = $cust_msg ? $cust_msg->insert : 'error preparing msg_template';
if ($error) {
warn "send_receipt: $error";
- return;
+ return $error;
}
my $queue = new FS::queue {
$error = $queue->insert( $cust_msg->custmsgnum );
} else {
-
warn "payment_receipt is on, but no payment_receipt_msgnum\n";
-
- }
-
- #not manual and no noemail flag (here or on the customer)
- } elsif ( ! $opt->{'noemail'} && ! $cust_main->invoice_noemail ) {
-
- my $queue = new FS::queue {
- 'job' => 'FS::cust_bill::queueable_email',
- 'paynum' => $self->paynum,
- 'custnum' => $cust_main->custnum,
- };
-
- my %opt = (
- 'invnum' => $cust_bill->invnum,
- 'no_coupon' => 1,
- );
-
- if ( my $mode = $conf->config('payment_receipt_statement_mode') ) {
- $opt{'mode'} = $mode;
- } else {
- # backward compatibility, no good fix for this yet as some people may
- # still have "invoice_latex_statement" and such options
- $opt{'template'} = 'statement';
- $opt{'notice_name'} = 'Statement';
+ $error = "payment_receipt is on, but no payment_receipt_msgnum";
}
- $error = $queue->insert(%opt);
-
- }
-
- warn "send_receipt: $error\n" if $error;
+ return $error;
}
=item cust_bill_pay
'_date' => $new->_date,
'usernum' => $new->usernum,
'batchnum' => $new->batchnum,
+ 'invnum' => $old->invnum,
'gatewaynum' => $opt{'gatewaynum'},
'processor' => $opt{'processor'},
'auth' => $opt{'auth'},
package FS::cust_payby;
use base qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record );
+use feature 'state';
use strict;
use Scalar::Util qw( blessed );
#encrypted #|| $self->ut_textn('payinfo')
#encrypted #|| $self->ut_textn('paycvv')
# || $self->ut_textn('paymask') #XXX something
- #later #|| $self->ut_textn('paydate')
|| $self->ut_numbern('paystart_month')
|| $self->ut_numbern('paystart_year')
|| $self->ut_numbern('payissue')
return $error if $error;
}
+ $error = $self->ut_daten('paydate');
+ return $error if $error;
+
$self->SUPER::check;
}
=back
+=item has_autobill_cards
+
+Returns the number of unexpired cards configured for autobill
+
+=cut
+
+sub has_autobill_cards {
+ scalar FS::Record::qsearch({
+ table => 'cust_payby',
+ addl_from => 'JOIN cust_main USING (custnum)',
+ order_by => 'LIMIT 1',
+ hashref => {
+ paydate => { op => '>', value => DateTime->now->ymd },
+ weight => { op => '>', value => 0 },
+ },
+ extra_sql =>
+ "AND cust_payby.payby IN ('CARD', 'DCRD') ".
+ 'AND '.
+ $FS::CurrentUser::CurrentUser->agentnums_sql( table => 'cust_main' ),
+ });
+}
+
+=item has_autobill_checks
+
+Returns the number of check accounts configured for autobill
+
+=cut
+
+sub has_autobill_checks {
+ scalar FS::Record::qsearch({
+ table => 'cust_payby',
+ addl_from => 'JOIN cust_main USING (custnum)',
+ order_by => 'LIMIT 1',
+ hashref => {
+ weight => { op => '>', value => 0 },
+ },
+ extra_sql =>
+ "AND cust_payby.payby IN ('CHEK','DCHEK','DCHK') ".
+ 'AND '.
+ $FS::CurrentUser::CurrentUser->agentnums_sql( table => 'cust_main' ),
+ });
+}
+
+=item future_autobill_report_title
+
+Determine if the future_autobill report should be available.
+If so, return a dynamic title for it
+
=cut
+sub future_autobill_report_title {
+ # Perhaps this function belongs somewhere else
+ state $title;
+ return $title if defined $title;
+
+ # Report incompatible with tax engines
+ return $title = '' if FS::TaxEngine->new->info->{batch};
+
+ my $has_cards = has_autobill_cards();
+ my $has_checks = has_autobill_checks();
+ my $_title = 'Future %s transactions';
+
+ if ( $has_cards && $has_checks ) {
+ $title = sprintf $_title, 'credit card and electronic check';
+ } elsif ( $has_cards ) {
+ $title = sprintf $_title, 'credit card';
+ } elsif ( $has_checks ) {
+ $title = sprintf $_title, 'electronic check';
+ } else {
+ $title = '';
+ }
+
+ $title;
+}
+
sub _upgrade_data {
my $class = shift;
local $ignore_expired_card = 1;
local $ignore_invalid_card = 1;
$class->upgrade_set_cardtype;
+ $class->_upgrade_data_paydate_edgebug;
+
+}
+
+=item _upgrade_data_paydate_edgebug
+
+Correct bad data injected into payment expire date column by Edge browser bug
+
+The month and year values may have an extra character injected into form POST
+data by Edge browser. It was possible for some bad month values to slip
+past data validation.
+
+If the stored value was out of range, it was causing payments screen to crash.
+We can detect and fix this by dropping the second digit.
+
+If the stored value is is 11 or 12, it's possible the user inputted a 1. In
+this case, the payment method will fail to authorize, but the record will
+not cause crashdumps for being out of range.
+
+In short, check for any expiration month > 12, and drop the extra digit
+
+=cut
+
+sub _upgrade_data_paydate_edgebug {
+ my $journal_label = 'cust_payby_paydate_edgebug';
+ return if FS::upgrade_journal->is_done( $journal_label );
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+
+ for my $row (
+ FS::Record::qsearch(
+ cust_payby => { paydate => { op => '!=', value => '' }}
+ )
+ ) {
+ next unless $row->ut_daten('paydate');
+
+ # paydate column stored in database has failed date validation
+ my $bad_paydate = $row->paydate;
+
+ my @date = split /[\-\/]/, $bad_paydate;
+ @date = @date[2,0,1] if $date[2] > 1900;
+
+ # Only autocorrecting when month > 12 - notify operator
+ unless ( $date[1] > 12 ) {
+ die sprintf(
+ 'Unable to correct bad paydate stored in cust_payby row '.
+ 'custpaybynum(%s) custnum(%s) paydate(%s)',
+ $row->custpaybynum,
+ $row->custnum,
+ $bad_paydate,
+ );
+ }
+
+ $date[1] = substr( $date[1], 0, 1 );
+ $row->paydate( join('-', @date ));
+
+ if ( my $error = $row->replace ) {
+ die sprintf(
+ 'Failed to autocorrect bad paydate stored in cust_payby row '.
+ 'custpaybynum(%s) custnum(%s) paydate(%s) - error: %s',
+ $row->custpaybynum,
+ $row->custnum,
+ $bad_paydate,
+ $error
+ );
+ }
+
+ warn sprintf(
+ 'Autocorrected bad paydate stored in cust_payby row '.
+ "custpaybynum(%s) custnum(%s) old-paydate(%s) new-paydate(%s)\n",
+ $row->custpaybynum,
+ $row->custnum,
+ $bad_paydate,
+ $row->paydate,
+ );
+
+ }
+ FS::upgrade_journal->set_done( $journal_label );
+ dbh->commit unless $oldAutoCommit;
}
=head1 BUGS
$keep_dates = 0;
$hash{'last_bill'} = '';
$hash{'bill'} = '';
+
+ # Optionally, carry over the next bill date from the changed cust_pkg
+ # so an invoice isn't generated until the customer's usual billing date
+ if ( $self->part_pkg->option('prorate_defer_change_bill', 1) ) {
+ $hash{bill} = $self->bill;
+ }
}
if ( $keep_dates ) {
my $param = shift;
warn Dumper($param) if $DEBUG;
- my $old_part_pkg = qsearchs('part_pkg',
- { pkgpart => $param->{'old_pkgpart'} });
my $new_part_pkg = qsearchs('part_pkg',
{ pkgpart => $param->{'new_pkgpart'} });
- die "Must select a new package type\n" unless $new_part_pkg;
+ die "Must select a new package definition\n" unless $new_part_pkg;
+
#my $keep_dates = $param->{'keep_dates'} || 0;
my $keep_dates = 1; # there is no good reason to turn this off
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- my @cust_pkgs = qsearch('cust_pkg', { 'pkgpart' => $param->{'old_pkgpart'} } );
+ my @old_pkgpart = ref($param->{'old_pkgpart'}) ? @{ $param->{'old_pkgpart'} }
+ : $param->{'old_pkgpart'};
+
+ my @cust_pkgs = qsearch({
+ 'table' => 'cust_pkg',
+ 'extra_sql' => ' WHERE pkgpart IN ('.
+ join(',', @old_pkgpart). ')',
+ });
my $i = 0;
foreach my $old_cust_pkg ( @cust_pkgs ) {
}
+=item fcc_477_record
+
+Returns a fcc_477 record based on option name.
+
+=cut
+
+sub fcc_477_record {
+ my ($self, $option_name) = @_;
+
+ my $fcc_record = qsearchs({
+ 'table' => 'part_pkg_fcc_option',
+ 'hashref' => { 'pkgpart' => $self->{Hash}->{pkgpart}, 'fccoptionname' => $option_name, },
+ });
+
+ return ( $fcc_record );
+
+}
+
=item tax_locationnum_sql
Returns an SQL expression for the tax location for a package, based
'default' => [],
'all_dates' => [],
'svc_acct' => [qw( username _password domsvc )],
+ 'svc_broadband' => [qw( ip_addr description routernum blocknum sectornum speed_up speed_down )],
'svc_phone' => [qw( countrycode phonenum sip_password pin )],
'svc_external' => [qw( id title )],
'location' => [qw( address1 address2 city state zip country )],
die $response->status_line unless $response->is_success;
$data = decode_json($response->content);
die $data->{error}{message} if $data->{error};
+ last unless scalar @{$data->{features}}; #Nothing to insert
foreach my $feature (@{ $data->{features} }) {
my $geoid = $feature->{attributes}{GEOID}; # the prize
queue
upgrade
upgrade_taxable_billpkgnum
+ freeside-ipifony-download
freeside-paymentech-upload
freeside-paymentech-download
test
my $self = shift;
my $cust_msg = shift or die "cust_msg required";
+ if ( $FS::Misc::DISABLE_ALL_NOTICES ) {
+ warn 'send_prepared() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
+ return;
+ }
+
my $domain = 'example.com';
if ( $cust_msg->env_from =~ /\@([\w\.\-]+)/ ) {
$domain = $1;
" AS $integer )";
}
+=item condition_sql_option_money OPTION
+
+As I<condition_sql_option>, but cast the option value to DECIMAL so that
+comparison to other monetary values is type-correct.
+
+=cut
+
+sub condition_sql_option_money {
+ my ($class, $option ) = @_;
+
+ 'CAST(
+ COALESCE('. $class->condition_sql_option($option).
+ " ,'0') ".
+ " AS DECIMAL(10,2) )";
+}
+
=head1 NEW CONDITION CLASSES
A module should be added in FS/FS/part_event/Condition/ which implements the
sub option_fields {
(
- 'agentnum' => { label=>'Agent', type=>'select-agent', },
+ 'agentnum' => { label=>'Agent', type=>'select-agent', multiple => '1' },
);
}
my $cust_main = $self->cust_main($object);
- my $agentnum = $self->option('agentnum');
-
- $cust_main->agentnum == $agentnum;
+ my $hashref = $self->option('agentnum') || {};
+ grep $hashref->{ $_->agentnum }, $cust_main->agent;
}
sub condition_sql {
my( $class, $table, %opt ) = @_;
- "cust_main.agentnum = " . $class->condition_sql_option_integer('agentnum', $opt{'driver_name'});
+ "cust_main.agentnum IN " . $class->condition_sql_option_option_integer('agentnum', $opt{'driver_name'});
}
1;
--- /dev/null
+package FS::part_event::Condition::cust_birthdate;
+use base qw( FS::part_event::Condition );
+use strict;
+use warnings;
+use DateTime;
+
+=head2 NAME
+
+FS::part_event::Condition::cust_birthdate
+
+=head1 DESCRIPTION
+
+Billing event triggered by the time until the customer's next
+birthday (cust_main.birthdate)
+
+=cut
+
+sub description {
+ 'Customer birthdate occurs within the given timeframe';
+}
+
+sub option_fields {
+ (
+ timeframe => {
+ label => 'Timeframe',
+ type => 'freq',
+ value => '1m',
+ }
+ );
+}
+
+sub condition {
+ my( $self, $object, %opt ) = @_;
+ my $cust_main = $self->cust_main($object);
+
+ my $birthdate = $cust_main->birthdate || return 0;
+
+ my %timeframe;
+ if ( $self->option('timeframe') =~ /(\d+)([mwdh])/ ) {
+ my $k = {qw|m months w weeks d days h hours|}->{$2};
+ $timeframe{ $k } = $1;
+ } else {
+ die "Unparsable timeframe given: ".$self->option('timeframe');
+ }
+
+ my $ck_dt = DateTime->from_epoch( epoch => $opt{time} );
+ my $bd_dt = DateTime->from_epoch( epoch => $birthdate );
+
+ # Find the birthday for this calendar year. If customer birthday
+ # has already passed this year, find the birthday for next year.
+ my $next_bd_dt = DateTime->new(
+ month => $bd_dt->month,
+ day => $bd_dt->day,
+ year => $ck_dt->year,
+ );
+ $next_bd_dt->add( years => 1 )
+ if DateTime->compare( $next_bd_dt, $ck_dt ) == -1;
+
+ # Does next birthday occur between now and specified duration?
+ $ck_dt->add( %timeframe );
+ DateTime->compare( $next_bd_dt, $ck_dt ) != 1 ? 1 : 0;
+}
+
+1;
};
}
-#sub option_fields {
-# (
-# 'field' => 'description',
-#
-# 'another_field' => { 'label'=>'Amount', 'type'=>'money', },
-#
-# 'third_field' => { 'label' => 'Types',
-# 'type' => 'checkbox-multiple',
-# 'options' => [ 'h', 's' ],
-# 'option_labels' => { 'h' => 'Happy',
-# 's' => 'Sad',
-# },
-# );
-#}
-
sub condition {
my($self, $cust_pay_batch, %opt) = @_;
- #my $cust_main = $self->cust_main($object);
- #my $value_of_field = $self->option('field');
- #my $time = $opt{'time'}; #use this instead of time or $^T
-
$cust_pay_batch->status =~ /Declined/i;
-
}
-#sub condition_sql {
-# my( $class, $table ) = @_;
-# #...
-# 'true';
-#}
+sub condition_sql {
+ my( $class, $table ) = @_;
+
+ "(cust_pay_batch.status IS NOT NULL AND cust_pay_batch.status = 'Declined')";
+}
1;
sub condition_sql {
my( $class, $table, %opt ) = @_;
+ my $active_sql = FS::cust_main->active_sql;
+ $active_sql =~ s/cust_main.custnum/cust_main.referral_custnum/;
+
+ my $under = $class->condition_sql_option_money('balance');
+
my $age = $class->condition_sql_option_age_from('age', $opt{'time'});
- my $balance_sql = FS::cust_main->balance_sql( $age );
- my $balance_date_sql = FS::cust_main->balance_date_sql;
- my $active_sql = FS::cust_main->active_sql;
- $balance_sql =~ s/cust_main.custnum/cust_main.referral_custnum/;
+ my $balance_date_sql = FS::cust_main->balance_date_sql($age);
$balance_date_sql =~ s/cust_main.custnum/cust_main.referral_custnum/;
- $active_sql =~ s/cust_main.custnum/cust_main.referral_custnum/;
-
- my $sql = "cust_main.referral_custnum IS NOT NULL".
- " AND (".$class->condition_sql_option('active')." IS NULL OR $active_sql)".
- " AND ($balance_date_sql <= $balance_sql)";
+ my $bal_sql = "$balance_date_sql <= $under";
- return $sql;
+ "cust_main.referral_custnum IS NOT NULL
+ AND (". $class->condition_sql_option('active'). " IS NULL OR $active_sql)
+ AND (". $class->condition_sql_option('check_bal'). " IS NULL OR $bal_sql )
+ ";
}
1;
--- /dev/null
+package FS::part_event::Condition::invoice_has_not_been_sent;
+
+use strict;
+use FS::Record qw( qsearchs );
+use FS::cust_bill;
+use Time::Local 'timelocal';
+
+use base qw( FS::part_event::Condition );
+
+sub description {
+ 'Invoice has not been sent previously';
+}
+
+sub eventtable_hashref {
+ { 'cust_main' => 0,
+ 'cust_bill' => 1,
+ 'cust_pkg' => 0,
+ };
+}
+
+sub condition {
+ my($self, $cust_bill, %opt) = @_;
+
+ my $event = qsearchs( {
+ 'table' => 'cust_event',
+ 'addl_from' => 'LEFT JOIN part_event USING ( eventpart )',
+ 'hashref' => {
+ 'tablenum' => $cust_bill->{Hash}->{invnum},
+ 'eventtable' => 'cust_bill',
+ 'status' => 'done',
+ },
+ 'order_by' => " LIMIT 1",
+ } );
+
+ return 0 if $event;
+
+ 1;
+
+}
+
+1;
\ No newline at end of file
}
}
+use FS::upgrade_journal;
+sub _upgrade_data { #class method
+ my ($class, %opts) = @_;
+
+ # migrate part_event_condition_option agentnum to part_event_condition_option_option agentnum
+ unless ( FS::upgrade_journal->is_done('agentnum_to_hash') ) {
+
+ foreach my $condition_option (qsearch('part_event_condition_option', { optionname => 'agentnum', })) {
+ my %options;
+ my $optionvalue = $condition_option->get("optionvalue");
+ if ($optionvalue eq 'HASH' ) { next; }
+ elsif ($optionvalue eq '') {
+ foreach my $agent (qsearch('agent', {})) {
+ $options{$agent->agentnum} = '1';
+ }
+
+ }
+ else {
+ $options{$optionvalue} = '1';
+ }
+
+ $condition_option->optionvalue(ref(\%options));
+ my $error = $condition_option->replace(\%options);
+ die $error if $error;
+
+ }
+
+ FS::upgrade_journal->set_done('agentnum_to_hash');
+
+ }
+
+}
+
=back
=head1 SEE ALSO
die "no default export hostname for export ".$self->exportnum;
}
-#these should probably all go away, just let the subclasses define em
-
=item export_insert SVC_OBJECT
=cut
+# Do not overload! Overload _export_insert instead
+
sub export_insert {
my $self = shift;
#$self->rebless;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp "export_insert() suppressed by noexport_hack" if $DEBUG;
+ return;
+ }
$self->_export_insert(@_);
}
=cut
+# Do not overload! Overload _export_replace instead
+
sub export_replace {
my $self = shift;
#$self->rebless;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp "export_replace() suppressed by noexport_hack" if $DEBUG;
+ return;
+ }
$self->_export_replace(@_);
}
=cut
+# Do not overload! Overload _export_delete instead
+
sub export_delete {
my $self = shift;
#$self->rebless;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp "export_delete() suppressed by noexport_hack" if $DEBUG;
+ return;
+ }
$self->_export_delete(@_);
}
=cut
+# Do not overload! Overload _export_suspend instead
+
sub export_suspend {
my $self = shift;
#$self->rebless;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp "export_suspend() suppressed by noexport_hack" if $DEBUG;
+ return;
+ }
$self->_export_suspend(@_);
}
=cut
+# Do not overload! Overload _export_unsuspend instead
+
sub export_unsuspend {
my $self = shift;
#$self->rebless;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp "export_unsuspend() suppressed by noexport_hack" if $DEBUG;
+ return;
+ }
$self->_export_unsuspend(@_);
}
'';
}
-sub export_insert {
+sub _export_insert {
my $self = shift;
my $svc = shift;
my $cust_pkg = $svc->cust_svc->cust_pkg;
'';
}
-sub export_delete {
+sub _export_delete {
my $self = shift;
my $svc = shift;
'';
}
-sub export_replace {
+sub _export_replace {
my $self = shift;
my $new = shift;
my $old = shift || $self->replace_old;
'';
}
-sub export_suspend {
+sub _export_suspend {
my $self = shift;
my $svc = shift;
$error || '';
}
-sub export_unsuspend {
+sub _export_unsuspend {
my $self = shift;
my $svc = shift;
return;
}
-sub export_insert {
+sub _export_insert {
my $self = shift;
my $new = shift;
my $app = $self->app;
}
}
-sub export_delete {
+sub _export_delete {
my $self = shift;
my $old = shift;
my $app = $self->app;
}
}
-sub export_replace {
+sub _export_replace {
my $self = shift;
my ($new, $old) = @_;
my $app = $self->app;
}
}
-sub export_suspend {
+sub _export_suspend {
my $self = shift;
my $svc = shift;
my $unsuspend = shift || 0;
return;
}
-sub export_unsuspend {
+sub _export_unsuspend {
my ($self, $svc) = @_;
$self->export_suspend($svc, 1);
}
'
);
-sub export_insert {
+sub _export_insert {
my ($self, $svc) = @_;
my $result = $self->request_user_edit(
'Add' => 1,
$result;
}
-sub export_replace {
+sub _export_replace {
my ($self, $new, $old) = @_;
if ($new->email ne $old->email) {
return $old->export_delete || $new->export_insert;
);
}
-sub export_suspend {
+sub _export_suspend {
my ($self, $svc) = @_;
$self->request_user_edit(
'Modify' => 1,
);
}
-sub export_unsuspend {
+sub _export_unsuspend {
my ($self, $svc) = @_;
$self->request_user_edit(
'Modify' => 1,
);
}
-sub export_delete {
+sub _export_delete {
my ($self, $svc) = @_;
$self->request_user_edit(
'ConfirmDelete' => 1,
END
);
-sub export_insert {
+sub _export_insert {
my($self, $svc_phone) = (shift, shift);
local $SIG{__DIE__};
try {
};
}
-sub export_replace {
+sub _export_replace {
my ($self, $new, $old) = @_;
# we only export the IP address and the phone number,
# neither of which we can change in place.
'';
}
-sub export_delete {
+sub _export_delete {
my ($self, $svc_phone) = (shift, shift);
local $SIG{__DIE__};
try {
=cut
-sub export_insert {
+sub _export_insert {
my $self = shift;
my $svc_broadband = shift;
my %hash = (
return;
}
-sub export_delete {
+sub _export_delete {
my $self = shift;
my $svc_broadband = shift;
my $svcnum = $svc_broadband->svcnum;
return;
}
-sub export_replace {
+sub _export_replace {
my $self = shift;
my ($new_svc, $old_svc) = (shift, shift);
END
);
-sub export_insert {
+sub _export_insert {
my $self = shift;
$self->export_command('insert', @_);
}
-sub export_delete {
+sub _export_delete {
my $self = shift;
$self->export_command('delete', @_);
}
-sub export_replace {
+sub _export_replace {
my $self = shift;
$self->export_command('replace', @_);
}
-sub export_suspend {
+sub _export_suspend {
my $self = shift;
$self->export_command('suspend', @_);
}
-sub export_unsuspend {
+sub _export_unsuspend {
my $self = shift;
$self->export_command('unsuspend', @_);
}
'snmp_community' => { 'label'=>'Community', 'default'=>'public' },
'snmp_timeout' => { label=>'Timeout (seconds)', 'default'=>1 },
'snmp_oid' => { label=>'Object ID', multiple=>1 },
+ 'snmp_oid_name' => { label=>'Object Name', multiple=>1 },
;
%info = (
my $vers = $self->option('snmp_version');
my $time = ($self->option('snmp_timeout') || 1) * 1000000;
my @oids = split("\n", $self->option('snmp_oid'));
+ my @oidnames = split("\n", $self->option('snmp_oid_name'));
my %connect = (
'DestHost' => $host,
'Community' => $comm,
return { 'error' => 'Error creating SNMP session' } unless $snmp;
return { 'error' => $snmp->{'ErrorStr'} } if $snmp->{'ErrorStr'};
my @out;
- foreach my $oid (@oids) {
+ for (my $i=0; $i <= $#oids; $i++) {
+ my $oid = $oids[$i];
+ my $oidname = $oidnames[$i];
$oid = $SNMP::MIB{$oid}->{'objectID'} if $SNMP::MIB{$oid};
my @values;
if ($vers eq '1') {
next;
}
my %result = map { $_ => $SNMP::MIB{$oid}{$_} } qw( objectID label );
+ $result{'name'} = $oidname;
# unbless @values, for ease of JSON encoding
$result{'values'} = [];
foreach my $value (@values) {
use Tie::IxHash;
use FS::Record qw(dbh qsearch qsearchs);
use Locale::SubCountry;
+use Carp qw(carp);
our $me = '[broadworks]';
our %client; # exportnum => client object
END
);
-sub export_insert {
+sub _export_insert {
my($self, $svc_x) = (shift, shift);
my $cust_main = $svc_x->cust_main;
'';
}
-sub export_replace {
+sub _export_replace {
my($self, $svc_new, $svc_old) = @_;
my $cust_main = $svc_new->cust_main;
'';
}
-sub export_delete {
+sub _export_delete {
my ($self, $svc_x) = @_;
my $cust_main = $svc_x->cust_main;
sub export_device_insert {
my ($self, $svc_x, $device) = @_;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_device_insert() suppressed by noexport_hack'
+ if $self->option('debug');
+ return;
+ }
+
if ( $device->count('svcnum = ?', $svc_x->svcnum) > 1 ) {
return "This service already has a device.";
}
sub export_device_replace {
my ($self, $svc_x, $new_device, $old_device) = @_;
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_device_replace() suppressed by noexport_hack'
+ if $self->option('debug');
+ return;
+ }
+
my $cust_main = $svc_x->cust_main;
my $groupId = $self->groupId($cust_main);
sub export_device_delete {
my ($self, $svc_x, $device) = @_;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_device_delete() suppressed by noexport_hack'
+ if $self->option('debug');
+ return;
+ }
+
if ( $device->isa('FS::phone_device') ) {
my $error = $self->set_endpoint( $self->userId($svc_x), '' );
return $error if $error;
use Tie::IxHash;
use IPC::Run qw(run);
use FS::CGI qw(rooturl);
+use Carp qw(carp);
$DEBUG = 0;
sub gs_create_config {
my($self, $mac, %opt) = (@_);
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'gs_create_config() suppressed by noexport_hack'
+ if $self->option('debug') || $DEBUG;
+ return;
+ }
+
eval "use Net::SCP;";
die $@ if $@;
sub gs_delete {
my($self, $mac) = (shift, shift);
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'gs_delete() suppressed by noexport_hack'
+ if $self->option('debug') || $DEBUG;
+ return;
+ }
+
$mac = sprintf('%012s', lc($mac));
ssh_cmd( user => $self->option('user'),
use LWP::UserAgent;
use HTTP::Request::Common;
use Email::Valid;
+use Carp qw(carp);
tie my %options, 'Tie::IxHash',
'url' => { label => 'URL', },
sub export_getstatus {
my( $self, $svc_x, $htmlref, $hashref ) = @_;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_getstatus() suppressed by noexport_hack'
+ if $self->option('debug') || $DEBUG;
+ return;
+ }
+
my $url;
my $urlopt = $self->option('url');
no strict 'vars';
sub export_setstatus_listX {
my( $self, $svc_x, $action, $list, $address_item ) = @_;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_setstatus_listX() suppressed by noexport_hack'
+ if $self->option('debug') || $DEBUG;
+ return;
+ }
+
my $option;
if ( $list =~ /^[WA]/i ) { #Whitelist/Allow
$option = 'whitelist_';
sub export_setstatus_vacationX {
my( $self, $svc_x, $action, $hr ) = @_;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_setstatus_vacationX() suppressed by noexport_hack'
+ if $self->option('debug') || $DEBUG;
+ return;
+ }
+
my $option = 'vacation_'. $action. '_url';
my $subject = uri_escape($hr->{subject});
}
1;
-
-1;
use FS::part_export;
use FS::svc_dsl;
use Data::Dumper;
+use Carp qw(carp);
@ISA = qw(FS::part_export);
$me= '[' . __PACKAGE__ . ']';
sub export_expire {
my($self, $svc_dsl, $date) = (shift, shift, shift);
-
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_expire() suppressed by noexport_hack'
+ if $self->option('debug');
+ return;
+ }
+
return 'Invalid operation - Import Mode is enabled' if $self->import_mode;
my $result = $self->valid_order($svc_dsl,'expire');
use Parse::FixedLength;
use File::Temp qw(tempfile);
use vars qw(%info %options $initial_load_hack $DEBUG);
+use Carp qw( carp );
my %upload_targets;
my $self = shift;
my $batch = shift;
local $DEBUG = $self->option('debug');
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'FS::part_export::nena2::process() suppressed by noexport_hack'
+ if $DEBUG;
+ return;
+ }
+
local $FS::UID::AutoCommit = 0;
my $error;
use Date::Format qw( time2str );
use Regexp::Common qw( URI );
use REST::Client;
+use Carp qw(carp);
$me = '[FS::part_export::netsapiens]';
sub export_device_insert {
my( $self, $svc_phone, $phone_device ) = (shift, shift, shift);
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_device_insert() suppressed by noexport_hack'
+ if $self->option('debug');
+ return;
+ }
+
my $domain = $self->ns_domain($svc_phone);
my $countrycode = $svc_phone->countrycode;
my $phonenum = $svc_phone->phonenum;
sub export_device_delete {
my( $self, $svc_phone, $phone_device ) = (shift, shift, shift);
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_device_delete() suppressed by noexport_hack'
+ if $self->option('debug');
+ return;
+ }
+
my $ns = $self->ns_device_command(
'DELETE', $self->ns_device($svc_phone, $phone_device),
);
return $self->get('client');
}
-sub export_insert {
+sub _export_insert {
my( $self, $svc_phone ) = (shift, shift);
my %location_hash = $svc_phone->location_hash;
'';
}
-sub export_replace {
+sub _export_replace {
my( $self, $new, $old ) = (shift, shift, shift);
# except when changing the phone number, exactly like export_insert;
$self->export_insert($new);
}
-sub export_delete {
+sub _export_delete {
my ($self, $svc_phone) = (shift, shift);
if ($self->option('debug')) {
use Tie::IxHash;
use String::ShellQuote;
use FS::part_export;
+use Carp qw(carp);
@ISA = qw(FS::part_export);
my $command = $self->option($action);
return '' if $command =~ /^\s*$/;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp "_export_command($action) suppressed by noexport_hack"
+ if $self->option('debug');
+ return;
+ }
+
#set variable for the command
no strict 'vars';
{
use REST::Client;
use Data::Dumper;
use FS::Conf;
+use Carp qw(carp);
=pod
This export offers basic svc_broadband provisioning for Saisei.
-This is a customer integration with Saisei. This will setup a rate plan and tie
-the rate plan to a host via the Saisei API when the broadband service is provisioned.
-It will also untie the rate plan via the API upon unprovisioning of the broadband service.
+This is a customer integration with Saisei. This will set up a rate plan and tie
+the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
+It will also untie the host from the rate plan, setting it to the default rate plan via the API upon unprovisioning of the broadband service.
-This export will use the broadband service descriptive label for the Saisei rate plan name and
-will use the email from the first contact for the Saisei username that will be
-attached to this rate plan. It will use the Saisei default Access Point.
+This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
+This will also create and modify an access point at Saisei as soon as the tower is created or modified.
-Hostname or IP - Host name to Saisei API
-Port - <I>Port number to Saisei API
-User Name - <I>Saisei API user name
-Password - <I>Saisei API password
+To use this export, follow the below instructions:
+
+Create a new service definition and set the table to svc_broadband. The service name will become the Saisei rate plan name.
+Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
+Attach this Saisei export to this service.
+
+Create a tower and add a sector to that tower. The sector name will be the name of the access point,
+Make sure you have set the up and down rate limit for the tower and the sector. This is required to be able to export the access point.
+The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector. They will be modified at Saisei when modified in freeside.
+Each sector will be attached to its tower access point using the Saisei uplink field.
+
+Create a package for the above created service, and order this package for a customer.
+
+Provision the service, making sure to enter the IP address associated with this service and select the tower and sector for it's access point.
+This provisioned service will then be exported as a host to Saisei.
+
+Unprovisioning this service will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
+
+After this export is set up and attached to a service, you can export the already provisioned services by clicking the link Export provisioned services attached to this export.
+Clicking on this link will export all services attached to this export not currently exported to Saisei.
This module also provides generic methods for working through the L</Saisei API>.
=cut
+tie my %scripts, 'Tie::IxHash',
+ 'export_provisioned_services' => { component => '/elements/popup_link.html',
+ label => 'Export provisioned services',
+ description => 'will export provisioned services of part service with Saisei export attached.',
+ html_label => '<b>Export provisioned services attached to this export.</b>',
+ },
+;
+
tie my %options, 'Tie::IxHash',
'port' => { label => 'Port',
default => 5000 },
- 'username' => { label => 'User Name',
+ 'username' => { label => 'Saisei API User Name',
default => '' },
- 'password' => { label => 'Password',
+ 'password' => { label => 'Saisei API Password',
default => '' },
'debug' => { type => 'checkbox',
label => 'Enable debug warnings' },
'svc' => 'svc_broadband',
'desc' => 'Export broadband service/account to Saisei',
'options' => \%options,
+ 'scripts' => \%scripts,
'notes' => <<'END',
-This is a customer integration with Saisei. This will setup a rate plan and tie
-the rate plan to a host via the Saisei API when the broadband service is provisioned.
-It will also untie the rate plan via the API upon unprovisioning of the broadband service.
-<P>This export will use the broadband service descriptive label for the Saisei rate plan name and
-will use the email from the first contact for the Saisei username that will be
-attached to this rate plan. It will use the Saisei default Access Point.
+This is a customer integration with Saisei. This will set up a rate plan and tie
+the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
+It will also untie the host from the rate plan, setting it to the default rate plan via the API upon unprovisioning of the broadband service.
+<P>
+This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
+This will also create and modify an access point at Saisei as soon as the tower is created or modified.
+<P>
+To use this export, follow the below instructions:
+<P>
+<OL>
+<LI>
+Create a new service definition and set the table to svc_broadband. The service name will become the Saisei rate plan name.
+Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
+Attach this Saisei export to this service.
+</LI>
<P>
-Required Fields:
-<UL>
-<LI>Hostname or IP - <I>Host name to Saisei API</I></LI>
-<LI>Port - <I>Port number to Saisei API</I></LI>
-<LI>User Name - <I>Saisei API user name</I></LI>
-<LI>Password - <I>Saisei API password</I></LI>
-</UL>
+<LI>
+Create a tower and add a sector to that tower. The sector name will be the name of the access point,
+Make sure you have set the up and down rate limit for the tower and the sector. This is required to be able to export the access point.
+The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector. They will be modified at Saisei when modified in freeside.
+Each sector will be attached to its tower access point using the Saisei uplink field.
+</LI>
+<P>
+<LI>
+Create a package for the above created service, and order this package for a customer.
+</LI>
+<P>
+<LI>
+Provision the service, making sure to enter the IP address associated with this service and select the tower and sector for it's access point.
+This provisioned service will then be exported as a host to Saisei.
+<P>
+Unprovisioning this service will set the host entry at Saisei to the default rate plan with the user and access point set to <i>none</i>.
+</LI>
+<P>
+<LI>
+After this export is set up and attached to a service, you can export the already provisioned services by clicking the link <b>Export provisioned services attached to this export</b>.
+Clicking on this link will export all services attached to this export not currently exported to Saisei.
+</LI>
+</OL>
+<P>
+
END
);
sub _export_insert {
my ($self, $svc_broadband) = @_;
- my $rateplan_name = $svc_broadband->{Hash}->{description};
- $rateplan_name =~ s/\s/_/g;
-
-
- # load needed info from our end
- my $cust_main = $svc_broadband->cust_main;
- return "Could not load service customer" unless $cust_main;
- my $conf = new FS::Conf;
- # get policy list
- my $policies = $self->api_get_policies();
+ my $rateplan_name = $self->get_rateplan_name($svc_broadband);
# check for existing rate plan
my $existing_rateplan;
# if no existing rate plan create one and modify it.
$self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
- $self->api_modify_rateplan($policies->{collection}, $svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
+ $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
+ return $self->api_error if $self->{'__saisei_error'};
# set rateplan to existing one or newly created one.
my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
- my @email = map { $_->emailaddress } FS::Record::qsearch({
- 'table' => 'cust_contact',
- 'select' => 'emailaddress',
- 'addl_from' => ' JOIN contact_email USING (contactnum)',
- 'hashref' => { 'custnum' => $cust_main->{Hash}->{custnum}, },
- });
- my $username = $email[0];
- my $description = $cust_main->{Hash}->{first}." ".$cust_main->{Hash}->{last};
+ my $username = $svc_broadband->{Hash}->{svcnum};
+ my $description = $svc_broadband->{Hash}->{description};
if (!$username) {
$self->{'__saisei_error'} = 'no username - can not export';
- warn "No email found $username\n" if $self->option('debug');
- return;
+ return $self->api_error;
}
else {
# check for existing user.
# if no existing user create one.
$self->api_create_user($username, $description) unless $existing_user;
+ return $self->api_error if $self->{'__saisei_error'};
# set user to existing one or newly created one.
my $user = $existing_user ? $existing_user : $self->api_get_user($username);
- ## add access point ?
-
- ## tie host to user
- $self->api_add_host_to_user($user->{collection}->[0]->{name}, $rateplan->{collection}->[0]->{name}, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
+ ## add access point
+ my $tower_sector = FS::Record::qsearchs({
+ 'table' => 'tower_sector',
+ 'select' => 'tower.towername,
+ tower.up_rate_limit as tower_upratelimit,
+ tower.down_rate_limit as tower_downratelimit,
+ tower_sector.sectorname,
+ tower_sector.up_rate_limit as sector_upratelimit,
+ tower_sector.down_rate_limit as sector_downratelimit ',
+ 'addl_from' => 'LEFT JOIN tower USING ( towernum )',
+ 'hashref' => {
+ 'sectornum' => $svc_broadband->{Hash}->{sectornum},
+ },
+ });
+
+ my $tower_name = $tower_sector->{Hash}->{towername};
+ $tower_name =~ s/\s/_/g;
+
+ my $tower_opt = {
+ 'tower_name' => $tower_name,
+ 'tower_uprate_limit' => $tower_sector->{Hash}->{tower_upratelimit},
+ 'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
+ };
+
+ my $tower_ap = process_tower($self, $tower_opt);
+ return $self->api_error if $self->{'__saisei_error'};
+
+ my $sector_name = $tower_sector->{Hash}->{sectorname};
+ $sector_name =~ s/\s/_/g;
+
+ my $sector_opt = {
+ 'tower_name' => $tower_name,
+ 'sector_name' => $sector_name,
+ 'sector_uprate_limit' => $tower_sector->{Hash}->{sector_upratelimit},
+ 'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
+ };
+ my $accesspoint = process_sector($self, $sector_opt);
+ return $self->api_error if $self->{'__saisei_error'};
+
+## get custnum and pkgpart from cust_pkg for virtual access point
+ my $cust_pkg = FS::Record::qsearchs({
+ 'table' => 'cust_pkg',
+ 'hashref' => { 'pkgnum' => $svc_broadband->{Hash}->{pkgnum}, },
+ });
+ my $virtual_ap_name = $cust_pkg->{Hash}->{custnum}.'_'.$cust_pkg->{Hash}->{pkgpart}.'_'.$svc_broadband->{Hash}->{speed_down}.'_'.$svc_broadband->{Hash}->{speed_up};
+
+ my $virtual_ap_opt = {
+ 'virtual_name' => $virtual_ap_name,
+ 'sector_name' => $sector_name,
+ 'virtual_uprate_limit' => $svc_broadband->{Hash}->{speed_up},
+ 'virtual_downrate_limit' => $svc_broadband->{Hash}->{speed_down},
+ };
+ my $virtual_ap = process_virtual_ap($self, $virtual_ap_opt);
+ return $self->api_error if $self->{'__saisei_error'};
+
+ ## tie host to user add sector name as access point.
+ $self->api_add_host_to_user(
+ $user->{collection}->[0]->{name},
+ $rateplan->{collection}->[0]->{name},
+ $svc_broadband->{Hash}->{ip_addr},
+ $virtual_ap->{collection}->[0]->{name},
+ ) unless $self->{'__saisei_error'};
}
- return '';
+ return $self->api_error;
}
sub _export_replace {
- my ($self, $svc_phone) = @_;
- return '';
+ my ($self, $svc_broadband) = @_;
+ my $error = $self->_export_insert($svc_broadband);
+ return $error;
}
sub _export_delete {
my ($self, $svc_broadband) = @_;
- my $cust_main = $svc_broadband->cust_main;
- return "Could not load service customer" unless $cust_main;
- my $conf = new FS::Conf;
+ my $rateplan_name = $self->get_rateplan_name($svc_broadband);
- my $rateplan_name = $svc_broadband->{Hash}->{description};
- $rateplan_name =~ s/\s/_/g;
+ my $username = $svc_broadband->{Hash}->{svcnum};
- my @email = map { $_->emailaddress } FS::Record::qsearch({
- 'table' => 'cust_contact',
- 'select' => 'emailaddress',
- 'addl_from' => ' JOIN contact_email USING (contactnum)',
- 'hashref' => { 'custnum' => $cust_main->{Hash}->{custnum}, },
- });
- my $username = $email[0];
-
- ## tie host to user
+ ## untie host to user
$self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
return '';
}
sub _export_suspend {
- my ($self, $svc_phone) = @_;
+ my ($self, $svc_broadband) = @_;
return '';
}
sub _export_unsuspend {
- my ($self, $svc_phone) = @_;
+ my ($self, $svc_broadband) = @_;
return '';
}
+sub export_partsvc {
+ my ($self, $svc_part) = @_;
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_partsvc() suppressed by noexport_hack'
+ if $self->option('debug');
+ return;
+ }
+
+ my $fcc_477_speeds;
+ if ($svc_part->{Hash}->{svc_broadband__speed_down} eq "down" || $svc_part->{Hash}->{svc_broadband__speed_up} eq "up") {
+ for my $type (qw( down up )) {
+ my $speed_type = "broadband_".$type."stream";
+ foreach my $pkg_svc (FS::Record::qsearch({
+ 'table' => 'pkg_svc',
+ 'select' => 'pkg_svc.*, part_pkg_fcc_option.fccoptionname, part_pkg_fcc_option.optionvalue',
+ 'addl_from' => ' LEFT JOIN part_pkg_fcc_option USING (pkgpart) ',
+ 'extra_sql' => " WHERE pkg_svc.svcpart = ".$svc_part->{Hash}->{svcpart}." AND pkg_svc.quantity > 0 AND part_pkg_fcc_option.fccoptionname = '".$speed_type."'",
+ })) { $fcc_477_speeds->{
+ $pkg_svc->{Hash}->{pkgpart}}->{$speed_type} = $pkg_svc->{Hash}->{optionvalue} * 1000 unless !$pkg_svc->{Hash}->{optionvalue}; }
+ }
+ }
+ else {
+ $fcc_477_speeds->{1}->{broadband_downstream} = $svc_part->{Hash}->{"svc_broadband__speed_down"};
+ $fcc_477_speeds->{1}->{broadband_upstream} = $svc_part->{Hash}->{"svc_broadband__speed_up"};
+ }
+
+ foreach my $key (keys %$fcc_477_speeds) {
+
+ $svc_part->{Hash}->{speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
+ $svc_part->{Hash}->{speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
+ $svc_part->{Hash}->{svc_broadband__speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
+ $svc_part->{Hash}->{svc_broadband__speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
+
+ my $temp_svc = $svc_part->{Hash};
+ my $svc_broadband = {};
+ map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; } } keys %$temp_svc;
+
+ my $rateplan_name = $self->get_rateplan_name($svc_broadband, $svc_part->{Hash}->{svc});
+
+ # check for existing rate plan
+ my $existing_rateplan;
+ $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
+
+ # Modify the existing rate plan with new service data.
+ $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
+
+ # if no existing rate plan create one and modify it.
+ $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
+ $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
+
+ }
+
+ return $self->api_error;
+
+}
+
+sub export_tower_sector {
+ my ($self, $tower) = @_;
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_tower_sector() suppressed by noexport_hack'
+ if $self->option('debug');
+ return;
+ }
+
+ #modify tower or create it.
+ my $tower_name = $tower->{Hash}->{towername};
+ $tower_name =~ s/\s/_/g;
+ my $tower_opt = {
+ 'tower_name' => $tower_name,
+ 'tower_uprate_limit' => $tower->{Hash}->{up_rate_limit},
+ 'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
+ 'modify_existing' => '1', # modify an existing access point with this info
+ };
+
+ my $tower_access_point = process_tower($self, $tower_opt);
+
+ #get list of all access points
+ my $hash_opt = {
+ 'table' => 'tower_sector',
+ 'select' => '*',
+ 'hashref' => { 'towernum' => $tower->{Hash}->{towernum}, },
+ };
+
+ #for each one modify or create it.
+ foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
+ my $sector_name = $tower_sector->{Hash}->{sectorname};
+ $sector_name =~ s/\s/_/g;
+ my $sector_opt = {
+ 'tower_name' => $tower_name,
+ 'sector_name' => $sector_name,
+ 'sector_uprate_limit' => $tower_sector->{Hash}->{up_rate_limit},
+ 'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
+ 'modify_existing' => '1', # modify an existing access point with this info
+ };
+ my $sector_access_point = process_sector($self, $sector_opt);
+ }
+
+ return $self->api_error;
+}
+
+## creates the rateplan name
+sub get_rateplan_name {
+ my ($self, $svc_broadband, $svc_name) = @_;
+
+ my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ) unless $svc_name;
+ my $service_name = $svc_name ? $svc_name : $service_part->{Hash}->{svc};
+
+ my $rateplan_name = $service_name . " " . $svc_broadband->{Hash}->{speed_down} . "-" . $svc_broadband->{Hash}->{speed_up};
+ $rateplan_name =~ s/\s/_/g;
+
+ return $rateplan_name;
+}
+
=head1 Saisei API
These methods allow access to the Saisei API using the credentials
sub api_call {
my ($self,$method,$path,$params) = @_;
+
$self->{'__saisei_error'} = '';
my $auth_info = $self->option('username') . ':' . $self->option('password');
$params ||= {};
}
}
else {
- $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent();
+ $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent()
+ unless ($method eq "GET");
warn "Response Content is\n".$client->responseContent."\n" if $self->option('debug');
return;
}
=head2 api_error
-Returns the error string set by L</PortaOne API> methods,
+Returns the error string set by L</Saisei API> methods,
or a blank string if most recent call produced no errors.
=cut
$self->{'__saisei_error'} = "Did not receive any global policies"
unless $get_policies;
- return $get_policies;
+ return $get_policies->{collection};
}
=head2 api_get_rateplan
my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
return if $self->api_error;
- $self->{'__saisei_error'} = "Did not receive any rateplan info"
- unless $get_rateplan;
return $get_rateplan;
}
my $get_user = $self->api_call("GET", "/users/$user");
return if $self->api_error;
- $self->{'__saisei_error'} = "Did not receive any user info"
- unless $get_user;
return $get_user;
}
sub api_get_accesspoint {
my $self = shift;
- my $accesspoint;
+ my $accesspoint = shift;
my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
return if $self->api_error;
- $self->{'__saisei_error'} = "Did not receive any user info"
- unless $get_accesspoint;
- return;
+ return $get_accesspoint;
+}
+
+=head2 api_get_host
+
+Gets user info for specific host.
+
+=cut
+
+sub api_get_host {
+ my $self = shift;
+ my $ip = shift;
+
+ my $get_host = $self->api_call("GET", "/hosts/$ip");
+
+ return if $self->api_error;
+
+ return $get_host;
}
=head2 api_create_rateplan
sub api_create_rateplan {
my ($self, $svc, $rateplan) = @_;
+ $self->{'__saisei_error'} = "No downrate listed for service $rateplan" if !$svc->{Hash}->{speed_down};
+ $self->{'__saisei_error'} = "No uprate listed for service $rateplan" if !$svc->{Hash}->{speed_up};
+
my $new_rateplan = $self->api_call(
"PUT",
"/rate_plans/$rateplan",
'downstream_rate' => $svc->{Hash}->{speed_down},
'upstream_rate' => $svc->{Hash}->{speed_up},
},
- );
+ ) unless $self->{'__saisei_error'};
$self->{'__saisei_error'} = "Rate Plan not created"
- unless $new_rateplan; # should never happen
+ unless ($new_rateplan || $self->{'__saisei_error'});
+
return $new_rateplan;
}
=head2 api_modify_rateplan
-Modify a rateplan.
+Modify a new rateplan.
=cut
sub api_modify_rateplan {
- my ($self,$policies,$svc,$rateplan_name) = @_;
+ my ($self,$svc,$rateplan_name) = @_;
+
+ # get policy list
+ my $policies = $self->api_get_policies();
foreach my $policy (@$policies) {
my $policyname = $policy->{name};
},
);
- $self->{'__saisei_error'} = "Rate Plan not modified"
- unless $modified_rateplan; # should never happen
+ $self->{'__saisei_error'} = "Rate Plan not modified after create"
+ unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
}
}
+=head2 api_modify_existing_rateplan
+
+Modify a existing rateplan.
+
+=cut
+
+sub api_modify_existing_rateplan {
+ my ($self,$svc,$rateplan_name) = @_;
+
+ my $modified_rateplan = $self->api_call(
+ "PUT",
+ "/rate_plans/$rateplan_name",
+ {
+ 'downstream_rate' => $svc->{Hash}->{speed_down},
+ 'upstream_rate' => $svc->{Hash}->{speed_up},
+ },
+ );
+
+ $self->{'__saisei_error'} = "Rate Plan not modified"
+ unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
+
+ return;
+
+}
+
=head2 api_create_user
Creates a user.
);
$self->{'__saisei_error'} = "User not created"
- unless $new_user; # should never happen
+ unless ($new_user || $self->{'__saisei_error'}); # should never happen
return $new_user;
=cut
sub api_create_accesspoint {
- my ($self,$accesspoint) = @_;
+ my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
# this has not been tested, but should work, if needed.
- #my $new_accesspoint = $self->api_call(
- # "PUT",
- # "/access_points/$accesspoint",
- # {
- # 'description' => 'my description',
- # },
- #);
-
- #$self->{'__saisei_error'} = "Access point not created"
- # unless $new_accesspoint; # should never happen
+ my $new_accesspoint = $self->api_call(
+ "PUT",
+ "/access_points/$accesspoint",
+ {
+ 'downstream_rate_limit' => $downratelimit,
+ 'upstream_rate_limit' => $upratelimit,
+ },
+ );
+
+ $self->{'__saisei_error'} = "Access point not created"
+ unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
+ return;
+
+}
+
+=head2 api_modify_accesspoint
+
+Modify a new access point.
+
+=cut
+
+sub api_modify_accesspoint {
+ my ($self, $accesspoint, $uplink) = @_;
+
+ my $modified_accesspoint = $self->api_call(
+ "PUT",
+ "/access_points/$accesspoint",
+ {
+ 'uplink' => $uplink, # name of attached access point
+ },
+ );
+
+ $self->{'__saisei_error'} = "Rate Plan not modified"
+ unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
+
+ return;
+
+}
+
+=head2 api_modify_existing_accesspoint
+
+Modify a existing accesspoint.
+
+=cut
+
+sub api_modify_existing_accesspoint {
+ my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
+
+ my $modified_accesspoint = $self->api_call(
+ "PUT",
+ "/access_points/$accesspoint",
+ {
+ 'downstream_rate_limit' => $downratelimit,
+ 'upstream_rate_limit' => $upratelimit,
+# 'uplink' => $uplink, # name of attached access point
+ },
+ );
+
+ $self->{'__saisei_error'} = "Access point not modified"
+ unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
+
return;
}
=cut
sub api_add_host_to_user {
- my ($self,$user, $rateplan, $ip) = @_;
+ my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
my $new_host = $self->api_call(
"PUT",
{
'user' => $user,
'rate_plan' => $rateplan,
+ 'access_point' => $accesspoint,
},
);
$self->{'__saisei_error'} = "Host not created"
- unless $new_host; # should never happen
+ unless ($new_host || $self->{'__saisei_error'}); # should never happen
return $new_host;
=head2 api_delete_host_to_user
-unties host to user and rateplan.
+unties host from user and rateplan.
+this will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
=cut
);
$self->{'__saisei_error'} = "Host not created"
- unless $delete_host; # should never happen
+ unless ($delete_host || $self->{'__saisei_error'}); # should never happen
return $delete_host;
}
+sub process_tower {
+ my ($self, $opt) = @_;
+
+ my $existing_tower_ap;
+ my $tower_name = $opt->{tower_name};
+
+ #check if tower has been set up as an access point.
+ $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
+
+ # modify the existing accesspoint if changing tower .
+ $self->api_modify_existing_accesspoint (
+ $tower_name,
+ '', # tower does not have a uplink on sectors.
+ $opt->{tower_uprate_limit},
+ $opt->{tower_downrate_limit},
+ ) if $existing_tower_ap && $opt->{modify_existing};
+
+ #if tower does not exist as an access point create it.
+ $self->api_create_accesspoint(
+ $tower_name,
+ $opt->{tower_uprate_limit},
+ $opt->{tower_downrate_limit}
+ ) unless $existing_tower_ap;
+
+ my $accesspoint = $self->api_get_accesspoint($tower_name);
+
+ return $accesspoint;
+}
+
+sub process_sector {
+ my ($self, $opt) = @_;
+
+ my $existing_sector_ap;
+ my $sector_name = $opt->{sector_name};
+
+ #check if sector has been set up as an access point.
+ $existing_sector_ap = $self->api_get_accesspoint($sector_name);
+
+ # modify the existing accesspoint if changing sector .
+ $self->api_modify_existing_accesspoint (
+ $sector_name,
+ $opt->{tower_name},
+ $opt->{sector_uprate_limit},
+ $opt->{sector_downrate_limit},
+ ) if $existing_sector_ap && $opt->{modify_existing};
+
+ #if sector does not exist as an access point create it.
+ $self->api_create_accesspoint(
+ $sector_name,
+ $opt->{sector_uprate_limit},
+ $opt->{sector_downrate_limit},
+ ) unless $existing_sector_ap;
+
+ # Attach newly created sector to it's tower.
+ $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap);
+
+ # set access point to existing one or newly created one.
+ my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
+
+ return $accesspoint;
+}
+
+sub process_virtual_ap {
+ my ($self, $opt) = @_;
+
+ my $existing_virtual_ap;
+ my $virtual_name = $opt->{virtual_name};
+
+ #check if sector has been set up as an access point.
+ $existing_virtual_ap = $self->api_get_accesspoint($virtual_name);
+
+ # modify the existing virtual accesspoint if changing it. this should never happen
+ $self->api_modify_existing_accesspoint (
+ $virtual_name,
+ $opt->{sector_name},
+ $opt->{virtual_uprate_limit},
+ $opt->{virtual_downrate_limit},
+ ) if $existing_virtual_ap && $opt->{modify_existing};
+
+ #if virtual ap does not exist as an access point create it.
+ $self->api_create_accesspoint(
+ $virtual_name,
+ $opt->{virtual_uprate_limit},
+ $opt->{virtual_downrate_limit},
+ ) unless $existing_virtual_ap;
+
+my $update_sector;
+if ($existing_virtual_ap && ($existing_virtual_ap->{collection}->[0]->{uplink}->{link}->{name} ne $opt->{sector_name})) {
+ $update_sector = 1;
+}
+
+ # Attach newly created virtual ap to tower sector ap or if sector has changed.
+ $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
+
+ # set access point to existing one or newly created one.
+ my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
+
+ return $accesspoint;
+}
+
+sub export_provisioned_services {
+ my $job = shift;
+ my $param = shift;
+
+ my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
+ or die "unknown exportnum $param->{export_provisioned_services_exportnum}";
+ bless $part_export;
+
+ my @svcparts = FS::Record::qsearch({
+ 'table' => 'export_svc',
+ 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart ) ',
+ 'hashref' => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
+ });
+ my $part_count = scalar @svcparts;
+
+ my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
+
+ my @svcs = FS::Record::qsearch({
+ 'table' => 'cust_svc',
+ 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ',
+ 'extra_sql' => " WHERE svcpart in ('".$parts."')",
+ }) unless !$parts;
+
+ my $svc_count = scalar @svcs;
+
+ my %status = {};
+ for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
+
+ my $process_count=0;
+ foreach my $svc (@svcs) {
+ if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
+ ## check if service exists as host if not export it.
+ _export_insert($part_export,$svc) unless api_get_host($part_export, $svc->{Hash}->{ip_addr});
+ $process_count++;
+ }
+
+ return;
+
+}
+
=head1 SEE ALSO
L<FS::part_export>
use Net::OpenSSH;
use FS::part_export;
use FS::Record qw( qsearch qsearchs );
+use Carp qw(carp);
@ISA = qw(FS::part_export);
sub export_pkg_change {
my( $self, $svc_acct, $new_cust_pkg, $old_cust_pkg ) = @_;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_pkg_change() suppressed by noexport_hack'
+ if $self->option('debug');
+ return;
+ }
+
my @fields = qw( pkgnum pkgpart agent_pkgid ); #others?
my @date_fields = qw( order_date start_date setup bill last_bill susp adjourn
resume cancel uncancel expire contract_end );
sub _export_command_or_super {
my($self, $action) = (shift, shift);
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp "_export_command_or_super($action) suppressed by noexport_hack"
+ if $self->option('debug');
+ return;
+ }
+
if ( $self->option($action) =~ /^\s*$/ ) {
my $method = "SUPER::_export_$action";
$self->$method(@_);
my ( $self, $action, $svc_acct) = (shift, shift, shift);
my $command = $self->option($action);
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp "_export_command($action) suppressed by noexport_hack"
+ if $self->option('debug');
+ return;
+ }
+
return '' if $command =~ /^\s*$/;
my $stdin = $self->option($action."_stdin");
use DateTime;
use Number::Phone;
use Try::Tiny;
+use Carp qw(carp);
our $me = '[sipwise]';
our $DEBUG = 0;
END
);
-sub export_insert {
+sub _export_insert {
my($self, $svc_x) = (shift, shift);
local $SIG{__DIE__};
'';
}
-sub export_replace {
+sub _export_replace {
my ($self, $svc_new, $svc_old) = @_;
local $SIG{__DIE__};
'';
}
-sub export_delete {
+sub _export_delete {
my ($self, $svc_x) = (shift, shift);
local $SIG{__DIE__};
# logic to set subscribers to locked/active is in replace_subscriber
-sub export_suspend {
+sub _export_suspend {
my $self = shift;
my $svc_x = shift;
my $role = $self->svc_role($svc_x);
'';
}
-sub export_unsuspend {
+sub _export_unsuspend {
my $self = shift;
my $svc_x = shift;
my $role = $self->svc_role($svc_x);
sub export_did {
my $self = shift;
my ($new, $old) = @_;
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'export_did() suppressed by noexport_hack'
+ if $self->option('debug') || $DEBUG;
+ return;
+ }
+
if ( $old and $new->forward_svcnum ne $old->forward_svcnum ) {
my $old_svc_acct = $self->acct_for_did($old);
$self->replace_subscriber( $old_svc_acct ) if $old_svc_acct;
use FS::part_export;
use FS::svc_acct;
use FS::export_svc;
-use Carp qw( cluck );
+use Carp qw( carp cluck );
use NEXT;
use Net::OpenSSH;
}
sub sqlradius_insert { #subroutine, not method
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_insert() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my( $table, $username, %attributes ) = @_;
}
sub sqlradius_usergroup_insert { #subroutine, not method
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_usergroup_insert() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my $username = shift;
my $usergroup = ( $_[0] =~ /^(rad)?usergroup/i ) ? shift : 'usergroup';
}
sub sqlradius_usergroup_delete { #subroutine, not method
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_usergroup_delete() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my $username = shift;
my $usergroup = ( $_[0] =~ /^(rad)?usergroup/i ) ? shift : 'usergroup';
}
sub sqlradius_rename { #subroutine, not method
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_rename() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my($new_username, $old_username) = (shift, shift);
my $usergroup = ( $_[0] =~ /^(rad)?usergroup/i ) ? shift : 'usergroup';
}
sub sqlradius_attrib_delete { #subroutine, not method
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_attrib_delete() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my( $table, $username, @attrib ) = @_;
}
sub sqlradius_delete { #subroutine, not method
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_delete() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my $username = shift;
my $usergroup = ( $_[0] =~ /^(rad)?usergroup/i ) ? shift : 'usergroup';
sub update_svc {
my $self = shift;
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'update_svc() suppressed by noexport_hack'
+ if $self->option('debug') || $DEBUG;
+ return;
+ }
+
my $conf = new FS::Conf;
my $fdbh = dbh;
sub export_nas_action {
my $self = shift;
my ($action, $new, $old) = @_;
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp "export_nas_action($action) suppressed by noexport_hack"
+ if $self->option('debug') || $DEBUG;
+ return;
+ }
+
# find the NAS in the target table by its name
my $nasname = ($action eq 'replace') ? $old->nasname : $new->nasname;
my $nasnum = $new->nasnum;
}
sub sqlradius_nas_insert {
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_nas_insert() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my %opt = @_;
my $nas = qsearchs('nas', { nasnum => $opt{'nasnum'} })
}
sub sqlradius_nas_delete {
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_nas_delete() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my %opt = @_;
my $sth = $dbh->prepare('DELETE FROM nas WHERE nasname = ?');
}
sub sqlradius_nas_replace {
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_nas_replace() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my %opt = @_;
my $nas = qsearchs('nas', { nasnum => $opt{'nasnum'} })
}
sub sqlradius_attr_insert {
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_attr_insert() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my %opt = @_;
}
sub sqlradius_attr_delete {
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_attr_delete() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my %opt = @_;
}
sub sqlradius_group_replace {
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_group_replace() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my $usergroup = shift;
$usergroup =~ /^(rad)?usergroup$/
=cut
sub sqlradius_user_disconnect {
+
+ if ( $FS::svc_Common::noexport_hack ) {
+ carp 'sqlradius_user_disconnect() suppressed by noexport_hack' if $DEBUG;
+ return;
+ }
+
my $dbh = sqlradius_connect(shift, shift, shift);
my %opt = @_;
# get list of nas
'';
}
-sub export_insert {
+sub _export_insert {
my($self, $svc_x) = (shift, shift);
my $error = $self->check_svc($svc_x);
}
}
-sub export_replace {
+sub _export_replace {
my ($self, $svc_new, $svc_old) = @_;
my $error = $self->check_svc($svc_new);
}
}
-sub export_delete {
+sub _export_delete {
my ($self, $svc_x) = (shift, shift);
my $role = $self->svc_role($svc_x)
'';
}
-sub export_insert {
+sub _export_insert {
my ($self, $sector) = @_;
return unless $self->option('use_coverage');
}
-sub export_replace { # do the same thing as insert
+sub _export_replace { # do the same thing as insert
my $self = shift;
$self->export_insert(@_);
}
END
);
-sub export_insert {
+sub _export_insert {
my($self, $svc_x) = (shift, shift);
my $role = $self->svc_role($svc_x);
'';
}
-sub export_replace {
+sub _export_replace {
my ($self, $svc_new, $svc_old) = @_;
my $role = $self->svc_role($svc_new);
my $error;
'';
}
-sub export_delete {
+sub _export_delete {
my ($self, $svc_x) = (shift, shift);
my $role = $self->svc_role($svc_x);
if ( $role eq 'subacct' ) {
'';
}
-sub export_suspend {
+sub _export_suspend {
my $self = shift;
my $svc_x = shift;
my $role = $self->svc_role($svc_x);
'';
}
-sub export_unsuspend {
+sub _export_unsuspend {
my $self = shift;
my $svc_x = shift;
my $role = $self->svc_role($svc_x);
# $chg_months: the number of months we are charging recur for
# $months: $chg_months or the months left on the discount, whchever is less
- my $chg_months = $cust_pkg->part_pkg->freq || 1;
+ my $chg_months = 1;
+ unless ($cust_pkg->part_pkg->freq !~ /^\d+$/) {
+ $chg_months = $cust_pkg->part_pkg->freq || 1;
+ }
if ( defined($param->{'months'}) ) { # then override
$chg_months = $param->{'months'};
}
'the customer\'s next bill date',
'type' => 'checkbox',
},
+ 'prorate_defer_change_bill' => {
+ 'name' => 'When synchronizing, defer bill for '.
+ 'package changes until the customer\'s '.
+ 'next bill date',
+ 'type' => 'checkbox',
+ },
'prorate_round_day' => {
'name' => 'When synchronizing, round the prorated '.
'period',
},
'fieldorder' => [ qw( recur_temporality
start_1st
- sync_bill_date prorate_defer_bill prorate_round_day
+ sync_bill_date prorate_defer_bill
+ prorate_defer_change_bill prorate_round_day
suspend_bill unsuspend_adjust_bill
bill_recur_on_cancel
bill_suspend_as_cancel
sub item_discount {
my ($self, $cust_pkg) = @_;
- return unless $self->option('show_as_discount');
+ return unless $self->option('show_as_discount',1);
my $intro_end = $self->intro_end($cust_pkg);
my $amount = sprintf('%.2f',
$self->option('intro_fee') - $self->option('recur_fee')
},
'prorate_defer_bill' => {
'name' => 'When prorating, defer the first bill until the '.
- 'billing day',
+ 'billing day or customers next bill date if synchronizing.',
'type' => 'checkbox',
},
'prorate_verbose' => {
'shortname' => 'External SQL query',
'inherit_fields' => [ 'prorate_Mixin', 'global_Mixin' ],
'fields' => {
+ 'sync_bill_date' => { 'name' => 'Prorate first month to synchronize '.
+ 'with the customer\'s other packages',
+ 'type' => 'checkbox',
+ },
'cutoff_day' => { 'name' => 'Billing Day (1 - 28) for prorating or '.
'subscription',
'default' => '1',
},
},
- 'fieldorder' => [qw( recur_method cutoff_day ),
+ 'fieldorder' => [qw( recur_method cutoff_day sync_bill_date),
FS::part_pkg::prorate_Mixin::fieldorder,
qw( datasrc db_username db_password query query_style
)],
($cust_pkg->quantity || 1) * $self->calc_recur_Common($cust_pkg,$sdate,$details,$param);
}
+sub cutoff_day {
+ my( $self, $cust_pkg ) = @_;
+ my $error = FS::part_pkg::flat::cutoff_day( $self, $cust_pkg );
+ return $error;
+}
+
sub can_discount { 1; }
sub is_free { 0; }
grep $_->can('dsl_pull'), $self->part_export;
}
+=item part_export_partsvc
+
+Returns a list of any exports (see L<FS::part_export>) for this service that
+are capable of pushing a change after part svc is changed.
+
+=cut
+
+sub part_export_partsvc {
+ my $self = shift;
+ grep $_->can('export_partsvc'), $self->part_export;
+}
+
=item cust_svc [ PKGPART ]
Returns a list of associated customer services (FS::cust_svc records).
map {
my $f = $svcdb.'__'.$_;
my $flag = $param->{ $f.'_flag' } || ''; #silence warnings
- if ( $flag =~ /^[MAH]$/ ) {
+ if ( $flag =~ /^[MAHP]$/ ) {
$param->{ $f } = delete( $param->{ $f.'_classnum' } );
}
- if ( ( $flag =~ /^[MAHS]$/ or $_ eq 'usergroup' )
+ if ( ( $flag =~ /^[MAHSP]$/ or $_ eq 'usergroup' )
and ref($param->{ $f }) ) {
$param->{ $f } = join(',', @{ $param->{ $f } });
}
);
die "$error\n" if $error;
+
+ foreach my $part_svc_export ( $new->part_export_partsvc ) {
+ $error = $part_svc_export->export_partsvc($new);
+ }
+ return $error if $error;
}
=item process_bulk_cust_svc
;
return $error if $error;
- $self->columnflag =~ /^([DFSMAHX]?)$/
+ $self->columnflag =~ /^([DFSMAHXP]?)$/
or return "illegal columnflag ". $self->columnflag;
$self->columnflag(uc($1));
die "invalid branch/routing number '$aba'\n";
}
+ ## set custname to business name if business checking or savings account is used otherwise leave as first and last name.
+ my $custname = $cust_pay_batch->cust_main->batch_payment_payname($cust_pay_batch);
+
$i++;
## set to D for debit by default, then override to what cust_pay_batch has as payments may not have paycode.
sprintf("%010.0f",$cust_pay_batch->amount*100).
' '.
time2str("%Y%j", time + 86400).
- sprintf("%-30.30s", encode('utf8', $cust_pay_batch->cust_main->first . ' ' .
- $cust_pay_batch->cust_main->last)).
+ sprintf("%-30.30s", encode('utf8', $custname)).
'E'. # English
' '.
sprintf("%-15s", $shortname).
},
);
+## this format can handle credit transactions
+sub can_handle_credits {
+ 1;
+}
+
1;
use Date::Parse 'str2time';
use Tie::IxHash;
use FS::Conf;
-use FS::Misc 'bytes_substr';
+use Unicode::Truncate 'truncate_egc';
my $conf;
my ($bin, $merchantID, $terminalID, $username, $password, $with_recurringInd);
$hash->{'error_message'} = $hash->{'procStatusMessage'};
}
},
- 'approved' => sub { my $hash = shift;
- $hash->{'approvalStatus'}
- },
- 'declined' => sub { my $hash = shift;
- ! $hash->{'approvalStatus'}
- },
+ 'approved' => sub { shift->{'approvalStatus'} == 1 },
+ 'declined' => sub { shift->{'approvalStatus'} != 1 },
);
my %paytype = (
ecpBankAcctType => $paytype{lc($_->paytype)},
ecpDelvMethod => 'A',
),
- avsZip => bytes_substr($_->zip, 0, 10),
- avsAddress1 => bytes_substr($_->address1, 0, 30),
- avsAddress2 => bytes_substr($_->address2, 0, 30),
- avsCity => bytes_substr($_->city, 0, 20),
- avsState => bytes_substr($_->state, 0, 2),
- avsName => bytes_substr($_->first. ' '. $_->last, 0, 30),
+ # truncate_egc will die() on empty string
+ avsZip => $_->zip ? truncate_egc($_->zip, 10) : undef,
+ avsAddress1 => $_->address1 ? truncate_egc($_->address1, 30) : undef,
+ avsAddress2 => $_->address2 ? truncate_egc($_->address2, 30) : undef,
+ avsCity => $_->city ? truncate_egc($_->city, 20) : undef,
+ avsState => $_->state ? truncate_egc($_->state, 2) : undef,
+ avsName => ($_->first || $_->last)
+ ? truncate_egc($_->first. ' '. $_->last, 30) : undef,
( $paymentech_countries{ $_->country }
? ( avsCountryCode => $_->country )
: ()
use strict;
use NEXT;
-use FS::Record qw(qsearchs qsearch);
+use Carp qw(croak carp);
+use FS::Record qw(qsearchs qsearch dbh);
use FS::Conf;
use FS::router;
use FS::part_svc_router;
my $error = $self->ip_check;
return $error if $error;
if ( my $router = $self->router ) {
- if ( grep { $_->routernum eq $router->routernum } $self->allowed_routers ) {
+ if ( grep { $_->routernum == $router->routernum } $self->allowed_routers ) {
return '';
} else {
return 'Router '.$router->routername.' not available for this service';
}
sub _used_addresses {
- my ($class, $block, $exclude) = @_;
- my $ip_field = $class->table_info->{'ip_field'}
- or return ();
- # if the service doesn't have an ip_field, then it has no IP addresses
- # in use, yes?
-
- my %hash = ( $ip_field => { op => '!=', value => '' } );
- #$hash{'blocknum'} = $block->blocknum if $block;
- $hash{'svcnum'} = { op => '!=', value => $exclude->svcnum } if ref $exclude;
- map { my $na = $_->NetAddr; $na ? $na->addr : () }
- qsearch({
- table => $class->table,
- hashref => \%hash,
- extra_sql => " AND $ip_field != '0e0'",
- });
+ my ($class, $block, $exclude_svc) = @_;
+
+ croak "_used_addresses() requires an FS::addr_block parameter"
+ unless ref $block && $block->isa('FS::addr_block');
+
+ my $ip_field = $class->table_info->{'ip_field'};
+ if ( !$ip_field ) {
+ carp "_used_addresses() skipped, no ip_field";
+ return;
+ }
+
+ my %qsearch = ( $ip_field => { op => '!=', value => '' });
+ $qsearch{svcnum} = { op => '!=', value => $exclude_svc->svcnum }
+ if ref $exclude_svc && $exclude_svc->svcnum;
+
+ my $block_na = $block->NetAddr;
+
+ my $octets;
+ if ($block->ip_netmask >= 24) {
+ $octets = 3;
+ } elsif ($block->ip_netmask >= 16) {
+ $octets = 2;
+ } elsif ($block->ip_netmask >= 8) {
+ $octets = 1;
+ }
+
+ # e.g.
+ # SELECT ip_addr
+ # FROM svc_broadband
+ # WHERE ip_addr != ''
+ # AND ip_addr != '0e0'
+ # AND ip_addr LIKE '10.0.2.%';
+ #
+ # For /24, /16 and /8 this approach is fast, even when svc_broadband table
+ # contains 650,000+ ip records. For other allocations, this approach is
+ # not speedy, but usable.
+ #
+ # Note: A use case like this would could greatly benefit from a qsearch()
+ # parameter to bypass FS::Record objects creation and just
+ # return hashrefs from DBI. 200,000 hashrefs are many seconds faster
+ # than 200,000 FS::Record objects
+ my %qsearch_param = (
+ table => $class->table,
+ select => $ip_field,
+ hashref => \%qsearch,
+ extra_sql => " AND $ip_field != '0e0' ",
+ );
+ if ( $octets ) {
+ my $block_str = join('.', (split(/\D/, $block_na->first))[0..$octets-1]);
+ $qsearch_param{extra_sql}
+ .= " AND $ip_field LIKE ".dbh->quote("${block_str}.%");
+ }
+
+ if ( $block->ip_netmask % 8 ) {
+ # Some addresses returned by qsearch may be outside the network block,
+ # so each ip address is tested to be in the block before it's returned.
+ return
+ grep { $block_na->contains( NetAddr::IP->new( $_ ) ) }
+ map { $_->$ip_field }
+ qsearch( \%qsearch );
+ }
+
+ return
+ map { $_->$ip_field }
+ qsearch( \%qsearch_param );
}
sub _is_used {
disable_select => 1, #UI wonky, pry works otherwise
},
'sectornum' => 'Tower sector',
+ 'routernum' => 'Router/block',
+ 'blocknum' => {
+ 'label' => 'Address block',
+ 'type' => 'select',
+ 'select_table' => 'addr_block',
+ 'select_key' => 'blocknum',
+ 'select_label' => 'cidr',
+ 'disable_inventory' => 1,
+ },
'usergroup' => {
label => 'RADIUS groups',
type => 'select-radius_group.html',
type => 'text',
disable_inventory => 1,
disable_select => 1,
- disable_part_svc_column => 1,
+ #disable_part_svc_column => 1,
},
'upbytes' => { label => 'Upload',
type => 'text',
return '' unless $amount;
+ return ''
+ if $self->cust_svc->part_svc->part_svc_column($column)->columnflag eq 'F';
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
'fields' => {
'svcnum' => 'Service',
'description' => 'Descriptive label',
- 'speed_down' => 'Download speed (Kbps)',
- 'speed_up' => 'Upload speed (Kbps)',
+ 'speed_up' => {
+ 'label' => 'Upload speed (Kbps)',
+ 'type' => 'fcc_477_speed',
+ 'def_info' => 'both upload and download speed must be set to FCC 477 information if using that modifier',
+ },
+ 'speed_down' => {
+ 'label' => 'Download speed (Kbps)',
+ 'type' => 'fcc_477_speed',
+ 'def_info' => 'both upload and download speed must be set to FCC 477 information if using that modifier',
+ },
'ip_addr' => 'IP address',
- 'blocknum' =>
- { 'label' => 'Address block',
- 'type' => 'select',
- 'select_table' => 'addr_block',
- 'select_key' => 'blocknum',
- 'select_label' => 'cidr',
+ 'blocknum' => {
+ 'label' => 'Address block',
+ 'type' => 'select',
+ 'select_table' => 'addr_block',
+ 'select_key' => 'blocknum',
+ 'select_label' => 'cidr',
'disable_inventory' => 1,
},
'plan_id' => 'Service Plan Id',
#select_table => 'radius_group',
#select_key => 'groupnum',
#select_label => 'groupname',
+ disable_select => 1,
disable_inventory => 1,
multiple => 1,
},
disable_inventory => 1,
},
'serviceid' => 'Torrus serviceid', #but is should be hidden
+ 'speed_test_up' => { 'label' => 'Speed test upload (Kbps)' },
+ 'speed_test_down' => { 'label' => 'Speed test download (Kbps)' },
+ 'speed_test_latency' => 'Speed test latency (ms)',
},
};
}
|| $self->ut_textn('description')
|| $self->ut_numbern('speed_up')
|| $self->ut_numbern('speed_down')
+ || $self->ut_numbern('speed_test_up')
+ || $self->ut_numbern('speed_test_down')
|| $self->ut_ipn('ip_addr')
|| $self->ut_hexn('mac_addr')
|| $self->ut_hexn('auth_key')
#next SVC;
}
+ require FS::Misc::FixIPFormat;
+ FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+ 'svc_broadband', 'svcnum', 'ip_addr',
+ );
+
'';
}
=cut
1;
-
primary key
+=item providernum
+
+Provider (see L<FS::cable_provider>)
+
+=item ordernum
+
+Provider order number
+
+=item modelnum
+
+Cable device model (see L<FS::cable_model>)
+
+=item serialnum
+
+Cable device serial number
+
+=item mac_addr
+
+Cable device MAC address
+
=back
=head1 METHODS
'LEFT JOIN circuit_type USING ( typenum )';
}
+sub _upgrade_data {
+
+ require FS::Misc::FixIPFormat;
+ FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+ 'svc_circuit', 'svcnum', 'endpoint_ip_addr',
+ );
+
+ '';
+
+}
+
=back
=head1 SEE ALSO
=cut
1;
-
=over 4
-=item svcnum - Primary key (assigned automatcially for new DSL))
+=item svcnum
-=item pushed - Time DSL order pushed to vendor/telco, if applicable
+Primary key (assigned automatcially for new DSL))
-=item desired_due_date - Desired Due Date
+=item pushed
-=item due_date - Due Date
+Time DSL order pushed to vendor/telco, if applicable
-=item vendor_order_id - Vendor/telco DSL order #
+=item desired_due_date
+
+Desired Due Date
+
+=item due_date
+
+Due Date
+
+=item vendor_order_id
+
+Vendor/telco DSL order #
=item vendor_order_type
Vendor/telco DSL order status (e.g. (N)ew, (A)ssigned, (R)ejected, (M)revised,
(C)ompleted, (X)cancelled, or similar)
-=item first - End-user first name
+=item first
+
+End-user first name
+
+=item last
+
+End-user last name
+
+=item company
-=item last - End-user last name
+End-user company name
-=item company - End-user company name
+=item phonenum
-=item phonenum - DSL Telephone Number
+DSL Telephone Number
-=item gateway_access_number - Gateway access number, if different
+=item gateway_access_number
-=item loop_type - Loop-type - vendor/telco-specific
+Gateway access number, if different
-=item local_voice_provider - Local Voice Provider's name
+=item loop_type
-=item circuitnum - Circuit #
+Loop-type - vendor/telco-specific
+
+=item local_voice_provider
+
+Local Voice Provider's name
+
+=item circuitnum
+
+Circuit #
=item vpi
=item vci
-=item rate_band - Rate Band
+=item rate_band
+
+Rate Band
=item isp_chg
Ikano-specific fields, do not use otherwise
-=item username - if outsourced PPPoE/RADIUS, username
+=item username
+
+if outsourced PPPoE/RADIUS, username
+
+=item password
+
+if outsourced PPPoE/RADIUS, password
+
+=item monitored
-=item password - if outsourced PPPoE/RADIUS, password
+Order is monitored (auto-pull/sync), either Y or blank
-=item monitored - Order is monitored (auto-pull/sync), either Y or blank
+=item last_pull
-=item last_pull - time of last data pull from vendor/telco
+time of last data pull from vendor/telco
=back
join(':', $self->hw_addr =~ /../g) : $self->hw_addr)
}
+sub _upgrade_data {
+
+ require FS::Misc::FixIPFormat;
+ FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+ 'svc_hardware', 'svcnum', 'ip_addr',
+ );
+
+ '';
+
+}
+
=back
=head1 SEE ALSO
=cut
1;
-
qsearchs ( $psearch->{query} );
}
+sub _upgrade_data {
+
+ require FS::Misc::FixIPFormat;
+ FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+ 'svc_pbx', 'svcnum', 'ip_addr',
+ );
+
+ '';
+
+}
+
=back
=head1 BUGS
=cut
1;
-
Disabled flag, empty or 'Y'
+=item up_rate_limit
+
+Up Rate limit for towner
+
+=item down_rate_limit
+
+Down Rate limit for tower
+
=back
=head1 METHODS
|| $self->ut_floatn('height')
|| $self->ut_floatn('veg_height')
|| $self->ut_alphan('color')
+ || $self->ut_numbern('up_rate_limit')
+ || $self->ut_numbern('down_rate_limit')
;
return $error if $error;
The coordinate boundaries of the coverage map.
+=item title
+
+The sector title.
+
+=item up_rate_limit
+
+Up rate limit for sector.
+
+=item down_rate_limit
+
+down rate limit for sector.
+
=back
=head1 METHODS
$self->ut_numbern('sectornum')
|| $self->ut_number('towernum', 'tower', 'towernum')
|| $self->ut_text('sectorname')
- || $self->ut_textn('ip_addr')
+ || $self->ut_ip46n('ip_addr')
|| $self->ut_floatn('height')
|| $self->ut_numbern('freq_mhz')
|| $self->ut_numbern('direction')
|| $self->ut_decimaln('antenna_gain')
|| $self->ut_numbern('hardware_typenum')
|| $self->ut_textn('title')
+ || $self->ut_numbern('up_rate_limit')
+ || $self->ut_numbern('down_rate_limit')
# all of these might get relocated as part of coverage refactoring
|| $self->ut_anything('image')
|| $self->ut_sfloatn('west')
});
}
+=item part_export_svc_broadband
+
+Returns all svc_broadband exports.
+
+=cut
+
+sub part_export_svc_broadband {
+ my $info = $FS::part_export::exports{'svc_broadband'} or return;
+ my @exporttypes = map { dbh->quote($_) } keys %$info or return;
+ qsearch({
+ 'table' => 'part_export',
+ 'extra_sql' => 'WHERE exporttype IN(' . join(',', @exporttypes) . ')'
+ });
+}
+
=back
=head1 SUBROUTINES
die $error if $error;
}
+sub _upgrade_data {
+
+ require FS::Misc::FixIPFormat;
+ FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+ 'tower_sector', 'sectornum', 'ip_addr',
+ );
+
+ '';
+
+}
+
=head1 BUGS
=head1 SEE ALSO
=cut
1;
-
"enddate=s" => \$enddate,
);
+$startdate = str2time($startdate) or die "can't parse start date $startdate\n";
+ $startdate = time2str('%m-%d-%Y', $startdate);
+$enddate = str2time($enddate) or die "can't parse start date $enddate\n";
+ $enddate = time2str('%m-%d-%Y', $enddate);
+
my $fsuser = $ARGV[-1];
die usage() unless $fsuser;
seek($cfh,0,0);
- print "Importing batch $cdrbatch\n";
+ warn "Importing batch $cdrbatch\n";
my $error = FS::cdr::batch_import({
'batch_namevalue' => $cdrbatch,
'file' => $cfh->filename,
'format' => 'telapi_'.$type
});
+ warn "Error importing CDR's\n".$error if $error;
+
exit;
\ No newline at end of file
use Date::Format qw(time2str);
use File::Temp qw(tempdir);
use Net::SFTP::Foreign;
+use File::Copy qw(copy);
+use Text::CSV;
use FS::UID qw(adminsuidsetup);
use FS::Record qw(qsearch qsearchs);
use FS::cust_main;
use FS::Conf;
-use File::Copy qw(copy);
-use Text::CSV;
+use FS::Log;
-my %opt;
+our %opt;
getopts('vqNa:P:C:e:', \%opt);
# Product codes that are subject to flat rate E911 charges. For these
}
# for now assume SFTP download as the only method
-print STDERR "Connecting to $sftpuser\@$host...\n" if $opt{v};
-
-my $sftp = Net::SFTP::Foreign->new(
- host => $host,
- user => $sftpuser,
- port => $port,
- # for now we don't support passwords. use authorized_keys.
- timeout => 30,
- #more => ($opt{v} ? '-v' : ''),
-);
-die "failed to connect to '$sftpuser\@$host'\n(".$sftp->error.")\n"
- if $sftp->error;
+my $sftp = sftp_connect($host, $sftpuser, $port);
+if ( $sftp->error ) {
+ my $error = "Connection failed to $sftpuser\@$host: ". $sftp->error.
+ ", giving up.";
+ mylog('critical', $error);
+ die $error;
+}
$sftp->setcwd($path) if $path;
my $files = $sftp->ls('ready', wanted => qr/\.csv$/, names_only => 1);
if (!@$files) {
- print STDERR "No charge files found.\n" if $opt{v};
+ mylog('warning',"No charge files found.");
exit(-1);
}
my %is_e911 = map {$_ => 1} @E911_CODES;
FILE: foreach my $filename (@$files) {
- print STDERR "Retrieving $filename\n" if $opt{v};
+ mylog('debug', "Retrieving $filename");
$sftp->get("ready/$filename", "$tmpdir/$filename");
if($sftp->error) {
warn "failed to download $filename\n";
# make sure server archive dir exists
if ( !$sftp->stat('done') ) {
- print STDERR "Creating $path/done\n" if $opt{v};
+ mylog('debug',"Creating $path/done");
$sftp->mkdir('done');
if($sftp->error) {
# something is seriously wrong
#copy to local archive dir
if ( $opt{a} ) {
- print STDERR "Copying $tmpdir/$filename to archive dir $opt{a}\n"
- if $opt{v};
+ mylog('debug', "Copying $tmpdir/$filename to archive dir $opt{a}");
copy("$tmpdir/$filename", $opt{a});
+ #log too? what's -a all about anyway?
warn "failed to copy $tmpdir/$filename to $opt{a}: $!" if $!;
}
@hash{@fields} = $csv->fields();
if ( $hash{custnum} =~ /^cust/ ) {
# there appears to be a header row
- print STDERR "skipping header row\n" if $opt{v};
+ mylog('debug', "skipping header row");
next;
}
my $cust_main =
warn "customer #$hash{custnum} not found\n";
next;
}
- print STDERR "Found customer #$hash{custnum}: ".$cust_main->name."\n"
- if $opt{v};
+ mylog('debug',"Found customer #$hash{custnum}: ".$cust_main->name);
my $amount = sprintf('%.2f',$hash{quantity} * $hash{unit_price});
}
$charge_opt{classnum} = $classnum_of{$classname};
}
- print STDERR " Charging $hash{unit_price} * $hash{quantity}\n"
- if $opt{v};
+ mylog('debug', " Charging $hash{unit_price} * $hash{quantity}");
my $error = $cust_main->charge(\%charge_opt);
if ($error) {
warn "Error creating charge: $error" if $error;
$dbh->commit;
-if ($opt{v}) {
- print STDERR "
+mylog('debug', "
Finished!
Processed files: @$files
Created charges: $num_charges
E911 charges: $num_e911
E911 lines: $num_lines
Errors: $num_errors
-";
+");
+
+sub sftp_connect {
+ my ($host, $sftpuser, $port) = @_;
+ my $sftp;
+ my $connection_tries = 1;
+
+ while (1) {
+ mylog('info', "Connecting to $sftpuser\@$host try number $connection_tries...");
+ $sftp = Net::SFTP::Foreign->new(
+ host => $host,
+ user => $sftpuser,
+ port => $port,
+ # for now we don't support passwords. use authorized_keys.
+ timeout => 30,
+ #more => ($opt{v} ? '-v' : ''),
+ );
+
+ if ($sftp->error && $connection_tries < 1200) {
+ $connection_tries++;
+ mylog('error', "Connection failed to $sftpuser\@$host: ". $sftp->error.
+ ", trying again in 60 sec...");
+ sleep 60;
+ }
+ else { last; }
+ }
+
+ return $sftp;
+}
+
+our $log;
+sub mylog {
+ my( $level, $message ) = @_;
+ #warn "$message\n" if $opt{v};
+ print STDERR "$message\n" if $opt{v};
+ $log ||= FS::Log->new('freeside-ipifony-download');
+ $log->log(level=>$level, message=>$message);
}
=head1 NAME
=head1 OPTIONAL PARAMETERS
--v: Be verbose.
+-v: Be verbose; send debugging information to STDERR in addition to the
+internal log..
-q: Include the quantity and unit price in the charge description.
$sftp = Net::SFTP::Foreign->new( host => $host,
user => $username,
password => $password,
- timeout => 30,
+ timeout => 300,
);
last unless $sftp->error;
$ssh_retry -= 1;
$sftp = Net::SFTP::Foreign->new( host => $host,
user => $username,
password => $password,
- timeout => 30,
+ timeout => 300,
);
last unless $sftp->error;
$ssh_retry -= 1;
my $custnum = $cust_main->custnum;
+ my $paydate = $cust_main->paydate;
+
my $paymask = FS::Record->scalar_sql(qq[
- SELECT paymask FROM h_cust_main WHERE custnum = $custnum AND history_action = 'replace_old' AND paymask IS NOT NULL AND paymask != 'N/A (tokenized)' ORDER BY historynum desc LIMIT 1
+ SELECT paymask FROM h_cust_main WHERE custnum = $custnum AND history_action = 'replace_old' AND paymask IS NOT NULL AND paymask != 'N/A (tokenized)' AND paydate = '$paydate' ORDER BY historynum desc LIMIT 1
]);
+ next unless length($paymask);
+
#dbh->do(
print
qq[UPDATE cust_main SET paymask = '$paymask' WHERE custnum = $custnum;]
When an e-mail is delivered, the TO and FROM are printed to STDOUT.
The TO, FROM and MSG are saved to a file in $message_save_dir
+Open a saved .eml file with Mozilla Thunderbird (or other mail clients)
+to review e-mail with all html/pdf attachments
+
=cut
use strict;
$client->process || next;
- open my $fh, '>', $message_save_dir.'/'.time().'.txt'
+ open my $fh, '>', $message_save_dir.'/'.time().'.eml'
or die "error: $!";
for my $f (qw/TO FROM/) {
if (ref $client->{$f} eq 'ARRAY') {
print "$f: $_\n" for @{$client->{$f}};
- print $fh "$f: $_\n" for @{$client->{$f}};
+ # print $fh "$f: $_\n" for @{$client->{$f}};
} else {
print "$f: $client->{$f}\n";
- print $fh "$f: $client->{$f}\n";
+ # print $fh "$f: $client->{$f}\n";
}
}
- print $fh "\n\n$client->{MSG}\n";
+ print $fh "$client->{MSG}\n";
print "\n";
close $fh;
}
--- /dev/null
+#!/usr/bin/perl
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearchs qsearch);
+use FS::svc_broadband;
+
+my $user = shift or die &usage;
+my $dbh = adminsuidsetup($user);
+
+my $fcc_up_speed = "(select part_pkg_fcc_option.optionvalue from part_pkg_fcc_option where fccoptionname = 'broadband_upstream' and pkgpart = cust_pkg.pkgpart) AS fcc477_upstream";
+my $fcc_down_speed = "(select part_pkg_fcc_option.optionvalue from part_pkg_fcc_option where fccoptionname = 'broadband_downstream' and pkgpart = cust_pkg.pkgpart) AS fcc477_downstream";
+
+foreach my $rec (qsearch({
+ 'select' => 'svc_broadband.*, cust_svc.svcpart, cust_pkg.pkgpart, '.$fcc_up_speed.', '.$fcc_down_speed,
+ 'table' => 'svc_broadband',
+ 'addl_from' => 'LEFT JOIN cust_svc USING ( svcnum ) LEFT JOIN cust_pkg USING ( pkgnum )',
+})) {
+ $rec->{Hash}->{speed_test_up} = $rec->{Hash}->{speed_up} ? $rec->{Hash}->{speed_up} : "null";
+ $rec->{Hash}->{speed_test_down} = $rec->{Hash}->{speed_down} ? $rec->{Hash}->{speed_down} : "null";
+ $rec->{Hash}->{speed_up} = $rec->{Hash}->{fcc477_upstream} ? $rec->{Hash}->{fcc477_upstream} * 1000 : "null";
+ $rec->{Hash}->{speed_down} = $rec->{Hash}->{fcc477_downstream} ? $rec->{Hash}->{fcc477_downstream} * 1000 : "null";
+
+ my $sql = "UPDATE svc_broadband set
+ speed_up = $rec->{Hash}->{speed_up},
+ speed_down = $rec->{Hash}->{speed_down},
+ speed_test_up = $rec->{Hash}->{speed_test_up},
+ speed_test_down = $rec->{Hash}->{speed_test_down}
+ WHERE svcnum = $rec->{Hash}->{svcnum}";
+
+ warn "Fixing broadband service speeds for service ".$rec->{Hash}->{svcnum}."-".$rec->{Hash}->{description}."\n";
+
+ my $sth = $dbh->prepare($sql) or die $dbh->errstr;
+ $sth->execute or die $sth->errstr;
+
+}
+
+$dbh->commit;
+
+warn "Completed fixing broadband service speeds!\n";
+
+exit;
+
+=head1 NAME
+
+move_svc_broadband_speeds
+
+=head1 SYNOPSIS
+
+ move_svc_broadband_speeds.pl [ user ]
+
+=head1 DESCRIPTION
+
+Moves value for speed_down to speed_test_down, speed_up to speed_test_up,
+and sets speed_down, speed_up to matching fcc_477 speeds from package for
+all svc_broadband services.
+
+user: freeside username
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_broadband>
+
+=cut
\ No newline at end of file
my $columncount = $unitprices ? 5 : 3;
foreach my $section ( grep { !$summary || $_->{description} ne $finance_section } @sections ) {
if ($section->{'pretotal'} && !$summary) {
- $OUT .= '</table>' if $notfirst;
+ $OUT .= '</table>' if $notfirst++;
$OUT .=
'<table width="100%"><tr><td>'.
'<p align="right"><b><font size="+1">'.
'</td></tr>';
}
unless ($section->{'summarized'}) {
- $OUT .= '</table>' if ( $notfirst || $section->{'pretotal'} && !$summary );
+ if ( $notfirst || $section->{'pretotal'} && !$summary ) {
+ $OUT .= '</table>';
+ $notfirst = 1;
+ }
$OUT .= '<table><tr><td>';
$OUT .= '<p class="allcaps"><b>';
my $sectionhead;
'<p>';
$OUT .= '</td></tr>';
}
-
- $notfirst++;
-
}
my $style = 'border-top: 3px solid #000000;';
</table>
<br><br>
+<%=
+
+ my @location_summary_sections =
+ grep {
+ ref $_->{location}
+ && $_->{locationnum}
+ && $_->{description}
+ && $_->{description} ne $finance_section
+ } @sections;
+
+ if ( $multisection eq 'location' && scalar(@location_summary_sections) > 1 ) {
+
+ $OUT .= '
+ <hr>
+ <table width="100%">
+ <tr>
+ <td>
+ <p class="allcaps">
+ <b>'.emt('Summary Of New Charges By Location').'</b>
+ <p>
+ </td>
+ </tr>
+ </table>
+
+ <table class="invoice_longtable" cellspacing="0" width="100%">
+ <thead>
+ <tr>
+ <th></th>
+ <th align="left">'.emt('Location').'</th>
+ <th align="right">'.emt('Amount').'</th>
+ </tr>
+ </thead>
+ <tbody>
+ ';
+
+ for my $section (@location_summary_sections) {
+ next unless $section->{description};
+ $OUT .= '
+ <tr class="invoice_desc_more">
+ <td></td>
+ <td>'.$section->{description}.'</td>
+ <td align="right">'. $section->{subtotal} .'</td>
+ </tr>
+ ';
+ }
+
+ $OUT .= '
+ <tr class="invoice_desc"><td> </td><td> </td><td> </td></tr>
+ </tbody>
+ </table>
+ <br><br>
+ ';
+
+} %>
+
<%= length($summary)
? ''
: ( $smallernotes
<td align="right"><b><%= $dollar.$current_less_finance %></b></td>
</tr>
<tr><th colspan=2><br></th></tr>
- <tr>
- <td><b><u><br>Summary of Payments and Credits<br></u></b></td>
- <td></td>
- </tr>
- <tr>
- <td><b>Payments and Credits</b></td>
- <td align="right"><b>-<%= $dollar.$balance_adjustments %></b></td>
- </tr>
- <tr><th colspan=2><br></th></tr>
<tr><td colspan=2><br></td></tr>
<tr>
<td><b><u>Invoice Summary</u></b></td>
<td><b>New Charges</b></td>
<td align="right"><b><%= $dollar.$current_less_finance %></b></td>
</tr>
- <%=
- foreach my $section ( grep $_->{adjust_section}, @sections) {
- $OUT .= '<tr><td><b>'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '</b></td>';
- $OUT .= qq(<th align="right"><b>). $section->{'subtotal'}. "</b></th></tr>";
- }
- %>
- <tr>
- <td><b>Payments and Credits</b></td>
- <th align="right"><b>-<%= $dollar.sprintf('%.2f', $balance_adjustments) %></b></th>
- </tr>
+ <%= if ( $balance_adjustments > 0 ) {
+ $OUT .= "
+ <tr>
+ <td><b>Payments and Credits</b></td>
+ <th align='right'><b>-$dollar" . sprintf('%.2f', $balance_adjustments). "</b></th>
+ </tr>
+ ";
+ } %>
<tr>
<td><b>Total Amount Due</b></td>
<td align="right"><b><%= $dollar.sprintf('%.2f', $balance) %></b></td>
}\r
\r
--@]\r
+[@--\r
+\r
+ my @location_summary_sections =\r
+ grep {\r
+ ref $_->{location}\r
+ && $_->{locationnum}\r
+ && $_->{description}\r
+ && $_->{description} ne $finance_section\r
+ } @sections;\r
+ if ( $multisection eq 'location' && scalar(@location_summary_sections) > 1 ) {\r
+\r
+$OUT .= '\r
+ \hline\r
+ \section*{}\r
+ \captionsetup{singlelinecheck=false,justification=raggedright,font={Large,sc,bf}}\r
+ \ifthenelse{\equal{\thepage}{1}}{\setlength{\LTextracouponspace}{\extracouponspace}}{\setlength{\LTextracouponspace}{0pt}}\r
+\r
+ \begin{longtable}{cllllllr}\r
+ \caption*{ '. emt('Summary of New Charges by Location') .' }\r
+ \\\\\r
+\r
+ \hline\r
+ \rule{0pt}{2.5ex}\r
+ \makebox[1.4cm]{} &\r
+ \multicolumn{6}{l}{\r
+ \truncate{13.0cm}{\textbf{'. emt('Location') .'}}\r
+ } &\r
+ \makebox[1.6cm][r]{\textbf{'. emt('Amount') .'}} \\\\\r
+ \hline\r
+\r
+ \endfirsthead\r
+ \multicolumn{7}{r}{\rule{0pt}{2.5ex}'. emt('Continued from previous page') .'}\r
+ \\\r
+ \FShead\r
+ \endhead\r
+ \multicolumn{7}{r}{\rule{0pt}{2.5ex}'. emt('Continued on next page...') .'}\r
+ \\\r
+ \endfoot\r
+ \hline\r
+ \endlastfoot\r
+ \hline\r
+ ';\r
+\r
+ for my $section (@location_summary_sections) {\r
+ $OUT.= '\r
+ \rule{0pt}{2.5ex}\r
+ \makebox[1.4cm]{} &\r
+ \multicolumn{6}{l}{\r
+ \truncate{12.0cm}{\textbf{'. $section->{description} .'}}\r
+ } &\r
+ \makebox[1.6cm][r]{\textbf{'. $section->{subtotal} .'}} \\\\\r
+ ';\r
+ }\r
+\r
+ $OUT .= '\end{longtable}';\r
+ }\r
+--@]\r
+\r
\vfill\r
\begin{minipage}[t]{\textwidth}\r
[@-- length($summary)\r
\begin{tabular}{lr}
\hline
&\\
-\textbf{\underline{Summary of Previous Balance and Payments}} & \\
+\textbf{\underline{Summary of Previous Balance}} & \\
&\\
-\textbf{Previous Balance}&\textbf{\dollar[@-- $true_previous_balance --@]}\\
-\textbf{Payments}&\textbf{\dollar[@-- $balance_adjustments --@]}\\
-\cline{2-2}
-\textbf{Balance Outstanding}&\textbf{\dollar[@-- sprintf('%.2f', $true_previous_balance -$balance_adjustments) --@]}\\
+\textbf{Previous Balance}&\textbf{\dollar[@-- sprintf('%.2f', $true_previous_balance) --@]}\\
&\\
\hline
&\\
&\\
\textbf{\underline{Invoice Summary}} & \\
& \\
-\textbf{Previous Past Due Charges}&\textbf{\dollar[@-- sprintf('%.2f', $true_previous_balance - $balance_adjustments) --@]}\\
+\textbf{Previous Past Due Charges}&\textbf{\dollar[@-- sprintf('%.2f', $true_previous_balance) --@]}\\
\textbf{Finance charges on overdue amount}&\textbf{\dollar[@-- $finance_amount --@]}\\
\textbf{New Charges}&\textbf{\dollar[@-- $current_less_finance --@]}\\
-
[@--
- #false laziness w/invoice_htmlsummary and above
- foreach my $section ( grep $_->{adjust_section}, @sections ) {
- $OUT .= '\textbf{'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '}';
- $OUT .= '&\textbf{'. $section->{'subtotal'}. '}\\\\';
+ if ( $balance_adjustments > 0 ) {
+ $OUT .= '\textbf{Payments and Credits}&\textbf{-\dollar'.$balance_adjustments.'}\\\\'
}
--@]
-
\cline{2-2}
\textbf{Total Amount Due}&\textbf{\dollar[@-- sprintf('%.2f', $balance) --@]}\\
&\\
libmap-splat-perl, libdatetime-format-ical-perl, librest-client-perl,
libgeo-streetaddress-us-perl, libbusiness-onlinepayment-perl,
libnet-vitelity-perl (>= 0.05), libnet-sslglue-perl, libexpect-perl,
- libspreadsheet-parsexlsx-perl
+ libspreadsheet-parsexlsx-perl, libunicode-truncate-perl (>= 0.303-1)
Conflicts: libparams-classify-perl (>= 0.013-6)
Replaces: freeside (<<4)
Breaks: freeside (<<4)
<TD>
<SELECT NAME="month">
<%= for ( ( map "0$_", 1 .. 9 ), 10 .. 12 ) {
- $OUT .= '<OPTION'. ($_ == $month ? ' SELECTED' : ''). ">$_\n";
+ $OUT .= '<OPTION'. ($_ == $month ? ' SELECTED' : ''). " VALUE='$_'>$_\n";
} %>
</SELECT>
</TD>
<TD>
<SELECT NAME="year">
<%= my @a = localtime; for ( $a[5]+1900 .. $a[5]+1915 ) {
- $OUT .= '<OPTION'. ($_ == $year ? ' SELECTED' : ''). ">$_\n";
+ $OUT .= '<OPTION'. ($_ == $year ? ' SELECTED' : ''). " VALUE='$_'>$_\n";
} %>
</SELECT>
</TD>
$cgi->param('paycvv') =~ /^\s*(.{0,4})\s*$/ or die "illegal CVV2";
my $paycvv = $1;
- $cgi->param('month') =~ /^(\d{2})$/ or die "illegal month";
+ $cgi->param('month') =~ /^(\d{2})/ or die "illegal month";
my $month = $1;
- $cgi->param('year') =~ /^(\d{4})$/ or die "illegal year";
+ $cgi->param('year') =~ /^(\d{4})/ or die "illegal year";
my $year = $1;
$cgi->param('payname') =~ /^(.{0,80})$/ or die "illegal payname";
);
}
-
-
'',
],
'fields' => [ 'NetAddr',
- sub { my $block = shift;
- my $router = $block->router;
+ sub { my $b = shift;
+ my $router = $b->router;
my $result = '';
if ($router) {
- $result .= $router->routername. ' (';
- $result .= scalar($block->svc_broadband). ' services)';
+ $result .= $router->routername. ' ('.
+ scalar($b->svc_broadband). ' broadband, '.
+ scalar($b->svc_acct). ' account services)';
}
$result;
},
<% include( 'elements/browse.html',
'title' => 'Discounts',
'name' => 'discounts',
- 'menubar' => [ 'Add a new discount' =>
- $p.'edit/discount.html',
- ],
- 'query' => { 'table' => 'discount', },
+ 'menubar' => \@menubar,
+ 'query' => \%query,
+ 'order_by_sql' => { description => 'discountnum' },
'count_query' => 'SELECT COUNT(*) FROM discount',
'disableable' => 1,
'disabled_statuspos' => 1,
- 'header' => [ 'Name', 'Comment', 'Class', 'Discount', ],
+ 'header' => [ 'Name', 'Class', 'Discount', ],
'fields' => [ 'name',
- 'comment',
'classname',
'description',
],
- 'links' => [ $link,
- $link,
- ],
+ 'links' => \@links
)
%>
<%init>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
-my $link = [ "${p}edit/discount.html?", 'discountnum' ];
+my @links = (
+ [ "${p}edit/discount.html?", 'discountnum' ],
+ [ "${p}edit/discount_class.html?", 'classnum' ],
+);
+
+my %query = (
+ select => 'discount.*, discount_class.*',
+ table => 'discount',
+ addl_from => 'LEFT JOIN discount_class USING(classnum)',
+);
+
+my @menubar = (
+ 'Add a new discount' => $p.'edit/discount.html',
+ 'Discount classes' => $p.'browse/discount_class.html',
+);
</%init>
'action' => "${p}edit/bulk-cust_pkg.html?".
'pkgpart='.$part_pkg->pkgpart,
'actionlabel' => 'Change Packages',
- 'width' => 569,
+ 'width' => 960,
'height' => 210,
).' ]</FONT>',
'align' => 'left',
$align .= 'c';
$html_form = qq!<FORM ACTION="${p}edit/bulk-part_pkg.html" METHOD="POST">!;
$html_foot = include('/search/elements/checkbox-foot.html',
- submit => 'edit report classes', # for now it's only report classes
- ) . '</FORM>';
+ actions => [
+ { submit => 'edit report classes', },
+ { label => 'change customer packages',
+ onclick=> include('/elements/popup_link_onclick.html',
+ 'label' => 'change',
+ 'js_action' => qq{
+ '${p}edit/bulk-cust_pkg.html?' + \$('input[name=pkgpart]').serialize()
+ },
+ 'actionlabel' => 'Change customer packages',
+ 'width' => 960,
+ 'height' => 420,
+ )
+ },
+ ],
+ ).
+ '</FORM>';
}
my @menubar;
'A' => 'Automatically filled in from inventory',
'H' => 'Selected from hardware class',
'X' => 'Excluded',
+ 'P' => 'From package 477 information',
);
my %search;
configCell.innerHTML = <% $value |js_string %>;
% } elsif ( $type eq 'select-sub' && ! $i->multiple ) {
configCell.innerHTML =
- <% $conf->config($i->key, $agentnum) |js_string %> + ': ' +
+ <% $conf->exists($i->key, $agentnum) ? $conf->config($i->key, $agentnum) : '' |js_string %> + ': ' +
<% &{ $i->option_sub }( $conf->config($i->key, $agentnum) ) |js_string %>;
% } else {
//alert('unknown type <% $type %>');
or ( $type =~ /^select(-(sub|part_svc|part_pkg|pkg_class|agent))?$/
|| $i->multiple )
) {
- if ( scalar(@{[ $cgi->param($i->key.$n) ]}) ) {
+ if ( scalar(@{[ $cgi->param($i->key.$n) ]}) && $cgi->param($i->key.$n) ne '' ) {
my $error = &{$i->validate}([ $cgi->param($i->key.$n) ], $n) if $i->validate;
push @error, $error if $error;
$conf->set($i->key, join("\n", @{[ $cgi->param($i->key.$n) ]} ), $agentnum);
<OPTION VALUE="<% $payment_gateway->gatewaynum %>"><% $payment_gateway->gateway_module %> (<% $payment_gateway->gateway_username %>)
% }
-
</SELECT>
-<BR><BR>
+<BR>
+
+<INPUT TYPE="checkbox" NAME="cardtype" VALUE="ACH"> for ACH only.
+<BR>
+<BR>
<INPUT TYPE="submit" VALUE="Add gateway override">
</FORM>
}
</SCRIPT>
<FORM NAME="OneTrueForm">
-% #false laziness with bulk-cust_svc.html
-% $cgi->param('pkgpart') =~ /^(\d+)$/
-% or die "illegal pkgpart: ". $cgi->param('pkgpart');
-%
-% my $old_pkgpart = $1;
-% my $src_part_pkg = qsearchs('part_pkg', { 'pkgpart' => $old_pkgpart } )
-% or die "unknown pkgpart: $old_pkgpart";
-%
+% foreach my $src_part_pkg (@src_part_pkg) {
+ <INPUT NAME="old_pkgpart" TYPE="hidden" VALUE="<% $src_part_pkg->pkgpart %>">
+ Change <B><% $src_part_pkg->pkg_comment |h %></B><BR>
+% }
-<INPUT NAME="old_pkgpart" TYPE="hidden" VALUE="<% $old_pkgpart %>">
-Change <B><% $src_part_pkg->pkg_comment %></B><BR>
-
+<BR>
to new package definition
<SELECT NAME="new_pkgpart">
% foreach my $dest_part_pkg ( qsearch('part_pkg', { 'disabled' => '' } ) ) {
- <OPTION VALUE="<% $dest_part_pkg->pkgpart %>"><% $dest_part_pkg->pkgpart %>: <% $dest_part_pkg->pkg %>
+ <OPTION VALUE="<% $dest_part_pkg->pkgpart %>"><% $dest_part_pkg->pkgpart %>: <% $dest_part_pkg->pkg |h %>
% }
</SELECT>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+my @src_part_pkg = ();
+foreach my $pkgpart ( $cgi->multi_param('pkgpart') ) {
+
+ $pkgpart =~ /^(\d+)$/
+ or die "illegal pkgpart: $pkgpart";
+
+ my $old_pkgpart = $1;
+ my $src_part_pkg = qsearchs('part_pkg', { 'pkgpart' => $old_pkgpart } )
+ or die "unknown pkgpart: $old_pkgpart";
+
+ push @src_part_pkg, $src_part_pkg;
+
+}
+
</%init>
+<%doc>
+
+Hmm, this is now entirely redundant with edit/cust_main/contacts_new.html, and
+this one isn't being maintained well. :/
+
+</%doc>
+
+<SCRIPT>
+ function checkPasswordValidation(fieldid) {
+ var validationResult = document.getElementById(fieldid+'_result').innerHTML;
+ if (validationResult.match(/Password valid!/)) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+</SCRIPT>
+
+<& '/elements/validate_password_js.html', &>
+
<& elements/edit.html,
- 'name_singular' => 'customer contacts', #yes, we're editing all of them
- 'table' => 'cust_main',
- 'post_url' => popurl(1). 'process/cust_main-contacts.html',
- 'no_pkey_display' => 1,
- 'labels' => {
- 'contactnum' => ' ', #'Contact',
- #'locationnum' => ' ',
- },
- 'fields' => [
+ 'name_singular' => 'customer contacts', #yes, we're editing all of them
+ 'table' => 'cust_main',
+ 'post_url' => popurl(1). 'process/cust_main-contacts.html',
+ 'no_pkey_display' => 1,
+ 'submit_id' => 'submit',
+ 'labels' => {
+ 'contactnum' => ' ', #'Contact',
+ #'locationnum' => ' ',
+ },
+ 'fields' => [
{ 'field' => 'contactnum',
'type' => 'contact',
'colspan' => 6,
'm2_error_callback' => $m2_error_callback,
},
],
- #'new_callback' => $new_callback,
- #'edit_callback' => $edit_callback,
- #'error_callback' => $error_callback,
- 'agent_virt' => 1,
- 'menubar' => [], #remove "view all" link
+ #'new_callback' => $new_callback,
+ #'edit_callback' => $edit_callback,
+ #'error_callback' => $error_callback,
+ 'agent_virt' => 1,
+ 'html_table_class' => 'fsinnerbox',
+ 'menubar' => [], #remove "view all" link
#XXX it would be nice if this could instead be after the error but before
# the table
my $curuser = $FS::CurrentUser::CurrentUser;
my $conf = new FS::Conf;
+if ( $cgi->param('redirect') ) {
+ my $session = $cgi->param('redirect');
+ my $pref = $curuser->option("redirect$session");
+ die "unknown redirect session $session\n" unless length($pref);
+ $cgi = new CGI($pref);
+}
+
my $custnum;
if ( $cgi->param('error') ) {
$custnum = scalar($cgi->param('custnum'));
my($cgi, $object) = @_;
#process_o2m fields in process/cust_main-contacts.html
- my @fields = qw( first last title comment );
+ my @fields = FS::contact->cgi_contact_fields;
my @gfields = ( '', map "_$_", @fields );
map {
</SCRIPT>
-<& cust_main/contacts_new.html, 'cust_main'=>$cust_main, &>
+<& cust_main/contacts_new.html, 'cust_main'=>$cust_main, 'submit_id'=>'submitButton', &>
%# billing info
<& cust_main/billing.html, $cust_main,
$cust_main->paycvv($paycvv);
}
@invoicing_list = $cust_main->invoicing_list;
- $ss = $conf->exists('unmask_ss') ? $cust_main->ss : $cust_main->masked('ss');
+ $ss = $cust_main->masked('ss');
$stateid = $cust_main->masked('stateid');
} else { #new customer
+<SCRIPT>
+ function checkPasswordValidation(fieldid) {
+ var validationResult = document.getElementById(fieldid+'_result').innerHTML;
+ if (validationResult.match(/Password valid!/)) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+</SCRIPT>
+
+<& '/elements/validate_password_js.html', &>
+
<DIV ID="contacts_div" STYLE="display:<% $display %>">
<BR>
<FONT CLASS="fsinnerbox-title">Contacts</FONT>
'embed' => $opt{cust_main},
'table' => 'cust_main',
'agent_virt' => 1,
+ 'submit_id' => $opt{submit_id},
'html_table_class' => 'fsinnerbox',
'labels' => { 'contactnum' => '', #'Contact',
#'locationnum' => ' ',
<%def .namepart>
-% my ($field, $value, $label, $extra) = @_;
+% my ($field, $value, $label, $extra, $unmask_field) = @_;
<DIV STYLE="display: inline-block" ID="<% $field %>_input">
<INPUT TYPE="text" NAME="<% $field %>" VALUE="<% $value |h %>" <%$extra%>>
+% if (
+% $value
+% && ref $unmask_field
+% && $FS::CurrentUser::CurrentUser->access_right( $unmask_field->{access_right} )
+% ) {
+ <& /elements/link-replace_element_text.html, {
+ target_id => $unmask_field->{target_id},
+ replace_text => $unmask_field->{replace_text},
+ } &>
+% }
<BR><FONT SIZE="-1" COLOR="#333333"><% emt($label) %></FONT>
</DIV>
</%def>
<& .namepart, 'first', $cust_main->first, 'First' &>
% if ( $conf->exists('show_ss') ) {
- <& .namepart, 'ss', $ss, 'SS#', "SIZE=11" &>
+ <& .namepart, 'ss', $ss, 'SS#', "SIZE=11 ID='ss'", {
+ target_id => 'ss',
+ replace_text => $cust_main->ss,
+ access_right => 'Unmask customer SSN',
+ } &>
% } else {
<INPUT TYPE="hidden" NAME="ss" VALUE="<% $ss %>">
% }
my $conf = FS::Conf->new;
my $ss;
-if ( $cgi->param('error') or $conf->exists('unmask_ss') ) {
+if ( $cgi->param('error') ) {
$ss = $cust_main->ss;
} else {
$ss = $cust_main->masked('ss');
% if ( $conf->exists('show_stateid') ) {
<TR>
<TH ALIGN="right"><% $stateid_label %></TH>
- <TD><INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>" SIZE=12></TD>
+ <TD>
+ <INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>" SIZE=12 ID="stateid">
+% if ( $stateid && $FS::CurrentUser::CurrentUser->access_right( 'Unmask customer DL' )) {
+ <& /elements/link-replace_element_text.html, {target_id => 'stateid', replace_text => $cust_main->stateid} &>
+% }
+ </TD>
<TD><& /elements/select-state.html,
state => $cust_main->stateid_state,
country => $cust_main->country, # how does this work on new customer?
% }
<BR>Payment
- <% ntable("#cccccc", 2) %>
+ <TABLE class="fsinnerbox">
<TR>
<TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">$<% $cust_pay->paid %></TD>
<BR>Refund
-<% ntable("#cccccc", 2) %>
+
+<TABLE class="fsinnerbox">
<TR>
<TD ALIGN="right">Date</TD>
<TD ALIGN="right">Check #</TD>
<TD COLSPAN=2><INPUT TYPE="text" NAME="payinfo" VALUE="<% $payinfo %>" SIZE=10></TD>
</TR>
+ </TABLE>
% }
-% elsif ($payby eq 'CHEK') {
+% elsif ($payby eq 'CHEK' || $payby eq 'CARD') {
%
+<SCRIPT TYPE="text/javascript">
+ function cust_payby_changed (what) {
+ var custpaybynum = what.options[what.selectedIndex].value
+ if ( custpaybynum == '' || custpaybynum == '0' ) {
+ //what.form.payinfo.disabled = false;
+ $('#cust_payby').slideDown();
+ } else {
+ //what.form.payinfo.value = '';
+ //what.form.payinfo.disabled = true;
+ $('#cust_payby').slideUp();
+ }
+ }
+</SCRIPT>
% my @cust_payby = ();
% if ( $payby eq 'CARD' ) {
% @cust_payby = $cust_main->cust_payby('CARD','DCRD');
% my $custpaybynum = length(scalar($cgi->param('custpaybynum')))
% ? scalar($cgi->param('custpaybynum'))
% : scalar(@cust_payby) && $cust_payby[0]->custpaybynum;
-<& /elements/tr-select-cust_payby.html,
+
+% if ($cust_pay) {
+ <INPUT TYPE="hidden" NAME="payinfo" VALUE="<% $payinfo %>" SIZE=10>
+% }
+% else {
+ <& /elements/tr-select-cust_payby.html,
'cust_payby' => \@cust_payby,
'curr_value' => $custpaybynum,
'onchange' => 'cust_payby_changed(this)',
-&>
- <INPUT TYPE="hidden" NAME="batch" VALUE="1">
+ &>
+% }
+
+% if ( $conf->exists("batch-enable")
+% || grep $payby eq $_, $conf->config('batch-enable_payby')
+% ) {
+% if ( grep $payby eq $_, $conf->config('realtime-disable_payby') ) {
+ <INPUT TYPE="hidden" NAME="batch" VALUE="1">
+% } else {
+ <TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="batch" VALUE="1" ID="batch" <% ($batchnum || $batch) ? 'checked' : '' %> ></TD>
+ <TH ALIGN="left"> <% mt('Add to current batch') |h %></TH>
+ </TR>
+% }
+% }
+
+ </TABLE>
+<P>
+
+% if ( !$cust_pay ) {
+<DIV ID="cust_payby"
+ <% $custpaybynum ? 'STYLE="display:none"'
+ : ''
+ %>
+>
+<TABLE class="fsinnerbox">
+
+ <& /elements/cust_payby_new.html,
+ 'cust_payby' => \@cust_payby,
+ 'curr_value' => $custpaybynum,
+ &>
+
+</TABLE>
+</DIV>
+% } # end if cust_pay
+
% } else {
<INPUT TYPE="hidden" NAME="payinfo" VALUE="">
+ </TABLE>
% }
+<P>
+<TABLE class="fsinnerbox">
<& /elements/tr-select-reason.html,
'field' => 'reasonnum',
'reason_class' => 'F',
my $payinfo = $cgi->param('payinfo');
my $reason = $cgi->param('reason');
my $link = $cgi->param('popup') ? 'popup' : '';
+my $batch = $cgi->param('batch');
die "access denied"
unless $FS::CurrentUser::CurrentUser->refund_access_right($payby);
-my( $paynum, $cust_pay ) = ( '', '' );
+my( $paynum, $cust_pay, $batchnum ) = ( '', '', '' );
if ( $cgi->param('paynum') =~ /^(\d+)$/ ) {
$paynum = $1;
$cust_pay = qsearchs('cust_pay', { paynum=>$paynum } )
or die "unknown payment # $paynum";
$refund ||= $cust_pay->unrefunded;
+ $batchnum = $cust_pay->batchnum;
if ( $custnum ) {
die "payment # $paynum is not for specified customer # $custnum"
unless $custnum == $cust_pay->custnum;
#we're in a popup (no title/menu/searchboxes)
'popup' => 1,
+ #if you need to access the submit button
+ 'submit_id' => 'mysubmitbuttonid',
+
#we're embedded (rows only: no header at all, no html_init, no error
# display, no <FORM>, no hidden fields for table name or primary key, no
# display of primary key, no submit button, no html_foot, no footer)
%
% my $layer_prefix_on = '';
%
+% my $submitid = $opt{submit_id} ? $opt{submit_id} : '';
+%
% my $include_sub = sub {
% my %opt = @_;
%
% 'field' => "$field$fieldnum",
% 'id' => "$field$fieldnum", #separate?
% 'label_id' => $field."_label$fieldnum", #don't want field0_label0...
+% 'submit_id' => $submitid,
% %include_common,
% %opt,
% );
var newrow = <% include(@layer_opt, html_only=>1) |js_string %>;
% #until the rest have html/js_only
-% if ( ($type eq 'selectlayers') || ($type eq 'selectlayersx') || ($type =~ /^select-cgp_rule_/) ) {
+% if ( ($type eq 'selectlayers') || ($type eq 'selectlayersx') || ($type =~ /^select-cgp_rule_/) || ($type eq 'contact') ) {
var newfunc = <% include(@layer_opt, js_only=>1) |js_string %>;
% } else {
var newfunc = '';
</script>
<table bgcolor="#cccccc" border=0 cellspacing=3>
-<TR><TH>Object ID</TH></TR>
+<TR><TH>Object Name</TH><TH>Object ID</TH></TR>
<TR id="broadband_snmp_get_template">
<TD>
+ <INPUT NAME="oid_name" ID="oid_name" SIZE="25">
+ </TD>
+ <TD>
<INPUT NAME="oid" ID="oid" SIZE="54">
<INPUT TYPE="button" VALUE="..." ID="openselector" onclick="open_select_mib_get(this)">
</TD>
</TR>
<& /elements/auto-table.html,
template_row => 'broadband_snmp_get_template',
- fieldorder => ['oid'],
+ fieldorder => ['oid_name','oid'],
data => \@data,
table => 'snmp',
&>
-<INPUT TYPE="hidden" NAME="multi_options" VALUE="snmp_oid">
+<INPUT TYPE="hidden" NAME="multi_options" VALUE="snmp_oid,snmp_oid_name">
<& foot.html, %opt &>
<%init>
my %opt = @_;
}
my @oids = split("\n", $part_export->option('snmp_oid'));
+my @oid_names = split("\n", $part_export->option('snmp_oid_name'));
my @data;
while (@oids) {
my @thisrow = (shift(@oids));
- push @data, \@thisrow if grep length($_), @thisrow;
+ my $name = shift(@oid_names);
+ push @data, [$name, \@thisrow] if grep length($_), @thisrow;
}
my $popup_name = 'popup-'.time."-$$-".rand() * 2**32;
# don't allow the 'inventory' flags (M, A) to be chosen for
# fields that aren't free-text
my $inv_sub = sub { $_[0]->{disable_inventory} || $_[0]->{type} ne 'text' };
+
tie my %flag, 'Tie::IxHash',
'' => { 'desc' => 'No default', 'condition' => sub { 0 } },
'D' => { 'desc' => 'Default',
'H' => { 'desc' => 'Select from hardware class',
'condition' => sub { $_[0]->{type} ne 'select-hardware' },
},
+ 'P' => { 'desc' => 'From package FCC 477 information',
+ 'condition' => sub { $_[0]->{type} ne 'fcc_477_speed' }, # get values from package fcc 477 information
+ },
'X' => { 'desc' => 'Excluded',
'condition' => sub { 1 }, # obsolete
},
% $mode = 'hardware';
% $multiple = 0;
% }
+%
+% if ( $def->{'type'} eq 'fcc_477_speed' ) {
+% if ($field eq 'speed_up') {
+ <SPAN ID="<% $name %>_select">
+ upstream speed
+ <INPUT TYPE="hidden" ID="<% $name %>_select" NAME="<% $name %>_classnum" VALUE="up">
+ </SPAN>
+% } elsif ($field eq 'speed_down') {
+ <SPAN ID="<% $name %>_select">
+ downstream speed
+ <INPUT TYPE="hidden" ID="<% $name %>_select" NAME="<% $name %>_classnum" VALUE="down">
+ </SPAN>
+% }
+% } else {
<& /elements/select-table.html,
'field' => $name.'_classnum',
'id' => $name.'_select',
'empty_label' => "Select $mode class",
'multiple' => $multiple,
&>
+% }
% }
</TD>
<TD>
];
} # shouldn't this be enforced for all 'S' fields?
+ elsif ( $flag eq 'P' ) { #form fcc_477 values
+ $f->{type} = 'fixed';
+ my $cust_pkg = FS::Record::qsearchs({
+ 'table' => 'cust_pkg',
+ 'hashref' => { 'pkgnum' => $object->{Hash}->{pkgnum} }
+ });
+ my $fcc_record = $cust_pkg->fcc_477_record('broadband_'.$columndef->columnvalue.'stream') if $cust_pkg;
+ $f->{'value'} = $fcc_record->{Hash}->{optionvalue} ? $fcc_record->{Hash}->{optionvalue} * 1000 : '';
+ } # end 477 values
+
if ( $f->{'type'} =~ /^select-svc/ )
{
$f->{'include_opt_callback'} =
$html .= ' CHECKED' if $part_export->no_suspend eq 'Y';
$html .= '></TD></TR>';
+ foreach my $script ( keys %{$exports->{$layer}{scripts}} ) {
+ $html .= '<TR><TD ALIGN="left" COLSPAN=2>' .
+ include('/elements/progress-init.html',
+ $part_export->exporttype,
+ [ $script.'_exportnum', $script.'_script' ],
+ rooturl().'view/svc_export/run_script.cgi',
+ rooturl().'edit/part_export.cgi?'.$part_export->{Hash}->{exportnum},
+ $script,
+ ) .
+ '<INPUT TYPE="hidden" NAME="'.$script.'_exportnum" VALUE="'.$part_export->{Hash}->{exportnum}.'">
+ <INPUT TYPE="hidden" NAME="'.$script.'_script" VALUE="'.$script.'">
+ <A HREF="#" onClick="'.$script.'process();">'.$exports->{$layer}{scripts}{$script}->{html_label}.'</A></TD></TR>';
+ }
+
$html .= '</TABLE>';
# false laziness with config_element above
select.multiple = false;
}
}
- } else if ( newflag == 'M' || newflag == 'A' || newflag == 'H' ) {
+ } else if ( newflag == 'M' || newflag == 'A' || newflag == 'H' || newflag == 'P' ) {
// these all require a class selection
if ( select ) {
select.disabled = false;
}
var required = document.getElementById(layer + '__' + field + '_required');
if (required && !required.disabledinit) {
- if (newflag == "F") {
+ if (newflag == "F" || newflag =="P") {
required.checked = false;
required.disabled = true;
} else {
'target_table' => 'access_group',
},
'precheck_callback' => \&precheck_callback,
- #'post_new_object_callback' => \&post_new_object_callback,
+ 'post_new_object_callback' => \&post_new_object_callback,
'noerror_callback' => \&noerror_callback,
)
%>
return '';
}
-#sub post_new_object_callback {
-# my( $cgi, $access_user ) = @_;
-#
-# if ( length($cgi->param('_password')) ) {
-# my $password = scalar($cgi->param('_password'));
-# my $error = $access_user->is_password_allowed($password);
-# #XXX and then bubble the error back up to the UI
-# }
-#}
+sub post_new_object_callback {
+ my( $cgi, $access_user ) = @_;
+
+ return '' unless length($cgi->param('_password'));
+
+ my $password = scalar($cgi->param('_password'));
+ my $error = $access_user->is_password_allowed($password);
+ return $error if $error;
+
+ $access_user->change_password_fields($password);
+ '';
+}
sub noerror_callback {
my( $cgi, $access_user ) = @_;
- if ( length($cgi->param('_password')) ) {
- my $password = scalar($cgi->param('_password'));
- $access_user->change_password($password);
- }
-
#handle installer checkbox
my @sched_item = $access_user->sched_item;
my $sched_item = $sched_item[0];
</%doc>
<% include('elements/process.html',
'table' => 'cust_main',
- 'error_redirect' => popurl(3). 'edit/cust_main-contacts.html?',
+ 'error_redirect' => popurl(3). 'edit/cust_main-contacts.html',
'agent_virt' => 1,
'skip_process' => 1, #we don't want to make any changes to cust_main
'precheck_callback' => $precheck_callback,
'CHEK' => 'electronic check (ACH)',
);
-my( $cust_payby, $payinfo, $paycvv, $month, $year, $payname );
+my( $cust_pay, $cust_payby, $payinfo, $paycvv, $month, $year, $payname );
my $paymask = '';
if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) {
$paycvv = $cust_payby->paycvv; # pass it if we got it, running a transaction will clear it
( $month, $year ) = $cust_payby->paydate_mon_year;
$payname = $cust_payby->payname;
+ $cgi->param(-name=>"paytype", -value=>$cust_payby->paytype) unless $cgi->param("paytype");
+
+} elsif ( $cgi->param('paynum') > 0) {
+
+ $cust_pay = qsearchs({
+ 'table' => 'cust_pay',
+ 'hashref' => { 'paynum' => $cgi->param('paynum') },
+ 'select' => 'cust_pay.*, cust_pay_batch.payname ',
+ 'addl_from' => "left join cust_pay_batch on cust_pay_batch.batchnum = cust_pay.batchnum and cust_pay_batch.custnum = $custnum ",
+ });
+ $payinfo = $cust_pay->payinfo;
+ $payname = $cust_pay->payname;
} else {
my $refund = "$1$2";
$cgi->param('paynum') =~ /^(\d*)$/ or die "Illegal paynum!";
my $paynum = $1;
- my $paydate = $cgi->param('exp_year'). '-'. $cgi->param('exp_month'). '-01';
- $options{'paydate'} = $paydate if $paydate =~ /^\d{2,4}-\d{1,2}-01$/;
+ my $paydate;
+ unless ($paynum) {
+ if ($cust_payby->paydate) { $paydate = "$year-$month-01"; }
+ else { $paydate = "2037-12-01"; }
+ }
if ( $cgi->param('batch') ) {
-
+ $paydate = "2037-12-01" unless $paydate;
$error ||= $cust_main->batch_card(
'payby' => $payby,
'amount' => $refund,
'payinfo' => $payinfo,
- 'paydate' => "$year-$month-01",
+ 'paydate' => $paydate,
'payname' => $payname,
'paycode' => 'C',
map { $_ => scalar($cgi->param($_)) }
);
errorpage($error) if $error;
-#### post refund #####
my %hash = map {
$_, scalar($cgi->param($_))
} fields('cust_refund');
- $paynum = $cgi->param('paynum');
- $paynum =~ /^(\d*)$/ or die "Illegal paynum!";
- if ($paynum) {
- my $cust_pay = qsearchs('cust_pay',{ 'paynum' => $paynum });
- die "Could not find paynum $paynum" unless $cust_pay;
- $error = $cust_pay->refund(\%hash);
- } else {
- my $new = new FS::cust_refund ( \%hash );
- $error = $new->insert;
- }
- # if not a batch refund run realtime.
+
+ my $new = new FS::cust_refund ( { 'paynum' => $paynum,
+ %hash,
+ } );
+ $error = $new->insert;
+
+ # if not a batch refund run realtime.
} else {
$error = $cust_main->realtime_refund_bop( $bop, 'amount' => $refund,
'paynum' => $paynum,
'reasonnum' => scalar($cgi->param('reasonnum')),
%options );
}
-} else {
+} else { # run cash refund.
my %hash = map {
$_, scalar($cgi->param($_))
} fields('cust_refund');
'precheck_callback' => sub { my( $cgi ) = @_; },
#after the new object is created
+ #return an error string or empty for no error
'post_new_object_callback' => sub { my( $cgi, $object ) = @_; },
+ #run right before replacing (not run for inserts)
+ 'edit_callback' => sub { my( $new, $old ) = @_; },
+
#after everything's inserted
'noerror_callback' => sub { my( $cgi, $object ) = @_; },
# for use with tables that are FS::option_Common (among other things)
'args_callback' => sub { my( $cgi, $object ) = @_; },
+ # if no errors after package insert or replace will update services attached to package.
+ 'update_svc' => sub { my( $cgi, $object ) = @_; },
+
'debug' => 1, #turns on debugging output
#agent virtualization
}
if ( $opt{'post_new_object_callback'} ) {
- &{ $opt{'post_new_object_callback'} }( $cgi, $new );
+ $error ||= &{ $opt{'post_new_object_callback'} }( $cgi, $new );
}
if ( $opt{'agent_virt'} ) {
}
}
+ if ( !$error and $opt{'update_svc'} ) {
+ my @args = ();
+ @args = &{ $opt{'args_callback'} }( $cgi, $new ) if $opt{'args_callback'};
+ $error = &{ $opt{'update_svc'} }( $cgi, $new, @args );
+ }
+
if ( $error ) {
$cgi->param('error', $error);
}
+if ($class eq "FS::tower") {
+ foreach my $part_svc_broadband_export ( FS::tower_sector->part_export_svc_broadband ) {
+ if ($part_svc_broadband_export and $part_svc_broadband_export->can('export_tower_sector')) {
+ $error = $part_svc_broadband_export->export_tower_sector($new);
+ }
+ }
+}
+
# set up redirect URLs
my $redirect;
'edit_ext' => 'cgi',
'precheck_callback' => $precheck_callback,
'args_callback' => $args_callback,
+ 'update_svc' => $update_svc,
'process_locale' => 'pkg',
'process_m2m' => \@process_m2m,
'process_o2m' => \@process_o2m,
};
+## update services upon package change.
+my $update_svc = sub {
+ my $cgi = shift @_;
+ my $new = shift @_;
+ my %args = @_;
+ my $error;
+
+ my @svcs = $new->pkg_svc();
+
+## update broadband services getting their up and down speeds from package fcc_477 options
+ foreach my $svc_part(@svcs) {
+ my @part_svc_column = qsearch('part_svc_column',{ 'svcpart' => $svc_part->{Hash}->{svcpart}, 'columnflag' => 'P' });
+
+ if ($svc_part->{Hash}->{svcdb} eq "svc_broadband" && (keys %{ $args{fcc_options} }) && @part_svc_column ) {
+ ## find provisioned services to update
+ my @svc_svcdb = qsearch({
+ 'table' => 'svc_broadband',
+ 'select' => 'svc_broadband.*, cust_svc.svcpart',
+ 'addl_from' => 'LEFT JOIN cust_svc USING (svcnum) LEFT JOIN cust_pkg USING (pkgnum)',
+ 'extra_sql' => " WHERE cust_svc.svcpart = '".$svc_part->{Hash}->{svcpart}."' AND cust_pkg.pkgpart = '".$svc_part->{Hash}->{pkgpart}."'",
+ });
+ foreach my $svc (@svc_svcdb) {
+ next if ($svc->{Hash}->{speed_down} == $args{fcc_options}->{broadband_downstream} * 1000 && $svc->{Hash}->{speed_up} == $args{fcc_options}->{broadband_upstream} * 1000);
+ $svc->{Hash}->{speed_down} = $args{fcc_options}->{broadband_downstream} * 1000;
+ $svc->{Hash}->{speed_up} = $args{fcc_options}->{broadband_upstream} * 1000;
+ $error = $svc->replace();
+ }
+ }
+ }
+ return $error;
+};
+
my $redirect_callback = sub {
#my( $cgi, $new ) = @_;
return '' unless $custnum;
$obj->usernum( $FS::CurrentUser::CurrentUser->usernum );
# if this would change it from its existing owner, replace_check
# will refuse
+
+ ''; #no error
};
</%init>
sectorname ip_addr height freq_mhz direction width
downtilt v_width db_high db_low power line_loss
antenna_gain hardware_typenum
- sector_range
+ sector_range up_rate_limit down_rate_limit
)],
},
&>
;
my @fields = (
- qw( description speed_down speed_up ),
+ qw( description speed_down speed_up speed_test_down speed_test_up speed_test_latency),
{ field=>'sectornum', type=>'select-tower_sector', },
{ field=>'routernum', type=>'select-router_block_ip',
include_opt_callback => sub {
my $columndef = $part_svc->part_svc_column($fieldref->{'field'});
if ($fieldref->{field} eq 'usergroup' && $columndef->columnflag eq 'F') {
-
$fieldref->{'formatted_value'} =
[ $object->radius_groups('long_description') ];
}
'altitude',
'height',
'veg_height',
+ 'up_rate_limit',
+ 'down_rate_limit',
# { field => 'sectornum',
# type => 'tower_sector',
# o2m_table => 'tower_sector',
'height' => 'Tower height (feet)',
'veg_height' => 'Vegetation height (feet)',
'color' => 'Color',
+ 'up_rate_limit' => 'Up Rate Limit(Kbps)',
+ 'down_rate_limit' => 'Down Rate Limit(Kbps)',
},
&>
<%init>
my ($cgi, $object) = @_;
my @fields = qw(
- sectorname ip_addr height freq_mhz direction width tilt v_width db_high db_low sector_range
+ sectorname ip_addr height freq_mhz direction width tilt v_width db_high db_low sector_range up_rate_limit down_rate_limit
);
map {
if (obj.error) {
var row = document.createElement('tr');
var cell = document.createElement('td');
- cell.colSpan = '2';
+ cell.colSpan = '3';
cell.innerHTML = obj['error'];
row.appendChild(cell);
table.appendChild(row);
var value = obj['values'][j];
var label = (obj['values'].length > 1) ? (value[0] + '.' + value[1]) : obj['label'];
var cell = document.createElement('td');
+ cell.innerHTML = obj['name'];
+ row.appendChild(cell);
+ cell = document.createElement('td');
cell.innerHTML = label;
row.appendChild(cell);
cell = document.createElement('td');
% if (!$opt{'no_label_display'}) {
<A ID="<%$pre%>link" HREF="javascript:void(0)" onclick="<%$pre%>toggle(true)">(<% emt( $change_title ) %>)</A>
% }
-<DIV ID="<%$pre%>form" CLASS="passwordbox">
+<DIV ID="<%$pre%>div" CLASS="passwordbox">
% if (!$opt{'noformtag'}) {
- <FORM METHOD="POST" ACTION="<%$fsurl%>misc/process/change-password.html" onsubmit="return checkPasswordValidation()">
+ <FORM ID="<%$pre%>form" METHOD="POST" ACTION="<%$fsurl%>misc/process/change-password.html" onsubmit="return <%$pre%>checkPasswordValidation()">
% }
<% $change_id_input %>
if (clear) {
document.getElementById('<%$pre%>password').value = '';
document.getElementById('<%$pre%>password_result').innerHTML = '';
-% if ($opt{'contact_num'}) {
- document.getElementById('<% $opt{'pre_pwd_field_label'} %>selfservice_access').value = 'Y';
-% }
-}
- document.getElementById('<%$pre%>form').style.display =
+ document.getElementById('<%$change_button_id%>').disabled = true;
+ }
+ document.getElementById('<%$pre%>div').style.display =
toggle ? 'inline-block' : 'none';
% if (!$opt{'no_label_display'}) {
document.getElementById('<%$pre%>link').style.display =
% }
}
-function checkPasswordValidation() {
+function <%$pre%>checkPasswordValidation(resultId) {
var validationResult = document.getElementById('<%$pre%>password_result').innerHTML;
if (validationResult.match(/Password valid!/)) {
return true;
}
elsif ($opt{'contact_num'}) {
$change_id_input = '
- <INPUT TYPE="hidden" NAME="'.$opt{'pre_pwd_field_label'}.'contactnum" VALUE="' . $opt{'contact_num'} . '">
- <INPUT TYPE="hidden" NAME="'.$opt{'pre_pwd_field_label'}.'custnum" VALUE="' . $opt{'custnum'} . '">
+ <INPUT TYPE="hidden" NAME="contactnum" VALUE="' . $opt{'contact_num'} . '">
+ <INPUT TYPE="hidden" NAME="custnum" VALUE="' . $opt{'custnum'} . '">
';
$pre .= $opt{'pre_pwd_field_label'};
}
>
% unless ( $opt{'disable_empty'} ) {
- <OPTION VALUE="" <% $opt{city} eq '' ? 'SELECTED' : '' %>><% $opt{empty_label} %>
+ <OPTION VALUE="" <% $opt{city} eq '' ? 'SELECTED' : '' %>><% $opt{empty_label} %></OPTION>
% }
% foreach my $city ( @cities ) {
<OPTION VALUE="<% $city |h %>"
<% $city eq $opt{city} ? 'SELECTED' : '' %>
- ><% $city eq $opt{empty_data_value} ? $opt{empty_data_label} : $city %>
+ ><% $city eq $opt{empty_data_value} ? $opt{empty_data_label} : $city %></OPTION>
% }
-% unless ( $opt{'js_only'} ) {
+% if ( $opt{'js_only'} ) {
+<% $js %>
+% } else {
<INPUT TYPE="hidden" NAME="<%$name%>" ID="<%$id%>" VALUE="<% $curr_value %>">
% }
% } elsif ( $field eq 'emailaddress' ) {
% $value = join(', ', map $_->emailaddress, $contact->contact_email);
+% } elsif ( $field eq 'password' ) {
+% $value = $contact->get('_password') ? '********' : '';
% } elsif ( $field eq 'selfservice_access'
% or $field eq 'comment'
% or $field eq 'invoice_dest'
ID = "<%$id%>_<%$field%>"
STYLE = "width: 140px"
>
- <OPTION VALUE="">Disabled
+ <OPTION VALUE="" <% !$value ? 'SELECTED' : '' %>>Disabled
% if ( $value || $self_base_url ) {
<OPTION VALUE="<% $value eq 'Y' ? 'Y' : 'E' %>" <% $value eq 'Y' ? 'SELECTED' : '' %>>Enabled
% if ( $value eq 'Y' && $self_base_url ) {
<OPTION VALUE="R">Re-email
- <OPTION VALUE="P"><% $pwd_change_label %>
% }
% }
</SELECT>
- <& /elements/change_password.html,
- 'contact_num' => $curr_value,
- 'custnum' => $opt{'custnum'},
- 'curr_value' => '',
- 'no_label_display' => '1',
- 'noformtag' => '1',
- 'pre_pwd_field_label' => $id.'_',
- &>
- <SCRIPT TYPE="text/javascript">
- document.getElementById("<%$id%>_<%$field%>").onchange = function() {
- if (this.value == "P" || this.value == "E") { changepw<%$id%>_toggle(true); }
- return false
- }
+% #password form
+% } elsif ( $field eq 'password') {
+ <INPUT TYPE = "text"
+ NAME = "<%$name%>_<%$field%>"
+ ID = "changepw<%$id%>_<%$field%>"
+ SIZE = "<% $size{$field} || 14 %>"
+ VALUE = ""
+ placeholder = "<% $value |h %>"
+ >
+ <SCRIPT>
+ <% $js %>
</SCRIPT>
% } elsif ( $field eq 'invoice_dest' || $field eq 'message_dest' ) {
% my $curr_value = $cgi->param($name . '_' . $field);
% }
<BR>
<FONT SIZE="-1"><% $label{$field} %></FONT>
+% if ( $field eq 'password' ) {
+ <DIV ID="changepw<%$id%>_<%$field%>_result" STYLE="font-size: smaller"></DIV>
+% }
</TD>
% }
</TR>
my $id = $opt{'id'} || 'contactnum';
my $curr_value = $opt{'curr_value'} || $opt{'value'};
+my $contactnum = $curr_value ? $curr_value : '0';
my $onchange = '';
if ( $opt{'onchange'} ) {
$label{'invoice_dest'} = 'Send invoices';
$label{'message_dest'} = 'Send messages';
$label{'selfservice_access'} = 'Self-service';
+ $label{'password'} = 'Password';
}
my $first = 0;
my @fields = $opt{'name_only'} ? qw( first last ) : keys %label;
-my $pwd_change_label = 'Change Password';
-$pwd_change_label = 'Setup Password' unless $contact->_password;
+my $submitid = $opt{'submit_id'} ? $opt{'submit_id'} : 'submit';
+
+my $js = qq(
+ add_password_validation('changepw$id\_password', '$submitid', '', '$contactnum');
+
+ var selfService = document.getElementById("$id\_selfservice_access").value;
+
+ if (selfService !== "Y") { document.getElementById("changepw$id\_password").disabled = 'true'; }
+ document.getElementById("$id\_selfservice_access").onchange = function() {
+ if (this.value == "P" || this.value == "E" || this.value =="Y") {
+ document.getElementById("changepw$id\_password").disabled = '';
+ }
+ else { document.getElementById("changepw$id\_password").disabled = 'true'; }
+ return false;
+ }
+);
</%init>
--- /dev/null
+% my $auto = 0;
+% if ( $payby eq 'CARD' ) {
+%
+% my( $payinfo, $paycvv, $month, $year ) = ( '', '', '', '' );
+% my $payname = $cust_main->first. ' '. $cust_main->getfield('last');
+% my $location = $cust_main->bill_location;
+ <TR>
+ <TH ALIGN="right"><% mt('Card number') |h %></TH>
+ <TD COLSPAN=7>
+ <TABLE>
+ <TR>
+ <TD>
+ <INPUT TYPE="text" NAME="payinfo" SIZE=20 MAXLENGTH=19 VALUE="<%$payinfo%>"> </TD>
+ <TH><% mt('Exp.') |h %></TH>
+ <TD>
+ <SELECT NAME="month">
+% for my $mm ( map{ sprintf( '%02d', $_ ) } (1..12) ) {
+ <OPTION value="<% $mm %>"<% $mm == $month ? ' SELECTED' : '' %>><% $mm %></OPTION>
+% }
+ </SELECT>
+ </TD>
+ <TD> / </TD>
+ <TD>
+ <SELECT NAME="year">
+% my @a = localtime; for my $yyyy ( $a[5]+1900 .. $a[5]+1915 ) {
+ <OPTION value="<% $yyyy %>"<% $yyyy == $year ? ' SELECTED' : '' %>><% $yyyy %></OPTION>
+% }
+ </SELECT>
+ </TD>
+ </TR>
+ </TABLE>
+ </TD>
+ </TR>
+ <TR>
+ <TH ALIGN="right"><% mt('CVV2') |h %></TH>
+ <TD><INPUT TYPE="text" NAME="paycvv" VALUE="<% $paycvv %>" SIZE=4 MAXLENGTH=4>
+ (<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/cvv2.html', 480, 352, 'cvv2_popup' ), CAPTION, 'CVV2 Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;"><% mt('help') |h %></A>)
+ </TD>
+ </TR>
+ <TR>
+ <TH ALIGN="right"><% mt('Exact name on card') |h %></TH>
+ <TD><INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="payname" VALUE="<%$payname%>"></TD>
+ </TR>
+
+ <& /elements/location.html,
+ 'object' => $location,
+ 'no_asterisks' => 1,
+ 'address1_label' => emt('Card billing address'),
+ &>
+
+% } elsif ( $payby eq 'CHEK' ) {
+%
+% my( $account, $aba, $branch, $payname, $ss, $paytype, $paystate,
+% $stateid, $stateid_state )
+% = ( '', '', '', '', '', '', '', '', '' );
+%
+% #false laziness w/{edit,view}/cust_main/billing.html
+% my $routing_label = $conf->config('echeck-country') eq 'US'
+% ? 'ABA/Routing number'
+% : 'Routing number';
+% my $routing_size = $conf->config('echeck-country') eq 'CA' ? 4 : 10;
+% my $routing_maxlength = $conf->config('echeck-country') eq 'CA' ? 3 : 9;
+
+ <INPUT TYPE="hidden" NAME="month" VALUE="12">
+ <INPUT TYPE="hidden" NAME="year" VALUE="2037">
+ <TR>
+ <TD ALIGN="right"><% mt('Account number') |h %></TD>
+ <TD><INPUT TYPE="text" SIZE=10 NAME="payinfo1" VALUE="<%$account%>"></TD>
+ <TD ALIGN="right"><% mt('Type') |h %></TD>
+ <TD><SELECT NAME="paytype"><% join('', map { qq!<OPTION VALUE="$_" !.($paytype eq $_ ? 'SELECTED' : '').">$_</OPTION>" } FS::cust_payby->paytypes) %></SELECT></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right"><% mt($routing_label) |h %></TD>
+ <TD>
+ <INPUT TYPE="text" SIZE="<% $routing_size %>" MAXLENGTH="<% $routing_maxlength %>" NAME="payinfo2" VALUE="<%$aba%>">
+ (<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/ach.html', 380, 240, 'ach_popup' ), CAPTION, 'ACH Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;"><% mt('help') |h %></A>)
+ </TD>
+ </TR>
+% if ( $conf->config('echeck-country') eq 'CA' ) {
+ <TR>
+ <TD ALIGN="right"><% mt('Branch number') |h %></TD>
+ <TD>
+ <INPUT TYPE="text" NAME="payinfo3" VALUE="<%$branch%>" SIZE=6 MAXLENGTH=5>
+ </TD>
+ </TR>
+% }
+ <TR>
+ <TD ALIGN="right"><% mt('Bank name') |h %></TD>
+ <TD><INPUT TYPE="text" NAME="payname" VALUE="<%$payname%>"></TD>
+ </TR>
+
+% if ( $conf->exists('show_bankstate') ) {
+ <TR>
+ <TD ALIGN="right"><% mt('Bank state') |h %></TD>
+ <TD><& /elements/select-state.html,
+ 'disable_empty' => 0,
+ 'empty_label' => emt('(choose)'),
+ 'state' => $paystate,
+ 'country' => $cust_main->country,
+ 'prefix' => 'pay',
+ &>
+ </TD>
+ </TR>
+% } else {
+ <INPUT TYPE="hidden" NAME="paystate" VALUE="<% $paystate %>">
+% }
+
+% if ( $conf->exists('show_ss') ) {
+ <TR>
+ <TD ALIGN="right">
+ <% mt('Account holder') |h %><BR>
+ <% mt('Social security or tax ID #') |h %>
+ </TD>
+ <TD><INPUT TYPE="text" NAME="ss" VALUE="<% $ss %>"></TD>
+ </TR>
+% } else {
+ <INPUT TYPE="hidden" NAME="ss" VALUE="<% $ss %>"></TD>
+% }
+
+% if ( $conf->exists('show_stateid') ) {
+ <TR>
+ <TD ALIGN="right">
+ <% mt('Account holder') |h %><BR>
+ <% mt("Driver's license or state ID #") |h %>
+ </TD>
+ <TD><INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>"></TD>
+ <TD ALIGN="right"><% mt('State') |h %></TD>
+ <TD><& /elements/select-state.html,
+ 'disable_empty' => 0,
+ 'empty_label' => emt('(choose)'),
+ 'state' => $stateid_state,
+ 'country' => $cust_main->country,
+ 'prefix' => 'stateid_',
+ &>
+ </TD>
+ </TR>
+% } else {
+ <INPUT TYPE="hidden" NAME="stateid" VALUE="<% $stateid %>">
+ <INPUT TYPE="hidden" NAME="stateid_state" VALUE="<% $stateid_state %>">
+% }
+
+% } #end CARD/CHEK-specific section
+
+
+<TR>
+ <TD COLSPAN=8>
+ <INPUT TYPE="checkbox" CHECKED NAME="save" VALUE="1">
+ <% mt('Remember this information') |h %>
+ </TD>
+</TR>
+
+<TR>
+ <TD COLSPAN=8>
+ <INPUT TYPE="checkbox"<% $auto ? ' CHECKED' : '' %> NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }">
+ <% mt("Charge future payments to this [_1] automatically",$type{$payby}) |h %>
+% if ( @cust_payby ) {
+ <% mt('as') |h %>
+ <SELECT NAME="weight">
+% for ( 1 .. 1+scalar(grep { $_->payby =~ /^(CARD|CHEK)$/ } @cust_payby) ) {
+ <OPTION VALUE="<%$_%>"><% mt( $weight{$_} ) |h %></OPTION>
+% }
+ </SELECT>
+% } else {
+ <INPUT TYPE="hidden" NAME="weight" VALUE="1">
+% }
+ </TD>
+</TR>
+
+<%once>
+
+my %weight = (
+ 1 => 'Primary',
+ 2 => 'Secondary',
+ 3 => 'Tertiary',
+ 4 => 'Fourth',
+ 5 => 'Fifth',
+ 6 => 'Sixth',
+ 7 => 'Seventh',
+);
+
+</%once>
+
+<%init>
+
+my %opt = @_;
+
+my @cust_payby = @{$opt{cust_payby}};
+
+my %type = ( 'CARD' => 'credit card',
+ 'CHEK' => 'electronic check (ACH)',
+ );
+
+$cgi->param('payby') =~ /^(CARD|CHEK)$/
+ or die "unknown payby ". $cgi->param('payby');
+my $payby = $1;
+
+$cgi->param('custnum') =~ /^(\d+)$/
+ or die "illegal custnum ". $cgi->param('custnum');
+my $custnum = $1;
+
+my $cust_main = qsearchs( 'cust_main', { 'custnum'=>$custnum } );
+die "unknown custnum $custnum" unless $cust_main;
+
+my $balance = $cust_main->balance;
+
+my $payinfo = '';
+
+my $conf = new FS::Conf;
+
+#false laziness w/selfservice make_payment.html shortcut for one-country
+my %states = map { $_->state => 1 }
+ qsearch('cust_main_county', {
+ 'country' => $conf->config('countrydefault') || 'US'
+ } );
+my @states = sort { $a cmp $b } keys %states;
+
+</%init>
\ No newline at end of file
% } else {
<& header-full.html, @_ &>
% }
+<& /misc/edge_browser_check-header.html &>
--- /dev/null
+<%doc>
+
+Display a link with javascript to replace text within a element.
+
+Usage:
+
+<& /elements/link-replace_element_text.html, {
+ target_id => 'input_id',
+ replace_text => 'hello',
+
+ element_type => 'input', # Uses jquery val() method to replace text
+ element_type => 'div', # Uses jquery text() method to replace text
+
+ href => ...
+ style => ...
+ class => ...
+ }
+&>
+
+</%doc>
+<a href="<% $param{href} %>"
+ style="<% $param{style} %>"
+% if ($param{class}) {
+ class="<% $param{class} %>"
+% }
+ onClick="$('#<% $param{target_id} %>').<% $param{jmethod} %>('<% $param{replace_text} |h %>');">◁</a>
+<%init>
+
+die "template call requires a parameter hashref" unless ref $_[0];
+
+# Defaults that can be overridden in param hashref
+my %param = (
+ target_id => 'SPECIFY_AN_INPUT_ELEMENT_ID',
+ replace_text => 'REPLACEMENT_TEXT_FOR_INPUT_ELEMENT',
+ element_type => 'input',
+
+ link_text => '%#x25C1;', # ◁
+ href => 'javascript:void(0)',
+ style => 'text-decoration:none;',
+ class => undef,
+
+ %{ $_[0] },
+);
+$param{jmethod} = $param{element_type} eq 'input' ? 'val' : 'text';
+</%init>
$report_financial{'Customer Accounting Summary'} = [ $fsurl.'search/report_customer_accounting_summary.html', 'Customer accounting summary report' ];
- $report_financial{'Upcoming Auto-Bill Transactions'} = [ $fsurl.'search/report_future_autobill.html', 'Upcoming auto-bill transactions' ];
+ if ( my $report_title = FS::cust_payby->future_autobill_report_title ) {
+ $report_financial{$report_title} = [ $fsurl.'search/report_future_autobill.html', "$report_title for customers with automatic payment methods (by date)" ];
+ }
} elsif($curuser->access_right('Receivables report')) {
my $div_id = "div_$id";
my $vertices_json = $opt{'curr_value'} || '[]';
+
+my $apikey = FS::Conf->new->config('google_maps_api_key');
+
</%init>
<& hidden.html, %opt &>
<div id="<% $div_id %>" style="height: 600px; width: 600px"></div>
<div id="<% $div_id %>_hint" style="width: 100%; border: 2px solid black; text-align: center; box-sizing: border-box; padding: 4px"> </div>
-<script src="https://maps.googleapis.com/maps/api/js?libraries=drawing&v=3.22"></script>
+<script src="https://maps.googleapis.com/maps/api/js?libraries=drawing&v=3.22<% $apikey ? "&key=$apikey" : '' %>"></script>
<script>
var map;
var drawingManager;
for(var j, x, i = pass.length; i; j = Math.floor(Math.random() * i), x = pass[--i], pass[i] = pass[j], pass[j] = x);
pass = pass.join('');
document.getElementById('<% $id %>').value = pass;
+ document.getElementById('<% $id %>_result').innerHTML = '<IMG SRC="<% $p %>images/tick.png" style="width: 1em; display: inline-block; padding-right: .5em"> <SPAN STYLE="color: green;">Password valid!</SPAN>';
% if ($submitid) {
document.getElementById('<% $submitid %>').disabled = false;
% }
>
% unless ( $opt{'disable_empty'} ) {
- <OPTION VALUE=""><% $opt{'empty_label'} || '(all)' %>
+ <OPTION VALUE=""><% $opt{'empty_label'} || '(all)' %></OPTION>
% }
% foreach my $country ( @all_countries ) {
-
- <OPTION VALUE="<% $country |h %>"
- <% $country eq $opt{'country'} ? ' SELECTED' : '' %>
- ><% FS::geocode_Mixin->code2country($country). " ($country)" %>
-
+ <OPTION VALUE="<% $country |h %>"<% $country eq $opt{'country'} ? ' SELECTED' : '' %>>
+ <% FS::geocode_Mixin->code2country($country). " ($country)" |h %>
+ </OPTION>
% }
</SELECT>
<% $empty ? '<OPTION VALUE="">' : '' %>
% foreach ( 1 .. 12 ) {
- <OPTION<% $_ == $mon ? ' SELECTED' : '' %> VALUE="<% $_ %>"><% $mon[$_-1] %>
+ <OPTION<% $_ == $mon ? ' SELECTED' : '' %> VALUE="<% sprintf('%02d', $_) %>"><% $mon[$_-1] %></OPTION>
% }
-
</SELECT>/<SELECT NAME="<% $prefix %>_year" SIZE="1" <% $disabled%>>
<% $empty ? '<OPTION VALUE="">' : '' %>
% for ( $start_year .. $end_year ) {
- <OPTION<% $_ == $year ? ' SELECTED' : '' %> VALUE="<% $_ %>"><% $_ %>
+ <OPTION<% $_ == $year ? ' SELECTED' : '' %> VALUE="<% $_ %>"><% $_ %></OPTION>
% }
</SELECT>
>
% unless ( $opt{'disable_empty'} ) {
- <OPTION VALUE=""<% $opt{state} eq '' ? ' SELECTED' : '' %>><% $opt{empty_label} %>
+ <OPTION VALUE=""<% $opt{state} eq '' ? ' SELECTED' : '' %>><% $opt{empty_label} %></OPTION>
% }
% foreach my $state ( keys %states ) {
-
- <OPTION VALUE="<% $state |h %>"<% $state eq $opt{'state'} ? ' SELECTED' : '' %>><% $states{$state} || '(n/a)' |h %>
-
+ <OPTION VALUE="<% $state |h %>"<% $state eq $opt{'state'} ? ' SELECTED' : '' %>><% $states{$state} || '(n/a)' |h %></OPTION>
% }
-
</SELECT>
<%init>
% || ( $value eq $pre_opt );
<OPTION VALUE="<% $pre_opt %>"
<% $selected ? 'SELECTED' : '' %>
- ><% $pre_label %>
+ ><% $pre_label %></OPTION>
% }
% unless ( $opt{'multiple'} || $opt{'disable_empty'} ) {
- <OPTION VALUE=""><% $opt{'empty_label'} || 'all' %>
+ <OPTION VALUE=""><% $opt{'empty_label'} || 'all' %></OPTION>
% }
% foreach my $record (
? &{ $opt{'label_callback'} }( $record )
: $record->$name_col()
|h
- %>
+ %></OPTION>
% }
% while ( @post_options ) {
% || ( $value eq $post_opt );
<OPTION VALUE="<% $post_opt %>"
<% $selected ? 'SELECTED' : '' %>
- ><% $post_label %>
+ ><% $post_label %></OPTION>
% }
</SELECT>
- <TR ID="payment_amount_row" <% $opt{'row_style'} %>>
+ <TR ID="payment_amount_row">
<TH ALIGN="right"><% mt('Payment amount') |h %></TH>
- <TD COLSPAN=7>
+ <TD>
<TABLE><TR><TD BGCOLOR="#ffffff">
<% $money_char %><INPUT NAME = "amount"
ID = "amount"
VALUE = "<% $amount %>"
SIZE = 8
STYLE = "text-align:right;"
-% if ( $fee ) {
+% if ( $fee || $surcharge_percentage || $surcharge_flatfee ) {
onChange = "amount_changed(this)"
onKeyDown = "amount_changed(this)"
onKeyUp = "amount_changed(this)"
<FONT SIZE="+1"><% length($amount) ? $money_char. sprintf('%.2f', ($fee_display eq 'add') ? $amount + $fee : $amount - $fee ) : '' %> <% $fee_display eq 'add' ? 'TOTAL' : 'AVAILABLE' %></FONT>
% }
+% if ( $surcharge_percentage || $surcharge_flatfee ) {
+ <INPUT TYPE="hidden" NAME="surcharge_percentage" ID="surcharge_percentage" VALUE="<% $surcharge_percentage %>">
+ <INPUT TYPE="hidden" NAME="surcharge_flatfee" ID="surcharge_flatfee" VALUE="<% $surcharge_flatfee %>">
+ </TD><TD ID="ajax_surcharge_cell" BGCOLOR="#dddddd" STYLE="border:1px solid blue">
+ <FONT SIZE="+1">A credit card surcharge of <% $money_char. sprintf('%.2f', $surcharge) %> is included in this payment</FONT>
+% }
</TD></TR></TABLE>
</TD>
</TR>
-% if ( $fee ) {
+% if ($fee || $surcharge_percentage || $surcharge_flatfee ) {
<SCRIPT TYPE="text/javascript">
function amount_changed(what) {
-
+% if ( $fee ) {
var total = '';
if ( what.value.length ) {
total = parseFloat(what.value) <% $fee_op %> <% $fee %>;
var total_cell = document.getElementById('ajax_total_cell');
total_cell.innerHTML = '<FONT SIZE="+1">' + total + ' <% $fee_display eq 'add' ? 'TOTAL' : 'AVAILABLE' %></FONT>';
+% }
+
+% if ( $surcharge_percentage || $surcharge_flatfee ) {
+ var surcharge_cell = document.getElementById('ajax_surcharge_cell');
+ var surcharge = ((what.value - <% $surcharge_flatfee %>) * <% $surcharge_percentage %>) + <% $surcharge_flatfee %>;
+ surcharge_cell.innerHTML = '<FONT SIZE="+1">A credit card surcharge of ' + surcharge.toFixed(2) + ' is included in this payment</FONT>';
+% }
}
my $fee_pkg = '';
my $fee_display = '';
my $fee_op = '';
+my $surcharge = '';
+my $surcharge_percentage = 0;
+my $surcharge_flatfee = 0;
if ( $opt{'process-pkgpart'}
and ! $opt{'process-skip_first'} || $opt{'num_payments'}
}
my $amount = $opt{'amount'};
-if ( $amount > 0 ) {
+if ( $amount ) {
+ # probably should not happen, but will prevent surcharge being applied to negative due amounts
+ unless ($amount > 0) { $amount = 0; }
+
$amount += $fee
if $fee && $fee_display eq 'subtract';
#&{ $opt{post_fee_callback} }( \$amount ) if $opt{post_fee_callback};
- $amount += $amount * $opt{'surcharge_percentage'}/100
- if $opt{'surcharge_percentage'} > 0;
+
+ $surcharge_percentage = $opt{'surcharge_percentage'}/100 if $opt{'surcharge_percentage'} > 0;
+ $surcharge_flatfee = $opt{'surcharge_flatfee'} if $opt{'surcharge_flatfee'} > 0;
+ $surcharge = $amount * $surcharge_percentage if $surcharge_percentage > 0;
+ $surcharge += $surcharge_flatfee if ( $surcharge_flatfee > 0 && $amount > 0 );
+
+ $amount += $surcharge;
$amount = sprintf("%.2f", $amount);
}
-% if ( scalar(@{ $opt{'cust_payby'} }) == 0 ) {
+% if ( scalar(@{ $opt{'cust_payby'} }) == 0 ) {
<INPUT TYPE="hidden" NAME="<% $opt{'element_name'} || $opt{'field'} || 'custpaybynum' %>" VALUE="">
include( '/elements/tr-select-invoice.html',
#opt - most get used in /elements/tr-amount-fee
- 'custnum' => 4, # customer number,
+ 'cust_main' => $cust_main, # cust_main,
+ 'status' => 'open' # type of invoices to show. Possible values are:
+ # open - shows only open invoices
+ # void - shows only voided invoices
+ # all - shows all invoices, this is default if no status is set.
'prefix' => 'pre', # prefix to fields and row ID's
)
<TR ID="invoice_row" STYLE="display:none;">
<TH ALIGN="right"><% mt('Open invoices') |h %></TH>
- <TD COLSPAN=7>
+ <TD>
<SELECT
ID = "<% $opt{prefix} %>invoice"
NAME = "<% $opt{prefix} %>invoice"
onChange = "<% $opt{prefix} %>invoice_select_changed(this)"
>
<OPTION VALUE="select">Select an invoice to pay</OPTION>
-% foreach my $record (@records) {
+% foreach my $record (@invoices) {
% my $read_date = time2str("%b %o, %Y", $record->_date);
- <OPTION VALUE="<% $record->charged %>"><% $record->invnum %> (<% $read_date %>) - <% $record->charged %></OPTION>
+% $hidden .= '<INPUT TYPE="hidden" ID="inv'.$record->invnum.'" NAME="inv'.$record->invnum.'" VALUE="'.$record->owed.'">';
+ <OPTION VALUE="<% $record->invnum %>"><% $record->invnum %> (<% $read_date %>) - <% $record->owed %></OPTION>
% }
- </SELECT>
+ </SELECT>
+
+ <% $hidden %>
+
</TD>
</TR>
<%init>
my %opt = @_;
+my $status = $opt{'status'} ? $opt{'status'} : 'all';
+my $hidden;
-my @records = qsearch( {
- 'select' => '*',
- 'table' => 'cust_bill',
- 'hashref' => { 'custnum' => $opt{custnum} },
- 'order_by' => 'ORDER BY _date',
-});
+my @invoices;
+if ($status eq "all") { @invoices = $opt{'cust_main'}->cust_bill; }
+elsif ($status eq "open") { @invoices = $opt{'cust_main'}->open_cust_bill; }
+elsif ($status eq "void") { @invoices = $opt{'cust_main'}->cust_bill_void; }
</%init>
include( '/elements/tr-select-payment_options.html',
#opt - most get used in /elements/tr-amount-fee
- 'custnum' => 4, # customer number needed for selecting invoices
+ 'cust_main' => $cust_main, # custmain needed for selecting invoices
'prefix' => 'pre', # prefix to fields and row ID's
- 'amount' => 1, # payment amount
+ 'amount' => 1, # payment amount optional, if no amount will grab balance due from cust_main
'process-pkgpart' => scalar($conf->config('manual_process-pkgpart', $cust_main->agentnum)),
'process-display' => scalar($conf->config('manual_process-display')),
'process-skip_first' => $conf->exists('manual_process-skip_first'),
? scalar($conf->config('credit-card-surcharge-percentage', $cust_main->agentnum))
: 0
),
+ 'surcharge_flatfee' =>
+ ( $payby eq 'CARD'
+ ? scalar($conf->config('credit-card-surcharge-flatfee', $cust_main->agentnum))
+ : 0
+ ),
)
</%doc>
- <TR STYLE="display:block">
- <TH ALIGN="right"><% mt('Payment options') |h %></TH>
- <TD COLSPAN=7>
+ <TR ID="payment_option_row">
+ <TH ALIGN="right"><% mt('What would you like to pay') |h %></TH>
+ <TD>
<SELECT
ID = "<% $opt{prefix} %>payment_option"
NAME = "<% $opt{prefix} %>payment_option"
onChange = "<% $opt{prefix} %>payment_option_changed(this)"
<% $opt{disabled} %>
- >
- <OPTION VALUE="select">Select payment option</OPTION>
- <OPTION VALUE="<% $opt{amount} %>">Pay full balance</OPTION>
- <OPTION VALUE="invoice">Pay specific invoice</OPTION>
- <OPTION VALUE="">Pay specific amount</OPTION>
- </SELECT>
+ >
+ <OPTION VALUE="select">Select the amount you would like to pay</OPTION>
+ <% ($amount > 0) ? '<OPTION VALUE="'.$amount.'">Pay full balance</OPTION>' : '' %>
+ <% (@open_invoices) ? '<OPTION VALUE="invoice">Pay specific invoice</OPTION>' : '' %>
+ <OPTION VALUE="specific">Pay specific amount</OPTION>
+ </SELECT>
</TD>
</TR>
<& /elements/tr-select-invoice.html,
- 'custnum' => $opt{custnum},
- 'prefix' => $opt{prefix},
+ 'cust_main' => $cust_main,
+ 'status' => 'open',
+ 'prefix' => $opt{prefix},
&>
<& /elements/tr-amount_fee.html,
- 'row_style' => 'STYLE="display:none;"',
+ 'amount' => $amount,
+ 'custnum' => $custnum,
%opt
&>
<SCRIPT TYPE="text/javascript">
+ $('#payment_option_row').<% $payment_option_row %>();
+ $('#payment_amount_row').<% $payment_amount_row %>();
+
+ if($('#payment_amount_row').is(':visible')) {
+ var surcharge;
+ var amount = document.getElementById('amount').value;
+
+ if ((document.getElementById('surcharge_percentage') || document.getElementById('surcharge_flatfee')) && amount > 0) {
+ surcharge = (+amount * +document.getElementById('surcharge_percentage').value) + +document.getElementById('surcharge_flatfee').value;
+ }
+ else { surcharge = 0; }
+ if (document.getElementById('ajax_surcharge_cell')) {
+ document.getElementById('ajax_surcharge_cell').innerHTML = '<FONT SIZE="+1">A credit card surcharge of <% $money_char %>' + surcharge.toFixed(2) + ' is included in this payment</FONT>';
+ }
+ }
+
function <% $opt{prefix} %>payment_option_changed(what) {
+ var surcharge;
+ if (document.getElementById('surcharge_percentage') || document.getElementById('surcharge_flatfee')) {
+ surcharge = (+what.value * +document.getElementById('surcharge_percentage').value) + +document.getElementById('surcharge_flatfee').value;
+ }
+ else { surcharge = 0; }
+ var amount = +what.value + +surcharge;
+ document.getElementById('amount').disabled = true;
+
if ( what.value == 'select' ) {
- document.getElementById('payment_amount_row').style.display = 'none';
- document.getElementById('invoice_row').style.display = 'none';
- document.getElementById('<% $opt{prefix} %>invoice').value = 'select';
- document.getElementById('amount').value = '';
+ $('#payment_amount_row').hide();
+ $('#invoice_row').hide();
+ $("#<% $opt{prefix} %>invoice").val('select');
+ $('#amount').val('');
}
else if ( what.value == 'invoice' ) {
- document.getElementById('payment_amount_row').style.display = 'none';
- document.getElementById('invoice_row').style.display = 'block';
- document.getElementById('amount').value = '';
+ $('#payment_amount_row').hide();
+ $('#invoice_row').show();
+ $('#apply_box_row').hide();
+ $('#apply_box').val('yes');
+ $("#<% $opt{prefix} %>payment_option option[value='select']").remove();
+ var selectExists = ($("#<% $opt{prefix} %>invoice option[value='select']").length > 0);
+ if(!selectExists) {
+ $("#<% $opt{prefix} %>invoice").prepend("<option value='select'>Select an invoice to pay</option>");
+ $("#<% $opt{prefix} %>invoice").val($('option:first', "#<% $opt{prefix} %>invoice").val());
+ }
+ $('#amount').val('');
+ }
+ else if ( what.value == 'specific' ) {
+ $('#payment_amount_row').show();
+ $('#invoice_row').hide();
+ $('#apply_box_row').show();
+ $("#<% $opt{prefix} %>payment_option option[value='select']").remove();
+ $('#amount').val('0.00');
+ document.getElementById('amount').disabled = false;
+ if (document.getElementById('ajax_surcharge_cell')) {
+ document.getElementById('ajax_surcharge_cell').innerHTML = '<FONT SIZE="+1">A credit card surcharge of <% $money_char %>0.00 is included in this payment</FONT>';
+ }
}
else {
- document.getElementById('payment_amount_row').style.display = 'block';
- document.getElementById('invoice_row').style.display = 'none';
- document.getElementById('<% $opt{prefix} %>invoice').value = 'select';
- document.getElementById('amount').value = what.value;
+ $('#payment_amount_row').show();
+ $('#invoice_row').hide();
+ $('#apply_box_row').hide();
+ $('#apply_box').val('yes');
+ $("#<% $opt{prefix} %>payment_option option[value='select']").remove();
+ $('#amount').val(amount.toFixed(2));
+ document.getElementById('amount').disabled = true;
+ if (document.getElementById('ajax_surcharge_cell')) {
+ document.getElementById('ajax_surcharge_cell').innerHTML = '<FONT SIZE="+1">A credit card surcharge of <% $money_char %>' + surcharge.toFixed(2) + ' is included in this payment</FONT>';
+ }
}
}
function <% $opt{prefix} %>invoice_select_changed(what) {
+ var surcharge;
+ var invdue = document.getElementById("<% $opt{prefix} %>inv" + what.value);
+ if (document.getElementById('surcharge_percentage') || document.getElementById('surcharge_flatfee')) {
+ surcharge = (+invdue.value * +document.getElementById('surcharge_percentage').value) + +document.getElementById('surcharge_flatfee').value;
+ }
+ else { surcharge = 0; }
+ var amount = +invdue.value + +surcharge;
+
if ( what.value == 'select' ) {
- document.getElementById('payment_amount_row').style.display = 'none';
- document.getElementById('amount').value = '';
+ $('#payment_amount_row').hide();
+ $('#amount').val('');
}
else {
- document.getElementById('payment_amount_row').style.display = 'block';
- document.getElementById('amount').value = what.value;
+ $('#payment_amount_row').show();
+ $("#<% $opt{prefix} %>invoice option[value='select']").remove();
+ $('#amount').val(amount.toFixed(2));
+ document.getElementById('amount').disabled = true;
+ if (document.getElementById('ajax_surcharge_cell')) {
+ document.getElementById('ajax_surcharge_cell').innerHTML = '<FONT SIZE="+1">A credit card surcharge of <% $money_char %>' + surcharge.toFixed(2) + ' is included in this payment</FONT>';
+ }
}
}
my %opt = @_;
+my $cust_main = $opt{'cust_main'};
+my $amount = $opt{'amount'} ? $opt{'amount'} : $cust_main->balance;
+my $custnum = $cust_main->custnum;
+
+my @open_invoices = $cust_main->open_cust_bill;
+
+my $payment_option_row = "show";
+my $payment_amount_row = "hide";
+
+unless ($amount > 0 && @open_invoices) {
+ $payment_option_row = "hide";
+ $payment_amount_row = "show";
+}
+
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
+
</%init>
\ No newline at end of file
var manual_addr_routernum = <% encode_json(\%manual_addr_routernum) %>;
var ip_addr_curr_value = <% $opt{'ip_addr'} |js_string %>;
var blocknum_curr_value = <% $opt{'blocknum'} |js_string %>;
-function update_ip_addr(obj, i) {
- var routernum = document.getElementById('router_select_0').value;
- var select_blocknum = document.getElementById('router_select_1');
- var blocknum = select_blocknum.value;
- var input_ip_addr = document.getElementById('input_ip_addr');
+
+function update_ip_addr() {
+ var routernum = $('#router_select_0').val() || "";
+ var blocknum = $('#router_select_1').val() || "";
+ var e_input_ip_addr = $('#input_ip_addr');
+ var e_router_select_1 = $('#router_select_1');
+
+ <% # Is block is automatically selected for this router? %>
if ( manual_addr_routernum[routernum] == 'Y' ) {
-%# hide block selection and default ip address to its previous value
- select_blocknum.style.display = 'none';
- input_ip_addr.value = ip_addr_curr_value;
- }
- else {
-%# the reverse
- select_blocknum.style.display = '';
-%# default ip address to null, unless the router/block are set to the
-%# previous value, in which case default it to current value
+ show_ip_input();
+ hide_ip_select();
+ e_router_select_1.hide();
+ e_input_ip_addr.val( ip_addr_curr_value );
+ } else {
+ e_router_select_1.show();
+ e_input_ip_addr.attr('placeholder', <% mt('(automatic)') | js_string %> );
if ( routernum == router_curr_values[0] &&
- blocknum == router_curr_values[1] ) {
- input_ip_addr.value = ip_addr_curr_value;
+ blocknum == router_curr_values[1] ) {
+ e_input_ip_addr.val( ip_addr_curr_value );
} else {
- input_ip_addr.value = <% mt('(automatic)') |js_string %>;
+ e_input_ip_addr.val('');
}
}
+ show_or_hide_toggle_ip();
+ populate_ip_select();
+}
+
+function toggle_ip_input() {
+ if ( $('#input_ip_addr').is(':hidden') ) {
+ show_ip_input();
+ } else {
+ show_ip_select();
+ }
+}
+
+function show_ip_input() {
+ $('#input_ip_addr').show();
+ $('#select_ip_addr').hide();
+ depopulate_ip_select();
+}
+
+function show_ip_select() {
+ var e_input_ip_addr = $('#input_ip_addr');
+ var e_select_ip_addr = $('#select_ip_addr');
+
+ e_select_ip_addr.width( e_input_ip_addr.width() );
+ e_input_ip_addr.hide();
+ e_select_ip_addr.show();
+ populate_ip_select();
+}
+
+function populate_ip_select() {
+ depopulate_ip_select();
+ var e = $('#select_ip_addr');
+ var blocknum = $('#router_select_1').val();
+
+ var opts = [ '<option value="">loading...</option>' ];
+ e.html(opts.join(''));
+
+% if ( $opt{ip_addr} ) {
+ opts = [
+ '<option value="<% $opt{ip_addr} |h %>"><% $opt{ip_addr} |h %></option>',
+ '<option value="">-----------</option>'
+ ];
+% } else {
+ opts = [ '<option value=""><% mt('(automatic)') |h %></option>' ];
+% }
+ if ( blocknum && $.isNumeric(blocknum) && ! e.is(':hidden')) {
+ $.getJSON(
+ '<% $p %>misc/xmlhttp-free_addresses_in_block.json.html',
+ {blocknum: blocknum},
+ function(ip_json) {
+ $.each( ip_json, function(idx, val) {
+ opts.push(
+ '<option' + (val == ip_addr_curr_value ? 'selected' : '') + '>'
+ + val
+ + '</option>'
+ );
+ });
+ e.html(opts.join(''));
+ }
+ );
+ }
}
-function clearhint_ip_addr (what) {
- if ( what.value == <% mt('(automatic)') |js_string %> )
- what.value = '';
+
+function depopulate_ip_select() {
+ $('#select_ip_addr').children().remove();
}
+
+function propogate_ip_select() {
+ $('#input_ip_addr').val( $('#select_ip_addr').val() );
+}
+
+function show_or_hide_toggle_ip() {
+ if ( $('#router_select_1').val() ) {
+ $('#toggle_ip').show();
+ } else {
+ show_ip_input();
+ $('#toggle_ip').hide();
+ }
+}
+
</script>
+
<& /elements/tr-td-label.html, label => ($opt{'label'} || 'Router'), required => $opt{'required'} &>
<td>
<& /elements/select-tiered.html, prefix => 'router_', tiers => [
</td></tr>
<& /elements/tr-td-label.html, label => ($opt{'ip_addr_label'} || 'IP address'), required => $opt{'ip_addr_required'} &>
<td>
-% #warn Dumper \%fixed;
% if ( exists $fixed{$ip_field} ) {
<input type="hidden" id="input_ip_addr" name="<% $ip_field %>"
value="<% $opt{'ip_addr'} |h%>"><% $opt{'ip_addr'} || '' %>
% }
% else {
- <input type="text" id="input_ip_addr" name="<% $ip_field %>"
- value="<% $opt{'ip_addr'} |h%>" onfocus="clearhint_ip_addr(this)">
+ <input type="text"
+ id="input_ip_addr"
+ name="<% $ip_field %>"
+ value="<% $opt{'ip_addr'} | h %>"
+ onfocus="clearhint_ip_addr(this)">
+ <select id="select_ip_addr" style="display: none;" onChange='javascript:propogate_ip_select();'>
+ <option><% mt('loading') |h %>...</option>
+ </select>
+ <button type="button" onClick='javascript:toggle_ip_input();' id="toggle_ip" style="display: none;">▼</button>
% }
</td> </tr>
<script type="text/javascript">
my @fields = qw(
sectorname ip_addr height freq_mhz direction width downtilt v_width
db_high db_low sector_range
- power line_loss antenna_gain hardware_typenum
+ power line_loss antenna_gain hardware_typenum up_rate_limit down_rate_limit
);
my @sectors;
value="<% $sector->db_low |h %>">
<% emt('dB (low quality)') %>
</div>
+ <p>
+ <label><% emt('Up Rate (Kbps)') %></label>
+ <input style="text-align: left"
+ id="<% $id %>_up_rate_limit"
+ name="<% $id %>_up_rate_limit"
+ value="<% $sector->up_rate_limit |h %>">
+ </p>
+ <p>
+ <label><% emt('Down Rate (Kbps)') %></label>
+ <input style="text-align: left"
+ id="<% $id %>_down_rate_limit"
+ name="<% $id %>_down_rate_limit"
+ value="<% $sector->down_rate_limit |h %>">
+ </p>
</div>
</%def>
</%doc>
-<& '/elements/xmlhttp.html',
- 'url' => $p.'misc/xmlhttp-validate_password.html',
- 'subs' => [ 'validate_password' ],
- 'method' => 'POST', # important not to put passwords in url
-&>
-<SCRIPT>
-function add_password_validation (fieldid, submitid) {
- var inputfield = document.getElementById(fieldid);
- inputfield.onkeydown = function(e) {
- var key;
- if (window.event) { key = window.event.keyCode; }
- else { key = e.which; } // for ff browsers
- // some browsers allow the enter key to submit a form even if the submit button is disabled
- // below prevents enter key from submiting form if password has not been validated.
- if (key == '13') {
- var check = checkPasswordValidation();
- return check;
- }
- }
- inputfield.onkeyup = function () {
- var fieldid = this.id+'_result';
- var resultfield = document.getElementById(fieldid);
- if (this.value) {
- resultfield.innerHTML = '<SPAN STYLE="color: blue;">Validating password...</SPAN>';
- validate_password('fieldid',fieldid,'svcnum','<% $opt{'svcnum'} %>','contactnum','<% $opt{'contactnum'} %>','password',this.value,
- function (result) {
- result = JSON.parse(result);
- var resultfield = document.getElementById(result.fieldid);
- if (resultfield) {
- var errorimg = '<IMG SRC="<% $p %>images/error.png" style="width: 1em; display: inline-block; padding-right: .5em">';
- var validimg = '<IMG SRC="<% $p %>images/tick.png" style="width: 1em; display: inline-block; padding-right: .5em">';
- if (result.valid) {
- resultfield.innerHTML = validimg+'<SPAN STYLE="color: green;">Password valid!</SPAN>';
- if (submitid){ document.getElementById(submitid).disabled = false; }
- } else if (result.error) {
- resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.error+'</SPAN>';
- if (submitid){ document.getElementById(submitid).disabled = true; }
- } else {
- result.syserror = result.syserror || 'Server error';
- resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.syserror+'</SPAN>';
- if (submitid){ document.getElementById(submitid).disabled = true; }
- }
- }
- }
- );
- } else {
- resultfield.innerHTML = '';
- }
- };
-}
+<& '/elements/validate_password_js.html', %opt &>
-add_password_validation('<% $opt{'fieldid'} %>', '<% $opt{'submitid'} %>');
+<SCRIPT>
+ add_password_validation('<% $opt{'fieldid'} %>', '<% $opt{'submitid'} %>', '<% $opt{'svcnum'} %>', '<% $opt{'contactnum'} %>');
</SCRIPT>
<%init>
--- /dev/null
+<%doc>
+
+JavaScript to perform password validation
+
+ <& '/elements/validate_password_js.html',
+ contactnum => $contactnum,
+ svcnum => $svcnum
+ &>
+
+The ID of the input field can be anything; the ID of the DIV in which to display results
+should be the input id plus '_result'.
+
+</%doc>
+
+<& '/elements/xmlhttp.html',
+ 'url' => $p.'misc/xmlhttp-validate_password.html',
+ 'subs' => [ 'validate_password' ],
+ 'method' => 'POST', # important not to put passwords in url
+&>
+<SCRIPT>
+function add_password_validation (fieldid, submitid, svcnum, contactnum) {
+ var inputfield = document.getElementById(fieldid);
+ inputfield.onkeydown = function(e) {
+ var key;
+ if (window.event) { key = window.event.keyCode; }
+ else { key = e.which; } // for ff browsers
+ // some browsers allow the enter key to submit a form even if the submit button is disabled
+ // below prevents enter key from submiting form if password has not been validated.
+ if (key == '13') {
+ var check = checkPasswordValidation(fieldid);
+ return check;
+ }
+ }
+ inputfield.onkeyup = function () {
+ var fieldid = this.id+'_result';
+ var resultfield = document.getElementById(fieldid);
+ if (this.value) {
+ resultfield.innerHTML = '<SPAN STYLE="color: blue;">Validating password...</SPAN>';
+ validate_password('fieldid',fieldid,'svcnum','<% $opt{'svcnum'} %>','contactnum', contactnum,'password',this.value,
+ function (result) {
+ result = JSON.parse(result);
+ var resultfield = document.getElementById(result.fieldid);
+ if (resultfield) {
+ var errorimg = '<IMG SRC="<% $p %>images/error.png" style="width: 1em; display: inline-block; padding-right: .5em">';
+ var validimg = '<IMG SRC="<% $p %>images/tick.png" style="width: 1em; display: inline-block; padding-right: .5em">';
+ if (result.valid) {
+ resultfield.innerHTML = validimg+'<SPAN STYLE="color: green;">Password valid!</SPAN>';
+ if (submitid){ document.getElementById(submitid).disabled = false; }
+ } else if (result.error) {
+ resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.error+'</SPAN>';
+ if (submitid){ document.getElementById(submitid).disabled = true; }
+ } else {
+ result.syserror = result.syserror || 'Server error';
+ resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.syserror+'</SPAN>';
+ if (submitid){ document.getElementById(submitid).disabled = true; }
+ }
+ }
+ }
+ );
+ } else {
+ resultfield.innerHTML = '';
+ if (submitid){ document.getElementById(submitid).disabled = false; }
+ }
+ };
+}
+
+</SCRIPT>
+
+<%init>
+my %opt = @_;
+</%init>
\ No newline at end of file
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+my $include_waived_setup = $cgi->param('include_waived_setup') || 0;
+
my $link = "${p}search/cust_bill_pkg_discount.html?";
+$link .= "include_waived_setup=Y&" if $include_waived_setup;
my $bottom_link = $link;
#XXX or virtual
$title .= 'Discount Overview';
-my $hue = 0;
+#my $hue = 0; # Start with illegible yellow-on-white
+my $hue = 255; # Start with red-on-white
#my $hue_increment = 170;
#my $hue_increment = 145;
my $hue_increment = 125;
#foreach my $pkg_class ( @pkg_class ) {
- push @items, 'cust_bill_pkg_discount';
+ push @items,
+ $include_waived_setup
+ ? 'cust_bill_pkg_discount_or_waived'
+ : 'cust_bill_pkg_discount';
push @labels,
( $sel_agent ? '' : $agent->agent.' ' );
'disable_empty' => 0,
&>
+ <& /elements/tr-checkbox.html,
+ label => 'Include waived setup fees:',
+ field => 'include_waived_setup',
+ value => 'Y',
+ &>
+
% # anything about line items, discounts or packages really
% # otaker?
% # package class?
<OPTION VALUE="svc_acct">Account service
<OPTION VALUE="svc_acct-agent_custid">Account service with agent_custid
<OPTION VALUE="svc_acct-locationnum">Account service with existing location
+ <OPTION VALUE="svc_broadband">Broadband service
<OPTION VALUE="svc_phone">Phone service
<OPTION VALUE="svc_phone-agent_custid">Phone service with agent_custid
<OPTION VALUE="svc_phone-locationnum">Phone service with existing location
<b>Account service with existing location</b> format has the following field order: <i>custnum<%$req%>, locationnum, pkgpart<%$req%>, discountnum, start_date, setup, bill, last_bill, susp, adjourn, cancel, expire, username, _password, domsvc</i>
<BR><BR>
+<b>Broadband service</b> format has the following field order: <i>custnum<%$req%>, pkgpart<%$req%>, discountnum, start_date, setup, bill, last_bill, susp, adjourn, cancel, expire, ip_addr<%$req%>, description, routernum, blocknum, sectornum, speed_up, speed_down</i>
+<BR><BR>
+
<b>Phone service</b> format has the following field order: <i>custnum<%$req%>, pkgpart<%$req%>, discountnum, start_date, setup, bill, last_bill, susp, adjourn, cancel, expire, countrycode, phonenum, sip_password, pin</i>
<BR><BR>
<li><i>quantity</i>
- <li><i>setup_fee</i>: Including this fee implements per-customer custom pricing for this package, overriding package definition pricing
+ <li><i>setup_fee</i>: Including this implements per-customer custom pricing for this package, overriding package definition pricing
- <li><i>recur_fee</i>: Including this fee implements per-customer custom pricing for this package, overriding package definition pricing
+ <li><i>recur_fee</i>: Including this implements per-customer custom pricing for this package, overriding package definition pricing
<li><i>invoice_details</i>: Package invoice details (optionally, can include multiple lines of details separated by a vertical bar)
$opt{'format'} = $1;
}
-my $pay_batch = qsearchs('pay_batch', { batchnum => $batchnum } );
+my $credit_transactions = "EXISTS (SELECT 1 FROM cust_pay_batch WHERE batchnum = $batchnum AND paycode = 'C') AS arecredits";
+my $pay_batch = qsearchs({ 'select' => "*, $credit_transactions",
+ 'table' => 'pay_batch',
+ 'hashref' => { batchnum => $batchnum },
+ });
die "Batch not found: '$batchnum'" if !$pay_batch;
+if ($pay_batch->{Hash}->{arecredits}) {
+ my $export_format = "FS::pay_batch::".$opt{'format'};
+ die "This format can not handle refunds." unless $export_format->can('can_handle_credits');
+}
+
my $exporttext = $pay_batch->export_batch(%opt);
unless ($exporttext) {
http_header('Content-Type' => 'text/html' );
--- /dev/null
+<& /elements/header.html, "Edge browser bug" &>
+
+<div id="edgebug" style="border: solid 1px #888; border-radius: 4px; margin: 5em; max-width: 400px; text-align: left; padding: 0 1em; background-color: #ffe; box-shadow: 2px 2px 4px">
+ <div style="text-align: center; font-size: 3em; color: #933; text-shadow: 1px 1px 2px black;">
+ ⚠
+ </div>
+ <h4 style="border-bottom: solid 1px #888; margin: 1em 0; text-align: center;">
+ Edge Browser Bug
+ </h4>
+ <p>
+ Your copy of Microsoft Edge has a data corrupting bug.
+ </p>
+ <p>
+ Microsoft fixed this bug with the <b>July RS4 Windows 10 Update</b>.
+ Please update your copy of Windows.
+ </p>
+ <p>
+ Alternatively, you may choose to use
+ <a href="https://mozilla.org/en-US/firefox/new/">Mozilla Firefox</a>
+ or <a href="https://chrome.google.com">Google Chrome</a>. They
+ are not affected by this bug.
+ </p>
+</div>
+
+<& /elements/footer.html &>
\ No newline at end of file
--- /dev/null
+% if ( $force_redirect ) {
+ <script type="text/javascript">
+ if ( <% $DEBUG %> || /Edge\/17\.17134/.test( navigator.userAgent )) {
+ if ( window.location.href.indexOf("fail_notice") == -1 ) {
+ window.location.href = "<% $fsurl %>misc/edge_browser_check-fail_notice.html";
+ }
+ }
+ </script>
+% } elsif ( $do_check ) {
+ <iframe id="edge_browser_check_iframe" style="display:none;"></iframe>
+ <script type="text/javascript">
+ if ( <% $DEBUG %> || /Edge\/17\.17134/.test( navigator.userAgent )) {
+ $("#edge_browser_check_iframe").attr(
+ 'src',
+ '<% $fsurl %>misc/edge_browser_check-iframe.html?edge_browser_check=1'
+ );
+ }
+ </script>
+% }
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+my $session = $FS::CurrentUser::CurrentSession;
+my $sessionkey = $session->sessionkey if $session;
+
+my $cgi = FS::UID::cgi();
+my $DEBUG = 0;
+
+my $do_check = 0;
+$do_check = 1
+ if $curuser
+ && !$cgi->param('edge_browser_check')
+ && $sessionkey
+ && $curuser->get_pref('edge_bug_vulnerable') ne $sessionkey;
+
+my $force_redirect = $curuser->get_pref('edge_bug_vulnerable') eq 'Y' ? 1 : 0;
+</%init>
--- /dev/null
+<form id="canary-form" action="<% $fsurl %>misc/edge_browser_check-iframe.html" method="POST">
+<input type="text" id="canary-result" value="<% scalar $cgi->param('edge_browser_canary') %>">
+<select name="edge_browser_canary">
+ <option>test
+ <option>test
+</select>
+<input id="canary-submit" type="submit">
+</form>
+
+<script type="text/javascript" src="<% $fsurl %>elements/jquery.js"></script>
+<script type="text/javascript">
+ $( function() {
+ if ( ! $("#canary-result").val() ) {
+ $("#canary-form").submit();
+ }
+ });
+</script>
+
+<%init>
+my $cgi = FS::UID::cgi();
+my $curuser = $FS::CurrentUser::CurrentUser;
+my $session = $FS::CurrentUser::CurrentSession;
+my $sessionkey = $session->sessionkey if $session;
+
+if ( $curuser ) {
+ my $canary = $cgi->param('edge_browser_canary');
+ $curuser->set_pref(
+ 'edge_bug_vulnerable',
+
+ $canary eq 'test' ? $sessionkey : 'Y',
+ );
+}
+
+</%init>
\ No newline at end of file
<TABLE class="fsinnerbox">
<& /elements/tr-select-payment_options.html,
- 'custnum' => $cust_main->custnum,
- 'amount' => $balance,
+ 'cust_main' => $cust_main,
'process-pkgpart' =>
scalar($conf->config('manual_process-pkgpart', $cust_main->agentnum)),
'process-display' => scalar($conf->config('manual_process-display')),
? scalar($conf->config('credit-card-surcharge-percentage', $cust_main->agentnum))
: 0
),
+ 'surcharge_flatfee' =>
+ ( $payby eq 'CARD'
+ ? scalar($conf->config('credit-card-surcharge-flatfee', $cust_main->agentnum))
+ : 0
+ ),
&>
% if ( $conf->exists('part_pkg-term_discounts') ) {
$('#cust_payby').slideUp();
}
}
+
+ function enableAmountField() {
+ document.getElementById('amount').disabled = false;
+ }
+
</SCRIPT>
% #can't quite handle CARD/CHEK on the same page yet, but very close
>
<TABLE class="fsinnerbox">
-% my $auto = 0;
-% if ( $payby eq 'CARD' ) {
-%
-% my( $payinfo, $paycvv, $month, $year ) = ( '', '', '', '' );
-% my $payname = $cust_main->first. ' '. $cust_main->getfield('last');
-% my $location = $cust_main->bill_location;
-
- <TR>
- <TH ALIGN="right"><% mt('Card number') |h %></TH>
- <TD COLSPAN=7>
- <TABLE>
- <TR>
- <TD>
- <INPUT TYPE="text" NAME="payinfo" SIZE=20 MAXLENGTH=19 VALUE="<%$payinfo%>"> </TD>
- <TH><% mt('Exp.') |h %></TH>
- <TD>
- <SELECT NAME="month">
-% for ( ( map "0$_", 1 .. 9 ), 10 .. 12 ) {
-
- <OPTION<% $_ == $month ? ' SELECTED' : '' %>><% $_ %>
-% }
-
- </SELECT>
- </TD>
- <TD> / </TD>
- <TD>
- <SELECT NAME="year">
-% my @a = localtime; for ( $a[5]+1900 .. $a[5]+1915 ) {
-
- <OPTION<% $_ == $year ? ' SELECTED' : '' %>><% $_ %>
-% }
-
- </SELECT>
- </TD>
- </TR>
- </TABLE>
- </TD>
- </TR>
- <TR>
- <TH ALIGN="right"><% mt('CVV2') |h %></TH>
- <TD><INPUT TYPE="text" NAME="paycvv" VALUE="<% $paycvv %>" SIZE=4 MAXLENGTH=4>
- (<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/cvv2.html', 480, 352, 'cvv2_popup' ), CAPTION, 'CVV2 Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;"><% mt('help') |h %></A>)
- </TD>
- </TR>
- <TR>
- <TH ALIGN="right"><% mt('Exact name on card') |h %></TH>
- <TD><INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="payname" VALUE="<%$payname%>"></TD>
- </TR>
-
- <& /elements/location.html,
- 'object' => $location,
- 'no_asterisks' => 1,
- 'address1_label' => emt('Card billing address'),
- &>
-
-% } elsif ( $payby eq 'CHEK' ) {
-%
-% my( $account, $aba, $branch, $payname, $ss, $paytype, $paystate,
-% $stateid, $stateid_state )
-% = ( '', '', '', '', '', '', '', '', '' );
-%
-% #false laziness w/{edit,view}/cust_main/billing.html
-% my $routing_label = $conf->config('echeck-country') eq 'US'
-% ? 'ABA/Routing number'
-% : 'Routing number';
-% my $routing_size = $conf->config('echeck-country') eq 'CA' ? 4 : 10;
-% my $routing_maxlength = $conf->config('echeck-country') eq 'CA' ? 3 : 9;
-
- <INPUT TYPE="hidden" NAME="month" VALUE="12">
- <INPUT TYPE="hidden" NAME="year" VALUE="2037">
- <TR>
- <TD ALIGN="right"><% mt('Account number') |h %></TD>
- <TD><INPUT TYPE="text" SIZE=10 NAME="payinfo1" VALUE="<%$account%>"></TD>
- <TD ALIGN="right"><% mt('Type') |h %></TD>
- <TD><SELECT NAME="paytype"><% join('', map { qq!<OPTION VALUE="$_" !.($paytype eq $_ ? 'SELECTED' : '').">$_</OPTION>" } FS::cust_payby->paytypes) %></SELECT></TD>
- </TR>
- <TR>
- <TD ALIGN="right"><% mt($routing_label) |h %></TD>
- <TD>
- <INPUT TYPE="text" SIZE="<% $routing_size %>" MAXLENGTH="<% $routing_maxlength %>" NAME="payinfo2" VALUE="<%$aba%>">
- (<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/ach.html', 380, 240, 'ach_popup' ), CAPTION, 'ACH Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;"><% mt('help') |h %></A>)
- </TD>
- </TR>
-% if ( $conf->config('echeck-country') eq 'CA' ) {
- <TR>
- <TD ALIGN="right"><% mt('Branch number') |h %></TD>
- <TD>
- <INPUT TYPE="text" NAME="payinfo3" VALUE="<%$branch%>" SIZE=6 MAXLENGTH=5>
- </TD>
- </TR>
-% }
- <TR>
- <TD ALIGN="right"><% mt('Bank name') |h %></TD>
- <TD><INPUT TYPE="text" NAME="payname" VALUE="<%$payname%>"></TD>
- </TR>
-
-% if ( $conf->exists('show_bankstate') ) {
- <TR>
- <TD ALIGN="right"><% mt('Bank state') |h %></TD>
- <TD><& /elements/select-state.html,
- 'disable_empty' => 0,
- 'empty_label' => emt('(choose)'),
- 'state' => $paystate,
- 'country' => $cust_main->country,
- 'prefix' => 'pay',
- &>
- </TD>
- </TR>
-% } else {
- <INPUT TYPE="hidden" NAME="paystate" VALUE="<% $paystate %>">
-% }
-
-% if ( $conf->exists('show_ss') ) {
- <TR>
- <TD ALIGN="right">
- <% mt('Account holder') |h %><BR>
- <% mt('Social security or tax ID #') |h %>
- </TD>
- <TD><INPUT TYPE="text" NAME="ss" VALUE="<% $ss %>"></TD>
- </TR>
-% } else {
- <INPUT TYPE="hidden" NAME="ss" VALUE="<% $ss %>"></TD>
-% }
-
-% if ( $conf->exists('show_stateid') ) {
- <TR>
- <TD ALIGN="right">
- <% mt('Account holder') |h %><BR>
- <% mt("Driver's license or state ID #") |h %>
- </TD>
- <TD><INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>"></TD>
- <TD ALIGN="right"><% mt('State') |h %></TD>
- <TD><& /elements/select-state.html,
- 'disable_empty' => 0,
- 'empty_label' => emt('(choose)'),
- 'state' => $stateid_state,
- 'country' => $cust_main->country,
- 'prefix' => 'stateid_',
- &>
- </TD>
- </TR>
-% } else {
- <INPUT TYPE="hidden" NAME="stateid" VALUE="<% $stateid %>">
- <INPUT TYPE="hidden" NAME="stateid_state" VALUE="<% $stateid_state %>">
-% }
-
-% } #end CARD/CHEK-specific section
-
-
-<TR>
- <TD COLSPAN=8>
- <INPUT TYPE="checkbox" CHECKED NAME="save" VALUE="1">
- <% mt('Remember this information') |h %>
- </TD>
-</TR>
-
-<TR>
- <TD COLSPAN=8>
- <INPUT TYPE="checkbox"<% $auto ? ' CHECKED' : '' %> NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }">
- <% mt("Charge future payments to this [_1] automatically",$type{$payby}) |h %>
-% if ( @cust_payby ) {
- <% mt('as') |h %>
- <SELECT NAME="weight">
-% for ( 1 .. 1+scalar(grep { $_->payby =~ /^(CARD|CHEK)$/ } @cust_payby) ) {
- <OPTION VALUE="<%$_%>"><% mt( $weight{$_} ) |h %>
-% }
- </SELECT>
-% } else {
- <INPUT TYPE="hidden" NAME="weight" VALUE="1">
-% }
- </TD>
-</TR>
+<& /elements/cust_payby_new.html,
+ 'cust_payby' => \@cust_payby,
+ 'curr_value' => $custpaybynum,
+&>
</TABLE>
</DIV>
<BR>
-<INPUT TYPE="submit" NAME="process" VALUE="<% mt('Process payment') |h %>">
+<INPUT TYPE="submit" NAME="process" ID="process" VALUE="<% mt('Process payment') |h %>" disabled="disabled" onclick="enableAmountField()">
</FORM>
+<SCRIPT TYPE="text/javascript">
+
+$(document).ready(function (){
+ validate();
+ $('<% $validate_select_fields %>').change(validate);
+ $('<% $validate_input_fields %>').keyup(validate);
+});
+
+function validate(){
+ if (
+ $('#amount').val() > 0 && (
+ ( $('#custpaybynum').val() > 0 ) ||
+% if ($payby eq "CHEK") {
+ ( $('input[name=payinfo1]').val().length > 0 &&
+ $('input[name=payinfo2]').val().length > 0 &&
+ $('input[name=payname]').val().length > 0 &&
+ $('select[name=paytype]').val().length > 0
+ )
+% }
+% elsif ($payby eq "CARD") {
+ ( $('input[name=payinfo]').val().length > 0 &&
+ $('input[name=paycvv]').val().length > 0 &&
+ $('input[name=payname]').val().length > 0 &&
+ $('#city').val().length > 0 &&
+ $('#city').val().length > 0 &&
+ $('#state').val().length > 0 &&
+ $('#country').val().length > 0
+ )
+% }
+ )
+ ) {
+ $("#process").prop("disabled", false);
+ }
+ else {
+ $("#process").prop("disabled", true);
+ }
+}
+
+</SCRIPT>
+
<& /elements/footer-cust_main.html &>
<%once>
or die "unknown payby ". $cgi->param('payby');
my $payby = $1;
+my $validate_select_fields = "#payment_option, #invoice, #custpaybynum, ";
+my $validate_input_fields = "#amount, input[name=payname], ";
+if ($payby eq "CHEK") {
+ $validate_input_fields .= "input[name=payinfo1], input[name=payinfo2]";
+ $validate_select_fields .= "select[name=paytype] ";
+}
+elsif ($payby eq "CARD") {
+ $validate_input_fields .= "input[name=payinfo], input[name=paycvv], input[name=address1], #city, #zip";
+ $validate_select_fields .= "#state, #country ";
+}
+
$cgi->param('custnum') =~ /^(\d+)$/
or die "illegal custnum ". $cgi->param('custnum');
my $custnum = $1;
my $conf = new FS::Conf;
-#false laziness w/selfservice make_payment.html shortcut for one-country
-my %states = map { $_->state => 1 }
- qsearch('cust_main_county', {
- 'country' => $conf->config('countrydefault') || 'US'
- } );
-my @states = sort { $a cmp $b } keys %states;
-
my $payunique = "webui-payment-". time. "-$$-". rand() * 2**32;
</%init>
<% $cgi->redirect($fsurl.'view/svc_acct.cgi?'.$cgi->query_string) %>
% }
% elsif ($contactnum) {
- <% $cgi->redirect($fsurl.'edit/cust_main-contacts.html?'.$cgi->param('custnum')) %>
+% my $freeside_status = "Contact ".$contact->{'Hash'}->{'first'}." ".$contact->{'Hash'}->{'last'}." password updated.";
+ <% $cgi->redirect( -uri => popurl(3). "view/cust_main.cgi?". $cgi->param('custnum'),
+ -cookie => CGI::Cookie->new(
+ -name => 'freeside_status',
+ -value => mt($freeside_status),
+ -expires => '+5m',
+ ),
+ )
+%>
% }
% }
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
+my $contact;
$cgi->param('svcnum') =~ /^(\d+)$/ or die "illegal svcnum" if $cgi->param('svcnum');
my $svcnum = $1;
+foreach my $prefix (grep /^(.*)(password)$/, $cgi->param) {
+ $cgi->param('password' => $cgi->param($prefix));
+}
+
$cgi->param('contactnum') =~ /^(\d+)$/ or die "illegal contactnum" if $cgi->param('contactnum');
my $contactnum = $1;
$cgi->delete('password');
}
elsif ($contactnum) {
- my $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
+ $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
or return { 'error' => "Contact not found" . $contactnum };
$error = $contact->is_password_allowed($newpass)
'extra_sql' => ' AND '. $curuser->agentnums_sql,
}) or die "unknown custnum $custnum";
+my $invoice = ($cgi->param('invoice') =~ /^(\d+)$/) ? $cgi->param('invoice') : '';
+
$cgi->param('amount') =~ /^\s*(\d*(\.\d\d)?)\s*$/
or errorpage("illegal amount ". $cgi->param('amount'));
my $amount = $1;
$paycvv = $cust_payby->paycvv; # pass it if we got it, running a transaction will clear it
( $month, $year ) = $cust_payby->paydate_mon_year;
$payname = $cust_payby->payname;
+ $cgi->param(-name=>"paytype", -value=>$cust_payby->paytype) unless $cgi->param("paytype");
} else {
# use new info
##
- $cgi->param('year') =~ /^(\d+)$/
+ $cgi->param('year') =~ /^(\d{4})/
or errorpage("illegal year ". $cgi->param('year'));
$year = $1;
- $cgi->param('month') =~ /^(\d+)$/
+ $cgi->param('month') =~ /^(\d{2})/
or errorpage("illegal month ". $cgi->param('month'));
$month = $1;
my $error = '';
my $paynum = '';
+
if ( $cgi->param('batch') ) {
$error = 'Prepayment discounts not supported with batched payments'
if $discount_term;
+ # Invalid payment expire dates are replaced with 2037-12-01 (why?)
+ my $paydate = "${year}-${month}-01";
+ {
+ use DateTime;
+ local $@;
+ eval { DateTime->new({ year => $year, month => $month, day => 1 }) };
+ $paydate = '2037-12-01' if $@;
+ }
+
$error ||= $cust_main->batch_card(
'payby' => $payby,
'amount' => $amount,
'payinfo' => $payinfo,
- 'paydate' => "$year-$month-01",
+ 'paydate' => $paydate,
'payname' => $payname,
+ 'invnum' => $invoice,
map { $_ => scalar($cgi->param($_)) }
@{$payby2fields{$payby}}
);
'discount_term' => $discount_term,
'no_auto_apply' => ($cgi->param('apply') eq 'never') ? 'Y' : '',
'no_invnum' => 1,
+ 'invnum' => $invoice,
map { $_ => scalar($cgi->param($_)) } @{$payby2fields{$payby}}
);
errorpage($error) if $error;
$ticket->Load($ticketmap{$id});
$ticket{$ticketmap{$id}} = $ticket->Subject;
$customers{$ticketmap{$id}} =
- [ map { $_->Resolver->AsString }
- grep { $_->Resolver->{'fstable'} eq 'cust_main' }
- grep { $_->Scheme eq 'freeside' }
- map { $_->TargetURI }
- @{ $ticket->_Links('Base')->ItemsArrayRef }
- ];
+ [ map { $_->Resolver->AsString }
+ grep { $_->Resolver->{'fstable'} eq 'cust_main' }
+ grep { $_->Scheme eq 'freeside' }
+ map { $_->TargetURI }
+ grep { $_->BaseURI->Scheme eq 'fsck.com-rt'
+ && $_->BaseURI->Resolver->ObjectType eq 'ticket'
+ }
+ @{ $ticket->_Links('Base')->ItemsArrayRef }
+ ];
}
}
--- /dev/null
+<%doc>
+ Return a json array containing all free ip addresses within a given block
+ Unless block is larger than /24 - Does somebody really want to populate
+ 65k addresses into a HTML selectbox?
+</%doc>
+<% encode_json($json) %>\
+<%init>
+
+my $json = [];
+
+my $blocknum = $cgi->param('blocknum');
+
+my $addr_block = qsearchs( addr_block => { blocknum => $blocknum });
+
+$json = $addr_block->free_addrs
+ if ref $addr_block && $addr_block->ip_netmask >= 24;
+
+</%init>
$result{'syserror'} = 'Invoked without password' unless $password;
return \%result if $result{'syserror'};
- if ($arg{'contactnum'}) {
+ if ($arg{'contactnum'} =~ /^\d+$/) {
my $contactnum = $arg{'contactnum'};
$result{'syserror'} = 'Invalid contactnum' unless $contactnum =~ /^\d*$/;
return \%result if $result{'syserror'};
my $contact = $contactnum
? qsearchs('contact',{'contactnum' => $contactnum})
- : '';
+ : (new FS::contact {});
$result{'error'} = $contact->is_password_allowed($password);
}
if ( $_[0]->pkgdiscountnum ) {
# Standard discount, not a waived setup fee
my $discount = qsearchs('discount',{
- pkgdiscountnum => $_[0]->pkgdiscountnum
+ discountnum => $_[0]->discountnum
});
return $discount->description;
} else {
}
# Filter: Include waived setup fees
-if ( !$cgi->param('include_waived_setup') ) {
+if ( $cgi->param('include_waived_setup') ) {
+ # Filter a hidden fee attached to a package with a waived setup fee from
+ # causing the waived-fee for that package to be double-counted
+ push @where, 'cust_bill_pkg.pkgpart_override IS NULL';
+} else {
push @where, "cust_bill_pkg_discount.pkgdiscountnum IS NOT NULL";
}
my $pkgnum = $cust_event->tablenum;
my $frag = "cust_pkg$pkgnum"; #hack for IE ignoring real #fragment
[ "${p}view/cust_main.cgi?custnum=$custnum$show;fragment=$frag#cust_pkg", 'tablenum' ];
+ } elsif ( $eventtable eq 'cust_pay' ) {
+ [ "${p}view/$eventtable.html?paynum=", 'tablenum' ];
+ } elsif ( $eventtable eq 'cust_statement' ) {
+ [ "${p}view/$eventtable.html?", 'tablenum' ];
+ } elsif ( $eventtable eq 'cust_pay_batch' ) {
+ [ "${p}search/cust_pay_batch.cgi?batchnum=", 'cust_pay_batch_batchnum' ];
} else {
[ "${p}view/$eventtable.cgi?", 'tablenum' ];
}
'part_event.*',
#'cust_bill.custnum',
#'cust_bill._date AS cust_bill_date',
+ 'cust_pay_batch.batchnum AS cust_pay_batch_batchnum',
'cust_main.custnum AS cust_main_custnum',
FS::UI::Web::cust_sql_fields(),
),
$sql_query = {
'table' => 'cust_pay_batch',
- 'select' => 'cust_pay_batch.*, cust_main.*, cust_pay.paynum',
+ 'select' => 'cust_pay_batch.*, cust_pay.paynum',
'hashref' => {},
'addl_from' => 'LEFT JOIN pay_batch USING ( batchnum ) '.
'LEFT JOIN cust_main USING ( custnum ) '.
'header' => \@header,
'fields' => \@fields,
'links' => \@links,
+ 'disable_maxselect' => '1',
&>
<%init>
## sql to get the first active date, last cancel date, and last reason.
my $active_date = 'select min(setup) from cust_pkg left join part_pkg using (pkgpart) where cust_pkg.custnum = cust_main.custnum and part_pkg.freq > \'0\'';
-my $cancel_date = 'select max(cancel) from cust_pkg where cust_pkg.custnum = cust_main.custnum';
+
+## set cancel date range here
+my($beginning_date, $ending_date) = FS::UI::Web::parse_beginning_ending($cgi, '');
+my $max_cancel_sql = "select max(cancel) from cust_pkg left join part_pkg using (pkgpart) where cust_pkg.custnum = cust_main.custnum and part_pkg.freq > \'0\'";
+my $cancel_date = $max_cancel_sql.' and (('.$max_cancel_sql.') >= '.$beginning_date.' and ('.$max_cancel_sql.') <= '.$ending_date.')';
+
my $cancel_reason = 'select reason.reason from cust_pkg
left join cust_pkg_reason on (cust_pkg.pkgnum = cust_pkg_reason.pkgnum)
left join reason on (cust_pkg_reason.reasonnum = reason.reasonnum)
- where cust_pkg.custnum = cust_main.custnum and cust_pkg_reason.date = ('.$cancel_date.')
+ where cust_pkg.custnum = cust_main.custnum and cust_pkg_reason.date = ('.$cancel_date.') limit 1
';
my @header = ( '#', 'Name', 'Address', 'Phone', 'Email', 'Active Date', 'Cancelled Date', 'Reason', 'Active Days' );
my @links = ( $customer_link, $customer_link, '', '', '', '', '', '', '' );
my @select = (
'cust_main.*',
- 'cust_location.*',
- 'part_pkg.*',
"(select to_char((select to_timestamp((".$active_date."))), 'Mon DD YYYY')) AS active_date",
"(select to_char((select to_timestamp((".$cancel_date."))), 'Mon DD YYYY')) AS cancel_date",
"($cancel_reason) AS cancel_reason",
+<%doc>
+
+ E911 Fee Report
+
+ Finds billing totals for a given pkgpart where the bill item matches
+ cust_pkg.pkgpart or cust_bill_pkg.pkgpart_override columns.
+
+ Given date range, filter by when the invoice was paid.
+
+ * E911 access lines - SUM(cust_bill_pkg.quantity)
+ * Total fees charged - SUM(cust_bill_pay_pkg.amount)
+ * Fee payments collected - SUM(cust_bill_pkg.setup) + SUM(cust_bill_pkg.recur)
+
+ * Administrative fee (1%) - 1% of Fee Payments Collected
+ * Amount due - 99% of Fee Payments Collected
+
+</%doc>
% if ( $row ) {
-%# pretty minimal report
<& /elements/header.html, 'E911 Fee Report' &>
+
<& /elements/table-grid.html &>
<STYLE TYPE="text/css">
table.grid TD:first-child { font-weight: normal }
text-align: right;
padding: 1px 2px }
</STYLE>
+
<TR><TH COLSPAN=2><% $legend %></TH></TR>
<TR>
- <TD>E911 access lines:</TD>
- <TD><% $row->{quantity} || 0 %></TD>
+ <TD><% mt('E911 access lines') %>:</TD>
+ <TD><% $report{e911_access_lines} %></TD>
</TR>
<TR>
- <TD>Total fees charged: </TD>
- <TD><% $money_char.sprintf('%.2f', $row->{charged_amount}) %></TD>
+ <TD><% mt('Total fees charged') %>: </TD>
+ <TD><% $money_char.$report{fees_charged} %></TD>
</TD>
<TR>
- <TD>Fee payments collected: </TD>
- <TD><% $money_char.sprintf('%.2f', $row->{paid_amount}) %></TD>
+ <TD><% mt('Fee payments collected') %>: </TD>
+ <TD><% $money_char.$report{fees_collected} %></TD>
</TR>
<TR>
- <TD>Administrative fee (1%): </TD>
- <TD><% $money_char.sprintf('%.2f', $row->{paid_amount} * $admin_fee) %></TD>
+ <TD><% mt('Administrative fee') %> (1%): </TD>
+ <TD><% $money_char.$report{admin_fee} %></TD>
</TR>
<TR>
- <TD>Amount due: </TD>
- <TD><% $money_char.sprintf('%.2f', $row->{paid_amount} * (1-$admin_fee) ) %>
- </TD>
+ <TD><% mt('Amount due') %>: </TD>
+ <TD><% $money_char.$report{e911_amount_due} %></TD>
</TR>
</TABLE>
<& /elements/footer.html &>
% }
<%init>
+our $DEBUG;
+
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
# package classes, etc.), do NOT simply loop through this and do a
# bazillion scalar_sql queries. Use a properly grouped aggregate query.
-my $select = 'SELECT cust_bill_pkg.billpkgnum, cust_bill_pkg.quantity, '.
-'cust_bill_pkg.setup, SUM(cust_bill_pay_pkg.amount) AS paid_amount';
-
-my $from = 'FROM cust_pkg
- JOIN cust_bill_pkg USING (pkgnum)
- JOIN cust_bill USING (invnum)
- LEFT JOIN cust_bill_pay_pkg USING (billpkgnum)
- LEFT JOIN cust_bill_pay USING (billpaynum)
-';
-# going by payment application date here, which should be
-# max(invoice date, payment date)
-my $where = "WHERE cust_pkg.pkgpart = $pkgpart
-AND ( (cust_bill_pay._date >= $begin AND cust_bill_pay._date < $end)
- OR cust_bill_pay.paynum IS NULL )";
+my $sql_statement = "
+ SELECT
+ sum(cust_bill_pkg.quantity) as quantity,
+ sum(cust_bill_pay_pkg.amount) as amount,
+ sum(cust_bill_pkg.setup) as setup,
+ sum(cust_bill_pkg.recur) as recur
+ FROM cust_pkg
+ LEFT JOIN cust_bill_pkg USING (pkgnum)
+ LEFT JOIN cust_bill_pay_pkg USING (billpkgnum)
+ LEFT JOIN cust_bill_pay USING (billpaynum)
+";
if ( $agentnum ) {
- $from .= ' JOIN cust_main ON (cust_pkg.custnum = cust_main.custnum)';
- $where .= "\n AND cust_main.agentnum = $agentnum";
+ $sql_statement .= "
+ LEFT JOIN cust_main USING (custnum)
+ WHERE
+ cust_main.agentnum = ?
+ AND ";
+} else {
+ $sql_statement .= "
+ WHERE
+ "
}
+$sql_statement .= "
+ ( cust_bill_pkg.pkgpart_override = ? OR cust_pkg.pkgpart = ? )
+ AND (
+ ( cust_bill_pay._date >= ? AND cust_bill_pay._date < ? )
+ OR cust_bill_pay.paynum IS NULL
+ );
+";
+
+# Preserving this oddball, unexplained epoch substitution
+$end = '' if $end == 4294967295;
-my $subquery = "$select $from $where
-GROUP BY cust_bill_pkg.billpkgnum, cust_bill_pkg.quantity";
-# This has one row for each E911 line item that has any payments applied.
-# Fields are the billpkgnum of the item (currently unused), the number of
-# E911 charges, and the total amount paid (always > 0).
+my @bind_values = (
+ $agentnum ? $agentnum : (),
+ $pkgpart,
+ $pkgpart,
+ $begin || 0,
+ $end || time(),
+);
-# now sum those rows.
-my $sql = "SELECT SUM(quantity) AS quantity, SUM(setup) AS charged_amount,
-SUM(paid_amount) AS paid_amount FROM ($subquery) AS paid_fees"; # no grouping
+if ( $DEBUG ) {
+ warn "\$sql_statement: $sql_statement\n";
+ warn "\@bind_values: ".join(', ',@bind_values)."\n";
+}
-my $sth = dbh->prepare($sql);
-$sth->execute;
+my $sth = dbh->prepare( $sql_statement );
+$sth->execute( @bind_values ) || die $sth->errstr;
my $row = $sth->fetchrow_hashref;
-my $admin_fee = 0.01; # 1% admin fee, allowed in Texas
+my %report = (
+ e911_access_lines => $row->{quantity} || 0,
-$end = '' if $end == 4294967295;
-my $legend = '';
-if ( $agentnum ) {
- $legend = FS::agent->by_key($agentnum)->agent . ', ';
-}
-if ( $begin and $end ) {
- $legend .= time2str('%h %o %Y', $begin) . '—' .
- time2str('%h %o %Y', $end);
+ fees_charged => sprintf(
+ "%.2f",
+ ( $row->{setup} + $row->{recur} ) || 0,
+ ),
+
+ fees_collected => sprintf(
+ "%.2f",
+ ( $row->{amount} || 0 ),
+ ),
+);
+
+# Does everybody use this 1% admin fee? Should this be configurable?
+$report{admin_fee} = sprintf( "%.2f", $report{fees_collected} * 0.01 );
+$report{e911_amount_due} = $report{fees_collected} - $report{admin_fee};
+
+my $begin_text =
+ $begin
+ ? DateTime->from_epoch(epoch => $begin)->mdy('/')
+ : mt('Anytime');
+
+my $end_text = DateTime->from_epoch(epoch => ( $end || time ))->mdy('/');
+
+my $legend = FS::agent->by_key($agentnum)->agent . ', ' if $agentnum;
+if ( $begin && $end ) {
+ $legend .= "$begin_text ↔ $end_text";
} elsif ( $begin ) {
- $legend .= time2str('after %h %o %Y', $begin);
-} elsif ( $end ) {
- $legend .= time2str('before %h %o %Y', $end);
+ $legend .= mt('After')." $begin_text";
} else {
- $legend .= 'any time';
+ $legend .= mt('Through')." $end_text"
}
-$legend = ucfirst($legend);
+
</%init>
html_foot => include('elements/checkbox-foot.html',
actions => [
{ label => 'Edit selected packages',
- action => 'popup_package_edit()',
+ onclick => 'popup_package_edit()',
},
{ submit => 'Delete selected packages',
confirm => 'Really delete these packages?'
<BR>
% foreach my $action (@$actions) {
% if ( $action->{onclick} ) {
-<INPUT TYPE="button" <% $action->{name} %> onclick="<% $opt{onclick} %>"\
+<INPUT TYPE="button" <% $action->{name} %> onclick="<% $action->{onclick} %>"\
VALUE="<% $action->{label} |h%>">
% } elsif ( $action->{submit} ) {
<INPUT TYPE="submit" <% $action->{name} %> <% $action->{confirm} %>\
$m->print($output);
</%perl>
% } else {
+% unless ( $suppress_header ) {
<& /elements/header.html, $title &>
+% }
<% $head %>
% my $myself = $cgi->self_url;
+% unless ( $suppress_header ) {
<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;
% 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 .= " rowspan=".$cell->{rowspan}
+% if exists $cell->{rowspan} && $cell->{rowspan} > 1;
+% $style .= " colspan=".$cell->{colspan}
+% if exists $cell->{colspan} && $cell->{colspan} > 1;
% $style .= ' class="' . $cell->{class} . '"' if $cell->{class};
% if ($cell->{bypass_filter}) {
<<%$td%><%$style%>><% $cell->{value} %></<%$td%>>
% }
</table>
<% $foot %>
+% unless ( $suppress_footer ) {
<& /elements/footer.html &>
% }
+% }
<%args>
$title
@rows
$foot => ''
$table_width => "100%"
$table_class => "report"
+$suppress_header => undef
+$suppress_footer => undef
</%args>
#setup some pagination things if we're in html mode
my $conf = new FS::Conf;
- $confmax = $conf->config('maxsearchrecordsperpage') || 100;
- if ( $cgi->param('maxrecords') =~ /^(\d+)$/ ) {
- $maxrecords = $1;
- } else {
- $maxrecords ||= $confmax;
- }
-
$opt{'disable_maxselect'} ||= $conf->exists('disable_maxselect');
+ unless ($opt{'disable_maxselect'}) {
+ $confmax = $conf->config('maxsearchrecordsperpage') || 100;
+ if ( $cgi->param('maxrecords') =~ /^(\d+)$/ ) {
+ $maxrecords = $1;
+ } else {
+ $maxrecords ||= $confmax;
+ }
+ }
$limit = $maxrecords ? "LIMIT $maxrecords" : '';
Report listing upcoming auto-bill transactions
-Spec requested the ability to run this report with a longer date range,
-and see which charges will process on which day. Checkbox multiple_billing_dates
-enables this functionality.
+For every customer with a valid auto-bill payment method,
+report runs bill_and_collect() for each customer, for each
+day, from today through the report target date. After
+recording the results, all operations are rolled back.
-Performance:
-This is a dynamically generated report. The time this report takes to run
-will depends on the number of customers. Installations with a high number
-of auto-bill customers may find themselves unable to run this report
-because of browser timeout. Report could be implemented as a queued job if
-necessary, to solve the performance problem.
+This report relies on the ability to safely run bill_and_collect(),
+with all exports and messaging disabled, and then to roll back the
+results.
+
+This report takes time. If 200 customers have automatic
+payment methods, and requester is looking one week ahead,
+there will be 1,400 billing and payment cycles simulated
</%doc>
+<h4><% $report_subtitle %></h4>
<& elements/grid-report.html,
- title => 'Upcoming auto-bill transactions',
+ title => $report_title,
rows => \@rows,
cells => \@cells,
table_width => "",
td.gridreport { margin: 0 .2em; padding: 0 .4em; }
</style>
',
+ suppress_header => $job ? 1 : 0,
+ suppress_footer => $job ? 1 : 0,
&>
+% if ( %pmt_type_subtotal ) {
+ <table class="gridreport" style="margin-left: 2em;">
+ <tr>
+ <th class="gridreport" colspan="2">
+ Summary
+ </th>
+ </tr>
+% for my $pmt_type ( sort keys %pmt_type_subtotal ) {
+ <tr class="gridreport">
+ <td class="gridreport" style="text-align: right; margin-right: 1em;">
+ <% sprintf '$%.2f', $pmt_type_subtotal{ $pmt_type } %>
+ </td>
+ <td class="gridreport">
+ <% $pmt_type |h %>
+ </td>
+ </tr>
+% }
+% if ( keys %pmt_type_subtotal > 1 ) {
+% $pmt_type_subtotal{Total} += $_ for values %pmt_type_subtotal;
+ <tr class="gridreport" style="border-top: solid 1px #999;">
+ <td class="gridreport" style="text-align: right; margin-right: 1em; border-top: solid 1px #666;">
+ <% sprintf( '$%.2f', $pmt_type_subtotal{Total} ) %>
+ </td>
+ <td class="gridreport" style="border-top: solid 1px #666;">
+ Total
+ </td>
+ </tr>
+ </table>
+% }
+% }
<%init>
+ use DateTime;
+ use FS::Misc::Savepoint;
+ use FS::Report::Queued::FutureAutobill;
+ use FS::UID qw( dbh );
+
+ die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+ my $job = $FS::Report::Queued::FutureAutobill::job;
-use FS::UID qw( dbh myconnect );
+ $job->update_statustext('0,Finding customers') if $job;
-die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+ my $DEBUG = $cgi->param('DEBUG') || 0;
+
+ my $agentnum = $cgi->param('agentnum')
+ if $cgi->param('agentnum') =~ /^\d+/;
my $target_dt;
my @target_dates;
my %noon = (
hour => 12,
minute => 0,
- second => 0
+ second => 0,
);
-
my $now_dt = DateTime->now;
$now_dt = DateTime->new(
- month => $now_dt->month,
- day => $now_dt->day,
- year => $now_dt->year,
+ month => $now_dt->month,
+ day => $now_dt->day,
+ year => $now_dt->year,
%noon,
);
# Get target date from form
if ($cgi->param('target_date')) {
+ # DateTime::Format::DateParse would be better
my ($mm, $dd, $yy) = split /[\-\/]/,$cgi->param('target_date');
+ ( $yy, $mm, $dd ) = ( $mm, $dd, $yy ) if $mm > 1900;
+
$target_dt = DateTime->new(
- month => $mm,
- day => $dd,
- year => $yy,
+ month => $mm,
+ day => $dd,
+ year => $yy,
%noon,
- ) if $mm && $dd & $yy;
+ ) if $mm && $dd && $yy;
# Catch a date from the past: time only travels in one direction
- $target_dt = undef if $target_dt->epoch < $now_dt->epoch;
+ $target_dt = undef
+ unless $target_dt && $now_dt && $now_dt <= $target_dt;
}
# without a target date, default to tomorrow
unless ($target_dt) {
- $target_dt = DateTime->from_epoch( epoch => time() + 86400) ;
- $target_dt = DateTime->new(
- month => $target_dt->month,
- day => $target_dt->day,
- year => $target_dt->year,
- %noon
- );
+ $target_dt = $now_dt->clone->add( days => 1 );
}
- # If multiple_billing_dates checkbox selected, create a range of dates
- # from today until the given report date. Otherwise, use target date only.
- if ($cgi->param('multiple_billing_dates')) {
+ my $report_title = FS::cust_payby->future_autobill_report_title;
+ my $report_subtitle = sprintf(
+ '(%s through %s)',
+ $now_dt->mdy('/'),
+ $target_dt->mdy('/'),
+ );
+
+ # Create a range of dates from today until the given report date
+ # (leaving the probably useless 'quick-report' mode, but disabled)
+ if ( 1 || $cgi->param('multiple_billing_dates')) {
my $walking_dt = DateTime->from_epoch(epoch => $now_dt->epoch);
until ($walking_dt->epoch > $target_dt->epoch) {
push @target_dates, $walking_dt->epoch;
push @target_dates, $target_dt->epoch;
}
- # List all customers with an auto-bill method
- #
- # my %cust_payby = map {$_->custnum => $_} qsearch({
- # table => 'cust_payby',
- # hashref => {
- # weight => { op => '>', value => '0' },
- # paydate => { op => '>', value => $target_dt->ymd },
- # },
- # order_by => " ORDER BY weight DESC ",
- # });
-
# List all customers with an auto-bill method that's not expired
my %cust_payby = map {$_->custnum => $_} qsearch({
- table => 'cust_payby',
- hashref => {
- weight => { op => '>', value => '0' },
- },
- order_by => " ORDER BY weight DESC ",
- extra_sql => " AND ( payby = 'CHEK' OR ( paydate > '".$target_dt->ymd."')) ",
+ table => 'cust_payby',
+ addl_from => 'JOIN cust_main USING (custnum)',
+ hashref => { weight => { op => '>', value => '0' }},
+ order_by => " ORDER BY weight DESC ",
+ extra_sql =>
+ "AND (
+ cust_payby.payby IN ('CHEK','DCHK','DCHEK')
+ OR ( cust_payby.paydate > '".$target_dt->ymd."')
+ )
+ AND " . $FS::CurrentUser::CurrentUser->agentnums_sql
+ . ($agentnum ? "AND cust_main.agentnum = $agentnum" : ''),
});
+ my $completion_target = scalar(keys %cust_payby) * scalar( @target_dates );
+ my $completion_progress = 0;
+
+ my $fakebill_time = time();
my %abreport;
my @rows;
+ my %pmt_type_subtotal;
local $@;
local $SIG{__DIE__};
- my $temp_dbh = myconnect();
- eval { # Creating sandbox dbh where all connections are to be rolled back
- local $FS::UID::dbh = $temp_dbh;
+
+ eval { # Sandbox
+
+ # Supress COMMIT statements
+ my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
+ local $FS::UID::ForceObeyAutoCommit = 1;
+
+ # Suppress notices generated by billing events
+ local $FS::Misc::DISABLE_ALL_NOTICES = 1;
+
+ # Bypass payment processing, recording a fake payment
+ local $FS::cust_main::Billing_Realtime::BOP_TESTING = 1;
+ local $FS::cust_main::Billing_Realtime::BOP_TESTING_SUCCESS = 1;
- # Generate report data into @rows
+ my $savepoint_label = 'future_autobill';
+ savepoint_create( $savepoint_label );
+
+ warn sprintf "Report involves %s customers", scalar keys %cust_payby
+ if $DEBUG;
+
+ # Run bill_and_collect(), for each customer with an autobill payment method,
+ # for each day represented in the report
for my $custnum (keys %cust_payby) {
my $cust_main = qsearchs('cust_main', {custnum => $custnum});
+ warn "-- Processing custnum $custnum\n"
+ if $DEBUG;
+
# walk forward through billing dates
for my $query_epoch (@target_dates) {
+ $FS::cust_main::Billing_Realtime::BOP_TESTING_TIMESTAMP = $query_epoch;
my $return_bill = [];
- eval { # Don't let an error on one customer crash the report
- my $error = $cust_main->bill(
- time => $query_epoch,
- return_bill => $return_bill,
- no_usage_reset => 1,
- );
- die "$error (simulating future billing)" if $error;
- };
- warn ("$@: (future_autobill custnum:$custnum)");
-
- if (@{$return_bill}) {
- my $inv = $return_bill->[0];
- push @rows,{
- name => $cust_main->name,
- _date => $inv->_date,
- cells => [
- { class => 'gridreport', value => $custnum },
- { class => 'gridreport',
- value => '<a href="/view/cust_main.cgi?"'.$custnum.'">'.$cust_main->name.'</a>',
- bypass_filter => 1,
- },
- { class => 'gridreport', value => $inv->charged, format => 'money' },
- { class => 'gridreport', value => DateTime->from_epoch(epoch=>$inv->_date)->ymd },
- { class => 'gridreport', value => ($cust_payby{$custnum}->payby || $cust_payby{$custnum}->paytype) },
- { class => 'gridreport', value => $cust_payby{$custnum}->paymask },
- ]
- };
- }
+ warn "---- Set billtime to ".
+ DateTime->from_epoch( epoch => $query_epoch )."\n"
+ if $DEBUG;
+
+ my $error = $cust_main->bill_and_collect(
+ time => $query_epoch,
+ return_bill => $return_bill,
+ no_usage_reset => 1,
+ fake => 1,
+ );
+
+ warn "!!! $error (simulating future billing)\n" if $error;
+
+ my $statustext = sprintf(
+ '%s,Simulating upcoming invoices and payments',
+ int( ( ++$completion_progress / $completion_target ) * 100 )
+ );
+ $job->update_statustext( $statustext ) if $job;
+ warn "[ $completion_progress / $completion_target ] $statustext\n"
+ if $DEBUG;
}
- $temp_dbh->rollback;
- } # /foreach $custnum
+
+ # Generate report rows from recorded payments in cust_pay
+ for my $cust_pay (
+ qsearch( cust_pay => {
+ custnum => $custnum,
+ _date => { op => '>=', value => $fakebill_time },
+ })
+ ) {
+ push @rows,{
+ name => $cust_main->name,
+ _date => $cust_pay->_date,
+ cells => [
+
+ # Customer number
+ { class => 'gridreport', value => $custnum },
+
+ # Customer name / customer link
+ { class => 'gridreport',
+ value => qq{<a href="${fsurl}view/cust_main.cgi?${custnum}">} . encode_entities( $cust_main->name ). '</a>',
+ bypass_filter => 1
+ },
+
+ # Amount
+ { class => 'gridreport',
+ value => $cust_pay->paid,
+ format => 'money'
+ },
+
+ # Transaction Date
+ { class => 'gridreport',
+ value => DateTime->from_epoch( epoch => $cust_pay->_date )->ymd
+ },
+
+ # Payment Method
+ { class => 'gridreport',
+ value => encode_entities( $cust_pay->paycardtype || $cust_pay->payby ),
+ },
+
+ # Masked Payment Instrument
+ { class => 'gridreport',
+ value => encode_entities( $cust_pay->paymask ),
+ },
+ ]
+ };
+
+ $pmt_type_subtotal{ $cust_pay->paycardtype || $cust_pay-> payby }
+ += $cust_pay->paid;
+
+ } # /foreach payment
+
+ # Roll back database at the end of each customer
+ # Makes the report slighly slower, but ensures only one customer row
+ # locked at a time
+
+ warn "-- custnum $custnum -- rollback()\n" if $DEBUG;
+ savepoint_rollback( $savepoint_label );
+ dbh->rollback if $oldAutoCommit;
+
+ } # /foreach $custnum
}; # /eval
- warn("$@") if $@;
+ warn("future_autobill.html report generated error $@") if $@;
# Sort output by date, and format for output to grid-report.html
my @cells = [
}
$pm->prospect_contact
];
- ''
},
sub {
my $pr = shift->part_referral;
my @fields = fields('cdr');
push @fields, 'ratename';
+push @fields, map "cdr_termination.$_", qw( rated_price rated_seconds rated_minutes rated_granularity status svcnum );
+
my $labels = FS::cdr->table_info->{'fields'};
$labels->{ratename} = 'Rate plan';
+$labels->{'cdr_termination.rated_price'} = 'Termination rated price';
+$labels->{'cdr_termination.rated_seconds'} = 'Termination rated seconds';
+$labels->{'cdr_termination.rated_minutes'} = 'Termination rated minutes';
+$labels->{'cdr_termination.rated_granularity'} = 'Termination rated granularity';
+$labels->{'cdr_termination.status'} = 'Termination status';
+$labels->{'cdr_termination.svcnum'} = 'Termination service';
my $conf = new FS::Conf;
my $default_phone_countrycode =
%>
<FORM ACTION="cust_event.html" METHOD="GET">
- <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
- <TR>
- <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Search options</FONT></TH>
- </TR>
+ <FONT CLASS="fsinnerbox-title"><% emt('Search options') %></FONT>
+ <TABLE CLASS="fsinnerbox">
<% include( '/elements/tr-select-agent.html', 'disable_empty'=>0 ) %>
'field' => 'event_status',
'multiple' => 1,
'all_selected' => 1,
- 'size' => 5,
+ 'size' => 6,
'options' => [ qw( done_Y done_S done_N failed new locked ) ],
'option_labels' => { done_Y => 'Completed normally',
done_S => 'Completed, with an error',
'curr_value' => scalar( $cgi->param('cust_status') ),
&>
+ <& /elements/tr-input-beginning_ending.html &>
+
</FORM>
</TABLE>
--- /dev/null
+<% $server->process %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+my $server = new FS::UI::Web::JSRPC
+ 'FS::Report::Queued::FutureAutobill::make_report',
+ $cgi;
+
+</%init>
<%doc>
-Display date selector for the future_autobill.html report
+Display pre-report page for the Future Auto Bill Transactions report
+
+Report runs in the queue. Once the report is generated, user is
+redirected to the report results.
</%doc>
-<% include('/elements/header.html', 'Future Auto-Bill Transactions' ) %>
+<% include('/elements/header.html', $report_title ) %>
+
+
+% if ( FS::TaxEngine->new->info->{batch} ) {
+ <div style="font-color: red">
+ NOTE: This report is disabled due to tax engine configuration
+ </div>
-<FORM ACTION="future_autobill.html" METHOD="GET">
-<TABLE>
-<& /elements/tr-input-date-field.html,
- {
- name => 'target_date',
- value => $target_date,
- label => emt('Target billing date').': ',
- required => 1
- }
-&>
+% } else {
-<& /elements/tr-checkbox.html,
- 'label' => emt('Multiple billing dates (slow)').': ',
- 'field' => 'multiple_billing_dates',
- 'value' => '1',
-&>
+ <FORM NAME="future_autobill" ID="future_autobill">
+ <TABLE>
+ <& /elements/tr-input-date-field.html,
+ {
+ name => 'target_date',
+ value => $target_date,
+ label => emt('Target billing date').': ',
+ required => 1
+ }
+ &>
-</TABLE>
+ <% include('/elements/tr-select-agent.html',
+ 'label' => 'For agent: ',
+ 'disable_empty' => 0,
+ )
+ %>
+ </TABLE>
+ <BR>
-<BR>
-<INPUT TYPE="submit" VALUE="<% mt('Get Report') |h %>">
+ <INPUT ID="future_autobill_submit" TYPE="submit" VALUE="<% mt('Get Report') |h %>">
+ </FORM>
-</FORM>
+ <% include( '/elements/progress-init.html',
+ 'future_autobill',
+ [ qw( agentnum target_date ) ],
+ 'report_future_autobill-queued_job.html',
+ )
+ %>
+
+ <script type="text/javascript">
+ $('#future_autobill').submit( function( event ) {
+ $('#future_autobill').prop( 'disabled', true );
+ $('#future_autobill_submit').prop( 'disabled', true );
+ event.preventDefault();
+ process();
+ });
+ </script>
+
+% }
<% include('/elements/footer.html') %>
<%init>
+use FS::cust_payby;
+use FS::CurrentUser;
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
-my $target_date = DateTime->from_epoch(epoch=>(time()+86400))->mdy('/');
+my $target_date = DateTime->now->add(days => 1)->mdy('/');
+my $report_title = FS::cust_payby->future_autobill_report_title;
</%init>
+
<TD COLSPAN=5><% $cust_main->contact |h %></TD>
% if ( $conf->exists('show_ss') ) {
<TH ALIGN="right"><% mt('SS#') |h %></TH>
- <TD><% $conf->exists('unmask_ss')
- ? $cust_main->ss
- : $cust_main->masked('ss') || ' ' %></TD>
+ <TD>
+ <span id="ss_span" style="white-space:nowrap;">
+ <% $cust_main->masked('ss') || ' ' %>
+% if (
+% $cust_main->ss
+% && $FS::CurrentUser::CurrentUser->access_right('Unmask customer SSN')
+% ) {
+ <& /elements/link-replace_element_text.html, {
+ target_id => 'ss_span',
+ replace_text => $cust_main->ss,
+ element_type => 'span'
+ } &>
+% }
+ </span>
+ </TD>
% }
</TR>
% if ( $conf->exists('cust_main-enable_spouse') and
<TR>
<TH ALIGN="right"><% $stateid_label %></TH>
- <TD><% $cust_main->masked('stateid') || ' ' %></TD>
+ <TD>
+ <span id="stateid_span" style="white-space:nowrap;">
+ <% $cust_main->masked('stateid') || ' ' %>
+% if (
+% $cust_main->stateid
+% && $FS::CurrentUser::CurrentUser->access_right('Unmask customer DL')
+% ) {
+ <& /elements/link-replace_element_text.html, {
+ target_id => 'stateid_span',
+ replace_text => $cust_main->stateid,
+ element_type => 'span'
+ } &>
+% }
+ </span>
+ </TD>
<TH ALIGN="right"><% $stateid_state_label %></TH>
<TD><% $cust_main->stateid_state || ' ' %></TD>
</TR>
% my $bgcolor1 = '#ffffff';
% my $bgcolor2 = '#eeeeee';
% my $bgcolor = $bgcolor2;
+% my $count = 0;
% foreach my $cust_contact ( @cust_contacts ) {
% my $contact = $cust_contact->contact;
% my $td = qq(<TD CLASS="grid" BGCOLOR="$bgcolor">);
Enabled
%# <FONT SIZE="-1"><A HREF="XXX">disable</A>
%# <A HREF="XXX">re-email</A></FONT>
+ <FONT SIZE="-1">
+ <& /elements/change_password.html,
+ 'contact_num' => $cust_contact->contactnum,
+ 'custnum' => $cust_contact->custnum,
+ 'no_label_display' => '',
+ 'label' => 'change password',
+ 'curr_value' => '',
+ 'pre_pwd_field_label' => 'contact'.$count.'_',
+ &>
+ </FONT>
% } else {
Disabled
%# <FONT SIZE="-1"><A HREF="XXX">enable</A></FONT>
% } else {
% $bgcolor = $bgcolor1;
% }
+% $count++;
% }
</TABLE>
%}
# residential customers have a default "invisible" contact, but if they
# somehow get more than one contact, show them
-my $display = scalar(@cust_contacts) > 1;
+my $display = scalar(@cust_contacts) > 0;
</%init>
## condition => sub { $payby{MCHK} },
#},
{
- label => 'Batch Electronic check refund',
+ label => 'Enter electronic check refund',
popup => "edit/cust_refund.cgi?popup=1;payby=CHEK;custnum=$custnum",
actionlabel => 'Enter electronic check refund',
width => 440,
% foreach my $prospect_contact ( $prospect_main->prospect_contact ) {
% my $contact = $prospect_contact->contact;
<TR>
- <TH ALIGN="right"><% $prospect_contact->contact_classname %> Contact</TD>
- <TD BGCOLOR="#FFFFFF"><% $contact->line %></TD>
+ <TH ALIGN="right" VALIGN="top"><% $prospect_contact->contact_classname %> Contact</TH>
+ <TD BGCOLOR="#FFFFFF">
+ <% $contact->line %><br>
+ <table>
+% for my $row ( $contact->contact_email ) {
+ <tr><th>E-Mail:</th><td><% $row->emailaddress %></td></tr>
+% }
+% for my $row ( $contact->contact_phone ) {
+ <tr><th><% $row->phone_type->typename %>:</th><td><% $row->phonenum_pretty %></td></tr>
+% }
+% if ( $prospect_contact->comment ) {
+ <tr><th>Comment:</th><td><% $prospect_contact->comment %></td></tr>
+% }
+ </table>
+ </TD>
</TR>
%}
{ field => 'routernum', value_callback => \&router },
'speed_down',
'speed_up',
+ 'speed_test_down',
+ 'speed_test_up',
+ 'speed_test_latency',
{ field => 'ip_addr', value_callback => \&ip_addr },
{ field => 'sectornum', value_callback => \§ornum },
{ field => 'mac_addr', type=>'mac_addr', value_callback => \&mac_addr },
--- /dev/null
+<% $server->process %>
+<%init>
+
+my @args = $cgi->param('arg');
+my %param = ();
+ while ( @args ) {
+ my( $field, $value ) = splice(@args, 0, 2);
+ unless ( exists( $param{$field} ) ) {
+ $param{$field} = $value;
+ } elsif ( ! ref($param{$field}) ) {
+ $param{$field} = [ $param{$field}, $value ];
+ } else {
+ push @{$param{$field}}, $value;
+ }
+ }
+
+my $exportnum;
+my $method;
+for (grep /^*_script$/, keys %param) {
+ $exportnum = $param{$param{$_}.'_exportnum'};
+ $method = $param{$param{$_}.'_script'};
+}
+
+my $part_export = qsearchs('part_export', { 'exportnum'=> $exportnum, } )
+ or die "unknown exportnum $exportnum";
+
+my $class = 'FS::part_export::'.$part_export->{Hash}->{exporttype}.'::'.$method;
+
+my $server = new FS::UI::Web::JSRPC $class, $cgi;
+
+</%init>
\ No newline at end of file
+++ /dev/null
-body {
- background-color:#e8e8e8;
- //font-family:Arial, Verdana, Helvetica, sans-serif;
- //font-size:12px;
- //color:#0D0700;
-}
-
-body, li, ol, p, table, td, th, tr, a, ul, blockquote, div {
- //font-family:Arial, Verdana, Helvetica, sans-serif;
- //font-size:12px;
- color:#0D0700;
-}
-
-a {
- //color:#00527f;
- text-decoration:none;
-}
-
-a:hover {
- text-decoration:underline;
-}
-td.page{
- border-style:solid;
- border-width:2px;
- border-color:#cccccc;
- background-color:#f8f8f8;
- padding:10px;
-}
-
-#menu_ul {
- padding: 0;
- //width: 840px;
- margin: 0 auto;
-}
-
-#menu_ul li {
- float: left;
- list-style: none;
- position: relative;
- border-right: 4px solid #e8e8e8;
-}
-
-#menu_ul a {
- display: block;
- padding: 6px 8px;
- color: #525151;
- font-size: 13px;
- font-weight: bold;
- white-space: nowrap;
- background: #cccccc;
- -moz-border-radius-topleft:8px;
- -moz-border-radius-topright:8px;
- -webkit-border-radius-topleft:8px;
- -webkit-border-radius-topright:8px;
- border-top-left-radius:8px;
- border-top-right-radius:8px;
-}
-
-#menu_ul a:hover {
- text-decoration:none;
-}
-
-#menu_ul ul {
- margin:0;
- padding:0;
- display:none;
- position: absolute;
- top: 100%;
- left: -1px;
- background: #ae2099;
- border: 1px solid #ffffff;
-}
-
-#menu_ul ul li {
- float: none;
- border-style: none;
-}
-
-#menu_ul ul a {
- padding: 4px 10px;
- color: #ffffff;
- font-size: 12px;
- font-weight: normal;
- background: transparent;
-}
-
-#menu_ul ul a:hover {
- background: #7e0079;
- -moz-border-radius-topleft:0px;
- -moz-border-radius-topright:0px;
- -webkit-border-radius-topleft:0px;
- -webkit-border-radius-topright:0px;
- border-top-left-radius:0px;
- border-top-right-radius:0px;
-}
-
-#menu_ul a.current_menu, #menu_ul a.hover {
- color: #ffffff;
- background: #7e0079;
-}
-
-#menu_ul img {
- vertical-align:middle;
- width: 7px;
- height: 4px;
- border-style: none;
- margin-left: 10px;
-}
\ No newline at end of file
+++ /dev/null
-<TR>
- <TD ALIGN="right">Card number</TD>
- <TD COLSPAN=6>
- <TABLE>
- <TR>
- <TD>
- <INPUT TYPE="text" NAME="payinfo" SIZE=20 MAXLENGTH=19 VALUE="<? echo $payinfo ?>"> </TD>
- <TD>Exp.</TD>
- <TD>
- <SELECT NAME="month">
- <? $months = array( '01', '02', '03' ,'04', '05', '06', '07', '08', '09', '10', '11', '12' );
- foreach ( $months AS $m ) {
- ?>
- <OPTION <? if ($m == $month) { echo 'SELECTED'; } ?>><? echo $m; ?>
- <? } ?>
- </SELECT>
- </TD>
- <TD> / </TD>
- <TD>
- <SELECT NAME="year">
- <? $years = array( '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025', '2026' );
- foreach ( $years as $y ) {
- ?>
- <OPTION <? if ($y == $year ) { echo 'SELECTED'; } ?>><? echo $y; ?>
- <? } ?>
- </SELECT>
- </TD>
- </TR>
- </TABLE>
- </TD>
-</TR>
-<? if ( $withcvv ) { ?>
- <TR>
- <TD ALIGN="right">CVV2 (<A HREF="javascript:myopen('cvv2.html','cvv2','toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=no,resizable=yes,copyhistory=no,width=480,height=288')">help</A>)</TD>
- <TD><INPUT TYPE="text" NAME="paycvv" VALUE="" SIZE=4 MAXLENGTH=4></TD>
- </TR>
-<? } ?>
-<TR>
- <TD ALIGN="right">Exact name on card</TD>
- <TD COLSPAN=6><INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="payname" VALUE="<? echo $payname; ?>"></TD>
-</TR>
-
-<? $lf = $freeside->mason_comp(array(
- 'session_id' => $_COOKIE['session_id'],
- 'comp' => '/elements/location.html',
- 'args' => [
- 'no_asterisks' , 1,
- #'address1_label' , 'Card billing address',
- 'address1_label' , 'Card billing address',
- ],
- ));
- echo $lf['output'];
-?>
+++ /dev/null
-<? if ($ach_read_only) { $bgShade = 'BGCOLOR="#ffffff"'; } ?>
-<TR>
- <TD ALIGN="right">Account type</TD>
- <TD <? echo $bgShade; ?>>
- <? if ($ach_read_only) { echo htmlspecialchars($paytype); ?>
- <INPUT TYPE="hidden" NAME="paytype" VALUE="<? echo $paytype; ?>">
- <? } else { ?>
- <SELECT NAME="paytype">
- <? foreach ( $paytypes AS $pt ) { ?>
- <OPTION <? if ($pt == $paytype ) { echo 'SELECTED'; } ?> VALUE="<? echo $pt; ?>"><? echo $pt; ?>
- <? } ?>
- </SELECT>
- <? } ?>
- </TD>
-</TR><TR>
- <TD ALIGN="right">Account number</TD>
- <TD <? echo $bgShade; ?>>
- <? if ($ach_read_only) { echo htmlspecialchars($payinfo1); ?>
- <INPUT TYPE="hidden" NAME="payinfo1" VALUE="<? echo $payinfo1; ?>">
- <? } else { ?>
- <INPUT TYPE="text" NAME="payinfo1" SIZE=10 MAXLENGTH=20 VALUE="<? echo $payinfo1; ?>">
- <? } ?>
- </TD>
-</TR><TR>
- <TD ALIGN="right">ABA/Routing number</TD>
- <TD <? echo $bgShade; ?>>
- <? if ($ach_read_only) { echo htmlspecialchars($payinfo2); ?>
- <INPUT TYPE="hidden" NAME="payinfo2" VALUE="<? echo $payinfo2; ?>">
- <? } else { ?>
- <INPUT TYPE="text" NAME="payinfo2" SIZE=10 MAXLENGTH=9 VALUE="<? echo $payinfo2; ?>"></TD>
- <? } ?>
-</TR><TR>
- <TD ALIGN="right">Bank name</TD>
- <TD <? echo $bgShade; ?>>
- <? if ($ach_read_only) { echo htmlspecialchars($payname); ?>
- <INPUT TYPE="hidden" NAME="payname" VALUE="<? echo $payname; ?>"></TD>
- <? } else { ?>
- <INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="payname" VALUE="<? echo $payname; ?>"></TD>
- <? } ?>
-</TR><TR>
-
- <? if ($show_paystate) { ?>
- <TR>
- <TD ALIGN="right">Bank state</TD>
- <TD <? echo $bgShade; ?>>
- <? if ($ach_read_only) { echo htmlspecialchars($paystate); ?>
- <INPUT TYPE="hidden" NAME="paystate" VALUE="<? echo $paystate; ?>"></TD>
- <? } else { ?>
- <SELECT NAME="paystate">
- <? foreach ( $states AS $s ) { ?>
- <OPTION <? if ($s == $paystate ) { echo 'SELECTED'; } ?>><? echo $s; ?>
- <? } ?>
- </SELECT></TD>
- <? } ?>
- </TR>
- <? } ?>
-
- <? if ($show_ss) { ?>
- <TR>
- <TD ALIGN="right">Account holder<BR>Social security or tax ID #</TD>
- <TD <? echo $bgShade; ?>>
- <? if ($ach_read_only) { echo htmlspecialchars($ss); ?>
- <INPUT TYPE="hidden" NAME="ss" VALUE="<? echo $ss; ?>"></TD>
- <? } else { ?>
- <INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="ss" VALUE="<? echo $ss; ?>"></TD>
- <? } ?>
- </TR>
- <? } ?>
-
- <? if ($show_stateid) { ?>
- <TR>
- <TD ALIGN="right">Account holder<BR><? echo $stateid_label; ?></TD>
- <TD <? echo $bgShade; ?>>
- <? if ($ach_read_only) { echo htmlspecialchars($stateid); ?>
- <INPUT TYPE="hidden" NAME="stateid" VALUE="<? echo $stateid; ?>"></TD>
- <TD <? echo $bgShade; ?>> <? echo $stateid_state; ?>
- <INPUT TYPE="hidden" NAME="stateid_state" VALUE="<? echo $stateid_state; ?>"></TD>
- <? } else { ?>
- <INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="stateid" VALUE="<? echo $stateid; ?>"></TD>
- <TD ALIGN="right"><? echo $stateid_state_label; ?></TD>
- <TD><SELECT NAME="stateid_state">
- <? foreach ( $states AS $s ) { ?>
- <OPTION <? if ($s == $stateid_state ) { echo 'SELECTED'; } ?>><? echo $s; ?>
- <? } ?>
- </SELECT></TD>
- <? } ?>
- </TR>
- <? } ?>
+++ /dev/null
-<? if ($error) { ?>
- <FONT SIZE="+1" COLOR="#ff0000"><? echo htmlspecialchars($error); echo '<BR><BR>'; ?></FONT>
-<? } ?>
+++ /dev/null
-</BODY></HTML>
+++ /dev/null
-<!DOCTYPE html>
-<HTML>
- <HEAD>
- <TITLE>
- <? echo $title; ?>
- </TITLE>
- <link href="css/default.css" rel="stylesheet" type="text/css"/>
- <script type="text/javascript" src="js/jquery.js"></script>
- <script type="text/javascript" src="js/menu.js"></script>
- </HEAD>
- <BODY>
- <FONT SIZE=5><? echo $title; ?></FONT>
- <BR><BR>
-
+++ /dev/null
-<?
-
-require_once('session.php');
-
-$skin_info = $freeside->skin_info( array(
- 'session_id' => $_COOKIE['session_id'],
-) );
-
-extract($skin_info);
-
-?>
-<style type="text/css">
-#menu_ul ul li {
- display: inline;
- width: 100%;
-}
-</style>
-
-<ul id="menu_ul">
-
-<?
-
- $menu_array = array(
- 'payment.php Payments',
- 'payment_cc.php Credit Card Payment',
- 'payment_ach.php Electronic check payment',
- 'payment_paypal.php PayPal payment',
- 'payment_webpay.php Webpay payment',
- );
- $submenu = array();
-
- foreach ($menu_array AS $menu_item) {
- if ( preg_match('/^\s*$/', $menu_item) ) {
- print_menu($submenu, $current_menu, $menu_disable);
- $submenu = array();
- } else {
- $submenu[] = $menu_item;
- }
- }
- print_menu($submenu, $current_menu, $menu_disable);
-
- function print_menu($submenu_array, $current_menu, $menu_disable) {
- if ( count($submenu_array) == 0 ) { return; }
-
- $links = array();
- $labels = array();
- foreach ($submenu_array AS $submenu_item) {
- $pieces = preg_split('/\s+/', $submenu_item, 2, PREG_SPLIT_NO_EMPTY);
- $links[] = $pieces[0];
- $labels[] = $pieces[1];
- }
-
- print_link($links[0], $labels[0], $current_menu, $links);
-
- if ( count($links) > 1 ) {
- if ( in_array( $current_menu, $links ) ) {
- echo '<img src="images/dropdown_arrow_white.gif">';
- } else {
- echo '<img src="images/dropdown_arrow_white.gif" style="display:none;">';
- echo '<img src="images/dropdown_arrow_grey.gif">';
- }
- }
-
- array_shift($links);
- array_shift($labels);
-
- echo '</a>';
-
- if ( count($links) > 0 ) {
- echo '<ul>';
- foreach ($links AS $link) {
- $label = array_shift($labels);
- if ( in_array($label, $menu_disable) == 0) {
- print_link($link, $label, $current_menu, array($link) );
- echo '</a></li>';
- }
- }
- echo '</ul>';
- }
-
- echo '</li>';
-
- }
-
- function print_link($link, $label, $current_menu, $search_array) {
- echo '<li><a href="'. $link. '"';
- if ( in_array( $current_menu, $search_array ) ) {
- echo ' class="current_menu"';
- }
- echo '>'. _($label);
- }
-
-?>
-
-</ul>
-
-<div style="clear:both;"></div>
-<table cellpadding="0" cellspacing="0" border="0" style="min-width:666px">
-<tr>
-<td class="page">
\ No newline at end of file
+++ /dev/null
-</td>
-</tr>
-</table>
+++ /dev/null
-<?
-
-require_once('freeside.class.php');
-$freeside = new FreesideSelfService();
-
-?>
+++ /dev/null
-<?php
-
-#pre-php 5.4 compatible version?
-function flatten($hash) {
- if ( !is_array($hash) ) return $hash;
- $flat = array();
-
- array_walk($hash, function($value, $key, &$to) {
- array_push($to, $key, $value);
- }, $flat);
-
- if ( PHP_VERSION_ID >= 50400 ) {
-
- #php 5.4+ (deb 7+)
- foreach ($hash as $key => $value) {
- $flat[] = $key;
- $flat[] = $value;
- }
-
- }
-
- return($flat);
-}
-
-#php 5.4+?
-#function flatten($hash) {
-# if ( !is_array($hash) ) return $hash;
-#
-# $flat = array();
-#
-# foreach ($hash as $key => $value) {
-# $flat[] = $key;
-# $flat[] = $value;
-# }
-#
-# return($flat);
-#}
-
-class FreesideSelfService {
-
- //Change this to match the location of your selfservice xmlrpc.cgi or daemon
- #var $URL = 'https://localhost/selfservice/xmlrpc.cgi';
- #var $URL = 'http://localhost/selfservice/xmlrpc.cgi';
- var $URL = 'http://localhost:8080/';
-
- function FreesideSelfService() {
- $this;
- }
-
- public function __call($name, $arguments) {
-
- error_log("[FreesideSelfService] $name called, sending to ". $this->URL);
-
- $request = xmlrpc_encode_request("FS.ClientAPI_XMLRPC.$name", flatten($arguments[0]));
- $context = stream_context_create( array( 'http' => array(
- 'method' => "POST",
- 'header' => "Content-Type: text/xml",
- 'content' => $request
- )));
- $file = file_get_contents($this->URL, false, $context);
- $response = xmlrpc_decode($file);
- // uncomment to trace everything
- //error_log(print_r($response, true));
- if (xmlrpc_is_fault($response)) {
- trigger_error("[FreesideSelfService] XML-RPC communication error: $response[faultString] ($response[faultCode])");
- } else {
- //error_log("[FreesideSelfService] $response");
- return $response;
- }
- }
-
-}
-
-?>
+++ /dev/null
-<?
- $error = $_GET['error'];
- if ( $error ) {
- $username = $_GET['username'];
- $domain = $_GET['domain'];
- $title ='Login Error';
- include('elements/header.php');
- include('elements/error.php');
-?>
- <TABLE BORDER=0 CELLSPACING=2 CELLPADDING=0>
- <TR>
- <TD>
- Sorry we were unable to locate your account with ip <? echo $username; ?> .
- </TD>
- </TR>
- </TABLE>
-<?
- include('elements/footer.php');
- }
- else { include('login.php'); }
-?>
-
-<? #include('login.php'); ?>
-
-
-<?
-#require('freeside.class.php');
-#$freeside = new FreesideSelfService();
-#
-#$login_info = $freeside->login_info();
-#
-#extract($login_info);
-#
-#$error = $_GET['error'];
-#if ( $error ) {
-# $username = $_GET['username'];
-# $domain = $_GET['domain'];
-#}
-
-?>
\ No newline at end of file
+++ /dev/null
-/*! jQuery v1.10.1 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license
-//@ sourceMappingURL=jquery-1.10.1.min.map
-*/
-(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.1",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=lt(),k=lt(),E=lt(),S=!1,A=function(){return 0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=bt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+xt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return At(e.replace(z,"$1"),t,n,i)}function st(e){return K.test(e+"")}function lt(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function ut(e){return e[b]=!0,e}function ct(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function pt(e,t,n){e=e.split("|");var r,i=e.length,a=n?null:t;while(i--)(r=o.attrHandle[e[i]])&&r!==t||(o.attrHandle[e[i]]=a)}function ft(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:e[t]===!0?t.toLowerCase():null}function dt(e,t){return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}function ht(e){return"input"===e.nodeName.toLowerCase()?e.defaultValue:t}function gt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function mt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function yt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function vt(e){return ut(function(t){return t=+t,ut(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.parentWindow;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.frameElement&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ct(function(e){return e.innerHTML="<a href='#'></a>",pt("type|href|height|width",dt,"#"===e.firstChild.getAttribute("href")),pt(B,ft,null==e.getAttribute("disabled")),e.className="i",!e.getAttribute("className")}),r.input=ct(function(e){return e.innerHTML="<input>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")}),pt("value",ht,r.attributes&&r.input),r.getElementsByTagName=ct(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ct(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ct(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=st(n.querySelectorAll))&&(ct(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ct(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=st(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ct(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=st(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},r.sortDetached=ct(function(e){return 1&e.compareDocumentPosition(n.createElement("div"))}),A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return gt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?gt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:ut,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=bt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?ut(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:ut(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?ut(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:ut(function(e){return function(t){return at(e,t).length>0}}),contains:ut(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:ut(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:vt(function(){return[0]}),last:vt(function(e,t){return[t-1]}),eq:vt(function(e,t,n){return[0>n?n+t:n]}),even:vt(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:vt(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:vt(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:vt(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}};for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=mt(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=yt(n);function bt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function xt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function wt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function Tt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function Ct(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function Nt(e,t,n,r,i,o){return r&&!r[b]&&(r=Nt(r)),i&&!i[b]&&(i=Nt(i,o)),ut(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||St(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:Ct(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=Ct(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=Ct(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function kt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=wt(function(e){return e===t},s,!0),p=wt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[wt(Tt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return Nt(l>1&&Tt(f),l>1&&xt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&kt(e.slice(l,r)),i>r&&kt(e=e.slice(r)),i>r&&xt(e))}f.push(n)}return Tt(f)}function Et(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=Ct(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?ut(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=bt(e)),n=t.length;while(n--)o=kt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Et(i,r))}return o};function St(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function At(e,t,n,i){var a,s,u,c,p,f=bt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&xt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}o.pseudos.nth=o.pseudos.eq;function jt(){}jt.prototype=o.filters=o.pseudos,o.setFilters=new jt,r.sortStable=b.split("").sort(A).join("")===b,p(),[0,0].sort(A),r.detectDuplicates=S,x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],!l||i&&!u||(n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav></:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="<div></div>",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)
-}),n=s=l=u=r=o=null,t}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e,r="boolean"==typeof t;return x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var o,a=0,s=x(this),l=t,u=e.match(T)||[];while(o=u[a++])l=r?l:!s.hasClass(o),s[l?"addClass":"removeClass"](o)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/<tbody/i,wt=/<|&#?\w+;/,Tt=/<(?:script|style|link)/i,Ct=/^(?:checkbox|radio)$/i,Nt=/checked\s*(?:[^=]|=\s*.checked.)/i,kt=/^$|\/(?:java|ecma)script/i,Et=/^true\/(.*)/,St=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,At={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:x.support.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1></$2>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1></$2>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?"<table>"!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle);
-u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){var t="boolean"==typeof e;return this.each(function(){(t?e:nn(this))?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("<iframe frameborder='0' width='0' height='0'/>").css("cssText","display:block !important")).appendTo(t.documentElement),t=(Pt[0].contentWindow||Pt[0].contentDocument).document,t.write("<!doctype html><html><body>"),t.close(),n=un(e,t),Pt.detach()),Gt[e]=n),n}function un(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,n){x.cssHooks[n]={get:function(e,r,i){return r?0===e.offsetWidth&&Xt.test(x.css(e,"display"))?x.swap(e,Qt,function(){return sn(e,n,i)}):sn(e,n,i):t},set:function(e,t,r){var i=r&&Rt(e);return on(e,t,r?an(e,n,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x.support.opacity||(x.cssHooks.opacity={get:function(e,t){return It.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=x.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===x.trim(o.replace($t,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=$t.test(o)?o.replace($t,i):o+" "+i)}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,n){return n?x.swap(e,{display:"inline-block"},Wt,[e,"marginRight"]):t}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,n){x.cssHooks[n]={get:function(e,r){return r?(r=Wt(e,n),Yt.test(r)?x(e).position()[n]+"px":r):t}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight||!x.support.reliableHiddenOffsets&&"none"===(e.style&&e.style.display||x.css(e,"display"))},x.expr.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+Zt[r]+t]=o[r]||o[r-2]||o[0];return i}},Ut.test(e)||(x.cssHooks[e+t].set=on)});var cn=/%20/g,pn=/\[\]$/,fn=/\r?\n/g,dn=/^(?:submit|button|image|reset|file)$/i,hn=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&hn.test(this.nodeName)&&!dn.test(e)&&(this.checked||!Ct.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace(fn,"\r\n")}}):{name:t.name,value:n.replace(fn,"\r\n")}}).get()}}),x.param=function(e,n){var r,i=[],o=function(e,t){t=x.isFunction(t)?t():null==t?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(n===t&&(n=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){o(this.name,this.value)});else for(r in e)gn(r,e[r],n,o);return i.join("&").replace(cn,"+")};function gn(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||pn.test(e)?r(e,i):gn(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)gn(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var mn,yn,vn=x.now(),bn=/\?/,xn=/#.*$/,wn=/([?&])_=[^&]*/,Tn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Cn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Nn=/^(?:GET|HEAD)$/,kn=/^\/\//,En=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,Sn=x.fn.load,An={},jn={},Dn="*/".concat("*");try{yn=o.href}catch(Ln){yn=a.createElement("a"),yn.href="",yn=yn.href}mn=En.exec(yn.toLowerCase())||[];function Hn(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(T)||[];if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function qn(e,n,r,i){var o={},a=e===jn;function s(l){var u;return o[l]=!0,x.each(e[l]||[],function(e,l){var c=l(n,r,i);return"string"!=typeof c||a||o[c]?a?!(u=c):t:(n.dataTypes.unshift(c),s(c),!1)}),u}return s(n.dataTypes[0])||!o["*"]&&s("*")}function _n(e,n){var r,i,o=x.ajaxSettings.flatOptions||{};for(i in n)n[i]!==t&&((o[i]?e:r||(r={}))[i]=n[i]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,n,r){if("string"!=typeof e&&Sn)return Sn.apply(this,arguments);var i,o,a,s=this,l=e.indexOf(" ");return l>=0&&(i=e.slice(l,e.length),e=e.slice(0,l)),x.isFunction(n)?(r=n,n=t):n&&"object"==typeof n&&(a="POST"),s.length>0&&x.ajax({url:e,type:a,dataType:"html",data:n}).done(function(e){o=arguments,s.html(i?x("<div>").append(x.parseHTML(e)).find(i):e)}).complete(r&&function(e,t){s.each(r,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:yn,type:"GET",isLocal:Cn.test(mn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?_n(_n(e,x.ajaxSettings),t):_n(x.ajaxSettings,e)},ajaxPrefilter:Hn(An),ajaxTransport:Hn(jn),ajax:function(e,n){"object"==typeof e&&(n=e,e=t),n=n||{};var r,i,o,a,s,l,u,c,p=x.ajaxSetup({},n),f=p.context||p,d=p.context&&(f.nodeType||f.jquery)?x(f):x.event,h=x.Deferred(),g=x.Callbacks("once memory"),m=p.statusCode||{},y={},v={},b=0,w="canceled",C={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c){c={};while(t=Tn.exec(a))c[t[1].toLowerCase()]=t[2]}t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=v[n]=v[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(p.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)m[t]=[m[t],e[t]];else C.always(e[C.status]);return this},abort:function(e){var t=e||w;return u&&u.abort(t),k(0,t),this}};if(h.promise(C).complete=g.add,C.success=C.done,C.error=C.fail,p.url=((e||p.url||yn)+"").replace(xn,"").replace(kn,mn[1]+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=x.trim(p.dataType||"*").toLowerCase().match(T)||[""],null==p.crossDomain&&(r=En.exec(p.url.toLowerCase()),p.crossDomain=!(!r||r[1]===mn[1]&&r[2]===mn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(mn[3]||("http:"===mn[1]?"80":"443")))),p.data&&p.processData&&"string"!=typeof p.data&&(p.data=x.param(p.data,p.traditional)),qn(An,p,n,C),2===b)return C;l=p.global,l&&0===x.active++&&x.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Nn.test(p.type),o=p.url,p.hasContent||(p.data&&(o=p.url+=(bn.test(o)?"&":"?")+p.data,delete p.data),p.cache===!1&&(p.url=wn.test(o)?o.replace(wn,"$1_="+vn++):o+(bn.test(o)?"&":"?")+"_="+vn++)),p.ifModified&&(x.lastModified[o]&&C.setRequestHeader("If-Modified-Since",x.lastModified[o]),x.etag[o]&&C.setRequestHeader("If-None-Match",x.etag[o])),(p.data&&p.hasContent&&p.contentType!==!1||n.contentType)&&C.setRequestHeader("Content-Type",p.contentType),C.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Dn+"; q=0.01":""):p.accepts["*"]);for(i in p.headers)C.setRequestHeader(i,p.headers[i]);if(p.beforeSend&&(p.beforeSend.call(f,C,p)===!1||2===b))return C.abort();w="abort";for(i in{success:1,error:1,complete:1})C[i](p[i]);if(u=qn(jn,p,n,C)){C.readyState=1,l&&d.trigger("ajaxSend",[C,p]),p.async&&p.timeout>0&&(s=setTimeout(function(){C.abort("timeout")},p.timeout));try{b=1,u.send(y,k)}catch(N){if(!(2>b))throw N;k(-1,N)}}else k(-1,"No Transport");function k(e,n,r,i){var c,y,v,w,T,N=n;2!==b&&(b=2,s&&clearTimeout(s),u=t,a=i||"",C.readyState=e>0?4:0,c=e>=200&&300>e||304===e,r&&(w=Mn(p,C,r)),w=On(p,w,C,c),c?(p.ifModified&&(T=C.getResponseHeader("Last-Modified"),T&&(x.lastModified[o]=T),T=C.getResponseHeader("etag"),T&&(x.etag[o]=T)),204===e||"HEAD"===p.type?N="nocontent":304===e?N="notmodified":(N=w.state,y=w.data,v=w.error,c=!v)):(v=N,(e||!N)&&(N="error",0>e&&(e=0))),C.status=e,C.statusText=(n||N)+"",c?h.resolveWith(f,[y,N,C]):h.rejectWith(f,[C,N,v]),C.statusCode(m),m=t,l&&d.trigger(c?"ajaxSuccess":"ajaxError",[C,p,c?y:v]),g.fireWith(f,[C,N]),l&&(d.trigger("ajaxComplete",[C,p]),--x.active||x.event.trigger("ajaxStop")))}return C},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,n){return x.get(e,t,n,"script")}}),x.each(["get","post"],function(e,n){x[n]=function(e,r,i,o){return x.isFunction(r)&&(o=o||i,i=r,r=t),x.ajax({url:e,type:n,dataType:o,data:r,success:i})}});function Mn(e,n,r){var i,o,a,s,l=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),o===t&&(o=e.mimeType||n.getResponseHeader("Content-Type"));if(o)for(s in l)if(l[s]&&l[s].test(o)){u.unshift(s);break}if(u[0]in r)a=u[0];else{for(s in r){if(!u[0]||e.converters[s+" "+u[0]]){a=s;break}i||(i=s)}a=a||i}return a?(a!==u[0]&&u.unshift(a),r[a]):t}function On(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(p){return{state:"parsererror",error:a?p:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),x.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=a.head||x("head")[0]||a.documentElement;return{send:function(t,i){n=a.createElement("script"),n.async=!0,e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,t){(t||!n.readyState||/loaded|complete/.test(n.readyState))&&(n.onload=n.onreadystatechange=null,n.parentNode&&n.parentNode.removeChild(n),n=null,t||i(200,"success"))},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(t,!0)}}}});var Fn=[],Bn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Fn.pop()||x.expando+"_"+vn++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(n,r,i){var o,a,s,l=n.jsonp!==!1&&(Bn.test(n.url)?"url":"string"==typeof n.data&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Bn.test(n.data)&&"data");return l||"jsonp"===n.dataTypes[0]?(o=n.jsonpCallback=x.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,l?n[l]=n[l].replace(Bn,"$1"+o):n.jsonp!==!1&&(n.url+=(bn.test(n.url)?"&":"?")+n.jsonp+"="+o),n.converters["script json"]=function(){return s||x.error(o+" was not called"),s[0]},n.dataTypes[0]="json",a=e[o],e[o]=function(){s=arguments},i.always(function(){e[o]=a,n[o]&&(n.jsonpCallback=r.jsonpCallback,Fn.push(o)),s&&x.isFunction(a)&&a(s[0]),s=a=t}),"script"):t});var Pn,Rn,Wn=0,$n=e.ActiveXObject&&function(){var e;for(e in Pn)Pn[e](t,!0)};function In(){try{return new e.XMLHttpRequest}catch(t){}}function zn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}x.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&In()||zn()}:In,Rn=x.ajaxSettings.xhr(),x.support.cors=!!Rn&&"withCredentials"in Rn,Rn=x.support.ajax=!!Rn,Rn&&x.ajaxTransport(function(n){if(!n.crossDomain||x.support.cors){var r;return{send:function(i,o){var a,s,l=n.xhr();if(n.username?l.open(n.type,n.url,n.async,n.username,n.password):l.open(n.type,n.url,n.async),n.xhrFields)for(s in n.xhrFields)l[s]=n.xhrFields[s];n.mimeType&&l.overrideMimeType&&l.overrideMimeType(n.mimeType),n.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");try{for(s in i)l.setRequestHeader(s,i[s])}catch(u){}l.send(n.hasContent&&n.data||null),r=function(e,i){var s,u,c,p;try{if(r&&(i||4===l.readyState))if(r=t,a&&(l.onreadystatechange=x.noop,$n&&delete Pn[a]),i)4!==l.readyState&&l.abort();else{p={},s=l.status,u=l.getAllResponseHeaders(),"string"==typeof l.responseText&&(p.text=l.responseText);try{c=l.statusText}catch(f){c=""}s||!n.isLocal||n.crossDomain?1223===s&&(s=204):s=p.text?200:404}}catch(d){i||o(-1,d)}p&&o(s,c,p,u)},n.async?4===l.readyState?setTimeout(r):(a=++Wn,$n&&(Pn||(Pn={},x(e).unload($n)),Pn[a]=r),l.onreadystatechange=r):r()},abort:function(){r&&r(t,!0)}}}});var Xn,Un,Vn=/^(?:toggle|show|hide)$/,Yn=RegExp("^(?:([+-])=|)("+w+")([a-z%]*)$","i"),Jn=/queueHooks$/,Gn=[nr],Qn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Yn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),a=(x.cssNumber[e]||"px"!==o&&+r)&&Yn.exec(x.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,x.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};function Kn(){return setTimeout(function(){Xn=t}),Xn=x.now()}function Zn(e,t,n){var r,i=(Qn[t]||[]).concat(Qn["*"]),o=0,a=i.length;for(;a>o;o++)if(r=i[o].call(n,t,e))return r}function er(e,t,n){var r,i,o=0,a=Gn.length,s=x.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;var t=Xn||Kn(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;for(;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:Xn||Kn(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(tr(c,u.opts.specialEasing);a>o;o++)if(r=Gn[o].call(u,e,c,u.opts))return r;return x.map(c,Zn,u),x.isFunction(u.opts.start)&&u.opts.start.call(e,u),x.fx.timer(x.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function tr(e,t){var n,r,i,o,a;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=x.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(er,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Qn[n]=Qn[n]||[],Qn[n].unshift(t)},prefilter:function(e,t){t?Gn.unshift(e):Gn.push(e)}});function nr(e,t,n){var r,i,o,a,s,l,u=this,c={},p=e.style,f=e.nodeType&&nn(e),d=x._data(e,"fxshow");n.queue||(s=x._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,u.always(function(){u.always(function(){s.unqueued--,x.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(x.support.inlineBlockNeedsLayout&&"inline"!==ln(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",x.support.shrinkWrapBlocks||u.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],Vn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show"))continue;c[r]=d&&d[r]||x.style(e,r)}if(!x.isEmptyObject(c)){d?"hidden"in d&&(f=d.hidden):d=x._data(e,"fxshow",{}),o&&(d.hidden=!f),f?x(e).show():u.done(function(){x(e).hide()}),u.done(function(){var t;x._removeData(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)a=Zn(f?d[r]:0,r,u),r in d||(d[r]=a.start,f&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function rr(e,t,n,r,i){return new rr.prototype.init(e,t,n,r,i)}x.Tween=rr,rr.prototype={constructor:rr,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=rr.propHooks[this.prop];return e&&e.get?e.get(this):rr.propHooks._default.get(this)},run:function(e){var t,n=rr.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):rr.propHooks._default.set(this),this}},rr.prototype.init.prototype=rr.prototype,rr.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},rr.propHooks.scrollTop=rr.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(ir(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(nn).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),a=function(){var t=er(this,x.extend({},e),o);(i||x._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return"string"!=typeof e&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=null!=e&&e+"queueHooks",o=x.timers,a=x._data(this);if(n)a[n]&&a[n].stop&&i(a[n]);else for(n in a)a[n]&&a[n].stop&&Jn.test(n)&&i(a[n]);for(n=o.length;n--;)o[n].elem!==this||null!=e&&o[n].queue!==e||(o[n].anim.stop(r),t=!1,o.splice(n,1));(t||!r)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=x._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,a=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function ir(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=Zt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:ir("show"),slideUp:ir("hide"),slideToggle:ir("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},x.timers=[],x.fx=rr.prototype.init,x.fx.tick=function(){var e,n=x.timers,r=0;for(Xn=x.now();n.length>r;r++)e=n[r],e()||n[r]!==e||n.splice(r--,1);n.length||x.fx.stop(),Xn=t},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){Un||(Un=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(Un),Un=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){x.offset.setOffset(this,e,t)});var n,r,o={top:0,left:0},a=this[0],s=a&&a.ownerDocument;if(s)return n=s.documentElement,x.contains(n,a)?(typeof a.getBoundingClientRect!==i&&(o=a.getBoundingClientRect()),r=or(s),{top:o.top+(r.pageYOffset||n.scrollTop)-(n.clientTop||0),left:o.left+(r.pageXOffset||n.scrollLeft)-(n.clientLeft||0)}):o},x.offset={setOffset:function(e,t,n){var r=x.css(e,"position");"static"===r&&(e.style.position="relative");var i=x(e),o=i.offset(),a=x.css(e,"top"),s=x.css(e,"left"),l=("absolute"===r||"fixed"===r)&&x.inArray("auto",[a,s])>-1,u={},c={},p,f;l?(c=i.position(),p=c.top,f=c.left):(p=parseFloat(a)||0,f=parseFloat(s)||0),x.isFunction(t)&&(t=t.call(e,n,o)),null!=t.top&&(u.top=t.top-o.top+p),null!=t.left&&(u.left=t.left-o.left+f),"using"in t?t.using.call(e,u):i.css(u)}},x.fn.extend({position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===x.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(n=e.offset()),n.top+=x.css(e[0],"borderTopWidth",!0),n.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-x.css(r,"marginTop",!0),left:t.left-n.left-x.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.offsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);x.fn[e]=function(i){return x.access(this,function(e,i,o){var a=or(e);return o===t?a?n in a?a[n]:a.document.documentElement[i]:e[i]:(a?a.scrollTo(r?x(a).scrollLeft():o,r?o:x(a).scrollTop()):e[i]=o,t)},e,i,arguments.length,null)}});function or(e){return x.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}x.each({Height:"height",Width:"width"},function(e,n){x.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){x.fn[i]=function(i,o){var a=arguments.length&&(r||"boolean"!=typeof i),s=r||(i===!0||o===!0?"margin":"border");return x.access(this,function(n,r,i){var o;return x.isWindow(n)?n.document.documentElement["client"+e]:9===n.nodeType?(o=n.documentElement,Math.max(n.body["scroll"+e],o["scroll"+e],n.body["offset"+e],o["offset"+e],o["client"+e])):i===t?x.css(n,r,s):x.style(n,r,i,s)},n,a?i:t,a,null)}})}),x.fn.size=function(){return this.length},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:(e.jQuery=e.$=x,"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}))})(window);
+++ /dev/null
-$(document).ready(function() {
- $('#menu_ul > li').hover(function(){
- $('a:first', this).addClass('hover');
- $('ul:first', this).show();
- if ($('.current_menu:first', this).length == 0) {
- $('img[src*="dropdown_arrow_white"]', this).show();
- $('img[src*="dropdown_arrow_grey"]', this).hide();
- }
- }, function(){
- $('ul:first', this).hide();
- $('a:first', this).removeClass('hover');
- if ($('.current_menu:first', this).length == 0) {
- $('img[src*="dropdown_arrow_white"]', this).hide();
- $('img[src*="dropdown_arrow_grey"]', this).show();
- }
- });
-});
+++ /dev/null
-<?
-
-require('freeside.class.php');
-$freeside = new FreesideSelfService();
-
-$ip = $_SERVER['REMOTE_ADDR'];
-# need a routine here to get mac address from radius account table based on ip address. Every else should be good to go.
-$mac_addr = '1234567890FF';
-
-$response = $freeside->login( array(
- 'username' => $mac_addr,
- 'domain' => 'ip_mac',
-) );
-
-#error_log("[login] received response from freeside: $response");
-
-$error = $response['error'];
-
-if ( $error ) {
-
- header('Location:index.php?username='. urlencode($mac).
- '&domain='. urlencode($domain).
- '&email='. urlencode($email).
- '&error='. urlencode($error)
- );
- die();
-
-}
-
-// sucessful login
-
-$session_id = $response['session_id'];
-
-error_log("[login] logged into freeside with session_id=$session_id, setting cookie");
-
-// now what? for now, always redirect to the main page (or the select a
-// customer diversion).
-// eventually, other options?
-
-setcookie('session_id', $session_id);
-
-if ( $response['custnum'] || $response['svcnum'] ) {
-
- header("Location:main.php");
- die();
- //1;
-
-} elseif ( $response['customers'] ) {
-var_dump($response['customers']);
-?>
-
- <? $title ='Select customer'; include('elements/header.php'); ?>
- <? include('elements/error.php'); ?>
-
- <FORM NAME="SelectCustomerForm" ACTION="process_select_cust.php" METHOD=POST>
- <INPUT TYPE="hidden" NAME="action" VALUE="switch_cust">
-
- <TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=2 CELLPADDING=0>
-
- <TR>
- <TH ALIGN="right">Customer </TH>
- <TD>
- <SELECT NAME="custnum" ID="custnum" onChange="custnum_changed()">
- <OPTION VALUE="">Select a customer
- <? foreach ( $response['customers'] AS $custnum => $customer ) { ?>
- <OPTION VALUE="<? echo $custnum ?>"><? echo htmlspecialchars( $customer ) ?>
- <? } ?>
- </SELECT>
- </TD>
- </TR>
-
- <TR>
- <TD COLSPAN=2 ALIGN="center"><INPUT TYPE="submit" ID="submit" VALUE="Select customer" DISABLED></TD>
- </TR>
-
- </TABLE>
- </FORM>
-
- <SCRIPT TYPE="text/javascript">
-
- function custnum_changed () {
- var form = document.SelectCustomerForm;
- if ( form.custnum.selectedIndex > 0 ) {
- form.submit.disabled = false;
- } else {
- form.submit.disabled = true;
- }
- }
-
- </SCRIPT>
-
- <? include('elements/footer.php'); ?>
-
-<?
-
-// } else {
-//
-// die 'login successful, but unrecognized info (no custnum, svcnum or customers)';
-
-}
-
-?>
\ No newline at end of file
+++ /dev/null
-<? $title ='Make A Payment'; include('elements/header.php'); ?>
-<? $current_menu = 'payment.php'; include('elements/menu.php'); ?>
-
-<?
-$customer_info = $freeside->customer_info_short( array(
- 'session_id' => $_COOKIE['session_id'],
-) );
-
-
-if ( isset($customer_info['error']) && $customer_info['error'] ) {
- $error = $customer_info['error'];
- header('Location:index.php?error='. urlencode($error));
- die();
-}
-
-extract($customer_info);
-
-?>
-
-<? include('elements/error.php'); ?>
-
-<P>Hello <? echo htmlspecialchars($name); ?></P>
-
-<P>Your current balance is <B>$<? echo $balance ?></B> how would you like to make a payment today?</P>
-
-<div STYLE="margin-left: 25px;">
-<a href="payment_cc.php">Credit card payment</A><BR><BR>
-<a href="payment_ach.php">Electronic check payment</A><BR><BR>
-<a href="payment_paypal.php">PayPal payment</A><BR><BR>
-<a href="payment_webpay.php">Webpay payment</A><BR><BR>
-</div>
-
-<? include('elements/menu_footer.php'); ?>
-<? include('elements/footer.php'); ?>
\ No newline at end of file
+++ /dev/null
-<? $title ='Make A Payment'; include('elements/header.php'); ?>
-<? $current_menu = 'payment.php'; include('elements/menu.php'); ?>
-
-<? include('elements/error.php'); ?>
-
-<FONT SIZE="+1">
-<a href="payment_cc.php">Credit card payment</A><BR><BR>
-<a href="payment_ach.php">Electronic check payment</A><BR><BR>
-<a href="payment_paypal.php">PayPal payment</A><BR><BR>
-<a href="payment_webpay.php">Webpay payment</A><BR><BR>
-</FONT>
-
-<? include('elements/menu_footer.php'); ?>
-<? include('elements/footer.php'); ?>
\ No newline at end of file
+++ /dev/null
-<? $title ='Electronic Check Payment'; include('elements/header.php'); ?>
-<? $current_menu = 'payment_ach.php'; include('elements/menu.php'); ?>
-<?
-
-if ( isset($_POST['amount']) && $_POST['amount'] ) {
-
- $payment_results = $freeside->process_payment(array(
- 'session_id' => $_COOKIE['session_id'],
- 'payby' => 'CHEK',
- 'amount' => $_POST['amount'],
- 'payinfo1' => $_POST['payinfo1'],
- 'payinfo2' => $_POST['payinfo2'],
- 'month' => 12,
- 'year' => 2037,
- 'payname' => $_POST['payname'],
- 'paytype' => $_POST['paytype'],
- 'paystate' => $_POST['paystate'],
- 'ss' => $_POST['ss'],
- 'stateid' => $_POST['stateid'],
- 'stateid_state' => $_POST['stateid_state'],
- 'save' => $_POST['save'],
- 'auto' => $_POST['auto'],
- 'paybatch' => $_POST['paybatch'],
- //'discount_term' => $discount_term,
- ));
-
- if ( $payment_results['error'] ) {
- $payment_error = $payment_results['error'];
- } else {
- $receipt_html = $payment_results['receipt_html'];
- }
-
-}
-
-if ( $receipt_html ) { ?>
-
- Your payment was processed successfully. Thank you.<BR><BR>
- <? echo $receipt_html; ?>
-
-<? } else {
-
- $payment_info = $freeside->payment_info( array(
- 'session_id' => $_COOKIE['session_id'],
- 'payment_payby' => 'CHEK',
- ) );
-
- if ( isset($payment_info['error']) && $payment_info['error'] ) {
- $error = $payment_info['error'];
- header('Location:index.php?error='. urlencode($error));
- die();
- }
-
- extract($payment_info);
-
- $error = $payment_error;
-
-?>
-
- <? include('elements/error.php'); ?>
-
- <FORM NAME="OneTrueForm" METHOD="POST" ACTION="payment_ach.php" onSubmit="document.OneTrueForm.process.disabled=true">
-
- <TABLE>
- <TR>
- <TD ALIGN="right">Amount Due</TD>
- <TD>
- <TABLE><TR><TD BGCOLOR="#ffffff">
- $<? echo sprintf("%.2f", $balance) ?>
- </TD></TR></TABLE>
- </TD>
- </TR>
-
- <TR>
- <TD ALIGN="right">Payment amount</TD>
- <TD>
- <TABLE><TR><TD BGCOLOR="#ffffff">
- $<INPUT TYPE="text" NAME="amount" SIZE=8 VALUE="<? echo sprintf("%.2f", $balance) ?>">
- </TD></TR></TABLE>
- </TD>
- </TR>
- <? // include('elements/discount_term.php') ?>
-
- <? include('elements/check.php') ?>
-
- <? if ($ach_read_only) { ?>
- <? if ( $payby == 'CARD' ) { ?>
- <INPUT TYPE="hidden" NAME="auto" VALUE="1">
- <? } ?>
- </TD></TR>
- <? } else { ?>
- <TR>
- <TD COLSPAN=2>
- <INPUT TYPE="checkbox" <? if ( ! $save_unchecked ) { echo 'CHECKED'; } ?> NAME="save" VALUE="1">
- Remember this information
- </TD>
- </TR><TR>
- <TD COLSPAN=2>
- <INPUT TYPE="checkbox" <? if ( $payby == 'CARD' ) { echo ' CHECKED'; } ?> NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }">
- Charge future payments to this account automatically
- </TD>
- </TR>
- <? } ?>
-
- </TABLE>
- <BR>
- <INPUT TYPE="hidden" NAME="paybatch" VALUE="<? echo $paybatch; ?>">
- <INPUT TYPE="submit" NAME="process" VALUE="Process payment"> <!-- onClick="this.disabled=true"> -->
- </FORM>
-
-<? } ?>
-
-<? include('elements/menu_footer.php'); ?>
-<? include('elements/footer.php'); ?>
\ No newline at end of file
+++ /dev/null
-<? $title ='Credit Card Payment'; include('elements/header.php'); ?>
-<? $current_menu = 'payment_cc.php'; include('elements/menu.php'); ?>
-<?
-
-if ( isset($_POST['amount']) && $_POST['amount'] ) {
-
- $payment_results = $freeside->process_payment(array(
- 'session_id' => $_COOKIE['session_id'],
- 'payby' => 'CARD',
- 'amount' => $_POST['amount'],
- 'payinfo' => $_POST['payinfo'],
- 'paycvv' => $_POST['paycvv'],
- 'month' => $_POST['month'],
- 'year' => $_POST['year'],
- 'payname' => $_POST['payname'],
- 'address1' => $_POST['address1'],
- 'address2' => $_POST['address2'],
- 'city' => $_POST['city'],
- 'state' => $_POST['state'],
- 'zip' => $_POST['zip'],
- 'country' => $_POST['country'],
- 'save' => $_POST['save'],
- 'auto' => $_POST['auto'],
- 'paybatch' => $_POST['paybatch'],
- //'discount_term' => $discount_term,
- ));
-
- if ( $payment_results['error'] ) {
- $payment_error = $payment_results['error'];
- } else {
- $receipt_html = $payment_results['receipt_html'];
- }
-
-}
-
-if ( $receipt_html ) { ?>
-
- Your payment was processed successfully. Thank you.<BR><BR>
- <? echo $receipt_html; ?>
-
-<? } else {
-
- $payment_info = $freeside->payment_info( array(
- 'session_id' => $_COOKIE['session_id'],
- 'payment_payby' => 'CARD',
- ) );
-
- if ( isset($payment_info['error']) && $payment_info['error'] ) {
- $error = $payment_info['error'];
- header('Location:index.php?error='. urlencode($error));
- die();
- }
-
- extract($payment_info);
-
- $error = $payment_error;
-
- $tr_amount_fee = $freeside->mason_comp(array(
- 'session_id' => $_COOKIE['session_id'],
- 'comp' => '/elements/tr-amount_fee.html',
- 'args' => [ 'amount', $balance ],
- ));
- //$tr_amount_fee = $tr_amount_fee->{'error'} || $tr_amount_fee->{'output'};
- $tr_amount_fee = $tr_amount_fee['output'];
-
- ?>
-
- <? include('elements/error.php'); ?>
-
- <FORM NAME="OneTrueForm" METHOD="POST" ACTION="payment_cc.php" onSubmit="document.OneTrueForm.process.disabled=true">
-
- <TABLE>
- <TR>
- <TD ALIGN="right">Amount Due</TD>
- <TD COLSPAN=7>
- <TABLE><TR><TD>
- $<? echo sprintf("%.2f", $balance) ?>
- </TD></TR></TABLE>
- </TD>
- </TR>
-
- <? echo $tr_amount_fee; ?>
-
- <? //include('elements/discount_term.php') ?>
-
- <TR>
- <TD ALIGN="right">Card type</TD>
- <TD COLSPAN=7>
- <SELECT NAME="card_type"><OPTION></OPTION>
- <? foreach ( $card_types AS $ct ) { ?>
- <OPTION <? if ( $card_type == $ct ) { echo 'SELECTED'; } ?>
- VALUE="<? echo $ct; ?>"><? echo $ct; ?>
- <? } ?>
- </SELECT>
- </TD>
- </TR>
-
- <? include('elements/card.php'); ?>
-
- <TR>
- <TD COLSPAN=8>
- <INPUT TYPE="checkbox" <? if ( ! $save_unchecked ) { echo 'CHECKED'; } ?> NAME="save" VALUE="1">
- Remember this card and billing address
- </TD>
- </TR><TR>
- <TD COLSPAN=8>
- <INPUT TYPE="checkbox" <? if ( $payby == 'CARD' ) { echo ' CHECKED'; } ?> NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }">
- Charge future payments to this card automatically
- </TD>
- </TR>
- </TABLE>
- <BR>
- <INPUT TYPE="hidden" NAME="paybatch" VALUE="<? echo $paybatch ?>">
- <INPUT TYPE="submit" NAME="process" VALUE="Process payment"> <!-- onClick="this.disabled=true"> -->
- </FORM>
-
-<? } ?>
-
-<? include('elements/menu_footer.php'); ?>
-<? include('elements/footer.php'); ?>
\ No newline at end of file
+++ /dev/null
-<? $title ='Payment Confirmation'; include('elements/header.php'); ?>
-<? $current_menu = ''; include('elements/menu.php'); ?>
-<?
- $params = $_GET;
- $params['session_id'] = $_COOKIE['session_id'];
-
- //print_r($params);
- $payment_results = $freeside->finish_thirdparty($params);
-
- if ( isset($payment_results['error']) ) {
- $error = $payment_results['error'];
- include('elements/error.php');
- } else {
-?>
-<TABLE>
- <TR>
- <TH COLSPAN=2><FONT SIZE=+1><B>Your payment details</B></FONT></TH>
- </TR>
- <TR>
-<TR>
- <TD ALIGN="right">Payment #</TD>
- <TD BGCOLOR="#ffffff"><B><? echo($payment_results['paynum']); ?></B></TD>
-</TR>
-<TR>
- <TD ALIGN="right">Payment amount</TH>
- <TD BGCOLOR="#ffffff"><B>$<? printf('%.2f', $payment_results['paid']); ?></B>
- </TD>
-</TR>
-<TR>
- <TD ALIGN="right">Processing #</TD>
- <TD BGCOLOR="#ffffff"><B><? echo($payment_results['order_number']); ?></B>
- </TD>
-</TR>
-<? } ?>
\ No newline at end of file
+++ /dev/null
-<? $title ='PayPal Payment'; include('elements/header.php'); ?>
-<? $current_menu = 'payment_paypal.php'; include('elements/menu.php'); ?>
-<?
-if ( isset($_POST['amount']) && $_POST['amount'] ) {
-
- $payment_results = $freeside->start_thirdparty(array(
- 'session_id' => $_COOKIE['session_id'],
- 'method' => 'PAYPAL',
- 'amount' => $_POST['amount'],
- ));
-
- include('elements/post_thirdparty.php');
-
-} else {
-
- $payment_info = $freeside->payment_info( array(
- 'session_id' => $_COOKIE['session_id'],
- ) );
-
- $tr_amount_fee = $freeside->mason_comp(array(
- 'session_id' => $_COOKIE['session_id'],
- 'comp' => '/elements/tr-amount_fee.html',
- 'args' => [ 'amount', $payment_info['balance'] ],
- ));
- $tr_amount_fee = $tr_amount_fee['output'];
-
- include('elements/error.php'); ?>
-<FORM NAME="OneTrueForm" METHOD="POST" ACTION="payment_paypal.php">
- <TABLE>
- <TR>
- <TD ALIGN="right">Amount Due</TD>
- <TD>$<? echo sprintf('%.2f', $payment_info['balance']); ?></TD>
- </TR>
- <? echo $tr_amount_fee; ?>
- </TABLE>
- <BR>
- <INPUT TYPE="submit" NAME="process" VALUE="Start payment">
-</FORM>
-<? } ?>
-<? include('elements/menu_footer.php'); ?>
-<? include('elements/footer.php'); ?>
\ No newline at end of file
+++ /dev/null
-<? $title ='Webpay Payment'; include('elements/header.php'); ?>
-<? $current_menu = 'payment_webpay.php'; include('elements/menu.php'); ?>
-<?
-if ( isset($_POST['amount']) && $_POST['amount'] ) {
-
- $payment_results = $freeside->start_thirdparty(array(
- 'session_id' => $_COOKIE['session_id'],
- 'method' => 'CC',
- 'amount' => $_POST['amount'],
- ));
-
- include('elements/post_thirdparty.php');
-
-} else {
-
- $payment_info = $freeside->payment_info( array(
- 'session_id' => $_COOKIE['session_id'],
- ) );
-
- $tr_amount_fee = $freeside->mason_comp(array(
- 'session_id' => $_COOKIE['session_id'],
- 'comp' => '/elements/tr-amount_fee.html',
- 'args' => [ 'amount', $payment_info['balance'] ],
- ));
- $tr_amount_fee = $tr_amount_fee['output'];
-
- include('elements/error.php'); ?>
-<FORM NAME="OneTrueForm" METHOD="POST" ACTION="payment_webpay.php">
- <TABLE>
- <TR>
- <TD ALIGN="right">Amount Due</TD>
- <TD>$<? echo sprintf('%.2f', $payment_info['balance']); ?></TD>
- </TR>
- <? echo $tr_amount_fee; ?>
- </TABLE>
- <BR>
- <INPUT TYPE="submit" NAME="process" VALUE="Start payment">
-</FORM>
-<? } ?>
-<? include('elements/menu_footer.php'); ?>
-<? include('elements/footer.php'); ?>
\ No newline at end of file
+<?
+
+require_once('session.php');
+
+$page = basename($_SERVER['SCRIPT_FILENAME']);
+
+$access = $freeside->check_access( array(
+ 'session_id' => $_COOKIE['session_id'],
+ 'page' => $page,
+) );
+
+if ($access['error']) {
+ header('Location:no_access.php?error='. urlencode($access['error']));
+ die();
+}
+
+?>
+
<!DOCTYPE html>
<HTML>
<HEAD>
require('freeside.class.php');
$freeside = new FreesideSelfService();
-$login_info = $freeside->login_info();
+$login_info = $freeside->login_info( array('session_id' => $_COOKIE['session_id'],));
extract($login_info);
<? if ( $phone_login ) { ?>
<B>OR</B><BR><BR>
-
+
<FORM ACTION="process_login.php" METHOD=POST>
<INPUT TYPE="hidden" NAME="session" VALUE="login">
<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=2 CELLPADDING=0>
<? } ?>
+<!--
+<BR><BR><A HREF="ip_login.php">Login by IP (<? echo $_SERVER['REMOTE_ADDR']; ?>) to make a payment.</A>
+-->
+
<? include('elements/footer.php'); ?>
--- /dev/null
+<?
+
+require('freeside.class.php');
+$freeside = new FreesideSelfService();
+
+$ip = $_SERVER['REMOTE_ADDR'];
+
+$mac = $freeside->get_mac_address( array('ip' => $ip, ) );
+
+$response = $freeside->login( array(
+ 'username' => $mac['mac_address'],
+ 'domain' => 'ip_mac',
+) );
+
+$error = $response['error'];
+
+if ( $error ) {
+
+ $title ='Login'; include('elements/header.php');
+ include('elements/error.php');
+ echo "Sorry "+$error;
+
+ // header('Location:index.php?username='. urlencode($mac).
+ // '&domain='. urlencode($domain).
+ // '&email='. urlencode($email).
+ // '&error='. urlencode($error)
+ // );
+
+}
+else {
+// sucessful login
+
+$session_id = $response['session_id'];
+
+error_log("[login] logged into freeside with session_id=$session_id, setting cookie");
+
+// now what? for now, always redirect to the main page (or the select a
+// customer diversion).
+// eventually, other options?
+
+setcookie('session_id', $session_id);
+
+if ( $response['custnum'] || $response['svcnum'] ) {
+
+ header("Location:main.php");
+ die();
+ //1;
+
+} elseif ( $response['customers'] ) {
+ //var_dump($response['customers']);
+?>
+
+ <? $title ='Select customer'; include('elements/header.php'); ?>
+ <? include('elements/error.php'); ?>
+
+ <FORM NAME="SelectCustomerForm" ACTION="process_select_cust.php" METHOD=POST>
+ <INPUT TYPE="hidden" NAME="action" VALUE="switch_cust">
+
+ <TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=2 CELLPADDING=0>
+
+ <TR>
+ <TH ALIGN="right">Customer </TH>
+ <TD>
+ <SELECT NAME="custnum" ID="custnum" onChange="custnum_changed()">
+ <OPTION VALUE="">Select a customer
+ <? foreach ( $response['customers'] AS $custnum => $customer ) { ?>
+ <OPTION VALUE="<? echo $custnum ?>"><? echo htmlspecialchars( $customer ) ?>
+ <? } ?>
+ </SELECT>
+ </TD>
+ </TR>
+
+ <TR>
+ <TD COLSPAN=2 ALIGN="center"><INPUT TYPE="submit" ID="submit" VALUE="Select customer" DISABLED></TD>
+ </TR>
+
+ </TABLE>
+ </FORM>
+
+ <SCRIPT TYPE="text/javascript">
+
+ function custnum_changed () {
+ var form = document.SelectCustomerForm;
+ if ( form.custnum.selectedIndex > 0 ) {
+ form.submit.disabled = false;
+ } else {
+ form.submit.disabled = true;
+ }
+ }
+
+ </SCRIPT>
+
+<?
+
+// } else {
+//
+// die 'login successful, but unrecognized info (no custnum, svcnum or customers)';
+
+} // multiple customers found
+
+} //successfull login
+
+?>
+
+ <? include('elements/footer.php'); ?>
--- /dev/null
+<!DOCTYPE html>
+<HTML>
+ <HEAD>
+ <TITLE>
+ Access Denied
+ </TITLE>
+ <link href="css/default.css" rel="stylesheet" type="text/css"/>
+ <script type="text/javascript" src="js/jquery.js"></script>
+ <script type="text/javascript" src="js/menu.js"></script>
+ </HEAD>
+ <BODY>
+ <FONT SIZE=5>Access Denied</FONT>
+ <BR><BR>
+<? $current_menu = 'no_access.php'; include('elements/menu.php'); ?>
+<?
+
+$customer_info = $freeside->customer_info_short( array(
+ 'session_id' => $_COOKIE['session_id'],
+) );
+
+if ( isset($customer_info['error']) && $customer_info['error'] ) {
+ $error = $customer_info['error'];
+ header('Location:index.php?error='. urlencode($error));
+ die();
+}
+
+extract($customer_info);
+
+?>
+
+<P>Sorry you do not have access to the page you are trying to reach.</P>
+
+<? include('elements/menu_footer.php'); ?>
+<? include('elements/footer.php'); ?>
\ No newline at end of file
require('freeside.class.php');
$freeside = new FreesideSelfService();
+$ip = $_SERVER['REMOTE_ADDR'];
+
+if ($_POST['domain'] == "ip_mac") {
+ $mac_addr = $freeside->get_mac_address( array('ip' => $ip, ) );
+ $_POST['username'] = $mac_addr['mac_address'];
+}
+
$response = $freeside->login( array(
'email' => strtolower($_POST['email']),
'username' => strtolower($_POST['username']),
if ( $error ) {
- header('Location:index.php?username='. urlencode($username).
- '&domain='. urlencode($domain).
- '&email='. urlencode($email).
+ header('Location:index.php?username='. urlencode($_POST['username']).
+ '&domain='. urlencode($_POST['domain']).
+ '&email='. urlencode($_POST['email']).
'&error='. urlencode($error)
);
die();
die();
} elseif ( $response['customers'] ) {
-var_dump($response['customers']);
+ //var_dump($response['customers']);
?>
<? $title ='Select customer'; include('elements/header.php'); ?>
$self->{'Customers'} = $self->MemberOf->Clone;
+ my $RecordType = $self->RecordType;
+ my $uri_type = $RecordType eq 'Ticket' ? 'ticket' : "RT::$RecordType";
+
+ $self->{'Customers'}->Limit( FIELD => 'Base',
+ OPERATOR => 'STARTSWITH',
+ VALUE => 'fsck.com-rt://%/'.$uri_type.'/',
+ );
+
for my $fstable (qw(cust_main cust_svc)) {
$self->{'Customers'}->Limit(