Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan@freeside.biz>
Mon, 1 Oct 2018 19:03:01 +0000 (12:03 -0700)
committerIvan Kohler <ivan@freeside.biz>
Mon, 1 Oct 2018 19:03:01 +0000 (12:03 -0700)
224 files changed:
FS/FS/AccessRight.pm
FS/FS/Auth/internal.pm
FS/FS/ClientAPI/MasonComponent.pm
FS/FS/ClientAPI/MyAccount.pm
FS/FS/ClientAPI_XMLRPC.pm
FS/FS/Conf.pm
FS/FS/IP_Mixin.pm
FS/FS/Misc.pm
FS/FS/Misc/FixIPFormat.pm [new file with mode: 0644]
FS/FS/Misc/Savepoint.pm [new file with mode: 0644]
FS/FS/Record.pm
FS/FS/Report/Queued/FutureAutobill.pm [new file with mode: 0644]
FS/FS/Report/Table.pm
FS/FS/Schema.pm
FS/FS/TemplateItem_Mixin.pm
FS/FS/Template_Mixin.pm
FS/FS/UI/Web.pm
FS/FS/UID.pm
FS/FS/Upgrade.pm
FS/FS/access_group.pm
FS/FS/access_user.pm
FS/FS/access_user_log.pm
FS/FS/addr_block.pm
FS/FS/agent.pm
FS/FS/cdr/Import.pm
FS/FS/cdr/ani_networks.pm
FS/FS/cdr/telapi_voip.pm
FS/FS/contact.pm
FS/FS/cust_bill.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_bill_void.pm
FS/FS/cust_event.pm
FS/FS/cust_main.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_main/Billing_Batch.pm
FS/FS/cust_main/Billing_Realtime.pm
FS/FS/cust_main/Search.pm
FS/FS/cust_pay.pm
FS/FS/cust_pay_batch.pm
FS/FS/cust_payby.pm
FS/FS/cust_pkg.pm
FS/FS/cust_pkg/Import.pm
FS/FS/deploy_zone.pm
FS/FS/log_context.pm
FS/FS/msg_template/email.pm
FS/FS/part_event/Condition.pm
FS/FS/part_event/Condition/agent.pm
FS/FS/part_event/Condition/cust_birthdate.pm [new file with mode: 0644]
FS/FS/part_event/Condition/cust_pay_batch_declined.pm
FS/FS/part_event/Condition/has_referral_custnum.pm
FS/FS/part_event/Condition/invoice_has_not_been_sent.pm [new file with mode: 0644]
FS/FS/part_event_condition_option.pm
FS/FS/part_export.pm
FS/FS/part_export/a2billing.pm
FS/FS/part_export/acct_opensrs.pm
FS/FS/part_export/aradial.pm
FS/FS/part_export/bandwidth_com.pm
FS/FS/part_export/broadband_nas.pm
FS/FS/part_export/broadband_snmp.pm
FS/FS/part_export/broadband_snmp_get.pm
FS/FS/part_export/broadworks.pm
FS/FS/part_export/grandstream.pm
FS/FS/part_export/http_status.pm
FS/FS/part_export/ikano.pm
FS/FS/part_export/nena2.pm
FS/FS/part_export/netsapiens.pm
FS/FS/part_export/northern_911.pm
FS/FS/part_export/phone_shellcommands.pm
FS/FS/part_export/saisei.pm
FS/FS/part_export/shellcommands.pm
FS/FS/part_export/sipwise.pm
FS/FS/part_export/sqlradius.pm
FS/FS/part_export/thinktel.pm
FS/FS/part_export/tower_towercoverage.pm
FS/FS/part_export/voip_ms.pm
FS/FS/part_pkg/discount_Mixin.pm
FS/FS/part_pkg/flat.pm
FS/FS/part_pkg/flat_introrate.pm
FS/FS/part_pkg/prorate_Mixin.pm
FS/FS/part_pkg/sql_external.pm
FS/FS/part_svc.pm
FS/FS/part_svc_column.pm
FS/FS/pay_batch/RBC.pm
FS/FS/pay_batch/paymentech.pm
FS/FS/svc_IP_Mixin.pm
FS/FS/svc_acct.pm
FS/FS/svc_broadband.pm
FS/FS/svc_cable.pm
FS/FS/svc_circuit.pm
FS/FS/svc_dsl.pm
FS/FS/svc_hardware.pm
FS/FS/svc_pbx.pm
FS/FS/tower.pm
FS/FS/tower_sector.pm
FS/bin/freeside-cdr-telapi-import
FS/bin/freeside-ipifony-download
FS/bin/freeside-paymentech-download
FS/bin/freeside-paymentech-upload
bin/cust_main.restore-paymask
bin/fakesmtpserver.pl
bin/move_svc_broadband_speeds.pl [new file with mode: 0755]
conf/invoice_html
conf/invoice_htmlsummary
conf/invoice_latex
conf/invoice_latexsummary
debian/control
fs_selfservice/FS-SelfService/cgi/card.html
fs_selfservice/FS-SelfService/cgi/selfservice.cgi
httemplate/browse/addr_block.cgi
httemplate/browse/discount.html
httemplate/browse/part_pkg.cgi
httemplate/browse/part_svc.cgi
httemplate/config/config-process.cgi
httemplate/edit/agent_payment_gateway.html
httemplate/edit/bulk-cust_pkg.html
httemplate/edit/cust_main-contacts.html
httemplate/edit/cust_main.cgi
httemplate/edit/cust_main/contacts_new.html
httemplate/edit/cust_main/name.html
httemplate/edit/cust_main/stateid.html
httemplate/edit/cust_refund.cgi
httemplate/edit/elements/edit.html
httemplate/edit/elements/part_export/broadband_snmp_get.html
httemplate/edit/elements/part_svc_column.html
httemplate/edit/elements/svc_Common.html
httemplate/edit/part_export.cgi
httemplate/edit/part_svc.cgi
httemplate/edit/process/access_user.html
httemplate/edit/process/cust_main-contacts.html
httemplate/edit/process/cust_refund.cgi
httemplate/edit/process/elements/process.html
httemplate/edit/process/part_pkg.cgi
httemplate/edit/process/saved_search.html
httemplate/edit/process/tower.html
httemplate/edit/svc_broadband.cgi
httemplate/edit/tower.html
httemplate/elements/broadband_snmp_get.html
httemplate/elements/change_password.html
httemplate/elements/city.html
httemplate/elements/contact.html
httemplate/elements/cust_payby_new.html [new file with mode: 0644]
httemplate/elements/header.html
httemplate/elements/link-replace_element_text.html [new file with mode: 0644]
httemplate/elements/menu.html
httemplate/elements/polygon.html
httemplate/elements/random_pass.html
httemplate/elements/select-country.html
httemplate/elements/select-month_year.html
httemplate/elements/select-state.html
httemplate/elements/select-table.html
httemplate/elements/tr-amount_fee.html
httemplate/elements/tr-select-cust_payby.html
httemplate/elements/tr-select-invoice.html
httemplate/elements/tr-select-payment_options.html
httemplate/elements/tr-select-router_block_ip.html
httemplate/elements/tr-tower_sectors.html
httemplate/elements/validate_password.html
httemplate/elements/validate_password_js.html [new file with mode: 0644]
httemplate/graph/cust_bill_pkg_discount.html
httemplate/graph/report_cust_bill_pkg_discount.html
httemplate/misc/cust_pkg-import.html
httemplate/misc/download-batch.cgi
httemplate/misc/edge_browser_check-fail_notice.html [new file with mode: 0644]
httemplate/misc/edge_browser_check-header.html [new file with mode: 0644]
httemplate/misc/edge_browser_check-iframe.html [new file with mode: 0644]
httemplate/misc/payment.cgi
httemplate/misc/process/change-password.html
httemplate/misc/process/payment.cgi
httemplate/misc/timeworked.html
httemplate/misc/xmlhttp-free_addresses_in_block.json.html [new file with mode: 0644]
httemplate/misc/xmlhttp-validate_password.html
httemplate/search/cust_bill_pkg_discount.html
httemplate/search/cust_event.html
httemplate/search/cust_pay_batch.cgi
httemplate/search/cust_timespan.html
httemplate/search/e911.html
httemplate/search/elements/checkbox-foot.html
httemplate/search/elements/grid-report.html
httemplate/search/elements/search.html
httemplate/search/future_autobill.html
httemplate/search/prospect_main.html
httemplate/search/report_cdr.html
httemplate/search/report_cust_event.html
httemplate/search/report_cust_timespan.html
httemplate/search/report_future_autobill-queued_job.html [new file with mode: 0644]
httemplate/search/report_future_autobill.html
httemplate/view/cust_main/contacts.html
httemplate/view/cust_main/contacts_new.html
httemplate/view/cust_main/menu.html
httemplate/view/prospect_main.html
httemplate/view/svc_broadband.cgi
httemplate/view/svc_export/run_script.cgi [new file with mode: 0644]
min_selfservice/css/default.css [deleted file]
min_selfservice/elements/card.php [deleted file]
min_selfservice/elements/check.php [deleted file]
min_selfservice/elements/error.php [deleted file]
min_selfservice/elements/footer.php [deleted file]
min_selfservice/elements/header.php [deleted file]
min_selfservice/elements/menu.php [deleted file]
min_selfservice/elements/menu_footer.php [deleted file]
min_selfservice/elements/session.php [deleted file]
min_selfservice/freeside.class.php [deleted file]
min_selfservice/images/cross.png [deleted file]
min_selfservice/images/dropdown_arrow_grey.gif [deleted file]
min_selfservice/images/dropdown_arrow_white.gif [deleted file]
min_selfservice/images/error.png [deleted file]
min_selfservice/images/tick.png [deleted file]
min_selfservice/index.php [deleted file]
min_selfservice/js/jquery.js [deleted file]
min_selfservice/js/menu.js [deleted file]
min_selfservice/login.php [deleted file]
min_selfservice/main.php [deleted file]
min_selfservice/payment.php [deleted file]
min_selfservice/payment_ach.php [deleted file]
min_selfservice/payment_cc.php [deleted file]
min_selfservice/payment_finish.php [deleted file]
min_selfservice/payment_paypal.php [deleted file]
min_selfservice/payment_webpay.php [deleted file]
ng_selfservice/elements/header.php
ng_selfservice/index.php
ng_selfservice/ip_login.php [new file with mode: 0644]
ng_selfservice/no_access.php [new file with mode: 0644]
ng_selfservice/process_login.php
rt/lib/RT/Record.pm

index 471e32a..1b581b2 100644 (file)
@@ -156,6 +156,8 @@ tie my %rights, 'Tie::IxHash',
     'View package definition costs', #NEWNEW
     'Change package start date',
     'Change package contract end date',
+    'Unmask customer DL',
+    'Unmask customer SSN',
   ],
   
   ###
@@ -509,4 +511,3 @@ L<FS::access_right>, L<FS::access_group>, L<FS::access_user>
 =cut
 
 1;
-
index eea4870..dfc5f30 100644 (file)
@@ -48,7 +48,9 @@ sub change_password {
   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 );
 
index 3a4bfe1..d615c27 100644 (file)
@@ -63,6 +63,7 @@ my %session_callbacks = (
       '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 );
 
index 66697ef..6cb0a7c 100644 (file)
@@ -87,6 +87,8 @@ sub skin_info {
   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 ) {
 
@@ -105,7 +107,7 @@ sub skin_info {
   $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");
@@ -120,6 +122,8 @@ sub skin_info {
     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) ) }
@@ -143,7 +147,93 @@ sub skin_info {
       ( 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
@@ -172,16 +262,31 @@ sub skin_info {
                  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 {
@@ -191,8 +296,8 @@ 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')),
@@ -239,11 +344,20 @@ sub login {
 
   } 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}))
           )
@@ -630,6 +744,8 @@ sub customer_info_short {
     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($_)
@@ -921,6 +1037,7 @@ sub payment_info {
   $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,
@@ -1730,20 +1847,34 @@ sub update_payby {
                            })
     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 {
@@ -3899,4 +4030,3 @@ sub _custoragent_session_custnum {
 }
 
 1;
-
index dcf34fd..fefa577 100644 (file)
@@ -227,6 +227,8 @@ sub ss2clientapi {
   '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',
   };
index 9b89187..fd05231 100644 (file)
@@ -781,6 +781,22 @@ my $validate_email = sub { $_[0] =~
   },
 
   {
+    '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%',
@@ -1215,6 +1231,7 @@ my $validate_email = sub { $_[0] =~
     '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,
   },
 
   {
@@ -1578,9 +1595,19 @@ and customer address. Include units.',
   { 
     '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' },
   },
 
   { 
@@ -1599,6 +1626,15 @@ and customer address. Include units.',
   },
 
   {
+    '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.',
@@ -1679,6 +1715,13 @@ and customer address. Include units.',
     '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',
@@ -1771,7 +1814,7 @@ and customer address. Include units.',
   {
     'key'         => 'passwordmin',
     'section'     => 'password',
-    'description' => 'Minimum password length (default 6)',
+    'description' => 'Minimum password length (default 8)',
     'type'        => 'text',
   },
 
@@ -2134,7 +2177,7 @@ and customer address. Include units.',
 
   {
     'key'         => 'unmask_ss',
-    'section'     => 'e-checks',
+    'section'     => 'deprecated',
     'description' => "Don't mask social security numbers in the web interface.",
     'type'        => 'checkbox',
   },
@@ -2753,6 +2796,13 @@ and customer address. Include units.',
   },
 
   {
+    '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.',
@@ -5961,4 +6011,3 @@ and customer address. Include units.',
 );
 
 1;
-
index beb41d2..fc3a014 100644 (file)
@@ -94,6 +94,15 @@ sub ip_check {
     $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') ) {
@@ -130,6 +139,10 @@ sub assign_ip_addr {
   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;
 
@@ -260,18 +273,22 @@ sub router {
   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 {
index 669c44e..fd2c325 100644 (file)
@@ -1,7 +1,7 @@
 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;
@@ -22,7 +22,6 @@ use Encode;
                  generate_ps generate_pdf do_print
                  csv_from_fixed
                  ocr_image
-                 bytes_substr
                  money_pretty
                );
 
@@ -44,6 +43,32 @@ Miscellaneous subroutines.  This module contains miscellaneous subroutines
 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
@@ -119,6 +144,12 @@ FS::UID->install_callback( sub {
 
 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)';
@@ -451,6 +482,11 @@ sub send_fax {
   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;
   };
@@ -870,6 +906,11 @@ global value and agentnum).
 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'} );
@@ -982,23 +1023,26 @@ sub ocr_image {
 
 =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
 
diff --git a/FS/FS/Misc/FixIPFormat.pm b/FS/FS/Misc/FixIPFormat.pm
new file mode 100644 (file)
index 0000000..3f9a19b
--- /dev/null
@@ -0,0 +1,124 @@
+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;
diff --git a/FS/FS/Misc/Savepoint.pm b/FS/FS/Misc/Savepoint.pm
new file mode 100644 (file)
index 0000000..f8e2c5f
--- /dev/null
@@ -0,0 +1,160 @@
+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
index 5de4ca7..9dd08cf 100644 (file)
@@ -2881,11 +2881,9 @@ to 127.0.0.1.
 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
@@ -2913,7 +2911,17 @@ Check/untaint IPv4 or IPv6 address.
 
 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 '';
@@ -3211,6 +3219,60 @@ sub ut_enumn {
     : '';
 }
 
+=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
@@ -3579,7 +3641,19 @@ sub _quote {
            && 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);
   }
diff --git a/FS/FS/Report/Queued/FutureAutobill.pm b/FS/FS/Report/Queued/FutureAutobill.pm
new file mode 100644 (file)
index 0000000..82c9021
--- /dev/null
@@ -0,0 +1,132 @@
+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;
index 0c4d9bf..7c4f973 100644 (file)
@@ -745,6 +745,12 @@ sub cust_bill_pkg_detail {
   
 }
 
+=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) = @_;
@@ -770,6 +776,60 @@ sub cust_bill_pkg_discount {
   $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) = @_;
@@ -1055,7 +1115,7 @@ sub calculate_churn_cust {
                                                       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
   ";
index 245fb68..2a7a9d1 100644 (file)
@@ -2736,6 +2736,7 @@ sub tables_hashref {
         '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, '', '', 
@@ -2749,7 +2750,7 @@ sub tables_hashref {
       ],
       'primary_key'  => 'paybatchnum',
       'unique'       => [],
-      'index'        => [ ['batchnum'], ['invnum'], ['custnum'] ],
+      'index'        => [ ['batchnum'], ['invnum'], ['custnum'],['status'] ],
       'foreign_keys' => [
                           { columns    => [ 'batchnum' ],
                             table      => 'pay_batch',
@@ -3269,7 +3270,7 @@ sub tables_hashref {
       '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
@@ -4890,6 +4891,9 @@ sub tables_hashref {
         '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' ] ],
@@ -4927,6 +4931,8 @@ sub tables_hashref {
         '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' ] ],
@@ -4957,8 +4963,9 @@ sub tables_hashref {
         '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' ], ],
@@ -5965,6 +5972,7 @@ sub tables_hashref {
         'path',           'varchar',     '', 2*$char_d, '', '',
         '_date',                   @date_type,          '', '',
         'render_seconds',     'int', 'NULL',        '', '', '',
+        'pid',                'int', 'NULL',        '', '', '',
       ],
       'primary_key'  => 'lognum',
       'unique'       => [],
index 28fbd59..28ef845 100644 (file)
@@ -107,14 +107,13 @@ Returns a formatted time period for this line item.
 =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;
 
index 88fd4e8..34e9e6e 100644 (file)
@@ -19,7 +19,7 @@ use HTML::Entities;
 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;
@@ -299,7 +299,7 @@ before that line item (quotations only)
 
 =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
@@ -825,7 +825,7 @@ sub print_generic {
         );
     }
 
-    if ( $conf->exists('invoice_usesummary', $agentnum) ) {
+    if ( $conf->config_bool('invoice_usesummary', $agentnum) ) {
       $invoice_data{'summarypage'} = $summarypage = 1;
     }
 
@@ -933,9 +933,9 @@ sub print_generic {
 
   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 = ();
@@ -1092,7 +1092,7 @@ sub print_generic {
     }
   } 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
@@ -1196,6 +1196,9 @@ sub print_generic {
 
     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;
@@ -1204,13 +1207,28 @@ sub print_generic {
     $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;
@@ -1232,9 +1250,56 @@ sub print_generic {
       }
       $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',
@@ -1281,27 +1346,36 @@ sub print_generic {
   #$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,
@@ -1322,6 +1396,14 @@ sub print_generic {
       $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
@@ -1335,13 +1417,14 @@ sub print_generic {
         $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;
     }
@@ -1982,7 +2065,7 @@ sub balance_due_msg {
   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)
@@ -2190,8 +2273,7 @@ sub generate_email {
       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 {
 
@@ -2207,7 +2289,11 @@ sub generate_email {
     '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',
   );
 
@@ -2286,7 +2372,11 @@ sub generate_email {
                          '    </title>',
                          '  </head>',
                          '  <body bgcolor="#e8e8e8">',
-                         Encode::encode_utf8($html),
+                         Encode::encode(
+                           'UTF-8',
+                           $html,
+                           Encode::FB_WARN | Encode::LEAVE_SRC
+                         ),
                          '  </body>',
                          '</html>',
                        ],
@@ -2423,6 +2513,11 @@ use MIME::Base64;
 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;
@@ -2618,7 +2713,13 @@ sub _items_sections {
       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).
@@ -3043,6 +3144,10 @@ sub _items_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
@@ -3054,16 +3159,30 @@ sub _items_fee {
       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
@@ -3083,14 +3202,19 @@ sub _items_fee {
           $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?
       };
   }
@@ -3188,6 +3312,8 @@ location (whichever is defined).
 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
@@ -3222,6 +3348,8 @@ sub _items_cust_bill_pkg {
 
   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') ) {
@@ -3347,6 +3475,9 @@ sub _items_cust_bill_pkg {
         # 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
@@ -3367,8 +3498,15 @@ sub _items_cust_bill_pkg {
             || ($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)
@@ -3420,6 +3558,7 @@ sub _items_cust_bill_pkg {
             push @{ $s->{ext_description} }, @d;
           } else {
             $s = {
+              billpkgnum      => $cust_bill_pkg->billpkgnum,
               _is_setup       => 1,
               description     => $description,
               pkgpart         => $pkgpart,
@@ -3431,6 +3570,7 @@ sub _items_cust_bill_pkg {
               ext_description => \@d,
               svc_label       => ($svc_label || ''),
               locationnum     => $cust_pkg->locationnum, # sure, why not?
+              pkg_tax         => \@pkg_tax,
             };
           };
 
@@ -3463,10 +3603,15 @@ sub _items_cust_bill_pkg {
             $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
@@ -3584,6 +3729,7 @@ sub _items_cust_bill_pkg {
               push @{ $r->{ext_description} }, @d;
             } else {
               $r = {
+                billpkgnum      => $cust_bill_pkg->billpkgnum,
                 description     => $description,
                 pkgpart         => $pkgpart,
                 pkgnum          => $cust_bill_pkg->pkgnum,
@@ -3595,6 +3741,7 @@ sub _items_cust_bill_pkg {
                 ext_description => \@d,
                 svc_label       => ($svc_label || ''),
                 locationnum     => $cust_pkg->locationnum,
+                pkg_tax         => \@pkg_tax,
               };
               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
             }
@@ -3613,6 +3760,7 @@ sub _items_cust_bill_pkg {
             } elsif ( $amount ) {
               # create a new usage line
               $u = {
+                billpkgnum      => $cust_bill_pkg->billpkgnum,
                 description     => $description,
                 pkgpart         => $pkgpart,
                 pkgnum          => $cust_bill_pkg->pkgnum,
@@ -3622,6 +3770,7 @@ sub _items_cust_bill_pkg {
                 %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
           }
@@ -3755,4 +3904,68 @@ sub _items_discounts_avail {
 
 }
 
+=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;
index 6cc04b9..5412868 100644 (file)
@@ -743,6 +743,7 @@ use FS::CurrentUser;
 use FS::Record qw(qsearchs);
 use FS::queue;
 use FS::CGI qw(rooturl);
+use FS::Report::Queued::FutureAutobill;
 
 $DEBUG = 0;
 
index ebda99e..d3ee8d8 100644 (file)
@@ -5,7 +5,7 @@ use strict;
 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 );
@@ -26,7 +26,17 @@ $freeside_uid = scalar(getpwnam('freeside'));
 $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
index 0069e20..b47752d 100644 (file)
@@ -153,7 +153,7 @@ If you need to continue using the old Form 477 report, turn on the
 
   # 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', '');
   }
@@ -174,8 +174,8 @@ If you need to continue using the old Form 477 report, turn on the
     $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$//;
@@ -192,6 +192,19 @@ If you need to continue using the old Form 477 report, turn on the
       $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 {
@@ -338,7 +351,10 @@ sub upgrade {
       });
       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;
@@ -501,6 +517,23 @@ sub upgrade_data {
     #'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;
@@ -711,4 +744,3 @@ Sure.
 =cut
 
 1;
-
index a2b9774..4f6c85b 100644 (file)
@@ -2,6 +2,7 @@ package FS::access_group;
 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;
 
@@ -137,6 +138,54 @@ sub 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
@@ -148,4 +197,3 @@ L<FS::Record>, schema.html from the base documentation.
 =cut
 
 1;
-
index a9fdf5b..f23aa77 100644 (file)
@@ -12,6 +12,7 @@ use FS::Record qw( qsearch qsearchs dbh );
 use FS::agent;
 use FS::cust_main;
 use FS::sales;
+use Carp qw( croak );
 
 $DEBUG = 0;
 $me = '[FS::access_user]';
@@ -814,6 +815,103 @@ sub set_page_pref {
   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
index 026670c..552dd2a 100644 (file)
@@ -53,6 +53,10 @@ _date
 
 =back
 
+=item pid
+
+=back
+
 =head1 METHODS
 
 =over 4
@@ -84,6 +88,7 @@ sub insert_new_path {
     'path'           => $path,
     '_date'          => time,
     'render_seconds' => $render_seconds,
+    'pid'            => $$,
   } );
 
   #so we can still log pages after a transaction-aborting SQL error (and then
@@ -127,6 +132,7 @@ sub check {
     || $self->ut_text('path')
     || $self->ut_number('_date')
     || $self->ut_numbern('render_seconds')
+    || $self->ut_numbern('pid')
   ;
   return $error if $error;
 
index ba0f61d..5fd64bf 100755 (executable)
@@ -207,6 +207,27 @@ sub cidr {
   $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 
@@ -416,4 +437,3 @@ now because that's the smallest block that makes any sense at all.
 =cut
 
 1;
-
index e70b971..8aff96a 100644 (file)
@@ -294,7 +294,15 @@ sub payment_gateway {
     }
   }
 
-  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 : '',
index f2263c5..ce7fe8b 100644 (file)
@@ -60,6 +60,8 @@ sub dbi_import {
   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,
@@ -88,6 +90,7 @@ sub dbi_import {
       $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 {
@@ -97,6 +100,7 @@ sub dbi_import {
       $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} };
@@ -110,7 +114,7 @@ sub dbi_import {
   $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;
@@ -149,19 +153,6 @@ sub dbi_import {
 
       $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;
@@ -195,7 +186,7 @@ sub get_queries {
   $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},
   );
 
index cac30c4..b00ea13 100644 (file)
@@ -37,42 +37,40 @@ use Time::Local;
     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
 
   ],
 
index 65aed76..abc7d5b 100644 (file)
@@ -26,8 +26,10 @@ use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker );
       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
index fa047f5..81dfdbc 100644 (file)
@@ -199,8 +199,6 @@ sub insert {
 
   }
 
-  $error ||= $self->insert_password_history;
-
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -302,6 +300,15 @@ sub insert {
     }
   }
 
+  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;
 
   '';
@@ -811,7 +818,7 @@ sub authenticate_password {
 
     $hash eq $check_hash;
 
-  } else { 
+  } else {
 
     return 0 if $self->_password eq '';
 
index bd1b8bb..7158cb2 100644 (file)
@@ -41,6 +41,7 @@ use FS::cust_bill_void;
 use FS::reason;
 use FS::reason_type;
 use FS::L10N;
+use FS::Misc::Savepoint;
 
 $DEBUG = 0;
 $me = '[FS::cust_bill]';
@@ -148,15 +149,6 @@ Invoices are normally created by calling the bill method of a customer object
 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 {
@@ -530,7 +522,13 @@ Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
 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
@@ -977,6 +975,9 @@ sub apply_payments_and_credits {
   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 }
@@ -1065,6 +1066,7 @@ sub apply_payments_and_credits {
 
     my $error = $app->insert(%options);
     if ( $error ) {
+      savepoint_rollback_and_release( $savepoint_label );
       $dbh->rollback if $oldAutoCommit;
       return "Error inserting ". $app->table. " record: $error";
     }
@@ -1072,6 +1074,7 @@ sub apply_payments_and_credits {
 
   }
 
+  savepoint_release( $savepoint_label );
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   ''; #no error
 
@@ -1408,6 +1411,11 @@ See L</print_csv> for a description of the output format.
 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";
@@ -1484,6 +1492,11 @@ in the ICS format.
 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;
 
@@ -2719,7 +2732,7 @@ sub _items_svc_phone_sections {
 
 }
 
-=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),
@@ -2768,7 +2781,7 @@ sub _items_usage_class_summary {
   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.
@@ -2902,7 +2915,7 @@ sub _items_previous {
 
 }
 
-=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
@@ -2952,7 +2965,7 @@ sub __items_previous_map_invoice {
   }
 }
 
-=sub _items_credits()
+=item _items_credits()
 
   Return array of hashrefs containing credits to be shown as line-items
   when rendering this bill.
@@ -3091,7 +3104,7 @@ sub _items_credits {
   @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
@@ -3107,7 +3120,7 @@ sub _items_credits_total {
 
 
 
-=sub _items_credits_postbill()
+=item _items_credits_postbill()
 
   Returns an array of hashrefs for credits where
   - Credit issued after this invoice
@@ -3149,7 +3162,7 @@ sub _items_credits_postbill {
   }} @cust_credit_bill;
 }
 
-=sub _items_payments_postbill()
+=item _items_payments_postbill()
 
   Returns an array of hashrefs for payments where
   - Payment occured after this invoice
@@ -3185,7 +3198,7 @@ sub _items_payments_postbill {
   }} @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.
@@ -3246,7 +3259,6 @@ sub _items_payments {
 
   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
 
@@ -3270,7 +3282,7 @@ print "\nCASE 3\n";
     } 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;
 
@@ -3304,7 +3316,7 @@ print "\nCASE 2\n";
   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
@@ -3361,7 +3373,7 @@ sub __items_payments_make_hashref {
   return @return;
 }
 
-=sub _items_total()
+=item _items_total()
 
   Generate the line-items to be shown on the bill in the "Totals" section
 
index 77dce24..1262c38 100644 (file)
@@ -365,8 +365,10 @@ sub void {
     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
@@ -374,17 +376,13 @@ sub void {
     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;
@@ -406,6 +404,40 @@ sub void {
 
 }
 
+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.
@@ -716,6 +748,7 @@ Returns the customer (L<FS::cust_main> object) for this line item.
 =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
@@ -1815,6 +1848,92 @@ sub upgrade_tax_location {
   '';
 }
 
+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.
@@ -1873,4 +1992,3 @@ from the base documentation.
 =cut
 
 1;
-
index 50f69c9..43b2950 100644 (file)
@@ -119,15 +119,6 @@ sub table { 'cust_bill_void'; }
 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
 
@@ -375,4 +366,3 @@ L<FS::Record>, schema.html from the base documentation.
 =cut
 
 1;
-
index 094c4fa..2884f12 100644 (file)
@@ -315,11 +315,16 @@ sub join_sql {
 
   "
        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 )
index 7c9868d..2e8fe81 100644 (file)
@@ -79,6 +79,7 @@ use FS::sales;
 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
@@ -2212,11 +2213,15 @@ sub cancel_pkgs {
   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');
 
@@ -2233,7 +2238,8 @@ sub cancel_pkgs {
       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 );
       }
 
@@ -2253,11 +2259,13 @@ sub cancel_pkgs {
                              '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,
@@ -2271,17 +2279,22 @@ sub cancel_pkgs {
   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) {
@@ -2297,8 +2310,11 @@ sub cancel_pkgs {
     @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 ) {
@@ -2319,10 +2335,12 @@ sub cancel_pkgs {
     }
     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;
     }
   }
 
@@ -3922,6 +3940,27 @@ sub name {
   $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'
@@ -5393,6 +5432,16 @@ sub process_bill_and_collect {
   $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
index 08b10c1..1be7d39 100644 (file)
@@ -1,6 +1,7 @@
 package FS::cust_main::Billing;
 
 use strict;
+use feature 'state';
 use vars qw( $conf $DEBUG $me );
 use Carp;
 use Data::Dumper;
@@ -25,6 +26,7 @@ use FS::pkg_category;
 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
@@ -170,11 +172,8 @@ sub bill_and_collect {
 
   # 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')
@@ -1052,6 +1051,9 @@ sub _make_lines {
         }
     }
 
+    $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') ) {
@@ -1752,7 +1754,10 @@ sub collect {
   $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 ),
@@ -1960,9 +1965,13 @@ sub do_cust_event {
   }
 
   $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 ) {
 
@@ -2287,16 +2296,21 @@ sub apply_payments_and_credits {
   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
 
index 38d100e..70dc288 100644 (file)
@@ -55,7 +55,8 @@ sub batch_card {
     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,
@@ -114,7 +115,7 @@ sub batch_card {
   } );
 
   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{$_});
   }
@@ -138,11 +139,16 @@ sub batch_card {
     '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)
index f16752b..714a2e6 100644 (file)
@@ -16,6 +16,7 @@ use FS::cust_bill_pay;
 use FS::cust_refund;
 use FS::banned_pay;
 use FS::payment_gateway;
+use FS::Misc::Savepoint;
 
 $realtime_bop_decline_quiet = 0;
 
@@ -27,6 +28,7 @@ $me = '[FS::cust_main::Billing_Realtime]';
 
 our $BOP_TESTING = 0;
 our $BOP_TESTING_SUCCESS = 1;
+our $BOP_TESTING_TIMESTAMP = '';
 
 install_callback FS::UID sub { 
   $conf = new FS::Conf;
@@ -405,7 +407,7 @@ sub realtime_bop {
 
   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;
 
@@ -421,6 +423,8 @@ sub realtime_bop {
     $options{amount} = $amount;
   }
 
+  return '' unless $options{amount} > 0;
+
   # set fields from passed cust_payby
   _bop_cust_payby_options(\%options);
 
@@ -454,16 +458,24 @@ sub realtime_bop {
     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;
@@ -672,7 +684,7 @@ sub realtime_bop {
   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},
@@ -747,7 +759,7 @@ sub realtime_bop {
     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;
@@ -936,7 +948,7 @@ sub _realtime_bop_result {
        '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,
@@ -957,12 +969,16 @@ sub _realtime_bop_result {
     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'} ?
@@ -971,7 +987,8 @@ sub _realtime_bop_result {
       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".
@@ -986,9 +1003,10 @@ sub _realtime_bop_result {
     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;
@@ -998,7 +1016,8 @@ sub _realtime_bop_result {
        $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";
@@ -1020,8 +1039,8 @@ sub _realtime_bop_result {
     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";
@@ -1029,7 +1048,7 @@ sub _realtime_bop_result {
       return $e;
 
     } else {
-
+      savepoint_release( $savepoint_label );
       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
       if ( $options{'apply'} ) {
@@ -1068,9 +1087,11 @@ sub _realtime_bop_result {
          }
 
          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,
                                });
@@ -1193,6 +1214,7 @@ sub _realtime_bop_result {
               "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)";
     }
 
@@ -1520,7 +1542,7 @@ sub realtime_refund_bop {
  
     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 );
 
index 815304b..bfaf621 100644 (file)
@@ -96,8 +96,11 @@ sub smart_search {
 
     #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'",
@@ -106,15 +109,14 @@ sub smart_search {
                           " 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\%'",
@@ -132,8 +134,12 @@ sub smart_search {
   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'",
@@ -141,7 +147,6 @@ sub smart_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
@@ -206,6 +211,7 @@ sub smart_search {
       # 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, },
@@ -226,9 +232,9 @@ sub smart_search {
     #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)),
@@ -236,7 +242,7 @@ sub smart_search {
           " LOWER(company) = ". dbh->quote(lc($company)),
           $agentnums_sql,
         ),
-      } ),
+    } );
 
     #contacts?
     # probably not necessary for the "something a browser remembered" case
@@ -282,11 +288,12 @@ sub smart_search {
 
       #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)
@@ -314,11 +321,12 @@ sub smart_search {
       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
@@ -375,6 +383,7 @@ sub smart_search {
       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 '.
@@ -456,6 +465,7 @@ sub smart_search {
     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'   => {},
index 8b5e06d..4c82d10 100644 (file)
@@ -662,11 +662,75 @@ sub send_receipt {
        || ! $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) {
@@ -684,7 +748,7 @@ sub send_receipt {
       $error = $cust_msg ? $cust_msg->insert : 'error preparing msg_template';
       if ($error) {
         warn "send_receipt: $error";
-        return;
+        return $error;
       }
 
       my $queue = new FS::queue {
@@ -695,39 +759,11 @@ sub send_receipt {
       $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
index d29c6d0..614c117 100644 (file)
@@ -302,6 +302,7 @@ sub approve {
       '_date'     => $new->_date,
       'usernum'   => $new->usernum,
       'batchnum'  => $new->batchnum,
+      'invnum'    => $old->invnum,
       'gatewaynum'    => $opt{'gatewaynum'},
       'processor'     => $opt{'processor'},
       'auth'          => $opt{'auth'},
index 704741f..4e9f04f 100644 (file)
@@ -1,5 +1,6 @@
 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 );
@@ -315,7 +316,6 @@ sub check {
     #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')
@@ -546,6 +546,9 @@ sub check {
     return $error if $error;
   }
 
+  $error = $self->ut_daten('paydate');
+  return $error if $error;
+
   $self->SUPER::check;
 }
 
@@ -912,8 +915,81 @@ sub search_sql {
 
 =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;
@@ -921,7 +997,87 @@ sub _upgrade_data {
   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
index b24b3ab..f29ab9f 100644 (file)
@@ -2487,6 +2487,12 @@ sub change {
     $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 ) {
@@ -3329,11 +3335,10 @@ sub process_bulk_cust_pkg {
   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
 
@@ -3341,7 +3346,14 @@ sub process_bulk_cust_pkg {
   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 ) {
@@ -5451,6 +5463,24 @@ sub fcc_477_count {
 
 }
 
+=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
index 93bd88d..2b18323 100644 (file)
@@ -102,6 +102,7 @@ my %formatfields = (
   '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 )],
index efa3661..306b4fb 100644 (file)
@@ -418,6 +418,7 @@ sub process_block_lookup {
     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
index 387883b..74038fc 100644 (file)
@@ -26,6 +26,7 @@ my @contexts = ( qw(
   queue
   upgrade
   upgrade_taxable_billpkgnum
+  freeside-ipifony-download
   freeside-paymentech-upload
   freeside-paymentech-download
   test
index 37c1fab..c2c3707 100644 (file)
@@ -529,6 +529,11 @@ sub send_prepared {
   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;
index d1d5196..9900aca 100644 (file)
@@ -533,6 +533,22 @@ sub condition_sql_option_integer {
        " 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
index bdd4e12..917cf46 100644 (file)
@@ -13,7 +13,7 @@ sub description {
 
 sub option_fields {
   (
-    'agentnum'   => { label=>'Agent', type=>'select-agent', },
+    'agentnum'   => { label=>'Agent', type=>'select-agent', multiple => '1' },
   );
 }
 
@@ -22,16 +22,15 @@ sub condition {
 
   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;
diff --git a/FS/FS/part_event/Condition/cust_birthdate.pm b/FS/FS/part_event/Condition/cust_birthdate.pm
new file mode 100644 (file)
index 0000000..874e3ac
--- /dev/null
@@ -0,0 +1,64 @@
+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;
index b3a8d70..8efb278 100644 (file)
@@ -16,36 +16,16 @@ sub eventtable_hashref {
     };
 }
 
-#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;
index 007ce45..a56e3fa 100644 (file)
@@ -60,19 +60,20 @@ sub condition {
 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;
diff --git a/FS/FS/part_event/Condition/invoice_has_not_been_sent.pm b/FS/FS/part_event/Condition/invoice_has_not_been_sent.pm
new file mode 100644 (file)
index 0000000..882762d
--- /dev/null
@@ -0,0 +1,41 @@
+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
index 3256dc0..f1d1b6a 100644 (file)
@@ -138,6 +138,39 @@ sub optionvalue {
   }
 }
 
+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
index 572a1b6..1a8f43d 100644 (file)
@@ -554,15 +554,19 @@ sub default_export_machine {
   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(@_);
 }
 
@@ -579,9 +583,15 @@ sub 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(@_);
 }
 
@@ -589,9 +599,15 @@ sub 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(@_);
 }
 
@@ -599,9 +615,15 @@ sub 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(@_);
 }
 
@@ -609,9 +631,15 @@ sub 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(@_);
 }
 
index 15410ae..dbbd1be 100644 (file)
@@ -105,7 +105,7 @@ sub replace {
   '';
 }
 
-sub export_insert {
+sub _export_insert {
   my $self = shift;
   my $svc = shift;
   my $cust_pkg = $svc->cust_svc->cust_pkg;
@@ -290,7 +290,7 @@ sub export_insert {
   '';
 }
 
-sub export_delete {
+sub _export_delete {
   my $self = shift;
   my $svc = shift;
 
@@ -376,7 +376,7 @@ sub export_delete {
   '';
 }
 
-sub export_replace {
+sub _export_replace {
   my $self = shift;
   my $new = shift;
   my $old = shift || $self->replace_old;
@@ -421,7 +421,7 @@ sub export_replace {
   '';
 }
 
-sub export_suspend {
+sub _export_suspend {
   my $self = shift;
   my $svc = shift;
 
@@ -446,7 +446,7 @@ sub export_suspend {
   $error || '';
 }
 
-sub export_unsuspend {
+sub _export_unsuspend {
   my $self = shift;
   my $svc = shift;
 
index 51cee97..c131900 100644 (file)
@@ -87,7 +87,7 @@ sub app {
   return;
 }
 
-sub export_insert {
+sub _export_insert {
   my $self = shift;
   my $new = shift;
   my $app = $self->app;
@@ -134,7 +134,7 @@ sub export_insert {
   }
 }
 
-sub export_delete {
+sub _export_delete {
   my $self = shift;
   my $old = shift;
   my $app = $self->app;
@@ -160,7 +160,7 @@ sub export_delete {
   }
 }
 
-sub export_replace {
+sub _export_replace {
   my $self = shift;
   my ($new, $old) = @_;
   my $app = $self->app;
@@ -222,7 +222,7 @@ sub export_replace {
   }
 }
 
-sub export_suspend {
+sub _export_suspend {
   my $self = shift;
   my $svc = shift;
   my $unsuspend = shift || 0;
@@ -243,7 +243,7 @@ sub export_suspend {
   return;
 }
 
-sub export_unsuspend {
+sub _export_unsuspend {
   my ($self, $svc) = @_;
   $self->export_suspend($svc, 1);
 }
index c7356bf..c5c5545 100644 (file)
@@ -41,7 +41,7 @@ service types, create another export instance.</p>
 '
 );
 
-sub export_insert {
+sub _export_insert {
   my ($self, $svc) = @_;
   my $result = $self->request_user_edit(
     'Add'   => 1,
@@ -54,7 +54,7 @@ sub export_insert {
   $result;
 }
 
-sub export_replace {
+sub _export_replace {
   my ($self, $new, $old) = @_;
   if ($new->email ne $old->email) {
     return $old->export_delete || $new->export_insert;
@@ -70,7 +70,7 @@ sub export_replace {
   );
 }
 
-sub export_suspend {
+sub _export_suspend {
   my ($self, $svc) = @_;
   $self->request_user_edit(
     'Modify'  => 1,
@@ -79,7 +79,7 @@ sub export_suspend {
   );
 }
 
-sub export_unsuspend {
+sub _export_unsuspend {
   my ($self, $svc) = @_;
   $self->request_user_edit(
     'Modify'  => 1,
@@ -88,7 +88,7 @@ sub export_unsuspend {
   );
 }
 
-sub export_delete {
+sub _export_delete {
   my ($self, $svc) = @_;
   $self->request_user_edit(
     'ConfirmDelete' => 1,
index 6d868e6..b39bffb 100644 (file)
@@ -69,7 +69,7 @@ value, or a list of fixed values, for the sip_server field.</P>
 END
 );
 
-sub export_insert {
+sub _export_insert {
   my($self, $svc_phone) = (shift, shift);
   local $SIG{__DIE__};
   try {
@@ -100,7 +100,7 @@ sub export_insert {
   };
 }
 
-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.
@@ -111,7 +111,7 @@ sub export_replace {
   '';
 }
 
-sub export_delete {
+sub _export_delete {
   my ($self, $svc_phone) = (shift, shift);
   local $SIG{__DIE__};
   try {
index 8c152be..d52ccae 100644 (file)
@@ -69,7 +69,7 @@ will be applied to the attached NAS record.
 
 =cut
 
-sub export_insert {
+sub _export_insert {
   my $self = shift;
   my $svc_broadband = shift;
   my %hash = (
@@ -103,7 +103,7 @@ sub export_insert {
   return;
 }
 
-sub export_delete {
+sub _export_delete {
   my $self = shift;
   my $svc_broadband = shift;
   my $svcnum = $svc_broadband->svcnum;
@@ -118,7 +118,7 @@ sub export_delete {
   return;
 }
 
-sub export_replace {
+sub _export_replace {
   my $self = shift;
   my ($new_svc, $old_svc) = (shift, shift);
 
index 56d7816..8ebc716 100644 (file)
@@ -62,27 +62,27 @@ svc_broadband fields may be prefixed with <b>$new_</b> and <b>$old_</b>
 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', @_);
 }
index 1a86612..35dcd31 100644 (file)
@@ -21,6 +21,7 @@ tie my %options, 'Tie::IxHash',
   '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 = (
@@ -80,6 +81,7 @@ sub snmp_results {
   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,
@@ -90,7 +92,9 @@ sub snmp_results {
   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') {
@@ -115,6 +119,7 @@ sub snmp_results {
       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) {
index a04a70e..611bd00 100644 (file)
@@ -6,6 +6,7 @@ use strict;
 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
@@ -46,7 +47,7 @@ Until then, authentication will be denied.</P>
 END
 );
 
-sub export_insert {
+sub _export_insert {
   my($self, $svc_x) = (shift, shift);
 
   my $cust_main = $svc_x->cust_main;
@@ -68,7 +69,7 @@ sub export_insert {
   '';
 }
 
-sub export_replace {
+sub _export_replace {
   my($self, $svc_new, $svc_old) = @_;
 
   my $cust_main = $svc_new->cust_main;
@@ -121,7 +122,7 @@ sub export_replace {
   '';
 }
 
-sub export_delete {
+sub _export_delete {
   my ($self, $svc_x) = @_;
 
   my $cust_main = $svc_x->cust_main;
@@ -162,6 +163,12 @@ sub export_delete {
 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.";
   }
@@ -181,6 +188,13 @@ sub export_device_insert {
 
 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);
 
@@ -205,6 +219,12 @@ sub export_device_replace {
 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;
index 5c6f1ed..981eb19 100644 (file)
@@ -7,6 +7,7 @@ use MIME::Base64;
 use Tie::IxHash;
 use IPC::Run qw(run);
 use FS::CGI qw(rooturl);
+use Carp qw(carp);
 
 $DEBUG = 0;
 
@@ -50,6 +51,12 @@ sub rebless { shift; }
 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 $@;
 
@@ -131,6 +138,12 @@ sub gs_create {
 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'),
index 5c4a8d0..3e182d3 100644 (file)
@@ -8,6 +8,7 @@ use URI::Escape;
 use LWP::UserAgent;
 use HTTP::Request::Common;
 use Email::Valid;
+use Carp qw(carp);
 
 tie my %options, 'Tie::IxHash',
   'url' => { label => 'URL', },
@@ -53,6 +54,12 @@ sub _export_delete  { '' };
 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';
@@ -131,6 +138,12 @@ sub export_setstatus_listdel {
 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_';
@@ -182,6 +195,12 @@ sub export_setstatus_vacationdel {
 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});
@@ -216,5 +235,3 @@ sub export_setstatus_vacationX {
 }
 
 1;
-
-1;
index 23917bf..68b1a9f 100644 (file)
@@ -10,6 +10,7 @@ use FS::Record qw(qsearch qsearchs dbh);
 use FS::part_export;
 use FS::svc_dsl;
 use Data::Dumper;
+use Carp qw(carp);
 
 @ISA = qw(FS::part_export);
 $me= '[' .  __PACKAGE__ . ']';
@@ -678,7 +679,13 @@ sub _export_delete {
 
 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');
index f6a730e..cc4069c 100644 (file)
@@ -10,6 +10,7 @@ use Date::Format qw(time2str);
 use Parse::FixedLength;
 use File::Temp qw(tempfile);
 use vars qw(%info %options $initial_load_hack $DEBUG);
+use Carp qw( carp );
 
 my %upload_targets;
 
@@ -396,6 +397,13 @@ sub process {
   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;
 
index ac78dbc..c6110f5 100644 (file)
@@ -7,6 +7,7 @@ use Tie::IxHash;
 use Date::Format qw( time2str );
 use Regexp::Common qw( URI );
 use REST::Client;
+use Carp qw(carp);
 
 $me = '[FS::part_export::netsapiens]';
 
@@ -392,6 +393,12 @@ sub _export_unsuspend {
 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;
@@ -426,6 +433,12 @@ sub export_device_insert {
 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),
   );
index 027a52d..679f5da 100644 (file)
@@ -47,7 +47,7 @@ sub client {
   return $self->get('client');
 }
 
-sub export_insert {
+sub _export_insert {
   my( $self, $svc_phone ) = (shift, shift);
 
   my %location_hash = $svc_phone->location_hash;
@@ -98,7 +98,7 @@ sub export_insert {
   '';
 }
 
-sub export_replace {
+sub _export_replace {
   my( $self, $new, $old ) = (shift, shift, shift);
 
   # except when changing the phone number, exactly like export_insert;
@@ -109,7 +109,7 @@ sub export_replace {
   $self->export_insert($new);
 }
 
-sub export_delete {
+sub _export_delete {
   my ($self, $svc_phone) = (shift, shift);
 
   if ($self->option('debug')) {
index 71445bf..3f01de3 100644 (file)
@@ -5,6 +5,7 @@ use vars qw(@ISA %info);
 use Tie::IxHash;
 use String::ShellQuote;
 use FS::part_export;
+use Carp qw(carp);
 
 @ISA = qw(FS::part_export);
 
@@ -103,6 +104,12 @@ sub _export_command {
   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';
   {
index fc0dee5..6db43c1 100644 (file)
@@ -9,6 +9,7 @@ use MIME::Base64;
 use REST::Client;
 use Data::Dumper;
 use FS::Conf;
+use Carp qw(carp);
 
 =pod
 
@@ -24,29 +25,52 @@ Saisei integration for Freeside
 
 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' },
@@ -56,37 +80,56 @@ tie my %options, 'Tie::IxHash',
   '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;
@@ -94,24 +137,18 @@ sub _export_insert {
 
   # 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.
@@ -120,59 +157,223 @@ sub _export_insert {
  
     # 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
@@ -191,6 +392,7 @@ Returns empty on failure;  retrieve error messages using L</api_error>.
 
 sub api_call {
   my ($self,$method,$path,$params) = @_;
+
   $self->{'__saisei_error'} = '';
   my $auth_info = $self->option('username') . ':' . $self->option('password');
   $params ||= {};
@@ -218,7 +420,8 @@ sub api_call {
     }
   }
   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; 
   }
@@ -229,7 +432,7 @@ sub api_call {
 
 =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
@@ -253,7 +456,7 @@ sub api_get_policies {
   $self->{'__saisei_error'} = "Did not receive any global policies"
     unless $get_policies;
 
-  return $get_policies;
+  return $get_policies->{collection};
 }
 
 =head2 api_get_rateplan
@@ -268,8 +471,6 @@ sub 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;
 }
@@ -286,8 +487,6 @@ sub api_get_user {
 
   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;
 }
@@ -300,14 +499,29 @@ Gets user info for specific access point.
 
 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
@@ -319,6 +533,9 @@ Creates a 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",
@@ -326,22 +543,26 @@ sub api_create_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};
@@ -357,8 +578,8 @@ sub api_modify_rateplan {
       },
     );
 
-    $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
     
   }
 
@@ -366,6 +587,31 @@ sub api_modify_rateplan {
  
 }
 
+=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.
@@ -384,7 +630,7 @@ sub api_create_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;
 
@@ -397,19 +643,70 @@ Creates a access point.
 =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;
 
 }
@@ -421,7 +718,7 @@ ties host to user, rateplan and default access point.
 =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", 
@@ -429,11 +726,12 @@ sub api_add_host_to_user {
       {
         '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;
 
@@ -441,7 +739,8 @@ sub api_add_host_to_user {
 
 =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
 
@@ -466,12 +765,152 @@ sub api_delete_host_to_user {
   );
 
   $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>
index 09fa71b..7099ca8 100644 (file)
@@ -7,6 +7,7 @@ use String::ShellQuote;
 use Net::OpenSSH;
 use FS::part_export;
 use FS::Record qw( qsearch qsearchs );
+use Carp qw(carp);
 
 @ISA = qw(FS::part_export);
 
@@ -267,6 +268,12 @@ sub _export_unsuspend {
 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 );
@@ -291,6 +298,13 @@ sub export_pkg_change {
 
 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(@_);
@@ -303,6 +317,12 @@ sub _export_command {
   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");
 
index 9d4e336..287e604 100644 (file)
@@ -14,6 +14,7 @@ use FS::Misc::DateTime qw(parse_datetime);
 use DateTime;
 use Number::Phone;
 use Try::Tiny;
+use Carp qw(carp);
 
 our $me = '[sipwise]';
 our $DEBUG = 0;
@@ -67,7 +68,7 @@ our %info = (
 END
 );
 
-sub export_insert {
+sub _export_insert {
   my($self, $svc_x) = (shift, shift);
 
   local $SIG{__DIE__};
@@ -88,7 +89,7 @@ sub export_insert {
   '';
 }
 
-sub export_replace {
+sub _export_replace {
   my ($self, $svc_new, $svc_old) = @_;
   local $SIG{__DIE__};
 
@@ -110,7 +111,7 @@ sub export_replace {
   '';
 }
 
-sub export_delete {
+sub _export_delete {
   my ($self, $svc_x) = (shift, shift);
   local $SIG{__DIE__};
 
@@ -135,7 +136,7 @@ sub export_delete {
 
 # 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);
@@ -148,7 +149,7 @@ sub export_suspend {
   '';
 }
 
-sub export_unsuspend {
+sub _export_unsuspend {
   my $self = shift;
   my $svc_x = shift;
   my $role = $self->svc_role($svc_x);
@@ -295,6 +296,13 @@ previously, and the one it's linked to now.
 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;
index 9e65e51..926e36f 100644 (file)
@@ -8,7 +8,7 @@ use FS::Record qw( dbh qsearch qsearchs str2time_sql str2time_sql_closing );
 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;
 
@@ -489,6 +489,12 @@ sub suspended_usergroups {
 }
 
 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 ) = @_;
 
@@ -527,6 +533,12 @@ sub sqlradius_insert { #subroutine, not method
 }
 
 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';
@@ -565,6 +577,12 @@ sub sqlradius_usergroup_insert { #subroutine, not method
 }
 
 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';
@@ -582,6 +600,12 @@ sub sqlradius_usergroup_delete { #subroutine, not method
 }
 
 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';
@@ -595,6 +619,12 @@ sub sqlradius_rename { #subroutine, not method
 }
 
 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 ) = @_;
 
@@ -609,6 +639,12 @@ sub sqlradius_attrib_delete { #subroutine, not method
 }
 
 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';
@@ -883,6 +919,12 @@ sub usage_sessions {
 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;
@@ -1048,6 +1090,13 @@ sub export_nas_replace { shift->export_nas_action('replace', @_); }
 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;
@@ -1061,6 +1110,12 @@ sub export_nas_action {
 }
 
 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'} })
@@ -1075,6 +1130,12 @@ VALUES (?, ?, ?, ?, ?, ?, ?)');
 }
 
 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 = ?');
@@ -1082,6 +1143,12 @@ sub sqlradius_nas_delete {
 }
 
 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'} })
@@ -1157,6 +1224,12 @@ sub export_attr_action {
 }
 
 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 = @_;
 
@@ -1180,6 +1253,12 @@ sub sqlradius_attr_insert {
 }
 
 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 = @_;
 
@@ -1231,6 +1310,12 @@ sub export_group_replace {
 }
 
 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$/
@@ -1271,6 +1356,12 @@ Note this is NOT the opposite of sqlradius_connect.
 =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
index 67cf2b0..9ab6455 100644 (file)
@@ -131,7 +131,7 @@ sub check_svc { # check the service for validity
   '';
 }
 
-sub export_insert {
+sub _export_insert {
   my($self, $svc_x) = (shift, shift);
 
   my $error = $self->check_svc($svc_x);
@@ -294,7 +294,7 @@ sub insert_trunk {
   }
 }
 
-sub export_replace {
+sub _export_replace {
   my ($self, $svc_new, $svc_old) = @_;
 
   my $error = $self->check_svc($svc_new);
@@ -412,7 +412,7 @@ sub replace_gateway {
   }
 }
 
-sub export_delete {
+sub _export_delete {
   my ($self, $svc_x) = (shift, shift);
 
   my $role = $self->svc_role($svc_x)
index 5d3f835..ef8b266 100644 (file)
@@ -102,7 +102,7 @@ sub insert {
   '';
 }
 
-sub export_insert {
+sub _export_insert {
   my ($self, $sector) = @_;
 
   return unless $self->option('use_coverage');
@@ -175,7 +175,7 @@ sub export_insert {
 
 }
 
-sub export_replace { # do the same thing as insert
+sub _export_replace { # do the same thing as insert
   my $self = shift;
   $self->export_insert(@_);
 }
index 2519884..1eedd66 100644 (file)
@@ -133,7 +133,7 @@ our %info = (
 END
 );
 
-sub export_insert {
+sub _export_insert {
   my($self, $svc_x) = (shift, shift);
 
   my $role = $self->svc_role($svc_x);
@@ -162,7 +162,7 @@ sub export_insert {
   '';
 }
 
-sub export_replace {
+sub _export_replace {
   my ($self, $svc_new, $svc_old) = @_;
   my $role = $self->svc_role($svc_new);
   my $error;
@@ -175,7 +175,7 @@ sub export_replace {
   '';
 }
 
-sub export_delete {
+sub _export_delete {
   my ($self, $svc_x) = (shift, shift);
   my $role = $self->svc_role($svc_x);
   if ( $role eq 'subacct' ) {
@@ -204,7 +204,7 @@ sub export_delete {
   '';
 }
 
-sub export_suspend {
+sub _export_suspend {
   my $self = shift;
   my $svc_x = shift;
   my $role = $self->svc_role($svc_x);
@@ -215,7 +215,7 @@ sub export_suspend {
   '';
 }
 
-sub export_unsuspend {
+sub _export_unsuspend {
   my $self = shift;
   my $svc_x = shift;
   my $role = $self->svc_role($svc_x);
index 1e46536..ec37624 100644 (file)
@@ -102,7 +102,10 @@ sub calc_discount {
     # $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'};
     }
index 6fd9c7d..c06328b 100644 (file)
@@ -57,6 +57,12 @@ tie my %contract_years, 'Tie::IxHash', (
                                     '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',
@@ -87,7 +93,8 @@ tie my %contract_years, 'Tie::IxHash', (
   },
   '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
index e43a525..f12b1ac 100644 (file)
@@ -94,7 +94,7 @@ sub base_recur {
 
 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')
index 9e97cc5..9252143 100644 (file)
@@ -30,7 +30,7 @@ tie our %prorate_round_day_opts, 'Tie::IxHash',
     },
     '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' => {
index 9bf107b..a3866f3 100644 (file)
@@ -19,6 +19,10 @@ our @detail_cols = ( qw(amount format duration phonenum accountcode
   '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',
@@ -50,7 +54,7 @@ our @detail_cols = ( qw(amount format duration phonenum accountcode
     },
 
   },
-  '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
                   )],
@@ -140,6 +144,12 @@ sub calc_recur {
   ($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; }
index dcc7843..b82996e 100644 (file)
@@ -519,6 +519,18 @@ sub part_export_dsl_pull {
     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).
@@ -861,10 +873,10 @@ sub process {
               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 } });
                    }
@@ -909,6 +921,11 @@ sub process {
   );
 
   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
index 75a2dfb..e055af3 100644 (file)
@@ -97,7 +97,7 @@ sub check {
   ;
   return $error if $error;
 
-  $self->columnflag =~ /^([DFSMAHX]?)$/
+  $self->columnflag =~ /^([DFSMAHXP]?)$/
     or return "illegal columnflag ". $self->columnflag;
   $self->columnflag(uc($1));
 
index 21dae42..3d1d98b 100644 (file)
@@ -174,6 +174,9 @@ $name = 'RBC';
       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.
@@ -194,8 +197,7 @@ $name = 'RBC';
     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).
@@ -226,5 +228,10 @@ $name = 'RBC';
   },
 );
 
+## this format can handle credit transactions
+sub can_handle_credits {
+  1;
+}
+
 1;
 
index 3cf3134..094d501 100644 (file)
@@ -8,7 +8,7 @@ use Date::Format 'time2str';
 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);
@@ -67,12 +67,8 @@ my $gateway;
         $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 = (
@@ -131,12 +127,14 @@ my %paymentech_countries = map { $_ => 1 } qw( US CA GB UK );
           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 )
           : ()
index c89245f..4c2180e 100644 (file)
@@ -3,7 +3,8 @@ use base 'FS::IP_Mixin';
 
 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;
@@ -80,7 +81,7 @@ sub svc_ip_check {
   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';
@@ -90,21 +91,71 @@ sub svc_ip_check {
 }
 
 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 {
index de9199d..1118c0d 100644 (file)
@@ -329,6 +329,15 @@ sub table_info {
                          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',
@@ -341,7 +350,7 @@ sub table_info {
                          type  => 'text',
                          disable_inventory => 1,
                          disable_select => 1,
-                         disable_part_svc_column => 1,
+                         #disable_part_svc_column => 1,
                        },
         'upbytes'   => { label => 'Upload',
                          type  => 'text',
@@ -1971,6 +1980,9 @@ sub _op_usage {
 
   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';
index 38594f0..02136c5 100755 (executable)
@@ -107,15 +107,23 @@ sub table_info {
     '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',
@@ -134,6 +142,7 @@ sub table_info {
                          #select_table => 'radius_group',
                          #select_key   => 'groupnum',
                          #select_label => 'groupname',
+                         disable_select => 1,
                          disable_inventory => 1,
                          multiple => 1,
                        },
@@ -147,6 +156,9 @@ sub table_info {
                              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)',
     },
   };
 }
@@ -352,6 +364,8 @@ sub check {
     || $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')
@@ -501,6 +515,11 @@ sub _upgrade_data {
     #next SVC;
   }
 
+  require FS::Misc::FixIPFormat;
+  FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+      'svc_broadband', 'svcnum', 'ip_addr',
+  );
+
   '';
 }
 
@@ -523,4 +542,3 @@ FS::part_svc, schema.html from the base documentation.
 =cut
 
 1;
-
index 9e818e1..5e3f180 100644 (file)
@@ -36,6 +36,26 @@ from FS::Record.  The following fields are currently supported:
 
 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
index 7f49715..7f2ef80 100644 (file)
@@ -236,6 +236,17 @@ sub search_sql_addl_from {
   '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
@@ -245,4 +256,3 @@ L<FS::Record>
 =cut
 
 1;
-
index dcd6d1d..c07f186 100644 (file)
@@ -50,15 +50,25 @@ FS::svc_Common.  The following fields are currently supported:
 
 =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
 
@@ -69,27 +79,45 @@ Vendor/telco DSL order type (e.g. (M)ove, (A)dd, (C)hange, (D)elete, or similar)
 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
 
@@ -101,13 +129,21 @@ Vendor/telco DSL order status (e.g. (N)ew, (A)ssigned, (R)ejected, (M)revised,
 
 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
index 4bff483..019a564 100644 (file)
@@ -245,6 +245,17 @@ sub display_hw_addr {
     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
@@ -254,4 +265,3 @@ L<FS::Record>, L<FS::svc_Common>, schema.html from the base documentation.
 =cut
 
 1;
-
index a5e181d..b0f6e8d 100644 (file)
@@ -387,6 +387,17 @@ sub sum_cdrs {
   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
@@ -399,4 +410,3 @@ L<FS::cust_pkg>, schema.html from the base documentation.
 =cut
 
 1;
-
index 18b43fe..8a93d8f 100644 (file)
@@ -44,6 +44,14 @@ Tower name
 
 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
@@ -118,6 +126,8 @@ sub check {
     || $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;
 
index 2e92323..eb00d33 100644 (file)
@@ -95,6 +95,18 @@ The coverage map, as a PNG.
 
 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
@@ -235,7 +247,7 @@ sub check {
     $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')
@@ -248,6 +260,8 @@ sub check {
     || $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')
@@ -365,6 +379,21 @@ sub part_export {
   });
 }
 
+=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
@@ -442,6 +471,17 @@ sub process_generate_coverage {
   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
@@ -451,4 +491,3 @@ L<FS::tower>, L<FS::Record>, schema.html from the base documentation.
 =cut
 
 1;
-
index 4a637f5..6bb3e4a 100755 (executable)
@@ -35,6 +35,11 @@ GetOptions(
   "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;
@@ -65,11 +70,13 @@ print $cfh $page;
 
 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
index 10faa74..1e77c3a 100644 (file)
@@ -5,14 +5,15 @@ use Getopt::Std;
 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 
@@ -104,24 +105,19 @@ if ( $opt{P} =~ /^(\d+)$/ ) {
 }
 
 # 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);
 }
 
@@ -131,7 +127,7 @@ my %e911_qty; # custnum => sum of E911-subject quantity
 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";
@@ -140,7 +136,7 @@ FILE: foreach my $filename (@$files) {
 
   # 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
@@ -155,9 +151,9 @@ FILE: foreach my $filename (@$files) {
 
   #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 $!;
   }
 
@@ -172,7 +168,7 @@ FILE: foreach my $filename (@$files) {
     @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 = 
@@ -181,8 +177,7 @@ FILE: foreach my $filename (@$files) {
       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});
 
@@ -233,8 +228,7 @@ FILE: foreach my $filename (@$files) {
       }
       $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;
@@ -277,8 +271,7 @@ foreach my $custnum ( keys (%e911_qty) ) {
 
 $dbh->commit;
 
-if ($opt{v}) {
-  print STDERR "
+mylog('debug', "
 Finished!
   Processed files: @$files
   Created charges: $num_charges
@@ -286,7 +279,43 @@ Finished!
   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
@@ -320,7 +349,8 @@ directory is the one containing the "ready/" and "done/" subdirectories.
 
 =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.
 
index 9a1f609..4d99df2 100755 (executable)
@@ -62,7 +62,7 @@ while ($ssh_retry > 0) {
   $sftp = Net::SFTP::Foreign->new( host => $host,
                                    user => $username,
                                    password => $password,
-                                   timeout => 30,
+                                   timeout => 300,
                                  );
   last unless $sftp->error;
   $ssh_retry -= 1;
index 799e6c4..770239d 100755 (executable)
@@ -97,7 +97,7 @@ while ($ssh_retry > 0) {
   $sftp = Net::SFTP::Foreign->new( host => $host,
                                    user => $username,
                                    password => $password,
-                                   timeout => 30,
+                                   timeout => 300,
                                  );
   last unless $sftp->error;
   $ssh_retry -= 1;
index c6c8a75..14dddd1 100755 (executable)
@@ -15,10 +15,14 @@ foreach my $cust_main (
 
   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;]
index 1f2ca3f..5da6cb5 100755 (executable)
@@ -10,6 +10,9 @@ it could be updated to fork on client connections.
 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;
@@ -33,21 +36,21 @@ while(my $conn = $server->accept()) {
 
   $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;
 }
diff --git a/bin/move_svc_broadband_speeds.pl b/bin/move_svc_broadband_speeds.pl
new file mode 100755 (executable)
index 0000000..7d20ef6
--- /dev/null
@@ -0,0 +1,66 @@
+#!/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
index d8a2d0a..1241f87 100644 (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>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>
+      </tbody>
+      </table>
+      <br><br>
+    ';
+
+} %>
+
 <%= length($summary)
       ? ''
       : ( $smallernotes
index 249db9b..c07d7e2 100644 (file)
           <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>
index a710cdd..478405e 100644 (file)
   }\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
index bd4ea69..3b13327 100644 (file)
@@ -9,12 +9,9 @@
 \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) --@]}\\
 &\\
index 2391f73..2ec3689 100644 (file)
@@ -101,7 +101,7 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip,latex-xcolor,
  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)
index dfe6371..eb3e061 100644 (file)
@@ -9,7 +9,7 @@
         <TD>
           <SELECT NAME="month">
             <%= for ( ( map "0$_", 1 .. 9 ), 10 .. 12 ) {
-                  $OUT .= '<OPTION'. ($_ == $month ? ' SELECTED' : ''). ">$_\n";
+                  $OUT .= '<OPTION'. ($_ == $month ? ' SELECTED' : ''). " VALUE='$_'>$_\n";
             } %>
           </SELECT>
         </TD>
@@ -17,7 +17,7 @@
         <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>
index 6cf264c..3dc69e1 100755 (executable)
@@ -726,9 +726,9 @@ sub payment_results {
   $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";
@@ -1325,5 +1325,3 @@ sub include {
                     );
 
 }
-
-
index ba40bfd..9e7ef2a 100644 (file)
                           '',
                         ],
      '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;
                               },
index 9b2298a..deb98c3 100644 (file)
@@ -1,22 +1,18 @@
 <% 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>
index 8c51b35..f25c00e 100755 (executable)
@@ -562,7 +562,7 @@ if ( $acl_edit_global ) {
                                 'action'      => "${p}edit/bulk-cust_pkg.html?".
                                                  'pkgpart='.$part_pkg->pkgpart,
                                 'actionlabel' => 'Change Packages',
-                                'width'       => 569,
+                                'width'       => 960,
                                 'height'      => 210,
                               ).' ]</FONT>',
                             'align' => 'left',
@@ -796,8 +796,22 @@ if ( $acl_edit_bulk ) {
   $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;
index b947463..222433d 100755 (executable)
@@ -251,6 +251,7 @@ my %flag = (
   'A' => 'Automatically filled in from inventory',
   'H' => 'Selected from hardware class',
   'X' => 'Excluded',
+  'P' => 'From package 477 information',
 );
 
 my %search;
index 3d57b31..d84edce 100644 (file)
@@ -75,7 +75,7 @@
         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 %>');
@@ -164,7 +164,7 @@ foreach my $type ( ref($i->type) ? @{$i->type} : $i->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);
index 6d15164..38411f1 100644 (file)
@@ -18,9 +18,12 @@ Use gateway <SELECT NAME="gatewaynum">
 
   <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>
index 2ff38ca..8a082f4 100644 (file)
@@ -19,24 +19,18 @@ function areyousure() {
 }
 </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>
@@ -57,4 +51,18 @@ to new package definition
 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>
index 3b7eb07..317257b 100644 (file)
@@ -1,13 +1,35 @@
+<%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' => '&nbsp;',
-                          },
-     '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' => '&nbsp;',
+                           },
+     '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'));
@@ -77,7 +107,7 @@ my $m2_error_callback = sub {
   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 {
index 05bf437..3cc55f3 100755 (executable)
@@ -111,7 +111,7 @@ function samechanged(what) {
 
 </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,
@@ -286,7 +286,7 @@ if ( $cgi->param('error') ) {
     $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
index 1171e7d..9031425 100644 (file)
@@ -1,3 +1,17 @@
+<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>
@@ -5,6 +19,7 @@
      'embed'            => $opt{cust_main},
      'table'            => 'cust_main',
      'agent_virt'       => 1,
+     'submit_id'        => $opt{submit_id},
      'html_table_class' => 'fsinnerbox',
      'labels'           => { 'contactnum'  => '', #'Contact',
                              #'locationnum' => '&nbsp;',
index 713f54c..0319cf0 100644 (file)
@@ -1,7 +1,17 @@
 <%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') ) {
         &nbsp;
-        <& .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 %>">
 % }
@@ -47,7 +61,7 @@ my $agentnum = $cust_main->agentnum if $cust_main->custnum;
 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');
index 3500d63..0f28809 100644 (file)
@@ -1,7 +1,12 @@
 % 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?
index e1975ed..f3dec98 100755 (executable)
@@ -34,7 +34,7 @@
 %  }
 
   <BR>Payment
-  <% ntable("#cccccc", 2) %>
+  <TABLE class="fsinnerbox">
 
     <TR>
       <TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">$<% $cust_pay->paid %></TD>
@@ -85,7 +85,8 @@
 
 
 <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">&nbsp;&nbsp;&nbsp;<% 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',
@@ -159,16 +216,18 @@ my $payby   = $cgi->param('payby');
 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;
index 8ba703a..1d47209 100644 (file)
@@ -114,6 +114,9 @@ Example:
     #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)
@@ -398,6 +401,8 @@ Example:
 %
 %   my $layer_prefix_on = '';
 %
+%   my $submitid   = $opt{submit_id} ? $opt{submit_id} : '';
+%
 %   my $include_sub = sub {
 %     my %opt = @_;
 %
@@ -422,6 +427,7 @@ Example:
 %         'field'      => "$field$fieldnum",
 %         'id'         => "$field$fieldnum", #separate?
 %         'label_id'   => $field."_label$fieldnum", #don't want field0_label0...
+%         'submit_id'  => $submitid,
 %         %include_common,
 %         %opt,
 %     );
@@ -669,7 +675,7 @@ Example:
           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 = '';
index faf179a..a426e61 100644 (file)
@@ -45,20 +45,23 @@ function receive_mib_get(obj, rownum) {
 </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 = @_;
@@ -73,11 +76,13 @@ foreach my $field ( qw(snmp_version snmp_community snmp_timeout) ) {
 }
 
 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;
index 816f342..bdbce7c 100644 (file)
@@ -15,6 +15,7 @@ To be called from part_svc.cgi.
 # 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', 
@@ -38,6 +39,9 @@ tie my %flag, 'Tie::IxHash',
   '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
          },
@@ -202,6 +206,20 @@ my %communigate_fields = (
 %       $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',
@@ -211,6 +229,7 @@ my %communigate_fields = (
           'empty_label' => "Select $mode class",
           'multiple'    => $multiple,
       &>
+%     }
 %   }
     </TD>
     <TD>
index a4e345e..e1c3090 100644 (file)
                                      ];
                    } # 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'} =
index 5411feb..f6ec208 100644 (file)
@@ -290,6 +290,20 @@ my $widget = new HTML::Widgets::SelectLayers(
     $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
index fed2125..49c1c03 100755 (executable)
@@ -107,7 +107,7 @@ function flag_changed(obj) {
         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;
@@ -120,7 +120,7 @@ function flag_changed(obj) {
   }
   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 {
index fcd210f..c272620 100644 (file)
@@ -11,7 +11,7 @@
                                        '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,
              )
 %>
@@ -38,24 +38,22 @@ sub precheck_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];
index 5b8319f..6b7f1c2 100644 (file)
@@ -8,7 +8,7 @@
 </%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,
index 0a3d550..1f96456 100755 (executable)
@@ -53,7 +53,7 @@ if ( $error ) {
              '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 ) {
 
@@ -71,6 +71,18 @@ 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 {
 
@@ -192,16 +204,19 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) {
   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($_)) }
@@ -209,28 +224,23 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) {
                                    );
     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');
index 76722c9..d2b0370 100644 (file)
@@ -80,8 +80,12 @@ Example:
    '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 ) = @_; },
 
@@ -89,6 +93,9 @@ Example:
    # 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
@@ -273,7 +280,7 @@ foreach my $value ( @values ) {
     }
 
     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'} ) {
@@ -438,6 +445,12 @@ foreach my $value ( @values ) {
     }
   }
 
+  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);
@@ -459,6 +472,14 @@ foreach my $value ( @values ) {
 
 }
 
+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;
index c4d150b..3764910 100755 (executable)
@@ -9,6 +9,7 @@
               '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,
@@ -199,6 +200,38 @@ my $args_callback = sub {
 
 };
 
+## 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;
index 7ae7e0d..51e40ed 100644 (file)
@@ -10,6 +10,8 @@ my $callback = sub {
   $obj->usernum( $FS::CurrentUser::CurrentUser->usernum );
   # if this would change it from its existing owner, replace_check
   # will refuse
+
+  ''; #no error
 };
 
 </%init>
index cfbb4ff..8f62c4b 100644 (file)
@@ -6,7 +6,7 @@
                        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
                      )],
                    },
 &>
index 81c694a..bcf55fe 100644 (file)
@@ -100,7 +100,7 @@ END
 ;
 
 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 { 
@@ -179,7 +179,6 @@ my $svc_field_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') ];
   }
index 946a140..dfebc00 100644 (file)
@@ -13,6 +13,8 @@
                         'altitude',
                         'height',
                         'veg_height',
+                        'up_rate_limit',
+                        'down_rate_limit',
 #                        { field             => 'sectornum',
 #                          type              => 'tower_sector',
 #                          o2m_table         => 'tower_sector',
@@ -35,6 +37,8 @@
                         '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>
@@ -43,7 +47,7 @@ my $m2_error_callback = sub { # reconstruct the list
   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 {
index 213bc44..1164504 100644 (file)
@@ -26,7 +26,7 @@ function broadband_snmp_get (svcnum) {
           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);
@@ -36,6 +36,9 @@ function broadband_snmp_get (svcnum) {
               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');
index 7d95e19..068d7d7 100644 (file)
@@ -11,9 +11,9 @@
 % 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 %>
@@ -44,11 +44,9 @@ function <%$pre%>toggle(toggle, clear) {
   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 =
@@ -56,7 +54,7 @@ function <%$pre%>toggle(toggle, clear) {
 % }
 }
 
-function checkPasswordValidation()  {
+function <%$pre%>checkPasswordValidation(resultId)  {
   var validationResult = document.getElementById('<%$pre%>password_result').innerHTML;
   if (validationResult.match(/Password valid!/)) {
     return true;
@@ -83,8 +81,8 @@ if ($opt{'svc_acct'}) {
 }
 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'};
 }
index 4e9a609..05250fe 100644 (file)
@@ -132,14 +132,14 @@ function <% $pre %>county_changed(what, callback) {}
     >
 
 %   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>
 
 %   }
 
index 43e5201..48b5e2c 100644 (file)
@@ -1,4 +1,6 @@
-% unless ( $opt{'js_only'} ) {
+% if ( $opt{'js_only'} ) {
+<% $js %>
+% } else {
 
   <INPUT TYPE="hidden" NAME="<%$name%>" ID="<%$id%>" VALUE="<% $curr_value %>">
 
@@ -40,6 +42,8 @@
 %         }
 %       } 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>
@@ -119,6 +123,7 @@ my $name = $opt{'element_name'} || $opt{'field'} || 'contactnum';
 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'} ) {
@@ -171,6 +176,7 @@ unless ($opt{'for_prospect'}) {
   $label{'invoice_dest'} = 'Send&nbsp;invoices';
   $label{'message_dest'} = 'Send&nbsp;messages';
   $label{'selfservice_access'} = 'Self-service';
+  $label{'password'} = 'Password';
 }
 
 my $first = 0;
@@ -185,7 +191,21 @@ $label{'comment'} = 'Comment';
 
 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>
diff --git a/httemplate/elements/cust_payby_new.html b/httemplate/elements/cust_payby_new.html
new file mode 100644 (file)
index 0000000..8b1d93d
--- /dev/null
@@ -0,0 +1,217 @@
+% 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
index c6b10e3..6df45fb 100644 (file)
@@ -4,3 +4,4 @@
 % } else {
 <& header-full.html, @_ &>
 % }
+<& /misc/edge_browser_check-header.html &>
diff --git a/httemplate/elements/link-replace_element_text.html b/httemplate/elements/link-replace_element_text.html
new file mode 100644 (file)
index 0000000..8e61195
--- /dev/null
@@ -0,0 +1,45 @@
+<%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 %>');">&#x25C1;</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>
index eb065b6..cae0cdb 100644 (file)
@@ -418,7 +418,9 @@ if( $curuser->access_right('Financial reports') ) {
 
   $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')) {
 
index 469ae7f..a0d380b 100644 (file)
@@ -5,12 +5,15 @@ my $id = $opt{'id'} || $opt{'field'};
 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">&nbsp;</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;
index 778aa20..3a632b9 100644 (file)
@@ -19,6 +19,7 @@ function <% $id %>randomPass() {
   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;
 % }
index c981479..2868267 100644 (file)
@@ -91,15 +91,13 @@ Example:
 >
 
 % 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>
index 62c10b1..406c13b 100644 (file)
@@ -3,16 +3,15 @@
 <% $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>
index 3fb5597..8db157b 100644 (file)
@@ -27,16 +27,13 @@ Example:
 >
 
 % 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>
index a52fdfa..d86b7ee 100644 (file)
@@ -83,11 +83,11 @@ Example:
 %                   || ( $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 ( 
@@ -118,7 +118,7 @@ Example:
           ? &{ $opt{'label_callback'} }( $record )
           : $record->$name_col()
         |h
-     %>
+     %></OPTION>
 % } 
 
 % while ( @post_options ) { 
@@ -128,7 +128,7 @@ Example:
 %                  || ( $value eq $post_opt );
     <OPTION VALUE="<% $post_opt %>"
             <% $selected ? 'SELECTED' : '' %>
-    ><% $post_label %>
+    ><% $post_label %></OPTION>
 % } 
 
 </SELECT>
index 9c13f59..a84fef6 100644 (file)
@@ -1,6 +1,6 @@
-  <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"
@@ -8,7 +8,7 @@
                                 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>';
+% }
 
       }
 
@@ -66,6 +79,9 @@ my $fee = '';
 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'}
@@ -86,13 +102,21 @@ if ( $opt{'process-pkgpart'}
 }
 
 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);
 }
index e2b2e09..e5ace4d 100644 (file)
@@ -1,4 +1,4 @@
-% if ( scalar(@{ $opt{'cust_payby'} }) == 0 ) { 
+% if ( scalar(@{ $opt{'cust_payby'} }) == 0 ) {
 
   <INPUT TYPE="hidden" NAME="<% $opt{'element_name'} || $opt{'field'} || 'custpaybynum' %>" VALUE="">
 
index 3728d34..d35813f 100644 (file)
@@ -5,7 +5,11 @@ Example:
   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
   )
 
@@ -13,31 +17,35 @@ Example:
 
   <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>
index 2304c22..f86f3ed 100644 (file)
@@ -5,9 +5,9 @@ Example:
   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'),
@@ -17,70 +17,139 @@ Example:
           ? 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>';
+          }
         }
 
       }
@@ -91,4 +160,21 @@ Example:
 
 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
index 2aa715e..72640d3 100644 (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 => [
@@ -58,14 +134,20 @@ function clearhint_ip_addr (what) {
 </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;">&#9660;</button>
 % }
 </td> </tr>
 <script type="text/javascript">
index 106fc76..8acedb8 100644 (file)
@@ -17,7 +17,7 @@ my $tabcounter = 0;
 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;
@@ -291,6 +291,20 @@ $(function() {
              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>
index 4057f5d..6aada2f 100644 (file)
@@ -14,58 +14,10 @@ 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) {
-  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>
diff --git a/httemplate/elements/validate_password_js.html b/httemplate/elements/validate_password_js.html
new file mode 100644 (file)
index 0000000..64db0a9
--- /dev/null
@@ -0,0 +1,71 @@
+<%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
index 5074aa3..22327e0 100644 (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
@@ -35,7 +38,8 @@ my $title = $sel_agent ? $sel_agent->agent.' ' : '';
 $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;
@@ -56,7 +60,10 @@ foreach my $agent ( $sel_agent || qsearch('agent', { 'disabled' => '' } ) ) {
 
   #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.' ' );
index 36ad782..094c652 100644 (file)
        '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?
index 3b200e5..da242a2 100644 (file)
@@ -36,6 +36,7 @@ Import a file containing customer packages.
         <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
@@ -105,6 +106,9 @@ Uploaded files can be CSV (comma-separated value) files or Excel spreadsheets.
 <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>
 
@@ -215,9 +219,9 @@ Field information:
 
   <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)
 
index c4bc37e..c6a0b68 100644 (file)
@@ -20,9 +20,18 @@ elsif ( $cgi->param('format') =~ /^([\w\- ]+)$/ ) {
   $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' );
diff --git a/httemplate/misc/edge_browser_check-fail_notice.html b/httemplate/misc/edge_browser_check-fail_notice.html
new file mode 100644 (file)
index 0000000..fb42ffe
--- /dev/null
@@ -0,0 +1,25 @@
+<& /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;">
+    &#9888;
+  </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
diff --git a/httemplate/misc/edge_browser_check-header.html b/httemplate/misc/edge_browser_check-header.html
new file mode 100644 (file)
index 0000000..a88962b
--- /dev/null
@@ -0,0 +1,36 @@
+% 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>
diff --git a/httemplate/misc/edge_browser_check-iframe.html b/httemplate/misc/edge_browser_check-iframe.html
new file mode 100644 (file)
index 0000000..61ae9a0
--- /dev/null
@@ -0,0 +1,34 @@
+<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
index 5bfa29d..77f5acd 100644 (file)
@@ -13,8 +13,7 @@
 <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') ) {
@@ -97,6 +101,11 @@ function change_batch_checkbox () {
        $('#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
@@ -130,186 +139,58 @@ function change_batch_checkbox () {
 >
 <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>
 
@@ -337,6 +218,17 @@ $cgi->param('payby') =~ /^(CARD|CHEK)$/
   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;
@@ -350,13 +242,6 @@ 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;
-
 my $payunique = "webui-payment-". time. "-$$-". rand() * 2**32;
 
 </%init>
index a3e0601..1c746a4 100644 (file)
         <% $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;
 
@@ -61,7 +74,7 @@ if ($svcnum) {
        $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)
index 717d57c..7747bcb 100644 (file)
@@ -39,6 +39,8 @@ my $cust_main = qsearchs({
   '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;
@@ -90,6 +92,7 @@ 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");
 
 } else {
 
@@ -97,11 +100,11 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) {
   # 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;
 
@@ -208,17 +211,28 @@ if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) {
 
 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}}
                                    );
@@ -241,6 +255,7 @@ if ( $cgi->param('batch') ) {
     '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;
index a0cf743..24a0f5d 100755 (executable)
@@ -113,12 +113,15 @@ foreach my $id ( map { /^transactionid(\d+)$/; $1; }
     $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 } 
+      ];
                             
   }
 }
diff --git a/httemplate/misc/xmlhttp-free_addresses_in_block.json.html b/httemplate/misc/xmlhttp-free_addresses_in_block.json.html
new file mode 100644 (file)
index 0000000..801718d
--- /dev/null
@@ -0,0 +1,18 @@
+<%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>
index 4d9716b..c53abe8 100644 (file)
@@ -28,14 +28,14 @@ my $validate_password = sub {
   $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);
   }
index f4fbd56..eb39dea 100644 (file)
@@ -38,7 +38,7 @@ Parameters:
         if ( $_[0]->pkgdiscountnum ) {
             # Standard discount, not a waived setup fee
             my $discount = qsearchs('discount',{
-                pkgdiscountnum => $_[0]->pkgdiscountnum
+                discountnum => $_[0]->discountnum
             });
             return $discount->description;
         } else {
@@ -228,7 +228,11 @@ if ( $cgi->param('usernum') =~ /^(\d+)$/ ) {
 }
 
 # 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";
 }
 
index b1ba909..1766c19 100644 (file)
@@ -134,6 +134,12 @@ my $trigger_link = sub {
     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' ];
   }
@@ -199,6 +205,7 @@ my $sql_query = {
                     '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(),
                   ),
index cc958b9..6b175ad 100755 (executable)
@@ -114,7 +114,7 @@ $count_query = 'SELECT COUNT(*) FROM cust_pay_batch ' .
 
 $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 ) '.
index a380b78..9c9a826 100644 (file)
@@ -11,6 +11,7 @@
                   'header'      => \@header,
                   'fields'      => \@fields,
                   'links'       => \@links,
+                  'disable_maxselect' => '1',
 
 &>
 <%init>
@@ -84,11 +85,16 @@ my $active_pkg_sql = 'select pkgnum from cust_pkg where cust_pkg.custnum = cust_
 
 ## 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' );
@@ -96,8 +102,6 @@ my @fields = ( 'custnum', 'custname', $location_sub, 'daytime', $email_sub, 'act
 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",
index 75dbef7..6d387d5 100644 (file)
@@ -1,6 +1,23 @@
+<%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 }
@@ -8,27 +25,27 @@ table.grid TD { font-weight: bold;
                 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 &>
@@ -38,6 +55,8 @@ table.grid TD { font-weight: bold;
 % }
 <%init>
 
+our $DEBUG;
+
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
 
@@ -56,56 +75,89 @@ my $agentnum = $1;
 # 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) . '&mdash;' .
-             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 &#x2194; $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>
index ae8b794..f33a874 100644 (file)
@@ -4,7 +4,7 @@
   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?'
@@ -50,7 +50,7 @@ false.
 <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} %>\
index b1e5430..efc0097 100644 (file)
@@ -141,13 +141,17 @@ Usage:
   $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;
@@ -169,8 +173,10 @@ as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR>
 %     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%>>
@@ -182,8 +188,10 @@ as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR>
 % }
 </table>
 <% $foot %>
+% unless ( $suppress_footer ) {
 <& /elements/footer.html &>
 % }
+% }
 <%args>
 $title
 @rows
@@ -192,4 +200,6 @@ $head => ''
 $foot => ''
 $table_width => "100%"
 $table_class => "report"
+$suppress_header => undef
+$suppress_footer => undef
 </%args>
index 0e8c69a..730a51a 100644 (file)
@@ -389,14 +389,15 @@ unless ( $type =~ /^(csv|xml|\w*.xls)$/) {
     #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" : '';
 
index 711a25f..2e723ec 100644 (file)
@@ -2,20 +2,23 @@
 
 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 => "",
@@ -29,14 +32,57 @@ necessary, to solve the performance problem.
       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;
@@ -45,45 +91,49 @@ die "access denied"
   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;
@@ -93,80 +143,149 @@ die "access denied"
     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 = [
index d65d4d1..0eb45f3 100644 (file)
@@ -17,7 +17,6 @@
                                           }
                                           $pm->prospect_contact
                                     ];
-                                    ''
                                   },
                                   sub {
                                     my $pr = shift->part_referral;
index ef54478..4c16f74 100644 (file)
@@ -189,8 +189,16 @@ die "access denied"
 
 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 =
index 7aa4ff9..84eb45f 100644 (file)
@@ -5,11 +5,9 @@
 %>
 
     <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 ) %>
 
@@ -44,7 +42,7 @@
         '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',
index 4ff3bb8..27dd940 100644 (file)
@@ -20,6 +20,8 @@
        'curr_value' => scalar( $cgi->param('cust_status') ),
   &>
 
+  <& /elements/tr-input-beginning_ending.html &>
+
 </FORM>
 
 </TABLE>
diff --git a/httemplate/search/report_future_autobill-queued_job.html b/httemplate/search/report_future_autobill-queued_job.html
new file mode 100644 (file)
index 0000000..d23efb5
--- /dev/null
@@ -0,0 +1,11 @@
+<% $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>
index 1a0c9f4..28f589e 100644 (file)
@@ -1,42 +1,73 @@
 <%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>
+
index 1660c1c..efcf48e 100644 (file)
     <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') || '&nbsp;' %></TD>
+    <TD>
+      <span id="ss_span" style="white-space:nowrap;">
+      <% $cust_main->masked('ss') || '&nbsp;' %>
+%   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') || '&nbsp' %></TD>
+    <TD>
+      <span id="stateid_span" style="white-space:nowrap;">
+      <% $cust_main->masked('stateid') || '&nbsp' %>
+%   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 || '&nbsp' %></TD>
   </TR>
index fe412cc..9252b21 100644 (file)
@@ -22,6 +22,7 @@
 %   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>
@@ -63,6 +74,7 @@
 %      } else {
 %        $bgcolor = $bgcolor1;
 %      }
+%     $count++;
 %   }
 </TABLE>
 %}
@@ -80,6 +92,6 @@ my @cust_contacts = $cust_main->cust_contact;
 
 # 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>
index f3aca21..7ec4d07 100644 (file)
@@ -460,7 +460,7 @@ my @menu = (
         ##  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,
index f4dd414..504a5a8 100644 (file)
 % 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>
 %}
 
index 0517c30..189fe5e 100644 (file)
@@ -33,6 +33,9 @@ my @fields = (
   { 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 => \&sectornum },
   { field => 'mac_addr', type=>'mac_addr', value_callback => \&mac_addr },
diff --git a/httemplate/view/svc_export/run_script.cgi b/httemplate/view/svc_export/run_script.cgi
new file mode 100644 (file)
index 0000000..ba58bbd
--- /dev/null
@@ -0,0 +1,31 @@
+<% $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
diff --git a/min_selfservice/css/default.css b/min_selfservice/css/default.css
deleted file mode 100644 (file)
index 74f3565..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-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
diff --git a/min_selfservice/elements/card.php b/min_selfservice/elements/card.php
deleted file mode 100644 (file)
index 4d502c2..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-<TR>
-  <TD ALIGN="right">Card&nbsp;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&nbsp;(<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&nbsp;name&nbsp;on&nbsp;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&nbsp;billing&nbsp;address',
-                           ],
-         ));
-   echo $lf['output'];
-?>
diff --git a/min_selfservice/elements/check.php b/min_selfservice/elements/check.php
deleted file mode 100644 (file)
index fd0cd6d..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-<? if ($ach_read_only) { $bgShade = 'BGCOLOR="#ffffff"';  } ?>
-<TR>
-  <TD ALIGN="right">Account&nbsp;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&nbsp;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&nbsp;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&nbsp;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&nbsp;holder<BR>Social&nbsp;security&nbsp;or&nbsp;tax&nbsp;ID&nbsp;#</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&nbsp;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>
-  <? } ?>
diff --git a/min_selfservice/elements/error.php b/min_selfservice/elements/error.php
deleted file mode 100644 (file)
index c8d8a17..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<? if ($error) { ?>
-       <FONT SIZE="+1" COLOR="#ff0000"><? echo htmlspecialchars($error); echo '<BR><BR>'; ?></FONT>
-<? } ?>
diff --git a/min_selfservice/elements/footer.php b/min_selfservice/elements/footer.php
deleted file mode 100644 (file)
index fb662be..0000000
+++ /dev/null
@@ -1 +0,0 @@
-</BODY></HTML>
diff --git a/min_selfservice/elements/header.php b/min_selfservice/elements/header.php
deleted file mode 100644 (file)
index 6339965..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<!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>
-
diff --git a/min_selfservice/elements/menu.php b/min_selfservice/elements/menu.php
deleted file mode 100644 (file)
index 5f600d5..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-<?
-
-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
diff --git a/min_selfservice/elements/menu_footer.php b/min_selfservice/elements/menu_footer.php
deleted file mode 100644 (file)
index 8beeeaf..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-</td>
-</tr>
-</table>
diff --git a/min_selfservice/elements/session.php b/min_selfservice/elements/session.php
deleted file mode 100644 (file)
index a6b8b4a..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<?
-
-require_once('freeside.class.php');
-$freeside = new FreesideSelfService();
-
-?>
diff --git a/min_selfservice/freeside.class.php b/min_selfservice/freeside.class.php
deleted file mode 100644 (file)
index ee77ce0..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-<?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;
-        }
-    }
-
-}
-
-?>
diff --git a/min_selfservice/images/cross.png b/min_selfservice/images/cross.png
deleted file mode 100644 (file)
index 1514d51..0000000
Binary files a/min_selfservice/images/cross.png and /dev/null differ
diff --git a/min_selfservice/images/dropdown_arrow_grey.gif b/min_selfservice/images/dropdown_arrow_grey.gif
deleted file mode 100644 (file)
index fbf155d..0000000
Binary files a/min_selfservice/images/dropdown_arrow_grey.gif and /dev/null differ
diff --git a/min_selfservice/images/dropdown_arrow_white.gif b/min_selfservice/images/dropdown_arrow_white.gif
deleted file mode 100644 (file)
index c24d784..0000000
Binary files a/min_selfservice/images/dropdown_arrow_white.gif and /dev/null differ
diff --git a/min_selfservice/images/error.png b/min_selfservice/images/error.png
deleted file mode 100644 (file)
index 628cf2d..0000000
Binary files a/min_selfservice/images/error.png and /dev/null differ
diff --git a/min_selfservice/images/tick.png b/min_selfservice/images/tick.png
deleted file mode 100644 (file)
index a9925a0..0000000
Binary files a/min_selfservice/images/tick.png and /dev/null differ
diff --git a/min_selfservice/index.php b/min_selfservice/index.php
deleted file mode 100644 (file)
index c7e20c5..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<?
-  $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
diff --git a/min_selfservice/js/jquery.js b/min_selfservice/js/jquery.js
deleted file mode 100644 (file)
index e407e76..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-/*! 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);
diff --git a/min_selfservice/js/menu.js b/min_selfservice/js/menu.js
deleted file mode 100644 (file)
index 30e4816..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-$(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();
-               }
-       });
-});
diff --git a/min_selfservice/login.php b/min_selfservice/login.php
deleted file mode 100644 (file)
index 91e19cd..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-<?
-
-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
diff --git a/min_selfservice/main.php b/min_selfservice/main.php
deleted file mode 100644 (file)
index 9c58f3f..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-<? $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
diff --git a/min_selfservice/payment.php b/min_selfservice/payment.php
deleted file mode 100644 (file)
index f93d08e..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<? $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
diff --git a/min_selfservice/payment_ach.php b/min_selfservice/payment_ach.php
deleted file mode 100644 (file)
index 04e39c5..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-<? $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&nbsp;Due</TD>
-    <TD>
-      <TABLE><TR><TD BGCOLOR="#ffffff">
-        $<? echo sprintf("%.2f", $balance) ?>
-      </TD></TR></TABLE>
-    </TD>
-  </TR>
-
-  <TR>
-    <TD ALIGN="right">Payment&nbsp;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
diff --git a/min_selfservice/payment_cc.php b/min_selfservice/payment_cc.php
deleted file mode 100644 (file)
index f47e83c..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-<? $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&nbsp;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&nbsp;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
diff --git a/min_selfservice/payment_finish.php b/min_selfservice/payment_finish.php
deleted file mode 100644 (file)
index 04fdfa6..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-<? $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&nbsp;#</TD>
-  <TD BGCOLOR="#ffffff"><B><? echo($payment_results['paynum']); ?></B></TD>
-</TR>
-<TR>
-  <TD ALIGN="right">Payment&nbsp;amount</TH>
-  <TD BGCOLOR="#ffffff"><B>$<? printf('%.2f', $payment_results['paid']); ?></B>
-  </TD>
-</TR>
-<TR>
-  <TD ALIGN="right">Processing&nbsp;#</TD>
-  <TD BGCOLOR="#ffffff"><B><? echo($payment_results['order_number']); ?></B>
-  </TD>
-</TR>
-<? } ?>
\ No newline at end of file
diff --git a/min_selfservice/payment_paypal.php b/min_selfservice/payment_paypal.php
deleted file mode 100644 (file)
index 7a70f98..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<? $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&nbsp;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
diff --git a/min_selfservice/payment_webpay.php b/min_selfservice/payment_webpay.php
deleted file mode 100644 (file)
index e4343fc..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<? $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&nbsp;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
index 6339965..3ef5c6e 100644 (file)
@@ -1,3 +1,21 @@
+<?
+
+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>
index 62b6562..06e8fc1 100644 (file)
@@ -3,7 +3,7 @@
 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);
 
@@ -60,7 +60,7 @@ if ( $error ) {
 <? 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>
@@ -85,6 +85,10 @@ if ( $error ) {
 
 <? } ?>
 
+<!--
+<BR><BR><A HREF="ip_login.php">Login by IP (<? echo $_SERVER['REMOTE_ADDR']; ?>) to make a payment.</A>
+-->
+
 <? include('elements/footer.php'); ?>
 
 
diff --git a/ng_selfservice/ip_login.php b/ng_selfservice/ip_login.php
new file mode 100644 (file)
index 0000000..1530657
--- /dev/null
@@ -0,0 +1,105 @@
+<?
+
+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'); ?>
diff --git a/ng_selfservice/no_access.php b/ng_selfservice/no_access.php
new file mode 100644 (file)
index 0000000..b13cca9
--- /dev/null
@@ -0,0 +1,34 @@
+<!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
index 15b000b..d98281a 100644 (file)
@@ -3,6 +3,13 @@
 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']),
@@ -16,9 +23,9 @@ $error = $response['error'];
 
 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();
@@ -43,7 +50,7 @@ if ( $response['custnum'] || $response['svcnum'] ) {
   die();
 
 } elseif ( $response['customers'] ) {
-var_dump($response['customers']);
+  //var_dump($response['customers']);
 ?>
 
   <? $title ='Select customer'; include('elements/header.php'); ?>
index 1818c2d..b700fa7 100755 (executable)
@@ -1282,6 +1282,14 @@ sub Customers {
 
       $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(