This commit was manufactured by cvs2svn to create tag 'freeside_1_5_0pre4'. freeside_1_5_0pre4
authorcvs2git <cvs2git>
Sat, 28 Feb 2004 23:06:14 +0000 (23:06 +0000)
committercvs2git <cvs2git>
Sat, 28 Feb 2004 23:06:14 +0000 (23:06 +0000)
184 files changed:
CREDITS
FS/FS.pm
FS/FS/CGI.pm
FS/FS/ClientAPI/MyAccount.pm
FS/FS/ClientAPI/Signup.pm [new file with mode: 0644]
FS/FS/Conf.pm
FS/FS/Record.pm
FS/FS/UID.pm
FS/FS/acct_snarf.pm [new file with mode: 0644]
FS/FS/addr_block.pm
FS/FS/agent.pm
FS/FS/cust_bill.pm
FS/FS/cust_bill_pay.pm
FS/FS/cust_credit.pm
FS/FS/cust_credit_bill.pm
FS/FS/cust_main.pm
FS/FS/cust_main_county.pm
FS/FS/cust_pay.pm
FS/FS/cust_pay_batch.pm
FS/FS/cust_pkg.pm
FS/FS/cust_svc.pm
FS/FS/part_bill_event.pm
FS/FS/part_export.pm
FS/FS/part_export/communigate_pro.pm [new file with mode: 0644]
FS/FS/part_export/communigate_pro_singledomain.pm [new file with mode: 0644]
FS/FS/part_export/domain_shellcommands.pm
FS/FS/part_export/forward_shellcommands.pm
FS/FS/part_export/postfix.pm [new file with mode: 0644]
FS/FS/part_export/router.pm [new file with mode: 0644]
FS/FS/part_export/shellcommands.pm
FS/FS/part_export/www_shellcommands.pm
FS/FS/part_pkg.pm
FS/FS/part_referral.pm
FS/FS/pkg_svc.pm
FS/FS/raddb.pm
FS/FS/svc_Common.pm
FS/FS/svc_acct.pm
FS/FS/svc_domain.pm
FS/FS/svc_external.pm [new file with mode: 0644]
FS/FS/svc_forward.pm
FS/MANIFEST
FS/bin/freeside-addoutsourceuser
FS/bin/freeside-daily
FS/bin/freeside-receivables-report [deleted file]
FS/bin/freeside-selfservice-server
FS/bin/freeside-setup
FS/t/acct_snarf.t [new file with mode: 0644]
FS/t/svc_broadband.t [new file with mode: 0644]
FS/t/svc_external.t [new file with mode: 0644]
Makefile
bin/bind.import
bin/create-fetchmailrc [new file with mode: 0644]
bin/dbdef-create
bin/generate-raddb
bin/masonize
bin/postfix.export [new file with mode: 0755]
bin/sendmail.import [new file with mode: 0644]
bin/shadow.reimport [new file with mode: 0755]
conf/invoice_latex [new file with mode: 0644]
conf/invoice_latexfooter [new file with mode: 0644]
conf/invoice_latexnotes [new file with mode: 0644]
conf/invoice_latexsmallfooter [new file with mode: 0644]
debian/control
eg/table_template-svc.pm
fs_selfservice/FS-SelfService/Makefile.PL
fs_selfservice/FS-SelfService/SelfService.pm
fs_selfservice/FS-SelfService/cgi/login.html
fs_selfservice/FS-SelfService/freeside-selfservice-clientd
fs_signup/FS-SignupClient/Makefile.PL
fs_signup/FS-SignupClient/SignupClient.pm
fs_signup/FS-SignupClient/cgi/cvv2.html [new file with mode: 0644]
fs_signup/FS-SignupClient/cgi/cvv2.png [new file with mode: 0644]
fs_signup/FS-SignupClient/cgi/cvv2_amex.png [new file with mode: 0644]
fs_signup/FS-SignupClient/cgi/signup-agentselect.html [new file with mode: 0755]
fs_signup/FS-SignupClient/cgi/signup-snarf.html [new file with mode: 0755]
fs_signup/FS-SignupClient/cgi/signup.cgi
fs_signup/FS-SignupClient/cgi/signup.html
fs_signup/fs_signup_server [deleted file]
htetc/global.asa
htetc/handler.pl
htetc/handler.pl-1.0x [deleted file]
httemplate/autohandler [new file with mode: 0644]
httemplate/browse/agent.cgi
httemplate/browse/agent_type.cgi
httemplate/browse/cust_main_county.cgi
httemplate/browse/cust_pay_batch.cgi
httemplate/browse/part_export.cgi
httemplate/browse/part_pkg.cgi
httemplate/browse/part_referral.cgi
httemplate/browse/part_svc.cgi
httemplate/browse/router.cgi
httemplate/browse/svc_acct_pop.cgi
httemplate/docs/billing.html
httemplate/docs/cvv2.html [new file with mode: 0644]
httemplate/docs/ieak.html [new file with mode: 0644]
httemplate/docs/index.html
httemplate/docs/install.html
httemplate/docs/legacy.html
httemplate/docs/schema.html
httemplate/docs/signup.html
httemplate/docs/upgrade-1.4.2.html [new file with mode: 0644]
httemplate/docs/upgrade10.html
httemplate/docs/upgrade9.html
httemplate/edit/REAL_cust_pkg.cgi
httemplate/edit/agent.cgi
httemplate/edit/cust_main.cgi
httemplate/edit/cust_main_county.cgi
httemplate/edit/part_bill_event.cgi
httemplate/edit/part_export.cgi
httemplate/edit/part_pkg.cgi
httemplate/edit/part_svc.cgi
httemplate/edit/process/cust_main.cgi
httemplate/edit/process/cust_main_county-collapse.cgi
httemplate/edit/process/cust_main_county.cgi
httemplate/edit/process/part_pkg.cgi
httemplate/edit/process/router.cgi
httemplate/edit/process/svc_external.cgi [new file with mode: 0755]
httemplate/edit/router.cgi
httemplate/edit/svc_acct.cgi
httemplate/edit/svc_broadband.cgi
httemplate/edit/svc_external.cgi [new file with mode: 0644]
httemplate/edit/svc_forward.cgi
httemplate/edit/svc_www.cgi
httemplate/elements/calendar-en.js [new file with mode: 0644]
httemplate/elements/calendar-setup.js [new file with mode: 0644]
httemplate/elements/calendar-win2k-2.css [new file with mode: 0644]
httemplate/elements/calendar.js [new file with mode: 0644]
httemplate/elements/calendar_stripped.js [new file with mode: 0644]
httemplate/elements/header.html [new file with mode: 0644]
httemplate/elements/menubar.html [new file with mode: 0644]
httemplate/elements/pager.html [new file with mode: 0644]
httemplate/elements/table.html [new file with mode: 0644]
httemplate/graph/money_time-graph.cgi
httemplate/graph/money_time.cgi
httemplate/images/calendar.png [new file with mode: 0644]
httemplate/images/cvv2.png [new file with mode: 0644]
httemplate/images/cvv2_amex.png [new file with mode: 0644]
httemplate/index.html
httemplate/misc/cust_main-cancel.cgi
httemplate/misc/delete-cust_credit.cgi [new file with mode: 0755]
httemplate/misc/download-batch.cgi [new file with mode: 0644]
httemplate/misc/dump.cgi [new file with mode: 0644]
httemplate/misc/email-invoice.cgi [new file with mode: 0755]
httemplate/misc/print-invoice.cgi
httemplate/misc/process/meta-import.cgi
httemplate/misc/upload-batch.cgi [new file with mode: 0644]
httemplate/search/cust_bill_event.cgi
httemplate/search/cust_main-quickpay.html
httemplate/search/cust_main.cgi
httemplate/search/cust_pay.cgi
httemplate/search/cust_pkg.cgi
httemplate/search/cust_pkg.html [deleted file]
httemplate/search/cust_pkg_report.cgi [new file with mode: 0755]
httemplate/search/elements/search.html [new file with mode: 0644]
httemplate/search/report_cc.html
httemplate/search/report_credit.html
httemplate/search/report_cust_pay.html
httemplate/search/report_prepaid_income.cgi [new file with mode: 0644]
httemplate/search/report_prepaid_income.html [new file with mode: 0644]
httemplate/search/report_receivables.cgi
httemplate/search/report_tax.html
httemplate/search/sql.cgi [deleted file]
httemplate/search/sql.html [new file with mode: 0644]
httemplate/search/svc_acct.cgi
httemplate/search/svc_domain.cgi
httemplate/search/svc_forward.cgi [new file with mode: 0755]
httemplate/view/cust_bill-pdf.cgi [new file with mode: 0755]
httemplate/view/cust_bill-ps.cgi [new file with mode: 0755]
httemplate/view/cust_bill.cgi
httemplate/view/cust_main.cgi
httemplate/view/svc_acct.cgi
httemplate/view/svc_broadband.cgi
httemplate/view/svc_external.cgi [new file with mode: 0644]
httemplate/view/svc_forward.cgi
init.d/freeside-init
install/debian/3.0/INSTALL [new file with mode: 0644]
install/freebsd/ports
install/openbsd/INSTALL [new file with mode: 0644]
install/openbsd/cpan [new file with mode: 0644]
install/openbsd/ports [new file with mode: 0644]
install/redhat/7.3/INSTALL
install/redhat/9/INSTALL [new file with mode: 0644]
install/redhat/9/sources.list [new file with mode: 0644]
rt/README [deleted file]

diff --git a/CREDITS b/CREDITS
index 0b4e2d9..592be44 100644 (file)
--- a/CREDITS
+++ b/CREDITS
@@ -113,5 +113,12 @@ Infostreet export.
 Richard Siddall <richard.siddall@elirion.net> sent in Mason fixes and other
 things I'm probably forgetting.
 
+Contains "JS Calendar" v0.9.3 <http://dynarch.com/mishoo/calendar.epl>
+by Mihai Bazon <mishoo@infoiasi.ro> licensed under the terms of the GNU LGPL.
+
+Latex invoice template based on a template from eBills
+<http://ebills.sourceforge.net/> by Mark Asplen-Taylor <mark@asplen.co.uk>,
+licensed under the terms fo the GNU GPL.
+
 Everything else is my (Ivan Kohler <ivan@420.am>) fault.
 
index e4a3208..36c3a17 100644 (file)
--- a/FS/FS.pm
+++ b/FS/FS.pm
@@ -54,6 +54,8 @@ L<FS::svc_Common> - Service base class
 
 L<FS::svc_acct> - Account (shell, RADIUS, POP3) class
 
+L<FS::acct_snarf> - External mail account class
+
 L<FS::radius_usergroup> - RADIUS groups
 
 L<FS::svc_domain> - Domain class
@@ -64,6 +66,10 @@ L<FS::svc_forward> - Mail forwarding class
 
 L<FS::svc_www> - Web virtual host class.
 
+L<FS::svc_broadband> - DSL, wireless and other broadband class.
+
+L<FS::svc_external> - Externally tracked service class.
+
 L<FS::part_svc> - Service definition class
 
 L<FS::part_svc_column> - Column constraint class
index 86d20f6..905189e 100644 (file)
@@ -44,8 +44,10 @@ Returns an HTML header.
 =cut
 
 sub header {
+  use Carp;
+  carp 'FS::CGI::header deprecated; include /elements/header.html instead';
+
   my($title,$menubar,$etc)=@_; #$etc is for things like onLoad= etc.
-  #use Carp;
   $etc = '' unless defined $etc;
 
   my $x =  <<END;
@@ -107,6 +109,9 @@ Returns an HTML menubar.
 =cut
 
 sub menubar { #$menubar=menubar('Main Menu', '../', 'Item', 'url', ... );
+  use Carp;
+  carp 'FS::CGI::menubar deprecated; include /elements/menubar.html instead';
+
   my($item,$url,@html);
   while (@_) {
     ($item,$url)=splice(@_,0,2);
@@ -209,7 +214,9 @@ Returns current URL with LEVEL levels of path removed from the end (default 0).
 sub popurl {
   my($up)=@_;
   my $cgi = &FS::UID::cgi;
-  my $url = new URI::URL ( $cgi->isa('Apache') ? $cgi->uri : $cgi->url );
+  my $url_string = $cgi->isa('Apache') ? $cgi->uri : $cgi->url;
+  $url_string =~ s/\?.*//;
+  my $url = new URI::URL ( $url_string );
   my(@path)=$url->path_components;
   splice @path, 0-$up;
   $url->path_components(@path);
@@ -225,6 +232,9 @@ Returns HTML tag for beginning a table.
 =cut
 
 sub table {
+  use Carp;
+  carp 'FS::CGI::table deprecated; include /elements/table.html instead';
+
   my $col = shift;
   if ( $col ) {
     qq!<TABLE BGCOLOR="$col" BORDER=1 WIDTH="100%" CELLSPACING=0 CELLPADDING=2 BORDERCOLOR="#999999">!;
index e12e93b..445f0ec 100644 (file)
@@ -14,19 +14,35 @@ use FS::svc_domain;
 use FS::cust_main;
 use FS::cust_bill;
 use FS::cust_main_county;
+use FS::cust_pkg;
 
 use FS::ClientAPI; #hmm
 FS::ClientAPI->register_handlers(
   'MyAccount/login'            => \&login,
   'MyAccount/customer_info'    => \&customer_info,
+  'MyAccount/edit_info'        => \&edit_info,
   'MyAccount/invoice'          => \&invoice,
   'MyAccount/cancel'           => \&cancel,
   'MyAccount/payment_info'     => \&payment_info,
   'MyAccount/process_payment'  => \&process_payment,
+  'MyAccount/list_pkgs'        => \&list_pkgs,
+  'MyAccount/order_pkg'        => \&order_pkg,
+  'MyAccount/cancel_pkg'       => \&cancel_pkg,
+  'MyAccount/charge'           => \&charge,
+);
+
+use vars qw( @cust_main_editable_fields );
+@cust_main_editable_fields = qw(
+  first last company address1 address2 city
+    county state zip country daytime night fax
+  ship_first ship_last ship_company ship_address1 ship_address2 ship_city
+    ship_state ship_zip ship_country ship_daytime ship_night ship_fax
 );
 
 #store in db?
-my $cache = new Cache::SharedMemoryCache();
+my $cache = new Cache::SharedMemoryCache( {
+   'namespace' => 'FS::ClientAPI::MyAccount',
+} );
 
 #false laziness w/FS::ClientAPI::passwd::passwd (needs to handle encrypted pw)
 sub login {
@@ -100,6 +116,10 @@ sub customer_info {
 
     $return{name} = $cust_main->first. ' '. $cust_main->get('last');
 
+    for (@cust_main_editable_fields) {
+      $return{$_} = $cust_main->get($_);
+    }
+
   } else { #no customer record
 
     my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
@@ -115,6 +135,27 @@ sub customer_info {
 
 }
 
+sub edit_info {
+  my $p = shift;
+  my $session = $cache->get($p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my $custnum = $session->{'custnum'}
+    or return { 'error' => "no customer record" };
+
+  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+    or return { 'error' => "unknown custnum $custnum" };
+
+  my $new = new FS::cust_main { $cust_main->hash };
+  $new->set( $_ => $p->{$_} )
+    foreach grep { exists $p->{$_} } @cust_main_editable_fields;
+  my $error = $new->replace($cust_main);
+  return { 'error' => $error } if $error;
+  #$cust_main = $new;
+  
+  return { 'error' => '' };
+}
+
 sub payment_info {
   my $p = shift;
   my $session = $cache->get($p->{'session_id'})
@@ -203,7 +244,7 @@ sub process_payment {
   my $error = $cust_main->realtime_bop( 'CC', $p->{'amount'}, quiet=>1,
     'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01',
     map { $_ => $p->{$_} }
-      qw( payname address1 address2 city state zip payinfo )
+      qw( payname address1 address2 city state zip payinfo paybatch )
   );
   return { 'error' => $error } if $error;
 
@@ -245,7 +286,7 @@ sub cancel {
   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
     or return { 'error' => "unknown custnum $custnum" };
 
-  my @errors = $cust_main->cancel;
+  my @errors = $cust_main->cancel( 'quiet'=>1 );
 
   my $error = scalar(@errors) ? join(' / ', @errors) : '';
 
@@ -253,5 +294,117 @@ sub cancel {
 
 }
 
+sub list_pkgs {
+  my $p = shift;
+  my $session = $cache->get($p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my $custnum = $session->{'custnum'};
+
+  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+    or return { 'error' => "unknown custnum $custnum" };
+
+  return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] };
+
+}
+
+sub order_pkg {
+  my $p = shift;
+  my $session = $cache->get($p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my $custnum = $session->{'custnum'};
+
+  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+    or return { 'error' => "unknown custnum $custnum" };
+
+  #false laziness w/ClientAPI/Signup.pm
+
+  my $cust_pkg = new FS::cust_pkg ( {
+    'custnum' => $custnum,
+    'pkgpart' => $p->{'pkgpart'},
+  } );
+  my $error = $cust_pkg->check;
+  return { 'error' => $error } if $error;
+
+  my $svc_acct = new FS::svc_acct ( {
+    'svcpart'   => $p->{'svcpart'} || $cust_pkg->part_pkg->svcpart('svc_acct'),
+    map { $_ => $p->{$_} }
+      qw( username _password sec_phrase popnum ),
+  } );
+
+  my @acct_snarf;
+  my $snarfnum = 1;
+  while ( length($p->{"snarf_machine$snarfnum"}) ) {
+    my $acct_snarf = new FS::acct_snarf ( {
+      'machine'   => $p->{"snarf_machine$snarfnum"},
+      'protocol'  => $p->{"snarf_protocol$snarfnum"},
+      'username'  => $p->{"snarf_username$snarfnum"},
+      '_password' => $p->{"snarf_password$snarfnum"},
+    } );
+    $snarfnum++;
+    push @acct_snarf, $acct_snarf;
+  }
+  $svc_acct->child_objects( \@acct_snarf );
+
+  my $y = $svc_acct->setdefault; # arguably should be in new method
+  return { 'error' => $y } if $y && !ref($y);
+
+  $error = $svc_acct->check;
+  return { 'error' => $error } if $error;
+
+  use Tie::RefHash;
+  tie my %hash, 'Tie::RefHash';
+  %hash = ( $cust_pkg => [ $svc_acct ] );
+  #msgcat
+  $error = $cust_main->order_pkgs( \%hash, '', 'noexport' => 1 );
+  return { 'error' => $error } if $error;
+
+  my $conf = new FS::Conf;
+  if ( $conf->exists('signup_server-realtime') ) {
+
+    my $old_balance = $cust_main->balance;
+
+    my $bill_error = $cust_main->bill;
+    $cust_main->apply_payments;
+    $cust_main->apply_credits;
+    $bill_error = $cust_main->collect;
+
+    if ( $cust_main->balance > $old_balance ) {
+      $cust_pkg->cancel('quiet'=>1);
+      return { 'error' => '_decline' };
+    } else {
+      $cust_pkg->reexport;
+    }
+
+  } else {
+    $cust_pkg->reexport;
+  }
+
+  return { error => '' };
+
+}
+
+sub cancel_pkg {
+  my $p = shift;
+  my $session = $cache->get($p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my $custnum = $session->{'custnum'};
+
+  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+    or return { 'error' => "unknown custnum $custnum" };
+
+  my $pkgnum = $session->{'pkgnum'};
+
+  my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
+                                        'pkgnum'  => $pkgnum,   } )
+    or return { 'error' => "unknown pkgnum $pkgnum" };
+
+  my $error = $cust_main->cancel( 'quiet'=>1 );
+  return { 'error' => $error };
+
+}
+
 1;
 
diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm
new file mode 100644 (file)
index 0000000..375958b
--- /dev/null
@@ -0,0 +1,235 @@
+package FS::ClientAPI::Signup;
+
+use strict;
+use Tie::RefHash;
+use FS::Conf;
+use FS::Record qw(qsearch qsearchs dbdef);
+use FS::agent;
+use FS::cust_main_county;
+use FS::part_pkg;
+use FS::svc_acct_pop;
+use FS::cust_main;
+use FS::cust_pkg;
+use FS::svc_acct;
+use FS::acct_snarf;
+use FS::Msgcat qw(gettext);
+
+use FS::ClientAPI; #hmm
+FS::ClientAPI->register_handlers(
+  'Signup/signup_info'  => \&signup_info,
+  'Signup/new_customer' => \&new_customer,
+);
+
+sub signup_info {
+  #my $packet = shift;
+
+  my $conf = new FS::Conf;
+
+  use vars qw($signup_info); #cache for performance;
+  $signup_info ||= {
+
+    'cust_main_county' =>
+      [ map { $_->hashref } qsearch('cust_main_county', {}) ],
+
+    'agent' =>
+      [
+        map { $_->hashref }
+          qsearch('agent', dbdef->table('agent')->column('disabled')
+                             ? { 'disabled' => '' }
+                             : {}
+                 )
+      ],
+
+    'part_referral' =>
+      [
+        map { $_->hashref }
+          qsearch('part_referral',
+                    dbdef->table('part_referral')->column('disabled')
+                      ? { 'disabled' => '' }
+                      : {}
+                 )
+      ],
+
+    'agentnum2part_pkg' =>
+      {
+        map {
+          my $href = $_->pkgpart_hashref;
+          $_->agentnum =>
+            [
+              map { { 'payby' => [ $_->payby ], %{$_->hashref} } }
+                grep { $_->svcpart('svc_acct') && $href->{ $_->pkgpart } }
+                  qsearch( 'part_pkg', { 'disabled' => '' } )
+            ];
+        } qsearch('agent', dbdef->table('agent')->column('disabled')
+                             ? { 'disabled' => '' }
+                             : {}
+                 )
+      },
+
+    'svc_acct_pop' => [ map { $_->hashref } qsearch('svc_acct_pop',{} ) ],
+
+    'security_phrase' => $conf->exists('security_phrase'),
+
+    'payby' => [ $conf->config('signup_server-payby') ],
+
+    'cvv_enabled' => defined dbdef->table('cust_main')->column('paycvv'),
+
+    'msgcat' => { map { $_=>gettext($_) } qw(
+      passwords_dont_match invalid_card unknown_card_type not_a
+    ) },
+
+    'statedefault' => $conf->config('statedefault') || 'CA',
+
+    'countrydefault' => $conf->config('countrydefault') || 'US',
+
+    'refnum' => $conf->config('signup_server-default_refnum'),
+
+  };
+
+  if (
+    $conf->config('signup_server-default_agentnum')
+    && !exists $signup_info->{'part_pkg'} #cache for performance
+  ) {
+    my $agentnum = $conf->config('signup_server-default_agentnum');
+    my $agent = qsearchs( 'agent', { 'agentnum' => $agentnum } )
+      or die "fatal: signup_server-default_agentnum $agentnum not found\n";
+    my $pkgpart_href = $agent->pkgpart_hashref;
+
+    $signup_info->{'part_pkg'} = [
+      #map { $_->hashref }
+      map { { 'payby' => [ $_->payby ], %{$_->hashref} } }
+        grep { $_->svcpart('svc_acct') && $pkgpart_href->{ $_->pkgpart } }
+          qsearch( 'part_pkg', { 'disabled' => '' } )
+    ];
+  }
+
+  $signup_info;
+
+}
+
+sub new_customer {
+  my $packet = shift;
+
+  my $conf = new FS::Conf;
+  
+  #things that aren't necessary in base class, but are for signup server
+    #return "Passwords don't match"
+    #  if $hashref->{'_password'} ne $hashref->{'_password2'}
+  return { 'error' => gettext('empty_password') }
+    unless $packet->{'_password'};
+  # a bit inefficient for large numbers of pops
+  return { 'error' => gettext('no_access_number_selected') }
+    unless $packet->{'popnum'} || !scalar(qsearch('svc_acct_pop',{} ));
+
+  #shares some stuff with htdocs/edit/process/cust_main.cgi... take any
+  # common that are still here and library them.
+  my $cust_main = new FS::cust_main ( {
+    #'custnum'          => '',
+    'agentnum'      => $packet->{agentnum}
+                       || $conf->config('signup_server-default_agentnum'),
+    'refnum'        => $packet->{refnum}
+                       || $conf->config('signup_server-default_refnum'),
+
+    map { $_ => $packet->{$_} } qw(
+      last first ss company address1 address2 city county state zip country
+      daytime night fax payby payinfo paycvv paydate payname referral_custnum
+      comments
+    ),
+
+  } );
+
+  return { 'error' => "Illegal payment type" }
+    unless grep { $_ eq $packet->{'payby'} }
+                $conf->config('signup_server-payby');
+
+  $cust_main->payinfo($cust_main->daytime)
+    if $cust_main->payby eq 'LECB' && ! $cust_main->payinfo;
+
+  my @invoicing_list = split( /\s*\,\s*/, $packet->{'invoicing_list'} );
+
+  $packet->{'pkgpart'} =~ /^(\d+)$/ or '' =~ /^()$/;
+  my $pkgpart = $1;
+  return { 'error' => 'Please select a package' } unless $pkgpart; #msgcat
+
+  my $part_pkg =
+    qsearchs( 'part_pkg', { 'pkgpart' => $pkgpart } )
+      or return { 'error' => "WARNING: unknown pkgpart: $pkgpart" };
+  my $svcpart = $part_pkg->svcpart('svc_acct');
+
+  my $cust_pkg = new FS::cust_pkg ( {
+    #later#'custnum' => $custnum,
+    'pkgpart' => $packet->{'pkgpart'},
+  } );
+  my $error = $cust_pkg->check;
+  return { 'error' => $error } if $error;
+
+  my $svc_acct = new FS::svc_acct ( {
+    'svcpart'   => $svcpart,
+    map { $_ => $packet->{$_} }
+      qw( username _password sec_phrase popnum ),
+  } );
+
+  my @acct_snarf;
+  my $snarfnum = 1;
+  while ( length($packet->{"snarf_machine$snarfnum"}) ) {
+    my $acct_snarf = new FS::acct_snarf ( {
+      'machine'   => $packet->{"snarf_machine$snarfnum"},
+      'protocol'  => $packet->{"snarf_protocol$snarfnum"},
+      'username'  => $packet->{"snarf_username$snarfnum"},
+      '_password' => $packet->{"snarf_password$snarfnum"},
+    } );
+    $snarfnum++;
+    push @acct_snarf, $acct_snarf;
+  }
+  $svc_acct->child_objects( \@acct_snarf );
+
+  my $y = $svc_acct->setdefault; # arguably should be in new method
+  return { 'error' => $y } if $y && !ref($y);
+
+  $error = $svc_acct->check;
+  return { 'error' => $error } if $error;
+
+  use Tie::RefHash;
+  tie my %hash, 'Tie::RefHash';
+  %hash = ( $cust_pkg => [ $svc_acct ] );
+  #msgcat
+  $error = $cust_main->insert( \%hash, \@invoicing_list, 'noexport' => 1 );
+  return { 'error' => $error } if $error;
+
+  if ( $conf->exists('signup_server-realtime') ) {
+
+    #warn "[fs_signup_server] Billing customer...\n" if $Debug;
+
+    my $bill_error = $cust_main->bill;
+    #warn "[fs_signup_server] error billing new customer: $bill_error"
+    #  if $bill_error;
+
+    $cust_main->apply_payments;
+    $cust_main->apply_credits;
+
+    $bill_error = $cust_main->collect;
+    #warn "[fs_signup_server] error collecting from new customer: $bill_error"
+    #  if $bill_error;
+
+    if ( $cust_main->balance > 0 ) {
+
+      #this makes sense.  credit is "un-doing" the invoice
+      $cust_main->credit( $cust_main->balance, 'signup server decline' );
+      $cust_main->apply_credits;
+
+      #should check list for errors...
+      #$cust_main->suspend;
+      local $FS::svc_Common::noexport_hack = 1;
+      $cust_main->cancel('quiet'=>1);
+
+      return { 'error' => '_decline' };
+    }
+
+  }
+  $cust_main->reexport;
+
+  return { error => '' };
+
+}
+
+1;
index d185f8d..99eee18 100644 (file)
@@ -174,7 +174,7 @@ sub config_items {
   my $self = shift; 
   #quelle kludge
   @config_items,
-  map { 
+  map { 
         my $basename = basename($_);
         $basename =~ /^(.*)$/;
         $basename = $1;
@@ -185,7 +185,19 @@ sub config_items {
                            'type'        => 'textarea',
                          }
       } glob($self->dir. '/invoice_template_*')
-  ;
+  ),
+  ( map { 
+        my $basename = basename($_);
+        $basename =~ /^(.*)$/;
+        $basename = $1;
+        new FS::ConfItem {
+                           'key'         => $basename,
+                           'section'     => 'billing',
+                           'description' => 'Alternate LaTeX template for invoices.  See the <a href="../docs/billing.html">billing documentation</a> for details.',
+                           'type'        => 'textarea',
+                         }
+      } glob($self->dir. '/invoice_latex_*')
+  );
 }
 
 =back
@@ -320,7 +332,14 @@ httemplate/docs/config.html
   {
     'key'         => 'deletepayments',
     'section'     => 'UI',
-    'description' => 'Enable deletion of unclosed payments.  Be very careful!  Only delete payments that were data-entry errors, not adjustments. Optionally specify one or more comma-separated email addresses to be notified when a payment is deleted.',
+    'description' => 'Enable deletion of unclosed payments.  Be very careful!  Only delete payments that were data-entry errors, not adjustments.  Optionally specify one or more comma-separated email addresses to be notified when a payment is deleted.',
+    'type'        => [qw( checkbox text )],
+  },
+
+  {
+    'key'         => 'deletecredits',
+    'section'     => 'UI',
+    'description' => 'Enable deletion of unclosed credits.  Be very careful!  Only delete credits that were data-entry errors, not adjustments.  Optionally specify one or more comma-separated email addresses to be notified when a credit is deleted.',
     'type'        => [qw( checkbox text )],
   },
 
@@ -451,6 +470,49 @@ httemplate/docs/config.html
   },
 
   {
+    'key'         => 'invoice_latex',
+    'section'     => 'billing',
+    'description' => 'Optional LaTeX template for typeset PostScript invoices.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'invoice_latexnotes',
+    'section'     => 'billing',
+    'description' => 'Notes section for LaTeX typeset PostScript invoices.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'invoice_latexfooter',
+    'section'     => 'billing',
+    'description' => 'Footer for LaTeX typeset PostScript invoices.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'invoice_latexsmallfooter',
+    'section'     => 'billing',
+    'description' => 'Optional small footer for multi-page LaTeX typeset PostScript invoices.',
+    'type'        => 'textarea',
+  },
+
+  { 
+    'key'         => 'invoice_default_terms',
+    'section'     => 'billing',
+    'description' => 'Optional default invoice term, used to calculate a due date printed on invoices.',
+    'type'        => 'select',
+    'select_enum' => [ '', 'Payable upon receipt', 'Net 0', 'Net 10', 'Net 15', 'Net 30', 'Net 45', 'Net 60' ],
+  },
+
+  {
+    'key'         => 'invoice_send_receipts',
+    'section'     => 'billing',
+    'description' => 'Send receipts for payments and credits.',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'lpr',
     'section'     => 'required',
     'description' => 'Print command for paper invoices, for example `lpr -h\'',
@@ -760,7 +822,7 @@ httemplate/docs/config.html
   {
     'key'         => 'username-ampersand',
     'section'     => 'username',
-    'description' => 'Allow the ampersand character (&amp;) in usernames.  Be careful when using this option in conjunction with <a href="#shellmachine-useradd">shellmachine-useradd</a> and other configuration options which execute shell commands, as the ampersand will be interpreted by the shell if not quoted.',
+    'description' => 'Allow the ampersand character (&amp;) in usernames.  Be careful when using this option in conjunction with <a href="../browse/part_export.cgi">exports</a> which execute shell commands, as the ampersand will be interpreted by the shell if not quoted.',
     'type'        => 'checkbox',
   },
 
@@ -888,15 +950,15 @@ httemplate/docs/config.html
 
   {
     'key'         => 'selfservice_server-quiet',
-    'section'     => '',
-    'description' => 'Disable decline and cancel emails generated by transactions initiated by the selfservice server. Not recommended, unless the customer will get instant feedback from a customer service UI, and receiving an email would be confusing/overkill.',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, the self-service server no longer sends superfluous decline and cancel emails.  Used to disable decline and cancel emails generated by transactions initiated by the selfservice server.',
     'type'        => 'checkbox',
   },
 
   {
     'key'         => 'signup_server-quiet',
-    'section'     => '',
-    'description' => 'Disable decline and cancel emails generated by transactions initiated by the signup server. Not recommended, unless the customer will get instant feedback from a customer service UI, and receiving an email would be confusing/overkill. Does not disable welcome emails.',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, the signup server is now part of the self-service server and no longer sends superfluous decline and cancel emails.  Used to disable decline and cancel emails generated by transactions initiated by the signup server.  Does not disable welcome emails.',
     'type'        => 'checkbox',
   },
 
@@ -910,11 +972,24 @@ httemplate/docs/config.html
 
   {
     'key'         => 'signup_server-email',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, this feature is no longer available.  See the ***fill me in*** report instead.  Used to contain a comma-separated list of email addresses to receive notification of signups via the signup server.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'signup_server-default_agentnum',
     'section'     => '',
-    'description' => 'Comma-separated list of email addresses to receive notification of signups via the signup server.',
+    'description' => 'Default agentnum for the signup server',
     'type'        => 'text',
   },
 
+  {
+    'key'         => 'signup_server-default_refnum',
+    'section'     => '',
+    'description' => 'Default advertising source number for the signup server',
+    'type'        => 'text',
+  },
 
   {
     'key'         => 'show-msgcat-codes',
@@ -945,6 +1020,13 @@ httemplate/docs/config.html
   },
 
   {
+    'key'         => 'emaildecline-exclude',
+    'section'     => 'billing',
+    'description' => 'List of error messages that should not trigger email decline notices, one per line.',
+    'type'        => 'textarea',
+  },
+
+  {
     'key'         => 'cancelmessage',
     'section'     => 'billing',
     'description' => 'Template file for cancellation emails.',
@@ -1053,6 +1135,42 @@ httemplate/docs/config.html
     'type'        => 'text',
   },
 
+  {
+    'key'         => 'users-allow_comp',
+    'section'     => '',
+    'description' => 'Usernames (Freeside users, created with <a href="../docs/man/bin/freeside-adduser.html">freeside-adduser</a>) which can create complimentary customers, one per line.  If no usernames are entered, all users can create complimentary accounts.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'cvv-save',
+    'section'     => 'billing',
+    'description' => 'Save CVV2 information after the initial transaction for the selected credit card types.  Enabling this option may be in violation of your merchant agreement(s), so please check them carefully before enabling this option for any credit card types.',
+    'type'        => 'selectmultiple',
+    'select_enum' => [ "VISA card",
+                       "MasterCard",
+                       "Discover card",
+                       "American Express card",
+                       "Diner's Club/Carte Blanche",
+                       "enRoute",
+                       "JCB",
+                       "BankCard",
+                     ],
+  },
+
+  {
+    'key'         => 'allow_negative_charges',
+    'section'     => 'billing',
+    'description' => 'Allow negative charges.  Normally not used unless importing data from a legacy system that requires this.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'system_usernames',
+    'section'     => 'username',
+    'description' => 'A list of system usernames that cannot be edited or removed, one per line.  Use a bare username to prohibit modification/deletion of the username in any domain, or username@domain to prohibit modification/deletetion of a specific username and domain.',
+    'type'        => 'textarea',
+  },
 );
 
 1;
index 9d82d94..801b89d 100644 (file)
@@ -2,14 +2,14 @@ package FS::Record;
 
 use strict;
 use vars qw( $dbdef_file $dbdef $setup_hack $AUTOLOAD @ISA @EXPORT_OK $DEBUG
-             $me %dbdef_cache );
+             $me %dbdef_cache %virtual_fields_cache );
 use subs qw(reload_dbdef);
 use Exporter;
 use Carp qw(carp cluck croak confess);
 use File::CounterFile;
 use Locale::Country;
 use DBI qw(:sql_types);
-use DBIx::DBSchema 0.21;
+use DBIx::DBSchema 0.23;
 use FS::UID qw(dbh getotaker datasrc driver_name);
 use FS::SearchCache;
 use FS::Msgcat qw(gettext);
@@ -21,7 +21,7 @@ use Tie::IxHash;
 @ISA = qw(Exporter);
 @EXPORT_OK = qw(dbh fields hfields qsearch qsearchs dbdef jsearch);
 
-$DEBUG = 2;
+$DEBUG = 0;
 $me = '[FS::Record]';
 
 #ask FS::UID to run this stuff for us later
@@ -230,7 +230,8 @@ sub qsearch {
       if ( ! defined( $record->{$_} ) || $record->{$_} eq '' ) {
         if ( $op eq '=' ) {
           if ( driver_name eq 'Pg' ) {
-            if ( $dbdef->table($table)->column($column)->type =~ /(int)/i ) {
+            my $type = $dbdef->table($table)->column($column)->type;
+            if ( $type =~ /(int|serial)/i ) {
               qq-( $column IS NULL )-;
             } else {
               qq-( $column IS NULL OR $column = '' )-;
@@ -240,7 +241,8 @@ sub qsearch {
           }
         } elsif ( $op eq '!=' ) {
           if ( driver_name eq 'Pg' ) {
-            if ( $dbdef->table($table)->column($column)->type =~ /(int)/i ) {
+            my $type = $dbdef->table($table)->column($column)->type;
+            if ( $type =~ /(int|serial)/i ) {
               qq-( $column IS NOT NULL )-;
             } else {
               qq-( $column IS NOT NULL AND $column != '' )-;
@@ -309,7 +311,7 @@ sub qsearch {
     grep defined( $record->{$_} ) && $record->{$_} ne '', @real_fields
   ) {
     if ( $record->{$field} =~ /^\d+(\.\d+)?$/
-         && $dbdef->table($table)->column($field)->type =~ /(int)/i
+         && $dbdef->table($table)->column($field)->type =~ /(int|serial)/i
     ) {
       $sth->bind_param($bind++, $record->{$field}, { TYPE => SQL_INTEGER } );
     } else {
@@ -664,7 +666,7 @@ sub insert {
   if (@virtual_fields) {
     my %v_values = map { $_, $self->getfield($_) } @virtual_fields;
 
-    my $vfieldpart = vfieldpart_hashref($table);
+    my $vfieldpart = $self->vfieldpart_hashref;
 
     my $v_statement = "INSERT INTO virtual_field(recnum, vfieldpart, value) ".
                     "VALUES (?, ?, ?)";
@@ -753,7 +755,7 @@ sub delete {
   my $primary_key = $self->dbdef_table->primary_key;
   my $v_sth;
   my @del_vfields;
-  my $vfp = vfieldpart_hashref($self->table);
+  my $vfp = $self->vfieldpart_hashref;
   foreach($self->virtual_fields) {
     next if $self->getfield($_) eq '';
     unless(@del_vfields) {
@@ -804,7 +806,24 @@ returns the error, otherwise returns false.
 =cut
 
 sub replace {
-  my ( $new, $old ) = ( shift, shift );
+  my $new = shift;
+
+  my $old;
+  if ( @_ ) { 
+    $old = shift;
+  } else {
+    warn "[debug]$me replace called with no arguments; autoloading old record\n"
+     if $DEBUG;
+    my $primary_key = $new->dbdef_table->primary_key;
+    if ( $primary_key ) {
+      $old = qsearchs($new->table, { $primary_key => $new->$primary_key() } )
+        or croak "can't find ". $new->table. ".$primary_key ".
+                 $new->$primary_key();
+    } else {
+      croak $new->table. " has no primary key; pass old record as argument";
+    }
+  }
+
   warn "[debug]$me $new ->replace $old\n" if $DEBUG;
 
   return "Records not in same table!" unless $new->table eq $old->table;
@@ -836,7 +855,7 @@ sub replace {
         $old->getfield($_) eq ''
           #? "( $_ IS NULL OR $_ = \"\" )"
           ? ( driver_name eq 'Pg'
-                ? "$_ IS NULL"
+                ? "( $_ IS NULL OR $_ = '' )"
                 : "( $_ IS NULL OR $_ = \"\" )"
             )
           : "$_ = ". _quote($old->getfield($_),$old->table,$_)
@@ -870,7 +889,7 @@ sub replace {
   my $v_rep_sth;
   my $v_del_sth;
   my (@add_vfields, @rep_vfields, @del_vfields);
-  my $vfp = vfieldpart_hashref($old->table);
+  my $vfp = $old->vfieldpart_hashref;
   foreach(grep { exists($diff{$_}) } $new->virtual_fields) {
     if($diff{$_} eq '') {
       # Delete
@@ -1051,6 +1070,21 @@ sub ut_float {
   '';
 }
 
+=item ut_snumber COLUMN
+
+Check/untaint signed numeric data (whole numbers).  May not be null.  If there
+is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub ut_snumber {
+  my($self, $field) = @_;
+  $self->getfield($field) =~ /^(-?)\s*(\d+)$/
+    or return "Illegal or empty (numeric) $field: ". $self->getfield($field);
+  $self->setfield($field, "$1$2");
+  '';
+}
+
 =item ut_number COLUMN
 
 Check/untaint simple numeric data (whole numbers).  May not be null.  If there
@@ -1369,20 +1403,25 @@ be exported, and should only be called as an instance or class method.
 =cut
 
 sub virtual_fields {
-  my $something = shift;
+  my $self = shift;
   my $table;
-  $table = $something->table or confess "virtual_fields called on non-table";
+  $table = $self->table or confess "virtual_fields called on non-table";
 
   confess "Unknown table $table" unless $dbdef->table($table);
 
-  # This should be smart enough to cache results.
+  return () unless $self->dbdef->table('part_virtual_field');
+
+  unless ( $virtual_fields_cache{$table} ) {
+    my $query = 'SELECT name from part_virtual_field ' .
+                "WHERE dbtable = '$table'";
+    my $dbh = dbh;
+    my $result = $dbh->selectcol_arrayref($query);
+    confess $dbh->errstr if $dbh->err;
+    $virtual_fields_cache{$table} = $result;
+  }
+
+  @{$virtual_fields_cache{$table}};
 
-  my $query = 'SELECT name from part_virtual_field ' .
-              "WHERE dbtable = '$table'";
-  my $dbh = dbh;
-  my $result = $dbh->selectcol_arrayref($query);
-  confess $dbh->errstr if $dbh->err;
-  return @$result;
 }
 
 
@@ -1509,9 +1548,11 @@ TABLE.
 =cut
 
 sub vfieldpart_hashref {
-  my ($table) = @_;
+  my $self = shift;
+  my $table = $self->table;
+
+  return {} unless $self->dbdef->table('part_virtual_field');
 
-  return () unless $table;
   my $dbh = dbh;
   my $statement = "SELECT vfieldpart, name FROM part_virtual_field WHERE ".
                   "dbtable = '$table'";
index f670051..8271f89 100644 (file)
@@ -87,7 +87,7 @@ sub forksuidsetup {
   getsecrets;
   $dbh = DBI->connect($datasrc,$db_user,$db_pass, {
                           'AutoCommit' => 0,
-                          #'ChopBlanks' => 1,
+                          'ChopBlanks' => 1,
   } ) or die "DBI->connect error: $DBI::errstr\n";
 
   foreach ( keys %callback ) {
diff --git a/FS/FS/acct_snarf.pm b/FS/FS/acct_snarf.pm
new file mode 100644 (file)
index 0000000..b4e88bf
--- /dev/null
@@ -0,0 +1,128 @@
+package FS::acct_snarf;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::acct_snarf - Object methods for acct_snarf records
+
+=head1 SYNOPSIS
+
+  use FS::acct_snarf;
+
+  $record = new FS::acct_snarf \%hash;
+  $record = new FS::acct_snarf { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::svc_acct object represents an external mail account, typically for
+download of mail.  FS::acct_snarf inherits from FS::Record.  The following
+fields are currently supported:
+
+=over 4
+
+=item snarfnum - primary key
+
+=item svcnum - Account (see L<FS::svc_acct>)
+
+=item machine - external machine to download mail from
+
+=item protocol - protocol (pop3, imap, etc.)
+
+=item username - external login username
+
+=item _password - external login password
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'acct_snarf'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid external mail account.  If
+there is an error, returns the error, otherwise returns false.  Called by the
+insert and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+  my $error =
+       $self->ut_numbern('snarfnum')
+    || $self->ut_number('svcnum')
+    || $self->ut_foreign_key('svcnum', 'svc_acct', 'svcnum')
+    || $self->ut_domain('machine')
+    || $self->ut_alphan('protocol')
+    || $self->ut_textn('username')
+  ;
+  return $error if $error;
+
+  $self->_password =~ /^[^\t\n]*$/ or return "illegal password";
+  $self->_password($1);
+
+  ''; #no error
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 4b034ef..1fb6060 100755 (executable)
@@ -176,11 +176,12 @@ sub next_free_addr {
   my $conf = new FS::Conf;
   my @excludeaddr = $conf->config('exclude_ip_addr');
   
-  my @used = (
-    map { $_->NetAddr->addr } 
-      ($self, 
-       qsearch('svc_broadband', { blocknum => $self->blocknum }) ),
-     @excludeaddr );
+my @used =
+( (map { $_->NetAddr->addr }
+    ($self,
+     qsearch('svc_broadband', { blocknum => $self->blocknum }))
+  ), @excludeaddr
+);
 
   my @free = $self->NetAddr->hostenum;
   while (my $ip = shift @free) {
index 6de15ae..2f70d65 100644 (file)
@@ -50,6 +50,12 @@ from FS::Record.  The following fields are currently supported:
 
 =item freq - For future use.
 
+=item disabled - Disabled flag, empty or 'Y'
+
+=item username - Username for the Agent interface
+
+=item _password - Password for the Agent interface
+
 =back
 
 =head1 METHODS
@@ -110,6 +116,24 @@ sub check {
   ;
   return $error if $error;
 
+  if ( $self->dbdef_table->column('disabled') ) {
+    $error = $self->ut_enum('disabled', [ '', 'Y' ] );
+    return $error if $error;
+  }
+
+  if ( $self->dbdef_table->column('username') ) {
+    $error = $self->ut_alphan('username');
+    return $error if $error;
+    if ( length($self->username) ) {
+      my $conflict = qsearchs('agent', { 'username' => $self->username } );
+      return 'duplicate agent username (with '. $conflict->agent. ')'
+        if $conflict;
+      $error = $self->ut_text('password'); # ut_text... arbitrary choice
+    } else {
+      $self->_password('');
+    }
+  }
+
   return "Unknown typenum!"
     unless $self->agent_type;
 
@@ -144,7 +168,7 @@ sub pkgpart_hashref {
 
 =head1 VERSION
 
-$Id: agent.pm,v 1.4 2003-08-05 00:20:40 khoff Exp $
+$Id: agent.pm,v 1.6 2003-09-30 15:01:46 ivan Exp $
 
 =head1 BUGS
 
index 4793608..a3e7662 100644 (file)
@@ -343,6 +343,10 @@ sub send {
 
   }
 
+  if ( $conf->config('invoice_latex') ) {
+    @print_text = $self->print_ps('', $template);
+  }
+
   if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
     my $lpr = $conf->config('lpr');
     open(LPR, "|$lpr")
@@ -590,7 +594,10 @@ sub realtime_bop {
   my( $self, $method ) = @_;
 
   my $cust_main = $self->cust_main;
-  my $amount = $self->owed;
+  my $balance = $cust_main->balance;
+  my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
+  $amount = sprintf("%.2f", $amount);
+  return "not run (balance $balance)" unless $amount > 0;
 
   my $description = 'Internet Services';
   if ( $conf->exists('business-onlinepayment-description') ) {
@@ -636,7 +643,6 @@ sub batch_card {
     'state'    => $cust_main->getfield('state'),
     'zip'      => $cust_main->getfield('zip'),
     'country'  => $cust_main->getfield('country'),
-    'trancode' => 77,
     'cardnum'  => $cust_main->getfield('payinfo'),
     'exp'      => $cust_main->getfield('paydate'),
     'payname'  => $cust_main->getfield('payname'),
@@ -648,7 +654,7 @@ sub batch_card {
   '';
 }
 
-=item print_text [TIME];
+=item print_text [ TIME [ , TEMPLATE ] ]
 
 Returns an text invoice, as a list of lines.
 
@@ -705,7 +711,9 @@ sub print_text {
       my $pkg = $part_pkg->pkg;
 
       if ( $cust_bill_pkg->setup != 0 ) {
-        push @buf, [ "$pkg Setup",
+        my $description = $pkg;
+        $description .= ' Setup' if $cust_bill_pkg->recur != 0;
+        push @buf, [ $description,
                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
         push @buf,
           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
@@ -783,8 +791,10 @@ sub print_text {
   }
 
   #balance due
+  my $balance_due_msg = $self->balance_due_msg;
+
   push @buf,['','-----------'];
-  push @buf,['Balance Due', $money_char. 
+  push @buf,[$balance_due_msg, $money_char. 
     sprintf("%10.2f", $balance_due ) ];
 
   #create the template
@@ -878,6 +888,505 @@ sub print_text {
 
 }
 
+=item print_latex [ TIME [ , TEMPLATE ] ]
+
+Internal method - returns a filename of a filled-in LaTeX template for this
+invoice (Note: add ".tex" to get the actual filename).
+
+See print_ps and print_pdf for methods that return PostScript and PDF output.
+
+TIME an optional value used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+#still some false laziness w/print_text
+sub print_latex {
+
+  my( $self, $today, $template ) = @_;
+  $today ||= time;
+
+#  my $invnum = $self->invnum;
+  my $cust_main = $self->cust_main;
+  $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
+    unless $cust_main->payname && $cust_main->payby ne 'CHEK';
+
+  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
+#  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
+  #my $balance_due = $self->owed + $pr_total - $cr_total;
+  my $balance_due = $self->owed + $pr_total;
+
+  #my @collect = ();
+  #my($description,$amount);
+  @buf = ();
+
+  #create the template
+  my $templatefile = 'invoice_latex';
+  $templatefile .= "_$template" if $template;
+  my @invoice_template = $conf->config($templatefile)
+    or die "cannot load config file $templatefile";
+
+  my %invoice_data = (
+    'invnum'       => $self->invnum,
+    'date'         => time2str('%b %o, %Y', $self->_date),
+    'agent'        => _latex_escape($cust_main->agent->agent),
+    'payname'      => _latex_escape($cust_main->payname),
+    'company'      => _latex_escape($cust_main->company),
+    'address1'     => _latex_escape($cust_main->address1),
+    'address2'     => _latex_escape($cust_main->address2),
+    'city'         => _latex_escape($cust_main->city),
+    'state'        => _latex_escape($cust_main->state),
+    'zip'          => _latex_escape($cust_main->zip),
+    'country'      => _latex_escape($cust_main->country),
+    'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
+    'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
+    'quantity'     => 1,
+    'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
+    #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
+  );
+
+  my $countrydefault = $conf->config('countrydefault') || 'US';
+  $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
+
+  #do variable substitutions in notes
+  $invoice_data{'notes'} =
+    join("\n",
+      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+        $conf->config('invoice_latexnotes')
+    );
+
+  $invoice_data{'footer'} =~ s/\n+$//;
+  $invoice_data{'smallfooter'} =~ s/\n+$//;
+  $invoice_data{'notes'} =~ s/\n+$//;
+
+  $invoice_data{'po_line'} =
+    (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
+      ? _latex_escape("Purchase Order #". $cust_main->payinfo)
+      : '~';
+
+  my @line_item = ();
+  my @total_item = ();
+  my @filled_in = ();
+  while ( @invoice_template ) {
+    my $line = shift @invoice_template;
+
+    if ( $line =~ /^%%Detail\s*$/ ) {
+
+      while ( ( my $line_item_line = shift @invoice_template )
+              !~ /^%%EndDetail\s*$/                            ) {
+        push @line_item, $line_item_line;
+      }
+      foreach my $line_item ( $self->_items ) {
+      #foreach my $line_item ( $self->_items_pkg ) {
+        $invoice_data{'ref'} = $line_item->{'pkgnum'};
+        $invoice_data{'description'} = _latex_escape($line_item->{'description'});
+        if ( exists $line_item->{'ext_description'} ) {
+          $invoice_data{'description'} .=
+            "\\tabularnewline\n~~".
+            join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
+        }
+        $invoice_data{'amount'} = $line_item->{'amount'};
+        $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
+        push @filled_in,
+          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
+      }
+
+    } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
+
+      while ( ( my $total_item_line = shift @invoice_template )
+              !~ /^%%EndTotalDetails\s*$/                      ) {
+        push @total_item, $total_item_line;
+      }
+
+      my @total_fill = ();
+
+      my $taxtotal = 0;
+      foreach my $tax ( $self->_items_tax ) {
+        $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
+        $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
+        push @total_fill,
+          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+              @total_item;
+      }
+
+      if ( $taxtotal ) {
+        $invoice_data{'total_item'} = 'Sub-total';
+        $invoice_data{'total_amount'} =
+          '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
+        unshift @total_fill,
+          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+              @total_item;
+      }
+
+      $invoice_data{'total_item'} = '\textbf{Total}';
+      $invoice_data{'total_amount'} =
+        '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
+      push @total_fill,
+        map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+            @total_item;
+
+      #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
+
+      # credits
+      foreach my $credit ( $self->_items_credits ) {
+        $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
+        #$credittotal
+        $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
+        push @total_fill, 
+          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+              @total_item;
+      }
+
+      # payments
+      foreach my $payment ( $self->_items_payments ) {
+        $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
+        #$paymenttotal
+        $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
+        push @total_fill, 
+          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+              @total_item;
+      }
+
+      $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
+      $invoice_data{'total_amount'} =
+        '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
+      push @total_fill,
+        map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+            @total_item;
+
+      push @filled_in, @total_fill;
+
+    } else {
+      #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
+      $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
+      push @filled_in, $line;
+    }
+
+  }
+
+  sub nounder {
+    my $var = $1;
+    $var =~ s/_/\-/g;
+    $var;
+  }
+
+  my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
+  my $unique = int(rand(2**31)); #UGH... use File::Temp or something
+
+  chdir($dir);
+  my $file = $self->invnum. ".$unique";
+
+  open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
+  print TEX join("\n", @filled_in ), "\n";
+  close TEX;
+
+  return $file;
+
+}
+
+=item print_ps [ TIME [ , TEMPLATE ] ]
+
+Returns an postscript invoice, as a scalar.
+
+TIME an optional value used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+sub print_ps {
+  my $self = shift;
+
+  my $file = $self->print_latex(@_);
+
+  #error checking!!
+  system('pslatex', "$file.tex");
+  system('pslatex', "$file.tex");
+  system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" );
+
+  open(POSTSCRIPT, "<$file.ps")
+    or die "can't open $file.ps (probable error in LaTeX template): $!\n";
+
+  unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
+
+  my $ps = '';
+  while (<POSTSCRIPT>) {
+    $ps .= $_;
+  }
+
+  close POSTSCRIPT;
+
+  return $ps;
+
+}
+
+=item print_pdf [ TIME [ , TEMPLATE ] ]
+
+Returns an PDF invoice, as a scalar.
+
+TIME an optional value used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+sub print_pdf {
+  my $self = shift;
+
+  my $file = $self->print_latex(@_);
+
+  #system('pdflatex', "$file.tex");
+  #system('pdflatex', "$file.tex");
+  #! LaTeX Error: Unknown graphics extension: .eps.
+
+  #error checking!!
+  system('pslatex', "$file.tex");
+  system('pslatex', "$file.tex");
+
+  #system('dvipdf', "$file.dvi", "$file.pdf" );
+  system("dvips -q -t letter -f $file.dvi | gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf -c save pop -");
+
+  open(PDF, "<$file.pdf")
+    or die "can't open $file.pdf (probably error in LaTeX tempalte: $!\n";
+
+  unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
+
+  my $pdf = '';
+  while (<PDF>) {
+    $pdf .= $_;
+  }
+
+  close PDF;
+
+  return $pdf;
+
+}
+
+# quick subroutine for print_latex
+#
+# There are ten characters that LaTeX treats as special characters, which
+# means that they do not simply typeset themselves: 
+#      # $ % & ~ _ ^ \ { }
+#
+# TeX ignores blanks following an escaped character; if you want a blank (as
+# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
+
+sub _latex_escape {
+  my $value = shift;
+  $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
+  $value;
+}
+
+#utility methods for print_*
+
+sub balance_due_msg {
+  my $self = shift;
+  my $msg = 'Balance Due';
+  return $msg unless $conf->exists('invoice_default_terms');
+  if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
+    $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
+  } elsif ( $conf->config('invoice_default_terms') ) {
+    $msg .= ' - '. $conf->config('invoice_default_terms');
+  }
+  $msg;
+}
+
+sub _items {
+  my $self = shift;
+  my @display = scalar(@_)
+                ? @_
+                : qw( _items_previous _items_pkg );
+                #: qw( _items_pkg );
+                #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
+  my @b = ();
+  foreach my $display ( @display ) {
+    push @b, $self->$display(@_);
+  }
+  @b;
+}
+
+sub _items_previous {
+  my $self = shift;
+  my $cust_main = $self->cust_main;
+  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
+  my @b = ();
+  foreach ( @pr_cust_bill ) {
+    push @b, {
+      'description' => 'Previous Balance, Invoice #'. $_->invnum. 
+                       ' ('. time2str('%x',$_->_date). ')',
+      #'pkgpart'     => 'N/A',
+      'pkgnum'      => 'N/A',
+      'amount'      => sprintf("%10.2f", $_->owed),
+    };
+  }
+  @b;
+
+  #{
+  #    'description'     => 'Previous Balance',
+  #    #'pkgpart'         => 'N/A',
+  #    'pkgnum'          => 'N/A',
+  #    'amount'          => sprintf("%10.2f", $pr_total ),
+  #    'ext_description' => [ map {
+  #                                 "Invoice ". $_->invnum.
+  #                                 " (". time2str("%x",$_->_date). ") ".
+  #                                 sprintf("%10.2f", $_->owed)
+  #                         } @pr_cust_bill ],
+
+  #};
+}
+
+sub _items_pkg {
+  my $self = shift;
+  my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
+  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+}
+
+sub _items_tax {
+  my $self = shift;
+  my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
+  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+}
+
+sub _items_cust_bill_pkg {
+  my $self = shift;
+  my $cust_bill_pkg = shift;
+
+  my @b = ();
+  foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
+
+    if ( $cust_bill_pkg->pkgnum ) {
+
+      my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
+      my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
+      my $pkg = $part_pkg->pkg;
+
+      my %labels;
+      #tie %labels, 'Tie::IxHash';
+      push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
+      my @ext_description;
+      foreach my $label ( keys %labels ) {
+        my @values = @{ $labels{$label} };
+        my $num = scalar(@values);
+        if ( $num > 5 ) {
+          push @ext_description, "$label ($num)";
+        } else {
+          push @ext_description, map { "$label: $_" } @values;
+        }
+      }
+
+      if ( $cust_bill_pkg->setup != 0 ) {
+        my $description = $pkg;
+        $description .= ' Setup' if $cust_bill_pkg->recur != 0;
+        my @d = @ext_description;
+        push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
+        push @b, {
+          'description'     => $description,
+          #'pkgpart'         => $part_pkg->pkgpart,
+          'pkgnum'          => $cust_pkg->pkgnum,
+          'amount'          => sprintf("%10.2f", $cust_bill_pkg->setup),
+          'ext_description' => \@d,
+        };
+      }
+
+      if ( $cust_bill_pkg->recur != 0 ) {
+        push @b, {
+          'description'     => "$pkg (" .
+                               time2str('%x', $cust_bill_pkg->sdate). ' - '.
+                               time2str('%x', $cust_bill_pkg->edate). ')',
+          #'pkgpart'         => $part_pkg->pkgpart,
+          'pkgnum'          => $cust_pkg->pkgnum,
+          'amount'          => sprintf("%10.2f", $cust_bill_pkg->recur),
+          'ext_description' => [ @ext_description,
+                                 $cust_bill_pkg->details,
+                               ],
+        };
+      }
+
+    } else { #pkgnum tax or one-shot line item (??)
+
+      my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
+                     ? ( $cust_bill_pkg->itemdesc || 'Tax' )
+                     : 'Tax';
+      if ( $cust_bill_pkg->setup != 0 ) {
+        push @b, {
+          'description' => $itemdesc,
+          'amount'      => sprintf("%10.2f", $cust_bill_pkg->setup),
+        };
+      }
+      if ( $cust_bill_pkg->recur != 0 ) {
+        push @b, {
+          'description' => "$itemdesc (".
+                           time2str("%x", $cust_bill_pkg->sdate). ' - '.
+                           time2str("%x", $cust_bill_pkg->edate). ')',
+          'amount'      => sprintf("%10.2f", $cust_bill_pkg->recur),
+        };
+      }
+
+    }
+
+  }
+
+  @b;
+
+}
+
+sub _items_credits {
+  my $self = shift;
+
+  my @b;
+  #credits
+  foreach ( $self->cust_credited ) {
+
+    #something more elaborate if $_->amount ne $_->cust_credit->credited ?
+
+    my $reason = $_->cust_credit->reason;
+    #my $reason = substr($_->cust_credit->reason,0,32);
+    #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
+    $reason = " ($reason) " if $reason;
+    push @b, {
+      #'description' => 'Credit ref\#'. $_->crednum.
+      #                 " (". time2str("%x",$_->cust_credit->_date) .")".
+      #                 $reason,
+      'description' => 'Credit applied'.
+                       time2str("%x",$_->cust_credit->_date). $reason,
+      'amount'      => sprintf("%10.2f",$_->amount),
+    };
+  }
+  #foreach ( @cr_cust_credit ) {
+  #  push @buf,[
+  #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
+  #    $money_char. sprintf("%10.2f",$_->credited)
+  #  ];
+  #}
+
+  @b;
+
+}
+
+sub _items_payments {
+  my $self = shift;
+
+  my @b;
+  #get & print payments
+  foreach ( $self->cust_bill_pay ) {
+
+    #something more elaborate if $_->amount ne ->cust_pay->paid ?
+
+    push @b, {
+      'description' => "Payment received ".
+                       time2str("%x",$_->cust_pay->_date ),
+      'amount'      => sprintf("%10.2f", $_->amount )
+    };
+  }
+
+  @b;
+
+}
+
 =back
 
 =head1 BUGS
index 5f4a491..c8b5525 100644 (file)
@@ -1,13 +1,18 @@
 package FS::cust_bill_pay;
 
 use strict;
-use vars qw( @ISA );
+use vars qw( @ISA $conf );
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::cust_bill;
 use FS::cust_pay;
 
 @ISA = qw( FS::Record );
 
+#ask FS::UID to run this stuff for us later
+FS::UID->install_callback( sub { 
+  $conf = new FS::Conf;
+} );
+
 =head1 NAME
 
 FS::cust_bill_pay - Object methods for cust_bill_pay records
@@ -101,7 +106,8 @@ sub insert {
            " greater than cust_pay.paid ". $cust_pay->paid;
   }
 
-  my $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } ) or do {
+  my $cust_bill = $self->cust_bill;
+  unless ( $cust_bill ) {
     $dbh->rollback if $oldAutoCommit;
     return "unknown cust_bill.invnum: ". $self->invnum;
   };
@@ -120,6 +126,11 @@ sub insert {
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
+  if ( $conf->exists('invoice_send_receipts') ) {
+    my $send_error = $cust_bill->send;
+    warn "Error sending receipt: $send_error\n" if $send_error;
+  }
+
   '';
 }
 
@@ -197,10 +208,6 @@ sub cust_bill {
 
 =back
 
-=head1 VERSION
-
-$Id: cust_bill_pay.pm,v 1.13 2003-08-05 00:20:41 khoff Exp $
-
 =head1 BUGS
 
 Delete and replace methods.
index e668abd..19a5453 100644 (file)
@@ -2,8 +2,10 @@ package FS::cust_credit;
 
 use strict;
 use vars qw( @ISA $conf $unsuspendauto );
+use Date::Format;
 use FS::UID qw( dbh getotaker );
 use FS::Record qw( qsearch qsearchs );
+use FS::Misc qw(send_email);
 use FS::cust_main;
 use FS::cust_refund;
 use FS::cust_credit_bill;
@@ -130,7 +132,64 @@ Currently unimplemented.
 sub delete {
   my $self = shift;
   return "Can't delete closed credit" if $self->closed =~ /^Y/i;
-  $self->SUPER::delete(@_);
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $cust_credit_bill ( $self->cust_credit_bill ) {
+    my $error = $cust_credit_bill->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  my $error = $self->SUPER::delete(@_);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $conf->config('deletecredits') ne '' ) {
+
+    my $cust_main = qsearchs('cust_main',{ 'custnum' => $self->custnum });
+
+    my $error = send_email(
+      'from'    => $conf->config('invoice_from'), #??? well as good as any
+      'to'      => $conf->config('deletecredits'),
+      'subject' => 'FREESIDE NOTIFICATION: Credit deleted',
+      'body'    => [
+        "This is an automatic message from your Freeside installation\n",
+        "informing you that the following credit has been deleted:\n",
+        "\n",
+        'crednum: '. $self->crednum. "\n",
+        'custnum: '. $self->custnum.
+          " (". $cust_main->last. ", ". $cust_main->first. ")\n",
+        'amount: $'. sprintf("%.2f", $self->amount). "\n",
+        'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
+        'reason: '. $self->reason. "\n",
+      ],
+    );
+
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't send credit deletion notification: $error";
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
 }
 
 =item replace OLD_RECORD
@@ -141,7 +200,10 @@ posted.
 =cut
 
 sub replace {
-  return "Can't modify credit!"
+  #return "Can't modify credit!"
+  my $self = shift;
+  return "Can't modify closed credit" if $self->closed =~ /^Y/i;
+  $self->SUPER::replace(@_);
 }
 
 =item check
@@ -240,13 +302,9 @@ sub credited {
 
 =back
 
-=head1 VERSION
-
-$Id: cust_credit.pm,v 1.17 2003-08-05 00:20:41 khoff Exp $
-
 =head1 BUGS
 
-The delete method.
+The delete method.  The replace method.
 
 =head1 SEE ALSO
 
index a54acb6..bd76c2e 100644 (file)
@@ -1,7 +1,7 @@
 package FS::cust_credit_bill;
 
 use strict;
-use vars qw( @ISA );
+use vars qw( @ISA $conf );
 use FS::UID qw( getotaker );
 use FS::Record qw( qsearch qsearchs );
 use FS::cust_main;
@@ -11,6 +11,11 @@ use FS::cust_bill;
 
 @ISA = qw( FS::Record );
 
+#ask FS::UID to run this stuff for us later
+FS::UID->install_callback( sub { 
+  $conf = new FS::Conf;
+} );
+
 =head1 NAME
 
 FS::cust_credit_bill - Object methods for cust_credit_bill records
@@ -69,6 +74,21 @@ sub table { 'cust_credit_bill'; }
 Adds this cust_credit_bill to the database ("Posts" all or part of a credit).
 If there is an error, returns the error, otherwise returns false.
 
+=cut
+
+sub insert {
+  my $self = shift;
+  my $error = $self->SUPER::insert(@_);
+  return $error if $error;
+
+  if ( $conf->exists('invoice_send_receipts') ) {
+    my $send_error = $self->cust_bill->send;
+    warn "Error sending receipt: $send_error\n" if $send_error;
+  }
+
+  '';
+}
+
 =item delete
 
 Currently unimplemented.
@@ -76,7 +96,10 @@ Currently unimplemented.
 =cut
 
 sub delete {
-  return "Can't unapply credit!"
+  my $self = shift;
+  return "Can't delete application for closed credit"
+    if $self->cust_credit->closed =~ /^Y/i;
+  $self->SUPER::delete(@_);
 }
 
 =item replace OLD_RECORD
@@ -141,11 +164,18 @@ sub cust_credit {
   qsearchs( 'cust_credit', { 'crednum' => $self->crednum } );
 }
 
-=back
+=item cust_bill 
+
+Returns the invoice (see L<FS::cust_bill>)
+
+=cut
 
-=head1 VERSION
+sub cust_bill {
+  my $self = shift;
+  qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
+}
 
-$Id: cust_credit_bill.pm,v 1.8 2003-08-05 00:20:41 khoff Exp $
+=back
 
 =head1 BUGS
 
index 2af2e98..6ca3287 100644 (file)
@@ -2,11 +2,12 @@ package FS::cust_main;
 
 use strict;
 use vars qw( @ISA $conf $Debug $import );
+use vars qw( $realtime_bop_decline_quiet ); #ugh
 use Safe;
 use Carp;
 BEGIN {
   eval "use Time::Local;";
-  die "Time::Local version 1.05 required with Perl versions before 5.6"
+  die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
     if $] < 5.006 && !defined($Time::Local::VERSION);
   eval "use Time::Local qw(timelocal timelocal_nocheck);";
 }
@@ -21,6 +22,7 @@ use FS::cust_bill;
 use FS::cust_bill_pkg;
 use FS::cust_pay;
 use FS::cust_credit;
+use FS::cust_refund;
 use FS::part_referral;
 use FS::cust_main_county;
 use FS::agent;
@@ -38,7 +40,9 @@ use FS::Msgcat qw(gettext);
 
 @ISA = qw( FS::Record );
 
-$Debug = 1;
+$realtime_bop_decline_quiet = 0;
+
+$Debug = 0;
 #$Debug = 1;
 
 $import = 0;
@@ -169,6 +173,8 @@ FS::Record.  The following fields are currently supported:
 
 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
 
+=item paycvv - Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
+
 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
 
 =item payname - name on card or billing name
@@ -198,7 +204,7 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 sub table { 'cust_main'; }
 
-=item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
+=item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
 
 Adds this customer to the database.  If there is an error, returns the error,
 otherwise returns false.
@@ -226,12 +232,18 @@ invoicing_list destination to the newly-created svc_acct.  Here's an example:
 
   $cust_main->insert( {}, [ $email, 'POST' ] );
 
+Currently available options are: I<noexport>
+
+If I<noexport> is set true, no provisioning jobs (exports) are scheduled.
+(You can schedule them later with the B<reexport> method.)
+
 =cut
 
 sub insert {
   my $self = shift;
   my $cust_pkgs = @_ ? shift : {};
   my $invoicing_list = @_ ? shift : '';
+  my %options = @_;
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -283,7 +295,8 @@ sub insert {
   }
 
   # packages
-  $error = $self->order_pkgs($cust_pkgs, \$seconds);
+  #local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
+  $error = $self->order_pkgs($cust_pkgs, \$seconds, %options);
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -317,9 +330,27 @@ sub insert {
 
 }
 
-=item order_pkgs
+=item order_pkgs HASHREF, [ , OPTION => VALUE ... ] ]
+
+Like the insert method on an existing record, this method orders a package
+and included services atomicaly.  Pass a Tie::RefHash data structure to this
+method containing FS::cust_pkg and FS::svc_I<tablename> objects.  There should
+be a better explanation of this, but until then, here's an example:
+
+  use Tie::RefHash;
+  tie %hash, 'Tie::RefHash'; #this part is important
+  %hash = (
+    $cust_pkg => [ $svc_acct ],
+    ...
+  );
+  $cust_main->order_pkgs( \%hash, 'noexport'=>1 );
+
+Currently available options are: I<noexport>
 
-document me.  like ->insert(%cust_pkg) on an existing record
+If I<noexport> is set true, no provisioning jobs (exports) are scheduled.
+(You can schedule them later with the B<reexport> method for each
+cust_pkg object.  Using the B<reexport> method on the cust_main object is not
+recommended, as existing services will also be reexported.)
 
 =cut
 
@@ -327,6 +358,7 @@ sub order_pkgs {
   my $self = shift;
   my $cust_pkgs = shift;
   my $seconds = shift;
+  my %options = @_;
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -339,6 +371,8 @@ sub order_pkgs {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
+
   foreach my $cust_pkg ( keys %$cust_pkgs ) {
     $cust_pkg->custnum( $self->custnum );
     my $error = $cust_pkg->insert;
@@ -365,6 +399,41 @@ sub order_pkgs {
   ''; #no error
 }
 
+=item reexport
+
+Re-schedules all exports by calling the B<reexport> method of all associated
+packages (see L<FS::cust_pkg>).  If there is an error, returns the error;
+otherwise returns false.
+
+=cut
+
+sub reexport {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
+    my $error = $cust_pkg->reexport;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
 =item delete NEW_CUSTNUM
 
 This deletes the customer.  If there is an error, returns the error, otherwise
@@ -372,7 +441,7 @@ returns false.
 
 This will completely remove all traces of the customer record.  This is not
 what you want when a customer cancels service; for that, cancel all of the
-customer's packages (see L<FS::cust_pkg/cancel>).
+customer's packages (see L</cancel>).
 
 If the customer has any uncancelled packages, you need to pass a new (valid)
 customer number for those packages to be transferred to.  Cancelled packages
@@ -399,19 +468,19 @@ sub delete {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
+  if ( $self->cust_bill ) {
     $dbh->rollback if $oldAutoCommit;
     return "Can't delete a customer with invoices";
   }
-  if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
+  if ( $self->cust_credit ) {
     $dbh->rollback if $oldAutoCommit;
     return "Can't delete a customer with credits";
   }
-  if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
+  if ( $self->cust_pay ) {
     $dbh->rollback if $oldAutoCommit;
     return "Can't delete a customer with payments";
   }
-  if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) {
+  if ( $self->cust_refund ) {
     $dbh->rollback if $oldAutoCommit;
     return "Can't delete a customer with refunds";
   }
@@ -490,6 +559,12 @@ sub replace {
   local $SIG{TSTP} = 'IGNORE';
   local $SIG{PIPE} = 'IGNORE';
 
+  if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
+       && $conf->config('users-allow_comp')                  ) {
+    return "You are not permitted to create complimentary accounts."
+      unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
+  }
+
   my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
@@ -603,7 +678,7 @@ sub check {
     || $self->ut_numbern('referral_custnum')
   ;
   #barf.  need message catalogs.  i18n.  etc.
-  $error .= "Please select a advertising source."
+  $error .= "Please select an advertising source."
     if $error =~ /^Illegal or empty \(numeric\) refnum: /;
   return $error if $error;
 
@@ -722,6 +797,21 @@ sub check {
       or return gettext('invalid_card'); # . ": ". $self->payinfo;
     return gettext('unknown_card_type')
       if cardtype($self->payinfo) eq "Unknown";
+    if ( defined $self->dbdef_table->column('paycvv') ) {
+      if ( length($self->paycvv) ) {
+        if ( cardtype($self->payinfo) eq 'American Express card' ) {
+          $self->paycvv =~ /^(\d{4})$/
+            or return "CVV2 (CID) for American Express cards is four digits.";
+          $self->paycvv($1);
+        } else {
+          $self->paycvv =~ /^(\d{3})$/
+            or return "CVV2 (CVC2/CID) is three digits.";
+          $self->paycvv($1);
+        }
+      } else {
+        $self->paycvv('');
+      }
+    }
 
   } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
 
@@ -730,6 +820,7 @@ sub check {
     $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
     $payinfo = "$1\@$2";
     $self->payinfo($payinfo);
+    $self->paycvv('') if $self->dbdef_table->column('paycvv');
 
   } elsif ( $self->payby eq 'LECB' ) {
 
@@ -738,16 +829,24 @@ sub check {
     $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
     $payinfo = $1;
     $self->payinfo($payinfo);
+    $self->paycvv('') if $self->dbdef_table->column('paycvv');
 
   } elsif ( $self->payby eq 'BILL' ) {
 
     $error = $self->ut_textn('payinfo');
     return "Illegal P.O. number: ". $self->payinfo if $error;
+    $self->paycvv('') if $self->dbdef_table->column('paycvv');
 
   } elsif ( $self->payby eq 'COMP' ) {
 
+    if ( !$self->custnum && $conf->config('users-allow_comp') ) {
+      return "You are not permitted to create complimentary accounts."
+        unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
+    }
+
     $error = $self->ut_textn('payinfo');
     return "Illegal comp account issuer: ". $self->payinfo if $error;
+    $self->paycvv('') if $self->dbdef_table->column('paycvv');
 
   } elsif ( $self->payby eq 'PREPAY' ) {
 
@@ -758,6 +857,7 @@ sub check {
     return "Illegal prepayment identifier: ". $self->payinfo if $error;
     return "Unknown prepayment identifier"
       unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
+    $self->paycvv('') if $self->dbdef_table->column('paycvv');
 
   }
 
@@ -780,7 +880,7 @@ sub check {
       if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
   }
 
-  if ( $self->payname eq '' && $self->payby ne 'CHEK' &&
+  if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
        ( ! $conf->exists('require_cardname')
          || $self->payby !~ /^(CARD|DCRD)$/  ) 
   ) {
@@ -794,7 +894,7 @@ sub check {
   $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
   $self->tax($1);
 
-  $self->otaker(getotaker);
+  $self->otaker(getotaker) unless $self->otaker;
 
   #warn "AFTER: \n". $self->_dump;
 
@@ -902,16 +1002,21 @@ sub suspend {
   grep { $_->suspend } $self->unsuspended_pkgs;
 }
 
-=item cancel
+=item cancel [ OPTION => VALUE ... ]
 
 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
+
+Available options are: I<quiet>
+
+I<quiet> can be set true to supress email cancellation notices.
+
 Always returns a list: an empty list on success or a list of errors.
 
 =cut
 
 sub cancel {
   my $self = shift;
-  grep { $_->cancel } $self->ncancelled_pkgs;
+  grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs;
 }
 
 =item agent
@@ -932,15 +1037,19 @@ conjunction with the collect method.
 
 Options are passed as name-value pairs.
 
-The only currently available option is `time', which bills the customer as if
-it were that time.  It is specified as a UNIX timestamp; see
-L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
-functions.  For example:
+Currently available options are:
+
+resetup - if set true, re-charges setup fees.
+
+time - bills the customer as if it were that time.  Specified as a UNIX
+timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and
+L<Date::Parse> for conversion functions.  For example:
 
  use Date::Parse;
  ...
  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
 
+
 If there is an error, returns the error, otherwise returns false.
 
 =cut
@@ -997,7 +1106,7 @@ sub bill {
 
     # bill setup
     my $setup = 0;
-    unless ( $cust_pkg->setup ) {
+    if ( !$cust_pkg->setup || $options{'resetup'} ) {
       my $setup_prog = $part_pkg->getfield('setup');
       $setup_prog =~ /^(.*)$/ or do {
         $dbh->rollback if $oldAutoCommit;
@@ -1017,14 +1126,14 @@ sub bill {
         return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
                "(expression $setup_prog): $@";
       }
-      $cust_pkg->setfield('setup',$time);
+      $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
       $cust_pkg_mod_flag=1; 
     }
 
     #bill recurring fee
     my $recur = 0;
     my $sdate;
-    if ( $part_pkg->getfield('freq') > 0 &&
+    if ( $part_pkg->getfield('freq') ne '0' &&
          ! $cust_pkg->getfield('susp') &&
          ( $cust_pkg->getfield('bill') || 0 ) <= $time
     ) {
@@ -1062,8 +1171,19 @@ sub bill {
       $cust_pkg->last_bill($sdate)
         if $cust_pkg->dbdef_table->column('last_bill');
 
-      $mon += $part_pkg->freq;
-      until ( $mon < 12 ) { $mon -= 12; $year++; }
+      if ( $part_pkg->freq =~ /^\d+$/ ) {
+        $mon += $part_pkg->freq;
+        until ( $mon < 12 ) { $mon -= 12; $year++; }
+      } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
+        my $weeks = $1;
+        $mday += $weeks * 7;
+      } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
+        my $days = $1;
+        $mday += $days;
+      } else {
+        $dbh->rollback if $oldAutoCommit;
+        return "unparsable frequency: ". $part_pkg->freq;
+      }
       $cust_pkg->setfield('bill',
         timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
       $cust_pkg_mod_flag = 1; 
@@ -1073,7 +1193,6 @@ sub bill {
     warn "\$recur is undefined" unless defined($recur);
     warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
 
-    my $taxable_charged = 0;
     if ( $cust_pkg_mod_flag ) {
       $error=$cust_pkg->replace($old_cust_pkg);
       if ( $error ) { #just in case
@@ -1082,15 +1201,15 @@ sub bill {
       }
       $setup = sprintf( "%.2f", $setup );
       $recur = sprintf( "%.2f", $recur );
-      if ( $setup < 0 ) {
+      if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
         $dbh->rollback if $oldAutoCommit;
         return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
       }
-      if ( $recur < 0 ) {
+      if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
         $dbh->rollback if $oldAutoCommit;
         return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
       }
-      if ( $setup > 0 || $recur > 0 ) {
+      if ( $setup != 0 || $recur != 0 ) {
         my $cust_bill_pkg = new FS::cust_bill_pkg ({
           'pkgnum'  => $cust_pkg->pkgnum,
           'setup'   => $setup,
@@ -1102,88 +1221,113 @@ sub bill {
         push @cust_bill_pkg, $cust_bill_pkg;
         $total_setup += $setup;
         $total_recur += $recur;
-        $taxable_charged += $setup
-          unless $part_pkg->setuptax =~ /^Y$/i;
-        $taxable_charged += $recur
-          unless $part_pkg->recurtax =~ /^Y$/i;
-          
-        unless ( $self->tax =~ /Y/i
-                 || $self->payby eq 'COMP'
-                 || $taxable_charged == 0 ) {
-
-          my $cust_main_county = qsearchs('cust_main_county',{
-              'state'    => $self->state,
-              'county'   => $self->county,
-              'country'  => $self->country,
-              'taxclass' => $part_pkg->taxclass,
-          } );
-          $cust_main_county ||= qsearchs('cust_main_county',{
-              'state'    => $self->state,
-              'county'   => $self->county,
-              'country'  => $self->country,
-              'taxclass' => '',
-          } );
-          unless ( $cust_main_county ) {
+
+        unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
+
+          my @taxes = qsearch( 'cust_main_county', {
+                                 'state'    => $self->state,
+                                 'county'   => $self->county,
+                                 'country'  => $self->country,
+                                 'taxclass' => $part_pkg->taxclass,
+                                                                      } );
+          unless ( @taxes ) {
+            @taxes =  qsearch( 'cust_main_county', {
+                                  'state'    => $self->state,
+                                  'county'   => $self->county,
+                                  'country'  => $self->country,
+                                  'taxclass' => '',
+                                                                      } );
+          }
+
+          #one more try at a whole-country tax rate
+          unless ( @taxes ) {
+            @taxes =  qsearch( 'cust_main_county', {
+                                  'state'    => '',
+                                  'county'   => '',
+                                  'country'  => $self->country,
+                                  'taxclass' => '',
+                                                                      } );
+          }
+
+          # maybe eliminate this entirely, along with all the 0% records
+          unless ( @taxes ) {
             $dbh->rollback if $oldAutoCommit;
             return
               "fatal: can't find tax rate for state/county/country/taxclass ".
               join('/', ( map $self->$_(), qw(state county country) ),
                         $part_pkg->taxclass ).  "\n";
           }
+  
+          foreach my $tax ( @taxes ) {
+
+            my $taxable_charged = 0;
+            $taxable_charged += $setup
+              unless $part_pkg->setuptax =~ /^Y$/i
+                  || $tax->setuptax =~ /^Y$/i;
+            $taxable_charged += $recur
+              unless $part_pkg->recurtax =~ /^Y$/i
+                  || $tax->recurtax =~ /^Y$/i;
+            next unless $taxable_charged;
+
+            if ( $tax->exempt_amount > 0 ) {
+              my ($mon,$year) = (localtime($sdate) )[4,5];
+              $mon++;
+              my $freq = $part_pkg->freq || 1;
+              if ( $freq !~ /(\d+)$/ ) {
+                $dbh->rollback if $oldAutoCommit;
+                return "daily/weekly package definitions not (yet?)".
+                       " compatible with monthly tax exemptions";
+              }
+              my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
+              foreach my $which_month ( 1 .. $freq ) {
+                my %hash = (
+                  'custnum' => $self->custnum,
+                  'taxnum'  => $tax->taxnum,
+                  'year'    => 1900+$year,
+                  'month'   => $mon++,
+                );
+                #until ( $mon < 12 ) { $mon -= 12; $year++; }
+                until ( $mon < 13 ) { $mon -= 12; $year++; }
+                my $cust_tax_exempt =
+                  qsearchs('cust_tax_exempt', \%hash)
+                  || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
+                my $remaining_exemption = sprintf("%.2f",
+                  $tax->exempt_amount - $cust_tax_exempt->amount );
+                if ( $remaining_exemption > 0 ) {
+                  my $addl = $remaining_exemption > $taxable_per_month
+                    ? $taxable_per_month
+                    : $remaining_exemption;
+                  $taxable_charged -= $addl;
+                  my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
+                    $cust_tax_exempt->hash,
+                    'amount' =>
+                      sprintf("%.2f", $cust_tax_exempt->amount + $addl),
+                  } );
+                  $error = $new_cust_tax_exempt->exemptnum
+                    ? $new_cust_tax_exempt->replace($cust_tax_exempt)
+                    : $new_cust_tax_exempt->insert;
+                  if ( $error ) {
+                    $dbh->rollback if $oldAutoCommit;
+                    return "fatal: can't update cust_tax_exempt: $error";
+                  }
+  
+                } # if $remaining_exemption > 0
+  
+              } #foreach $which_month
+  
+            } #if $tax->exempt_amount
+
+            $taxable_charged = sprintf( "%.2f", $taxable_charged);
+
+            #$tax += $taxable_charged * $cust_main_county->tax / 100
+            $tax{ $tax->taxname || 'Tax' } +=
+              $taxable_charged * $tax->tax / 100
+
+          } #foreach my $tax ( @taxes )
+
+        } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
 
-          if ( $cust_main_county->exempt_amount ) {
-            my ($mon,$year) = (localtime($sdate) )[4,5];
-            $mon++;
-            my $freq = $part_pkg->freq || 1;
-            my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
-            foreach my $which_month ( 1 .. $freq ) {
-              my %hash = (
-                'custnum' => $self->custnum,
-                'taxnum'  => $cust_main_county->taxnum,
-                'year'    => 1900+$year,
-                'month'   => $mon++,
-              );
-              #until ( $mon < 12 ) { $mon -= 12; $year++; }
-              until ( $mon < 13 ) { $mon -= 12; $year++; }
-              my $cust_tax_exempt =
-                qsearchs('cust_tax_exempt', \%hash)
-                || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
-              my $remaining_exemption = sprintf("%.2f",
-                $cust_main_county->exempt_amount - $cust_tax_exempt->amount );
-              if ( $remaining_exemption > 0 ) {
-                my $addl = $remaining_exemption > $taxable_per_month
-                  ? $taxable_per_month
-                  : $remaining_exemption;
-                $taxable_charged -= $addl;
-                my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
-                  $cust_tax_exempt->hash,
-                  'amount' => sprintf("%.2f", $cust_tax_exempt->amount + $addl),
-                } );
-                $error = $new_cust_tax_exempt->exemptnum
-                  ? $new_cust_tax_exempt->replace($cust_tax_exempt)
-                  : $new_cust_tax_exempt->insert;
-                if ( $error ) {
-                  $dbh->rollback if $oldAutoCommit;
-                  return "fatal: can't update cust_tax_exempt: $error";
-                }
-
-              } # if $remaining_exemption > 0
-
-            } #foreach $which_month
-
-          } #if $cust_main_county->exempt_amount
-
-          $taxable_charged = sprintf( "%.2f", $taxable_charged);
-
-          #$tax += $taxable_charged * $cust_main_county->tax / 100
-          $tax{ $cust_main_county->taxname || 'Tax' } +=
-            $taxable_charged * $cust_main_county->tax / 100
-
-        } #unless $self->tax =~ /Y/i
-          #       || $self->payby eq 'COMP'
-          #       || $taxable_charged == 0
-
-      } #if $setup > 0 || $recur > 0
+      } #if $setup != 0 || $recur != 0
       
     } #if $cust_pkg_mod_flag
 
@@ -1210,21 +1354,42 @@ sub bill {
 #      $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
 #    );
 
-  foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
-    my $tax = sprintf("%.2f", $tax{$taxname} );
-    $charged = sprintf( "%.2f", $charged+$tax );
+  if ( dbdef->table('cust_bill_pkg')->column('itemdesc') ) { #1.5 schema
+
+    foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
+      my $tax = sprintf("%.2f", $tax{$taxname} );
+      $charged = sprintf( "%.2f", $charged+$tax );
+  
+      my $cust_bill_pkg = new FS::cust_bill_pkg ({
+        'pkgnum'   => 0,
+        'setup'    => $tax,
+        'recur'    => 0,
+        'sdate'    => '',
+        'edate'    => '',
+        'itemdesc' => $taxname,
+      });
+      push @cust_bill_pkg, $cust_bill_pkg;
+    }
+  
+  } else { #1.4 schema
+
+    my $tax = 0;
+    foreach ( values %tax ) { $tax += $_ };
+    $tax = sprintf("%.2f", $tax);
+    if ( $tax > 0 ) {
+      $charged = sprintf( "%.2f", $charged+$tax );
+
+      my $cust_bill_pkg = new FS::cust_bill_pkg ({
+        'pkgnum' => 0,
+        'setup'  => $tax,
+        'recur'  => 0,
+        'sdate'  => '',
+        'edate'  => '',
+      });
+      push @cust_bill_pkg, $cust_bill_pkg;
+    }
 
-    my $cust_bill_pkg = new FS::cust_bill_pkg ({
-      'pkgnum'   => 0,
-      'setup'    => $tax,
-      'recur'    => 0,
-      'sdate'    => '',
-      'edate'    => '',
-      'itemdesc' => $taxname,
-    });
-    push @cust_bill_pkg, $cust_bill_pkg;
   }
-#  }
 
   my $cust_bill = new FS::cust_bill ( {
     'custnum' => $self->custnum,
@@ -1288,6 +1453,8 @@ report_badcard - This option is deprecated.
 
 force_print - This option is deprecated; see the invoice events web interface.
 
+quiet - set true to surpress email card/ACH decline notices.
+
 =cut
 
 sub collect {
@@ -1325,24 +1492,15 @@ sub collect {
     }
   }
 
-  foreach my $cust_bill ( $self->cust_bill ) {
-
-    #this has to be before next's
-    my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
-                                  ? $balance
-                                  : $cust_bill->owed
-    );
-    $balance = sprintf( "%.2f", $balance - $amount );
-
-    next unless $cust_bill->owed > 0;
+  foreach my $cust_bill ( $self->open_cust_bill ) {
 
     # don't try to charge for the same invoice if it's already in a batch
     #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
 
-    warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
-
-    next unless $amount > 0;
+    last if $self->balance <= 0;
 
+    warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")"
+      if $Debug;
 
     foreach my $part_bill_event (
       sort {    $a->seconds   <=> $b->seconds
@@ -1359,12 +1517,18 @@ sub collect {
                                        'disabled' => '',           } )
     ) {
 
-      last unless $cust_bill->owed > 0; #don't run subsequent events if owed=0
+      last if $cust_bill->owed <= 0  # don't run subsequent events if owed<=0
+           || $self->balance   <= 0; # or if balance<=0
 
       warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
         if $Debug;
       my $cust_main = $self; #for callback
-      my $error = eval $part_bill_event->eventcode;
+
+      my $error;
+      {
+        local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
+        $error = eval $part_bill_event->eventcode;
+      }
 
       my $status = '';
       my $statustext = '';
@@ -1548,9 +1712,20 @@ sub realtime_bop {
 
   my %content;
   if ( $method eq 'CC' ) { 
+
     $content{card_number} = $self->payinfo;
     $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
     $content{expiration} = "$2/$1";
+
+    $content{cvv2} = $self->paycvv
+      if defined $self->dbdef_table->column('paycvv')
+         && length($self->paycvv);
+
+    $content{recurring_billing} = 'YES'
+      if qsearch('cust_pay', { 'custnum' => $self->custnum,
+                               'payby'   => 'CARD',
+                               'payinfo' => $self->payinfo, } );
+
   } elsif ( $method eq 'ECHECK' ) {
     my($account_number,$routing_code) = $self->payinfo;
     ( $content{account_number}, $content{routing_code} ) =
@@ -1635,6 +1810,21 @@ sub realtime_bop {
 
   }
 
+  #remove paycvv after initial transaction
+  #make this disable-able via a config option if anyone insists?  
+  # (though that probably violates cardholder agreements)
+  if ( defined $self->dbdef_table->column('paycvv')
+       && length($self->paycvv)
+       && ! grep { $_ eq cardtype($self->payinfo) } $conf->config('cvv-save')
+  ) {
+    my $new = new FS::cust_main { $self->hash };
+    $new->paycvv('');
+    my $error = $new->replace($self);
+    if ( $error ) {
+      warn "error removing cvv: $error\n";
+    }
+  }
+
   #result handling
   if ( $transaction->is_success() ) {
 
@@ -1669,8 +1859,11 @@ sub realtime_bop {
 
     my $perror = "$processor error: ". $transaction->error_message;
 
-    if ( !$options{'quiet'} && $conf->exists('emaildecline')
+    if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
+         && $conf->exists('emaildecline')
          && grep { $_ ne 'POST' } $self->invoicing_list
+         && ! grep { $_ eq $transaction->error_message }
+                   $conf->config('emaildecline-exclude')
     ) {
       my @templ = $conf->config('declinetemplate');
       my $template = new Text::Template (
@@ -2196,6 +2389,42 @@ sub open_cust_bill {
   grep { $_->owed > 0 } $self->cust_bill;
 }
 
+=item cust_credit
+
+Returns all the credits (see L<FS::cust_credit>) for this customer.
+
+=cut
+
+sub cust_credit {
+  my $self = shift;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
+}
+
+=item cust_pay
+
+Returns all the payments (see L<FS::cust_pay>) for this customer.
+
+=cut
+
+sub cust_pay {
+  my $self = shift;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
+}
+
+=item cust_refund
+
+Returns all the refunds (see L<FS::cust_refund>) for this customer.
+
+=cut
+
+sub cust_refund {
+  my $self = shift;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
+}
+
 =back
 
 =head1 SUBROUTINES
index f631d8c..76c982a 100644 (file)
@@ -63,6 +63,10 @@ currently supported:
 
 =item taxname - if defined, printed on invoices instead of "Tax"
 
+=item setuptax - if 'Y', this tax does not apply to setup fees
+
+=item recurtax - if 'Y', this tax does not apply to recurring fees
+
 =back
 
 =head1 METHODS
@@ -113,10 +117,38 @@ sub check {
     || $self->ut_textn('taxclass') # ...
     || $self->ut_money('exempt_amount')
     || $self->ut_textn('taxname')
+    || $self->ut_enum('setuptax', [ '', 'Y' ] )
+    || $self->ut_enum('recurtax', [ '', 'Y' ] )
     || $self->SUPER::check
     ;
 
+}
+
+sub taxname {
+  my $self = shift;
+  if ( $self->dbdef_table->column('taxname') ) {
+    return $self->setfield('taxname', $_[0]) if @_;
+    return $self->getfield('taxname');
+  }  
+  return '';
+}
+
+sub setuptax {
+  my $self = shift;
+  if ( $self->dbdef_table->column('setuptax') ) {
+    return $self->setfield('setuptax', $_[0]) if @_;
+    return $self->getfield('setuptax');
+  }  
+  return '';
+}
 
+sub recurtax {
+  my $self = shift;
+  if ( $self->dbdef_table->column('recurtax') ) {
+    return $self->setfield('recurtax', $_[0]) if @_;
+    return $self->getfield('recurtax');
+  }  
+  return '';
 }
 
 =back
index 7be1153..e1943ae 100644 (file)
@@ -385,11 +385,23 @@ sub unapplied {
   sprintf("%.2f", $amount );
 }
 
+=item cust_main
+
+Returns the parent customer object (see L<FS::cust_main>).
+
+=cut
+
+sub cust_main {
+  my $self = shift;
+  qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+}
+
+
 =back
 
 =head1 VERSION
 
-$Id: cust_pay.pm,v 1.25 2003-08-05 00:20:42 khoff Exp $
+$Id: cust_pay.pm,v 1.26 2003-09-10 10:54:46 ivan Exp $
 
 =head1 BUGS
 
index 1a53046..8059f1c 100644 (file)
@@ -2,7 +2,7 @@ package FS::cust_pay_batch;
 
 use strict;
 use vars qw( @ISA );
-use FS::Record;
+use FS::Record qw(dbh qsearchs);
 use Business::CreditCard;
 
 @ISA = qw( FS::Record );
@@ -188,11 +188,199 @@ sub check {
   $self->SUPER::check;
 }
 
+=item cust_main
+
+Returns the customer (see L<FS::cust_main>) for this batched credit card
+payment.
+
+=cut
+
+sub cust_main {
+  my $self = shift;
+  qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+}
+
 =back
 
-=head1 VERSION
+=head1 SUBROUTINES
+
+=over 4
+
+=item import_results
+
+=cut
+
+sub import_results {
+  use Time::Local;
+  use FS::cust_pay;
+  eval "use Text::CSV_XS;";
+  die $@ if $@;
+#
+  my $param = shift;
+  my $fh = $param->{'filehandle'};
+  my $format = $param->{'format'};
+  my $paybatch = $param->{'paybatch'};
+
+  my @fields;
+  my $end_condition;
+  my $end_hook;
+  my $hook;
+  my $approved_condition;
+  my $declined_condition;
+
+  if ( $format eq 'csv-td_canada_trust-merchant_pc_batch' ) {
+
+    @fields = (
+      'paybatchnum', # Reference#:  Invoice number of the transaction
+      'paid',        # Amount:  Amount of the transaction.  Dollars and cents
+                     #          with no decimal entered.
+      '',            # Card Type:  0 - MCrd, 1 - Visa, 2 - AMEX, 3 - Discover,
+                     #             4 - Insignia, 5 - Diners/EnRoute, 6 - JCB
+      '_date',       # Transaction Date:  Date the Transaction was processed
+      'time',        # Transaction Time:  Time the transaction was processed
+      'payinfo',     # Card Number:  Card number for the transaction
+      '',            # Expiry Date:  Expiry date of the card
+      '',            # Auth#:  Authorization number entered for force post
+                     #         transaction
+      'type',        # Transaction Type:  0 - purchase, 40 - refund,
+                     #                    20 - force post
+      'result',      # Processing Result: 3 - Approval,
+                     #                    4 - Declined/Amount over limit,
+                     #                    5 - Invalid/Expired/stolen card,
+                     #                    6 - Comm Error
+      '',            # Terminal ID: Terminal ID used to process the transaction
+    );
+
+    $end_condition = sub {
+      my $hash = shift;
+      $hash->{'type'} eq '0BC';
+    };
+
+    $end_hook = sub {
+      my( $hash, $total) = @_;
+      $total = sprintf("%.2f", $total);
+      my $batch_total = sprintf("%.2f", $hash->{'paybatchnum'} / 100 );
+      return "Our total $total does not match bank total $batch_total!"
+        if $total != $batch_total;
+      '';
+    };
+
+    $hook = sub {
+      my $hash = shift;
+      $hash->{'paid'} = sprintf("%.2f", $hash->{'paid'} / 100 );
+      $hash->{'_date'} = timelocal( substr($hash->{'time'},  4, 2),
+                                    substr($hash->{'time'},  2, 2),
+                                    substr($hash->{'time'},  0, 2),
+                                    substr($hash->{'_date'}, 6, 2),
+                                    substr($hash->{'_date'}, 4, 2)-1,
+                                    substr($hash->{'_date'}, 0, 4)-1900, );
+    };
+
+    $approved_condition = sub {
+      my $hash = shift;
+      $hash->{'type'} eq '0' && $hash->{'result'} == 3;
+    };
+
+    $declined_condition = sub {
+      my $hash = shift;
+      $hash->{'type'} eq '0' && (    $hash->{'result'} == 4
+                                  || $hash->{'result'} == 5 );
+    };
+
+
+  } else {
+    return "Unknown format $format";
+  }
+
+  my $csv = new Text::CSV_XS;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $total = 0;
+  my $line;
+  while ( defined($line=<$fh>) ) {
+
+    next if $line =~ /^\s*$/; #skip blank lines
 
-$Id: cust_pay_batch.pm,v 1.7 2003-08-05 00:20:42 khoff Exp $
+    $csv->parse($line) or do {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't parse: ". $csv->error_input();
+    };
+
+    my @values = $csv->fields();
+    my %hash;
+    foreach my $field ( @fields ) {
+      my $value = shift @values;
+      next unless $field;
+      $hash{$field} = $value;
+    }
+
+    if ( &{$end_condition}(\%hash) ) {
+      my $error = &{$end_hook}(\%hash, $total);
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+      last;
+    }
+
+    my $cust_pay_batch =
+      qsearchs('cust_pay_batch', { 'paybatchnum' => $hash{'paybatchnum'} } );
+    unless ( $cust_pay_batch ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "unknown paybatchnum $hash{'paybatchnum'}\n";
+    }
+    my $custnum = $cust_pay_batch->custnum,
+
+    my $error = $cust_pay_batch->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "error removing paybatchnum $hash{'paybatchnum'}: $error\n";
+    }
+
+    &{$hook}(\%hash);
+
+    if ( &{$approved_condition}(\%hash) ) {
+
+      my $cust_pay = new FS::cust_pay ( {
+        'custnum'  => $custnum,
+        'payby'    => 'CARD',
+        'paybatch' => $paybatch,
+        map { $_ => $hash{$_} } (qw( paid _date payinfo )),
+      } );
+      $error = $cust_pay->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error adding payment paybatchnum $hash{'paybatchnum'}: $error\n";
+      }
+      $total += $hash{'paid'};
+  
+      $cust_pay->cust_main->apply_payments;
+
+    } elsif ( &{$declined_condition}(\%hash) ) {
+
+      #this should be configurable... if anybody else ever uses batches
+      $cust_pay_batch->cust_main->suspend;
+
+    }
+
+  }
+  
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=back
 
 =head1 BUGS
 
index f59b45a..c218211 100644 (file)
@@ -1,8 +1,7 @@
 package FS::cust_pkg;
 
 use strict;
-use vars qw(@ISA $disable_agentcheck);
-use vars qw( $quiet );
+use vars qw(@ISA $disable_agentcheck $DEBUG);
 use FS::UID qw( getotaker dbh );
 use FS::Record qw( qsearch qsearchs );
 use FS::Misc qw( send_email );
@@ -26,6 +25,8 @@ use FS::Conf;
 
 @ISA = qw( FS::Record );
 
+$DEBUG = 0;
+
 $disable_agentcheck = 0;
 
 sub _cache {
@@ -149,7 +150,7 @@ sub insert {
   return $error if $error;
 
   my $cust_main = $self->cust_main;
-  return "Unknown customer ". $self->custnum unless $cust_main;
+  return "Unknown custnum: ". $self->custnum unless $cust_main;
 
   unless ( $disable_agentcheck ) {
     my $agent = qsearchs( 'agent', { 'agentnum' => $cust_main->agentnum } );
@@ -245,25 +246,31 @@ sub check {
   $self->otaker($1);
 
   if ( $self->dbdef_table->column('manual_flag') ) {
-    $self->manual_flag =~ /^([01]?)$/ or return "Illegal manual_flag";
+    $self->manual_flag('') if $self->manual_flag eq ' ';
+    $self->manual_flag =~ /^([01]?)$/
+      or return "Illegal manual_flag ". $self->manual_flag;
     $self->manual_flag($1);
   }
 
   $self->SUPER::check;
 }
 
-=item cancel
+=item cancel [ OPTION => VALUE ... ]
 
 Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>)
 in this package, then cancels the package itself (sets the cancel field to
 now).
 
+Available options are: I<quiet>
+
+I<quiet> can be set true to supress email cancellation notices.
+
 If there is an error, returns the error, otherwise returns false.
 
 =cut
 
 sub cancel {
-  my $self = shift;
+  my( $self, %options ) = @_;
   my $error;
 
   local $SIG{HUP} = 'IGNORE';
@@ -304,7 +311,7 @@ sub cancel {
 
   my $conf = new FS::Conf;
   my @invoicing_list = grep { $_ ne 'POST' } $self->cust_main->invoicing_list;
-  if ( !$quiet && $conf->exists('emailcancel') && @invoicing_list ) {
+  if ( !$options{'quiet'} && $conf->exists('emailcancel') && @invoicing_list ) {
     my $conf = new FS::Conf;
     my $error = send_email(
       'from'    => $conf->config('invoice_from'),
@@ -609,7 +616,7 @@ sub attribute_since_sqlradacct {
 
 Transfers as many services as possible from this package to another package.
 The destination package must already exist.  Services are moved only if 
-the destination allows services with the correct I<svcnum> (not svcdb).  
+the destination allows services with the correct I<svcpart> (not svcdb).  
 Any services that can't be moved remain in the original package.
 
 Returns an error, if there is one; otherwise, returns the number of services 
@@ -660,6 +667,41 @@ sub transfer {
   return $remaining;
 }
 
+=item reexport
+
+=cut
+
+sub reexport {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $cust_svc ( $self->cust_svc ) {
+    #false laziness w/svc_Common::insert
+    my $svc_x = $cust_svc->svc_x;
+    foreach my $part_export ( $cust_svc->part_svc->part_export ) {
+      my $error = $part_export->export_insert($svc_x);
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
 =back
 
 =head1 SUBROUTINES
@@ -686,13 +728,16 @@ newly-created cust_pkg objects.
 =cut
 
 sub order {
-
-  # Rewritten to make use of the transfer() method, and in general 
-  # to not suck so badly.
-
   my ($custnum, $pkgparts, $remove_pkgnum, $return_cust_pkg) = @_;
 
   # Transactionize this whole mess
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE'; 
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE'; 
+  local $SIG{PIPE} = 'IGNORE'; 
+
   my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
index 7aa311b..0d8a121 100644 (file)
@@ -231,7 +231,7 @@ sub check {
     });
     return "Already ". scalar(@cust_svc). " ". $part_svc->svc.
            " services for pkgnum ". $self->pkgnum
-      if scalar(@cust_svc) >= $quantity && (!$ignore_quantity || !$quantity);
+      if scalar(@cust_svc) >= $quantity && !$ignore_quantity;
   }
 
   $self->SUPER::check;
@@ -281,10 +281,15 @@ sub label {
   if ( $svcdb eq 'svc_acct' ) {
     $tag = $svc_x->email;
   } elsif ( $svcdb eq 'svc_forward' ) {
-    my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_x->srcsvc } );
-    $tag = $svc_acct->email. '->';
+    if ( $svc_x->srcsvc ) {
+      my $svc_acct = $svc_x->srcsvc_acct;
+      $tag = $svc_acct->email;
+    } else {
+      $tag = $svc_x->src;
+    }
+    $tag .= '->';
     if ( $svc_x->dstsvc ) {
-      $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_x->dstsvc } );
+      my $svc_acct = $svc_x->dstsvc_acct;
       $tag .= $svc_acct->email;
     } else {
       $tag .= $svc_x->dst;
@@ -296,6 +301,8 @@ sub label {
     $tag = $domain->zone;
   } elsif ( $svcdb eq 'svc_broadband' ) {
     $tag = $svc_x->ip_addr;
+  } elsif ( $svcdb eq 'svc_external' ) {
+    $tag = $svc_x->id. ': '. $svc_x->title;
   } else {
     cluck "warning: asked for label of unsupported svcdb; using svcnum";
     $tag = $svc_x->getfield('svcnum');
@@ -353,15 +360,20 @@ for records where B<svcdb> is not "svc_acct".
 sub seconds_since_sqlradacct {
   my($self, $start, $end) = @_;
 
-  my $username = $self->svc_x->username;
+  my $svc_x = $self->svc_x;
 
-  my @part_export = $self->part_svc->part_export('sqlradius')
-    or die "no sqlradius export configured for this service type";
+  my @part_export = $self->part_svc->part_export('sqlradius');
+  push @part_export, $self->part_svc->part_export('sqlradius_withdomain');
+  die "no sqlradius or sqlradius_withdomain export configured for this".
+      "service type"
+    unless @part_export;
     #or return undef;
 
   my $seconds = 0;
   foreach my $part_export ( @part_export ) {
 
+    next if $part_export->option('ignore_accounting');
+
     my $dbh = DBI->connect( map { $part_export->option($_) }
                             qw(datasrc username password)    )
       or die "can't connect to sqlradius database: ". $DBI::errstr;
@@ -378,6 +390,15 @@ sub seconds_since_sqlradacct {
       $str2time = 'extract(epoch from ';
     }
 
+    my $username;
+    if ( $part_export->exporttype eq 'sqlradius' ) {
+      $username = $svc_x->username;
+    } elsif ( $part_export->exporttype eq 'sqlradius_withdomain' ) {
+      $username = $svc_x->email;
+    } else {
+      die 'unknown exporttype '. $part_export->exporttype;
+    }
+
     my $query;
   
     #find closed sessions completely within the given range
@@ -456,16 +477,21 @@ for records where B<svcdb> is not "svc_acct".
 sub attribute_since_sqlradacct {
   my($self, $start, $end, $attrib) = @_;
 
-  my $username = $self->svc_x->username;
+  my $svc_x = $self->svc_x;
 
-  my @part_export = $self->part_svc->part_export('sqlradius')
-    or die "no sqlradius export configured for this service type";
+  my @part_export = $self->part_svc->part_export('sqlradius');
+  push @part_export, $self->part_svc->part_export('sqlradius_withdomain');
+  die "no sqlradius or sqlradius_withdomain export configured for this".
+      "service type"
+    unless @part_export;
     #or return undef;
 
   my $sum = 0;
 
   foreach my $part_export ( @part_export ) {
 
+    next if $part_export->option('ignore_accounting');
+
     my $dbh = DBI->connect( map { $part_export->option($_) }
                             qw(datasrc username password)    )
       or die "can't connect to sqlradius database: ". $DBI::errstr;
@@ -482,6 +508,15 @@ sub attribute_since_sqlradacct {
       $str2time = 'extract(epoch from ';
     }
 
+    my $username;
+    if ( $part_export->exporttype eq 'sqlradius' ) {
+      $username = $svc_x->username;
+    } elsif ( $part_export->exporttype eq 'sqlradius_withdomain' ) {
+      $username = $svc_x->email;
+    } else {
+      die 'unknown exporttype '. $part_export->exporttype;
+    }
+
     my $sth = $dbh->prepare("SELECT SUM($attrib)
                                FROM radacct
                                WHERE UserName = ?
@@ -499,6 +534,62 @@ sub attribute_since_sqlradacct {
 
 }
 
+=item get_session_history_sqlradacct TIMESTAMP_START TIMESTAMP_END
+
+See L<FS::svc_acct/get_session_history_sqlradacct>.  Equivalent to
+$cust_svc->svc_x->get_session_history_sqlradacct, but more efficient.
+Meaningless for records where B<svcdb> is not "svc_acct".
+
+=cut
+
+sub get_session_history {
+  my($self, $start, $end, $attrib) = @_;
+
+  my $username = $self->svc_x->username;
+
+  my @part_export = $self->part_svc->part_export('sqlradius')
+    or die "no sqlradius export configured for this service type";
+    #or return undef;
+                     
+  my @sessions = ();
+
+  foreach my $part_export ( @part_export ) {
+                                            
+    my $dbh = DBI->connect( map { $part_export->option($_) }
+                            qw(datasrc username password)    )
+      or die "can't connect to sqlradius database: ". $DBI::errstr;
+
+    #select a unix time conversion function based on database type
+    my $str2time;                                                 
+    if ( $dbh->{Driver}->{Name} eq 'mysql' ) {
+      $str2time = 'UNIX_TIMESTAMP(';          
+    } elsif ( $dbh->{Driver}->{Name} eq 'Pg' ) {
+      $str2time = 'EXTRACT( EPOCH FROM ';       
+    } else {
+      warn "warning: unknown database type ". $dbh->{Driver}->{Name}.
+           "; guessing how to convert to UNIX timestamps";
+      $str2time = 'extract(epoch from ';                  
+    }
+
+    my @fields = qw( acctstarttime acctstoptime acctsessiontime
+                     acctinputoctets acctoutputoctets framedipaddress );
+     
+    my $sth = $dbh->prepare('SELECT '. join(', ', @fields).
+                            "  FROM radacct
+                               WHERE UserName = ?
+                                 AND $str2time AcctStopTime ) >= ?
+                                 AND $str2time AcctStopTime ) <=  ?
+                                 ORDER BY AcctStartTime DESC
+    ") or die $dbh->errstr;                                 
+    $sth->execute($username, $start, $end) or die $sth->errstr;
+
+    push @sessions, map { { %$_ } } @{ $sth->fetchall_arrayref({}) };
+
+  }
+  \@sessions
+
+}
+
 =back
 
 =head1 BUGS
index 9e5d821..86f9294 100644 (file)
@@ -160,6 +160,12 @@ sub check {
           join("\n", $conf->config('invoice_template') )
       );
     }
+    unless ( $conf->exists("invoice_latex_$name") ) {
+      $conf->set(
+        "invoice_latex_$name" =>
+          join("\n", $conf->config('invoice_latex') )
+      );
+    }
   }
 
   $self->SUPER::check;
index ab0a4b5..8423da2 100644 (file)
@@ -274,10 +274,6 @@ sub check {
   ;
   return $error if $error;
 
-  $self->machine =~ /^([\w\-\.]*)$/
-    or return "Illegal machine: ". $self->machine;
-  $self->machine($1);
-
   $self->nodomain =~ /^(Y?)$/ or return "Illegal nodomain: ". $self->nodomain;
   $self->nodomain($1);
 
@@ -472,18 +468,22 @@ sub _export_delete {
   return "_export_delete: unknown export type ". $self->exporttype;
 }
 
-#fallbacks providing null operations
+#call svcdb-specific fallbacks
 
 sub _export_suspend {
   my $self = shift;
   #warn "warning: _export_suspened unimplemented for". ref($self);
-  '';
+  my $svc_x = shift;
+  my $new = $svc_x->clone_suspended;
+  $self->_export_replace( $new, $svc_x );
 }
 
 sub _export_unsuspend {
   my $self = shift;
   #warn "warning: _export_unsuspend unimplemented for ". ref($self);
-  '';
+  my $svc_x = shift;
+  my $old = $svc_x->clone_kludge_unsuspend;
+  $self->_export_replace( $svc_x, $old );
 }
 
 =back
@@ -580,13 +580,13 @@ tie my %shellcommands_options, 'Tie::IxHash',
                         type =>'checkbox',
                       },
   'suspend' => { label=>'Suspension command',
-                 default=>'',
+                 default=>'usermod -L $username',
                },
   'suspend_stdin' => { label=>'Suspension command STDIN',
                        default=>'',
                      },
   'unsuspend' => { label=>'Unsuspension command',
-                   default=>'',
+                   default=>'usermod -U $username',
                  },
   'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
                          default=>'',
@@ -681,7 +681,7 @@ tie my %router_options, 'Tie::IxHash',
 ;
 
 tie my %domain_shellcommands_options, 'Tie::IxHash',
-  'user' => { lable=>'Remote username', default=>'root' },
+  'user' => { label=>'Remote username', default=>'root' },
   'useradd' => { label=>'Insert command',
                  default=>'',
                },
@@ -702,12 +702,20 @@ tie my %sqlradius_options, 'Tie::IxHash',
   'datasrc'  => { label=>'DBI data source ' },
   'username' => { label=>'Database username' },
   'password' => { label=>'Database password' },
+  'ignore_accounting' => {
+     type => 'checkbox',
+     label=>'Ignore accounting records from this database'
+  },
 ;
 
 tie my %sqlradius_withdomain_options, 'Tie::IxHash',
   'datasrc'  => { label=>'DBI data source ' },
   'username' => { label=>'Database username' },
   'password' => { label=>'Database password' },
+  'ignore_accounting' => {
+     type => 'checkbox',
+     label=>'Ignore accounting records from this database'
+  },
 ;
 
 tie my %cyrus_options, 'Tie::IxHash',
@@ -741,6 +749,41 @@ tie my %vpopmail_options, 'Tie::IxHash',
                },
 ;
 
+tie my %communigate_pro_options, 'Tie::IxHash',
+  'port'     => { label=>'Port number', default=>'106', },
+  'login'    => { label=>'The administrator account name.  The name can contain a domain part.', },
+  'password' => { label=>'The administrator account password.', },
+  'accountType' => { label=>'Type for newly-created accounts',
+                     type=>'select',
+                     options=>[qw( MultiMailbox TextMailbox MailDirMailbox )],
+                     default=>'MultiMailbox',
+                   },
+  'externalFlag' => { label=> 'Create accounts with an external (visible for legacy mailers) INBOX.',
+                      type=>'checkbox',
+                    },
+  'AccessModes' => { label=>'Access modes',
+                     default=>'Mail POP IMAP PWD WebMail WebSite',
+                   },
+;
+
+tie my %communigate_pro_singledomain_options, 'Tie::IxHash',
+  'port'     => { label=>'Port number', default=>'106', },
+  'login'    => { label=>'The administrator account name.  The name can contain a domain part.', },
+  'password' => { label=>'The administrator account password.', },
+  'domain'   => { label=>'Domain', },
+  'accountType' => { label=>'Type for newly-created accounts',
+                     type=>'select',
+                     options=>[qw( MultiMailbox TextMailbox MailDirMailbox )],
+                     default=>'MultiMailbox',
+                   },
+  'externalFlag' => { label=> 'Create accounts with an external (visible for legacy mailers) INBOX.',
+                      type=>'checkbox',
+                    },
+  'AccessModes' => { label=>'Access modes',
+                     default=>'Mail POP IMAP PWD WebMail WebSite',
+                   },
+;
+
 tie my %bind_options, 'Tie::IxHash',
   #'machine'     => { label=>'named machine' },
   'named_conf'   => { label  => 'named.conf location',
@@ -852,7 +895,7 @@ tie my %ldap_options, 'Tie::IxHash',
 ;
 
 tie my %forward_shellcommands_options, 'Tie::IxHash',
-  'user' => { lable=>'Remote username', default=>'root' },
+  'user' => { label=>'Remote username', default=>'root' },
   'useradd' => { label=>'Insert command',
                  default=>'',
                },
@@ -864,6 +907,13 @@ tie my %forward_shellcommands_options, 'Tie::IxHash',
                 },
 ;
 
+tie my %postfix_options, 'Tie::IxHash',
+  'user' => { label=>'Remote username', default=>'root' },
+  'aliases' => { label=>'aliases file location', default=>'/etc/aliases' },
+  'virtual' => { label=>'virtual file location', default=>'/etc/postfix/virtual' },
+  'mydomain' => { label=>'local domain', default=>'' },
+;
+
 #export names cannot have dashes...
 %exports = (
   'svc_acct' => {
@@ -896,7 +946,7 @@ tie my %forward_shellcommands_options, 'Tie::IxHash',
       'desc' => 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
       'options' => \%shellcommands_options,
       'nodomain' => 'Y',
-      'notes' => 'Run remote commands via SSH.  Usernames are considered unique (also see shellcommands_withdomain).  You probably want this if the commands you are running will not accept a domain as a parameter.  You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="Linux/NetBSD" onClick=\'this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username"; this.form.useradd_stdin.value = ""; this.form.userdel.value = "userdel -r $username"; this.form.userdel_stdin.value=""; this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username"; this.form.usermod_stdin.value = "";\'><LI><INPUT TYPE="button" VALUE="FreeBSD" onClick=\'this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0"; this.form.useradd_stdin.value = "$_password\n"; this.form.userdel.value = "pw userdel $username -r"; this.form.userdel_stdin.value=""; this.form.usermod.value = "pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -c $new_finger -h 0"; this.form.usermod_stdin.value = "$new__password\n";\'><LI><INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick=\'this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = ""; this.form.usermod.value = "[ -d $old_dir ] && mv $old_dir $new_dir || ( chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; find . -depth -print | cpio -pdm $new_dir; chmod u-t $new_dir; chown -R $new_uid.$new_gid $new_dir; rm -rf $old_dir )"; this.form.usermod_stdin.value = ""; this.form.userdel.value = "rm -rf $dir"; this.form.userdel_stdin.value="";\'></UL>The following variables are available for interpolation (prefixed with new_ or old_ for replace operations): <UL><LI><code>$username</code><LI><code>$_password</code><LI><code>$quoted_password</code> - unencrypted password quoted for the shell<LI><code>$crypt_password</code> - encrypted password<LI><code>$uid</code><LI><code>$gid</code><LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)<LI><code>$dir</code> - home directory<LI><code>$shell</code><LI><code>$quota</code><LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.</UL>',
+      'notes' => 'Run remote commands via SSH.  Usernames are considered unique (also see shellcommands_withdomain).  You probably want this if the commands you are running will not accept a domain as a parameter.  You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="Linux" onClick=\'this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username"; this.form.useradd_stdin.value = ""; this.form.userdel.value = "userdel -r $username"; this.form.userdel_stdin.value=""; this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username"; this.form.usermod_stdin.value = ""; this.form.suspend.value = "usermod -L $username"; this.form.suspend_stdin.value=""; this.form.unsuspend.value = "usermod -U $username"; this.form.unsuspend_stdin.value="";\'><LI><INPUT TYPE="button" VALUE="FreeBSD" onClick=\'this.form.useradd.value = "lockf /etc/passwd.lock pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0"; this.form.useradd_stdin.value = "$_password\n"; this.form.userdel.value = "lockf /etc/passwd.lock pw userdel $username -r"; this.form.userdel_stdin.value=""; this.form.usermod.value = "lockf /etc/passwd.lock pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -c $new_finger -h 0"; this.form.usermod_stdin.value = "$new__password\n"; this.form.suspend.value = "lockf /etc/passwd.lock pw lock $username"; this.form.suspend_stdin.value=""; this.form.unsuspend.value = "lockf /etc/passwd.lock pw unlock $username"; this.form.unsuspend_stdin.value="";\'> Note: On FreeBSD, due to deficient locking in pw(1), you must disable the chpass(1), chsh(1), chfn(1), passwd(1), and vipw(1) commands, or replace them with wrappers that prepend "lockf /etc/passwd.lock".  Alternatively, apply the patch in <A HREF="http://www.freebsd.org/cgi/query-pr.cgi?pr=23501">FreeBSD PR#23501</A> and remove the "lockf /etc/passwd.lock" from these default commands.<LI><INPUT TYPE="button" VALUE="NetBSD/OpenBSD" onClick=\'this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username"; this.form.useradd_stdin.value = ""; this.form.userdel.value = "userdel -r $username"; this.form.userdel_stdin.value=""; this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username"; this.form.usermod_stdin.value = ""; this.form.suspend.value = ""; this.form.suspend_stdin.value=""; this.form.unsuspend.value = ""; this.form.unsuspend_stdin.value="";\'><LI><INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick=\'this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = ""; this.form.usermod.value = "[ -d $old_dir ] && mv $old_dir $new_dir || ( chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; find . -depth -print | cpio -pdm $new_dir; chmod u-t $new_dir; chown -R $new_uid.$new_gid $new_dir; rm -rf $old_dir )"; this.form.usermod_stdin.value = ""; this.form.userdel.value = "rm -rf $dir"; this.form.userdel_stdin.value=""; this.form.suspend.value = ""; this.form.suspend_stdin.value=""; this.form.unsuspend.value = ""; this.form.unsuspend_stdin.value="";\'></UL>The following variables are available for interpolation (prefixed with new_ or old_ for replace operations): <UL><LI><code>$username</code><LI><code>$_password</code><LI><code>$quoted_password</code> - unencrypted password quoted for the shell<LI><code>$crypt_password</code> - encrypted password<LI><code>$uid</code><LI><code>$gid</code><LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)<LI><code>$dir</code> - home directory<LI><code>$shell</code><LI><code>$quota</code><LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.</UL>',
     },
 
     'shellcommands_withdomain' => {
@@ -912,17 +962,17 @@ tie my %forward_shellcommands_options, 'Tie::IxHash',
     },
 
     'sqlradius' => {
-      'desc' => 'Real-time export to SQL-backed RADIUS (ICRADIUS, FreeRADIUS)',
+      'desc' => 'Real-time export to SQL-backed RADIUS (FreeRADIUS, ICRADIUS, Radiator)',
       'options' => \%sqlradius_options,
       'nodomain' => 'Y',
-      'notes' => 'Real-time export of radcheck, radreply and usergroup tables to any SQL database for <a href="http://www.freeradius.org/">FreeRADIUS</a> or <a href="http://radius.innercite.com/">ICRADIUS</a>.  This export does not export RADIUS realms (see also sqlradius_withdomain).  An existing RADIUS database will be updated in realtime, but you can use <a href="../docs/man/bin/freeside-sqlradius-reset">freeside-sqlradius-reset</a> to delete the entire RADIUS database and repopulate the tables from the Freeside database.  See the <a href="http://search.cpan.org/doc/TIMB/DBI/DBI.pm">DBI documentation</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">documentation for your DBD</a> for the exact syntax of a DBI data source.',
+      'notes' => 'Real-time export of radcheck, radreply and usergroup tables to any SQL database for <a href="http://www.freeradius.org/">FreeRADIUS</a>, <a href="http://radius.innercite.com/">ICRADIUS</a> or <a href="http://www.open.com.au/radiator/">Radiator</a>.  This export does not export RADIUS realms (see also sqlradius_withdomain).  An existing RADIUS database will be updated in realtime, but you can use <a href="../docs/man/bin/freeside-sqlradius-reset">freeside-sqlradius-reset</a> to delete the entire RADIUS database and repopulate the tables from the Freeside database.  See the <a href="http://search.cpan.org/doc/TIMB/DBI/DBI.pm#connect">DBI documentation</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">documentation for your DBD</a> for the exact syntax of a DBI data source.<ul><li>Using FreeRADIUS 0.9.0 with the PostgreSQL backend, the db_postgresql.sql schema and postgresql.conf queries contain incompatible changes.  This is fixed in 0.9.1.  Only new installs with 0.9.0 and PostgreSQL are affected - upgrades and other database backends and versions are unaffected.<li>Using ICRADIUS, add a dummy "op" column to your database: <blockquote><code>ALTER&nbsp;TABLE&nbsp;radcheck&nbsp;ADD&nbsp;COLUMN&nbsp;op&nbsp;VARCHAR(2)&nbsp;NOT&nbsp;NULL&nbsp;DEFAULT&nbsp;\'==\'<br>ALTER&nbsp;TABLE&nbsp;radreply&nbsp;ADD&nbsp;COLUMN&nbsp;op&nbsp;VARCHAR(2)&nbsp;NOT&nbsp;NULL&nbsp;DEFAULT&nbsp;\'==\'<br>ALTER&nbsp;TABLE&nbsp;radgroupcheck&nbsp;ADD&nbsp;COLUMN&nbsp;op&nbsp;VARCHAR(2)&nbsp;NOT&nbsp;NULL&nbsp;DEFAULT&nbsp;\'==\'<br>ALTER&nbsp;TABLE&nbsp;radgroupreply&nbsp;ADD&nbsp;COLUMN&nbsp;op&nbsp;VARCHAR(2)&nbsp;NOT&nbsp;NULL&nbsp;DEFAULT&nbsp;\'==\'</code></blockquote><li>Using Radiator, see the <a href="http://www.open.com.au/radiator/faq.html#38">Radiator FAQ</a> for configuration information.</ul>',
     },
 
     'sqlradius_withdomain' => {
-      'desc' => 'Real-time export to SQL-backed RADIUS (ICRADIUS, FreeRADIUS) with realms',
+      'desc' => 'Real-time export to SQL-backed RADIUS (FreeRADIUS, ICRADIUS, Radiator) with realms',
       'options' => \%sqlradius_withdomain_options,
       'nodomain' => '',
-      'notes' => 'Real-time export of radcheck, radreply and usergroup tables to any SQL database for <a href="http://www.freeradius.org/">FreeRADIUS</a> or <a href="http://radius.innercite.com/">ICRADIUS</a>.  This export exports domains to RADIUS realms (see also sqlradius).  An existing RADIUS database will be updated in realtime, but you can use <a href="../docs/man/bin/freeside-sqlradius-reset">freeside-sqlradius-reset</a> to delete the entire RADIUS database and repopulate the tables from the Freeside database.  See the <a href="http://search.cpan.org/doc/TIMB/DBI/DBI.pm">DBI documentation</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">documentation for your DBD</a> for the exact syntax of a DBI data source.',
+      'notes' => 'Real-time export of radcheck, radreply and usergroup tables to any SQL database for <a href="http://www.freeradius.org/">FreeRADIUS</a>, <a href="http://radius.innercite.com/">ICRADIUS</a> or <a href="http://www.open.com.au/radiator/">Radiator</a>.  This export exports domains to RADIUS realms (see also sqlradius).  An existing RADIUS database will be updated in realtime, but you can use <a href="../docs/man/bin/freeside-sqlradius-reset">freeside-sqlradius-reset</a> to delete the entire RADIUS database and repopulate the tables from the Freeside database.  See the <a href="http://search.cpan.org/doc/TIMB/DBI/DBI.pm#connect">DBI documentation</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">documentation for your DBD</a> for the exact syntax of a DBI data source.<ul><li>Using FreeRADIUS 0.9.0 with the PostgreSQL backend, the db_postgresql.sql schema and postgresql.conf queries contain incompatible changes.  This is fixed in 0.9.1.  Only new installs with 0.9.0 and PostgreSQL are affected - upgrades and other database backends and versions are unaffected.<li>Using ICRADIUS, add a dummy "op" column to your database: <blockquote><code>ALTER&nbsp;TABLE&nbsp;radcheck&nbsp;ADD&nbsp;COLUMN&nbsp;op&nbsp;VARCHAR(2)&nbsp;NOT&nbsp;NULL&nbsp;DEFAULT&nbsp;\'==\'<br>ALTER&nbsp;TABLE&nbsp;radreply&nbsp;ADD&nbsp;COLUMN&nbsp;op&nbsp;VARCHAR(2)&nbsp;NOT&nbsp;NULL&nbsp;DEFAULT&nbsp;\'==\'<br>ALTER&nbsp;TABLE&nbsp;radgroupcheck&nbsp;ADD&nbsp;COLUMN&nbsp;op&nbsp;VARCHAR(2)&nbsp;NOT&nbsp;NULL&nbsp;DEFAULT&nbsp;\'==\'<br>ALTER&nbsp;TABLE&nbsp;radgroupreply&nbsp;ADD&nbsp;COLUMN&nbsp;op&nbsp;VARCHAR(2)&nbsp;NOT&nbsp;NULL&nbsp;DEFAULT&nbsp;\'==\'</code></blockquote><li>Using Radiator, see the <a href="http://www.open.com.au/radiator/faq.html#38">Radiator FAQ</a> for configuration information.</ul>',
     },
 
     'sqlmail' => {
@@ -958,6 +1008,19 @@ tie my %forward_shellcommands_options, 'Tie::IxHash',
       'notes' => 'Real time export to <a href="http://inter7.com/vpopmail/">vpopmail</a> text files.  <a href="http://search.cpan.org/search?dist=File-Rsync">File::Rsync</a> must be installed, and you will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a> to <b>vpopmail</b>@<i>export.host</i>.',
     },
 
+    'communigate_pro' => {
+      'desc' => 'Real-time export to a CommuniGate Pro mail server',
+      'options' => \%communigate_pro_options,
+      'notes' => 'Real time export to a <a href="http://www.stalker.com/CommuniGatePro/">CommuniGate Pro</a> mail server.  The <a href="http://www.stalker.com/CGPerl/">CommuniGate Pro Perl Interface</a> must be installed as CGP::CLI.',
+    },
+
+    'communigate_pro_singledomain' => {
+      'desc' => 'Real-time export to a CommuniGate Pro mail server, one domain only',
+      'options' => \%communigate_pro_singledomain_options,
+      'nodomain' => 'Y',
+      'notes' => 'Real time export to a <a href="http://www.stalker.com/CommuniGatePro/">CommuniGate Pro</a> mail server.  This is an unusual export to CommuniGate Pro that forces all accounts into a single domain.  As CommuniGate Pro supports multiple domains, unless you have a specific reason for using this export, you probably want to use the communigate_pro export instead.  The <a href="http://www.stalker.com/CGPerl/">CommuniGate Pro Perl Interface</a> must be installed as CGP::CLI.',
+    },
+
   },
 
   'svc_domain' => {
@@ -1001,7 +1064,7 @@ tie my %forward_shellcommands_options, 'Tie::IxHash',
       'desc' => 'Real-time export to SQL-backed mail server',
       'options' => \%sqlmail_options,
       #'nodomain' => 'Y',
-      'notes' => 'Database schema can be made to work with Courier IMAP and Exim.  Others could work but are untested. (...extended description from pc-intouch?...)',
+      'notes' => 'Database schema can be made to work with Courier IMAP and Exim.  Others could work but are untested. (...extended description from fire2wire?...)',
     },
 
     'forward_shellcommands' => {
@@ -1009,6 +1072,14 @@ tie my %forward_shellcommands_options, 'Tie::IxHash',
       'options' => \%forward_shellcommands_options,
       'notes' => 'Run remote commands via SSH, for forwards.  You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="text vpopmail maintenance" onClick=\'this.form.useradd.value = "[ -d /home/vpopmail/domains/$domain/$username ] && { echo \"$destination\" > /home/vpopmail/domains/$domain/$username/.qmail; chown vpopmail:vchkpw /home/vpopmail/domains/$domain/$username/.qmail; }"; this.form.userdel.value = "rm /home/vpopmail/domains/$domain/$username/.qmail"; this.form.usermod.value = "mv /home/vpopmail/domains/$old_domain/$old_username/.qmail /home/vpopmail/domains/$new_domain/$new_username; [ \"$old_destination\" != \"$new_destination\" ] && { echo \"$new_destination\" > /home/vpopmail/domains/$new_domain/$new_username/.qmail; chown vpopmail:vchkpw /home/vpopmail/domains/$new_domain/$new_username/.qmail; }";\'></UL>The following variables are available for interpolation (prefixed with <code>new_</code> or <code>old_</code> for replace operations): <UL><LI><code>$username</code><LI><code>$domain</code><LI><code>$destination</code> - forward destination<LI>All other fields in <a href="../docs/schema.html#svc_forward">svc_forward</a> are also available.</UL>',
     },
+
+    'postfix' => {
+      'desc' => 'Real-time export to Postfix text files',
+      'options' => \%postfix_options,
+      #'nodomain' => 'Y',
+      'notes' => 'Batch export of Postfix aliases and virtual files.  <a href="http://search.cpan.org/search?dist=File-Rsync">File::Rsync</a> must be installed.  Run bin/postfix.export to export the files.',
+    },
+
   },
 
   'svc_www' => {
@@ -1032,6 +1103,10 @@ tie my %forward_shellcommands_options, 'Tie::IxHash',
       'notes' => '',
     },
   },
+
+  'svc_external' => {
+  },
+
 );
 
 =back
diff --git a/FS/FS/part_export/communigate_pro.pm b/FS/FS/part_export/communigate_pro.pm
new file mode 100644 (file)
index 0000000..557aad9
--- /dev/null
@@ -0,0 +1,144 @@
+package FS::part_export::communigate_pro;
+
+use vars qw(@ISA);
+use FS::part_export;
+use FS::queue;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub export_username {
+  my($self, $svc_acct) = (shift, shift);
+  $svc_acct->email;
+}
+
+sub _export_insert {
+  my( $self, $svc_acct ) = (shift, shift);
+  my @options = ( $svc_acct->svcnum, 'CreateAccount',
+    'accountName'    => $self->export_username($svc_acct),
+    'accountType'    => $self->option('accountType'),
+    'AccessModes'    => $self->option('AccessModes'),
+    'RealName'       => $svc_acct->finger,
+    'Password'       => $svc_acct->_password,
+  );
+  push @options, 'MaxAccountSize' => $svc_acct->quota if $svc_acct->quota;
+  push @options, 'externalFlag'   => $self->option('externalFlag')
+    if $self->option('externalFlag');
+
+  $self->communigate_pro_queue( @options );
+}
+
+sub _export_replace {
+  my( $self, $new, $old ) = (shift, shift, shift);
+  return "can't (yet) change username with CommuniGate Pro"
+    if $old->username ne $new->username;
+  return "can't (yet) change domain with CommuniGate Pro"
+    if $self->export_username($old) ne $self->export_username($new);
+  return "can't (yet) change GECOS with CommuniGate Pro"
+    if $old->finger ne $new->finger;
+  return "can't (yet) change quota with CommuniGate Pro"
+    if $old->quota ne $new->quota;
+  return '' unless $old->username ne $new->username
+                || $old->_password ne $new->_password
+                || $old->finger ne $new->finger
+                || $old->quota ne $new->quota;
+
+  return '' if '*SUSPENDED* '. $old->_password eq $new->_password;
+
+  #my $err_or_queue = $self->communigate_pro_queue( $new->svcnum,'RenameAccount',
+  #  $old->email, $new->email );
+  #return $err_or_queue unless ref($err_or_queue);
+  #my $jobnum = $err_or_queue->jobnum;
+
+  $self->communigate_pro_queue( $new->svcnum, 'SetAccountPassword',
+                                $self->export_username($new), $new->_password        )
+    if $new->_password ne $old->_password;
+
+}
+
+sub _export_delete {
+  my( $self, $svc_acct ) = (shift, shift);
+  $self->communigate_pro_queue( $svc_acct->svcnum, 'DeleteAccount',
+    $self->export_username($svc_acct),
+  );
+}
+
+sub _export_suspend {
+  my( $self, $svc_acct ) = (shift, shift);
+  $self->communigate_pro_queue( $svc_acct->svcnum, 'UpdateAccountSettings',
+    'accountName' => $self->export_username($svc_acct),
+    'AccessModes' => 'Mail',
+  );
+}
+
+sub _export_unsuspend {
+  my( $self, $svc_acct ) = (shift, shift);
+  $self->communigate_pro_queue( $svc_acct->svcnum, 'UpdateAccountSettings',
+    'accountName' => $self->export_username($svc_acct),
+    'AccessModes' => $self->option('AccessModes'),
+  );
+}
+
+sub communigate_pro_queue {
+  my( $self, $svcnum, $method ) = (shift, shift, shift);
+  my @kludge_methods = qw(CreateAccount UpdateAccountSettings);
+  my $sub = 'communigate_pro_command';
+  $sub = $method if grep { $method eq $_ } @kludge_methods;
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::communigate_pro::$sub",
+  };
+  $queue->insert(
+    $self->machine,
+    $self->option('port'),
+    $self->option('login'),
+    $self->option('password'),
+    $method,
+    @_,
+  );
+
+}
+
+sub CreateAccount {
+  my( $machine, $port, $login, $password, $method, %args ) = @_;
+  my $accountName  = delete $args{'accountName'};
+  my $accountType  = delete $args{'accountType'};
+  my $externalFlag = delete $args{'externalFlag'};
+  $args{'AccessModes'} = [ split(' ', $args{'AccessModes'}) ];
+  my @args = ( accountName => $accountName,
+               accountType  => $accountType,
+               settings     => \%args,
+             );
+               #externalFlag => $externalFlag,
+  push @args, externalFlag => $externalFlag if $externalFlag;
+
+  communigate_pro_command( $machine, $port, $login, $password, $method, @args );
+
+}
+
+sub UpdateAccountSettings {
+  my( $machine, $port, $login, $password, $method, %args ) = @_;
+  my $accountName  = delete $args{'accountName'};
+  $args{'AccessModes'} = [ split(' ', $args{'AccessModes'}) ];
+  @args = ( $accountName, \%args );
+  communigate_pro_command( $machine, $port, $login, $password, $method, @args );
+}
+
+sub communigate_pro_command { #subroutine, not method
+  my( $machine, $port, $login, $password, $method, @args ) = @_;
+
+  eval "use CGP::CLI";
+
+  my $cli = new CGP::CLI( {
+    'PeerAddr' => $machine,
+    'PeerPort' => $port,
+    'login'    => $login,
+    'password' => $password,
+  } ) or die "Can't login to CGPro: $CGP::ERR_STRING\n";
+
+  $cli->$method(@args) or die "CGPro error: ". $cli->getErrMessage;
+
+  $cli->Logout or die "Can't logout of CGPro: $CGP::ERR_STRING\n";
+
+}
diff --git a/FS/FS/part_export/communigate_pro_singledomain.pm b/FS/FS/part_export/communigate_pro_singledomain.pm
new file mode 100644 (file)
index 0000000..11574af
--- /dev/null
@@ -0,0 +1,11 @@
+package FS::part_export::communigate_pro_singledomain;
+
+use vars qw(@ISA);
+use FS::part_export::communigate_pro;
+
+@ISA = qw(FS::part_export::communigate_pro);
+
+sub export_username {
+  my($self, $svc_acct) = (shift, shift);
+  $svc_acct->username. '@'. $self->option('domain');
+}
index cf56033..d295eec 100644 (file)
@@ -97,7 +97,7 @@ sub shellcommands_queue {
 }
 
 sub ssh_cmd { #subroutine, not method
-  use Net::SSH '0.07';
+  use Net::SSH '0.08';
   &Net::SSH::ssh_cmd( { @_ } );
 }
 
index f6fcb60..5d31457 100644 (file)
@@ -97,7 +97,7 @@ sub shellcommands_queue {
 }
 
 sub ssh_cmd { #subroutine, not method
-  use Net::SSH '0.07';
+  use Net::SSH '0.08';
   &Net::SSH::ssh_cmd( { @_ } );
 }
 
diff --git a/FS/FS/part_export/postfix.pm b/FS/FS/part_export/postfix.pm
new file mode 100644 (file)
index 0000000..6d5e449
--- /dev/null
@@ -0,0 +1,7 @@
+package FS::part_export::postfix;
+
+use vars qw(@ISA);
+use FS::part_export::null;
+
+@ISA = qw(FS::part_export::null);
+
diff --git a/FS/FS/part_export/router.pm b/FS/FS/part_export/router.pm
new file mode 100644 (file)
index 0000000..07b5b9e
--- /dev/null
@@ -0,0 +1,166 @@
+package FS::part_export::router;
+
+=head1 FS::part_export::router
+
+This export connects to a router and transmits commands via telnet or SSH.
+It requires the following custom router fields:
+
+=over 4
+
+=item admin_address - IP address (or hostname) to connect
+
+=item admin_user - username for admin access
+
+=item admin_password - password for admin access
+
+=back
+
+The export itself needs the following options:
+
+=over 4
+
+=item insert, replace, delete - command strings (to be interpolated)
+
+=item Prompt - prompt string to expect from router after successful login
+
+=item Timeout - time to wait for prompt string
+
+=back
+
+(Prompt and Timeout are required only for telnet connections.)
+
+=cut
+
+use vars qw(@ISA @saltset);
+use String::ShellQuote;
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my($self) = shift;
+  $self->_export_command('insert', @_);
+}
+
+sub _export_delete {
+  my($self) = shift;
+  $self->_export_command('delete', @_);
+}
+
+sub _export_suspend {
+  my($self) = shift;
+  $self->_export_command('suspend', @_);
+}
+
+sub _export_unsuspend {
+  my($self) = shift;
+  $self->_export_command('unsuspend', @_);
+}
+
+sub _export_command {
+  my ( $self, $action, $svc_broadband) = (shift, shift, shift);
+  my $command = $self->option($action);
+  return '' if $command =~ /^\s*$/;
+
+  no strict 'vars';
+  {
+    no strict 'refs';
+    ${$_} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
+  }
+  # fetch router info
+  my $router = $svc_broadband->addr_block->router;
+  my %r;
+  $r{$_} = $router->getfield($_) foreach $router->virtual_fields;
+  #warn qq("$command");
+  #warn eval(qq("$command"));
+
+  warn "admin_address: '$r{admin_address}'";
+
+  if ($r{admin_address} ne '') {
+    $self->router_queue( $svc_broadband->svcnum, $self->option('protocol'),
+      user         => $r{admin_user},
+      password     => $r{admin_password},
+      host         => $r{admin_address},
+      Timeout      => $self->option('Timeout'),
+      Prompt       => $self->option('Prompt'),
+      command      => eval(qq("$command")),
+    );
+  } else {
+    return '';
+  }
+}
+
+sub _export_replace {
+
+  # We don't handle the case of a svc_broadband moving between routers.
+  # If you want to do that, reprovision the service.
+
+  my($self, $new, $old ) = (shift, shift, shift);
+  my $command = $self->option('replace');
+  no strict 'vars';
+  {
+    no strict 'refs';
+    ${"old_$_"} = $old->getfield($_) foreach $old->fields;
+    ${"new_$_"} = $new->getfield($_) foreach $new->fields;
+  }
+
+  my $router = $new->addr_block->router;
+  my %r;
+  $r{$_} = $router->getfield($_) foreach $router->virtual_fields;
+
+  if ($r{admin_address} ne '') {
+    $self->router_queue( $new->svcnum, $self->option('protocol'),
+      user         => $r{admin_user},
+      password     => $r{admin_password},
+      host         => $r{admin_address},
+      Timeout      => $self->option('Timeout'),
+      Prompt       => $self->option('Prompt'),
+      command      => eval(qq("$command")),
+    );
+  } else {
+    return '';
+  }
+}
+
+#a good idea to queue anything that could fail or take any time
+sub router_queue {
+  #warn join ':', @_;
+  my( $self, $svcnum, $protocol ) = (shift, shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+  };
+  $queue->job ("FS::part_export::router::".$protocol."_cmd");
+  $queue->insert( @_ );
+}
+
+sub ssh_cmd { #subroutine, not method
+  use Net::SSH '0.08';
+  &Net::SSH::ssh_cmd( { @_ } );
+}
+
+sub telnet_cmd {
+  use Net::Telnet;
+
+  warn join(', ', @_);
+
+  my %arg = @_;
+
+  my $t = new Net::Telnet (Timeout => $arg{Timeout},
+                           Prompt  => $arg{Prompt});
+  $t->open($arg{host});
+  $t->login($arg{user}, $arg{password});
+  my @error = $t->cmd($arg{command});
+  die @error if (grep /^ERROR/, @error);
+}
+
+#sub router_insert { #subroutine, not method
+#}
+#sub router_replace { #subroutine, not method
+#}
+#sub router_delete { #subroutine, not method
+#}
+
index edc9440..db2e7aa 100644 (file)
@@ -40,6 +40,13 @@ sub _export_command {
   {
     no strict 'refs';
     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
+
+    my $count = 1;
+    foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
+      ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
+        foreach qw( machine username _password );
+      $count++;
+    }
   }
 
   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
@@ -90,6 +97,12 @@ sub _export_replace {
     if ( $old_domain ne $new_domain ) {
       $error ||= "can't change domain";
     }
+    if ( $old_uid != $new_uid ) {
+      $error ||= "can't change uid";
+    }
+    if ( $old_dir ne $new_dir ) {
+      $error ||= "can't change dir";
+    }
     return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
       if $error;
   }
@@ -112,7 +125,7 @@ sub shellcommands_queue {
 }
 
 sub ssh_cmd { #subroutine, not method
-  use Net::SSH '0.07';
+  use Net::SSH '0.08';
   &Net::SSH::ssh_cmd( { @_ } );
 }
 
index 20658c7..3e00874 100644 (file)
@@ -96,7 +96,7 @@ sub shellcommands_queue {
 }
 
 sub ssh_cmd { #subroutine, not method
-  use Net::SSH '0.07';
+  use Net::SSH '0.08';
   &Net::SSH::ssh_cmd( { @_ } );
 }
 
index 12ee804..dcce66b 100644 (file)
@@ -2,7 +2,7 @@ package FS::part_pkg;
 
 use strict;
 use vars qw( @ISA );
-use FS::Record qw( qsearch dbh );
+use FS::Record qw( qsearch dbh dbdef );
 use FS::pkg_svc;
 use FS::agent_type;
 use FS::type_pkgs;
@@ -229,11 +229,19 @@ sub check {
 
   }
 
+  if ( $self->dbdef_table->column('freq')->type =~ /(int)/i ) {
+    my $error = $self->ut_number('freq');
+    return $error if $error;
+  } else {
+    $self->freq =~ /^(\d+[dw]?)$/
+      or return "Illegal or empty freq: ". $self->freq;
+    $self->freq($1);
+  }
+
     $self->ut_numbern('pkgpart')
       || $self->ut_text('pkg')
       || $self->ut_text('comment')
       || $self->ut_anything('setup')
-      || $self->ut_number('freq')
       || $self->ut_anything('recur')
       || $self->ut_alphan('plan')
       || $self->ut_anything('plandata')
@@ -259,20 +267,24 @@ sub pkg_svc {
 
 =item svcpart [ SVCDB ]
 
-Returns the svcpart of a single service definition (see L<FS::part_svc>)
+Returns the svcpart of the primary service definition (see L<FS::part_svc>)
 associated with this billing item definition (see L<FS::pkg_svc>).  Returns
-false if there not exactly one service definition with quantity 1, or if 
-SVCDB is specified and does not match the svcdb of the service definition, 
+false if there not a primary service definition or exactly one service
+definition with quantity 1, or if SVCDB is specified and does not match the
+svcdb of the service definition, 
 
 =cut
 
 sub svcpart {
   my $self = shift;
   my $svcdb = scalar(@_) ? shift : '';
-  my @pkg_svc = grep {
-    $_->quantity == 1
-    && ( $svcdb eq $_->part_svc->svcdb || !$svcdb )
-  } $self->pkg_svc;
+  my @svcdb_pkg_svc =
+    grep { ( $svcdb eq $_->part_svc->svcdb || !$svcdb ) } $self->pkg_svc;
+  my @pkg_svc = ();
+  @pkg_svc = grep { $_->primary_svc =~ /^Y/i } @svcdb_pkg_svc
+    if dbdef->table('pkg_svc')->column('primary_svc');
+  @pkg_svc = grep {$_->quantity == 1 } @svcdb_pkg_svc
+    unless @pkg_svc;
   return '' if scalar(@pkg_svc) != 1;
   $pkg_svc[0]->svcpart;
 }
index f30ddad..c0858c0 100644 (file)
@@ -38,6 +38,8 @@ The following fields are currently supported:
 
 =item referral - Text name of this advertising source
 
+=item disabled - Disabled flag, empty or 'Y'
+
 =back
 
 =head1 NOTE
@@ -91,10 +93,17 @@ replace methods.
 sub check {
   my $self = shift;
 
-  $self->ut_numbern('refnum')
+  my $error = $self->ut_numbern('refnum')
     || $self->ut_text('referral')
-    || $self->SUPER::check
   ;
+  return $error if $error;
+
+  if ( $self->dbdef_table->column('disabled') ) {
+    $error = $self->ut_enum('disabled', [ '', 'Y' ] );
+    return $error if $error;
+  }
+
+  $self->SUPER::check;
 }
 
 =back
index 2ac1a55..ea52176 100644 (file)
@@ -46,6 +46,8 @@ FS::Record.  The following fields are currently supported:
 =item quantity - Quantity of this service definition that this billing item
 definition includes
 
+=item primary_svc - primary flag, empty or 'Y'
+
 =back
 
 =head1 METHODS
@@ -108,6 +110,11 @@ sub check {
   return "Unknown pkgpart!" unless $self->part_pkg;
   return "Unknown svcpart!" unless $self->part_svc;
 
+  if ( $self->dbdef_table->column('primary_svc') ) {
+    $error = $self->ut_enum('primary_svc', [ '', 'Y' ] );
+    return $error if $error;
+  }
+
   $self->SUPER::check;
 }
 
@@ -135,10 +142,6 @@ sub part_svc {
 
 =back
 
-=head1 VERSION
-
-$Id: pkg_svc.pm,v 1.4 2003-08-05 00:20:45 khoff Exp $
-
 =head1 BUGS
 
 =head1 SEE ALSO
index a35a757..efeb739 100644 (file)
@@ -4,1099 +4,1592 @@ use vars qw(%attrib);
 %attrib = (
   'usr_at_zip_output_filter' => 'USR-AT-Zip-Output-Filter',
   'ms_filter' => 'MS-Filter',
+  'annex_compression_protoc' => 'Annex-Compression-Protocol',
+  'xedia_ssh_privileges' => 'Xedia-SSH-Privileges',
   'usr_blocks_received' => 'USR-Blocks-Received',
   'shiva_called_number' => 'Shiva-Called-Number',
   'annex_filter' => 'Annex-Filter',
   'usr_channel_expansion' => 'USR-Channel-Expansion',
+  'erx_tunnel_tos' => 'ERX-Tunnel-Tos',
   'session_timeout' => 'Session-Timeout',
-  'usr_simplified_mnp_level' => 'USR-Simplified-MNP-Levels',
   'ascend_route_ipx' => 'Ascend-Route-IPX',
-  'annex_user_server_locati' => 'Annex-User-Server-Location',
+  'annex_error_correction_p' => 'Annex-Error-Correction-Prot',
   'acc_callback_mode' => 'Acc-Callback-Mode',
   'usr_filter_zones' => 'USR-Filter-Zones',
+  'erx_input_gigapkts' => 'ERX-Input-Gigapkts',
   'ascend_session_svr_key' => 'Ascend-Session-Svr-Key',
-  'le_nat_tcp_session_timeo' => 'LE-NAT-TCP-Session-Timeout',
+  'bind_l2tp_tunnel_namf' => 'Bind_L2TP_Tunnel_Name',
+  'ascend_dsl_cir_recv_limi' => 'Ascend-Dsl-CIR-Recv-Limit',
+  'altiga_secondary_wins_g' => 'Altiga-Secondary-WINS-G',
   'ascend_ts_idle_limit' => 'Ascend-TS-Idle-Limit',
   'usr_port_tap_priority' => 'USR-Port-Tap-Priority',
+  'cvpn3000_ipsec_client_fw' => 'CVPN3000-IPSec-Client-Fw-Filter-Name',
+  'ascend_private_route_req' => 'Ascend-Private-Route-Required',
   'ascend_private_route' => 'Ascend-Private-Route',
   'prompt' => 'Prompt',
   'acct_link_count' => 'Acct-Link-Count',
+  'bind_auth_service_grq' => 'Bind_Auth_Service_Grp',
+  'itk_tunnel_ip' => 'ITK-Tunnel-IP',
   'login_lat_node' => 'Login-LAT-Node',
   'usr_mbi_ct_pri_card_slot' => 'USR-Mbi_Ct_PRI_Card_Slot',
+  'lac_real_poru' => 'LAC_Real_Port',
   'erx_ingress_statistics' => 'ERX-Ingress-Statistics',
+  'digest_nonce' => 'Digest-Nonce',
   'annex_system_disc_reason' => 'Annex-System-Disc-Reason',
+  'pool_name' => 'Pool-Name',
+  'altiga_use_client_addres' => 'Altiga-Use-Client-Address-G/U',
+  'police_bursu' => 'Police_Burst',
   'usr_call_arrival_time' => 'USR-Call-Arrival-Time',
   'ascend_disconnect_cause' => 'Ascend-Disconnect-Cause',
   'ascend_user_acct_time' => 'Ascend-User-Acct-Time',
-  'ascend_appletalk_peer_mo' => 'Ascend-Appletalk-Peer-Mode',
   'chap_challenge' => 'CHAP-Challenge',
   'ascend_mpp_idle_percent' => 'Ascend-MPP-Idle-Percent',
   'ascend_user_acct_port' => 'Ascend-User-Acct-Port',
+  'ldap_group' => 'Ldap-Group',
   'ascend_numbering_plan_id' => 'Ascend-Numbering-Plan-ID',
-  'ascend_access_intercept_' => 'Ascend-Access-Intercept-LEA',
-  'pvc_encapsulation_type' => 'PVC_Encapsulation_Type',
+  'usr_last_number_dialed_o' => 'USR-Last-Number-Dialed-Out',
+  'pvc_encapsulation_type' => 'PVC-Encapsulation-Type',
   'ascend_bir_bridge_group' => 'Ascend-BIR-Bridge-Group',
   'ascend_atm_group' => 'Ascend-ATM-Group',
   'ascend_fr_svc_addr' => 'Ascend-FR-SVC-Addr',
   'x_ascend_send_auth' => 'X-Ascend-Send-Auth',
   'le_ip_pool' => 'LE-IP-Pool',
-  'annex_addr_resolution_se' => 'Annex-Addr-Resolution-Servers',
-  'usr_last_callers_number_' => 'USR-Last-Callers-Number-ANI',
+  'post_proxy_type' => 'Post-Proxy-Type',
+  'wispr_session_terminate_' => 'WISPr-Session-Terminate-Time',
+  'bintec_pppextiftable' => 'BinTec-pppExtIfTable',
+  'nomadix_subnet' => 'Nomadix-Subnet',
   'login_port' => 'Login-Port',
   'ms_chap2_response' => 'MS-CHAP2-Response',
-  'annex_secondary_dns_serv' => 'Annex-Secondary-DNS-Server',
   'ascend_ipsec_profile' => 'Ascend-IPSEC-Profile',
+  'usr_compression_algorith' => 'USR-Compression-Algorithm',
   'usr_accm_type' => 'USR-ACCM-Type',
   'simultaneous_use' => 'Simultaneous-Use',
+  'cisco_account_info' => 'Cisco-Account-Info',
   'framed_protocol' => 'Framed-Protocol',
+  'erx_tunnel_maximum_sessi' => 'ERX-Tunnel-Maximum-Sessions',
+  'redcreek_tunneled_wins_t' => 'RedCreek-Tunneled-WINS-Server2',
   'ascend_recv_name' => 'Ascend-Recv-Name',
   'usr_call_connecting_time' => 'USR-Call-Connecting-Time',
-  'tunnel_remote_name' => 'Tunnel_Remote_Name',
+  'quintum_h323_gw_id' => 'Quintum-h323-gw-id',
+  'acct_dyn_ac_ent' => 'Acct-Dyn-Ac-Ent',
+  'tunnel_remote_name' => 'Tunnel-Remote-Name',
+  'annex_ppp_trace_level' => 'Annex-PPP-Trace-Level',
+  'cisco_call_type' => 'Cisco-Call-Type',
+  'cisco_fax_recipient_coun' => 'Cisco-Fax-Recipient-Count',
+  'altiga_ipsec_authenticat' => 'Altiga-IPSec-Authentication-G',
+  'wispr_location_id' => 'WISPr-Location-ID',
+  'itk_start_delay' => 'ITK-Start-Delay',
+  'ascend_pre_output_packet' => 'Ascend-Pre-Output-Packets',
+  'usr_rmmie_firmware_versi' => 'USR-RMMIE-Firmware-Version',
   'usr_vts_session_key' => 'USR-VTS-Session-Key',
   'ascend_fr_dce_n393' => 'Ascend-FR-DCE-N393',
   'login_host' => 'Login-Host',
   'usr_reply_script3' => 'USR-Reply-Script3',
+  'cvpn3000_ipsec_split_tuo' => 'CVPN3000-IPSec-Split-Tunneling-Policy',
   'ascend_pppoe_enable' => 'Ascend-PPPoE-Enable',
   'annex_primary_dns_server' => 'Annex-Primary-DNS-Server',
   'x_ascend_bridge_address' => 'X-Ascend-Bridge-Address',
   'usr_number_of_link_naks' => 'USR-Number-of-Link-NAKs',
+  'altiga_priority_on_sep_g' => 'Altiga-Priority-on-SEP-G/U',
   'annex_cli_command' => 'Annex-CLI-Command',
   'usr_pw_framed_routing_v2' => 'USR-PW_Framed_Routing_V2',
-  'usr_tunnel_switch_endpoi' => 'USR-Tunnel-Switch-Endpoint',
+  'session_error_codf' => 'Session_Error_Code',
+  'annex_user_server_locati' => 'Annex-User-Server-Location',
+  'cisco_fax_mdn_address' => 'Cisco-Fax-Mdn-Address',
+  'ascend_calling_subaddres' => 'Ascend-Calling-Subaddress',
   'ascend_call_by_call' => 'Ascend-Call-By-Call',
   'ascend_first_dest' => 'Ascend-First-Dest',
-  'usr_appletalk_network_ra' => 'USR-Appletalk-Network-Range',
   'annex_tunnel_authen_type' => 'Annex-Tunnel-Authen-Type',
+  'acct_type' => 'Acct-Type',
   'sql_user_name' => 'SQL-User-Name',
   'erx_secondary_dns' => 'ERX-Secondary-Dns',
+  'bridge_grouq' => 'Bridge_Group',
   'h323_return_code' => 'h323-return-code',
   'annex_host_allow' => 'Annex-Host-Allow',
+  'cvx_modem_end_recv_line_' => 'CVX-Modem-End-Recv-Line-Lvl',
+  'sip_method' => 'Sip-Method',
   'x_ascend_require_auth' => 'X-Ascend-Require-Auth',
+  'cvpn3000_sep_card_assign' => 'CVPN3000-SEP-Card-Assignment',
   'le_ipsec_deny_action' => 'LE-IPSec-Deny-Action',
   'annex_edo' => 'Annex-EDO',
   'acct_delay_time' => 'Acct-Delay-Time',
-  'ascend_call_block_durati' => 'Ascend-Call-Block-Duration',
   'login_tcp_port' => 'Login-TCP-Port',
   'ascend_temporary_rtes' => 'Ascend-Temporary-Rtes',
+  'versanet_termination_cau' => 'Versanet-Termination-Cause',
   'ascend_dialed_number' => 'Ascend-Dialed-Number',
-  'x_ascend_dec_channel_cou' => 'X-Ascend-Dec-Channel-Count',
+  'cvpn3000_ipsec_authentic' => 'CVPN3000-IPSec-Authentication',
   'ascend_fr_dlci' => 'Ascend-FR-DLCI',
   'annex_modem_disc_reason' => 'Annex-Modem-Disc-Reason',
   'x_ascend_receive_secret' => 'X-Ascend-Receive-Secret',
+  'usr_ospf_addressless_ind' => 'USR-OSPF-Addressless-Index',
+  'usr_ip_default_route_opt' => 'USR-IP-Default-Route-Option',
   'char_noecho' => 'Char-Noecho',
+  'redcreek_tunneled_search' => 'RedCreek-Tunneled-Search-List',
   'ascend_pri_number_type' => 'Ascend-PRI-Number-Type',
-  'ascend_dsl_upstream_limi' => 'Ascend-Dsl-Upstream-Limit',
+  'aat_ip_tos_apply_to' => 'AAT-IP-TOS-Apply-To',
   'x_ascend_modem_shelfno' => 'X-Ascend-Modem-ShelfNo',
   'prefix' => 'Prefix',
   'usr_rad_dvmrp_metric' => 'USR-Rad-Dvmrp-Metric',
+  'x_ascend_call_attempt_li' => 'X-Ascend-Call-Attempt-Limit',
   'usr_ip_saa_filter' => 'USR-IP-SAA-Filter',
-  'ms_link_utilization_thre' => 'MS-Link-Utilization-Threshold',
+  'itk_prompt' => 'ITK-Prompt',
+  'ascend_port_redir_protoc' => 'Ascend-Port-Redir-Protocol',
+  'cvx_modem_tx_packets' => 'CVX-Modem-Tx-Packets',
+  'usr_tunnel_switch_endpoi' => 'USR-Tunnel-Switch-Endpoint',
   'ascend_home_network_name' => 'Ascend-Home-Network-Name',
   'acc_customer_id' => 'Acc-Customer-Id',
   'message_authenticator' => 'Message-Authenticator',
-  'ascend_secondary_home_ag' => 'Ascend-Secondary-Home-Agent',
-  'x_ascend_pre_output_octe' => 'X-Ascend-Pre-Output-Octets',
+  'cisco_fax_coverpage_flag' => 'Cisco-Fax-Coverpage-Flag',
   'usr_multicast_forwarding' => 'USR-Multicast-Forwarding',
+  'cvpn3000_allow_network_e' => 'CVPN3000-Allow-Network-Extension-Mode',
   'ascend_call_direction' => 'Ascend-Call-Direction',
   'acc_connect_rx_speed' => 'Acc-Connect-Rx-Speed',
   'ascend_force_56' => 'Ascend-Force-56',
+  'st_service_domain' => 'ST-Service-Domain',
   'usr_harc_disconnect_code' => 'USR-HARC-Disconnect-Code',
   'shasta_service_profile' => 'Shasta-Service-Profile',
   'cisco_maximum_time' => 'Cisco-Maximum-Time',
   'usr_tunnel_auth_hostname' => 'USR-Tunnel-Auth-Hostname',
-  'ascend_client_assign_win' => 'Ascend-Client-Assign-WINS',
-  'acc_modem_modulation_typ' => 'Acc-Modem-Modulation-Type',
   'acc_ip_gateway_pri' => 'Acc-Ip-Gateway-Pri',
   'ascend_bridge_address' => 'Ascend-Bridge-Address',
+  'altiga_pptp_min_authenti' => 'Altiga-PPTP-Min-Authentication-G/U',
+  'ns_secondary_wins' => 'NS-Secondary-WINS',
+  'cbbsm_bandwidth' => 'CBBSM-Bandwidth',
   'x_ascend_fr_link_mgt' => 'X-Ascend-FR-Link-Mgt',
+  'altiga_ipsec_banner_g' => 'Altiga-IPSec-Banner-G',
   'ascend_handle_ipx' => 'Ascend-Handle-IPX',
   'ascend_x25_pad_alias_2' => 'Ascend-X25-Pad-Alias-2',
+  'st_policy_name' => 'ST-Policy-Name',
   'ascend_group' => 'Ascend-Group',
   'ascend_dsl_rate_type' => 'Ascend-Dsl-Rate-Type',
+  'tunnel_contexu' => 'Tunnel_Context',
   'ascend_require_auth' => 'Ascend-Require-Auth',
+  'cvx_modem_local_retrains' => 'CVX-Modem-Local-Retrains',
+  'cvpn5000_echo' => 'CVPN5000-Echo',
+  'cvx_secondary_dns' => 'CVX-Secondary-DNS',
   'x_ascend_billing_number' => 'X-Ascend-Billing-Number',
   'usr_orig_nas_type' => 'USR-Orig-NAS-Type',
   'ascend_remote_fw' => 'Ascend-Remote-FW',
   'acct_output_packets' => 'Acct-Output-Packets',
   'lm_password' => 'LM-Password',
-  'tunnel_window' => 'Tunnel_Window',
-  'x_ascend_link_compressio' => 'X-Ascend-Link-Compression',
-  'x_ascend_base_channel_co' => 'X-Ascend-Base-Channel-Count',
+  'tunnel_window' => 'Tunnel-Window',
   'cisco_avpair' => 'Cisco-AVPair',
+  'st_service_name' => 'ST-Service-Name',
   'shiva_event_flags' => 'Shiva-Event-Flags',
-  'usr_number_of_rings_limi' => 'USR-Number-of-Rings-Limit',
+  'annex_retrain_requests_s' => 'Annex-Retrain-Requests-Sent',
   'ascend_ts_idle_mode' => 'Ascend-TS-Idle-Mode',
-  'ascend_bi_directional_au' => 'Ascend-Bi-Directional-Auth',
+  'usr_ip_rip_simple_auth_p' => 'USR-IP-RIP-Simple-Auth-Password',
+  'tunnel_deadtimf' => 'Tunnel_Deadtime',
   'state' => 'State',
   'usr_keypress_timeout' => 'USR-Keypress-Timeout',
   'usr_pw_vpn_neighbor' => 'USR-PW_VPN_Neighbor',
+  'erx_pppoe_description' => 'ERX-Pppoe-Description',
   'ldap_userdn' => 'Ldap-UserDn',
   'x_ascend_fr_n391' => 'X-Ascend-FR-N391',
-  'ascend_tunneling_protoco' => 'Ascend-Tunneling-Protocol',
+  'ascend_calling_id_presen' => 'Ascend-Calling-Id-Presentatn',
+  'erx_local_loopback_inter' => 'ERX-Local-Loopback-Interface',
   'x_ascend_fr_direct' => 'X-Ascend-FR-Direct',
   'nas_ip_address' => 'NAS-IP-Address',
   'usr_call_end_time' => 'USR-Call-End-Time',
-  'tunnel_algorithm' => 'Tunnel_Algorithm',
+  'acct_mcast_out_packett' => 'Acct_Mcast_Out_Packets',
+  'tunnel_algorithm' => 'Tunnel-Algorithm',
   'usr_vpn_encrypter' => 'USR-VPN-Encrypter',
+  'tunnel_grouq' => 'Tunnel_Group',
   'ascend_atm_connect_group' => 'Ascend-ATM-Connect-Group',
   'x_ascend_ft1_caller' => 'X-Ascend-FT1-Caller',
+  'usr_dnis_reauthenticatio' => 'USR-DNIS-ReAuthentication',
   'login_callback_number' => 'Login-Callback-Number',
   'usr_ip_rip_input_filter' => 'USR-IP-RIP-Input-Filter',
-  'usr_rmmie_last_update_ev' => 'USR-RMMIE-Last-Update-Event',
+  'usr_rmmie_rcv_pwrlvl_330' => 'USR-RMMIE-Rcv-PwrLvl-3300Hz',
   'h323_disconnect_cause' => 'h323-disconnect-cause',
   'x_ascend_handle_ipx' => 'X-Ascend-Handle-IPX',
   'usr_igmp_version' => 'USR-IGMP-Version',
   'usr_imsi' => 'USR-IMSI',
   'group_name' => 'Group-Name',
   'usr_nas_type' => 'USR-NAS-Type',
+  'context_namf' => 'Context-Name',
   'ascend_ip_tos' => 'Ascend-IP-TOS',
   'x_ascend_token_immediate' => 'X-Ascend-Token-Immediate',
-  'ascend_private_route_tab' => 'Ascend-Private-Route-Table-ID',
+  'tunnel_session_auth_serw' => 'Tunnel_Session_Auth_Service_Grp',
   'ms_chap2_cpw' => 'MS-CHAP2-CPW',
-  'tunnel_session_auth_ctx' => 'Tunnel_Session_Auth_Ctx',
+  'tunnel_session_auth_ctx' => 'Tunnel-Session-Auth-Ctx',
   'usr_mobile_numbytes_rxed' => 'USR-Mobile-NumBytes-Rxed',
   'usr_mbi_ct_tdm_time_slot' => 'USR-Mbi_Ct_TDM_Time_Slot',
   'ascend_x25_nui' => 'Ascend-X25-Nui',
   'x_ascend_first_dest' => 'X-Ascend-First-Dest',
-  'x_ascend_num_in_multilin' => 'X-Ascend-Num-In-Multilink',
   'usr_send_password' => 'USR-Send-Password',
+  'x_ascend_fr_direct_profi' => 'X-Ascend-FR-Direct-Profile',
   'x_ascend_fr_t391' => 'X-Ascend-FR-T391',
+  'altiga_ipsec_sec_associa' => 'Altiga-IPSec-Sec-Association-G/U',
+  'ip_address_pool_namf' => 'Ip_Address_Pool_Name',
   'acct_input_octets' => 'Acct-Input-Octets',
-  'bridge_group' => 'Bridge_Group',
+  'cvx_modem_begin_modulati' => 'CVX-Modem-Begin-Modulation',
+  'wispr_session_terminatea' => 'WISPr-Session-Terminate-End-Of-Day',
+  'cvpn3000_use_client_addr' => 'CVPN3000-Use-Client-Address',
+  'bridge_group' => 'Bridge-Group',
   'annex_sec_profile_index' => 'Annex-Sec-Profile-Index',
   'acc_dns_server_pri' => 'Acc-Dns-Server-Pri',
   'ms_acct_auth_type' => 'MS-Acct-Auth-Type',
+  'x_ascend_maximum_call_du' => 'X-Ascend-Maximum-Call-Duration',
   'tunnel_password' => 'Tunnel-Password',
+  'framed_ipv6_prefix' => 'Framed-IPv6-Prefix',
   'usr_reply_script5' => 'USR-Reply-Script5',
   'shiva_links_in_bundle' => 'Shiva-Links-In-Bundle',
   'ascend_fr_profile_name' => 'Ascend-FR-Profile-Name',
   'ascend_mtu' => 'Ascend-MTU',
+  'nokia_charging_id' => 'Nokia-Charging-Id',
+  'cvpn3000_ms_client_subne' => 'CVPN3000-MS-Client-Subnet-Mask',
+  'cvpn3000_ipsec_sec_assoc' => 'CVPN3000-IPSec-Sec-Association',
   'cisco_ppp_async_map' => 'Cisco-PPP-Async-Map',
+  'cvpn3000_user_auth_servf' => 'CVPN3000-User-Auth-Server-Port',
   'cisco_num_in_multilink' => 'Cisco-Num-In-Multilink',
+  'wispr_logoff_url' => 'WISPr-Logoff-URL',
   'usr_mobile_ip_address' => 'USR-Mobile-IP-Address',
+  'usr_final_tx_link_data_r' => 'USR-Final-Tx-Link-Data-Rate',
+  'itk_ppp_compression_prot' => 'ITK-PPP-Compression-Prot',
   'ascend_bridge' => 'Ascend-Bridge',
   'x_ascend_presession_time' => 'X-Ascend-PreSession-Time',
-  'tunnel_cmd_timeout' => 'Tunnel_Cmd_Timeout',
+  'aat_client_primary_dns' => 'AAT-Client-Primary-DNS',
+  'cvpn3000_strip_realm' => 'CVPN3000-Strip-Realm',
+  'tunnel_cmd_timeout' => 'Tunnel-Cmd-Timeout',
   'ascend_multicast_client' => 'Ascend-Multicast-Client',
+  'cvx_modem_remote_rate_ne' => 'CVX-Modem-Remote-Rate-Negs',
   'tunnel_private_group_id' => 'Tunnel-Private-Group-Id',
   'usr_rmmie_rcv_tot_pwrlvl' => 'USR-RMMIE-Rcv-Tot-PwrLvl',
   'calling_station_id' => 'Calling-Station-Id',
-  'tunnel_rate_limit_burst' => 'Tunnel_Rate_Limit_Burst',
+  'tunnel_rate_limit_burst' => 'Tunnel-Rate-Limit-Burst',
   'usr_device_connected_to' => 'USR-Device-Connected-To',
+  'aat_source_ip_check' => 'AAT-Source-IP-Check',
   'login_lat_service' => 'Login-LAT-Service',
-  'x_ascend_home_network_na' => 'X-Ascend-Home-Network-Name',
   'ascend_h323_fegw_address' => 'Ascend-H323-Fegw-Address',
   'usr_called_party_number' => 'USR-Called-Party-Number',
+  'bintec_ipnatpresettable' => 'BinTec-ipNatPresetTable',
   'ascend_remove_seconds' => 'Ascend-Remove-Seconds',
   'shiva_user_attributes' => 'Shiva-User-Attributes',
+  'cisco_fax_dsn_flag' => 'Cisco-Fax-Dsn-Flag',
   'x_ascend_route_ipx' => 'X-Ascend-Route-IPX',
   'acc_route_policy' => 'Acc-Route-Policy',
+  'bind_l2tp_flow_controm' => 'Bind_L2TP_Flow_Control',
+  'erx_qos_profile_name' => 'ERX-Qos-Profile-Name',
   'x_ascend_client_gateway' => 'X-Ascend-Client-Gateway',
-  'ms_mppe_encryption_polic' => 'MS-MPPE-Encryption-Policy',
+  'pre_proxy_type' => 'Pre-Proxy-Type',
+  'smb_account_ctrl_text' => 'SMB-Account-CTRL-TEXT',
   'x_ascend_data_filter' => 'X-Ascend-Data-Filter',
+  'usr_rmmie_last_update_ti' => 'USR-RMMIE-Last-Update-Time',
   'ascend_atm_direct' => 'Ascend-ATM-Direct',
   'ascend_session_type' => 'Ascend-Session-Type',
   'x_ascend_fr_linkup' => 'X-Ascend-FR-LinkUp',
   'ascend_metric' => 'Ascend-Metric',
+  'x_ascend_assign_ip_clien' => 'X-Ascend-Assign-IP-Client',
   'usr_speed_of_connection' => 'USR-Speed-Of-Connection',
+  'cvpn3000_require_hw_clie' => 'CVPN3000-Require-HW-Client-Auth',
+  'session_type' => 'Session-Type',
+  'acct_input_octets_65' => 'Acct_Input_Octets_64',
   'le_nat_outsource_outmap' => 'LE-NAT-Outsource-Outmap',
-  'pppoe_url' => 'PPPOE_URL',
-  'acct_mcast_out_octets' => 'Acct_Mcast_Out_Octets',
+  'cvx_modem_local_rate_neg' => 'CVX-Modem-Local-Rate-Negs',
+  'mcast_sene' => 'Mcast_Send',
+  'pppoe_url' => 'PPPOE-URL',
+  'erx_service_bundle' => 'ERX-Service-Bundle',
+  'altiga_secondary_dns_g' => 'Altiga-Secondary-DNS-G',
+  'bg_trans_bpdv' => 'BG_Trans_BPDU',
+  'cvx_data_filter' => 'CVX-Data-Filter',
+  'acct_mcast_out_octets' => 'Acct-Mcast-Out-Octets',
   'ascend_callback' => 'Ascend-Callback',
   'tunnel_client_auth_id' => 'Tunnel-Client-Auth-Id',
   'acct_unique_session_id' => 'Acct-Unique-Session-Id',
   'usr_port_tap_format' => 'USR-Port-Tap-Format',
   'ascend_ckt_type' => 'Ascend-Ckt-Type',
   'ascend_ppp_async_map' => 'Ascend-PPP-Async-Map',
+  'usr_rmmie_rcv_pwrlvl_375' => 'USR-RMMIE-Rcv-PwrLvl-3750Hz',
   'usr_acct_reason_code' => 'USR-Acct-Reason-Code',
   'ascend_filter' => 'Ascend-Filter',
   'h323_redirect_number' => 'h323-redirect-number',
   'port_limit' => 'Port-Limit',
-  'x_ascend_shared_profile_' => 'X-Ascend-Shared-Profile-Enable',
-  'tunnel_police_rate' => 'Tunnel_Police_Rate',
-  'ascend_calling_id_screen' => 'Ascend-Calling-Id-Screening',
+  'rewrite_rule' => 'Rewrite-Rule',
+  'tunnel_police_rate' => 'Tunnel-Police-Rate',
   'usr_multicast_proxy' => 'USR-Multicast-Proxy',
+  'ascend_max_shared_users' => 'Ascend-Max-Shared-Users',
   'usr_bridging' => 'USR-Bridging',
-  'usr_originate_answer_mod' => 'USR-Originate-Answer-Mode',
+  'cvx_presession_time' => 'CVX-PreSession-Time',
+  'cvpn5000_vpn_groupinfo' => 'CVPN5000-VPN-GroupInfo',
+  'autz_type' => 'Autz-Type',
   'x_ascend_fr_dlci' => 'X-Ascend-FR-DLCI',
   'usr_request_type' => 'USR-Request-Type',
-  'acc_dialout_auth_usernam' => 'Acc-Dialout-Auth-Username',
+  'acc_igmp_admin_state' => 'Acc-Igmp-Admin-State',
   'ascend_host_info' => 'Ascend-Host-Info',
+  'ascend_dhcp_maximum_leas' => 'Ascend-DHCP-Maximum-Leases',
   'usr_rmmie_num_of_updates' => 'USR-RMMIE-Num-Of-Updates',
   'x_ascend_fr_profile_name' => 'X-Ascend-FR-Profile-Name',
   'ascend_fr_direct_profile' => 'Ascend-FR-Direct-Profile',
   'x_ascend_bridge' => 'X-Ascend-Bridge',
-  'tunnel_deadtime' => 'Tunnel_Deadtime',
+  'tunnel_deadtime' => 'Tunnel-Deadtime',
   'ms_chap_error' => 'MS-CHAP-Error',
   'framed_route' => 'Framed-Route',
+  'sip_from' => 'Sip-From',
   'expiration' => 'Expiration',
   'ascend_backup' => 'Ascend-Backup',
   'ascend_pre_output_octets' => 'Ascend-Pre-Output-Octets',
+  'ascend_calling_id_number' => 'Ascend-Calling-Id-Number-Plan',
   'framed_appletalk_zone' => 'Framed-AppleTalk-Zone',
   'annex_audit_level' => 'Annex-Audit-Level',
-  'bind_auth_context' => 'Bind_Auth_Context',
-  'cisco_asing_ip_pool' => 'Cisco-Asing-IP-Pool',
+  'digest_algorithm' => 'Digest-Algorithm',
+  'bind_auth_context' => 'Bind-Auth-Context',
   'ascend_user_acct_base' => 'Ascend-User-Acct-Base',
-  'mcast_receive' => 'Mcast_Receive',
+  'st_secondary_dns_server' => 'ST-Secondary-DNS-Server',
+  'mcast_receive' => 'Mcast-Receive',
   'usr_ds0' => 'USR-DS0',
+  'aat_atm_traffic_profile' => 'AAT-ATM-Traffic-Profile',
   'ms_ras_vendor' => 'MS-RAS-Vendor',
-  'tunnel_domain' => 'Tunnel_Domain',
-  'usr_secondary_nbns_serve' => 'USR-Secondary_NBNS_Server',
-  'tunnel_max_sessions' => 'Tunnel_Max_Sessions',
+  'tunnel_domain' => 'Tunnel-Domain',
+  'tunnel_max_sessions' => 'Tunnel-Max-Sessions',
   'ascend_ip_direct' => 'Ascend-IP-Direct',
+  'xedia_address_pool' => 'Xedia-Address-Pool',
   'idle_timeout' => 'Idle-Timeout',
+  'tunnel_rate_limit_ratf' => 'Tunnel_Rate_Limit_Rate',
+  'annex_rate_reneg_req_sen' => 'Annex-Rate-Reneg-Req-Sent',
+  'usr_initial_tx_link_data' => 'USR-Initial-Tx-Link-Data-Rate',
   'tunnel_server_auth_id' => 'Tunnel-Server-Auth-Id',
+  'cvpn3000_ipsec_banner1' => 'CVPN3000-IPSec-Banner1',
   'usr_start_time' => 'USR-Start-Time',
   'usr_ip' => 'USR-IP',
+  'cvpn3000_reqrd_client_fw' => 'CVPN3000-Reqrd-Client-Fw-Vendor-Code',
+  'altiga_ipsec_secondary_d' => 'Altiga-IPSec-Secondary-Domains-G',
   'usr_gateway_ip_address' => 'USR-Gateway-IP-Address',
-  'usr_number_of_characters' => 'USR-Number-Of-Characters-Lost',
   'ascend_dba_monitor' => 'Ascend-DBA-Monitor',
+  'ms_link_utilization_thre' => 'MS-Link-Utilization-Threshold',
+  'st_primary_dns_server' => 'ST-Primary-DNS-Server',
+  'acc_ace_token_ttl' => 'Acc-Ace-Token-Ttl',
   'ms_chap_domain' => 'MS-CHAP-Domain',
   'cisco_pre_input_octets' => 'Cisco-Pre-Input-Octets',
+  'ascend_primary_home_agen' => 'Ascend-Primary-Home-Agent',
   'acct_session_time' => 'Acct-Session-Time',
   'framed_ip_address' => 'Framed-IP-Address',
-  'x_ascend_ip_pool_definit' => 'X-Ascend-IP-Pool-Definition',
-  'erx_alternate_cli_access' => 'ERX-Alternate-Cli-Access-Level',
-  'medium_type' => 'Medium_Type',
-  'acct_output_octets_64' => 'Acct_Output_Octets_64',
+  'ns_admin_privilege' => 'NS-Admin-Privilege',
+  'medium_type' => 'Medium-Type',
+  'acct_output_octets_64' => 'Acct-Output-Octets-64',
   'ascend_cir_timer' => 'Ascend-CIR-Timer',
-  'police_rate' => 'Police_Rate',
+  'police_rate' => 'Police-Rate',
+  'tunnel_functioo' => 'Tunnel_Function',
+  'quintum_h323_time_and_da' => 'Quintum-h323-time-and-day',
+  'ip_tos_fiele' => 'IP_TOS_Field',
+  'erx_framed_ip_route_tag' => 'ERX-Framed-Ip-Route-Tag',
   'ms_mppe_send_key' => 'MS-MPPE-Send-Key',
-  'ascend_multicast_gleave_' => 'Ascend-Multicast-GLeave-Delay',
+  'ascend_maximum_call_dura' => 'Ascend-Maximum-Call-Duration',
+  'pppoe_motn' => 'PPPOE_MOTM',
+  'lac_poru' => 'LAC_Port',
+  'bind_dot1q_slou' => 'Bind_Dot1q_Slot',
+  'ascend_secondary_home_ag' => 'Ascend-Secondary-Home-Agent',
+  'usr_ip_call_output_filte' => 'USR-IP-Call-Output-Filter',
   'x_ascend_host_info' => 'X-Ascend-Host-Info',
   'erx_egress_policy_name' => 'ERX-Egress-Policy-Name',
+  'erx_ppp_password' => 'ERX-PPP-Password',
   'user_name' => 'User-Name',
-  'bind_bypass_bypass' => 'Bind_Bypass_Bypass',
+  'usr_number_of_characters' => 'USR-Number-Of-Characters-Lost',
+  'bind_bypass_bypass' => 'Bind-Bypass-Bypass',
+  'usr_rad_multicast_routip' => 'USR-Rad-Multicast-Routing-Proto',
   'annex_acct_servers' => 'Annex-Acct-Servers',
+  'cvpn5000_tunnel_throughp' => 'CVPN5000-Tunnel-Throughput',
   'usr_chassis_call_channel' => 'USR-Chassis-Call-Channel',
   'annex_input_filter' => 'Annex-Input-Filter',
-  'ascend_home_agent_passwo' => 'Ascend-Home-Agent-Password',
+  'wispr_billing_class_of_s' => 'WISPr-Billing-Class-Of-Service',
   'nas_port_type' => 'NAS-Port-Type',
+  'cvx_client_assign_dns' => 'CVX-Client-Assign-DNS',
+  'nomadix_maxbytesdown' => 'Nomadix-MaxBytesDown',
   'ascend_endpoint_disc' => 'Ascend-Endpoint-Disc',
-  'tunnel_police_burst' => 'Tunnel_Police_Burst',
-  'bind_auth_max_sessions' => 'Bind_Auth_Max_Sessions',
+  'tunnel_police_burst' => 'Tunnel-Police-Burst',
+  'bind_auth_max_sessions' => 'Bind-Auth-Max-Sessions',
+  'cvx_identification' => 'CVX-Identification',
+  'cvpn3000_ipsec_allow_pas' => 'CVPN3000-IPSec-Allow-Passwd-Store',
+  'ascend_calling_id_type_o' => 'Ascend-Calling-Id-Type-Of-Num',
   'x_ascend_fr_dce_n392' => 'X-Ascend-FR-DCE-N392',
   'usr_connect_term_reason' => 'USR-Connect-Term-Reason',
-  'usr_mbi_ct_pri_card_span' => 'USR-Mbi_Ct_PRI_Card_Span_Line',
   'erx_egress_statistics' => 'ERX-Egress-Statistics',
   'ascend_fr_dte_n392' => 'Ascend-FR-DTE-N392',
   'usr_esn' => 'USR-ESN',
   'x_ascend_fr_dte_n392' => 'X-Ascend-FR-DTE-N392',
+  'itk_modem_init_string' => 'ITK-Modem-Init-String',
   'x_ascend_fr_nailed_grp' => 'X-Ascend-FR-Nailed-Grp',
   'ascend_bridge_non_pppoe' => 'Ascend-Bridge-Non-PPPoE',
+  'cvpn3000_ipsec_reqrd_cli' => 'CVPN3000-IPSec-Reqrd-Client-Fw-Cap',
   'ascend_ipx_alias' => 'Ascend-IPX-Alias',
   'acc_tunnel_port' => 'Acc-Tunnel-Port',
+  'quintum_h323_return_code' => 'Quintum-h323-return-code',
+  'cvpn3000_l2tp_encryption' => 'CVPN3000-L2TP-Encryption',
   'acct_input_gigawords' => 'Acct-Input-Gigawords',
+  'bind_dot1q_poru' => 'Bind_Dot1q_Port',
+  'altiga_primary_wins_g' => 'Altiga-Primary-WINS-G',
   'ascend_maximum_channels' => 'Ascend-Maximum-Channels',
+  'x_ascend_home_agent_pass' => 'X-Ascend-Home-Agent-Password',
   'x_ascend_ppp_async_map' => 'X-Ascend-PPP-Async-Map',
+  'usr_rmmie_manufacturer_i' => 'USR-RMMIE-Manufacturer-ID',
   'usr_retrains_requested' => 'USR-Retrains-Requested',
   'x_ascend_metric' => 'X-Ascend-Metric',
   'acc_apsm_oversubscribed' => 'Acc-Apsm-Oversubscribed',
+  'usr_originate_answer_mod' => 'USR-Originate-Answer-Mode',
   'erx_atm_pcr' => 'ERX-Atm-PCR',
+  'itk_nas_name' => 'ITK-NAS-Name',
   'usr_ipx_routing' => 'USR-IPX-Routing',
   'usr_tunneled_mlpp' => 'USR-Tunneled-MLPP',
   'usr_send_script5' => 'USR-Send-Script5',
   'ascend_traffic_shaper' => 'Ascend-Traffic-Shaper',
+  'ascend_client_secondarya' => 'Ascend-Client-Secondary-DNS',
   'ascend_bacp_enable' => 'Ascend-BACP-Enable',
+  'usr_call_terminate_in_gm' => 'USR-Call-Terminate-in-GMT',
   'login_time' => 'Login-Time',
+  'bg_path_cosu' => 'BG_Path_Cost',
+  'aat_require_auth' => 'AAT-Require-Auth',
+  'cvpn3000_reqrd_client_fy' => 'CVPN3000-Reqrd-Client-Fw-Description',
   'ascend_call_type' => 'Ascend-Call-Type',
   'erx_address_pool_name' => 'ERX-Address-Pool-Name',
+  'cvpn3000_ipsec_backup_sf' => 'CVPN3000-IPSec-Backup-Server-List',
   'h323_incoming_conf_id' => 'h323-incoming-conf-id',
+  'user_profile' => 'User-Profile',
+  'ip_host_adds' => 'Ip_Host_Addr',
+  'ns_primary_wins' => 'NS-Primary-WINS',
   'packet_type' => 'Packet-Type',
+  'bind_auth_max_sessiont' => 'Bind_Auth_Max_Sessions',
+  'altiga_allow_alpha_only_' => 'Altiga-Allow-Alpha-Only-Passwords-G',
   'usr_security_resp_limit' => 'USR-Security-Resp-Limit',
-  'ip_address_pool_name' => 'Ip_Address_Pool_Name',
-  'ascend_cbcp_trunk_group' => 'Ascend-CBCP-Trunk-Group',
+  'ip_address_pool_name' => 'Ip-Address-Pool-Name',
   'ascend_ipx_node_addr' => 'Ascend-IPX-Node-Addr',
+  'ascend_cbcp_trunk_group' => 'Ascend-CBCP-Trunk-Group',
   'ascend_menu_selector' => 'Ascend-Menu-Selector',
+  'ascend_assign_ip_global_' => 'Ascend-Assign-IP-Global-Pool',
   'usr_ds0s' => 'USR-DS0s',
   'usr_actual_voltage' => 'USR-Actual-Voltage',
+  'quintum_h323_call_type' => 'Quintum-h323-call-type',
   'annex_sw_version' => 'Annex-SW-Version',
-  'ascend_history_weigh_typ' => 'Ascend-History-Weigh-Type',
   'ascend_receive_secret' => 'Ascend-Receive-Secret',
+  'bintec_qospolicytable' => 'BinTec-qosPolicyTable',
   'usr_ip_rip_policies' => 'USR-IP-RIP-Policies',
+  'redcreek_tunneled_ip_add' => 'RedCreek-Tunneled-IP-Addr',
   'ascend_pw_warntime' => 'Ascend-PW-Warntime',
-  'x_ascend_assign_ip_serve' => 'X-Ascend-Assign-IP-Server',
-  'tunnel_session_auth_serv' => 'Tunnel_Session_Auth_Service_Grp',
+  'x_ascend_inc_channel_cou' => 'X-Ascend-Inc-Channel-Count',
   'usr_blocks_resent' => 'USR-Blocks-Resent',
   'usr_fallback_enabled' => 'USR-Fallback-Enabled',
   'arap_challenge_response' => 'ARAP-Challenge-Response',
-  'tunnel_session_auth' => 'Tunnel_Session_Auth',
+  'tunnel_session_auth' => 'Tunnel-Session-Auth',
   'usr_sync_async_mode' => 'USR-Sync-Async-Mode',
+  'itk_dialout_type' => 'ITK-Dialout-Type',
+  'extreme_netlogin_url' => 'Extreme-Netlogin-Url',
   'client_port_dnis' => 'Client-Port-DNIS',
+  'digest_realm' => 'Digest-Realm',
   'ascend_ppp_vj_1172' => 'Ascend-PPP-VJ-1172',
-  'ascend_remote_addr' => 'Ascend-Remote-Addr',
   'ascend_fr_n391' => 'Ascend-FR-N391',
+  'ascend_remote_addr' => 'Ascend-Remote-Addr',
   'client_port_id' => 'Client-Port-Id',
-  'usr_num_fax_pages_proces' => 'USR-Num-Fax-Pages-Processed',
+  'digest_body_digest' => 'Digest-Body-Digest',
   'le_ipsec_active_profile' => 'LE-IPSec-Active-Profile',
+  'digest_cnonce' => 'Digest-CNonce',
   'usr_port_tap_facility' => 'USR-Port-Tap-Facility',
   'usr_callback_type' => 'USR-Callback-Type',
+  'client_dns_prj' => 'Client_DNS_Pri',
+  'digest_response' => 'Digest-Response',
   'login_lat_group' => 'Login-LAT-Group',
   'x_ascend_call_type' => 'X-Ascend-Call-Type',
   'ascend_route_ip' => 'Ascend-Route-IP',
+  'usr_rad_multicast_routio' => 'USR-Rad-Multicast-Routing-RtLim',
   'usr_pw_vpn_id' => 'USR-PW_VPN_ID',
-  'le_nat_sess_dir_fail_act' => 'LE-NAT-Sess-Dir-Fail-Action',
+  'cvx_modem_end_modulation' => 'CVX-Modem-End-Modulation',
+  'cvpn3000_pptp_mppc_compr' => 'CVPN3000-PPTP-MPPC-Compression',
   'cisco_pre_output_octets' => 'Cisco-Pre-Output-Octets',
   'h323_billing_model' => 'h323-billing-model',
   'usr_equalization_type' => 'USR-Equalization-Type',
   'acc_clearing_cause' => 'Acc-Clearing-Cause',
+  'altiga_access_hours_g_u' => 'Altiga-Access-Hours-G/U',
+  'cvpn3000_ipsec_user_grou' => 'CVPN3000-IPSec-User-Group-Lock',
   'x_ascend_menu_selector' => 'X-Ascend-Menu-Selector',
   'x_ascend_netware_timeout' => 'X-Ascend-Netware-timeout',
   'ascend_fr_linkup' => 'Ascend-FR-LinkUp',
-  'police_burst' => 'Police_Burst',
+  'annex_num_in_multilink' => 'Annex-Num-In-Multilink',
+  'police_burst' => 'Police-Burst',
+  'altiga_l2tp_min_authenti' => 'Altiga-L2TP-Min-Authentication-G/U',
   'ascend_filter_required' => 'Ascend-Filter-Required',
-  'usr_compression_algorith' => 'USR-Compression-Algorithm',
-  'le_ipsec_outsource_profi' => 'LE-IPSec-Outsource-Profile',
   'x_ascend_idle_limit' => 'X-Ascend-Idle-Limit',
-  'usr_call_terminate_in_gm' => 'USR-Call-Terminate-in-GMT',
-  'usr_ipx_call_output_filt' => 'USR-IPX-Call-Output-Filter',
-  'ip_tos_field' => 'IP_TOS_Field',
+  'nomadix_logoff_url' => 'Nomadix-Logoff-URL',
+  'cvpn3000_ms_client_icpt_' => 'CVPN3000-MS-Client-Icpt-DHCP-Conf-Msg',
+  'ip_tos_field' => 'IP-TOS-Field',
   'ascend_ip_tos_apply_to' => 'Ascend-IP-TOS-Apply-To',
-  'tunnel_l2f_second_passwo' => 'Tunnel_L2F_Second_Password',
   'usr_call_event_code' => 'USR-Call-Event-Code',
+  'usr_et_bridge_output_fil' => 'USR-ET-Bridge-Output-Filter',
+  'le_nat_sess_dir_fail_act' => 'LE-NAT-Sess-Dir-Fail-Action',
   'usr_rmmie_product_code' => 'USR-RMMIE-Product-Code',
   'usr_host_type' => 'USR-Host-Type',
+  'erx_tunnel_interface_id' => 'ERX-Tunnel-Interface-Id',
   'ascend_send_auth' => 'Ascend-Send-Auth',
   'shiva_compression_type' => 'Shiva-Compression-Type',
-  'filter_id' => 'Filter-Id',
+  'itk_banner' => 'ITK-Banner',
   'ascend_ft1_caller' => 'Ascend-FT1-Caller',
-  'erx_cli_initial_access_l' => 'ERX-Cli-Initial-Access-Level',
+  'filter_id' => 'Filter-Id',
+  'annex_pre_output_octets' => 'Annex-Pre-Output-Octets',
+  'acct_mcast_in_octett' => 'Acct_Mcast_In_Octets',
   'usr_log_filter_packets' => 'USR-Log-Filter-Packets',
   'ascend_fr_nailed_grp' => 'Ascend-FR-Nailed-Grp',
-  'usr_initial_tx_link_data' => 'USR-Initial-Tx-Link-Data-Rate',
+  'ascend_atm_loopback_cell' => 'Ascend-ATM-Loopback-Cell-Loss',
+  'usr_at_rtmp_output_filte' => 'USR-AT-RTMP-Output-Filter',
   'acc_input_errors' => 'Acc-Input-Errors',
   'x_ascend_user_acct_port' => 'X-Ascend-User-Acct-Port',
   'erx_secondary_wins' => 'ERX-Secondary-Wins',
   'usr_rmmie_serial_number' => 'USR-RMMIE-Serial-Number',
-  'ascend_client_primary_dn' => 'Ascend-Client-Primary-DNS',
+  'usr_et_bridge_input_filt' => 'USR-ET-Bridge-Input-Filter',
+  'ns_primary_dns' => 'NS-Primary-DNS',
   'usr_slot_connected_to' => 'USR-Slot-Connected-To',
   'shiva_disconnect_reason' => 'Shiva-Disconnect-Reason',
+  'cvpn5000_client_assignee' => 'CVPN5000-Client-Assigned-IPX',
+  'cvx_radius_redirect' => 'CVX-Radius-Redirect',
   'usr_receive_acc_map' => 'USR-Receive-Acc-Map',
-  'usr_compression_reset_mo' => 'USR-Compression-Reset-Mode',
-  'usr_rmmie_planned_discon' => 'USR-RMMIE-Planned-Disconnect',
-  'ascend_client_assign_dns' => 'Ascend-Client-Assign-DNS',
+  'x_ascend_tunneling_proto' => 'X-Ascend-Tunneling-Protocol',
+  'itk_acct_serv_ip' => 'ITK-Acct-Serv-IP',
   'ascend_fr_type' => 'Ascend-FR-Type',
+  'ascend_client_assign_dns' => 'Ascend-Client-Assign-DNS',
+  'annex_retrain_requests_r' => 'Annex-Retrain-Requests-Rcvd',
+  'x_ascend_assign_ip_globa' => 'X-Ascend-Assign-IP-Global-Pool',
   'tunnel_client_endpoint' => 'Tunnel-Client-Endpoint',
+  'alteon_service_type' => 'Alteon-Service-Type',
   'x_ascend_send_secret' => 'X-Ascend-Send-Secret',
   'x_ascend_call_filter' => 'X-Ascend-Call-Filter',
   'usr_ipx_rip_input_filter' => 'USR-IPX-RIP-Input-Filter',
   'x_ascend_maximum_time' => 'X-Ascend-Maximum-Time',
-  'ascend_x25_pad_x3_profil' => 'Ascend-X25-Pad-X3-Profile',
-  'pvc_profile_name' => 'PVC_Profile_Name',
+  'pvc_profile_name' => 'PVC-Profile-Name',
+  'usr_framed_ip_address_po' => 'USR-Framed_IP_Address_Pool_Name',
+  'cvpn3000_ipsec_split_dns' => 'CVPN3000-IPSec-Split-DNS-Names',
   'ascend_global_call_id' => 'Ascend-Global-Call-Id',
-  'tunnel_local_name' => 'Tunnel_Local_Name',
+  'usr_initial_rx_link_data' => 'USR-Initial-Rx-Link-Data-Rate',
+  'st_primary_nbns_server' => 'ST-Primary-NBNS-Server',
+  'usr_number_of_rings_limi' => 'USR-Number-of-Rings-Limit',
+  'tunnel_local_name' => 'Tunnel-Local-Name',
   'ascend_fr_t392' => 'Ascend-FR-T392',
-  'usr_dnis_reauthenticatio' => 'USR-DNIS-ReAuthentication',
-  'ascend_pre_output_packet' => 'Ascend-Pre-Output-Packets',
+  'annex_pool_id' => 'Annex-Pool-Id',
   'ascend_token_immediate' => 'Ascend-Token-Immediate',
+  'usr_rmmie_firmware_build' => 'USR-RMMIE-Firmware-Build-Date',
+  'wispr_bandwidth_min_down' => 'WISPr-Bandwidth-Min-Down',
   'usr_chassis_call_slot' => 'USR-Chassis-Call-Slot',
-  'rate_limit_burst' => 'Rate_Limit_Burst',
+  'rate_limit_burst' => 'Rate-Limit-Burst',
   'cisco_route_ip' => 'Cisco-Route-IP',
-  'dhcp_max_leases' => 'DHCP_Max_Leases',
+  'xedia_netbios_server' => 'Xedia-NetBios-Server',
+  'session_error_msg' => 'Session-Error-Msg',
+  'dhcp_max_leases' => 'DHCP-Max-Leases',
+  'acc_vpsm_reject_cause' => 'Acc-Vpsm-Reject-Cause',
   'user_category' => 'User-Category',
-  'x_ascend_maximum_call_du' => 'X-Ascend-Maximum-Call-Duration',
-  'bind_type' => 'Bind_Type',
+  'x_ascend_multicast_rate_' => 'X-Ascend-Multicast-Rate-Limit',
+  'cvpn3000_ipsec_auth_on_r' => 'CVPN3000-IPSec-Auth-On-Rekey',
+  'altiga_min_password_leng' => 'Altiga-Min-Password-Length-G',
+  'bind_type' => 'Bind-Type',
+  'ascend_tunneling_protoco' => 'Ascend-Tunneling-Protocol',
+  'cvx_modem_retx_packets' => 'CVX-Modem-ReTx-Packets',
   'usr_framed_ipx_route' => 'USR-Framed-IPX-Route',
-  'rate_limit_rate' => 'Rate_Limit_Rate',
+  'rate_limit_rate' => 'Rate-Limit-Rate',
   'ascend_atm_connect_vpi' => 'Ascend-ATM-Connect-Vpi',
-  'x_ascend_inc_channel_cou' => 'X-Ascend-Inc-Channel-Count',
   'connect_info' => 'Connect-Info',
-  'x_ascend_pre_input_packe' => 'X-Ascend-Pre-Input-Packets',
   'usr_port_tap_address' => 'USR-Port-Tap-Address',
-  'ascend_home_agent_udp_po' => 'Ascend-Home-Agent-UDP-Port',
-  'usr_final_rx_link_data_r' => 'USR-Final-Rx-Link-Data-Rate',
+  'usr_simplified_mnp_level' => 'USR-Simplified-MNP-Levels',
+  'mcast_receivf' => 'Mcast_Receive',
+  'annex_begin_modulation' => 'Annex-Begin-Modulation',
   'usr_pw_usr_ifilter_ip' => 'USR-PW_USR_IFilter_IP',
   'ascend_route_appletalk' => 'Ascend-Route-Appletalk',
   'ms_chap_lm_enc_pw' => 'MS-CHAP-LM-Enc-PW',
+  'altiga_ipsec_over_nat_po' => 'Altiga-IPSec-Over-NAT-Port-Num-G',
+  'itk_isdn_prot' => 'ITK-ISDN-Prot',
   'ascend_callback_delay' => 'Ascend-Callback-Delay',
+  'session_error_code' => 'Session-Error-Code',
+  'nomadix_endofsession' => 'Nomadix-EndofSession',
   'x_ascend_bacp_enable' => 'X-Ascend-BACP-Enable',
-  'bg_trans_bpdu' => 'BG_Trans_BPDU',
+  'bg_trans_bpdu' => 'BG-Trans-BPDU',
+  'bind_int_interface_namf' => 'Bind_Int_Interface_Name',
+  'foundry_privilege_level' => 'Foundry-Privilege-Level',
   'huntgroup_name' => 'Huntgroup-Name',
   'x_ascend_ipx_alias' => 'X-Ascend-IPX-Alias',
-  'x_ascend_secondary_home_' => 'X-Ascend-Secondary-Home-Agent',
+  'tunnel_l2f_second_passwp' => 'Tunnel_L2F_Second_Password',
+  'xedia_dns_server' => 'Xedia-DNS-Server',
   'usr_ipx_wan' => 'USR-IPX-WAN',
+  'annex_addr_resolution_se' => 'Annex-Addr-Resolution-Servers',
+  'acct_output_octets_65' => 'Acct_Output_Octets_64',
   'menu' => 'Menu',
+  'erx_tunnel_nas_port_meth' => 'ERX-Tunnel-Nas-Port-Method',
+  'aat_output_octets_diff' => 'AAT-Output-Octets-Diff',
   'x_ascend_fr_direct_dlci' => 'X-Ascend-FR-Direct-DLCI',
   'acct_status_type' => 'Acct-Status-Type',
   'ascend_port_redir_server' => 'Ascend-Port-Redir-Server',
+  'telebit_port_name' => 'Telebit-Port-Name',
   'acc_dns_server_sec' => 'Acc-Dns-Server-Sec',
+  'cvx_modem_remote_retrain' => 'CVX-Modem-Remote-Retrains',
   'ascend_minimum_channels' => 'Ascend-Minimum-Channels',
-  'ascend_telnet_profile' => 'Ascend-Telnet-Profile',
   'ascend_ipx_route' => 'Ascend-IPX-Route',
+  'ascend_telnet_profile' => 'Ascend-Telnet-Profile',
   'usr_call_connect_in_gmt' => 'USR-Call-Connect-in-GMT',
+  'usr_cusr_hat_script_rule' => 'USR-CUSR-hat-Script-Rules',
   'x_ascend_dba_monitor' => 'X-Ascend-DBA-Monitor',
+  'response_packet_type' => 'Response-Packet-Type',
   'usr_event_id' => 'USR-Event-Id',
+  'cvpn3000_ipsec_over_udp_' => 'CVPN3000-IPSec-Over-UDP-Port',
   'ascend_inc_channel_count' => 'Ascend-Inc-Channel-Count',
   'usr_send_script3' => 'USR-Send-Script3',
+  'annex_pre_input_packets' => 'Annex-Pre-Input-Packets',
   'framed_callback_id' => 'Framed-Callback-Id',
+  'xedia_client_access_netw' => 'Xedia-Client-Access-Network',
   'arap_zone_access' => 'ARAP-Zone-Access',
+  'ascend_port_redir_portnu' => 'Ascend-Port-Redir-Portnum',
   'service_type' => 'Service-Type',
   'usr_nfas_id' => 'USR-NFAS-ID',
   'shiva_calling_number' => 'Shiva-Calling-Number',
   'ascend_user_acct_host' => 'Ascend-User-Acct-Host',
+  'tunnel_session_auth_serv' => 'Tunnel-Session-Auth-Service-Grp',
+  'juniper_deny_commands' => 'Juniper-Deny-Commands',
   'ascend_fr_link_mgt' => 'Ascend-FR-Link-Mgt',
+  'nokia_imsi' => 'Nokia-IMSI',
+  'quintum_h323_prompt_id' => 'Quintum-h323-prompt-id',
+  'cvpn3000_require_individ' => 'CVPN3000-Require-Individual-User-Auth',
+  'tunnel_retransmiu' => 'Tunnel_Retransmit',
+  'source_validatioo' => 'Source_Validation',
+  'sip_to' => 'Sip-To',
   'ms_primary_nbns_server' => 'MS-Primary-NBNS-Server',
   'quintum_avpair' => 'Quintum-AVPair',
-  'x_ascend_home_agent_pass' => 'X-Ascend-Home-Agent-Password',
   'ascend_transit_number' => 'Ascend-Transit-Number',
   'ascend_cache_refresh' => 'Ascend-Cache-Refresh',
-  'versanet_termination_cau' => 'Versanet-Termination-Cause',
   'ascend_user_acct_type' => 'Ascend-User-Acct-Type',
+  'usr_num_fax_pages_proces' => 'USR-Num-Fax-Pages-Processed',
   'usr_mic' => 'USR-MIC',
-  'ascend_base_channel_coun' => 'Ascend-Base-Channel-Count',
-  'x_ascend_dhcp_pool_numbe' => 'X-Ascend-DHCP-Pool-Number',
+  'usr_failure_to_connect_r' => 'USR-Failure-to-Connect-Reason',
+  'cisco_fax_auth_status' => 'Cisco-Fax-Auth-Status',
+  'bind_dot1q_vlan_tag_ie' => 'Bind_Dot1q_Vlan_Tag_Id',
   'ms_chap2_success' => 'MS-CHAP2-Success',
+  'erx_tunnel_virtual_route' => 'ERX-Tunnel-Virtual-Router',
   'cisco_idle_limit' => 'Cisco-Idle-Limit',
   'ascend_pw_lifetime' => 'Ascend-PW-Lifetime',
+  'cvpn3000_access_hours' => 'CVPN3000-Access-Hours',
+  'bintec_sapcirctable' => 'BinTec-sapCircTable',
   'usr_packet_bus_session' => 'USR-Packet-Bus-Session',
-  'ascend_atm_loopback_cell' => 'Ascend-ATM-Loopback-Cell-Loss',
-  'acct_input_packets_64' => 'Acct_Input_Packets_64',
+  'acct_input_packets_64' => 'Acct-Input-Packets-64',
+  'ascend_x25_pad_x3_parame' => 'Ascend-X25-Pad-X3-Parameters',
+  'usr_secondary_nbns_serve' => 'USR-Secondary_NBNS_Server',
   'ascend_modem_slotno' => 'Ascend-Modem-SlotNo',
+  'digest_qop' => 'Digest-QOP',
   'usr_characters_received' => 'USR-Characters-Received',
+  'rate_limit_ratf' => 'Rate_Limit_Rate',
   'ms_bap_usage' => 'MS-BAP-Usage',
   'cisco_data_filter' => 'Cisco-Data-Filter',
-  'ascend_seconds_of_histor' => 'Ascend-Seconds-Of-History',
+  'usr_simplified_v42bis_us' => 'USR-Simplified-V42bis-Usage',
   'h323_setup_time' => 'h323-setup-time',
-  'acc_dialout_auth_passwor' => 'Acc-Dialout-Auth-Password',
+  'annex_wan_number' => 'Annex-Wan-Number',
+  'cvx_vpop_id' => 'CVX-VPOP-ID',
+  'usr_pw_tunnel_authentica' => 'USR-PW_Tunnel_Authentication',
   'le_nat_outsource_inmap' => 'LE-NAT-Outsource-Inmap',
+  'cvx_modem_begin_recv_lin' => 'CVX-Modem-Begin-Recv-Line-Lvl',
+  'telebit_login_command' => 'Telebit-Login-Command',
+  'cisco_command_code' => 'Cisco-Command-Code',
+  'itk_ppp_auth_type' => 'ITK-PPP-Auth-Type',
+  'bintec_qosiftable' => 'BinTec-qosIfTable',
+  'x_ascend_mpp_idle_percen' => 'X-Ascend-MPP-Idle-Percent',
   'usr_sap_filter_in' => 'USR-SAP-Filter-In',
   'framed_appletalk_link' => 'Framed-AppleTalk-Link',
-  'usr_initial_rx_link_data' => 'USR-Initial-Rx-Link-Data-Rate',
-  'usr_ospf_addressless_ind' => 'USR-OSPF-Addressless-Index',
+  'tunnel_domaio' => 'Tunnel_Domain',
   'usr_ipx' => 'USR-IPX',
+  'nas_real_poru' => 'NAS_Real_Port',
   'shiva_connect_reason' => 'Shiva-Connect-Reason',
+  'x_ascend_pre_output_octe' => 'X-Ascend-Pre-Output-Octets',
   'cisco_ppp_vj_slot_comp' => 'Cisco-PPP-VJ-Slot-Comp',
+  'freeradius_proxied_to' => 'Freeradius-Proxied-To',
   'ascend_atm_vpi' => 'Ascend-ATM-Vpi',
   'acc_ml_mlx_admin_state' => 'Acc-ML-MLX-Admin-State',
+  'cvx_modem_snr' => 'CVX-Modem-SNR',
   'usr_igmp_robustness' => 'USR-IGMP-Robustness',
+  'annex_rate_reneg_req_rcv' => 'Annex-Rate-Reneg-Req-Rcvd',
   'add_prefix' => 'Add-Prefix',
   'x_ascend_call_by_call' => 'X-Ascend-Call-By-Call',
-  'x_ascend_connect_progres' => 'X-Ascend-Connect-Progress',
+  'usr_last_callers_number_' => 'USR-Last-Callers-Number-ANI',
+  'postauth_type' => 'PostAuth-Type',
+  'pvc_circuit_paddinh' => 'PVC_Circuit_Padding',
   'usr_at_rtmp_input_filter' => 'USR-AT-RTMP-Input-Filter',
   'erx_igmp_enable' => 'ERX-Igmp-Enable',
-  'usr_rmmie_rcv_pwrlvl_375' => 'USR-RMMIE-Rcv-PwrLvl-3750Hz',
+  'bind_bypass_contexu' => 'Bind_Bypass_Context',
+  'x_ascend_num_in_multilin' => 'X-Ascend-Num-In-Multilink',
   'usr_pw_packet' => 'USR-PW_Packet',
   'dialback_no' => 'Dialback-No',
   'ascend_ip_tos_precedence' => 'Ascend-IP-TOS-Precedence',
+  'cvpn5000_vpn_password' => 'CVPN5000-VPN-Password',
   'annex_cli_filter' => 'Annex-CLI-Filter',
   'x_ascend_dial_number' => 'X-Ascend-Dial-Number',
   'usr_iwf_call_identifier' => 'USR-IWF-Call-Identifier',
   'ms_secondary_dns_server' => 'MS-Secondary-DNS-Server',
-  'ascend_client_secondary_' => 'Ascend-Client-Secondary-WINS',
   'shiva_type_of_service' => 'Shiva-Type-Of-Service',
-  'usr_framed_ip_address_po' => 'USR-Framed_IP_Address_Pool_Name',
-  'bind_ses_context' => 'Bind_Ses_Context',
+  'bind_ses_context' => 'Bind-Ses-Context',
   'acc_reason_code' => 'Acc-Reason-Code',
   'ms_chap_cpw_1' => 'MS-CHAP-CPW-1',
+  'wispr_bandwidth_max_down' => 'WISPr-Bandwidth-Max-Down',
   'h323_call_type' => 'h323-call-type',
+  'bind_bypass_bypast' => 'Bind_Bypass_Bypass',
+  'usr_number_of_link_timeo' => 'USR-Number-of-Link-Timeouts',
   'ascend_fr_08_mode' => 'Ascend-FR-08-Mode',
   'usr_calling_party_number' => 'USR-Calling-Party-Number',
-  'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-RtLim',
   'usr_reply_script2' => 'USR-Reply-Script2',
   'usr_security_login_limit' => 'USR-Security-Login-Limit',
   'cisco_link_compression' => 'Cisco-Link-Compression',
-  'usr_et_bridge_output_fil' => 'USR-ET-Bridge-Output-Filter',
   'ascend_vrouter_name' => 'Ascend-VRouter-Name',
+  'erx_ppp_auth_protocol' => 'ERX-PPP-Auth-Protocol',
+  'x_ascend_call_block_dura' => 'X-Ascend-Call-Block-Duration',
   'usr_modem_setup_time' => 'USR-Modem-Setup-Time',
+  'pppoe_urm' => 'PPPOE_URL',
   'cisco_ip_direct' => 'Cisco-IP-Direct',
   'x_ascend_temporary_rtes' => 'X-Ascend-Temporary-Rtes',
   'ascend_x25_pad_alias_3' => 'Ascend-X25-Pad-Alias-3',
-  'usr_rmmie_pwrlvl_xmit_lv' => 'USR-RMMIE-PwrLvl-Xmit-Lvl',
+  'annex_multilink_id' => 'Annex-Multilink-Id',
+  'mcast_maxgroupt' => 'Mcast_MaxGroups',
   'configuration_token' => 'Configuration-Token',
-  'usr_at_rtmp_output_filte' => 'USR-AT-RTMP-Output-Filter',
-  'usr_ip_default_route_opt' => 'USR-IP-Default-Route-Option',
-  'ascend_calling_subaddres' => 'Ascend-Calling-Subaddress',
+  'ascend_h323_conference_i' => 'Ascend-H323-Conference-Id',
+  'ascend_ipx_header_compre' => 'Ascend-IPX-Header-Compression',
   'stripped_user_name' => 'Stripped-User-Name',
+  'usr_ipx_rip_output_filte' => 'USR-IPX-RIP-Output-Filter',
   'cisco_call_filter' => 'Cisco-Call-Filter',
+  'nas_ipv6_address' => 'NAS-IPv6-Address',
   'termination_menu' => 'Termination-Menu',
+  'ascend_shared_profile_en' => 'Ascend-Shared-Profile-Enable',
   'port_message' => 'Port-Message',
-  'usr_igmp_maximum_respons' => 'USR-IGMP-Maximum-Response-Time',
   'erx_ingress_policy_name' => 'ERX-Ingress-Policy-Name',
-  'ascend_call_attempt_limi' => 'Ascend-Call-Attempt-Limit',
   'acc_service_profile' => 'Acc-Service-Profile',
   'ascend_bir_proxy' => 'Ascend-BIR-Proxy',
+  'aat_ppp_address' => 'AAT-PPP-Address',
+  'usr_mbi_ct_pri_card_span' => 'USR-Mbi_Ct_PRI_Card_Span_Line',
   'ascend_x25_nui_prompt' => 'Ascend-X25-Nui-Prompt',
-  'usr_rmmie_pwrlvl_noise_l' => 'USR-RMMIE-PwrLvl-Noise-Lvl',
-  'usr_rmmie_pwrlvl_nearech' => 'USR-RMMIE-PwrLvl-NearEcho-Canc',
-  'x_ascend_multicast_clien' => 'X-Ascend-Multicast-Client',
+  'itk_modem_pool_id' => 'ITK-Modem-Pool-Id',
+  'usr_compression_reset_mo' => 'USR-Compression-Reset-Mode',
   'usr_unauthenticated_time' => 'USR-Unauthenticated-Time',
+  'ascend_multicast_gleave_' => 'Ascend-Multicast-GLeave-Delay',
   'acc_callback_cbcp_type' => 'Acc-Callback-CBCP-Type',
+  'medium_typf' => 'Medium_Type',
   'login_service' => 'Login-Service',
-  'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-Bound',
+  'itk_username_prompt' => 'ITK-Username-Prompt',
   'ascend_dial_number' => 'Ascend-Dial-Number',
+  'framed_ipv6_route' => 'Framed-IPv6-Route',
   'x_ascend_remote_addr' => 'X-Ascend-Remote-Addr',
-  'usr_rmmie_rcv_pwrlvl_330' => 'USR-RMMIE-Rcv-PwrLvl-3300Hz',
   'usr_call_end_date_time' => 'USR-Call-End-Date-Time',
-  'bind_dot1q_slot' => 'Bind_Dot1q_Slot',
+  'bind_dot1q_slot' => 'Bind-Dot1q-Slot',
   'le_connect_detail' => 'LE-Connect-Detail',
   'annex_user_level' => 'Annex-User-Level',
-  'tunnel_dnis' => 'Tunnel_DNIS',
-  'assigned_ip_address' => 'Assigned_IP_Address',
+  'tunnel_dnis' => 'Tunnel-DNIS',
+  'assigned_ip_address' => 'Assigned-IP-Address',
   'acc_bridging_support' => 'Acc-Bridging-Support',
   'usr_channel' => 'USR-Channel',
   'arap_security_data' => 'ARAP-Security-Data',
-  'bind_auth_service_grp' => 'Bind_Auth_Service_Grp',
-  'x_ascend_pre_output_pack' => 'X-Ascend-Pre-Output-Packets',
-  'x_ascend_seconds_of_hist' => 'X-Ascend-Seconds-Of-History',
+  'bind_auth_service_grp' => 'Bind-Auth-Service-Grp',
+  'cisco_abort_cause' => 'Cisco-Abort-Cause',
+  'bg_span_dit' => 'BG_Span_Dis',
   'h323_voice_quality' => 'h323-voice-quality',
-  'usr_rmmie_last_update_ti' => 'USR-RMMIE-Last-Update-Time',
-  'usr_disconnect_cause_ind' => 'USR-Disconnect-Cause-Indicator',
+  'lac_real_port_typf' => 'LAC_Real_Port_Type',
   'usr_channel_connected_to' => 'USR-Channel-Connected-To',
-  'ascend_calling_id_number' => 'Ascend-Calling-Id-Number-Plan',
+  'ascend_client_assign_win' => 'Ascend-Client-Assign-WINS',
+  'redcreek_tunneled_gatewa' => 'RedCreek-Tunneled-Gateway',
   'usr_number_of_fallbacks' => 'USR-Number-of-Fallbacks',
-  'usr_ip_call_output_filte' => 'USR-IP-Call-Output-Filter',
+  'nokia_prepaid_ind' => 'Nokia-Prepaid-Ind',
+  'nomadix_maxbytesup' => 'Nomadix-MaxBytesUp',
+  'login_hosu' => 'Login-Host',
   'ascend_bir_enable' => 'Ascend-BIR-Enable',
   'usr_connect_time_limit' => 'USR-Connect-Time-Limit',
   'ascend_presession_time' => 'Ascend-PreSession-Time',
-  'ascend_private_route_req' => 'Ascend-Private-Route-Required',
-  'ascend_dsl_cir_xmit_limi' => 'Ascend-Dsl-CIR-Xmit-Limit',
+  'altiga_simultaneous_logi' => 'Altiga-Simultaneous-Logins-G/U',
+  'cvpn3000_ipsec_default_d' => 'CVPN3000-IPSec-Default-Domain',
+  'aat_atm_vci' => 'AAT-ATM-VCI',
+  'extreme_netlogin_url_des' => 'Extreme-Netlogin-Url-Desc',
+  'itk_auth_serv_ip' => 'ITK-Auth-Serv-IP',
+  'erx_alternate_cli_vroute' => 'ERX-Alternate-Cli-Vrouter-Name',
   'framed_compression' => 'Framed-Compression',
   'ascend_svc_enabled' => 'Ascend-SVC-Enabled',
   'proxy_state' => 'Proxy-State',
-  'ascend_tunnel_vrouter_na' => 'Ascend-Tunnel-VRouter-Name',
-  'usr_ipx_call_input_filte' => 'USR-IPX-Call-Input-Filter',
-  'x_ascend_assign_ip_globa' => 'X-Ascend-Assign-IP-Global-Pool',
-  'erx_alternate_cli_vroute' => 'ERX-Alternate-Cli-Vrouter-Name',
-  'ascend_dhcp_maximum_leas' => 'Ascend-DHCP-Maximum-Leases',
+  'aat_vrouter_name' => 'AAT-Vrouter-Name',
+  'usr_rmmie_pwrlvl_farecho' => 'USR-RMMIE-PwrLvl-FarEcho-Canc',
+  'nas_poru' => 'NAS-Port',
+  'wispr_location_name' => 'WISPr-Location-Name',
+  'digest_user_name' => 'Digest-User-Name',
   'ascend_modem_shelfno' => 'Ascend-Modem-ShelfNo',
-  'bind_auth_protocol' => 'Bind_Auth_Protocol',
   'shasta_user_privilege' => 'Shasta-User-Privilege',
+  'bind_auth_protocol' => 'Bind-Auth-Protocol',
+  'ascend_home_agent_passwo' => 'Ascend-Home-Agent-Password',
   'acct_interim_interval' => 'Acct-Interim-Interval',
+  'ascend_history_weigh_typ' => 'Ascend-History-Weigh-Type',
+  'ms_link_drop_time_limit' => 'MS-Link-Drop-Time-Limit',
   'hint' => 'Hint',
   'x_ascend_target_util' => 'X-Ascend-Target-Util',
-  'ms_link_drop_time_limit' => 'MS-Link-Drop-Time-Limit',
   'acc_access_partition' => 'Acc-Access-Partition',
-  'x_ascend_multilink_id' => 'X-Ascend-Multilink-ID',
   'usr_power_supply_number' => 'USR-Power-Supply-Number',
-  'acc_ipx_compression' => 'Acc-Ipx-Compression',
+  'x_ascend_multilink_id' => 'X-Ascend-Multilink-ID',
+  'redcreek_tunneled_domain' => 'RedCreek-Tunneled-DomainName',
   'nomadix_bw_down' => 'Nomadix-Bw-Down',
-  'usr_call_reference_numbe' => 'USR-Call-Reference-Number',
+  'acc_ipx_compression' => 'Acc-Ipx-Compression',
+  'quintum_h323_setup_time' => 'Quintum-h323-setup-time',
   'cisco_target_util' => 'Cisco-Target-Util',
-  'usr_back_channel_data_ra' => 'USR-Back-Channel-Data-Rate',
   'acc_ip_gateway_sec' => 'Acc-Ip-Gateway-Sec',
-  'usr_dte_ring_no_answer_l' => 'USR-DTE-Ring-No-Answer-Limit',
-  'usr_connect_time' => 'USR-Connect-Time',
+  'ascend_dsl_cir_xmit_limi' => 'Ascend-Dsl-CIR-Xmit-Limit',
   'ascend_ip_pool_definitio' => 'Ascend-IP-Pool-Definition',
+  'bind_sub_user_at_contexu' => 'Bind_Sub_User_At_Context',
+  'itk_dest_no' => 'ITK-Dest-No',
+  'usr_connect_time' => 'USR-Connect-Time',
   'usr_call_start_date_time' => 'USR-Call-Start-Date-Time',
+  'altiga_l2tp_encryption_g' => 'Altiga-L2TP-Encryption-G',
+  'ascend_auth_delay' => 'Ascend-Auth-Delay',
+  'ascend_x25_pad_x3_profil' => 'Ascend-X25-Pad-X3-Profile',
+  'ascend_access_intercepta' => 'Ascend-Access-Intercept-Log',
+  'ascend_home_agent_udp_po' => 'Ascend-Home-Agent-UDP-Port',
+  'bind_tun_context' => 'Bind-Tun-Context',
   'dialback_name' => 'Dialback-Name',
-  'bind_tun_context' => 'Bind_Tun_Context',
   'h323_redirect_ip_address' => 'h323-redirect-ip-address',
   'annex_keypress_timeout' => 'Annex-Keypress-Timeout',
+  'x_ascend_home_network_na' => 'X-Ascend-Home-Network-Name',
   'ascend_x25_pad_alias_1' => 'Ascend-X25-Pad-Alias-1',
+  'ascend_call_attempt_limi' => 'Ascend-Call-Attempt-Limit',
+  'quintum_h323_currency_ty' => 'Quintum-h323-currency-type',
   'ms_chap_response' => 'MS-CHAP-Response',
+  'st_secondary_nbns_server' => 'ST-Secondary-NBNS-Server',
+  'x_ascend_history_weigh_t' => 'X-Ascend-History-Weigh-Type',
   'usr_max_channels' => 'USR-Max-Channels',
   'ascend_fr_dte_n393' => 'Ascend-FR-DTE-N393',
   'ascend_pre_input_octets' => 'Ascend-Pre-Input-Octets',
   'erx_atm_mbs' => 'ERX-Atm-MBS',
+  'cvpn3000_simultaneous_lo' => 'CVPN3000-Simultaneous-Logins',
+  'juniper_allow_commands' => 'Juniper-Allow-Commands',
   'usr_line_reversals' => 'USR-Line-Reversals',
+  'itk_users_default_pw' => 'ITK-Users-Default-Pw',
   'x_ascend_third_prompt' => 'X-Ascend-Third-Prompt',
+  'cisco_fax_msg_id' => 'Cisco-Fax-Msg-Id',
   'x_ascend_pw_warntime' => 'X-Ascend-PW-Warntime',
   'ascend_data_filter' => 'Ascend-Data-Filter',
   'framed_address' => 'Framed-Address',
   'context_name' => 'Context-Name',
   'usr_send_script2' => 'USR-Send-Script2',
   'ms_arap_pw_change_reason' => 'MS-ARAP-PW-Change-Reason',
+  'tunnel_session_auth_cty' => 'Tunnel_Session_Auth_Ctx',
   'acct_session_id' => 'Acct-Session-Id',
+  'annex_port' => 'Annex-Port',
+  'quintum_h323_call_origin' => 'Quintum-h323-call-origin',
+  'erx_cli_initial_access_l' => 'ERX-Cli-Initial-Access-Level',
+  'x_ascend_shared_profile_' => 'X-Ascend-Shared-Profile-Enable',
+  'tunnel_cmd_timeouu' => 'Tunnel_Cmd_Timeout',
   'initial_modulation_type' => 'Initial-Modulation-Type',
   'ascend_h323_gatekeeper' => 'Ascend-H323-Gatekeeper',
   'x_ascend_fcp_parameter' => 'X-Ascend-FCP-Parameter',
-  'tunnel_type' => 'Tunnel-Type',
   'multi_link_flag' => 'Multi-Link-Flag',
+  'tunnel_type' => 'Tunnel-Type',
+  'erx_output_gigapkts' => 'ERX-Output-Gigapkts',
   'ascend_idle_limit' => 'Ascend-Idle-Limit',
+  'ns_user_group' => 'NS-User-Group',
   'password_retry' => 'Password-Retry',
   'h323_remote_address' => 'h323-remote-address',
   'erx_atm_service_category' => 'ERX-Atm-Service-Category',
   'acct_input_packets' => 'Acct-Input-Packets',
   'h323_disconnect_time' => 'h323-disconnect-time',
-  'ascend_billing_number' => 'Ascend-Billing-Number',
   'usr_syslog_tap' => 'USR-Syslog-Tap',
+  'telebit_accounting_info' => 'Telebit-Accounting-Info',
+  'ascend_billing_number' => 'Ascend-Billing-Number',
+  'ascend_tunnel_vrouter_na' => 'Ascend-Tunnel-VRouter-Name',
   'ms_mppe_encryption_type' => 'MS-MPPE-Encryption-Type',
+  'quintum_h323_credit_amou' => 'Quintum-h323-credit-amount',
+  'acc_ace_token' => 'Acc-Ace-Token',
   'ascend_assign_ip_pool' => 'Ascend-Assign-IP-Pool',
+  'annex_end_modulation' => 'Annex-End-Modulation',
   'usr_routing_protocol' => 'USR-Routing-Protocol',
+  'cvx_assign_ip_pool' => 'CVX-Assign-IP-Pool',
   'usr_rad_location_type' => 'USR-Rad-Location-Type',
+  'usr_rmmie_pwrlvl_noise_l' => 'USR-RMMIE-PwrLvl-Noise-Lvl',
   'usr_characters_sent' => 'USR-Characters-Sent',
   'usr_mp_edo_hiper' => 'USR-MP-EDO-HIPER',
+  'ascend_x25_nui_password_' => 'Ascend-X25-Nui-Password-Prompt',
   'annex_host_restrict' => 'Annex-Host-Restrict',
   'user_service_type' => 'User-Service-Type',
   'acct_multi_session_id' => 'Acct-Multi-Session-Id',
   'ms_chap_cpw_2' => 'MS-CHAP-CPW-2',
-  'x_ascend_primary_home_ag' => 'X-Ascend-Primary-Home-Agent',
+  'x_ascend_secondary_home_' => 'X-Ascend-Secondary-Home-Agent',
   'x_ascend_dialout_allowed' => 'X-Ascend-Dialout-Allowed',
   'ascend_connect_progress' => 'Ascend-Connect-Progress',
   'x_ascend_ara_pw' => 'X-Ascend-Ara-PW',
+  'cisco_fax_modem_time' => 'Cisco-Fax-Modem-Time',
+  'sql_group' => 'Sql-Group',
+  'annex_multicast_rate_lim' => 'Annex-Multicast-Rate-Limit',
+  'cvpn3000_user_auth_servg' => 'CVPN3000-User-Auth-Server-Secret',
   'ns_mta_md5_password' => 'NS-MTA-MD5-Password',
+  'annex_addr_resolution_pr' => 'Annex-Addr-Resolution-Protocol',
   'callback_number' => 'Callback-Number',
-  'acct_output_packets_64' => 'Acct_Output_Packets_64',
+  'cvx_multilink_match_info' => 'CVX-Multilink-Match-Info',
+  'tunnel_max_tunnelt' => 'Tunnel_Max_Tunnels',
+  'tunnel_local_namf' => 'Tunnel_Local_Name',
+  'quintum_h323_conf_id' => 'Quintum-h323-conf-id',
+  'acct_output_packets_64' => 'Acct-Output-Packets-64',
+  'annex_signal_to_noise_ra' => 'Annex-Signal-to-Noise-Ratio',
+  'acct_output_packets_65' => 'Acct_Output_Packets_64',
   'x_ascend_user_acct_key' => 'X-Ascend-User-Acct-Key',
+  'erx_dial_out_number' => 'ERX-Dial-Out-Number',
   'ascend_modem_portno' => 'Ascend-Modem-PortNo',
   'ascend_assign_ip_server' => 'Ascend-Assign-IP-Server',
   'ascend_fcp_parameter' => 'Ascend-FCP-Parameter',
-  'ascend_inter_arrival_jit' => 'Ascend-Inter-Arrival-Jitter',
+  'usr_chassis_temp_thresho' => 'USR-Chassis-Temp-Threshold',
+  'usr_mpip_tunnel_originat' => 'USR-MPIP-Tunnel-Originator',
+  'tunnel_rate_limit_bursu' => 'Tunnel_Rate_Limit_Burst',
   'client_ip_address' => 'Client-IP-Address',
-  'usr_number_of_link_timeo' => 'USR-Number-of-Link-Timeouts',
-  'ascend_dsl_cir_recv_limi' => 'Ascend-Dsl-CIR-Recv-Limit',
+  'le_nat_tcp_session_timeo' => 'LE-NAT-TCP-Session-Timeout',
+  'quintum_h323_redirect_ip' => 'Quintum-h323-redirect-ip-address',
   'ms_acct_eap_type' => 'MS-Acct-EAP-Type',
-  'x_ascend_user_acct_type' => 'X-Ascend-User-Acct-Type',
   'usr_rmmie_x2_status' => 'USR-RMMIE-x2-Status',
-  'ascend_dsl_downstream_li' => 'Ascend-Dsl-Downstream-Limit',
+  'x_ascend_user_acct_type' => 'X-Ascend-User-Acct-Type',
   'shiva_customer_id' => 'Shiva-Customer-Id',
-  'lac_real_port' => 'LAC_Real_Port',
+  'pvc_encapsulation_typf' => 'PVC_Encapsulation_Type',
+  'st_acct_vc_connection_id' => 'ST-Acct-VC-Connection-Id',
+  'lac_real_port' => 'LAC-Real-Port',
   'h323_connect_time' => 'h323-connect-time',
-  'old_password' => 'Old-Password',
   'usr_vpn_gw_location_id' => 'USR-VPN-GW-Location-Id',
+  'old_password' => 'Old-Password',
   'x_ascend_if_netmask' => 'X-Ascend-IF-Netmask',
   'add_suffix' => 'Add-Suffix',
-  'x_ascend_client_assign_d' => 'X-Ascend-Client-Assign-DNS',
-  'usr_q931_call_reference_' => 'USR-Q931-Call-Reference-Value',
+  'lac_port_typf' => 'LAC_Port_Type',
+  'acc_ip_pool_name' => 'Acc-Ip-Pool-Name',
   'usr_terminal_type' => 'USR-Terminal-Type',
   'usr_spoofing' => 'USR-Spoofing',
   'erx_tunnel_password' => 'ERX-Tunnel-Password',
-  'ascend_assign_ip_client' => 'Ascend-Assign-IP-Client',
+  'ascend_inter_arrival_jit' => 'Ascend-Inter-Arrival-Jitter',
+  'ascend_call_block_durati' => 'Ascend-Call-Block-Duration',
+  'itk_channel_binding' => 'ITK-Channel-Binding',
   'usr_server_time' => 'USR-Server-Time',
+  'ascend_assign_ip_client' => 'Ascend-Assign-IP-Client',
+  'erx_pppoe_max_sessions' => 'ERX-Pppoe-Max-Sessions',
+  'cvx_multilink_group_numb' => 'CVX-Multilink-Group-Number',
+  'x_ascend_client_assign_d' => 'X-Ascend-Client-Assign-DNS',
+  'erx_pppoe_url' => 'ERX-Pppoe-Url',
+  'police_ratf' => 'Police_Rate',
   'ascend_data_svc' => 'Ascend-Data-Svc',
   'annex_authen_servers' => 'Annex-Authen-Servers',
   'nomadix_bw_up' => 'Nomadix-Bw-Up',
+  'cvx_modem_data_compressi' => 'CVX-Modem-Data-Compression',
   'shiva_link_speed' => 'Shiva-Link-Speed',
   'usr_reply_script6' => 'USR-Reply-Script6',
   'usr_expansion_algorithm' => 'USR-Expansion-Algorithm',
-  'x_ascend_mpp_idle_percen' => 'X-Ascend-MPP-Idle-Percent',
+  'cabletron_protocol_calla' => 'Cabletron-Protocol-Callable',
   'cisco_data_rate' => 'Cisco-Data-Rate',
   'usr_primary_dns_server' => 'USR-Primary_DNS_Server',
-  'erx_local_loopback_inter' => 'ERX-Local-Loopback-Interface',
+  'juniper_deny_configurati' => 'Juniper-Deny-Configuration',
   'ascend_target_util' => 'Ascend-Target-Util',
-  'usr_default_dte_data_rat' => 'USR-Default-DTE-Data-Rate',
+  'digest_method' => 'Digest-Method',
+  'altiga_ipsec_split_tunne' => 'Altiga-IPSec-Split-Tunnel-List-G',
+  'erx_alternate_cli_access' => 'ERX-Alternate-Cli-Access-Level',
   'x_ascend_event_type' => 'X-Ascend-Event-Type',
+  'usr_q931_call_reference_' => 'USR-Q931-Call-Reference-Value',
   'usr_mp_mrru' => 'USR-MP-MRRU',
-  'bind_bypass_context' => 'Bind_Bypass_Context',
+  'cvx_ipsvc_mask' => 'CVX-IPSVC-Mask',
+  'bind_bypass_context' => 'Bind-Bypass-Context',
+  'usr_rmmie_last_update_ev' => 'USR-RMMIE-Last-Update-Event',
   'no_such_attribute' => 'No-Such-Attribute',
-  'acct_mcast_out_packets' => 'Acct_Mcast_Out_Packets',
+  'acct_mcast_out_packets' => 'Acct-Mcast-Out-Packets',
   'tunnel_medium_type' => 'Tunnel-Medium-Type',
+  'quintum_h323_remote_addr' => 'Quintum-h323-remote-address',
   'acc_callback_delay' => 'Acc-Callback-Delay',
-  'x_ascend_home_agent_udp_' => 'X-Ascend-Home-Agent-UDP-Port',
-  'acct_input_octets_64' => 'Acct_Input_Octets_64',
+  'acct_input_octets_64' => 'Acct-Input-Octets-64',
+  'ascend_base_channel_coun' => 'Ascend-Base-Channel-Count',
   'ascend_atm_connect_vci' => 'Ascend-ATM-Connect-Vci',
   'erx_primary_dns' => 'ERX-Primary-Dns',
+  'altiga_ipsec_over_nat_g' => 'Altiga-IPSec-Over-NAT-G',
+  'cvx_multicast_rate_limit' => 'CVX-Multicast-Rate-Limit',
   'ascend_xmit_rate' => 'Ascend-Xmit-Rate',
   'ms_new_arap_password' => 'MS-New-ARAP-Password',
   'usr_call_error_code' => 'USR-Call-Error-Code',
   'acct_output_octets' => 'Acct-Output-Octets',
-  'usr_failure_to_connect_r' => 'USR-Failure-to-Connect-Reason',
+  'ascend_client_primary_wi' => 'Ascend-Client-Primary-WINS',
+  'cvpn3000_primary_wins' => 'CVPN3000-Primary-WINS',
+  'bintec_ipextrttable' => 'BinTec-ipExtRtTable',
+  'cisco_fax_mdn_flag' => 'Cisco-Fax-Mdn-Flag',
+  'ascend_destination_nas_p' => 'Ascend-Destination-Nas-Port',
   'ascend_num_in_multilink' => 'Ascend-Num-In-Multilink',
+  'digest_attributes' => 'Digest-Attributes',
+  'cvpn3000_ipsec_tunnel_ty' => 'CVPN3000-IPSec-Tunnel-Type',
   'x_ascend_number_sessions' => 'X-Ascend-Number-Sessions',
   'usr_ip_rip_output_filter' => 'USR-IP-RIP-Output-Filter',
-  'usr_chassis_temp_thresho' => 'USR-Chassis-Temp-Threshold',
+  'tunnel_police_bursu' => 'Tunnel_Police_Burst',
+  'redcreek_tunneled_wins_s' => 'RedCreek-Tunneled-WINS-Server1',
   'usr_blocks_sent' => 'USR-Blocks-Sent',
+  'erx_cli_allow_all_vr_acc' => 'ERX-Cli-Allow-All-VR-Access',
+  'tunnel_police_ratf' => 'Tunnel_Police_Rate',
   'usr_ids0_call_type' => 'USR-IDS0-Call-Type',
   'acc_ccp_option' => 'Acc-Ccp-Option',
   'ascend_client_gateway' => 'Ascend-Client-Gateway',
-  'x_ascend_multicast_rate_' => 'X-Ascend-Multicast-Rate-Limit',
+  'cvx_maximum_channels' => 'CVX-Maximum-Channels',
+  'bg_aging_timf' => 'BG_Aging_Time',
+  'annex_secondary_dns_serv' => 'Annex-Secondary-DNS-Server',
   'le_ipsec_passive_profile' => 'LE-IPSec-Passive-Profile',
   'usr_chassis_call_span' => 'USR-Chassis-Call-Span',
-  'usr_mobileip_home_agent_' => 'USR-MobileIP-Home-Agent-Address',
+  'aat_client_primary_wins_' => 'AAT-Client-Primary-WINS-NBNS',
+  'h323_currency' => 'h323-currency',
   'password' => 'Password',
   'le_nat_log_options' => 'LE-NAT-Log-Options',
-  'x_ascend_ppp_address' => 'X-Ascend-PPP-Address',
   'usr_fallback_limit' => 'USR-Fallback-Limit',
+  'x_ascend_ppp_address' => 'X-Ascend-PPP-Address',
   'suffix' => 'Suffix',
   'usr_multicast_receive' => 'USR-Multicast-Receive',
-  'client_dns_sec' => 'Client_DNS_Sec',
+  'client_dns_sec' => 'Client-DNS-Sec',
   'annex_product_name' => 'Annex-Product-Name',
   'cisco_pw_lifetime' => 'Cisco-PW-Lifetime',
   'x_ascend_fr_dce_n393' => 'X-Ascend-FR-DCE-N393',
   'x_ascend_ts_idle_limit' => 'X-Ascend-TS-Idle-Limit',
-  'usr_last_number_dialed_o' => 'USR-Last-Number-Dialed-Out',
-  'mcast_send' => 'Mcast_Send',
-  'pppoe_motm' => 'PPPOE_MOTM',
+  'mcast_send' => 'Mcast-Send',
+  'x_ascend_primary_home_ag' => 'X-Ascend-Primary-Home-Agent',
+  'tunnel_max_sessiont' => 'Tunnel_Max_Sessions',
+  'pppoe_motm' => 'PPPOE-MOTM',
   'usr_pw_usr_ifilter_ipx' => 'USR-PW_USR_IFilter_IPX',
-  'usr_pw_tunnel_authentica' => 'USR-PW_Tunnel_Authentication',
-  'ascend_source_ip_check' => 'Ascend-Source-IP-Check',
-  'ascend_assign_ip_global_' => 'Ascend-Assign-IP-Global-Pool',
   'ms_ras_version' => 'MS-RAS-Version',
-  'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-Ttl',
-  'x_ascend_modem_slotno' => 'X-Ascend-Modem-SlotNo',
+  'ascend_source_ip_check' => 'Ascend-Source-IP-Check',
+  'bintec_ospfiftable' => 'BinTec-ospfIfTable',
   'acc_ml_call_threshold' => 'Acc-ML-Call-Threshold',
+  'x_ascend_modem_slotno' => 'X-Ascend-Modem-SlotNo',
   'ascend_menu_item' => 'Ascend-Menu-Item',
-  'usr_cdma_call_reference_' => 'USR-CDMA-Call-Reference-Number',
   'callback_id' => 'Callback-Id',
   'framed_ipx_network' => 'Framed-IPX-Network',
-  'x_ascend_disconnect_caus' => 'X-Ascend-Disconnect-Cause',
+  'altiga_pptp_encryption_g' => 'Altiga-PPTP-Encryption-G',
+  'ascend_x25_reverse_charg' => 'Ascend-X25-Reverse-Charging',
   'ascend_user_acct_key' => 'Ascend-User-Acct-Key',
   'x_ascend_pw_lifetime' => 'X-Ascend-PW-Lifetime',
   'user_name_is_star' => 'User-Name-Is-Star',
-  'x_ascend_authen_alias' => 'X-Ascend-Authen-Alias',
+  'nomadix_url_redirection' => 'Nomadix-URL-Redirection',
   'framed_pool' => 'Framed-Pool',
+  'x_ascend_authen_alias' => 'X-Ascend-Authen-Alias',
+  'cisco_fax_dsn_address' => 'Cisco-Fax-Dsn-Address',
   'ms_primary_dns_server' => 'MS-Primary-DNS-Server',
+  'acc_dialout_auth_usernam' => 'Acc-Dialout-Auth-Username',
   'realm' => 'Realm',
   'arap_features' => 'ARAP-Features',
+  'bind_auth_protocom' => 'Bind_Auth_Protocol',
   'acc_connect_tx_speed' => 'Acc-Connect-Tx-Speed',
-  'usr_last_number_dialed_i' => 'USR-Last-Number-Dialed-In-DNIS',
   'usr_chassis_temperature' => 'USR-Chassis-Temperature',
+  'altiga_ipsec_mode_config' => 'Altiga-IPSec-Mode-Config-G',
+  'ascend_home_agent_ip_add' => 'Ascend-Home-Agent-IP-Addr',
   'x_ascend_xmit_rate' => 'X-Ascend-Xmit-Rate',
+  'cvpn3000_secondary_dns' => 'CVPN3000-Secondary-DNS',
   'x_ascend_send_passwd' => 'X-Ascend-Send-Passwd',
+  'bind_int_contexu' => 'Bind_Int_Context',
+  'cisco_fax_account_id_ori' => 'Cisco-Fax-Account-Id-Origin',
   'le_modem_info' => 'LE-Modem-Info',
   'ascend_ipx_peer_mode' => 'Ascend-IPX-Peer-Mode',
-  'le_nat_other_session_tim' => 'LE-NAT-Other-Session-Timeout',
-  'tunnel_rate_limit_rate' => 'Tunnel_Rate_Limit_Rate',
-  'ascend_maximum_call_dura' => 'Ascend-Maximum-Call-Duration',
+  'juniper_local_user_name' => 'Juniper-Local-User-Name',
+  'tunnel_rate_limit_rate' => 'Tunnel-Rate-Limit-Rate',
+  'quintum_h323_credit_time' => 'Quintum-h323-credit-time',
+  'acc_modem_modulation_typ' => 'Acc-Modem-Modulation-Type',
+  'x_ascend_seconds_of_hist' => 'X-Ascend-Seconds-Of-History',
   'ascend_dhcp_pool_number' => 'Ascend-DHCP-Pool-Number',
+  'redcreek_tunneled_ip_net' => 'RedCreek-Tunneled-IP-Netmask',
   'x_ascend_callback' => 'X-Ascend-Callback',
-  'ascend_access_intercept_' => 'Ascend-Access-Intercept-Log',
   'usr_iwf_ip_address' => 'USR-IWF-IP-Address',
+  'aat_input_octets_diff' => 'AAT-Input-Octets-Diff',
   'nas_port_id' => 'NAS-Port-Id',
   'le_advice_of_charge' => 'LE-Advice-of-Charge',
+  'x_ascend_dhcp_pool_numbe' => 'X-Ascend-DHCP-Pool-Number',
   'ascend_add_seconds' => 'Ascend-Add-Seconds',
   'annex_transmit_speed' => 'Annex-Transmit-Speed',
   'usr_port_tap' => 'USR-Port-Tap',
   'usr_at_call_input_filter' => 'USR-AT-Call-Input-Filter',
+  'framed_ipv6_pool' => 'Framed-IPv6-Pool',
   'ascend_qos_downstream' => 'Ascend-QOS-Downstream',
-  'ascend_x25_reverse_charg' => 'Ascend-X25-Reverse-Charging',
-  'lac_port' => 'LAC_Port',
+  'lac_port' => 'LAC-Port',
   'tunnel_assignment_id' => 'Tunnel-Assignment-Id',
+  'acct_mcast_out_octett' => 'Acct_Mcast_Out_Octets',
+  'ascend_bi_directional_au' => 'Ascend-Bi-Directional-Auth',
   'fall_through' => 'Fall-Through',
+  'cvpn3000_ipsec_ip_compre' => 'CVPN3000-IPSec-IP-Compression',
   'cisco_disconnect_cause' => 'Cisco-Disconnect-Cause',
-  'module_message' => 'Module-Message',
+  'usr_rad_multicast_routiq' => 'USR-Rad-Multicast-Routing-Bound',
+  'altiga_tunneling_protoco' => 'Altiga-Tunneling-Protocols-G/U',
+  'itk_tunnel_prot' => 'ITK-Tunnel-Prot',
+  'client_dns_sed' => 'Client_DNS_Sec',
   'framed_ip_netmask' => 'Framed-IP-Netmask',
+  'usr_call_reference_numbe' => 'USR-Call-Reference-Number',
   'ascend_egress_enabled' => 'Ascend-Egress-Enabled',
   'ascend_dsl_rate_mode' => 'Ascend-Dsl-Rate-Mode',
-  'x_ascend_client_primary_' => 'X-Ascend-Client-Primary-DNS',
   'usr_pw_usr_ofilter_sap' => 'USR-PW_USR_OFilter_SAP',
+  'bintec_iproutetable' => 'BinTec-ipRouteTable',
   'acct_terminate_cause' => 'Acct-Terminate-Cause',
   'x_ascend_fr_dte_n393' => 'X-Ascend-FR-DTE-N393',
-  'x_ascend_call_block_dura' => 'X-Ascend-Call-Block-Duration',
   'ascend_ppp_address' => 'Ascend-PPP-Address',
+  'erx_maximum_bps' => 'ERX-Maximum-BPS',
   'caller_id' => 'Caller-ID',
-  'bind_int_interface_name' => 'Bind_Int_Interface_Name',
-  'x_ascend_ppp_vj_slot_com' => 'X-Ascend-PPP-VJ-Slot-Comp',
+  'bintec_ipfiltertable' => 'BinTec-ipFilterTable',
+  'x_ascend_base_channel_co' => 'X-Ascend-Base-Channel-Count',
+  'bind_int_interface_name' => 'Bind-Int-Interface-Name',
   'usr_modem_group' => 'USR-Modem-Group',
   'cisco_maximum_channels' => 'Cisco-Maximum-Channels',
+  'erx_ppp_username' => 'ERX-PPP-Username',
   'ascend_link_compression' => 'Ascend-Link-Compression',
+  'annex_retransmitted_pack' => 'Annex-Retransmitted-Packets',
   'usr_retrains_granted' => 'USR-Retrains-Granted',
   'ascend_dropped_packets' => 'Ascend-Dropped-Packets',
+  'erx_bearer_type' => 'ERX-Bearer-Type',
   'usr_pw_usr_ofilter_ip' => 'USR-PW_USR_OFilter_IP',
   'quintum_nas_port' => 'Quintum-NAS-Port',
+  'x_ascend_pre_output_pack' => 'X-Ascend-Pre-Output-Packets',
+  'usr_cdma_call_reference_' => 'USR-CDMA-Call-Reference-Number',
+  'tunnel_function' => 'Tunnel-Function',
   'annex_tunnel_authen_mode' => 'Annex-Tunnel-Authen-Mode',
-  'tunnel_function' => 'Tunnel_Function',
   'usr_mp_edo' => 'USR-MP-EDO',
   'le_nat_outmap' => 'LE-NAT-Outmap',
+  'cvpn3000_primary_dns' => 'CVPN3000-Primary-DNS',
   'usr_modulation_type' => 'USR-Modulation-Type',
+  'ascend_calling_id_screen' => 'Ascend-Calling-Id-Screening',
   'ascend_maximum_time' => 'Ascend-Maximum-Time',
+  'user_password' => 'User-Password',
   'annex_callback_portlist' => 'Annex-Callback-Portlist',
-  'x_ascend_remove_seconds' => 'X-Ascend-Remove-Seconds',
+  'cvpn3000_ipsec_split_tun' => 'CVPN3000-IPSec-Split-Tunnel-List',
+  'annex_pre_output_packets' => 'Annex-Pre-Output-Packets',
+  'usr_at_call_output_filte' => 'USR-AT-Call-Output-Filter',
+  'x_ascend_client_primary_' => 'X-Ascend-Client-Primary-DNS',
   'tunnel_server_endpoint' => 'Tunnel-Server-Endpoint',
+  'x_ascend_remove_seconds' => 'X-Ascend-Remove-Seconds',
+  'cvpn3000_user_auth_serve' => 'CVPN3000-User-Auth-Server-Name',
   'arap_password' => 'ARAP-Password',
+  'x_ascend_assign_ip_serve' => 'X-Ascend-Assign-IP-Server',
+  'cisco_fax_pages' => 'Cisco-Fax-Pages',
   'ms_chap_mppe_keys' => 'MS-CHAP-MPPE-Keys',
   'ascend_source_auth' => 'Ascend-Source-Auth',
   'group' => 'Group',
   'usr_send_script6' => 'USR-Send-Script6',
   'le_nat_inmap' => 'LE-NAT-Inmap',
   'chap_password' => 'CHAP-Password',
-  'annex_primary_nbns_serve' => 'Annex-Primary-NBNS-Server',
   'annex_receive_speed' => 'Annex-Receive-Speed',
-  'usr_rmmie_manufacturer_i' => 'USR-RMMIE-Manufacturer-ID',
-  'bind_l2tp_flow_control' => 'Bind_L2TP_Flow_Control',
+  'usr_mobileip_home_agent_' => 'USR-MobileIP-Home-Agent-Address',
+  'bind_l2tp_flow_control' => 'Bind-L2TP-Flow-Control',
   'smb_account_ctrl' => 'SMB-Account-CTRL',
-  'ascend_calling_id_presen' => 'Ascend-Calling-Id-Presentatn',
   'ascend_ip_pool_chaining' => 'Ascend-IP-Pool-Chaining',
   'le_admin_group' => 'LE-Admin-Group',
-  'nas_identifier' => 'NAS-Identifier',
-  'x_ascend_history_weigh_t' => 'X-Ascend-History-Weigh-Type',
   'tunnel_connection_id' => 'Tunnel-Connection-Id',
-  'nas_real_port' => 'NAS_Real_Port',
+  'tunnel_windox' => 'Tunnel_Window',
+  'nas_identifier' => 'NAS-Identifier',
+  'dhcp_max_leaset' => 'DHCP_Max_Leases',
+  'digest_nonce_count' => 'Digest-Nonce-Count',
+  'nas_real_port' => 'NAS-Real-Port',
   'ms_old_arap_password' => 'MS-Old-ARAP-Password',
-  'usr_ip_rip_simple_auth_p' => 'USR-IP-RIP-Simple-Auth-Password',
-  'erx_primary_wins' => 'ERX-Primary-Wins',
   'usr_pw_index' => 'USR-PW_Index',
-  'erx_cli_allow_all_vr_acc' => 'ERX-Cli-Allow-All-VR-Access',
+  'erx_primary_wins' => 'ERX-Primary-Wins',
+  'ascend_appletalk_peer_mo' => 'Ascend-Appletalk-Peer-Mode',
   'le_ipsec_log_options' => 'LE-IPSec-Log-Options',
-  'ascend_home_agent_ip_add' => 'Ascend-Home-Agent-IP-Addr',
+  'x_ascend_maximum_channel' => 'X-Ascend-Maximum-Channels',
+  'cvx_ipsvc_aznlvl' => 'CVX-IPSVC-AZNLVL',
+  'x_ascend_client_secondar' => 'X-Ascend-Client-Secondary-DNS',
   'annex_re_chap_timeout' => 'Annex-Re-CHAP-Timeout',
-  'usr_final_tx_link_data_r' => 'USR-Final-Tx-Link-Data-Rate',
-  'client_dns_pri' => 'Client_DNS_Pri',
+  'aat_ip_pool_definition' => 'AAT-IP-Pool-Definition',
+  'client_dns_pri' => 'Client-DNS-Pri',
+  'cisco_service_info' => 'Cisco-Service-Info',
   'usr_primary_nbns_server' => 'USR-Primary_NBNS_Server',
-  'usr_cusr_hat_script_rule' => 'USR-CUSR-hat-Script-Rules',
-  'ascend_multicast_rate_li' => 'Ascend-Multicast-Rate-Limit',
-  'usr_rmmie_pwrlvl_farecho' => 'USR-RMMIE-PwrLvl-FarEcho-Canc',
+  'aat_atm_direct' => 'AAT-ATM-Direct',
+  'bind_ses_contexu' => 'Bind_Ses_Context',
+  'sip_translated_request_u' => 'Sip-Translated-Request-URI',
   'acc_acct_on_off_reason' => 'Acc-Acct-On-Off-Reason',
   'le_multicast_client' => 'LE-Multicast-Client',
+  'bind_sub_passwore' => 'Bind_Sub_Password',
+  'cvpn3000_cisco_ip_phone_' => 'CVPN3000-Cisco-IP-Phone-Bypass',
   'ascend_send_passwd' => 'Ascend-Send-Passwd',
-  'annex_unauthenticated_ti' => 'Annex-Unauthenticated-Time',
-  'tunnel_context' => 'Tunnel_Context',
-  'acc_nbns_server_sec' => 'Acc-Nbns-Server-Sec',
+  'tunnel_remote_namf' => 'Tunnel_Remote_Name',
+  'cvx_disconnect_cause' => 'CVX-Disconnect-Cause',
+  'itk_auth_serv_prot' => 'ITK-Auth-Serv-Prot',
+  'tunnel_context' => 'Tunnel-Context',
+  'digest_uri' => 'Digest-URI',
   'usr_channel_decrement' => 'USR-Channel-Decrement',
-  'usr_rmmie_firmware_versi' => 'USR-RMMIE-Firmware-Version',
+  'acc_nbns_server_sec' => 'Acc-Nbns-Server-Sec',
   'ms_chap_challenge' => 'MS-CHAP-Challenge',
-  'x_ascend_client_secondar' => 'X-Ascend-Client-Secondary-DNS',
+  'cisco_assign_ip_pool' => 'Cisco-Assign-IP-Pool',
   'ascend_cbcp_mode' => 'Ascend-CBCP-Mode',
   'ascend_x25_rpoa' => 'Ascend-X25-Rpoa',
   'usr_dtr_false_timeout' => 'USR-DTR-False-Timeout',
-  'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-Proto',
-  'ascend_x25_pad_x3_parame' => 'Ascend-X25-Pad-X3-Parameters',
+  'acct_dyn_ac_enu' => 'Acct_Dyn_Ac_Ent',
   'usr_physical_state' => 'USR-Physical-State',
+  'x_ascend_ppp_vj_slot_com' => 'X-Ascend-PPP-VJ-Slot-Comp',
+  'x_ascend_link_compressio' => 'X-Ascend-Link-Compression',
   'ascend_fr_t391' => 'Ascend-FR-T391',
-  'bind_dot1q_port' => 'Bind_Dot1q_Port',
-  'lac_port_type' => 'LAC_Port_Type',
-  'bg_aging_time' => 'BG_Aging_Time',
+  'bind_dot1q_port' => 'Bind-Dot1q-Port',
+  'ns_secondary_dns' => 'NS-Secondary-DNS',
+  'altiga_ipsec_tunnel_type' => 'Altiga-IPSec-Tunnel-Type-G',
+  'lac_port_type' => 'LAC-Port-Type',
+  'bg_aging_time' => 'BG-Aging-Time',
   'erx_atm_scr' => 'ERX-Atm-SCR',
+  'x_ascend_pre_input_octet' => 'X-Ascend-Pre-Input-Octets',
+  'cisco_fax_connect_speed' => 'Cisco-Fax-Connect-Speed',
   'x_ascend_menu_item' => 'X-Ascend-Menu-Item',
+  'quintum_h323_voice_quali' => 'Quintum-h323-voice-quality',
   'ascend_x25_pad_banner' => 'Ascend-X25-Pad-Banner',
+  'module_failure_message' => 'Module-Failure-Message',
   'h323_gw_id' => 'h323-gw-id',
   'h323_preferred_lang' => 'h323-preferred-lang',
   'usr_min_compression_size' => 'USR-Min-Compression-Size',
   'usr_compression_type' => 'USR-Compression-Type',
-  'x_ascend_call_attempt_li' => 'X-Ascend-Call-Attempt-Limit',
+  'bintec_ipxstaticroutetab' => 'BinTec-ipxStaticRouteTable',
   'ascend_dialout_allowed' => 'Ascend-Dialout-Allowed',
   'annex_local_username' => 'Annex-Local-Username',
   'cisco_pre_input_packets' => 'Cisco-Pre-Input-Packets',
-  'ascend_send_secret' => 'Ascend-Send-Secret',
   'shiva_function' => 'Shiva-Function',
-  'usr_dte_data_idle_timout' => 'USR-DTE-Data-Idle-Timout',
+  'ascend_send_secret' => 'Ascend-Send-Secret',
   'usr_number_of_blers' => 'USR-Number-of-Blers',
+  'usr_dte_data_idle_timout' => 'USR-DTE-Data-Idle-Timout',
   'usr_card_type' => 'USR-Card-Type',
-  'ascend_token_idle' => 'Ascend-Token-Idle',
+  'x_ascend_connect_progres' => 'X-Ascend-Connect-Progress',
   'x_ascend_group' => 'X-Ascend-Group',
+  'ascend_token_idle' => 'Ascend-Token-Idle',
+  'erx_qos_profile_interfac' => 'ERX-Qos-Profile-Interface-Type',
+  'ascend_private_route_tab' => 'Ascend-Private-Route-Table-ID',
   'nt_password' => 'NT-Password',
-  'acct_mcast_in_packets' => 'Acct_Mcast_In_Packets',
+  'acct_mcast_in_packets' => 'Acct-Mcast-In-Packets',
+  'x_ascend_multicast_clien' => 'X-Ascend-Multicast-Client',
   'usr_supports_tags' => 'USR-Supports-Tags',
+  'cvpn3000_authd_user_idle' => 'CVPN3000-Authd-User-Idle-Timeout',
   'ascend_number_sessions' => 'Ascend-Number-Sessions',
   'x_ascend_add_seconds' => 'X-Ascend-Add-Seconds',
   'usr_number_of_upshifts' => 'USR-Number-of-Upshifts',
   'proxy_to_realm' => 'Proxy-To-Realm',
+  'aat_client_secondary_win' => 'AAT-Client-Secondary-WINS-NBNS',
+  'aat_ip_tos_precedence' => 'AAT-IP-TOS-Precedence',
   'acc_callback_num_valid' => 'Acc-Callback-Num-Valid',
-  'x_ascend_maximum_channel' => 'X-Ascend-Maximum-Channels',
+  'nokia_ggsn_ip_address' => 'Nokia-GGSN-IP-Address',
   'acc_access_community' => 'Acc-Access-Community',
-  'x_ascend_fr_direct_profi' => 'X-Ascend-FR-Direct-Profile',
+  'ascend_multicast_rate_li' => 'Ascend-Multicast-Rate-Limit',
+  'usr_default_dte_data_rat' => 'USR-Default-DTE-Data-Rate',
+  'usr_rmmie_pwrlvl_nearech' => 'USR-RMMIE-PwrLvl-NearEcho-Canc',
   'usr_send_name' => 'USR-Send-Name',
   'usr_chassis_slot' => 'USR-Chassis-Slot',
   'login_ip_host' => 'Login-IP-Host',
   'ascend_netware_timeout' => 'Ascend-Netware-timeout',
+  'bind_sub_user_at_context' => 'Bind-Sub-User-At-Context',
   'vendor_specific' => 'Vendor-Specific',
-  'bind_sub_user_at_context' => 'Bind_Sub_User_At_Context',
   'ascend_fr_direct_dlci' => 'Ascend-FR-Direct-DLCI',
-  'ascend_atm_fault_managem' => 'Ascend-ATM-Fault-Management',
   'ascend_qos_upstream' => 'Ascend-QOS-Upstream',
-  'source_validation' => 'Source_Validation',
+  'aat_user_mac_address' => 'AAT-User-MAC-Address',
+  'source_validation' => 'Source-Validation',
   'x_ascend_token_expiry' => 'X-Ascend-Token-Expiry',
+  'altiga_ipsec_user_group_' => 'Altiga-IPSec-User-Group-Lock-G',
   'ascend_dec_channel_count' => 'Ascend-Dec-Channel-Count',
+  'assigned_ip_addrest' => 'Assigned_IP_Address',
   'usr_local_framed_ip_addr' => 'USR-Local-Framed-IP-Addr',
   'usr_service_option' => 'USR-Service-Option',
   'usr_transmit_acc_map' => 'USR-Transmit-Acc-Map',
   'ascend_fr_direct' => 'Ascend-FR-Direct',
+  'usr_final_rx_link_data_r' => 'USR-Final-Rx-Link-Data-Rate',
   'x_ascend_expect_callback' => 'X-Ascend-Expect-Callback',
+  'x_ascend_disconnect_caus' => 'X-Ascend-Disconnect-Cause',
   'acc_ml_damping_factor' => 'Acc-ML-Damping-Factor',
   'framed_netmask' => 'Framed-Netmask',
   'usr_connect_speed' => 'USR-Connect-Speed',
-  'ascend_client_primary_wi' => 'Ascend-Client-Primary-WINS',
+  'x_ascend_home_agent_ip_a' => 'X-Ascend-Home-Agent-IP-Addr',
+  'usr_disconnect_cause_ind' => 'USR-Disconnect-Cause-Indicator',
+  'bg_span_dis' => 'BG-Span-Dis',
   'cisco_multilink_id' => 'Cisco-Multilink-ID',
-  'bg_span_dis' => 'BG_Span_Dis',
+  'tunnel_max_tunnels' => 'Tunnel-Max-Tunnels',
+  'ascend_dsl_downstream_li' => 'Ascend-Dsl-Downstream-Limit',
   'ascend_multilink_id' => 'Ascend-Multilink-ID',
-  'tunnel_max_tunnels' => 'Tunnel_Max_Tunnels',
+  'altiga_ipsec_default_dom' => 'Altiga-IPSec-Default-Domain-G',
   'ascend_dhcp_reply' => 'Ascend-DHCP-Reply',
+  'login_ipv6_host' => 'Login-IPv6-Host',
   'ascend_x25_cug' => 'Ascend-X25-Cug',
   'shiva_network_protocols' => 'Shiva-Network-Protocols',
+  'cvpn3000_ipsec_mode_conf' => 'CVPN3000-IPSec-Mode-Config',
+  'extreme_netlogin_vlan' => 'Extreme-Netlogin-Vlan',
   'ascend_ara_pw' => 'Ascend-Ara-PW',
-  'ip_host_addr' => 'Ip_Host_Addr',
+  'tunnel_l2f_second_passwo' => 'Tunnel-L2F-Second-Password',
+  'altiga_sep_card_assignme' => 'Altiga-SEP-Card-Assignment-G/U',
+  'ip_host_addr' => 'Ip-Host-Addr',
   'le_ip_gateway' => 'LE-IP-Gateway',
   'usr_mobile_numbytes_txed' => 'USR-Mobile-NumBytes-Txed',
+  'altiga_ipsec_allow_passw' => 'Altiga-IPSec-Allow-Passwd-Store-G/U',
+  'itk_users_default_entry' => 'ITK-Users-Default-Entry',
+  'quintum_h323_redirect_nu' => 'Quintum-h323-redirect-number',
   'x_ascend_fr_t392' => 'X-Ascend-FR-T392',
+  'acc_igmp_version' => 'Acc-Igmp-Version',
   'cisco_pre_output_packets' => 'Cisco-Pre-Output-Packets',
-  'tunnel_group' => 'Tunnel_Group',
-  'bind_sub_password' => 'Bind_Sub_Password',
+  'tunnel_group' => 'Tunnel-Group',
+  'x_ascend_home_agent_udp_' => 'X-Ascend-Home-Agent-UDP-Port',
+  'cvpn3000_tunneling_proto' => 'CVPN3000-Tunneling-Protocols',
+  'usr_igmp_maximum_respons' => 'USR-IGMP-Maximum-Response-Time',
+  'bind_sub_password' => 'Bind-Sub-Password',
   'eap_message' => 'EAP-Message',
   'exec_program' => 'Exec-Program',
-  'bg_path_cost' => 'BG_Path_Cost',
-  'auth_type' => 'Auth-Type',
+  'cvpn3000_reqrd_client_fx' => 'CVPN3000-Reqrd-Client-Fw-Product-Code',
+  'bg_path_cost' => 'BG-Path-Cost',
   'usr_modem_training_time' => 'USR-Modem-Training-Time',
-  'ascend_cbcp_enable' => 'Ascend-CBCP-Enable',
+  'auth_type' => 'Auth-Type',
+  'itk_acct_serv_prot' => 'ITK-Acct-Serv-Prot',
   'x_ascend_ipx_route' => 'X-Ascend-IPX-Route',
+  'altiga_primary_dns_g' => 'Altiga-Primary-DNS-G',
+  'ascend_cbcp_enable' => 'Ascend-CBCP-Enable',
+  'ms_mppe_encryption_polic' => 'MS-MPPE-Encryption-Policy',
+  'annex_unauthenticated_ti' => 'Annex-Unauthenticated-Time',
+  'annex_begin_receive_line' => 'Annex-Begin-Receive-Line-Level',
+  'ascend_atm_direct_profil' => 'Ascend-ATM-Direct-Profile',
+  'redcreek_tunneled_dns_se' => 'RedCreek-Tunneled-DNS-Server',
   'ascend_redirect_number' => 'Ascend-Redirect-Number',
   'h323_credit_time' => 'h323-credit-time',
+  'cvx_idle_limit' => 'CVX-Idle-Limit',
   'ascend_appletalk_route' => 'Ascend-Appletalk-Route',
+  'aat_ip_tos' => 'AAT-IP-TOS',
+  'cvx_ppp_address' => 'CVX-PPP-Address',
+  'aat_data_filter' => 'AAT-Data-Filter',
+  'cvx_primary_dns' => 'CVX-Primary-DNS',
   'shiva_link_protocol' => 'Shiva-Link-Protocol',
   'x_ascend_fr_circuit_name' => 'X-Ascend-FR-Circuit-Name',
-  'client_id' => 'Client-Id',
   'usr_appletalk' => 'USR-Appletalk',
-  'usr_mpip_tunnel_originat' => 'USR-MPIP-Tunnel-Originator',
+  'client_id' => 'Client-Id',
+  'tunnel_algorithn' => 'Tunnel_Algorithm',
+  'aat_assign_ip_pool' => 'AAT-Assign-IP-Pool',
+  'quintum_h323_incoming_co' => 'Quintum-h323-incoming-conf-id',
+  'aat_atm_vpi' => 'AAT-ATM-VPI',
   'annex_output_filter' => 'Annex-Output-Filter',
-  'pvc_circuit_padding' => 'PVC_Circuit_Padding',
-  'x_ascend_minimum_channel' => 'X-Ascend-Minimum-Channels',
+  'pvc_circuit_padding' => 'PVC-Circuit-Padding',
+  'usr_ipx_call_output_filt' => 'USR-IPX-Call-Output-Filter',
+  'usr_rmmie_planned_discon' => 'USR-RMMIE-Planned-Disconnect',
+  'session_error_msh' => 'Session_Error_Msg',
+  'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-Ttl',
   'h323_time_and_day' => 'h323-time-and-day',
-  'ascend_ipx_header_compre' => 'Ascend-IPX-Header-Compression',
+  'cvpn3000_ipsec_backup_se' => 'CVPN3000-IPSec-Backup-Servers',
   'termination_action' => 'Termination-Action',
-  'x_ascend_modem_portno' => 'X-Ascend-Modem-PortNo',
+  'cvpn3000_ipsec_client_fx' => 'CVPN3000-IPSec-Client-Fw-Filter-Opt',
+  'aat_client_primary_dnt' => 'AAT-Client-Primary-DNS',
   'acct_tunnel_packets_lost' => 'Acct-Tunnel-Packets-Lost',
+  'x_ascend_modem_portno' => 'X-Ascend-Modem-PortNo',
   'framed_filter_id' => 'Framed-Filter-Id',
   'usr_ccp_algorithm' => 'USR-CCP-Algorithm',
+  'quintum_h323_preferred_l' => 'Quintum-h323-preferred-lang',
+  'ascend_fr_link_status_dl' => 'Ascend-FR-Link-Status-DLCI',
   'ascend_token_expiry' => 'Ascend-Token-Expiry',
-  'annex_secondary_nbns_ser' => 'Annex-Secondary-NBNS-Server',
-  'usr_et_bridge_call_outpu' => 'USR-ET-Bridge-Call-Output-Filte',
+  'itk_auth_req_type' => 'ITK-Auth-Req-Type',
   'acc_modem_error_protocol' => 'Acc-Modem-Error-Protocol',
   'acc_request_type' => 'Acc-Request-Type',
+  'usr_last_number_dialed_i' => 'USR-Last-Number-Dialed-In-DNIS',
   'x_ascend_ipx_peer_mode' => 'X-Ascend-IPX-Peer-Mode',
   'ascend_ppp_vj_slot_comp' => 'Ascend-PPP-VJ-Slot-Comp',
   'cisco_presession_time' => 'Cisco-PreSession-Time',
   'usr_chat_script_name' => 'USR-Chat-Script-Name',
+  'tunnel_session_auti' => 'Tunnel_Session_Auth',
   'ascend_fr_circuit_name' => 'Ascend-FR-Circuit-Name',
   'ascend_expect_callback' => 'Ascend-Expect-Callback',
   'framed_mtu' => 'Framed-MTU',
-  'ascend_port_redir_protoc' => 'Ascend-Port-Redir-Protocol',
   'usr_pw_vpn_name' => 'USR-PW_VPN_Name',
+  'nomadix_ip_upsell' => 'Nomadix-IP-Upsell',
   'ascend_nas_port_format' => 'Ascend-NAS-Port-Format',
-  'shasta_vpn_name' => 'Shasta-VPN-Name',
   'usr_dtr_true_timeout' => 'USR-DTR-True-Timeout',
-  'ascend_third_prompt' => 'Ascend-Third-Prompt',
+  'shasta_vpn_name' => 'Shasta-VPN-Name',
   'connect_rate' => 'Connect-Rate',
-  'usr_block_error_count_li' => 'USR-Block-Error-Count-Limit',
+  'ascend_third_prompt' => 'Ascend-Third-Prompt',
+  'cabletron_protocol_enabl' => 'Cabletron-Protocol-Enable',
+  'annex_pre_input_octets' => 'Annex-Pre-Input-Octets',
+  'cvx_modem_error_correcti' => 'CVX-Modem-Error-Correction',
+  'cvx_ss7_session_id_type' => 'CVX-SS7-Session-ID-Type',
   'called_station_id' => 'Called-Station-Id',
+  'itk_ddi' => 'ITK-DDI',
   'usr_pw_cutoff' => 'USR-PW_Cutoff',
   'ascend_data_rate' => 'Ascend-Data-Rate',
+  'acct_input_packets_65' => 'Acct_Input_Packets_64',
   'x_ascend_ts_idle_mode' => 'X-Ascend-TS-Idle-Mode',
   'ascend_x25_pad_prompt' => 'Ascend-X25-Pad-Prompt',
   'x_ascend_dhcp_reply' => 'X-Ascend-DHCP-Reply',
   'acc_nbns_server_pri' => 'Acc-Nbns-Server-Pri',
+  'post_auth_type' => 'Post-Auth-Type',
   'ascend_call_filter' => 'Ascend-Call-Filter',
   'acc_tunnel_secret' => 'Acc-Tunnel-Secret',
-  'usr_simplified_v42bis_us' => 'USR-Simplified-V42bis-Usage',
-  'bind_int_context' => 'Bind_Int_Context',
+  'colubris_avpair' => 'Colubris-AVPair',
+  'bind_int_context' => 'Bind-Int-Context',
+  'annex_logical_channel_nu' => 'Annex-Logical-Channel-Number',
   'erx_virtual_router_name' => 'ERX-Virtual-Router-Name',
+  'wispr_redirection_url' => 'WISPr-Redirection-URL',
+  'bintec_ipextiftable' => 'BinTec-ipExtIfTable',
   'crypt_password' => 'Crypt-Password',
   'challenge_state' => 'Challenge-State',
-  'ascend_client_secondary_' => 'Ascend-Client-Secondary-DNS',
-  'strip_user_name' => 'Strip-User-Name',
+  'x_ascend_pre_input_packe' => 'X-Ascend-Pre-Input-Packets',
+  'altiga_ipsec_l2l_keepali' => 'Altiga-IPSec-L2L-Keepalives-G',
+  'x_ascend_dhcp_maximum_le' => 'X-Ascend-DHCP-Maximum-Leases',
+  'acc_dialout_auth_passwor' => 'Acc-Dialout-Auth-Password',
+  'itk_ip_pool' => 'ITK-IP-Pool',
+  'pvc_profile_namf' => 'PVC_Profile_Name',
   'x_ascend_user_acct_host' => 'X-Ascend-User-Acct-Host',
-  'x_ascend_route_ip' => 'X-Ascend-Route-IP',
-  'x_ascend_assign_ip_clien' => 'X-Ascend-Assign-IP-Client',
+  'strip_user_name' => 'Strip-User-Name',
+  'itk_ppp_client_server_mo' => 'ITK-PPP-Client-Server-Mode',
   'usr_mbi_ct_bchannel_used' => 'USR-Mbi_Ct_BChannel_Used',
+  'x_ascend_route_ip' => 'X-Ascend-Route-IP',
+  'ascend_seconds_of_histor' => 'Ascend-Seconds-Of-History',
+  'cvx_data_rate' => 'CVX-Data-Rate',
   'ascend_x25_profile_name' => 'Ascend-X25-Profile-Name',
+  'itk_ftp_auth_ip' => 'ITK-Ftp-Auth-IP',
+  'cisco_control_info' => 'Cisco-Control-Info',
+  'cvpn3000_secondary_wins' => 'CVPN3000-Secondary-WINS',
   'usr_call_type' => 'USR-Call-Type',
   'x_ascend_user_acct_base' => 'X-Ascend-User-Acct-Base',
+  'acct_mcast_in_packett' => 'Acct_Mcast_In_Packets',
+  'ns_vsys_name' => 'NS-VSYS-Name',
   'acct_output_gigawords' => 'Acct-Output-Gigawords',
-  'usr_rmmie_firmware_build' => 'USR-RMMIE-Firmware-Build-Date',
-  'ascend_fr_link_status_dl' => 'Ascend-FR-Link-Status-DLCI',
+  'bind_typf' => 'Bind_Type',
+  'bintec_ipqostable' => 'BinTec-ipQoSTable',
+  'bintec_ipxstaticservtabl' => 'BinTec-ipxStaticServTable',
+  'cvpn3000_l2tp_mppc_compr' => 'CVPN3000-L2TP-MPPC-Compression',
   'login_lat_port' => 'Login-LAT-Port',
   'usr_call_arrival_in_gmt' => 'USR-Call-Arrival-in-GMT',
-  'acct_mcast_in_octets' => 'Acct_Mcast_In_Octets',
+  'acct_mcast_in_octets' => 'Acct-Mcast-In-Octets',
   'erx_sa_validate' => 'ERX-Sa-Validate',
   'ascend_service_type' => 'Ascend-Service-Type',
-  'ascend_x25_nui_password_' => 'Ascend-X25-Nui-Password-Prompt',
   'usr_pw_vpn_gateway' => 'USR-PW_VPN_Gateway',
-  'ascend_fr_dce_n392' => 'Ascend-FR-DCE-N392',
   'acc_ip_compression' => 'Acc-Ip-Compression',
-  'lac_real_port_type' => 'LAC_Real_Port_Type',
-  'ascend_if_netmask' => 'Ascend-IF-Netmask',
+  'ascend_fr_dce_n392' => 'Ascend-FR-DCE-N392',
+  'bintec_ipxcirctable' => 'BinTec-ipxCircTable',
+  'lac_real_port_type' => 'LAC-Real-Port-Type',
+  'ascend_client_primary_dn' => 'Ascend-Client-Primary-DNS',
   'acct_session_start_time' => 'Acct-Session-Start-Time',
+  'ascend_if_netmask' => 'Ascend-IF-Netmask',
   'ms_chap_nt_enc_pw' => 'MS-CHAP-NT-Enc-PW',
-  'ascend_port_redir_portnu' => 'Ascend-Port-Redir-Portnum',
-  'mcast_maxgroups' => 'Mcast_MaxGroups',
-  'x_ascend_home_agent_ip_a' => 'X-Ascend-Home-Agent-IP-Addr',
+  'ms_mppe_encryption_types' => 'MS-MPPE-Encryption-Types',
+  'cisco_fax_process_abort_' => 'Cisco-Fax-Process-Abort-Flag',
+  'mcast_maxgroups' => 'Mcast-MaxGroups',
+  'annex_end_receive_line_l' => 'Annex-End-Receive-Line-Level',
+  'usr_ipx_call_input_filte' => 'USR-IPX-Call-Input-Filter',
+  'usr_back_channel_data_ra' => 'USR-Back-Channel-Data-Rate',
   'ascend_cache_time' => 'Ascend-Cache-Time',
   'x_ascend_data_svc' => 'X-Ascend-Data-Svc',
-  'erx_tunnel_virtual_route' => 'ERX-Tunnel-Virtual-Router',
   'usr_re_chap_timeout' => 'USR-Re-Chap-Timeout',
+  'bintec_bibodialtable' => 'BinTec-biboDialTable',
+  'annex_connect_progress' => 'Annex-Connect-Progress',
   'x_ascend_ppp_vj_1172' => 'X-Ascend-PPP-VJ-1172',
   'usr_igmp_routing' => 'USR-IGMP-Routing',
+  'x_ascend_ip_pool_definit' => 'X-Ascend-IP-Pool-Definition',
   'h323_prompt_id' => 'h323-prompt-id',
+  'foundry_command_string' => 'Foundry-Command-String',
   'le_terminate_detail' => 'LE-Terminate-Detail',
+  'cvpn3000_pptp_encryption' => 'CVPN3000-PPTP-Encryption',
+  'quintum_h323_disconnect_' => 'Quintum-h323-disconnect-time',
   'acc_ml_clear_threshold' => 'Acc-ML-Clear-Threshold',
   'x_ascend_ip_direct' => 'X-Ascend-IP-Direct',
-  'nas_port' => 'NAS-Port',
-  'x_ascend_data_rate' => 'X-Ascend-Data-Rate',
   'usr_ip_call_input_filter' => 'USR-IP-Call-Input-Filter',
+  'x_ascend_data_rate' => 'X-Ascend-Data-Rate',
+  'nas_port' => 'NAS-Port',
+  'ascend_client_secondary_' => 'Ascend-Client-Secondary-WINS',
   'ascend_auth_type' => 'Ascend-Auth-Type',
   'x_ascend_preempt_limit' => 'X-Ascend-Preempt-Limit',
+  'cvx_xmit_rate' => 'CVX-Xmit-Rate',
+  'annex_transmitted_packet' => 'Annex-Transmitted-Packets',
   'h323_credit_amount' => 'h323-credit-amount',
   'usr_reply_script1' => 'USR-Reply-Script1',
-  'usr_et_bridge_input_filt' => 'USR-ET-Bridge-Input-Filter',
   'current_time' => 'Current-Time',
   'cisco_xmit_rate' => 'Cisco-Xmit-Rate',
-  'ascend_authen_alias' => 'Ascend-Authen-Alias',
   'x_ascend_session_svr_key' => 'X-Ascend-Session-Svr-Key',
+  'ascend_authen_alias' => 'Ascend-Authen-Alias',
+  'erx_redirect_vr_name' => 'ERX-Redirect-VR-Name',
+  'module_success_message' => 'Module-Success-Message',
   'acc_dialout_auth_mode' => 'Acc-Dialout-Auth-Mode',
+  'bind_auth_contexu' => 'Bind_Auth_Context',
+  'x_ascend_minimum_channel' => 'X-Ascend-Minimum-Channels',
   'usr_event_date_time' => 'USR-Event-Date-Time',
   'x_ascend_ipx_node_addr' => 'X-Ascend-IPX-Node-Addr',
-  'ascend_primary_home_agen' => 'Ascend-Primary-Home-Agent',
+  'cvpn3000_ipsec_over_udp' => 'CVPN3000-IPSec-Over-UDP',
   'x_ascend_user_acct_time' => 'X-Ascend-User-Acct-Time',
-  'usr_at_call_output_filte' => 'USR-AT-Call-Output-Filter',
+  'cisco_email_server_ack_f' => 'Cisco-Email-Server-Ack-Flag',
+  'telebit_activate_command' => 'Telebit-Activate-Command',
   'acc_output_errors' => 'Acc-Output-Errors',
-  'usr_ipx_rip_output_filte' => 'USR-IPX-RIP-Output-Filter',
+  'juniper_allow_configurat' => 'Juniper-Allow-Configuration',
+  'bind_l2tp_tunnel_name' => 'Bind-L2TP-Tunnel-Name',
   'x_ascend_pri_number_type' => 'X-Ascend-PRI-Number-Type',
-  'bind_l2tp_tunnel_name' => 'Bind_L2TP_Tunnel_Name',
-  'replicate_to_realm' => 'Replicate-To-Realm',
+  'bintec_biboppptable' => 'BinTec-biboPPPTable',
+  'le_ipsec_outsource_profi' => 'LE-IPSec-Outsource-Profile',
   'usr_at_zip_input_filter' => 'USR-AT-Zip-Input-Filter',
+  'replicate_to_realm' => 'Replicate-To-Realm',
   'annex_mrru' => 'Annex-MRRU',
   'event_timestamp' => 'Event-Timestamp',
+  'nokia_sgsn_ip_address' => 'Nokia-SGSN-IP-Address',
   'ascend_pre_input_packets' => 'Ascend-Pre-Input-Packets',
+  'cvpn5000_client_assigned' => 'CVPN5000-Client-Assigned-IP',
+  'tunnel_dnit' => 'Tunnel_DNIS',
   'h323_call_origin' => 'h323-call-origin',
   'x_ascend_fr_type' => 'X-Ascend-FR-Type',
+  'itk_provider_id' => 'ITK-Provider-Id',
+  'cvx_ppp_log_mask' => 'CVX-PPP-Log-Mask',
   'x_ascend_token_idle' => 'X-Ascend-Token-Idle',
+  'usr_rmmie_pwrlvl_xmit_lv' => 'USR-RMMIE-PwrLvl-Xmit-Lvl',
   'usr_igmp_query_interval' => 'USR-IGMP-Query-Interval',
+  'quintum_h323_billing_mod' => 'Quintum-h323-billing-model',
   'ascend_atm_vci' => 'Ascend-ATM-Vci',
   'usr_port_tap_output' => 'USR-Port-Tap-Output',
   'session' => 'Session',
+  'itk_welcome_message' => 'ITK-Welcome-Message',
+  'cvpn3000_ike_keep_alives' => 'CVPN3000-IKE-Keep-Alives',
   'ascend_uu_info' => 'Ascend-UU-Info',
-  'ms_mppe_recv_key' => 'MS-MPPE-Recv-Key',
+  'usr_et_bridge_call_outpu' => 'USR-ET-Bridge-Call-Output-Filte',
   'usr_secondary_dns_server' => 'USR-Secondary_DNS_Server',
-  'x_ascend_tunneling_proto' => 'X-Ascend-Tunneling-Protocol',
+  'ms_mppe_recv_key' => 'MS-MPPE-Recv-Key',
+  'bintec_ripcirctable' => 'BinTec-ripCircTable',
   'acc_dial_port_index' => 'Acc-Dial-Port-Index',
   'cisco_nas_port' => 'Cisco-NAS-Port',
+  'itk_username' => 'ITK-Username',
   'usr_send_script1' => 'USR-Send-Script1',
+  'cvpn3000_ipsec_ike_peer_' => 'CVPN3000-IPSec-IKE-Peer-ID-Check',
+  'ascend_dsl_upstream_limi' => 'Ascend-Dsl-Upstream-Limit',
+  'x_ascend_dec_channel_cou' => 'X-Ascend-Dec-Channel-Count',
   'usr_tunnel_security' => 'USR-Tunnel-Security',
   'arap_security' => 'ARAP-Security',
   'tunnel_preference' => 'Tunnel-Preference',
+  'cisco_port_used' => 'Cisco-Port-Used',
   'usr_reply_script4' => 'USR-Reply-Script4',
-  'h323_currency_type' => 'h323-currency-type',
+  'cvpn5000_client_real_ip' => 'CVPN5000-Client-Real-IP',
   'usr_rmmie_status' => 'USR-RMMIE-Status',
-  'ascend_shared_profile_en' => 'Ascend-Shared-Profile-Enable',
-  'annex_syslog_tap' => 'Annex-Syslog-Tap',
   'usr_send_script4' => 'USR-Send-Script4',
+  'quintum_h323_connect_tim' => 'Quintum-h323-connect-time',
+  'annex_syslog_tap' => 'Annex-Syslog-Tap',
+  'redcreek_tunneled_hostna' => 'RedCreek-Tunneled-HostName',
   'acc_clearing_location' => 'Acc-Clearing-Location',
+  'ascend_access_intercept_' => 'Ascend-Access-Intercept-LEA',
   'annex_disconnect_reason' => 'Annex-Disconnect-Reason',
-  'x_ascend_dhcp_maximum_le' => 'X-Ascend-DHCP-Maximum-Leases',
   'usr_at_input_filter' => 'USR-AT-Input-Filter',
   'usr_auth_mode' => 'USR-Auth-Mode',
-  'shiva_session_id' => 'Shiva-Session-Id',
   'usr_expected_voltage' => 'USR-Expected-Voltage',
+  'shiva_session_id' => 'Shiva-Session-Id',
+  'annex_maximum_call_durat' => 'Annex-Maximum-Call-Duration',
+  'usr_block_error_count_li' => 'USR-Block-Error-Count-Limit',
   'ascend_owner_ip_addr' => 'Ascend-Owner-IP-Addr',
-  'ascend_atm_direct_profil' => 'Ascend-ATM-Direct-Profile',
+  'bind_tun_contexu' => 'Bind_Tun_Context',
   'usr_pw_usr_ofilter_ipx' => 'USR-PW_USR_OFilter_IPX',
   'framed_routing' => 'Framed-Routing',
-  'pam_auth' => 'Pam-Auth',
+  'annex_primary_nbns_serve' => 'Annex-Primary-NBNS-Server',
   'usr_interface_index' => 'USR-Interface-Index',
-  'x_ascend_transit_number' => 'X-Ascend-Transit-Number',
+  'pam_auth' => 'Pam-Auth',
   'usr_end_time' => 'USR-End-Time',
+  'rate_limit_bursu' => 'Rate_Limit_Burst',
+  'nomadix_expiration' => 'Nomadix-Expiration',
+  'x_ascend_transit_number' => 'X-Ascend-Transit-Number',
+  'itk_usergroup' => 'ITK-Usergroup',
   'x_ascend_assign_ip_pool' => 'X-Ascend-Assign-IP-Pool',
+  'annex_secondary_nbns_ser' => 'Annex-Secondary-NBNS-Server',
+  'bind_dot1q_vlan_tag_id' => 'Bind-Dot1q-Vlan-Tag-Id',
   'ms_secondary_nbns_server' => 'MS-Secondary-NBNS-Server',
-  'bind_dot1q_vlan_tag_id' => 'Bind_Dot1q_Vlan_Tag_Id',
+  'tunnel_retransmit' => 'Tunnel-Retransmit',
   'acct_tunnel_connection' => 'Acct-Tunnel-Connection',
-  'tunnel_retransmit' => 'Tunnel_Retransmit',
   'x_ascend_backup' => 'X-Ascend-Backup',
+  'xedia_ppp_echo_interval' => 'Xedia-PPP-Echo-Interval',
   'usr_bearer_capabilities' => 'USR-Bearer-Capabilities',
-  'ascend_calling_id_type_o' => 'Ascend-Calling-Id-Type-Of-Num',
   'shiva_acct_serv_switch' => 'Shiva-Acct-Serv-Switch',
-  'ascend_h323_conference_i' => 'Ascend-H323-Conference-Id',
   'acct_authentic' => 'Acct-Authentic',
+  'le_nat_other_session_tim' => 'LE-NAT-Other-Session-Timeout',
+  'cvpn3000_ipsec_banner2' => 'CVPN3000-IPSec-Banner2',
   'x_ascend_force_56' => 'X-Ascend-Force-56',
   'framed_appletalk_network' => 'Framed-AppleTalk-Network',
   'reply_message' => 'Reply-Message',
-  'annex_addr_resolution_pr' => 'Annex-Addr-Resolution-Protocol',
   'class' => 'Class',
   'h323_conf_id' => 'h323-conf-id',
+  'quintum_h323_disconnecta' => 'Quintum-h323-disconnect-cause',
+  'itk_filter_rule' => 'ITK-Filter-Rule',
+  'wispr_bandwidth_max_up' => 'WISPr-Bandwidth-Max-Up',
+  'usr_appletalk_network_ra' => 'USR-Appletalk-Network-Range',
   'ascend_cbcp_delay' => 'Ascend-CBCP-Delay',
+  'usr_dte_ring_no_answer_l' => 'USR-DTE-Ring-No-Answer-Limit',
+  'pre_acct_type' => 'Pre-Acct-Type',
+  'usr_local_ip_address' => 'USR-Local-IP-Address',
   'ascend_dropped_octets' => 'Ascend-Dropped-Octets',
   'ascend_h323_dialed_time' => 'Ascend-H323-Dialed-Time',
-  'usr_local_ip_address' => 'USR-Local-IP-Address',
+  'cisco_email_server_addre' => 'Cisco-Email-Server-Address',
   'ascend_x25_x121_address' => 'Ascend-X25-X121-Address',
-  'ascend_destination_nas_p' => 'Ascend-Destination-Nas-Port',
-  'annex_local_ip_address' => 'Annex-Local-IP-Address',
+  'cvx_multicast_client' => 'CVX-Multicast-Client',
+  'wispr_bandwidth_min_up' => 'WISPr-Bandwidth-Min-Up',
   'usr_at_output_filter' => 'USR-AT-Output-Filter',
+  'annex_local_ip_address' => 'Annex-Local-IP-Address',
   'cisco_ip_pool_definition' => 'Cisco-IP-Pool-Definition',
+  'cisco_gateway_id' => 'Cisco-Gateway-Id',
+  'itk_password_prompt' => 'ITK-Password-Prompt',
   'annex_domain_name' => 'Annex-Domain-Name',
+  'foundry_command_exceptio' => 'Foundry-Command-Exception-Flag',
   'ascend_preempt_limit' => 'Ascend-Preempt-Limit',
+  'erx_minimum_bps' => 'ERX-Minimum-BPS',
+  'aat_mcast_client' => 'AAT-MCast-Client',
+  'ascend_atm_fault_managem' => 'Ascend-ATM-Fault-Management',
   'ascend_event_type' => 'Ascend-Event-Type',
-  'x_ascend_pre_input_octet' => 'X-Ascend-Pre-Input-Octets',
   'exec_program_wait' => 'Exec-Program-Wait',
-
-  #NOMENT
-  'nomadix_ip_upsell' => 'Nomadix-IP-Upsell',
+  'framed_interface_id' => 'Framed-Interface-Id',
 
   #NETC.NET.AU (RADIATOR?)
   'authentication_type' => 'Authentication-Type',
index 524e550..a154f3f 100644 (file)
@@ -49,8 +49,10 @@ sub virtual_fields {
 
   if ($self->svcpart) { # Case 1
     $svcpart = $self->svcpart;
-  } elsif (my $cust_svc = $self->cust_svc) { # Case 2
-    $svcpart = $cust_svc->svcpart;
+  } elsif ( $self->svcnum
+            && qsearchs('cust_svc',{'svcnum'=>$self->svcnum} )
+          ) { #Case 2
+    $svcpart = $self->cust_svc->svcpart;
   } else { # Case 3
     $svcpart = '';
   }
@@ -80,7 +82,7 @@ sub check {
   $self->SUPER::check;
 }
 
-=item insert [ JOBNUM_ARRAYREF ]
+=item insert [ JOBNUM_ARRAYREF [ OBJECTS_ARRAYREF ] ]
 
 Adds this record to the database.  If there is an error, returns the error,
 otherwise returns false.
@@ -91,11 +93,16 @@ defined.  An FS::cust_svc record will be created and inserted.
 If an arrayref is passed as parameter, the B<jobnum>s of any export jobs will
 be added to the array.
 
+If an arrayref of FS::tablename objects (for example, FS::acct_snarf objects)
+is passed as the optional second parameter, they will have their svcnum fields
+set and will be inserted after this record, but before any exports are run.
+
 =cut
 
 sub insert {
   my $self = shift;
   local $FS::queue::jobnums = shift if @_;
+  my $objects = scalar(@_) ? shift : [];
   my $error;
 
   local $SIG{HUP} = 'IGNORE';
@@ -113,10 +120,12 @@ sub insert {
   return $error if $error;
 
   my $svcnum = $self->svcnum;
-  my $cust_svc;
-  unless ( $svcnum ) {
+  my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : '';
+  #unless ( $svcnum ) {
+  if ( !$svcnum or !$cust_svc ) {
     $cust_svc = new FS::cust_svc ( {
       #hua?# 'svcnum'  => $svcnum,
+      'svcnum'  => $self->svcnum,
       'pkgnum'  => $self->pkgnum,
       'svcpart' => $self->svcpart,
     } );
@@ -127,7 +136,7 @@ sub insert {
     }
     $svcnum = $self->svcnum($cust_svc->svcnum);
   } else {
-    $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
+    #$cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
     unless ( $cust_svc ) {
       $dbh->rollback if $oldAutoCommit;
       return "no cust_svc record found for svcnum ". $self->svcnum;
@@ -142,6 +151,15 @@ sub insert {
     return $error;
   }
 
+  foreach my $object ( @$objects ) {
+    $object->svcnum($self->svcnum);
+    $error = $object->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
   #new-style exports!
   unless ( $noexport_hack ) {
     foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
@@ -294,7 +312,7 @@ sub setx {
 
   #get part_svc
   my $svcpart;
-  if ( $self->svcnum ) {
+  if ( $self->svcnum && qsearchs('cust_svc', {'svcnum'=>$self->svcnum}) ) {
     my $cust_svc = $self->cust_svc;
     return "Unknown svcnum" unless $cust_svc; 
     $svcpart = $cust_svc->svcpart;
@@ -412,11 +430,31 @@ methods.  Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
 
 sub cancel { ''; }
 
-=back
+=item clone_suspended
+
+Constructor used by FS::part_export::_export_suspend fallback.  Stub returning
+same object for svc_ classes which don't implement a suspension fallback
+(everything except svc_acct at the moment).  Document better.
 
-=head1 VERSION
+=cut
+
+sub clone_suspended {
+  shift;
+}
+
+=item clone_kludge_unsuspend 
+
+Constructor used by FS::part_export::_export_unsuspend fallback.  Stub returning
+same object for svc_ classes which don't implement a suspension fallback
+(everything except svc_acct at the moment).  Document better.
 
-$Id: svc_Common.pm,v 1.13 2003-08-05 00:20:47 khoff Exp $
+=cut
+
+sub clone_kludge_unsuspend {
+  shift;
+}
+
+=back
 
 =head1 BUGS
 
index 0ee7a72..32d8720 100644 (file)
@@ -16,7 +16,7 @@ use Carp;
 use Fcntl qw(:flock);
 use FS::UID qw( datasrc );
 use FS::Conf;
-use FS::Record qw( qsearch qsearchs fields dbh );
+use FS::Record qw( qsearch qsearchs fields dbh dbdef );
 use FS::svc_Common;
 use FS::cust_svc;
 use FS::part_svc;
@@ -188,10 +188,16 @@ The additional field I<usergroup> can optionally be defined; if so it should
 contain an arrayref of group names.  See L<FS::radius_usergroup>.  (used in
 sqlradius export only)
 
+The additional field I<child_objects> can optionally be defined; if so it
+should contain an arrayref of FS::tablename objects.  They will have their
+svcnum fields set and will be inserted after this record, but before any
+exports are run.
+
 (TODOC: L<FS::queue> and L<freeside-queued>)
 
 (TODOC: new exports!)
 
+
 =cut
 
 sub insert {
@@ -220,7 +226,7 @@ sub insert {
   #                             'domsvc'   => $self->domsvc,
   #                           } );
 
-  if ( $self->svcnum ) {
+  if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
     my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
     unless ( $cust_svc ) {
       $dbh->rollback if $oldAutoCommit;
@@ -319,7 +325,7 @@ sub insert {
   #see?  i told you it was more complicated
 
   my @jobnums;
-  $error = $self->SUPER::insert(\@jobnums);
+  $error = $self->SUPER::insert(\@jobnums, $self->child_objects || [] );
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -421,6 +427,8 @@ The corresponding FS::cust_svc record will be deleted as well.
 sub delete {
   my $self = shift;
 
+  return "can't delete system account" if $self->_check_system;
+
   return "Can't delete an account which is a (svc_forward) source!"
     if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
 
@@ -509,6 +517,8 @@ sub replace {
   my $error;
   warn "$me replacing $old with $new\n" if $DEBUG;
 
+  return "can't modify system account" if $old->_check_system;
+
   return "Username in use"
     if $old->username ne $new->username &&
       qsearchs( 'svc_acct', { 'username' => $new->username,
@@ -599,39 +609,26 @@ sub replace {
 
 =item suspend
 
-Suspends this account by prefixing *SUSPENDED* to the password.  If there is an
-error, returns the error, otherwise returns false.
+Suspends this account by calling export-specific suspend hooks.  If there is
+an error, returns the error, otherwise returns false.
 
 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
 
-Calls any export-specific suspend hooks.
-
 =cut
 
 sub suspend {
   my $self = shift;
-  my %hash = $self->hash;
-  unless ( $hash{_password} =~ /^\*SUSPENDED\* /
-           || $hash{_password} eq '*'
-         ) {
-    $hash{_password} = '*SUSPENDED* '.$hash{_password};
-    my $new = new FS::svc_acct ( \%hash );
-    my $error = $new->replace($self);
-    return $error if $error;
-  }
-
+  return "can't suspend system account" if $self->_check_system;
   $self->SUPER::suspend;
 }
 
 =item unsuspend
 
-Unsuspends this account by removing *SUSPENDED* from the password.  If there is
-an error, returns the error, otherwise returns false.
+Unsuspends this account by by calling export-specific suspend hooks.  If there
+is an error, returns the error, otherwise returns false.
 
 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
 
-Calls any export-specific unsuspend hooks.
-
 =cut
 
 sub unsuspend {
@@ -784,7 +781,7 @@ sub check {
       or return "Illegal finger: ". $self->getfield('finger');
   $self->setfield('finger', $1);
 
-  $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota";
+  $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
   $recref->{quota} = $1;
 
   unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
@@ -819,10 +816,12 @@ sub check {
     #$recref->{password} = $1.
     #  crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
     #;
-  } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,34})$/ ) {
+  } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,60})$/ ) {
     $recref->{_password} = $1.$3;
   } elsif ( $recref->{_password} eq '*' ) {
     $recref->{_password} = '*';
+  } elsif ( $recref->{_password} eq '!' ) {
+    $recref->{_password} = '!';
   } elsif ( $recref->{_password} eq '!!' ) {
     $recref->{_password} = '!!';
   } else {
@@ -835,6 +834,17 @@ sub check {
   $self->SUPER::check;
 }
 
+=item _check_system
+
+=cut
+
+sub _check_system {
+  my $self = shift;
+  scalar( grep { $self->username eq $_ || $self->email eq $_ }
+               $conf->config('system_usernames')
+        );
+}
+
 =item radius
 
 Depriciated, use radius_reply instead.
@@ -947,6 +957,22 @@ sub email {
   $self->username. '@'. $self->domain;
 }
 
+=item acct_snarf
+
+Returns an array of FS::acct_snarf records associated with the account.
+If the acct_snarf table does not exist or there are no associated records,
+an empty list is returned
+
+=cut
+
+sub acct_snarf {
+  my $self = shift;
+  return () unless dbdef->table('acct_snarf');
+  eval "use FS::acct_snarf;";
+  die $@ if $@;
+  qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
+}
+
 =item seconds_since TIMESTAMP
 
 Returns the number of seconds this account has been online since TIMESTAMP,
@@ -1005,6 +1031,18 @@ sub attribute_since_sqlradacct {
   $self->cust_svc->attribute_since_sqlradacct(@_);
 }
 
+=item get_session_history_sqlradacct TIMESTAMP_START TIMESTAMP_END
+
+Returns an array of hash references of this customers login history for the
+given time range.  (document this better)
+
+=cut
+
+sub get_session_history_sqlradacct {
+  my $self = shift;
+  $self->cust_svc->get_session_history_sqlradacct(@_);
+}
+
 =item radius_groups
 
 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
@@ -1023,6 +1061,34 @@ sub radius_groups {
   }
 }
 
+=item clone_suspended
+
+Constructor used by FS::part_export::_export_suspend fallback.  Document
+better.
+
+=cut
+
+sub clone_suspended {
+  my $self = shift;
+  my %hash = $self->hash;
+  $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
+  new FS::svc_acct \%hash;
+}
+
+=item clone_kludge_unsuspend 
+
+Constructor used by FS::part_export::_export_unsuspend fallback.  Document
+better.
+
+=cut
+
+sub clone_kludge_unsuspend {
+  my $self = shift;
+  my %hash = $self->hash;
+  $hash{_password} = '';
+  new FS::svc_acct \%hash;
+}
+
 =back
 
 =head1 SUBROUTINES
index ff0fa2f..10d5d8f 100644 (file)
@@ -342,9 +342,8 @@ sub check {
     return "Unknown catchall" unless $svc_acct;
   }
 
-  my $error = $self->ut_textn('purpose')
-           or $self->SUPER::check;
-  return $error if $error;
+  $self->ut_textn('purpose')
+    or $self->SUPER::check;
 
 }
 
diff --git a/FS/FS/svc_external.pm b/FS/FS/svc_external.pm
new file mode 100644 (file)
index 0000000..fe4ea1d
--- /dev/null
@@ -0,0 +1,174 @@
+package FS::svc_external;
+
+use strict;
+use vars qw(@ISA); # $conf
+use FS::UID;
+#use FS::Record qw( qsearch qsearchs dbh);
+use FS::svc_Common;
+
+@ISA = qw( FS::svc_Common );
+
+#FS::UID::install_callback( sub {
+#  $conf = new FS::Conf;
+#};
+
+=head1 NAME
+
+FS::svc_external - Object methods for svc_external records
+
+=head1 SYNOPSIS
+
+  use FS::svc_external;
+
+  $record = new FS::svc_external \%hash;
+  $record = new FS::svc_external { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $error = $record->suspend;
+
+  $error = $record->unsuspend;
+
+  $error = $record->cancel;
+
+=head1 DESCRIPTION
+
+An FS::svc_external object represents a externally tracked service.
+FS::svc_external inherits from FS::svc_Common.  The following fields are
+currently supported:
+
+=over 4
+
+=item svcnum - primary key
+
+=item id - unique number of external record
+
+=item title - for invoice line items
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new external service.  To add the external service to the database,
+see L<"insert">.  
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'svc_external'; }
+
+=item insert
+
+Adds this external service to the database.  If there is an error, returns the
+error, otherwise returns false.
+
+The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
+defined.  An FS::cust_svc record will be created and inserted.
+
+=cut
+
+sub insert {
+  my $self = shift;
+  my $error;
+
+  $error = $self->SUPER::insert;
+  return $error if $error;
+
+  '';
+}
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+sub delete {
+  my $self = shift;
+  my $error;
+
+  $error = $self->SUPER::delete;
+  return $error if $error;
+
+  '';
+}
+
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub replace {
+  my ( $new, $old ) = ( shift, shift );
+  my $error;
+
+  $error = $new->SUPER::replace($old);
+  return $error if $error;
+
+  '';
+}
+
+=item suspend
+
+Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item unsuspend
+
+Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item cancel
+
+Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item check
+
+Checks all fields to make sure this is a valid external service.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and repalce methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $x = $self->setfixed;
+  return $x unless ref($x);
+  my $part_svc = $x;
+
+  my $error = 
+    $self->ut_numbern('svcnum')
+    || $self->ut_number('id')
+    || $self->ut_textn('title')
+  ;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>,
+L<FS::cust_pkg>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 7a121b8..b9e8ff8 100644 (file)
@@ -47,9 +47,11 @@ inherits from FS::Record.  The following fields are currently supported:
 
 =item srcsvc - svcnum of the source of the forward (see L<FS::svc_acct>)
 
+=item src - literal source (username or full email address)
+
 =item dstsvc - svcnum of the destination of the forward (see L<FS::svc_acct>)
 
-=item dst - foreign destination (email address) - forward not local to freeside
+=item dst - literal destination (username or full email address)
 
 =back
 
@@ -214,12 +216,19 @@ sub check {
   #my $part_svc = $x;
 
   my $error = $self->ut_numbern('svcnum')
-              || $self->ut_number('srcsvc')
+              || $self->ut_numbern('srcsvc')
               || $self->ut_numbern('dstsvc')
   ;
   return $error if $error;
 
-  return "Unknown srcsvc" unless $self->srcsvc_acct;
+  return "Both srcsvc and src were defined; only one can be specified"
+    if $self->srcsvc && $self->src;
+
+  return "one of srcsvc or src is required"
+    unless $self->srcsvc || $self->src;
+
+  return "Unknown srcsvc: ". $self->srcsvc
+    unless ! $self->srcsvc || $self->srcsvc_acct;
 
   return "Both dstsvc and dst were defined; only one can be specified"
     if $self->dstsvc && $self->dst;
@@ -227,16 +236,24 @@ sub check {
   return "one of dstsvc or dst is required"
     unless $self->dstsvc || $self->dst;
 
-  #return "Unknown dstsvc: $dstsvc" unless $self->dstsvc_acct || ! $self->dstsvc;
-  return "Unknown dstsvc"
-    unless qsearchs('svc_acct', { 'svcnum' => $self->dstsvc } )
-           || ! $self->dstsvc;
+  return "Unknown dstsvc: ". $self->dstsvc
+    unless ! $self->dstsvc || $self->dstsvc_acct;
+  #return "Unknown dstsvc"
+  #  unless qsearchs('svc_acct', { 'svcnum' => $self->dstsvc } )
+  #         || ! $self->dstsvc;
 
+  if ( $self->src ) {
+    $self->src =~ /^([\w\.\-\&]*)(\@([\w\-]+\.)+\w+)?$/
+       or return "Illegal src: ". $self->dst;
+    $self->src("$1$2");
+  } else {
+    $self->src('');
+  }
 
   if ( $self->dst ) {
-    $self->dst =~ /^([\w\.\-]+)\@(([\w\-]+\.)+\w+)$/
+    $self->dst =~ /^([\w\.\-\&]*)(\@([\w\-]+\.)+\w+)?$/
        or return "Illegal dst: ". $self->dst;
-    $self->dst("$1\@$2");
+    $self->dst("$1$2");
   } else {
     $self->dst('');
   }
@@ -246,7 +263,8 @@ sub check {
 
 =item srcsvc_acct
 
-Returns the FS::svc_acct object referenced by the srcsvc column.
+Returns the FS::svc_acct object referenced by the srcsvc column, or false for
+literally specified forwards.
 
 =cut
 
@@ -258,7 +276,7 @@ sub srcsvc_acct {
 =item dstsvc_acct
 
 Returns the FS::svc_acct object referenced by the srcsvc column, or false for
-forwards not local to freeside.
+literally specified forwards.
 
 =cut
 
index 0fbbf5c..3cbf0e9 100644 (file)
@@ -19,7 +19,6 @@ bin/freeside-email
 bin/freeside-expiration-alerter
 bin/freeside-queued
 bin/freeside-radgroup
-bin/freeside-receivables-report
 bin/freeside-reexport
 bin/freeside-selfservice-server
 bin/freeside-setinvoice
@@ -45,6 +44,7 @@ FS/UI/Gtk.pm
 FS/UI/agent.pm
 FS/UID.pm
 FS/Msgcat.pm
+FS/acct_snarf.pm
 FS/agent.pm
 FS/agent_type.pm
 FS/cust_bill.pm
@@ -71,6 +71,8 @@ FS/part_export/apache.pm
 FS/part_export/bind.pm
 FS/part_export/bind_slave.pm
 FS/part_export/bsdshell.pm
+FS/part_export/communigate_pro.pm
+FS/part_export/communigate_pro_singledomain.pm
 FS/part_export/cp.pm
 FS/part_export/cyrus.pm
 FS/part_export/domain_shellcommands.pm
@@ -100,6 +102,7 @@ FS/svc_acct.pm
 FS/svc_acct_pop.pm
 FS/svc_broadband.pm
 FS/svc_domain.pm
+FS/svc_external.pm
 FS/router.pm
 FS/type_pkgs.pm
 FS/nas.pm
@@ -182,8 +185,10 @@ t/radius_usergroup.t
 t/session.t
 t/svc_acct.t
 t/svc_acct_pop.t
+t/svc_broadband.t
 t/svc_Common.t
 t/svc_domain.t
+t/svc_external.t
 t/svc_forward.t
 t/svc_www.t
 t/type_pkgs.t
index 180cd93..abb515b 100644 (file)
@@ -10,6 +10,6 @@ freeside-adduser -h /usr/local/etc/freeside/htpasswd \
                  $username $password 2>/dev/null
 
 [ -e /usr/local/etc/freeside/dbdef.DBI:Pg:host=localhost\;dbname=$domain ] \
- || ( freeside-setup $username 2>/dev/null; \
-      /home/ivan/freeside/bin/populate-msgcat $username )
+ || ( freeside-setup -s $username 2>/dev/null; \
+      /home/ivan/freeside/bin/populate-msgcat $username 2>/dev/null )
 
index 63e621b..5fb9666 100755 (executable)
@@ -10,8 +10,8 @@ use FS::Conf;
 use FS::cust_main;
 
 &untaint_argv; #what it sounds like  (eww)
-use vars qw($opt_d $opt_v $opt_p);
-getopts("p:d:v");
+use vars qw($opt_d $opt_v $opt_p $opt_s $opt_y);
+getopts("p:d:vsy:");
 my $user = shift or die &usage;
 
 adminsuidsetup $user;
@@ -28,6 +28,7 @@ my @cust_main = @ARGV
 
 #we're at now now (and later).
 my($time)= $opt_d ? str2time($opt_d) : $^T;
+$time += $opt_y * 86400 if $opt_y;
 
 my($cust_main,%saw);
 foreach $cust_main ( @cust_main ) {
@@ -42,7 +43,8 @@ foreach $cust_main ( @cust_main ) {
       if $error;
   }
 
-  my $error = $cust_main->bill( 'time' => $time );
+  my $error = $cust_main->bill( 'time'    => $time,
+                                'resetup' => $opt_s, );
   warn "Error billing, custnum ". $cust_main->custnum. ": $error" if $error;
 
   $cust_main->apply_payments;
@@ -98,7 +100,7 @@ freeside-daily - Run daily billing and invoice collection events.
 
 =head1 SYNOPSIS
 
-  freeside-daily [ -d 'date' ] [ -p 'payby' ] [ -v ] user [ custnum custnum ... ]
+  freeside-daily [ -d 'date' ] [ -y days ] [ -p 'payby' ] [ -s ] [ -v ] user [ custnum custnum ... ]
 
 =head1 DESCRIPTION
 
@@ -113,8 +115,15 @@ the bill and collect methods of a cust_main object.  See L<FS::cust_main>.
   -d: Pretend it's 'date'.  Date is in any format Date::Parse is happy with,
       but be careful.
 
+  -y: In addition to -d, which specifies an absolute date, the -y switch
+      specifies an offset, in days.  For example, "-y 15" would increment the
+      "pretend date" 15 days from whatever was specified by the -d switch
+      (or now, if no -d switch was given).
+
   -p: Only process customers with the specified payby (I<CARD>, I<DCRD>, I<CHEK>, I<DCHK>, I<BILL>, I<COMP>, I<LECB>)
 
+  -s: re-charge setup fees
+
   -v: enable debugging
 
 user: From the mapsecrets file - see config.html from the base documentation
diff --git a/FS/bin/freeside-receivables-report b/FS/bin/freeside-receivables-report
deleted file mode 100755 (executable)
index f3ad2a1..0000000
+++ /dev/null
@@ -1,217 +0,0 @@
-#!/usr/bin/perl -Tw
-
-use strict;
-use Date::Parse;
-use Time::Local;
-use Getopt::Std;
-use Text::Template;
-use Net::SMTP;
-use Mail::Header;
-use Mail::Internet;
-use FS::Conf;
-use FS::UID qw(adminsuidsetup);
-use FS::Record qw(qsearch);
-use FS::cust_main;
-
-
-&untaint_argv; #what it sounds like  (eww)
-use vars qw($opt_v $opt_p $opt_m $opt_e $opt_t $report_lines $report_template @buf $header);
-getopts("vpmet:");     #switches
-
-#we're at now now (and later).
-my($_date)= $^T;
-
-# Get the current month
-my ($sec,$min,$hour,$mday,$mon,$year) =
-       (localtime($_date) )[0,1,2,3,4,5]; 
-$mon++;
-$year += 1900;
-
-# Login to the database
-my $user = shift or die &usage;
-adminsuidsetup $user;
-
-# Get the needed configuration files
-my $conf = new FS::Conf;
-my $lpr = $conf->config('lpr');
-my $email = $conf->config('email');
-my $smtpmachine = $conf->config('smtpmachine');
-my $mail_sender = $conf->exists('invoice_from') ? $conf->config('invoice_from') :
-  'postmaster';
-my @report_template = $conf->config('report_template')
-  or die "cannot load config file report_template";
-$report_lines = 0;
-  foreach ( grep /report_lines\(\d+\)/, @report_template ) { #kludgy :/
-  /report_lines\((\d+)\)/;
-  $report_lines += $1;
-}
-die "no report_lines() functions in template?" unless $report_lines;
-$report_template = new Text::Template (
-  TYPE   => 'ARRAY',
-  SOURCE => [ map "$_\n", @report_template ],
-) or die "can't create new Text::Template object: $Text::Template::ERROR";
-
-
-my(@customers)=qsearch('cust_main',{});
-if (scalar(@customers) == 0)
-{
-       exit 1;
-}
-
-# Open print and email pipes
-# $lpr and opt_p for printing
-# $email and opt_m for email
-
-if ($lpr && $opt_p)
-{
-        open(LPR, "|$lpr");
-}
-
-if ($email && $opt_m)
-{
-  $ENV{MAILADDRESS} = $mail_sender;
-  $header = new Mail::Header ( [
-    "From: Account Processor",
-    "To: $email",
-    "Sender: $mail_sender",
-    "Reply-To: $mail_sender",
-    "Subject: Receivables",
-  ] );
-}
-
-my $total = 0;
-
-
-# Now I can start looping
-foreach my $customer (@customers)
-{
-  my $custnum = $customer->getfield('custnum');
-  my $first = $customer->getfield('first');
-  my $last = $customer->getfield('last');
-  my $company = $customer->getfield('company');
-  my $daytime = $customer->getfield('daytime');
-  my $balance = $customer->balance;
-
-
-  if ($balance != 0) {
-    $total += $balance;
-    push @buf, sprintf(qq{%8d %-32.32s %12s %9.2f},
-      $custnum,
-      $first . " " . $last . "   " . $company,
-      $daytime,
-      $balance);
-
-  }
-
-}
-
-push @buf, ('', sprintf(qq{%61s}, "========="), sprintf(qq{%61.2f}, $total));
-
-sub FS::receivables_report::_template::report_lines {
-  my $lines = shift;
-  map {
-    scalar(@buf) ? shift @buf : '' ;
-  }
-  ( 1 .. $lines );
-}
-
-$FS::receivables_report::_template::title = " R E C E I V A B L E S ";
-$FS::receivables_report::_template::title = $opt_t if $opt_t;
-$FS::receivables_report::_template::page = 1;
-$FS::receivables_report::_template::date = $_date;
-$FS::receivables_report::_template::date = $_date;
-$FS::receivables_report::_template::total_pages = 
-  int( scalar(@buf) / $report_lines);
-$FS::receivables_report::_template::total_pages++ if scalar(@buf) % $report_lines;
-
-my @report;
-while (@buf) {
-  push @report, split("\n", 
-    $report_template->fill_in( PACKAGE => 'FS::receivables_report::_template' )
-  );
-  $FS::receivables_report::_template::page++;
-}
-
-if ($opt_v) {
-  print map "$_\n", @report;
-}
-if($lpr && $opt_p)
-{
-  print LPR map "$_\n", @report;
-  print LPR "\f" if $opt_e;
-  close LPR || die "Could not close printer: $lpr\n";
-}
-if($email && $opt_m)
-{
-  my $message = new Mail::Internet (
-    'Header' => $header,
-    'Body' => [ (@report) ],
-  );
-  $!=0;
-  $message->smtpsend( Host => "$smtpmachine" )
-    or die "can't send report to $email via $smtpmachine: $!";
-}
-
-
-# subroutines
-
-sub untaint_argv {
-  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
-    $ARGV[$_] =~ /^([\w\-\/ \.]*)$/ || die "Illegal argument \"$ARGV[$_]\"";
-    $ARGV[$_]=$1;
-  }
-}
-
-sub usage {
-  die "Usage:\n\n  freeside-receivables-report [-v] [-p] [-e] user\n";
-}
-
-=head1 NAME
-
-freeside-receivables-report - Prints or emails outstanding receivables.
-
-=head1 SYNOPSIS
-
-  freeside-receivables-report [-v] [-p] [-m] [-e] [-t "title"] user
-
-=head1 DESCRIPTION
-
-Prints or emails outstanding receivables
-
-B<-v>: Verbose - Prints records to STDOUT.
-
-B<-p>: Print to printer lpr as found in the conf directory.
-
-B<-m>: Mail output to user found in the Conf email file.
-
-B<-e>: Print a final form feed to the printer.
-
-B<-t>: supply a title for the top of each page.
-
-user: From the mapsecrets file - see config.html from the base documentation
-
-=head1 VERSION
-
-$Id: freeside-receivables-report,v 1.6 2002-09-09 22:57:34 ivan Exp $
-
-=head1 BUGS
-
-Yes..... Use at your own risk. No guarantees or warrantees of any
-kind apply to this program. Parts of this program are hacked from
-other GNU licensed software created mainly by Ivan Kohler.
-
-This is released under the GNU Public License. See www.gnu.org
-for more information regarding this license.
-
-=head1 SEE ALSO
-
-L<FS::cust_main>, config.html from the base documentation
-
-=head1 AUTHOR
-
-Jeff Finucane <jeff@cmh.net>
-
-based on print-batch by Joel Griffiths <griff@aver-computer.com>
-
-=cut
-
index 264cbc5..371a646 100644 (file)
@@ -9,6 +9,7 @@
 
 use strict;
 use vars qw( $Debug %kids $kids $max_kids $shutdown $log_file $ssh_pid );
+use subs qw( lock_write unlock_write );
 use Fcntl qw(:flock);
 use POSIX qw(:sys_wait_h setsid);
 use IO::Handle;
@@ -32,18 +33,18 @@ $kids = 0;
 
 my $user = shift or die &usage;
 my $machine = shift or die &usage;
-my $pid_file = "/var/run/freeside-selfservice-server.$user.pid";
-#my $pid_file = "/var/run/freeside-selfservice-server.$user.pid"; $FS::UID::datasrc not posible, but should include machine name at least, hmm
+my $tag = scalar(@ARGV) ? shift : '';
+
+# $FS::UID::datasrc not posible
+my $pid_file = "/var/run/freeside-selfservice-server.$user.$machine.pid";
+
+my $lock_file = "/usr/local/etc/freeside/selfservice.$machine.writelock";
+open(LOCKFILE,">$lock_file") or die "can't open $lock_file: $!";
 
 &init($user);
 
 my $conf = new FS::Conf;
 
-if ($conf->exists('selfservice_server-quiet')) {
-    $FS::cust_bill::quiet = 1;
-    $FS::cust_pkg::quiet = 1;
-}
-
 my $clientd = "/usr/local/sbin/freeside-selfservice-clientd"; #better name?
 
 my $warnkids=0;
@@ -51,7 +52,7 @@ while (1) {
   my($writer,$reader,$error) = (new IO::Handle, new IO::Handle, new IO::Handle);
   warn "connecting to $machine\n" if $Debug;
 
-  $ssh_pid = sshopen2($machine,$reader,$writer,$clientd);
+  $ssh_pid = sshopen2($machine,$reader,$writer,$clientd,$tag);
 
 #  nstore_fd(\*writer, {'hi'=>'there'});
 
@@ -74,7 +75,18 @@ while (1) {
 
     warn "receiving packet from client\n" if $Debug;
 
-    my $packet = fd_retrieve($reader);
+    my $packet = eval { fd_retrieve($reader); };
+    if ( $@ ) {
+      warn "Storable error receiving packet from client".
+           " (assuming lost connection): $@\n"
+        if $Debug;
+      if ( $ssh_pid ) {
+        warn "sending TERM signal to ssh process $ssh_pid\n" if $Debug;
+        kill 'TERM', $ssh_pid;
+        $ssh_pid = 0;
+      }
+      last;
+    }
     warn "packet received\n".
          join('', map { " $_=>$packet->{$_}\n" } keys %$packet )
       if $Debug > 1;
@@ -109,10 +121,10 @@ while (1) {
       $rv->{_token} = $packet->{_token}; #identifier
 
       warn "sending response\n" if $Debug;
-      flock($writer, LOCK_EX) or die "FATAL: can't lock write stream: $!";
+      lock_write;
       nstore_fd($rv, $writer) or die "FATAL: can't send response: $!";
       $writer->flush or die "FATAL: can't flush: $!";
-      flock($writer, LOCK_UN) or die "WARNING: can't release write lock: $!";
+      unlock_write;
 
       warn "child exiting\n" if $Debug;
       exit; #end-of-kid
@@ -120,6 +132,9 @@ while (1) {
 
   }
 
+  warn "connection lost, reconnecting\n" if $Debug;
+  sleep 3;
+
 }
 
 ###
@@ -229,7 +244,23 @@ sub _do_logmsg {
   close $log;
 }
 
+sub lock_write {
+  #broken on freebsd?
+  #flock($writer, LOCK_EX) or die "FATAL: can't lock write stream: $!";
+
+  flock(LOCKFILE, LOCK_EX) or die "FATAL: can't lock $lock_file: $!";
+
+}
+
+sub unlock_write {
+  #broken on freebsd?
+  #flock($writer, LOCK_UN) or die "WARNING: can't release write lock: $!";
+
+  flock(LOCKFILE, LOCK_UN) or die "FATAL: can't unlock $lock_file: $!";
+
+}
+
 sub usage {
-  die "Usage:\n\n  fs_signup_server user machine\n";
+  die "Usage:\n\n  freeside-selfservice-server user machine\n";
 }
 
index 2cb555e..7512cc1 100755 (executable)
@@ -336,10 +336,13 @@ sub tables_hash_hack {
         'typenum',  'int',            '',     '',
         'freq',     'int',       'NULL', '',
         'prog',     @perl_type,
+        'disabled',     'char', 'NULL', 1,
+        'username', 'varchar',       'NULL',     $char_d,
+        '_password','varchar',       'NULL',     $char_d,
       ],
       'primary_key' => 'agentnum',
       'unique' => [],
-      'index' => [ ['typenum'] ],
+      'index' => [ ['typenum'], ['disabled'] ],
     },
 
     'agent_type' => {
@@ -405,7 +408,7 @@ sub tables_hash_hack {
       ],
       'primary_key' => 'eventpart',
       'unique' => [],
-      'index' => [ ['payby'] ],
+      'index' => [ ['payby'], ['disabled'], ],
     },
 
     'cust_bill_pkg' => {
@@ -499,6 +502,7 @@ sub tables_hash_hack {
         'ship_fax',      'varchar', 'NULL', 12,
         'payby',    'char', '',     4,
         'payinfo',  'varchar', 'NULL', $char_d,
+        'paycvv',   'varchar', 'NULL', 4,
         #'paydate',  @date_type,
         'paydate',  'varchar', 'NULL', 10,
         'payname',  'varchar', 'NULL', $char_d,
@@ -539,6 +543,8 @@ sub tables_hash_hack {
         'exempt_amount', @money_type,
         'tax',      'real',  '',    '', #tax %
         'taxname',  'varchar',  'NULL',    $char_d,
+        'setuptax',  'char', 'NULL', 1, # Y = setup tax exempt
+        'recurtax',  'char', 'NULL', 1, # Y = recur tax exempt
       ],
       'primary_key' => 'taxnum',
       'unique' => [],
@@ -561,7 +567,7 @@ sub tables_hash_hack {
       ],
       'primary_key' => 'paynum',
       'unique' => [],
-      'index' => [ [ 'custnum' ], [ 'paybatch' ] ],
+      'index' => [ [ 'custnum' ], [ 'paybatch' ], [ 'payby' ], [ '_date' ] ],
     },
 
     'cust_bill_pay' => {
@@ -673,7 +679,7 @@ sub tables_hash_hack {
         'pkg',        'varchar',   '',   $char_d,
         'comment',    'varchar',   '',   $char_d,
         'setup',      @perl_type,
-        'freq',       'int', '', '',  #billing frequency (months)
+        'freq',       'varchar',   '',   $char_d,  #billing frequency
         'recur',      @perl_type,
         'setuptax',  'char', 'NULL', 1,
         'recurtax',  'char', 'NULL', 1,
@@ -702,6 +708,7 @@ sub tables_hash_hack {
         'pkgpart',    'int',    '',   '',
         'svcpart',    'int',    '',   '',
         'quantity',   'int',    '',   '',
+        'primary_svc','char', 'NULL',  1,
       ],
       'primary_key' => '',
       'unique' => [ ['pkgpart', 'svcpart'] ],
@@ -712,10 +719,11 @@ sub tables_hash_hack {
       'columns' => [
         'refnum',   'serial',    '',   '',
         'referral', 'varchar',   '',   $char_d,
+        'disabled',     'char', 'NULL', 1,
       ],
       'primary_key' => 'refnum',
       'unique' => [],
-      'index' => [],
+      'index' => [ ['disabled'] ],
     },
 
     'part_svc' => {
@@ -776,7 +784,7 @@ sub tables_hash_hack {
       'columns' => [
         'svcnum',    'int',    '',   '',
         'username',  'varchar',   '',   $username_len, #unique (& remove dup code)
-        '_password', 'varchar',   '',   50, #13 for encryped pw's plus ' *SUSPENDED* (mp5 passwords can be 34)
+        '_password', 'varchar',   '',   72, #13 for encryped pw's plus ' *SUSPENDED* (md5 passwords can be 34, blowfish 60)
         'sec_phrase', 'varchar',  'NULL',   $char_d,
         'popnum',    'int',    'NULL',   '',
         'uid',       'int', 'NULL',   '',
@@ -834,10 +842,11 @@ sub tables_hash_hack {
 
     'svc_forward' => {
       'columns' => [
-        'svcnum',   'int',    '',  '',
-        'srcsvc',   'int',    '',  '',
-        'dstsvc',   'int',    '',  '',
-        'dst',      'varchar',    'NULL',  $char_d,
+        'svcnum',   'int',            '',   '',
+        'srcsvc',   'int',        'NULL',   '',
+        'src',      'varchar',    'NULL',  255,
+        'dstsvc',   'int',        'NULL',   '',
+        'dst',      'varchar',    'NULL',  255,
       ],
       'primary_key' => 'svcnum',
       'unique'      => [],
@@ -1099,6 +1108,31 @@ sub tables_hash_hack {
       'index' => [],
     },
 
+    'acct_snarf' => {
+      'columns' => [
+        'snarfnum',  'int', '', '',
+        'svcnum',    'int', '', '',
+        'machine',   'varchar', '', 255,
+        'protocol',  'varchar', '', $char_d,
+        'username',  'varchar', '', $char_d,
+        '_password', 'varchar', '', $char_d,
+      ],
+      'primary_key' => 'snarfnum',
+      'unique' => [],
+      'index'  => [ [ 'svcnum' ] ],
+    },
+
+    'svc_external' => {
+      'columns' => [
+        'svcnum', 'int', '', '',
+        'id',     'int', '', '',
+        'title',  'varchar', 'NULL', $char_d,
+      ],
+      'primary_key' => 'svcnum',
+      'unique'      => [],
+      'index'       => [],
+    },
+
   );
 
   %tables;
diff --git a/FS/t/acct_snarf.t b/FS/t/acct_snarf.t
new file mode 100644 (file)
index 0000000..642760f
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::acct_snarf;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_broadband.t b/FS/t/svc_broadband.t
new file mode 100644 (file)
index 0000000..02dc112
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_broadband;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_external.t b/FS/t/svc_external.t
new file mode 100644 (file)
index 0000000..20a6767
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_external;
+$loaded=1;
+print "ok 1\n";
index 6256ccc..72b3e58 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -6,33 +6,41 @@ DATASOURCE = DBI:Pg:dbname=freeside
 DB_USER = freeside
 DB_PASSWORD=
 
-TEMPLATE = asp
-#TEMPLATE = mason
+#TEMPLATE = asp
+TEMPLATE = mason
 
 ASP_GLOBAL = /usr/local/etc/freeside/asp-global
 MASON_HANDLER = /usr/local/etc/freeside/handler.pl
 MASONDATA = /usr/local/etc/freeside/masondata
 
-#deb, others?
+#deb
 FREESIDE_DOCUMENT_ROOT = /var/www/freeside
+#redhat, mandrake
+#FREESIDE_DOCUMENT_ROOT = /var/www/html/freeside
 #freebsd
 #FREESIDE_DOCUMENT_ROOT = /usr/local/www/data/freeside
+#openbsd
+#FREESIDE_DOCUMENT_ROOT = /var/www/htdocs/freeside
 
-#deb, others?
+#deb, redhat, mandrake, others?
 INIT_FILE = /etc/init.d/freeside
 #freebsd
 #INIT_FILE = /usr/local/etc/rc.d/011.freeside.sh
 
-#deb, others?
+#deb
 HTTPD_RESTART = /etc/init.d/apache restart
+#redhat, mandrake
+#HTTPD_RESTART = /etc/init.d/httpd restart
 #freebsd
 #HTTPD_RESTART = /usr/local/etc/rc.d/apache.sh stop; sleep 1; /usr/local/etc/rc.d/apache.sh start
+#openbsd
+#HTTPD_RESTART = kill -TERM `cat /var/www/logs/httpd.pid`; sleep 1; /usr/sbin/httpd -u -DSSL
 
 FREESIDE_RESTART = ${INIT_FILE} restart
 
-#deb, others?
+#deb, redhat, mandrake, others?
 INSTALLGROUP = root
-#freebsd
+#freebsd, openbsd
 #INSTALLGROUP = wheel
 
 #edit the stuff below to have the daemons start
@@ -42,24 +50,17 @@ QUEUED_USER=fs_queue
 #eventually this shouldn't be needed
 FREESIDE_PATH = `pwd`
 
-PASSWD_USER = nostart
-PASSWD_MACHINE = localhost
-
-SIGNUP_USER = nostart
-SIGNUP_MACHINE = localhost
-SIGNUP_AGENTNUM = 2
-SIGNUP_REFNUM = 2
-
 SELFSERVICE_USER = fs_selfservice
-SELFSERVICE_MACHINE = localhost
+SELFSERVICE_MACHINES = localhost
+# SELFSERVICE_MACHINES = web1.example.com web2.example.com
 
 #---
 
 #not changable yet
 FREESIDE_CONF = /usr/local/etc/freeside
 
-VERSION=1.5.0pre3
-TAG=freeside_1_5_0pre3
+VERSION=1.5.0pre4
+TAG=freeside_1_5_0pre4
 
 help:
        @echo "supported targets: aspdocs masondocs alldocs docs install-docs"
@@ -136,14 +137,8 @@ install-init:
        perl -p -i -e "\
          s/%%%QUEUED_USER%%%/${QUEUED_USER}/g;\
          s'%%%FREESIDE_PATH%%%'${FREESIDE_PATH}'g;\
-         s/%%%PASSWD_USER%%%/${PASSWD_USER}/g;\
-         s/%%%PASSWD_MACHINE%%%/${PASSWD_MACHINE}/g;\
-         s/%%%SIGNUP_USER%%%/${SIGNUP_USER}/g;\
-         s/%%%SIGNUP_MACHINE%%%/${SIGNUP_MACHINE}/g;\
-         s/%%%SIGNUP_AGENTNUM%%%/${SIGNUP_AGENTNUM}/g;\
-         s/%%%SIGNUP_REFNUM%%%/${SIGNUP_REFNUM}/g;\
          s/%%%SELFSERVICE_USER%%%/${SELFSERVICE_USER}/g;\
-         s/%%%SELFSERVICE_MACHINE%%%/${SELFSERVICE_MACHINE}/g;\
+         s/%%%SELFSERVICE_MACHINES%%%/${SELFSERVICE_MACHINES}/g;\
        " ${INIT_FILE}
 
 install: install-perl-modules install-docs install-init
@@ -157,8 +152,7 @@ create-database:
 
 create-config: install-perl-modules
        [ -e ${FREESIDE_CONF} ] && mv ${FREESIDE_CONF} ${FREESIDE_CONF}.`date +%Y%m%d%H%M%S` || true
-       mkdir ${FREESIDE_CONF}
-       chown freeside ${FREESIDE_CONF}
+       install -d -o freeside ${FREESIDE_CONF}
 
        touch ${FREESIDE_CONF}/secrets
        chown freeside ${FREESIDE_CONF}/secrets
@@ -170,7 +164,8 @@ create-config: install-perl-modules
 
        mkdir "${FREESIDE_CONF}/conf.${DATASOURCE}"
        rm -rf conf/registries #old dirs just won't go away
-       cp conf/[a-z]* "${FREESIDE_CONF}/conf.${DATASOURCE}"
+       #cp conf/[a-z]* "${FREESIDE_CONF}/conf.${DATASOURCE}"
+       cp `ls -d conf/[a-z]* | grep -v CVS` "${FREESIDE_CONF}/conf.${DATASOURCE}"
        chown -R freeside "${FREESIDE_CONF}/conf.${DATASOURCE}"
 
        mkdir "${FREESIDE_CONF}/counters.${DATASOURCE}"
@@ -190,8 +185,8 @@ clean:
 #these are probably only useful if you're me...
 
 upload-docs: forcehtmlman
-       ssh cleanwhisker.420.am rm -rf /var/www/www.sisd.com/freeside/devdocs
-       scp -pr httemplate/docs cleanwhisker.420.am:/var/www/www.sisd.com/freeside/devdocs
+       ssh pouncequick.420.am rm -rf /var/www/www.sisd.com/freeside/devdocs
+       scp -pr httemplate/docs pouncequick.420.am:/var/www/www.sisd.com/freeside/devdocs
 
 release: upload-docs
        cd /home/ivan/freeside
@@ -202,7 +197,7 @@ release: upload-docs
        cvs export -r ${TAG} -d freeside-${VERSION} freeside
        tar czvf freeside-${VERSION}.tar.gz freeside-${VERSION}
 
-       scp freeside-${VERSION}.tar.gz ivan@cleanwhisker.420.am:/var/www/sisd.420.am/freeside/
+       scp freeside-${VERSION}.tar.gz ivan@pouncequick.420.am:/var/www/sisd.420.am/freeside/
        mv freeside-${VERSION} freeside-${VERSION}.tar.gz ..
 
 update-webdemo:
index 57eca2b..3287b01 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/perl -w
 #
-# $Id: bind.import,v 1.3 2002-07-15 01:44:23 ivan Exp $
+# $Id: bind.import,v 1.4 2004-02-12 10:44:11 ivan Exp $
 
 #need to manually put header in /usr/local/etc/freeside/export.<datasrc./bind/<machine>/named.conf.HEADER
 
@@ -79,7 +79,7 @@ print "\nBIND import completed.\n";
 ##
 
 sub usage {
-  die "Usage:\n\n  svc_domain.import user\n";
+  die "Usage:\n\n  bind.import user\n";
 }
 
 ########
diff --git a/bin/create-fetchmailrc b/bin/create-fetchmailrc
new file mode 100644 (file)
index 0000000..e929711
--- /dev/null
@@ -0,0 +1,45 @@
+#!/usr/bin/perl -w
+# this quick hack helps you generate/maintain .fetchmailrc files from
+# FS::acct_snarf data.  it is run from a shellcommands export as:
+# create-fetchmailrc $username $dir $snarf_machine1 $snarf_username1 $snarf__password1 $snarf_machine2 $snarf_username2 $snarf__password2 ...
+
+use strict;
+use POSIX qw( setuid setgid );
+
+my $header = <<END;
+# Configuration created by create-fetchmailrc
+set postmaster "postmaster"
+set bouncemail
+set no spambounce
+set properties ""
+set daemon 240
+END
+
+my $username = shift @ARGV or die "no username specified\n";
+my $homedir  = shift @ARGV or die "no homedir specified\n";
+my $filename = "$homedir/.fetchmailrc";
+
+my $gid = scalar(getgrnam($username)) or die "can't find $username's gid\n";
+my $uid = scalar(getpwnam($username)) or die "can't find $username's uid\n";
+
+open(FETCHMAILRC, ">$filename") or die "can't open $filename: $!\n";
+chown $uid, $gid, $filename     or die "can't chown $uid.$gid $filename: $!\n";
+chmod 0600, $filename           or die "can't chmod 600 $filename: $!\n";
+print FETCHMAILRC $header;
+
+while ($ARGV[0]) {
+  my( $s_machine, $s_username, $s_password ) = splice( @ARGV, 0, 3 );
+  print FETCHMAILRC <<END;
+poll $s_machine
+       user '$s_username' there with password '$s_password' is '$username' here
+END
+}
+
+close FETCHMAILRC;
+
+setgid($gid) or die "can't setgid $gid\n";
+setuid($uid) or die "can't setuid $uid\n";
+$ENV{HOME} = $homedir;
+
+system(qq(fetchmail -a -K --antispam "550,451" -d 180 -f $filename));
+
index c977f87..a449d67 100755 (executable)
@@ -1,10 +1,8 @@
 #!/usr/bin/perl -Tw
-#
-# $Id: dbdef-create,v 1.6 2002-09-19 13:34:52 ivan Exp $
 
 use strict;
 use DBI;
-use DBIx::DBSchema 0.21;
+use DBIx::DBSchema 0.22;
 use FS::UID qw(adminsuidsetup datasrc driver_name);
 
 my $user = shift or die &usage;
index 1d0053a..f946b05 100755 (executable)
@@ -13,10 +13,18 @@ END
 while (<>) {
   next if /^(#|\s*$|\$INCLUDE\s+)/;
   next if /^(VALUE|VENDOR|BEGIN\-VENDOR|END\-VENDOR)\s+/;
-  /^(ATTRIBUTE|ATTRIB_NMC)\s+([\w\-]+)\s+/ or die $_;
+  /^(ATTRIBUTE|ATTRIB_NMC)\s+([\w\-\/]+)\s+/ or die $_;
   $attrib = $2;
   $dbname = lc($2);
-  $dbname =~ s/\-/_/g;
+  $dbname =~ s/[\-\/]/_/g;
+  $dbname = substr($dbname,0,24);
+  while ( exists $hash{$dbname} ) {
+    #warn $dbname;
+    $dbname =~ s/(.)$//;
+    my $w = $1;
+    $w =~ tr/_a-z0-9/a-z0-9_/;
+    $dbname = "$dbname$w";
+  }
   $hash{$dbname} = $attrib;
   #print "$2\n";
 }
@@ -25,8 +33,10 @@ foreach ( keys %hash ) {
 #  print "$_\n" if length($_)>24;
 #  print substr($_,0,24),"\n" if length($_)>24; 
 #  $max = length($_) if length($_)>$max;
-#everything >24 is still unique, at least with freeradius comprehensive dataset
-  print "  '". substr($_,0,24). "' => '$hash{$_}',\n";
+# have to fudge things since everything >24 is *not* unique
+
+  #print "  '". substr($_,0,24). "' => '$hash{$_}',\n";
+  print "  '$_' => '$hash{$_}',\n";
 }
 
 print <<END;
index 475c9a6..3139e0a 100755 (executable)
@@ -1,6 +1,7 @@
 #!/usr/bin/perl
 
-foreach $file ( split(/\n/, `find . -depth -print | grep cgi\$`) ) {
+foreach $file ( split(/\n/, `find . -depth -print`) ) {
+  next unless $file =~ /(cgi|html)$/;
   open(F,$file) or die "can't open $file for reading: $!";
   @file = <F>;
   #print "$file ". scalar(@file). "\n";
@@ -8,6 +9,7 @@ foreach $file ( split(/\n/, `find . -depth -print | grep cgi\$`) ) {
   system("chmod u+w $file");
   open(W,">$file") or die "can't open $file for writing: $!";
   select W; $| = 1; select STDOUT;
+  $newline = ''; #avoid prepending extraneous newlines
   $all = join('',@file);
 
   $mode = 'html';
@@ -26,7 +28,7 @@ foreach $file ( split(/\n/, `find . -depth -print | grep cgi\$`) ) {
         #die;
         next;
       } elsif ( $all =~ /^<%(.*)$/s ) {
-        print W "\n";
+        print W $newline; $newline = "\n";
         $all = $1;
         $mode = 'perlc';
         next;
diff --git a/bin/postfix.export b/bin/postfix.export
new file mode 100755 (executable)
index 0000000..64d9738
--- /dev/null
@@ -0,0 +1,121 @@
+#!/usr/bin/perl -w
+
+use strict;
+#use File::Path;
+use File::Rsync;
+use Net::SSH qw(ssh);
+use FS::UID qw(adminsuidsetup datasrc);
+use FS::Record qw(qsearch); # qsearchs);
+use FS::part_export;
+#use FS::cust_pkg;
+use FS::cust_svc;
+#use FS::svc_domain;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/postfix";
+mkdir $spooldir, 0700 unless -d $spooldir;
+
+my @exports = qsearch('part_export', { 'exporttype' => 'postfix' } );
+
+my $rsync = File::Rsync->new({
+  rsh     => 'ssh',
+#  dry_run => 1,
+});
+
+foreach my $export ( @exports ) {
+
+  my $machine = $export->machine;
+  my $prefix = "$spooldir/$machine";
+  mkdir $prefix, 0700 unless -d $prefix;
+
+  #construct %domain hash
+
+  my $mydomain = $export->option('mydomain');
+  my %domain;
+  foreach my $svc_forward ( $export->svc_x ) {
+
+    my( $username, $domain );
+    my $srcsvc_acct = $svc_forward->srcsvc_acct;
+    if ( $srcsvc_acct ) {
+      ( $username, $domain ) = ( $srcsvc_acct->username, $srcsvc_acct->domain );
+    } elsif ( $svc_forward->src =~ /([^@]*)\@([^@]+)$/ ) {
+      ( $username, $domain ) = ( $1, $2 );
+    } else {
+      die "bad svc_forward record?  svcnum ". $svc_forward->svcnum. "\n";
+    }
+
+    my( $dusername, $ddomain );
+    my $dstsvc_acct = $svc_forward->dstsvc_acct;
+    if ( $dstsvc_acct ) {
+      $dusername = $dstsvc_acct->username;
+      $ddomain = $dstsvc_acct->domain;
+    } elsif ( $svc_forward->dst =~ /([^@]+)\@([^@]+)$/ ) {
+      ( $dusername, $ddomain ) = ( $1, $2 );
+    } else {
+      die "bad svc_forward record?  svcnum ". $svc_forward->svcnum. "\n";
+    }
+    my $dest;
+    if ( $ddomain eq $mydomain ) {
+      $dest = $dusername;
+    } else {
+      $dest = "$dusername\@$ddomain";
+    }
+
+    push @{$domain{$domain}{$username}}, $dest;
+
+  }
+
+  #write aliases
+
+  my $aliases = delete $domain{$mydomain};
+  open(ALIASES, ">$prefix/aliases") or die "can't open $prefix/aliases: $!";
+  foreach my $alias ( keys %$aliases ) {
+    print ALIASES "$alias: ". join(',', @{ $aliases->{$alias} } ). "\n";
+  }
+  close ALIASES;
+
+  #write virtual
+
+  open(VIRTUAL, ">$prefix/virtual") or die "can't open $prefix/virtual: $!";
+  foreach my $domain ( keys %domain ) {
+    print VIRTUAL "$domain DOMAIN\n";
+    #foreach my $virtual ( sort { $a ne '' <=> $b ne '' } keys %{$domain{$domain}} ) {
+    foreach my $virtual ( sort { ( ($b ne '') <=> ($a ne '') ) || $a cmp $b } keys %{$domain{$domain}} ) {
+      print VIRTUAL "$virtual\@$domain ".
+                    join(',', @{ $domain{$domain}{$virtual} } ). "\n";
+    }
+    print VIRTUAL "\n";
+  }
+  close VIRTUAL;
+
+  #rsync
+
+  my $user = $export->option('user');
+  $rsync->exec( {
+    src     => "$prefix/aliases",
+    dest    => "$user\@$machine:". $export->option('aliases'),
+  } ) or die "rsync to $machine failed: ". join(" / ", $rsync->err);
+#  warn $rsync->out;
+
+  ssh("$user\@$machine", "newaliases");
+#  ssh("$user\@$machine", "postfix reload");
+
+  $rsync->exec( {
+    src     => "$prefix/virtual",
+    dest    => "$user\@$machine:". $export->option('virtual'),
+  } ) or die "rsync to $machine failed: ". join(" / ", $rsync->err);
+#  warn $rsync->out;
+  ssh("$user\@$machine", "postmap hash:/etc/postfix/virtual");
+  ssh("$user\@$machine", "postfix reload");
+
+}
+
+# -----
+
+sub usage {
+  die "Usage:\n  postfix.export user\n"; 
+}
+
+
diff --git a/bin/sendmail.import b/bin/sendmail.import
new file mode 100644 (file)
index 0000000..8a9de9f
--- /dev/null
@@ -0,0 +1,176 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Term::Query qw(query);
+use Net::SCP qw(iscp);
+use FS::UID qw(adminsuidsetup datasrc);
+use FS::Record qw(qsearch qsearchs);
+##use FS::svc_acct_sm;
+#use FS::svc_domain;
+#use FS::domain_record;
+use FS::svc_acct;
+##use FS::part_svc;
+use FS::svc_forward;
+use FS::svc_domain;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+#$FS::svc_Common::noexport_hack = 1;
+#$FS::domain_record::noserial_hack = 1;
+
+use vars qw($defaultdomain);
+$defaultdomain = 'surferz.net';
+
+use vars qw($svcpart $forward_svcpart);
+$svcpart = 2;
+$forward_svcpart = 4;
+
+use vars qw($spooldir);
+$spooldir = "/usr/local/etc/freeside/export.". datasrc. "/sendmail";
+mkdir $spooldir unless -d $spooldir;
+
+print "\n\n", <<END;
+Enter the location and name of your Sendmail aliases file, for example
+"mail.isp.com:/etc/mail/aliases"
+END
+my($aliases)=&getvalue(":");
+
+use vars qw($aliases_machine $aliases_prefix);
+$aliases_machine = (split(/:/, $aliases))[0];
+$aliases_prefix = "$spooldir/$aliases_machine";
+mkdir $aliases_prefix unless -d $aliases_prefix;
+
+#iscp("root\@$aliases","$aliases_prefix/aliases.import");
+iscp("ivan\@$aliases","$aliases_prefix/aliases.import");
+
+print "\n\n", <<END;
+Enter the location and name of your Sendmail virtusertable directory, for example
+"mail.isp.com:/etc/mail/virtusertable"
+END
+my($virtusertable)=&getvalue(":");
+
+use vars qw($virtusertable_machine $virtusertable_prefix);
+$virtusertable_machine = (split(/:/, $virtusertable))[0];
+$virtusertable_prefix = "$spooldir/$virtusertable_machine";
+mkdir $virtusertable_prefix unless -d $virtusertable_prefix;
+mkdir "$virtusertable_prefix/virtusertable.import"
+  unless -d "$virtusertable_prefix/virtusertable.import";
+
+#iscp("root\@$virtusertable/*","$aliases_prefix/virtusertable.import/");
+iscp("ivan\@$virtusertable/*","$aliases_prefix/virtusertable.import/");
+
+sub getvalue {
+  my $prompt = shift;
+  $^W=0; # Term::Query isn't -w-safe
+  my $return = query $prompt, '';
+  $^W=1;
+  $return;
+}
+
+print "\n\n";
+
+##
+
+foreach my $file ( 
+  "$aliases_prefix/aliases.import",
+  glob("$aliases_prefix/virtusertable.import/*"),
+) {
+
+  warn "importing $file\n";
+  
+  open(FILE,"<$file") or die $!;
+  while (<FILE>) {
+    next if /^\s*#/ || /^\s*$/; #skip comments & blank lines
+  
+    unless ( /^([\w\@\.\-]+)[:\s]\s*(.*\S)\s*$/ ) {
+      warn "Unparsable line: $_";
+      next;
+    }
+    my($rawusername, $rawdest) = ($1, $2);
+  
+    my($username, $domain);
+    if ( $rawusername =~ /^([\w\-\.\&]*)\@([\w\.\-]+)$/ ) {
+      $username = $1;
+      $domain = $2;
+    } elsif ( $rawusername =~ /\@/ ) {
+      die "Unparsable username: $rawusername\n";
+    } else {
+      $username = $rawusername;
+      $domain = $defaultdomain;
+    }
+  
+    #find svc_acct record or set $src
+    my($srcsvc, $src) = &svcnum_or_literal($username, $domain);
+
+    foreach my $dest ( split(/,/, $rawdest) ) {
+
+      my($dusername, $ddomain);
+      if ( $dest =~ /^([\w\-\.\&]+)\@([\w\.\-]+)$/ ) {
+        $dusername = $1;
+        $ddomain = $2;          
+      } elsif ( $dest =~ /\@/ ) {
+        die "Unparsable username: $dest\n";
+      } else {                 
+        $dusername = $dest;
+        $ddomain = $defaultdomain;
+      }
+      my($dstsvc, $dst) = &svcnum_or_literal($dusername, $ddomain);
+
+      my $svc_forward = new FS::svc_forward ({
+        svcpart => $forward_svcpart,
+        srcsvc => $srcsvc,
+        src    => $src,
+        dstsvc => $dstsvc,
+        dst    => $dst,
+      });
+      my $error = $svc_forward->insert;
+      #my $error = $svc_forward->check;
+      if ( $error ) {
+        die "$rawusername: $rawdest: $error\n";
+      }
+    }
+
+
+  } #next entry
+
+} #next file
+  
+##
+
+sub svcnum_or_literal {
+  my($username, $domain) = @_;
+
+  my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } );
+  my $domsvc = $svc_domain ? $svc_domain->svcnum : '';
+
+  my @svc_acct = grep { $_->cust_svc->svcpart == $svcpart }
+                   qsearch('svc_acct', {
+                     'username' => $username,
+                     'domsvc'   => $domsvc,
+                   });
+
+  if ( scalar(@svc_acct) > 1 ) {
+    die "multiple sources found for $username\@$domain !\n";
+  }
+
+  my( $svcnum, $literal ) = ('', '');
+  if ( @svc_acct ) {
+    my $svc_acct = $svc_acct[0];
+    $svcnum = $svc_acct->svcnum;
+  } else {
+    $literal = "$username\@$domain";
+  }
+
+  return( $svcnum, $literal );
+
+}
+
+sub usage {
+  die "Usage:\n\n  sendmail.import user\n";
+}
+
+
+
+
+
diff --git a/bin/shadow.reimport b/bin/shadow.reimport
new file mode 100755 (executable)
index 0000000..2c0ad1f
--- /dev/null
@@ -0,0 +1,98 @@
+#!/usr/bin/perl -Tw
+# $Id: shadow.reimport,v 1.1 2004-02-03 00:19:45 ivan Exp $
+
+use strict;
+use vars qw(%part_svc);
+use Term::Query qw(query);
+use Net::SCP qw(iscp);
+use FS::UID qw(adminsuidsetup datasrc);
+use FS::Record qw(qsearch qsearchs);
+use FS::svc_acct;
+use FS::part_svc;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+push @FS::svc_acct::shells, qw(/bin/sync /sbin/shutdown /bin/halt /sbin/halt); #others?
+
+my($spooldir)="/usr/local/etc/freeside/export.". datasrc;
+
+#$FS::svc_acct::nossh_hack = 1;
+$FS::svc_Common::noexport_hack = 1;
+
+###
+
+%part_svc=map { $_->svcpart, $_ } qsearch('part_svc',{'svcdb'=>'svc_acct'});
+
+die "No services with svcdb svc_acct!\n" unless %part_svc;
+
+print "\n\n", &menu_svc, "\n", <<END;
+Enter part number to import.
+END
+my($shell_svcpart)=&getpart;
+
+print "\n\n", <<END;
+Enter the location and name of your _user_ shadow file, for example
+"mail.isp.com:/etc/shadow" or "bsd.isp.com:/etc/master.passwd"
+END
+my($loc_shadow)=&getvalue(":");
+iscp("root\@$loc_shadow", "$spooldir/shadow.import");
+
+sub menu_svc {
+  ( join "\n", map "$_: ".$part_svc{$_}->svc, sort keys %part_svc ). "\n";
+}
+sub getpart {
+  $^W=0; # Term::Query isn't -w-safe
+  my $return = query "Enter part number:", 'irk', [ keys %part_svc ];
+  $^W=1;
+  $return;
+}
+sub getvalue {
+  my $prompt = shift;
+  $^W=0; # Term::Query isn't -w-safe
+  my $return = query $prompt, '';
+  $^W=1;
+  $return;
+}
+
+print "\n\n";
+
+###
+
+open(SHADOW,"<$spooldir/shadow.import");
+
+my($line, $updated);
+while (<SHADOW>) {
+  $line++;
+  chop;
+  my($username,$password)=split(/:/);
+
+  my @svc_acct = grep { $_->cust_svc->svcpart == $shell_svcpart } 
+                 qsearch('svc_acct', { 'username' => $username } );
+
+  next unless @svc_acct;
+
+  if ( scalar(@svc_acct) > 1 ) {
+    warn "more than one $username found!\n";
+    next;
+  }
+
+  my $svc_acct = shift @svc_acct;
+
+  next if $svc_acct->_password eq $password;
+
+  my $new_svc_acct = new FS::svc_acct( { $svc_acct->hash } );
+  $new_svc_acct->_password($password);
+  #my $error = $new_svc_acct->replace($svc_acct);
+  #die "$username: $error" if $error;
+
+  $updated++;
+
+}
+
+warn "$updated of $line passwords changed\n";
+
+sub usage {
+  die "Usage:\n\n  shadow.reimport user\n";
+}
+
diff --git a/conf/invoice_latex b/conf/invoice_latex
new file mode 100644 (file)
index 0000000..f63ab32
--- /dev/null
@@ -0,0 +1,155 @@
+%% file: Standard Multipage.tex\r
+%% Purpose: Multipage bill template for e-Bills\r
+%% \r
+%% Created by Mark Asplen-Taylor\r
+%% Asplen Management Ltd\r
+%% www.asplen.co.uk\r
+%%\r
+%% Modified for Freeside by Ivan Kohler\r
+%%\r
+%% Changes\r
+%%     0.1     4/12/00 Created\r
+%%     0.2     18/10/01        More fields added\r
+%%     1.0     16/11/01        RELEASED\r
+%%     1.2     16/10/02        Invoice number added\r
+%%     1.3     2/12/02 Logo graphic added\r
+%%     1.4     7/2/03  Multipage headers/footers added\r
+%%      n/a     10/12/03 forked for Freeside; checked into CVS\r
+%%\r
+\r
+\documentclass[letterpaper]{article}\r
+\r
+\usepackage{fancyhdr,lastpage,ifthen,longtable,afterpage}\r
+\usepackage{graphicx}                  % required for logo graphic\r
+\r
+\addtolength{\voffset}{-0.0in} % top margin to top of header\r
+\addtolength{\hoffset}{-0.60in}        %left margin on page\r
+\addtolength{\topmargin}{-0.6in}       % top margin to top of header\r
+\setlength{\headheight}{1in}           % height of header\r
+\setlength{\headsep}{0.5in}            % between header and text\r
+\addtolength{\textheight}{-0.4in}      % height of main text\r
+\r
+\addtolength{\textheight}{-0.5in}      % height of main text\r
+\setlength{\footskip}{0.5in}           % bottom of footer from bottom of text\r
+\r
+\addtolength{\textwidth}{2.1in}        % width of text\r
+\setlength{\oddsidemargin}{0in}        % odd page left margin\r
+\setlength{\evensidemargin}{0in}       % even page left margin\r
+\r
+\renewcommand{\headrulewidth}{0pt}\r
+\renewcommand{\footrulewidth}{1pt}\r
+\r
+                                               % New command for address lines i.e. skip them if blank\r
+\r
+\newcommand{\addressline}[1]{\ifthenelse{\equal{#1}{}}{}{#1\newline}}\r
+\newcommand{\dollar}[1][]{\symbol{36}} % Inserts dollar symbol\r
+\r
+\pagestyle{fancy}\r
+\r
+%% Font options are:\r
+%%     bch     Bitsream Charter\r
+%%     put     Utopia\r
+%%     phv     Adobe Helvetica\r
+%%     pnc     New Century Schoolbook\r
+%%     ptm     Times\r
+%%     pcr     Courier\r
+\r
+\renewcommand{\familydefault}{phv}             \r
+\r
+\begin{document}\r
+%\r
+%%     Headers and footers defined for the first page\r
+\fancyfoot[CO,CE]{\small{\r
+\begin{tabular}{c}\r
+$footer\r
+\end{tabular}}}\r
+%\r
+%%     The LH Heading comprising logo\r
+%%     UNCOMMENT the following FOUR lines and change the path if necssary to provide a logo\r
+\fancyhead[LO,LE]{\r
+\begin{tabular}{l}\r
+\includegraphics{/usr/local/etc/freeside/logo.eps}\r
+\end{tabular}}\r
+%\r
+%%     The Heading comprising isue date, customer ref & INVOICE name\r
+\fancyhead[RO,RE]{\r
+\begin{tabular}{rcl}\r
+Invoice date & & Invoice number \\\r
+\vspace{0.2cm}\r
+\textbf{$date} & & \textbf{$invnum} \\\hline\r
+\rule{0pt}{5ex} &~~ \huge{\textsc{Invoice}}& \\\r
+\vspace{-0.2cm}\r
+ & & \\\hline\r
+\end{tabular}}\r
+%\r
+%%     Header & footer changes for subsequent pages\r
+%\r
+\afterpage{ \fancyfoot[RO,RE]{\small{\thepage\ of \pageref{LastPage}}} }\r
+\afterpage{ \fancyfoot[CO,CE]{\small{$smallfooter}} }\r
+\afterpage{ \fancyhead[LO,LE]{\small{}} }\r
+\afterpage{ \fancyhead[RO,RE]{\small{\r
+\begin{tabular}{ll}\r
+Invoice date & Invoice number\\\r
+\textbf{$date} & \textbf{$invnum}\\\r
+\end{tabular}}} }\r
+%\r
+%\r
+\makebox{\r
+\begin{minipage}[t]{2.9in}\r
+\vspace{0.20in}\r
+\textbf{$payname}\\\r
+\addressline{$company}\r
+\addressline{$address1}\r
+\addressline{$address2}\r
+\addressline{$city, $state  $zip}\r
+\addressline{$country}\r
+\end{minipage}}\r
+\hfill\r
+\makebox{\r
+\begin{minipage}[t]{2.5in}\r
+\begin{flushright}\r
+Terms: $terms\\\r
+$po_line\\\r
+\end{flushright}\r
+\end{minipage}}\r
+\vspace{0.5cm}\r
+%\r
+\section*{\textsc{Charges}}\r
+\begin{longtable}{|c|l|c|r|r|}\r
+\hline\r
+\rule{0pt}{2.5ex}\r
+\makebox[1.4cm]{\textbf{Ref}} & \r
+\makebox[7.9cm][l]{\textbf{Description}} & \r
+\makebox[1.3cm][c]{\textbf{Quantity}} & \r
+\makebox[2.5cm][r]{\textbf{Unit Price}} & \r
+\makebox[2.5cm][r]{\textbf{Amount}} \\\r
+\hline\r
+\endfirsthead\r
+\multicolumn{5}{r}{\rule{0pt}{2.5ex}Continued from previous page}\\\r
+\hline\r
+\rule{0pt}{2.5ex}\r
+\makebox[1.4cm]{\textbf{Ref}} & \r
+\makebox[7.9cm][l]{\textbf{Description}} & \r
+\makebox[1.3cm][c]{\textbf{Quantity}} & \r
+\makebox[2.5cm][r]{\textbf{Unit Price}} & \r
+\makebox[2.5cm][r]{\textbf{Amount}} \\\r
+\hline\r
+\endhead\r
+\multicolumn{5}{r}{\rule{0pt}{2.5ex}/cont...}\\\r
+\endfoot\r
+%%TotalDetails\r
+ & \multicolumn{3}{l}{$total_item}    & $total_amount\\\r
+%%EndTotalDetails\r
+\hline\r
+\endlastfoot\r
+%%Detail\r
+\rule{0pt}{2.5ex}$ref & \r
+\begin{tabular}{l}\r
+$description\tabularnewline\r
+\end{tabular}\r
+& $quantity & \dollar $amount & \dollar $amount\\\hline\r
+%%EndDetail\r
+\end{longtable}\r
+\vfill\r
+$notes\r
+\end{document}\r
diff --git a/conf/invoice_latexfooter b/conf/invoice_latexfooter
new file mode 100644 (file)
index 0000000..110b1e6
--- /dev/null
@@ -0,0 +1,5 @@
+Ivan Kohler\\
+P.O. Box 1272\\
+Carnelian Bay, CA~~96140\\
+ivan@sisd.com~~~~+1 415 462 1624\\
+Freeside - open-source billing - http://www.sisd.com/freeside\\
diff --git a/conf/invoice_latexnotes b/conf/invoice_latexnotes
new file mode 100644 (file)
index 0000000..29e1731
--- /dev/null
@@ -0,0 +1,9 @@
+%%
+%%     Add any customer specific notes in here
+%%
+\section*{\textsc{Notes}}
+\begin{enumerate}
+\item PLEASE NOTE NEW ADDRESS BELOW!
+\item Please make your check payable to \textbf{Ivan Kohler}.
+\item If you have any questions please email or telephone.
+\end{enumerate}
diff --git a/conf/invoice_latexsmallfooter b/conf/invoice_latexsmallfooter
new file mode 100644 (file)
index 0000000..527c356
--- /dev/null
@@ -0,0 +1 @@
+Ivan Kohler~~~Freeside - open-source billing
index de2820d..d7873b2 100644 (file)
@@ -8,8 +8,8 @@ Standards-Version: 3.5.2
 Package: freeside
 Architecture: any
 Depends: freeside-lib
-Recommends: freeside-doc, freeside-ui-web, libterm-query-perl
-Suggests: freeside-passwd-server, freeside-signup-server, freeside-session-server, freeside-selfservice-server
+Recommends: freeside-doc, freeside-ui-web
+Suggests: freeside-selfservice-server
 Description: Billing and administration package for ISPs.
  Freeside is a billing and account administration package for ISPs.  It stores
  customer information in an SQL database, and will update UNIX passwd and
@@ -30,63 +30,30 @@ Description: Freeside libraries and extension API
  This package contains the libraries which implement the business logic and
  backend functions of Freeside, a billing and account administration package
  for ISPs.  This package also contains the manual pages for the library API.
+ (? like a libmodule-perl package)
 
 Package: freeside-ui-web
 Architecture: all
-Depends: libstring-approx-perl, freeside-lib, libapache-mod-perl|apache-perl
+Depends: libhtml-mason-perl, libstring-approx-perl, freeside-lib, libapache-mod-perl|apache-perl
 Suggests: libapache-mod-ssl|apache-ssl
 Description: Easy-to-use web interface for Freeside
  This package contains the web interface for Freeside, a billing and account
  administration package for ISPs.  This is what sales or support folks will
  typically use to add new accounts, edit exiting accounts and so on.
 
-Package: freeside-passwd-server
-Architecture: all
-Depends: freeside-lib
-Description: Freeside password server 
- This component of Freeside, a billing and account administration package for
- ISPs, 
-
-Package: freeside-passwd-client
-Architecture: all
-Depends: 
-Description: 
- <rar>
-
-Package: freeside-signup-server
-Architecture: all
-Depends: freeside-lib
-Description:
- <rar>
-
-Package: freeside-signup-client
-Architecture: all
-Depends: 
-Description:
- <rar>
-
-Package: freeside-signup-client-webui
-Architecture: all
-Depends: freeside-signup-client-lib, httpd
-Description: 
- <rar>
-
-Package: freeside-session-server
+Package: freeside-selfservice-server
 Architecture: all
-Depends: freeside-lib
+Depends: freeside-lib, libnet-ssh-perl, ssh
 Description:
- <rar>
+ This package contains the server side of the customer self-service interface.
+ It is installed on a private backend machine, and opens an outgoing ssh
+ connection to one or more public web server(s).
 
-Package: freeside-session-client
+Package: freeside-selfservice-client
 Architecture: all
-Depends: ssh
-Description: 
- <rar>
-
-Package: freeside-selfservice-server
-Architecture: all
-Depends:
+Depends: libstorable-perl, libhttp-browserdetect-perl, libbusiness-creditcard-perl, ssh
 Description:
- <rar>
-
+ This package contains the client side of the customer self-service interface.
+ It is typically installed on a public webserver and interfaces with
+ freeside-selfservice-server installed on a private backend machine.
 
index ebf7299..7f7ef4b 100644 (file)
@@ -6,7 +6,7 @@ use vars qw(@ISA);
 use FS::svc_Common;
 use FS::cust_svc;
 
-@ISA = qw(svc_Common);
+@ISA = qw(FS::svc_Common);
 
 =head1 NAME
 
@@ -141,7 +141,7 @@ sub check {
   my $part_svc = $x;
 
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =back
index da0a0aa..e5cbd1a 100644 (file)
@@ -8,7 +8,9 @@ WriteMakefile(
     'INSTALLSCRIPT'     => '/usr/local/sbin',
     'INSTALLSITEBIN'    => '/usr/local/sbin',
     'PERM_RWX'          => '750',
-    'PREREQ_PM'                => {}, # e.g., Module::Name => 1.1
+    'PREREQ_PM'                => {
+                             'Storable' => 0,
+                           }, # e.g., Module::Name => 1.1
     ($] >= 5.005 ?    ## Add these new keywords supported since 5.005
       (ABSTRACT_FROM => 'SelfService.pm', # retrieve abstract from module
        AUTHOR     => 'Ivan Kohler <ivan-freeside-selfservice@420.am>') : ()),
index 5849b28..715f935 100644 (file)
@@ -1,7 +1,7 @@
 package FS::SelfService;
 
 use strict;
-use vars qw($VERSION @ISA @EXPORT_OK $socket %autoload );
+use vars qw($VERSION @ISA @EXPORT_OK $socket %autoload $tag);
 use Exporter;
 use Socket;
 use FileHandle;
@@ -14,17 +14,25 @@ $VERSION = '0.03';
 @ISA = qw( Exporter );
 
 $socket =  "/usr/local/freeside/selfservice_socket";
+$socket .= '.'.$tag if defined $tag && length($tag);
 
+#maybe should ask ClientAPI for this list
 %autoload = (
   'passwd'          => 'passwd/passwd',
   'chfn'            => 'passwd/passwd',
   'chsh'            => 'passwd/passwd',
   'login'           => 'MyAccount/login',
   'customer_info'   => 'MyAccount/customer_info',
+  'edit_info'       => 'MyAccount/edit_info',
   'invoice'         => 'MyAccount/invoice',
   'cancel'          => 'MyAccount/cancel',
   'payment_info'    => 'MyAccount/payment_info',
   'process_payment' => 'MyAccount/process_payment',
+  'list_pkgs'       => 'MyAccount/list_pkgs',
+  'order_pkg'       => 'MyAccount/order_pkg',
+  'cancel_pkg'      => 'MyAccount/cancel_pkg',
+  'signup_info'     => 'Signup/signup_info',
+  'new_customer'    => 'Signup/new_customer',
 );
 @EXPORT_OK = keys %autoload;
 
@@ -38,29 +46,6 @@ $ENV{'BASH_ENV'} = '';
 my $freeside_uid = scalar(getpwnam('freeside'));
 die "not running as the freeside user\n" if $> != $freeside_uid;
 
-=head1 NAME
-
-FS::SelfService - Freeside self-service API
-
-=head1 SYNOPSIS
-
-=head1 DESCRIPTION
-
-Use this API to implement your own client "self-service" module.
-
-If you just want to customize the look of the existing "self-service" module,
-see XXXX instead.
-
-=head1 FUNCTIONS
-
-=over 4
-
-=item passwd
-
-Returns the empty value on success, or an error message on errors.
-
-=cut
-
 foreach my $autoload ( keys %autoload ) {
 
   my $eval =
@@ -101,6 +86,554 @@ sub simple_packet {
   $return;
 }
 
+=head1 NAME
+
+FS::SelfService - Freeside self-service API
+
+=head1 SYNOPSIS
+
+  # password and shell account changes
+  use FS::SelfService qw(passwd chfn chsh);
+
+  # "my account" functionality
+  use FS::SelfService qw( login customer_info invoice cancel payment_info process_payment );
+
+  my $rv = login( { 'username' => $username,
+                    'domain'   => $domain,
+                    'password' => $password,
+                  }
+                );
+
+  if ( $rv->{'error'} ) {
+    #handle login error...
+  } else {
+    #successful login
+    my $session_id = $rv->{'session_id'};
+  }
+
+  my $customer_info = customer_info( { 'session_id' => $session_id } );
+
+  #payment_info and process_payment are available in 1.5+ only
+  my $payment_info = payment_info( { 'session_id' => $session_id } );
+
+  #!!! process_payment example
+
+  #!!! list_pkgs example
+
+  #!!! order_pkg example
+
+  #!!! cancel_pkg example
+
+  # signup functionality
+  use FS::SelfService qw( signup_info new_customer );
+
+  my $signup_info = signup_info;
+
+  $rv = new_customer( {
+                        'first'            => $first,
+                        'last'             => $last,
+                        'company'          => $company,
+                        'address1'         => $address1,
+                        'address2'         => $address2,
+                        'city'             => $city,
+                        'state'            => $state,
+                        'zip'              => $zip,
+                        'country'          => $country,
+                        'daytime'          => $daytime,
+                        'night'            => $night,
+                        'fax'              => $fax,
+                        'payby'            => $payby,
+                        'payinfo'          => $payinfo,
+                        'paycvv'           => $paycvv,
+                        'paydate'          => $paydate,
+                        'payname'          => $payname,
+                        'invoicing_list'   => $invoicing_list,
+                        'referral_custnum' => $referral_custnum,
+                        'pkgpart'          => $pkgpart,
+                        'username'         => $username,
+                        '_password'        => $password,
+                        'popnum'           => $popnum,
+                        'agentnum'         => $agentnum,
+                      }
+                    );
+  
+  my $error = $rv->{'error'};
+  if ( $error eq '_decline' ) {
+    print_decline();
+  } elsif ( $error ) {
+    reprint_signup();
+  } else {
+    print_success();
+  }
+
+=head1 DESCRIPTION
+
+Use this API to implement your own client "self-service" module.
+
+If you just want to customize the look of the existing "self-service" module,
+see XXXX instead.
+
+=head1 PASSWORD, GECOS, SHELL CHANGING FUNCTIONS
+
+=over 4
+
+=item passwd
+
+=item chfn
+
+=item chsh
+
+=back
+
+=head1 "MY ACCOUNT" FUNCTIONS
+
+=over 4
+
+=item login HASHREF
+
+Creates a user session.  Takes a hash reference as parameter with the
+following keys:
+
+=over 4
+
+=item username
+
+=item domain
+
+=item password
+
+=back
+
+Returns a hash reference with the following keys:
+
+=over 4
+
+=item error
+
+Empty on success, or an error message on errors.
+
+=item session_id
+
+Session identifier for successful logins
+
+=back
+
+=item customer_info HASHREF
+
+Returns general customer information.
+
+Takes a hash reference as parameter with a single key: B<session_id>
+
+Returns a hash reference with the following keys:
+
+=over 4
+
+=item name
+
+Customer name
+
+=item balance
+
+Balance owed
+
+=item open
+
+Array reference of hash references of open inoices.  Each hash reference has
+the following keys: invnum, date, owed
+
+=item small_custview
+
+An HTML fragment containing shipping and billing addresses.
+
+=item The following fields are also returned: first last company address1 address2 city county state zip country daytime night fax ship_first ship_last ship_company ship_address1 ship_address2 ship_city ship_state ship_zip ship_country ship_daytime ship_night ship_fax
+
+=back
+
+=item edit_info HASHREF
+
+Takes a hash reference as parameter with any of the following keys:
+
+first last company address1 address2 city county state zip country daytime night fax ship_first ship_last ship_company ship_address1 ship_address2 ship_city ship_state ship_zip ship_country ship_daytime ship_night ship_fax
+
+If a field exists, the customer record is updated with the new value of that
+field.  If a field does not exist, that field is not changed on the customer
+record.
+
+Returns a hash reference with a single key, B<error>, empty on success, or an
+error message on errors
+
+=item invoice HASHREF
+
+Returns an invoice.  Takes a hash reference as parameter with two keys:
+session_id and invnum
+
+Returns a hash reference with the following keys:
+
+=over 4
+
+=item error
+
+Empty on success, or an error message on errors
+
+=item invnum
+
+Invoice number
+
+=item invoice_text
+
+Invoice text
+
+=back
+
+=item cancel HASHREF
+
+Cancels this customer.
+
+Takes a hash reference as parameter with a single key: B<session_id>
+
+Returns a hash reference with a single key, B<error>, which is empty on
+success or an error message on errors.
+
+=item payment_info HASHREF
+
+Returns information that may be useful in displaying a payment page.
+
+Takes a hash reference as parameter with a single key: B<session_id>.
+
+Returns a hash reference with the following keys:
+
+=over 4
+
+=item error
+
+Empty on success, or an error message on errors
+
+=item balance
+
+Balance owed
+
+=item payname
+
+Exact name on credit card (CARD/DCRD)
+
+=item address1
+
+=item address2
+
+=item city
+
+=item state
+
+=item zip
+
+=item payby
+
+Customer's current default payment type.
+
+=item card_type
+
+For CARD/DCRD payment types, the card type (Visa card, MasterCard, Discover card, American Express card, etc.)
+
+=item payinfo
+
+For CARD/DCRD payment types, the card number
+
+=item month
+
+For CARD/DCRD payment types, expiration month
+
+=item year
+
+For CARD/DCRD payment types, expiration year
+
+=item cust_main_county
+
+County/state/country data - array reference of hash references, each of which has the fields of a cust_main_county record (see L<FS::cust_main_county>).  Note these are not FS::cust_main_county objects, but hash references of columns and values.
+
+=item states
+
+Array reference of all states in the current default country.
+
+=item card_types
+
+Hash reference of card types; keys are card types, values are the exact strings
+passed to the process_payment function
+
+=item paybatch
+
+Unique transaction identifier (prevents multiple charges), passed to the
+process_payment function
+
+=back
+
+=item process_payment HASHREF
+
+Processes a payment and possible change of address or payment type.  Takes a
+hash reference as parameter with the following keys:
+
+=over 4
+
+=item session_id
+
+=item save
+
+If true, address and card information entered will be saved for subsequent
+transactions.
+
+=item auto
+
+If true, future credit card payments will be done automatically (sets payby to
+CARD).  If false, future credit card payments will be done on-demand (sets
+payby to DCRD).  This option only has meaning if B<save> is set true.  
+
+=item payname
+
+=item address1
+
+=item address2
+
+=item city
+
+=item state
+
+=item zip
+
+=item payinfo
+
+Card number
+
+=item month
+
+Card expiration month
+
+=item year
+
+Card expiration year
+
+=item paybatch
+
+Unique transaction identifier, returned from the payment_info function.
+Prevents multiple charges.
+
+=back
+
+Returns a hash reference with a single key, B<error>, empty on success, or an
+error message on errors
+
+=item list_pkgs
+
+Returns package information for this customer.
+
+Takes a hash reference as parameter with a single key: B<session_id>
+
+Returns a hash reference containing customer package information.  The hash reference contains the following keys:
+
+=over 4
+
+=item cust_pkg HASHREF
+
+Array reference of hash references, each of which has the fields of a cust_pkg record (see L<FS::cust_pkg>).  Note these are not FS::cust_pkg objects, but hash references of columns and values.
+
+=back
+
+=item order_pkg
+
+Orders a package for this customer.
+
+Takes a hash reference as parameter with the following keys:
+
+=over 4
+
+=item session_id
+
+=item pkgpart
+
+=item svcpart
+
+optional svcpart, required only if the package definition does not contain
+one svc_acct service definition with quantity 1 (it may contain others with
+quantity >1)
+
+=item username
+
+=item _password
+
+=item sec_phrase
+
+=item popnum
+
+=back
+
+Returns a hash reference with a single key, B<error>, empty on success, or an
+error message on errors.  The special error '_decline' is returned for
+declined transactions.
+
+=item cancel_pkg
+
+Cancels a package for this customer.
+
+Takes a hash reference as parameter with the following keys:
+
+=over 4
+
+=item session_id
+
+=item pkgpart
+
+=back
+
+Returns a hash reference with a single key, B<error>, empty on success, or an
+error message on errors.
+
+=back
+
+=head1 SIGNUP FUNCTIONS
+
+=over 4
+
+=item signup_info
+
+Returns a hash reference containing information that may be useful in
+displaying a signup page.  The hash reference contains the following keys:
+
+=over 4
+
+=item cust_main_county
+
+County/state/country data - array reference of hash references, each of which has the fields of a cust_main_county record (see L<FS::cust_main_county>).  Note these are not FS::cust_main_county objects, but hash references of columns and values.
+
+=item part_pkg
+
+Available packages - array reference of hash references, each of which has the fields of a part_pkg record (see L<FS::part_pkg>).  Each hash reference also has an additional 'payby' field containing an array reference of acceptable payment types specific to this package (see below and L<FS::part_pkg/payby>).  Note these are not FS::part_pkg objects, but hash references of columns and values.  Requires the 'signup_server-default_agentnum' configuration value to be set.
+
+=item agent
+
+Array reference of hash references, each of which has the fields of an agent record (see L<FS::agent>).  Note these are not FS::agent objects, but hash references of columns and values.
+
+=item agentnum2part_pkg
+
+Hash reference; keys are agentnums, values are array references of available packages for that agent, in the same format as the part_pkg arrayref above.
+
+=item svc_acct_pop
+
+Access numbers - array reference of hash references, each of which has the fields of an svc_acct_pop record (see L<FS::svc_acct_pop>).  Note these are not FS::svc_acct_pop objects, but hash references of columns and values.
+
+=item security_phrase
+
+True if the "security_phrase" feature is enabled
+
+=item payby
+
+Array reference of acceptable payment types for signup
+
+=over 4
+
+=item CARD (credit card - automatic)
+
+=item DCRD (credit card - on-demand - version 1.5+ only)
+
+=item CHEK (electronic check - automatic)
+
+=item DCHK (electronic check - on-demand - version 1.5+ only)
+
+=item LECB (Phone bill billing)
+
+=item BILL (billing, not recommended for signups)
+
+=item COMP (free, definately not recommended for signups)
+
+=item PREPAY (special billing type: applies a credit (see FS::prepay_credit) and sets billing type to BILL)
+
+=back
+
+=item cvv_enabled
+
+True if CVV features are available (1.5+ or 1.4.2 with CVV schema patch)
+
+=item msgcat
+
+Hash reference of message catalog values, to support error message customization.  Currently available keys are: passwords_dont_match, invalid_card, unknown_card_type, and not_a (as in "Not a Discover card").  Values are configured in the web interface under "View/Edit message catalog".
+
+=item statedefault
+
+Default state
+
+=item countrydefault
+
+Default country
+
+=back
+
+=item new_customer HASHREF
+
+Creates a new customer.  Takes a hash reference as parameter with the
+following keys:
+
+=over 4
+
+=item first - first name (required)
+
+=item last - last name (required)
+
+=item ss (not typically collected; mostly used for ACH transactions)
+
+=item company
+
+=item address1 (required)
+
+=item address2
+
+=item city (required)
+
+=item county
+
+=item state (required)
+
+=item zip (required)
+
+=item daytime - phone
+
+=item night - phone
+
+=item fax - phone
+
+=item payby - CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY (see L</signup_info> (required)
+
+=item payinfo - Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
+
+=item paycvv - Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
+
+=item paydate - Expiration date for CARD/DCRD
+
+=item payname - Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
+
+=item invoicing_list - comma-separated list of email addresses for email invoices.  The special value 'POST' is used to designate postal invoicing (it may be specified alone or in addition to email addresses),
+
+=item referral_custnum - referring customer number
+
+=item pkgpart - pkgpart of initial package
+
+=item username
+
+=item _password
+
+=item sec_phrase - security phrase
+
+=item popnum - access number (index, not the literal number)
+
+=item agentnum - agent number
+
+=back
+
+Returns a hash reference with the following keys:
+
+=over 4
+
+=item error Empty on success, or an error message on errors.  The special error '_decline' is returned for declined transactions; other error messages should be suitable for display to the user (and are customizable in under Sysadmin | View/Edit message catalog)
+
+=back
+
+
 =back
 
 =head1 BUGS
index ca6251e..5607de7 100644 (file)
@@ -7,23 +7,20 @@
 <TR>
   <TH ALIGN="right">Username </TH>
   <TD>
-    <!-- <INPUT TYPE="text" NAME="username" VALUE="<%= $username %>"> -->
-    <INPUT TYPE="text" NAME="username" VALUE="hslink">
+    <INPUT TYPE="text" NAME="username" VALUE="<%= $username %>">
   </TD>
 </TR>
 <TR>
   <TH ALIGN="right">Domain </TH>
   <TD>
-    <!-- <INPUT TYPE="text" NAME="domain" VALUE="<%= $domain %>"> -->
-    <INPUT TYPE="text" NAME="domain" VALUE="pobox.com">
+    <INPUT TYPE="text" NAME="domain" VALUE="<%= $domain %>">
   </TD>
 </TR>
 <!--<INPUT TYPE="hidden" NAME="domain" VALUE="myisp.com">-->
 <TR>
   <TH ALIGN="right">Password </TH>
   <TD>
-    <!-- <INPUT TYPE="password" NAME="password"> -->
-    <INPUT TYPE="password" NAME="password" VALUE="UwjM5zdb">
+    <INPUT TYPE="password" NAME="password">
   </TD>
 </TR>
 </TABLE>
index f13dd42..925bce6 100644 (file)
@@ -5,7 +5,7 @@
 # This is run REMOTELY over ssh by freeside-selfservice-server
 
 use strict;
-use subs qw(spawn logmsg);
+use subs qw(spawn logmsg lock_write unlock_write);
 use Fcntl qw(:flock);
 use POSIX qw(:sys_wait_h);
 use Socket;
@@ -16,14 +16,18 @@ use IO::File;
 
 #STDOUT->setbuf('');
 
+my $tag = scalar(@ARGV) ? '.'.shift : '';
+
 use vars qw( $Debug );
-$Debug = 3; #2 will turn on child logging, 3 will log packet contents,
+$Debug = 2; #2 will turn on child logging, 3 will log packet contents,
             #including potentially compromising information
 
-my $socket = "/usr/local/freeside/selfservice_socket";
+my $socket = "/usr/local/freeside/selfservice_socket$tag";
 my $pid_file = "$socket.pid";
 
-my $log_file = "/usr/local/freeside/selfservice.log";
+my $log_file = "/usr/local/freeside/selfservice$tag.log";
+
+my $lock_file = "/usr/local/freeside/selfservice$tag.writelock";
 
 #my $me = '[client]';
 
@@ -35,6 +39,9 @@ $SIG{__WARN__} = \&_logmsg;
 #warn "$me Reading init data\n" if $Debug;
 #my $signup_init = 
 
+warn "Creating $lock_file\n" if $Debug;
+open(LOCKFILE,">$lock_file") or die "can't open $lock_file: $!";
+
 warn "Creating $socket\n" if $Debug;
 my $uaddr = sockaddr_un($socket);
 my $proto = getprotobyname('tcp');
@@ -138,14 +145,22 @@ while (1) {
         #handle some commands weirdly?
         $packet->{_token}=$$;
 
-        warn "[child-$$] sending packet to remote server" if $Debug > 1;
-        flock(STDOUT, LOCK_EX) or die "FATAL: can't lock write stream: $!";
+        warn "[child-$$] locking write stream\n" if $Debug > 1;
+        lock_write;
+
+        warn "[child-$$] sending packet to remote server\n" if $Debug > 1;
         nstore_fd($packet, \*STDOUT) or die "FATAL: can't send response: $!";
+        
+        warn "[child-$$] flushing write stream\n" if $Debug > 1;
         STDOUT->flush or die "FATAL: can't flush: $!";
-        flock(STDOUT, LOCK_UN) or die "FATAL: can't release write lock: $!";
+        
+        warn "[child-$$] releasing write lock\n" if $Debug > 1;
+        unlock_write;
+
+        warn "[child-$$] closing write stream\n" if $Debug > 1;
         close STDOUT or die "FATAL: can't close write stream: $!"; #??!
 
-        warn "[child-$$] waiting for response from parent" if $Debug > 1;
+        warn "[child-$$] waiting for response from parent\n" if $Debug > 1;
         my $w = new IO::Select;
         $w->add(\*STDIN);
         until ( $w->can_read ) {
@@ -224,3 +239,17 @@ sub _logmsg {
   flock($log, LOCK_UN);
   close $log;
 }
+
+sub lock_write {
+  #broken on freebsd?
+  #flock(STDOUT, LOCK_EX) or die "FATAL: can't lock write stream: $!";
+
+  flock(LOCKFILE, LOCK_EX) or die "FATAL: can't lock $lock_file: $!";
+}
+
+sub unlock_write {
+  #broken on freebsd?
+  #flock(STDOUT, LOCK_UN) or die "FATAL: can't release write lock: $!";
+
+  flock(LOCKFILE, LOCK_UN) or die "FATAL: can't unlock $lock_file: $!";
+}
index e740519..310200b 100644 (file)
@@ -11,8 +11,7 @@ WriteMakefile(
     'PREREQ_PM'     => {
                          'Business::CreditCard' => 0,
                          'HTTP::BrowserDetect' => 0,
-                         'HTTP::Headers::UserAgent' => 3,
-                         'Storable' => 0,
                          'Text::Template' => 0,
+                         'FS::SelfService' => 0,
                        },
 );
index 842064d..fb2b12f 100644 (file)
@@ -1,30 +1,19 @@
 package FS::SignupClient;
 
 use strict;
-use vars qw($VERSION @ISA @EXPORT_OK $fs_signupd_socket);
+use vars qw($VERSION @ISA @EXPORT_OK); # $fs_signupd_socket);
 use Exporter;
-use Socket;
-use FileHandle;
-use IO::Handle;
-use Storable qw(nstore_fd fd_retrieve);
+#use Socket;
+#use FileHandle;
+#use IO::Handle;
+#use Storable qw(nstore_fd fd_retrieve);
+use FS::SelfService; # qw( new_customer signup_info );
 
-$VERSION = '0.03';
+$VERSION = '0.04';
 
 @ISA = qw( Exporter );
 @EXPORT_OK = qw( signup_info new_customer );
 
-$fs_signupd_socket = "/usr/local/freeside/fs_signupd_socket";
-
-$ENV{'PATH'} ='/usr/bin:/usr/ucb:/bin';
-$ENV{'SHELL'} = '/bin/sh';
-$ENV{'IFS'} = " \t\n";
-$ENV{'CDPATH'} = '';
-$ENV{'ENV'} = '';
-$ENV{'BASH_ENV'} = '';
-
-my $freeside_uid = scalar(getpwnam('freeside'));
-die "not running as the freeside user\n" if $> != $freeside_uid;
-
 =head1 NAME
 
 FS::SignupClient - Freeside signup client API
@@ -33,8 +22,10 @@ FS::SignupClient - Freeside signup client API
 
   use FS::SignupClient qw( signup_info new_customer );
 
-  ( $locales, $packages, $pops ) = signup_info;
+  #this is the backwards-compatibility bit
+  ( $locales, $packages, $pops, $real_signup_info ) = signup_info;
 
+  #this is compatible with FS::SelfService::new_customer
   $error = new_customer ( {
     'first'            => $first,
     'last'             => $last,
@@ -52,6 +43,7 @@ FS::SignupClient - Freeside signup client API
     'fax'              => $fax,
     'payby'            => $payby,
     'payinfo'          => $payinfo,
+    'paycvv'           => $paycvv,
     'paydate'          => $paydate,
     'payname'          => $payname,
     'invoicing_list'   => $invoicing_list,
@@ -104,14 +96,10 @@ Each hash reference has the following keys:
 
 =cut
 
+#compatibility bit
 sub signup_info {
-  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
-  connect(SOCK, sockaddr_un($fs_signupd_socket)) or die "connect: $!";
-  print SOCK "signup_info\n";
-  SOCK->flush;
 
-  my $init_data = fd_retrieve(\*SOCK);
-  close SOCK;
+  my $init_data = FS::SelfService::signup_info();
 
   (map { $init_data->{$_} } qw( cust_main_county part_pkg svc_acct_pop ) ),
   $init_data;
@@ -138,6 +126,7 @@ a paramater with the following keys:
   fax
   payby
   payinfo
+  paycvv
   paydate
   payname
   invoicing_list
@@ -153,26 +142,10 @@ Returns a scalar error message, or the empty string for success.
 
 =cut
 
-sub new_customer {
-  my $hashref = shift;
-
-  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
-  connect(SOCK, sockaddr_un($fs_signupd_socket)) or die "connect: $!";
-  print SOCK "new_customer\n";
-
-  my $signup_data = { map { $_ => $hashref->{$_} } qw(
-    first last ss company address1 address2 city county state zip country
-    daytime night fax payby payinfo paydate payname invoicing_list
-    referral_custnum comments pkgpart username _password sec_phrase popnum
-  ) };
-
-  $signup_data->{agentnum} = $hashref->{agentnum} if $hashref->{agentnum};
-
-  nstore_fd($signup_data, \*SOCK) or die "can't send customer signup: $!";
-  SOCK->flush;
-
-  chop( my $error = <SOCK> );
-  $error;
+#compatibility bit
+sub new_customer { 
+  my $hash = FS::SelfService::new_customer(@_);
+  $hash->{'error'};
 }
 
 =back
diff --git a/fs_signup/FS-SignupClient/cgi/cvv2.html b/fs_signup/FS-SignupClient/cgi/cvv2.html
new file mode 100644 (file)
index 0000000..b178c85
--- /dev/null
@@ -0,0 +1,25 @@
+<HTML>
+  <HEAD>
+    <TITLE>
+      CVV2 information
+    </TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+  The CVV2 number (also called CVC2 or CID) is a three- or four-digit
+  security code used to reduce credit card fraud.<BR><BR>
+  <TABLE BORDER=0 CELLSPACING=4>
+    <TR>
+      <TH>Visa / MasterCard / Discover</TH>
+      <TH>American Express</TH>
+    </TR>
+    <TR>
+      <TD>
+        <IMG BORDER=0 ALT="Visa/MasterCard/Discover" SRC="cvv2.png">
+      </TD>
+      <TD>
+        <IMG BORDER=0 ALT="American Express" SRC="cvv2_amex.png">
+      </TD>
+  </TABLE>
+    <CENTER><A HREF="javascript:close()">(close window)</A></CENTER>
+  </BODY>
+</HTML>
diff --git a/fs_signup/FS-SignupClient/cgi/cvv2.png b/fs_signup/FS-SignupClient/cgi/cvv2.png
new file mode 100644 (file)
index 0000000..4610dcb
Binary files /dev/null and b/fs_signup/FS-SignupClient/cgi/cvv2.png differ
diff --git a/fs_signup/FS-SignupClient/cgi/cvv2_amex.png b/fs_signup/FS-SignupClient/cgi/cvv2_amex.png
new file mode 100644 (file)
index 0000000..21c36a0
Binary files /dev/null and b/fs_signup/FS-SignupClient/cgi/cvv2_amex.png differ
diff --git a/fs_signup/FS-SignupClient/cgi/signup-agentselect.html b/fs_signup/FS-SignupClient/cgi/signup-agentselect.html
new file mode 100755 (executable)
index 0000000..2451361
--- /dev/null
@@ -0,0 +1,195 @@
+<HTML><HEAD><TITLE>ISP Signup form</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>ISP Signup form</FONT><BR><BR>
+<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+<FORM ACTION="<%= $self_url %>" METHOD=POST>
+<INPUT TYPE="hidden" NAME="magic" VALUE="process">
+<INPUT TYPE="hidden" NAME="ref" VALUE="<%= $referral_custnum %>">
+<INPUT TYPE="hidden" NAME="ss" VALUE="">
+Agent <SELECT NAME="agentnum">
+<%=
+  warn $init_data;
+  warn $init_data->{'agent'};
+  foreach my $agent ( @{$init_data->{'agent'}} ) {
+    $OUT .= '<OPTION VALUE="'. $agent->{'agentnum'}. '"';
+    $OUT .= ' SELECTED' if $agent->{'agentnum'} eq $agentnum;
+    $OUT .= '>'. $agent->{'agent'};
+  }
+%>
+</SELECT><BR><BR>
+Contact Information
+<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Contact name<BR>(last, first)</TH>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="last" VALUE="<%= $last %>">,
+                <INPUT TYPE="text" NAME="first" VALUE="<%= $first %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Company</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="company" SIZE=70 VALUE="<%= $company %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Address</TH>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="address1" SIZE=70 VALUE="<%= $address1 %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">&nbsp;</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="address2" SIZE=70 VALUE="<%= $address2 %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>City</TH>
+  <TD><INPUT TYPE="text" NAME="city" VALUE="<%= $city %>"></TD>
+  <TH ALIGN="right"><font color="#ff0000">*</font>State/Country</TH>
+  <TD>
+    <%=
+        ($county_html, $state_html, $country_html) =
+          regionselector( $county, $state, $country );
+        "$county_html $state_html";
+    %>
+  </TD>
+  <TH><font color="#ff0000">*</font>Zip</TH>
+  <TD><INPUT TYPE="text" NAME="zip" SIZE=10 VALUE="<%= $zip %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Country</TH>
+  <TD><%= $country_html %></TD>
+<TR>
+  <TD ALIGN="right">Day Phone</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="daytime" VALUE="<%= $daytime %>" SIZE=18></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Night Phone</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="night" VALUE="<%= $night %>" SIZE=18></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Fax</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="fax" VALUE="<%= $fax %>" SIZE=12></TD>
+</TR>
+</TABLE><font color="#ff0000">*</font> required fields<BR>
+<BR>Billing information<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<TR><TD>
+
+  <%=
+    $OUT .= '<INPUT TYPE="checkbox" NAME="invoicing_list_POST" VALUE="POST"';
+    my @invoicing_list = split(', ', $invoicing_list );
+    $OUT .= ' CHECKED'
+      if ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list;
+    $OUT .= '>';
+  %>
+
+  Postal mail invoice
+</TD></TR>
+<TR><TD>Email invoice <INPUT TYPE="text" NAME="invoicing_list" VALUE="<%= join(', ', grep { $_ ne 'POST' } split(', ', $invoicing_list ) ) %>">
+</TD></TR>
+<%= scalar(@payby) > 1 ? '<TR><TD>Billing type</TD></TR>' : '' %>
+</TABLE>
+<TABLE BGCOLOR="#c0c0c0" BORDER=1 WIDTH="100%">
+<TR>
+
+  <%=
+
+    my $cardselect = '<SELECT NAME="CARD_type"><OPTION></OPTION>';
+    my %types = (
+                  'VISA' => 'VISA card',
+                  'MasterCard' => 'MasterCard',
+                  'Discover' => 'Discover card',
+                  'American Express' => 'American Express card',
+                );
+    foreach ( keys %types ) {
+      $selected = $cgi->param('CARD_type') eq $types{$_} ? 'SELECTED' : '';
+      $cardselect .= qq!<OPTION $selected VALUE="$types{$_}">$_</OPTION>!;
+    }
+    $cardselect .= '</SELECT>';
+  
+    my %payby = (
+      'CARD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="CARD_payinfo" VALUE="" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("CARD"). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="CARD_payname" VALUE="">!,
+      'DCRD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("DCRD"). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="">!,
+      'CHEK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE="" MAXLENGTH=10><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="">!,
+      'DCHK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE="" MAXLENGTH=10><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="">!,
+      'LECB' => qq!Phone bill billing<BR>${r}Phone number <INPUT TYPE="text" BANE="LECB_payinfo" VALUE="" MAXLENGTH=15 SIZE=16><INPUT TYPE="hidden" NAME="LECB_month" VALUE="12"><INPUT TYPE="hidden" NAME="LECB_year" VALUE="2037"><INPUT TYPE="hidden" NAME="LECB_payname" VALUE="">!,
+      'BILL' => qq!Billing<BR>P.O. <INPUT TYPE="text" NAME="BILL_payinfo" VALUE=""><BR><font color="#ff0000">*</font>Exp !. expselect("BILL", "12-2037"). qq!<BR><font color="#ff0000">*</font>Attention<BR><INPUT TYPE="text" NAME="BILL_payname" VALUE="Accounts Payable">!,
+      'COMP' => qq!Complimentary<BR><font color="#ff0000">*</font>Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE=""><BR><font color="#ff0000">*</font>Exp !. expselect("COMP"),
+      'PREPAY' => qq!Prepaid card<BR><font color="#ff0000">*</font><INPUT TYPE="text" NAME="PREPAY_payinfo" VALUE="" MAXLENGTH=80>!,
+    );
+
+    my( $account, $aba ) = split('@', $payinfo);
+    my %paybychecked = (
+      'CARD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("CARD", $paydate). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname">!,
+      'DCRD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("DCRD", $paydate). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="$payname">!,
+      'CHEK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE="$account" MAXLENGTH=10><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="$payname">!,
+      'DCHK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE="$account" MAXLENGTH=10><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="$payname">!,
+      'LECB' => qq!Phone bill billing<BR>${r}Phone number <INPUT TYPE="text" BANE="LECB_payinfo" VALUE="$payinfo" MAXLENGTH=15 SIZE=16><INPUT TYPE="hidden" NAME="LECB_month" VALUE="12"><INPUT TYPE="hidden" NAME="LECB_year" VALUE="2037"><INPUT TYPE="hidden" NAME="LECB_payname" VALUE="">!,
+      'BILL' => qq!Billing<BR>P.O. <INPUT TYPE="text" NAME="BILL_payinfo" VALUE="$payinfo"><BR><font color="#ff0000">*</font>Exp !. expselect("BILL", $paydate). qq!<BR><font color="#ff0000">*</font>Attention<BR><INPUT TYPE="text" NAME="BILL_payname" VALUE="$payname">!,
+      'COMP' => qq!Complimentary<BR><font color="#ff0000">*</font>Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE="$payinfo"><BR><font color="#ff0000">*</font>Exp !. expselect("COMP", $paydate),
+      'PREPAY' => qq!Prepaid card<BR><font color="#ff0000">*</font><INPUT TYPE="text" NAME="PREPAY_payinfo" VALUE="$payinfo" MAXLENGTH=80>!,
+    );
+
+    for (@payby) {
+      if ( scalar(@payby) == 1) {
+        $OUT .= '<TD VALIGN=TOP>'.
+                qq!<INPUT TYPE="hidden" NAME="payby" VALUE="$_">!.
+                "$paybychecked{$_}</TD>";
+      } else {
+        $OUT .= qq!<TD VALIGN=TOP><INPUT TYPE="radio" NAME="payby" VALUE="$_"!;
+        if ($payby eq $_) {
+          $OUT .= qq! CHECKED> $paybychecked{$_}</TD>!;
+        } else {
+          $OUT .= qq!> $payby{$_}</TD>!;
+        }
+
+      }
+    }
+  %>
+
+</TR></TABLE><font color="#ff0000">*</font> required fields for each billing type
+<BR><BR>First package
+<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<TR>
+  <TD COLSPAN=2><SELECT NAME="pkgpart"><OPTION VALUE="">(none)
+
+  <%=
+    foreach my $package ( @{$packages} ) {
+      $OUT .= '<OPTION VALUE="'. $package->{'pkgpart'}. '"';
+      $OUT .= ' SELECTED' if $pkgpart && $package->{'pkgpart'} == $pkgpart;
+      $OUT .= '>'. $package->{'pkg'};
+    }
+  %>
+
+  </SELECT></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Username</TD>
+  <TD><INPUT TYPE="text" NAME="username" VALUE="<%= $username %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Password</TD>
+  <TD><INPUT TYPE="password" NAME="_password" VALUE="<%= $password %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Re-enter Password</TD>
+  <TD><INPUT TYPE="password" NAME="_password2" VALUE="<%= $password2 %>"></TD>
+</TR>
+<%=
+  if ( $init_data->{'security_phrase'} ) {
+    $OUT .= <<ENDOUT;
+<TR>
+  <TD ALIGN="right">Security Phrase</TD>
+  <TD><INPUT TYPE="text" NAME="sec_phrase" VALUE="$sec_phrase">
+  </TD>
+</TR>
+ENDOUT
+  } else {
+    $OUT .= '<INPUT TYPE="hidden" NAME="sec_phrase" VALUE="">';
+  }
+%>
+<%=
+  if ( scalar(@$pops) ) {
+    $OUT .= '<TR><TD ALIGN="right">Access number</TD><TD>'.
+            popselector($popnum). '</TD></TR>';
+  } else {
+    $OUT .= popselector($popnum);
+  }
+%>
+</TABLE>
+<BR><BR><INPUT TYPE="submit" VALUE="Signup">
+</FORM></BODY></HTML>
diff --git a/fs_signup/FS-SignupClient/cgi/signup-snarf.html b/fs_signup/FS-SignupClient/cgi/signup-snarf.html
new file mode 100755 (executable)
index 0000000..d167efb
--- /dev/null
@@ -0,0 +1,228 @@
+<HTML><HEAD><TITLE>ISP Signup form</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8" onUnload="myclose()">
+<script language="JavaScript"><!--
+  var mywindow = -1;
+  function myopen(filename,windowname,properties) {
+    myclose();
+    mywindow = window.open(filename,windowname,properties);
+  }
+  function myclose() {
+    if ( mywindow != -1 )
+      mywindow.close();
+    mywindow = -1
+  }
+//--></script>
+<FONT SIZE=7>ISP Signup form</FONT><BR><BR>
+<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+<FORM ACTION="<%= $self_url %>" METHOD=POST>
+<INPUT TYPE="hidden" NAME="magic" VALUE="process">
+<INPUT TYPE="hidden" NAME="ref" VALUE="<%= $referral_custnum %>">
+<INPUT TYPE="hidden" NAME="ss" VALUE="">
+Contact Information
+<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Contact name<BR>(last, first)</TH>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="last" VALUE="<%= $last %>">,
+                <INPUT TYPE="text" NAME="first" VALUE="<%= $first %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Company</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="company" SIZE=70 VALUE="<%= $company %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Address</TH>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="address1" SIZE=70 VALUE="<%= $address1 %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">&nbsp;</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="address2" SIZE=70 VALUE="<%= $address2 %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>City</TH>
+  <TD><INPUT TYPE="text" NAME="city" VALUE="<%= $city %>"></TD>
+  <TH ALIGN="right"><font color="#ff0000">*</font>State/Country</TH>
+  <TD>
+    <%=
+        ($county_html, $state_html, $country_html) =
+          regionselector( $county, $state, $country );
+        "$county_html $state_html";
+    %>
+  </TD>
+  <TH><font color="#ff0000">*</font>Zip</TH>
+  <TD><INPUT TYPE="text" NAME="zip" SIZE=10 VALUE="<%= $zip %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Country</TH>
+  <TD><%= $country_html %></TD>
+<TR>
+  <TD ALIGN="right">Day Phone</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="daytime" VALUE="<%= $daytime %>" SIZE=18></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Night Phone</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="night" VALUE="<%= $night %>" SIZE=18></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Fax</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="fax" VALUE="<%= $fax %>" SIZE=12></TD>
+</TR>
+</TABLE><font color="#ff0000">*</font> required fields<BR>
+<BR>Billing information<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<TR><TD>
+
+  <%=
+    $OUT .= '<INPUT TYPE="checkbox" NAME="invoicing_list_POST" VALUE="POST"';
+    my @invoicing_list = split(', ', $invoicing_list );
+    $OUT .= ' CHECKED'
+      if ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list;
+    $OUT .= '>';
+  %>
+
+  Postal mail invoice
+</TD></TR>
+<TR><TD>Email invoice <INPUT TYPE="text" NAME="invoicing_list" VALUE="<%= join(', ', grep { $_ ne 'POST' } split(', ', $invoicing_list ) ) %>">
+</TD></TR>
+<%= scalar(@payby) > 1 ? '<TR><TD>Billing type</TD></TR>' : '' %>
+</TABLE>
+<TABLE BGCOLOR="#c0c0c0" BORDER=1 WIDTH="100%">
+<TR>
+
+  <%=
+
+    my $cardselect = '<SELECT NAME="CARD_type"><OPTION></OPTION>';
+    my %types = (
+                  'VISA' => 'VISA card',
+                  'MasterCard' => 'MasterCard',
+                  'Discover' => 'Discover card',
+                  'American Express' => 'American Express card',
+                );
+    foreach ( keys %types ) {
+      $selected = $cgi->param('CARD_type') eq $types{$_} ? 'SELECTED' : '';
+      $cardselect .= qq!<OPTION $selected VALUE="$types{$_}">$_</OPTION>!;
+    }
+    $cardselect .= '</SELECT>';
+  
+    my %payby = (
+      'CARD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="CARD_payinfo" VALUE="" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("CARD"). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="CARD_payname" VALUE="">!,
+      'DCRD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("DCRD"). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="">!,
+      'CHEK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE="" MAXLENGTH=10><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="">!,
+      'DCHK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE="" MAXLENGTH=10><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="">!,
+      'LECB' => qq!Phone bill billing<BR>${r}Phone number <INPUT TYPE="text" BANE="LECB_payinfo" VALUE="" MAXLENGTH=15 SIZE=16><INPUT TYPE="hidden" NAME="LECB_month" VALUE="12"><INPUT TYPE="hidden" NAME="LECB_year" VALUE="2037"><INPUT TYPE="hidden" NAME="LECB_payname" VALUE="">!,
+      'BILL' => qq!Billing<BR>P.O. <INPUT TYPE="text" NAME="BILL_payinfo" VALUE=""><BR><font color="#ff0000">*</font>Exp !. expselect("BILL", "12-2037"). qq!<BR><font color="#ff0000">*</font>Attention<BR><INPUT TYPE="text" NAME="BILL_payname" VALUE="Accounts Payable">!,
+      'COMP' => qq!Complimentary<BR><font color="#ff0000">*</font>Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE=""><BR><font color="#ff0000">*</font>Exp !. expselect("COMP"),
+      'PREPAY' => qq!Prepaid card<BR><font color="#ff0000">*</font><INPUT TYPE="text" NAME="PREPAY_payinfo" VALUE="" MAXLENGTH=80>!,
+    );
+
+    if ( $init_data->{'cvv_enabled'} ) {
+      foreach my $payby ( grep { exists $payby{$_} } qw(CARD DCRD) ) { #1.4/1.5
+        $payby{$payby} .= qq!<BR>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>)&nbsp;<INPUT TYPE="text" NAME=${payby}_paycvv VALUE="" SIZE=4 MAXLENGTH=4>!;
+      }
+    }
+
+    my( $account, $aba ) = split('@', $payinfo);
+    my %paybychecked = (
+      'CARD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("CARD", $paydate). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname">!,
+      'DCRD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("DCRD", $paydate). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="$payname">!,
+      'CHEK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE="$account" MAXLENGTH=10><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="$payname">!,
+      'DCHK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE="$account" MAXLENGTH=10><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="$payname">!,
+      'LECB' => qq!Phone bill billing<BR>${r}Phone number <INPUT TYPE="text" BANE="LECB_payinfo" VALUE="$payinfo" MAXLENGTH=15 SIZE=16><INPUT TYPE="hidden" NAME="LECB_month" VALUE="12"><INPUT TYPE="hidden" NAME="LECB_year" VALUE="2037"><INPUT TYPE="hidden" NAME="LECB_payname" VALUE="">!,
+      'BILL' => qq!Billing<BR>P.O. <INPUT TYPE="text" NAME="BILL_payinfo" VALUE="$payinfo"><BR><font color="#ff0000">*</font>Exp !. expselect("BILL", $paydate). qq!<BR><font color="#ff0000">*</font>Attention<BR><INPUT TYPE="text" NAME="BILL_payname" VALUE="$payname">!,
+      'COMP' => qq!Complimentary<BR><font color="#ff0000">*</font>Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE="$payinfo"><BR><font color="#ff0000">*</font>Exp !. expselect("COMP", $paydate),
+      'PREPAY' => qq!Prepaid card<BR><font color="#ff0000">*</font><INPUT TYPE="text" NAME="PREPAY_payinfo" VALUE="$payinfo" MAXLENGTH=80>!,
+    );
+
+    if ( $init_data->{'cvv_enabled'} ) {
+      foreach my $payby ( grep { exists $payby{$_} } qw(CARD DCRD) ) { #1.4/1.5
+        $paybychecked{$payby} .= qq!<BR>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>)&nbsp;<INPUT TYPE="text" NAME=${payby}_paycvv VALUE="$paycvv" SIZE=4 MAXLENGTH=4>!;
+      }
+    }
+
+    for (@payby) {
+      if ( scalar(@payby) == 1) {
+        $OUT .= '<TD VALIGN=TOP>'.
+                qq!<INPUT TYPE="hidden" NAME="payby" VALUE="$_">!.
+                "$paybychecked{$_}</TD>";
+      } else {
+        $OUT .= qq!<TD VALIGN=TOP><INPUT TYPE="radio" NAME="payby" VALUE="$_"!;
+        if ($payby eq $_) {
+          $OUT .= qq! CHECKED> $paybychecked{$_}</TD>!;
+        } else {
+          $OUT .= qq!> $payby{$_}</TD>!;
+        }
+
+      }
+    }
+  %>
+
+</TR></TABLE><font color="#ff0000">*</font> required fields for each billing type
+<BR><BR>First package
+<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<TR>
+  <TD COLSPAN=2><SELECT NAME="pkgpart"><OPTION VALUE="">(none)
+
+  <%=
+    foreach my $package ( @{$packages} ) {
+      $OUT .= '<OPTION VALUE="'. $package->{'pkgpart'}. '"';
+      $OUT .= ' SELECTED' if $pkgpart && $package->{'pkgpart'} == $pkgpart;
+      $OUT .= '>'. $package->{'pkg'};
+    }
+  %>
+
+  </SELECT></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Username</TD>
+  <TD><INPUT TYPE="text" NAME="username" VALUE="<%= $username %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Password</TD>
+  <TD><INPUT TYPE="password" NAME="_password" VALUE="<%= $password %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Re-enter Password</TD>
+  <TD><INPUT TYPE="password" NAME="_password2" VALUE="<%= $password2 %>"></TD>
+</TR>
+<%=
+  if ( $init_data->{'security_phrase'} ) {
+    $OUT .= <<ENDOUT;
+<TR>
+  <TD ALIGN="right">Security Phrase</TD>
+  <TD><INPUT TYPE="text" NAME="sec_phrase" VALUE="$sec_phrase">
+  </TD>
+</TR>
+ENDOUT
+  } else {
+    $OUT .= '<INPUT TYPE="hidden" NAME="sec_phrase" VALUE="">';
+  }
+%>
+<%=
+  if ( scalar(@$pops) ) {
+    $OUT .= '<TR><TD ALIGN="right">Access number</TD><TD>'.
+            popselector($popnum). '</TD></TR>';
+  } else {
+    $OUT .= popselector($popnum);
+  }
+%>
+</TABLE>
+<BR><BR>Enter up to ten external accounts from which to retrieve email
+<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<TR>
+  <TH ALIGN="left">Mail server</TH>
+  <TH ALIGN="left">Username</TH>
+  <TH ALIGN="left">Password</TH>
+</TR>
+<%=
+  for my $num ( 1..10 ) {
+    no strict 'vars';
+    $OUT .= qq!<TR><TD><INPUT TYPE="text" NAME="snarf_machine$num" VALUE="${"snarf_machine$num"}"></TD>!.
+            qq!<INPUT TYPE="hidden" NAME="snarf_protocol$num" VALUE="pop3">!.
+            qq!<TD><INPUT TYPE="text" NAME="snarf_username$num" VALUE="${"snarf_username$num"}"></TD>!.
+            qq!<TD><INPUT TYPE="password" NAME="snarf_password$num" VALUE="${"snarf_password$num"}"></TD>!.
+            qq!</TR>!;
+  }
+%>
+</TABLE>
+
+<BR><BR><INPUT TYPE="submit" VALUE="Signup">
+</FORM></BODY></HTML>
index 57b93d4..0a9a510 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/perl -Tw
 #
-# $Id: signup.cgi,v 1.43 2003-07-04 03:21:42 ivan Exp $
+# $Id: signup.cgi,v 1.50 2004-01-04 03:52:54 ivan Exp $
 
 use strict;
 use vars qw( @payby $cgi $locales $packages
@@ -8,10 +8,10 @@ use vars qw( @payby $cgi $locales $packages
              $init_data $error
              $last $first $ss $company $address1 $address2 $city $state $county
              $country $zip $daytime $night $fax $invoicing_list $payby $payinfo
-             $paydate $payname $referral_custnum $init_popstate
+             $paycvv $paydate $payname $referral_custnum $init_popstate
              $pkgpart $username $password $password2 $sec_phrase $popnum
-             $agentnum
-             $ieak_file $ieak_template $cck_file $cck_template
+             $agentnum $refnum
+             $ieak_file $ieak_template
              $signup_html $signup_template
              $success_html $success_template
              $decline_html $decline_template
@@ -26,7 +26,7 @@ use CGI;
 #use CGI::Carp qw(fatalsToBrowser);
 use Text::Template;
 use Business::CreditCard;
-use HTTP::Headers::UserAgent 2.00;
+use HTTP::BrowserDetect;
 use FS::SignupClient 0.03 qw( signup_info new_customer );
 
 #acceptable payment methods
@@ -37,7 +37,6 @@ use FS::SignupClient 0.03 qw( signup_info new_customer );
 @payby = qw( CARD PREPAY );
 
 $ieak_file = '/usr/local/freeside/ieak.template';
-$cck_file = '/usr/local/freeside/cck.template';
 $signup_html = -e 'signup.html'
                  ? 'signup.html'
                  : '/usr/local/freeside/signup.html';
@@ -62,17 +61,6 @@ if ( -e $ieak_file ) {
   $ieak_template = '';
 }
 
-if ( -e $cck_file ) {
-  my $cck_txt = Text::Template::_load_text($cck_file)
-    or die $Text::Template::ERROR;
-  $cck_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
-  $cck_txt = $1;
-  $cck_template = new Text::Template ( TYPE => 'STRING', SOURCE => $cck_txt )
-    or die $Text::Template::ERROR;
-} else {
-  $cck_template = '';
-}
-
 $agentnum = '';
 if ( -e $signup_html ) {
   my $signup_txt = Text::Template::_load_text($signup_html)
@@ -178,6 +166,9 @@ if ( defined $cgi->param('magic') ) {
     $paydate =
       $cgi->param( $payby. '_month' ). '-'. $cgi->param( $payby. '_year' );
     $payname = $cgi->param( $payby. '_payname' );
+    $paycvv = defined $cgi->param( $payby. '_paycvv' )
+                ? $cgi->param( $payby. '_paycvv' )
+                : '';
 
     if ( $invoicing_list = $cgi->param('invoicing_list') ) {
       $invoicing_list .= ', POST' if $cgi->param('invoicing_list_POST');
@@ -213,7 +204,9 @@ if ( defined $cgi->param('magic') ) {
     $password         = $cgi->param('_password');
     $popnum           = $cgi->param('popnum');
     #$agentnum, #         = $cgi->param('agentnum'),
+    $agentnum         ||= $cgi->param('agentnum');
     $init_popstate    = $cgi->param('init_popstate');
+    $refnum           = $cgi->param('refnum');
 
     if ( $cgi->param('_password') ne $cgi->param('_password2') ) {
       $error = $init_data->{msgcat}{passwords_dont_match}; #msgcat
@@ -251,6 +244,7 @@ if ( defined $cgi->param('magic') ) {
         'fax'              => $fax,
         'payby'            => $payby,
         'payinfo'          => $payinfo,
+        'paycvv'           => $paycvv,
         'paydate'          => $paydate,
         'payname'          => $payname,
         'invoicing_list'   => $invoicing_list,
@@ -261,6 +255,8 @@ if ( defined $cgi->param('magic') ) {
         '_password'        => $password,
         'popnum'           => $popnum,
         'agentnum'         => $agentnum,
+        'refnum'           => $refnum,
+        map { $_ => $cgi->param($_) } grep { /^snarf_/ } $cgi->param
       } );
 
     }
@@ -268,6 +264,9 @@ if ( defined $cgi->param('magic') ) {
     if ( $error eq '_decline' ) {
       print_decline();
     } elsif ( $error ) {
+      #fudge the snarf info
+      no strict 'refs';
+      ${$_} = $cgi->param($_) foreach grep { /^snarf_/ } $cgi->param;
       print_form();
     } else {
       print_okay();
@@ -305,6 +304,7 @@ if ( defined $cgi->param('magic') ) {
   $popnum = '';
   $referral_custnum = $cgi->param('ref') || '';
   $init_popstate = $cgi->param('init_popstate') || '';
+  $refnum = $init_data->{'refnum'};
   print_form;
 }
 
@@ -327,7 +327,7 @@ sub print_decline {
 }
 
 sub print_okay {
-  my $user_agent = new HTTP::Headers::UserAgent $ENV{HTTP_USER_AGENT};
+  my $user_agent = new HTTP::BrowserDetect $ENV{HTTP_USER_AGENT};
 
   $cgi->param('username') =~ /^(.+)$/
     or die "fatal: invalid username got past FS::SignupClient::new_customer";
@@ -350,27 +350,10 @@ sub print_okay {
   #global for template
   $pkg = ( grep { $_->{'pkgpart'} eq $pkgpart } @$packages )[0]->{'pkg'};
 
-  if ( $ieak_template
-       && $user_agent->platform eq 'ia32'
-       && $user_agent->os =~ /^win/
-       && ($user_agent->browser)[0] eq 'IE'
-     )
-  { #send an IEAK config
+  if ( $ieak_template && $user_agent->windows && $user_agent->ie ) {
+    #send an IEAK config
     print $cgi->header('application/x-Internet-signup'),
           $ieak_template->fill_in();
-  } elsif ( $cck_template
-            && $user_agent->platform eq 'ia32'
-            && $user_agent->os =~ /^win/
-            && ($user_agent->browser)[0] eq 'Netscape'
-          )
-  { #send a Netscape config
-    my $cck_data = $cck_template->fill_in();
-    print $cgi->header('application/x-netscape-autoconfigure-dialer-v2'),
-          map {
-            m/(.*)\s+(.*)$/;
-            pack("N", length($1)). $1. pack("N", length($2)). $2;
-          } split(/\n/, $cck_data);
-
   } else { #send a simple confirmation
     print $cgi->header( '-expires' => 'now' ),
           $success_template->fill_in();
@@ -524,7 +507,7 @@ sub regionselector {
   my ( $selected_county, $selected_state, $selected_country,
        $prefix, $onchange ) = @_;
 
-  my $prefix = '' unless defined $prefix;
+  $prefix = '' unless defined $prefix;
 
   my $countyflag = 0;
 
@@ -646,7 +629,7 @@ Signup information for <%= $email_name %>:
 <BR><BR>
 Username: <%= $username %><BR>
 Password: <%= $password %><BR>
-Access number: (<%= $ac %>) / $exch - $local<BR>
+Access number: (<%= $ac %>) / <%= $exch %> - <%= $local %><BR>
 Package: <%= $pkg %><BR>
 </BODY></HTML>
 END
index 8077409..96bdac6 100755 (executable)
@@ -1,10 +1,33 @@
 <HTML><HEAD><TITLE>ISP Signup form</TITLE></HEAD>
-<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>ISP Signup form</FONT><BR><BR>
+<BODY BGCOLOR="#e8e8e8" onUnload="myclose()">
+<script language="JavaScript"><!--
+  var mywindow = -1;
+  function myopen(filename,windowname,properties) {
+    myclose();
+    mywindow = window.open(filename,windowname,properties);
+  }
+  function myclose() {
+    if ( mywindow != -1 )
+      mywindow.close();
+    mywindow = -1
+  }
+//--></script>
+<FONT SIZE=7>ISP Signup form</FONT><BR><BR>
 <FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
 <FORM ACTION="<%= $self_url %>" METHOD=POST>
 <INPUT TYPE="hidden" NAME="magic" VALUE="process">
 <INPUT TYPE="hidden" NAME="ref" VALUE="<%= $referral_custnum %>">
 <INPUT TYPE="hidden" NAME="ss" VALUE="">
+Where did you hear about our service? <SELECT NAME="refnum">
+<%=
+  $OUT .= '<OPTION VALUE="">' unless $refnum;
+  foreach my $part_referral ( @{$init_data->{'part_referral'}} ) {
+    $OUT .= '<OPTION VALUE="'. $part_referral->{'refnum'}. '"';
+    $OUT .= ' SELECTED' if $part_referral->{'refnum'} eq $refnum;
+    $OUT .= '>'. $part_referral->{'referral'};
+  }
+%>
+</SELECT><BR><BR>
 Contact Information
 <TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0 WIDTH="100%">
 <TR>
@@ -101,6 +124,12 @@ Contact Information
       'PREPAY' => qq!Prepaid card<BR><font color="#ff0000">*</font><INPUT TYPE="text" NAME="PREPAY_payinfo" VALUE="" MAXLENGTH=80>!,
     );
 
+    if ( $init_data->{'cvv_enabled'} ) {
+      foreach my $payby ( grep { exists $payby{$_} } qw(CARD DCRD) ) { #1.4/1.5
+        $payby{$payby} .= qq!<BR>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>)&nbsp;<INPUT TYPE="text" NAME=${payby}_paycvv VALUE="" SIZE=4 MAXLENGTH=4>!;
+      }
+    }
+
     my( $account, $aba ) = split('@', $payinfo);
     my %paybychecked = (
       'CARD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("CARD", $paydate). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname">!,
@@ -113,6 +142,12 @@ Contact Information
       'PREPAY' => qq!Prepaid card<BR><font color="#ff0000">*</font><INPUT TYPE="text" NAME="PREPAY_payinfo" VALUE="$payinfo" MAXLENGTH=80>!,
     );
 
+    if ( $init_data->{'cvv_enabled'} ) {
+      foreach my $payby ( grep { exists $payby{$_} } qw(CARD DCRD) ) { #1.4/1.5
+        $paybychecked{$payby} .= qq!<BR>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>)&nbsp;<INPUT TYPE="text" NAME=${payby}_paycvv VALUE="$paycvv" SIZE=4 MAXLENGTH=4>!;
+      }
+    }
+
     for (@payby) {
       if ( scalar(@payby) == 1) {
         $OUT .= '<TD VALIGN=TOP>'.
diff --git a/fs_signup/fs_signup_server b/fs_signup/fs_signup_server
deleted file mode 100755 (executable)
index d6eb4a8..0000000
+++ /dev/null
@@ -1,289 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# fs_signup_server
-#
-
-use strict;
-use vars qw($pid);
-use IO::Handle;
-use Storable qw(nstore_fd fd_retrieve);
-use Tie::RefHash;
-use Net::SSH qw(sshopen2);
-use FS::UID qw(adminsuidsetup);
-use FS::Conf;
-use FS::Record qw( qsearch qsearchs );
-use FS::cust_main_county;
-use FS::cust_main;
-use FS::cust_bill;
-use FS::cust_pkg;
-use FS::Msgcat qw(gettext);
-
-use vars qw( $opt $Debug );
-
-$Debug = 2;
-
-my $user = shift or die &usage;
-&adminsuidsetup( $user ); 
-
-my $conf = new FS::Conf;
-
-if ($conf->exists('signup_server-quiet')) {
-    $FS::cust_bill::quiet = 1;
-    $FS::cust_pkg::quiet = 1;
-}
-
-#my @payby = qw(CARD PREPAY);
-my @payby = $conf->config('signup_server-payby');
-my $smtpmachine = $conf->config('smtpmachine');
-
-my $machine = shift or die &usage;
-
-my $agentnum = shift or die &usage;
-my $agent = qsearchs( 'agent', { 'agentnum' => $agentnum } ) or die &usage;
-my $pkgpart_href = $agent->pkgpart_hashref;
-
-my $refnum = shift or die &usage;
-
-#causing trouble for some folks
-#$SIG{CHLD} = sub { wait() };
-
-$SIG{HUP} = \&killssh;
-$SIG{INT} = \&killssh;
-$SIG{QUIT} = \&killssh;
-$SIG{TERM} = \&killssh;
-$SIG{PIPE} = \&killssh;
-sub killssh { kill 'TERM', $pid if $pid; exit; };
-
-my($fs_signupd)="/usr/local/sbin/fs_signupd";
-
-while (1) {
-  my($reader,$writer)=(new IO::Handle, new IO::Handle);
-  #seems to be broken - calling ->flush explicitly# $writer->autoflush(1);
-  warn "[fs_signup_server] Connecting to $machine...\n" if $Debug;
-  $pid = sshopen2($machine,$reader,$writer,$fs_signupd);
-
-  my @pops = qsearch('svc_acct_pop',{} );
-  my $init_data = {
-
-    #'_protocol' => 'signup',
-    #'_version' => '0.1',
-    #'_packet' => 'init'
-  
-    'cust_main_county' =>
-      [ map { $_->hashref } qsearch('cust_main_county', {}) ],
-      
-    'part_pkg' =>
-      [
-        #map { $_->hashref }
-        map { { 'payby' => [ $_->payby ], %{$_->hashref} } }
-          grep { $_->svcpart('svc_acct') && $pkgpart_href->{ $_->pkgpart } }
-            qsearch( 'part_pkg', { 'disabled' => '' } )
-      ],
-
-    'agentnum2part_pkg' =>
-      {
-        map {
-          my $href = $_->pkgpart_hashref;
-          $_->agentnum =>
-            [
-              map { { 'payby' => [ $_->payby ], %{$_->hashref} } }
-                grep { $_->svcpart('svc_acct') && $href->{ $_->pkgpart } }
-                  qsearch( 'part_pkg', { 'disabled' => '' } )
-            ];
-        } qsearch('agent', {} )
-      },
-
-    'svc_acct_pop' => [ map { $_->hashref } @pops ],
-
-    'security_phrase' => $conf->exists('security_phrase'),
-
-    'payby' => [ $conf->config('signup_server-payby') ],
-
-    'msgcat' => { map { $_=>gettext($_) } qw(
-      passwords_dont_match invalid_card unknown_card_type not_a
-    ) },
-
-    'statedefault' => $conf->config('statedefault') || 'CA',
-
-    'countrydefault' => $conf->config('countrydefault') || 'US',
-
-  };
-
-  warn "[fs_signup_server] Sending init data...\n" if $Debug;
-  nstore_fd($init_data, $writer) or die "can't send init data: $!";
-  $writer->flush;
-
-  warn "[fs_signup_server] Entering main loop...\n" if $Debug;
-  while (1) {
-    warn "[fs_signup_server] Reading (waiting for) signup data...\n" if $Debug;
-    my $signup_data = fd_retrieve($reader);
-
-    if ( $Debug > 1 ) {
-      warn join('',
-        map { "  $_ => ". $signup_data->{$_}. "\n" } keys %$signup_data );
-    }
-
-    warn "[fs_signup_server] Processing signup...\n" if $Debug;
-
-    my $error = '';
-
-    #things that aren't necessary in base class, but are for signup server
-      #return "Passwords don't match"
-      #  if $hashref->{'_password'} ne $hashref->{'_password2'}
-    $error ||= gettext('empty_password') unless $signup_data->{'_password'};
-    $error ||= gettext('no_access_number_selected')
-      unless $signup_data->{'popnum'} || !scalar(@pops);
-
-    #shares some stuff with htdocs/edit/process/cust_main.cgi... take any
-    # common that are still here and library them.
-    my $cust_main = new FS::cust_main ( {
-      #'custnum'          => '',
-      'agentnum'         => $signup_data->{agentnum} || $agentnum,
-      'refnum'           => $refnum,
-
-      map { $_ => $signup_data->{$_} } qw(
-        last first ss company address1 address2 city county state zip country
-        daytime night fax payby payinfo paydate payname referral_custnum comments
-      ),
-
-    } );
-
-    $error ||= "Illegal payment type"
-      unless grep { $_ eq $signup_data->{'payby'} } @payby;
-
-    $cust_main->payinfo($cust_main->daytime)
-      if $cust_main->payby eq 'LECB' && ! $cust_main->payinfo;
-
-    my @invoicing_list = split( /\s*\,\s*/, $signup_data->{'invoicing_list'} );
-
-    $signup_data->{'pkgpart'} =~ /^(\d+)$/ or '' =~ /^()$/;
-    my $pkgpart = $1;
-
-    my $part_pkg =
-      qsearchs( 'part_pkg', { 'pkgpart' => $pkgpart } )
-        or $error ||= "WARNING: unknown pkgpart: $pkgpart";
-    my $svcpart = $part_pkg->svcpart('svc_acct') unless $error;
-
-    my $cust_pkg = new FS::cust_pkg ( {
-      #later#'custnum' => $custnum,
-      'pkgpart' => $signup_data->{'pkgpart'},
-    } );
-    $error ||= $cust_pkg->check;
-
-    my $svc_acct = new FS::svc_acct ( {
-      'svcpart'   => $svcpart,
-      map { $_ => $signup_data->{$_} }
-        qw( username _password sec_phrase popnum ),
-    } );
-
-    my $y = $svc_acct->setdefault; # arguably should be in new method
-    $error ||= $y unless ref($y);
-
-    $error ||= $svc_acct->check;
-
-    use Tie::RefHash;
-    tie my %hash, 'Tie::RefHash';
-    %hash = ( $cust_pkg => [ $svc_acct ] );
-    $error ||= $cust_main->insert( \%hash, \@invoicing_list ); #msgcat
-
-    if ( ! $error && $conf->exists('signup_server-realtime') ) {
-
-      warn "[fs_signup_server] Billing customer...\n" if $Debug;
-
-      my $bill_error = $cust_main->bill;
-      warn "[fs_signup_server] error billing new customer: $bill_error"
-        if $bill_error;
-
-      $cust_main->apply_payments;
-      $cust_main->apply_credits;
-
-      $bill_error = $cust_main->collect;
-      warn "[fs_signup_server] error collecting from new customer: $bill_error"
-        if $bill_error;
-
-      if ( $cust_main->balance > 0 ) {
-
-        #this makes sense.  credit is "un-doing" the invoice
-        $cust_main->credit( $cust_main->balance, 'signup server decline' );
-        $cust_main->apply_credits;
-
-        #should check list for errors...
-        #$cust_main->suspend;
-        $cust_main->cancel;
-
-        $error = '_decline';
-      }
-    }
-
-    warn "[fs_signup_server] Sending results...\n" if $Debug;
-    print $writer $error, "\n";
-
-    next if $error;
-
-    if ( $conf->config('signup_server-email') ) {
-      warn "[fs_signup_server] Sending email...\n" if $Debug;
-
-      #false laziness w/FS::cust_bill::send & FS::cust_pay::delete
-      use Mail::Header;
-      use Mail::Internet 1.44;
-      use Date::Format;
-      my $from = $conf->config('invoice_from'); #??? as good as any
-      $ENV{MAILADDRESS} = $from;
-      my $header = new Mail::Header ( [
-        "From: $from",
-        "To: ". $conf->config('signup_server-email'),
-        "Sender: $from",
-        "Reply-To: $from",
-        "Date: ". time2str("%a, %d %b %Y %X %z", time),
-        "Subject: FREESIDE NOTIFICATION: Signup Server",
-      ] );
-      my $body = [
-        "This is an automatic message from your Freeside installation\n",
-        "informing you a customer has signed up via the signup server:\n",
-        "\n",
-        'custnum     : '. $cust_main->custnum. "\n",
-        'Name        : '. $cust_main->last. ", ". $cust_main->first. "\n",
-        'Agent       : '. $cust_main->agent->agent. "\n",
-        'Package     : '. $part_pkg->pkg. ' - '. $part_pkg->comment. "\n",
-        'Signup Date : '. time2str('%C', time). "\n",
-        'Username    : '. $svc_acct->username. "\n",
-        #'Password    : '. # config file to turn this on if noment insists
-        'Day phone   : '. $cust_main->daytime. "\n",
-        'Night phone : '. $cust_main->night. "\n",
-        'Address     : '. $cust_main->address1. "\n",
-        ( $cust_main->address2
-            ? '              '. $cust_main->address2. "\n"
-            : ''                                           ),
-        '              '. $cust_main->city. ', '. $cust_main->state. '  '.
-                          $cust_main->zip. "\n",
-        ( $cust_main->country eq 'US'
-            ? ''
-            : '              '. $cust_main->country. "\n" ),
-        "\n",
-      ];
-      #if ( $cust_main->balance > 0 ) {
-      #  push @$body,
-      #    "This customer has an outstanding balance and has been suspended.\n";
-      #}
-      my $message = new Mail::Internet ( 'Header' => $header, 'Body' => $body );
-      $!=0;
-      $message->smtpsend( Host => $smtpmachine )
-        or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
-          or warn "[fs_signup_server] can't send email to ".
-                   $conf->config('signup_server-email').
-                   " via server $smtpmachine with SMTP: $!";
-      #end-of-send mail
-    }
-
-  }
-  close $writer;
-  close $reader;
-  warn "connection to $machine lost!  waiting 60 seconds...\n";
-  sleep 60;
-  warn "reconnecting...\n";
-}
-
-sub usage {
-  die "Usage:\n\n  fs_signup_server user machine agentnum refnum\n";
-}
-
index 6f76fd2..8027ae3 100644 (file)
@@ -4,7 +4,8 @@ BEGIN { eval "use Devel::AutoProfiler;"; } #only if installed...
 
 use strict;
 use vars qw( $cgi $p );
-use CGI;
+use Apache::ASP 2.55;
+use CGI 2.47;
 #use CGI::Carp qw(fatalsToBrowser);
 use Date::Format;
 use Date::Parse;
@@ -15,13 +16,14 @@ use IO::Handle;
 use IO::File;
 use String::Approx qw(amatch);
 use Chart::LinesPoints;
-use HTML::Widgets::SelectLayers 0.02;
+use HTML::Widgets::SelectLayers 0.03;
 use FS::UID qw(cgisuidsetup dbh getotaker datasrc driver_name);
 use FS::Record qw(qsearch qsearchs fields dbdef);
 use FS::Conf;
 use FS::CGI qw(header menubar popurl table itable ntable idiot eidiot
                small_custview myexit http_header);
 use FS::Msgcat qw(gettext geterror);
+use FS::Misc qw( send_email );
 
 use FS::agent;
 use FS::agent_type;
@@ -56,6 +58,7 @@ use FS::svc_www;
 use FS::router;
 use FS::addr_block;
 use FS::svc_broadband;
+use FS::svc_external;
 use FS::type_pkgs;
 use FS::part_export;
 use FS::part_export_option;
@@ -72,7 +75,8 @@ sub Script_OnStart {
   &cgisuidsetup($cgi);
   $p = popurl(2);
   #print $cgi->header( '-expires' => 'now' );
-  dbh->{'private_profile'} = {} if dbh->can('sprintProfile');
+  #dbh->{'private_profile'} = {} if dbh->can('sprintProfile');
+  dbh->{'private_profile'} = {} if UNIVERSAL::can(dbh, 'sprintProfile');
 
   #really should check for FS::Profiler or something
     # Devel::AutoProfiler _our_ VERSION?  thanks a fucking lot
@@ -109,7 +113,7 @@ sub Script_OnFlush {
   #$$ref = $cgi->header() . $$ref;
   #warn "Script_OnFlush called with dbh ". dbh. "\n";
   #if ( dbh->can('sprintProfile') ) {
-  if ( UNIVERSAL::can(dbh,'sprintProfile') ) {
+  if ( UNIVERSAL::can(dbh, 'sprintProfile') ) {
     #warn "dbh can sprintProfile\n";
     if ( lc($Response->{ContentType}) eq 'text/html' ) { #con
       #warn "contenttype is sprintProfile\n";
@@ -136,8 +140,12 @@ if ( defined(@DBIx::Profile::ISA) ) {
     my( $self, $location) = @_;
     my $page =
       $cgi->header.
-      qq!<HTML><BODY>Redirect to <A HREF="$location">$location</A><BR><BR>!.
-      '<PRE>'. encode_entities(dbh->sprintProfile()).
+      qq!<HTML><BODY>Redirect to <A HREF="$location">$location</A>!.
+      '<BR><BR><PRE>'.
+        ( UNIVERSAL::can(dbh, 'sprintProfile')
+            ? encode_entities(dbh->sprintProfile())
+            : 'DBIx::Profile missing sprintProfile method;'.
+              'unpatched or too old?'                        ).
       "\n\n". &sprintAutoProfile().  '</PRE>'.
       '</BODY></HTML>';
     dbh->{'private_profile'} = {};
@@ -186,5 +194,40 @@ sub sprintAutoProfile {
 
 }
 
+sub include {
+  $Response->Include(@_);
+}
+
+if ( defined(@DBIx::Profile::ISA) ) {
+
+  #false laziness w/above
+  *redirect = sub {
+    my($location) = @_;
+
+    ${$Response->{BinaryRef}} = 
+      $cgi->header.
+      qq!<HTML><BODY>Redirect to <A HREF="$location">$location</A>!.
+      '<BR><BR><PRE>'.
+        ( UNIVERSAL::can(dbh, 'sprintProfile')
+            ? encode_entities(dbh->sprintProfile())
+            : 'DBIx::Profile missing sprintProfile method;'.
+              'unpatched or too old?'                        ).
+      "\n\n". &sprintAutoProfile().  '</PRE>'.
+      '</BODY></HTML>';
+
+    dbh->{'private_profile'} = {};
+
+    $Response->End;
+
+  };
+
+} else {
+
+  *redirect = sub {
+    $Response->Redirect(@_);
+  }
+
+}
+
 1;
 
index c81db86..618c585 100644 (file)
@@ -7,7 +7,7 @@
 package HTML::Mason;
 
 # Bring in main Mason package.
-use HTML::Mason;
+use HTML::Mason 1.1;
 
 # Bring in ApacheHandler, necessary for mod_perl integration.
 # Uncomment the second line (and comment the first) to use
@@ -62,7 +62,7 @@ sub handler
     { package HTML::Mason::Commands;
       use strict;
       use vars qw( $cgi $p );
-      use CGI;
+      use CGI 2.47;
       #use CGI::Carp qw(fatalsToBrowser);
       use Date::Format;
       use Date::Parse;
@@ -73,13 +73,14 @@ sub handler
       use IO::File;
       use String::Approx qw(amatch);
       use Chart::LinesPoints;
-      use HTML::Widgets::SelectLayers 0.02;
+      use HTML::Widgets::SelectLayers 0.03;
       use FS::UID qw(cgisuidsetup dbh getotaker datasrc driver_name);
       use FS::Record qw(qsearch qsearchs fields dbdef);
       use FS::Conf;
       use FS::CGI qw(header menubar popurl table itable ntable idiot eidiot
                      small_custview myexit http_header);
       use FS::Msgcat qw(gettext geterror);
+      use FS::Misc qw( send_email );
 
       use FS::agent;
       use FS::agent_type;
@@ -108,13 +109,13 @@ sub handler
       use FS::session;
       use FS::svc_acct;
       use FS::svc_acct_pop qw(popselector);
-      use FS::svc_acct_sm;
       use FS::svc_domain;
       use FS::svc_forward;
       use FS::svc_www;
       use FS::router;
       use FS::addr_block;
       use FS::svc_broadband;
+      use FS::svc_external;
       use FS::type_pkgs;
       use FS::part_export;
       use FS::part_export_option;
@@ -124,26 +125,72 @@ sub handler
       *CGI::redirect = sub {
         my( $self, $location ) = @_;
         use vars qw($m);
-        #http://www.masonhq.com/docs/faq/#how_do_i_do_an_external_redirect
-        $m->clear_buffer;
-        # The next two lines are necessary to stop Apache from re-reading
-        # POSTed data.
-        $r->method('GET');
-        $r->headers_in->unset('Content-length');
-        $r->content_type('text/html');
-        #$r->err_header_out('Location' => $location);
-        $r->header_out('Location' => $location);
-         $r->header_out('Content-Type' => 'text/html');
-         $m->abort(302);
-
-        '';
+
+        if ( defined(@DBIx::Profile::ISA) ) { #profiling redirect
+
+          my $page =
+            qq!<HTML><BODY>Redirect to <A HREF="$location">$location</A>!.
+            '<BR><BR><PRE>'.
+              ( UNIVERSAL::can(dbh, 'sprintProfile')
+                  ? encode_entities(dbh->sprintProfile())
+                  : 'DBIx::Profile missing sprintProfile method;'.
+                    'unpatched or too old?'                        ).
+            #"\n\n". &sprintAutoProfile().  '</PRE>'.
+            "\n\n".                         '</PRE>'.
+            '</BODY></HTML>';
+          dbh->{'private_profile'} = {};
+          return $page;
+
+        } else { #normal redirect
+
+          $m->redirect($location);
+          '';
+
+        }
+
       };
 
       $cgi = new CGI;
       &cgisuidsetup($cgi);
       #&cgisuidsetup($r);
       $p = popurl(2);
-    }
+
+      sub include {
+        use vars qw($m);
+        $m->scomp(@_);
+      }
+
+      sub redirect {
+        my( $location ) = @_;
+        use vars qw($m);
+        $m->clear_buffer;
+        #false laziness w/above
+        if ( defined(@DBIx::Profile::ISA) ) { #profiling redirect
+
+          $m->print(
+            qq!<HTML><BODY>Redirect to <A HREF="$location">$location</A>!.
+            '<BR><BR><PRE>'.
+              ( UNIVERSAL::can(dbh, 'sprintProfile')
+                  ? encode_entities(dbh->sprintProfile())
+                  : 'DBIx::Profile missing sprintProfile method;'.
+                    'unpatched or too old?'                        ).
+            #"\n\n". &sprintAutoProfile().  '</PRE>'.
+            "\n\n".                         '</PRE>'.
+            '</BODY></HTML>'
+          );
+          dbh->{'private_profile'} = {};
+
+          $m->abort(200);
+
+        } else { #normal redirect
+
+          $m->redirect($location);
+
+        }
+
+      }
+
+    } # end package HTML::Mason::Commands;
 
     $r->content_type('text/html');
     #eorar
diff --git a/htetc/handler.pl-1.0x b/htetc/handler.pl-1.0x
deleted file mode 100644 (file)
index def579f..0000000
+++ /dev/null
@@ -1,158 +0,0 @@
-#!/usr/bin/perl
-#
-# This is a basic, fairly fuctional Mason handler.pl.
-#
-# For something a little more involved, check out session_handler.pl
-
-package HTML::Mason;
-
-# Bring in main Mason package.
-use HTML::Mason;
-
-# Bring in ApacheHandler, necessary for mod_perl integration.
-# Uncomment the second line (and comment the first) to use
-# Apache::Request instead of CGI.pm to parse arguments.
-use HTML::Mason::ApacheHandler;
-# use HTML::Mason::ApacheHandler (args_method=>'mod_perl');
-
-# Uncomment the next line if you plan to use the Mason previewer.
-#use HTML::Mason::Preview;
-
-use strict;
-
-# List of modules that you want to use from components (see Admin
-# manual for details)
-#{  package HTML::Mason::Commands;
-#   use CGI;
-#}
-
-# Create Mason objects
-#
-my $parser = new HTML::Mason::Parser;
-my $interp = new HTML::Mason::Interp (parser=>$parser,
-                                      comp_root=>'/var/www/freeside',
-                                      data_dir=>'/usr/local/etc/freeside/masondata',
-                                      out_mode=>'stream',
-                                     );
-my $ah = new HTML::Mason::ApacheHandler ( interp => $interp,
-                                          #auto_send_headers => 0,
-                                        );
-
-# Activate the following if running httpd as root (the normal case).
-# Resets ownership of all files created by Mason at startup.
-#
-chown (Apache->server->uid, Apache->server->gid, $interp->files_written);
-
-sub handler
-{
-    my ($r) = @_;
-
-    # If you plan to intermix images in the same directory as
-    # components, activate the following to prevent Mason from
-    # evaluating image files as components.
-    #
-    #return -1 if $r->content_type && $r->content_type !~ m|^text/|i;
-
-    #rar
-    { package HTML::Mason::Commands;
-      use strict;
-      use vars qw( $cgi $p );
-      use CGI;
-      #use CGI::Carp qw(fatalsToBrowser);
-      use Date::Format;
-      use Date::Parse;
-      use Time::Local;
-      use Tie::IxHash;
-      use HTML::Entities;
-      use IO::Handle;
-      use IO::File;
-      use String::Approx qw(amatch);
-      use Chart::LinesPoints;
-      use HTML::Widgets::SelectLayers 0.02;
-      use FS::UID qw(cgisuidsetup dbh getotaker datasrc driver_name);
-      use FS::Record qw(qsearch qsearchs fields dbdef);
-      use FS::Conf;
-      use FS::CGI qw(header menubar popurl table itable ntable idiot eidiot
-                     small_custview myexit http_header);
-      use FS::Msgcat qw(gettext geterror);
-
-      use FS::agent;
-      use FS::agent_type;
-      use FS::domain_record;
-      use FS::cust_bill;
-      use FS::cust_bill_pay;
-      use FS::cust_credit;
-      use FS::cust_credit_bill;
-      use FS::cust_main;
-      use FS::cust_main_county;
-      use FS::cust_pay;
-      use FS::cust_pkg;
-      use FS::cust_refund;
-      use FS::cust_svc;
-      use FS::nas;
-      use FS::part_bill_event;
-      use FS::part_pkg;
-      use FS::part_referral;
-      use FS::part_svc;
-      use FS::part_svc_router;
-      use FS::part_virtual_field;
-      use FS::pkg_svc;
-      use FS::port;
-      use FS::queue qw(joblisting);
-      use FS::raddb;
-      use FS::session;
-      use FS::svc_acct;
-      use FS::svc_acct_pop qw(popselector);
-      use FS::svc_acct_sm;
-      use FS::svc_domain;
-      use FS::svc_forward;
-      use FS::svc_www;
-      use FS::router;
-      use FS::addr_block;
-      use FS::svc_broadband;
-      use FS::type_pkgs;
-      use FS::part_export;
-      use FS::part_export_option;
-      use FS::export_svc;
-      use FS::msgcat;
-
-      *CGI::redirect = sub {
-        my( $self, $location ) = @_;
-
-        #http://www.masonhq.com/docs/faq/#how_do_i_do_an_external_redirect
-        $m->clear_buffer;
-        # The next two lines are necessary to stop Apache from re-reading
-        # POSTed data.
-        $r->method('GET');
-        $r->headers_in->unset('Content-length');
-        $r->content_type('text/html');
-        #$r->err_header_out('Location' => $location);
-        $r->header_out('Location' => $location);
-         $r->header_out('Content-Type' => 'text/html');
-         $m->abort(302);
-
-        '';
-      };
-
-      $cgi = new CGI;
-      &cgisuidsetup($cgi);
-      #&cgisuidsetup($r);
-      $p = popurl(2);
-    }
-
-    $r->content_type('text/html');
-    #eorar
-
-    my $headers = $r->headers_out;
-    $headers->{'Pragma'} = $headers->{'Cache-control'} = 'no-cache';
-    #$r->no_cache(1);
-    $headers->{'Expires'} = '0';
-
-#    $r->send_http_header;
-
-    my $status = $ah->handle_request($r);
-
-    $status;
-}
-
-1;
diff --git a/httemplate/autohandler b/httemplate/autohandler
new file mode 100644 (file)
index 0000000..2bd3adf
--- /dev/null
@@ -0,0 +1,21 @@
+% $m->call_next;
+<%init>
+  dbh->{'private_profile'} = {} if UNIVERSAL::can(dbh, 'sprintProfile');
+</%init>
+<%filter>
+
+my $profile = '';
+if ( UNIVERSAL::can(dbh, 'sprintProfile') ) {
+
+  if ( lc($r->content_type) eq 'text/html' ) {
+
+    $profile = '<PRE>'. ("\n"x4096). encode_entities(dbh->sprintProfile()).
+               #"\n\n". &sprintAutoProfile(). '</PRE>';
+               "\n\n".                        '</PRE>';
+  } 
+
+  dbh->{'private_profile'} = {};
+}
+
+s/(<\/BODY>[\s\n]*<\/HTML>[\s\n]*)$/$profile$1/i;
+</%filter>
index cff111c..2eef5bb 100755 (executable)
@@ -1,15 +1,38 @@
 <!-- mason kludge -->
+
 <%
-#Begin silliness
-#
-#use FS::UI::CGI;
-#use FS::UI::agent;
-#
-#$ui = new FS::UI::agent;
-#$ui->browse;
-#exit;
-#__END__
-#End silliness
+
+  my %search;
+  if ( $cgi->param('showdisabled')
+       || !dbdef->table('agent')->column('disabled') ) {
+    %search = ();
+  } else {
+    %search = ( 'disabled' => '' );
+  }
+
+  #bad false laziness with search/cust_main.cgi (also needs fixing up for
+  #old mysql)
+  my $ncancelled = "
+     0 < ( SELECT COUNT(*) FROM cust_pkg
+                  WHERE cust_pkg.custnum = cust_main.custnum
+                    AND ( cust_pkg.cancel IS NULL
+                          OR cust_pkg.cancel = 0
+                        )
+              )
+       OR 0 = ( SELECT COUNT(*) FROM cust_pkg
+                  WHERE cust_pkg.custnum = cust_main.custnum
+              )
+  ";
+
+  my $ncancelled_sth = dbh->prepare("SELECT COUNT(*) FROM cust_main
+                                       WHERE agentnum = ?
+                                         AND ( $ncancelled )         ")
+    or die dbh->errstr;
+
+  my $total_sth = dbh->prepare("SELECT COUNT(*) FROM cust_main
+                                  WHERE agentnum = ?           ")
+    or die dbh->errstr;
+
 %>
 
 <%= header('Agent Listing', menubar(
@@ -21,10 +44,20 @@ Agents are resellers of your service. Agents may be limited to a subset of your
 full offerings (via their type).<BR><BR>
 <A HREF="<%= $p %>edit/agent.cgi"><I>Add a new agent</I></A><BR><BR>
 
+<% if ( dbdef->table('agent')->column('disabled') ) { %>
+  <%= $cgi->param('showdisabled')
+      ? do { $cgi->param('showdisabled', 0);
+             '( <a href="'. $cgi->self_url. '">hide disabled agents</a> )'; }
+      : do { $cgi->param('showdisabled', 1);
+             '( <a href="'. $cgi->self_url. '">show disabled agents</a> )'; }
+  %>
+<% } %>
+
 <%= table() %>
 <TR>
-  <TH COLSPAN=2>Agent</TH>
+  <TH COLSPAN=<%= ( $cgi->param('showdisabled') || !dbdef->table('agent')->column('disabled') ) ? 2 : 3 %>>Agent</TH>
   <TH>Type</TH>
+  <TH>Customers</TH>
   <TH><FONT SIZE=-1>Freq.</FONT></TH>
   <TH><FONT SIZE=-1>Prog.</FONT></TH>
 </TR>
@@ -35,29 +68,44 @@ full offerings (via their type).<BR><BR>
 foreach my $agent ( sort { 
   #$a->getfield('agentnum') <=> $b->getfield('agentnum')
   $a->getfield('agent') cmp $b->getfield('agent')
-} qsearch('agent',{}) ) {
-  my($hashref)=$agent->hashref;
-  my($typenum)=$hashref->{typenum};
-  my($agent_type)=qsearchs('agent_type',{'typenum'=>$typenum});
-  my($atype)=$agent_type->getfield('atype');
-  print <<END;
+} qsearch('agent', \%search ) ) {
+
+  $ncancelled_sth->execute($agent->agentnum) or die $ncancelled_sth->errstr;
+  my $num_ncancelled = $ncancelled_sth->fetchrow_arrayref->[0];
+
+  $total_sth->execute($agent->agentnum) or die $total_sth->errstr;
+  my $num_total = $total_sth->fetchrow_arrayref->[0];
+
+  my $num_cancelled = $num_total - $num_ncancelled;
+
+  my $cust_main_link = $p. 'search/cust_main.cgi?agentnum_on=1&'.
+                       'agentnum='. $agent->agentnum;
+
+%>
+
       <TR>
-        <TD><A HREF="${p}edit/agent.cgi?$hashref->{agentnum}">
-          $hashref->{agentnum}</A></TD>
-        <TD><A HREF="${p}edit/agent.cgi?$hashref->{agentnum}">
-          $hashref->{agent}</A></TD>
-        <TD><A HREF="${p}edit/agent_type.cgi?$typenum">$atype</A></TD>
-        <TD>$hashref->{freq}</TD>
-        <TD>$hashref->{prog}</TD>
+        <TD><A HREF="<%=$p%>edit/agent.cgi?<%= $agent->agentnum %>">
+          <%= $agent->agentnum %></A></TD>
+<% if ( dbdef->table('agent')->column('disabled')
+        && !$cgi->param('showdisabled')           ) { %>
+        <TD><%= $agent->disabled ? 'DISABLED' : '' %></TD>
+<% } %>
+
+        <TD><A HREF="<%=$p%>edit/agent.cgi?<%= $agent->agentnum %>">
+          <%= $agent->agent %></A></TD>
+        <TD><A HREF="<%=$p%>edit/agent_type.cgi?<%= $agent->typenum %>"><%= $agent->agent_type->atype %></A></TD>
+        <TD>
+          <FONT COLOR="#00CC00"><B><%= $num_ncancelled %></B></FONT>
+            <A HREF="<%= $cust_main_link %>&showcancelledcustomers=0">active</A>
+          <BR><FONT COLOR="#FF0000"><B><%= $num_cancelled %></B></FONT>
+            <A HREF="<%= $cust_main_link %>&showcancelledcustomers=1&cancelled=1">cancelled</A>
+        </TD>
+        <TD><%= $agent->freq %></TD>
+        <TD><%= $agent->prog %></TD>
       </TR>
-END
 
-}
+<% } %>
 
-print <<END;
     </TABLE>
   </BODY>
 </HTML>
-END
-
-%>
index 5a84385..5473804 100755 (executable)
@@ -17,9 +17,11 @@ agents.<BR><BR>
 foreach my $agent_type ( sort { 
   $a->getfield('typenum') <=> $b->getfield('typenum')
 } qsearch('agent_type',{}) ) {
-  my($hashref)=$agent_type->hashref;
-  my(@type_pkgs)=qsearch('type_pkgs',{'typenum'=> $hashref->{typenum} });
-  my($rowspan)=scalar(@type_pkgs);
+  my $hashref = $agent_type->hashref;
+  #more efficient to do this with SQL...
+  my @type_pkgs = grep { $_->part_pkg and ! $_->part_pkg->disabled }
+                       qsearch('type_pkgs',{'typenum'=> $hashref->{typenum} });
+  my $rowspan = scalar(@type_pkgs);
   $rowspan = int($rowspan/2+0.5) ;
   print <<END;
       <TR>
index c2473c4..1e0e088 100755 (executable)
@@ -24,7 +24,7 @@ print '<BR><BR>'. &table(). <<END;
         <TH>Taxclass<BR><FONT SIZE=-1>(per-package classification)</FONT></TH>
         <TH>Tax name<BR><FONT SIZE=-1>(printed on invoices)</FONT></TH>
         <TH><FONT SIZE=-1>Tax</FONT></TH>
-        <TH><FONT SIZE=-1>Exempt<BR>per<BR>month</TH>
+        <TH><FONT SIZE=-1>Exemption</TH>
       </TR>
 END
 
@@ -54,7 +54,9 @@ END
       last if $hashref->{country} ne $regions[$i+$j]->country
            || $hashref->{state} ne $regions[$i+$j]->state
            || $hashref->{tax} != $regions[$i+$j]->tax
-           || $hashref->{exempt_amount} != $regions[$i+$j]->exempt_amount;
+           || $hashref->{exempt_amount} != $regions[$i+$j]->exempt_amount
+           || $hashref->{setuptax} ne $regions[$i+$j]->setuptax
+           || $hashref->{recurtax} ne $regions[$i+$j]->recurtax;
     }
 
     my $newsup=0;
@@ -121,9 +123,13 @@ END
   print "</TD>";
 
   print "<TD BGCOLOR=\"#ffffff\">$hashref->{tax}%</TD>".
-        '<TD BGCOLOR="#ffffff">$'.
-          sprintf("%.2f", $hashref->{exempt_amount} || 0). '</TD>'.
-        '</TR>';
+        '<TD BGCOLOR="#ffffff">';
+  print '$'. sprintf("%.2f", $hashref->{exempt_amount} ).
+        '&nbsp;per&nbsp;month<BR>'
+    if $hashref->{exempt_amount} > 0;
+  print 'Setup&nbsp;fee<BR>' if $hashref->{setuptax} =~ /^Y$/i;
+  print 'Recurring&nbsp;fee<BR>' if $hashref->{recurtax} =~ /^Y$/i;
+  print '</TD></TR>';
 
 }
 
index 608a58d..3420e97 100755 (executable)
@@ -1,10 +1,38 @@
 <!-- mason kludge -->
+<%= header("Pending credit card batch", menubar( 'Main Menu' => $p,)) %>
+
+<FORM ACTION="<%=$p%>misc/download-batch.cgi" METHOD="POST">
+Download batch in format <SELECT NAME="format">
+<OPTION VALUE="csv-td_canada_trust-merchant_pc_batch">CSV file for TD Canada Trust Merchant PC Batch</OPTION>
+</SELECT><INPUT TYPE="submit" VALUE="Download"></FORM>
+<BR><BR>
+
+<FORM ACTION="<%=$p%>misc/upload-batch.cgi" METHOD="POST" ENCTYPE="multipart/form-data">
+Upload results<BR>
+Filename <INPUT TYPE="file" NAME="batch_results"><BR>
+Format <SELECT NAME="format">
+<OPTION VALUE="csv-td_canada_trust-merchant_pc_batch">CSV results from TD Canada Trust Merchant PC Batch</OPTION>
+</SELECT><BR>
+<INPUT TYPE="submit" VALUE="Upload"></FORM>
+<BR>
+
 <%
+  my $statement = "SELECT SUM(amount) from cust_pay_batch";
+  my $sth = dbh->prepare($statement) or die dbh->errstr. "doing $statement";
+  $sth->execute or die "Error executing \"$statement\": ". $sth->errstr;
+  my $total = $sth->fetchrow_arrayref->[0];
 
-print header("Pending credit card batch", menubar(
-  'Main Menu' => $p,
-#  'Add new referral' => "../edit/part_referral.cgi",
-)), &table(), <<END;
+  my $c_statement = "SELECT COUNT(*) from cust_pay_batch";
+  my $c_sth = dbh->prepare($c_statement)
+    or die dbh->errstr. "doing $c_statement";
+  $c_sth->execute or die "Error executing \"$c_statement\": ". $c_sth->errstr;
+  my $cards = $c_sth->fetchrow_arrayref->[0];
+%>
+<%= $cards %> credit card payments batched<BR>
+$<%= sprintf("%.2f", $total) %> total in pending batch<BR>
+
+<BR>
+<%= &table() %>
       <TR>
         <TH>#</TH>
         <TH><font size=-1>inv#</font></TH>
@@ -14,39 +42,35 @@ print header("Pending credit card batch", menubar(
         <TH>Exp</TH>
         <TH>Amount</TH>
       </TR>
-END
-
-foreach my $cust_pay_batch ( sort { 
-  $a->getfield('paybatchnum') <=> $b->getfield('paybatchnum')
-} qsearch('cust_pay_batch',{}) ) {
-#  my $date = time2str( "%a %b %e %T %Y", $queue->_date );
-#  my $status = $hashref->{status};
-#  if ( $status eq 'failed' || $status eq 'locked' ) {
-#    $status .=
-#      qq! ( <A HREF="$p/edit/cust_pay_batch.cgi?jobnum=$jobnum&action=new">retry</A> |!.
-#      qq! <A HREF="$p/edit/cust_pay_batch.cgi?jobnum$jobnum&action=del">remove </A> )!;
-#  }
-  my $cardnum = $cust_pay_batch->{cardnum};
-  $cardnum =~ s/.{4}$/xxxx/;
-  print <<END;
+
+<%
+foreach my $cust_pay_batch ( sort { $a->paybatchnum <=> $b->paybatchnum }
+                             qsearch('cust_pay_batch', {} )
+) {
+  my $cardnum = $cust_pay_batch->cardnum;
+  #$cardnum =~ s/.{4}$/xxxx/;
+  $cardnum = 'x'x(length($cardnum)-4). substr($cardnum,(length($cardnum)-4));
+
+  $cust_pay_batch->exp =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+  my( $mon, $year ) = ( $2, $1 );
+  $mon = "0$mon" if $mon < 10;
+  my $exp = "$mon/$year";
+
+%>
+
       <TR>
-        <TD>$cust_pay_batch->{paybatchnum}</TD>
-        <TD><A HREF="../view/cust_bill.cgi?$cust_pay_batch->{invnum}">$cust_pay_batch->{invnum}</TD>
-        <TD><A HREF="../view/cust_main.cgi?$cust_pay_batch->{custnum}">$cust_pay_batch->{custnum}</TD>
-        <TD>$cust_pay_batch->{last}, $cust_pay_batch->{last}</TD>
-        <TD>$cust_pay_batch->{payname}</TD>
-        <TD>$cardnum</TD>
-        <TD>$cust_pay_batch->{exp}</TD>
-        <TD align="right">\$$cust_pay_batch->{amount}</TD>
+        <TD><%= $cust_pay_batch->paybatchnum %></TD>
+        <TD><A HREF="../view/cust_bill.cgi?<%= $cust_pay_batch->invnum %>"><%= $cust_pay_batch->invnum %></TD>
+        <TD><A HREF="../view/cust_main.cgi?<%= $cust_pay_batch->custnum %>"><%= $cust_pay_batch->custnum %></TD>
+        <TD><%= $cust_pay_batch->get('last'). ', '. $cust_pay_batch->first %></TD>
+        <TD><%= $cust_pay_batch->payname %></TD>
+        <TD><%= $cardnum %></TD>
+        <TD><%= $exp %></TD>
+        <TD align="right">$<%= $cust_pay_batch->amount %></TD>
       </TR>
-END
 
-}
+<% } %>
 
-print <<END;
     </TABLE>
   </BODY>
 </HTML>
-END
-
-%>
index 76662e0..79c57ae 100755 (executable)
@@ -26,7 +26,7 @@ function part_export_areyousure(href) {
       <%= itable() %>
       <% my %opt = $part_export->options;
          foreach my $opt ( keys %opt ) { %>
-           <TR><TD><%= $opt %></TD><TD><%= $opt{$opt} %></TD></TR>
+           <TR><TD><%= $opt %></TD><TD><%= encode_entities($opt{$opt}) %></TD></TR>
       <% } %>
       </TABLE>
     </TD>
index 7b9436c..180f182 100755 (executable)
@@ -12,7 +12,8 @@ my @part_pkg = qsearch('part_pkg', \%search );
 my $total = scalar(@part_pkg);
 
 my $sortby;
-my %num_active_cust_pkg;
+my %num_active_cust_pkg = ();
+my( $suspended_sth, $canceled_sth ) = ( '', '' );
 if ( $cgi->param('active') ) {
   my $active_sth = dbh->prepare(
     'SELECT COUNT(*) FROM cust_pkg WHERE pkgpart = ?'.
@@ -24,7 +25,21 @@ if ( $cgi->param('active') ) {
     $num_active_cust_pkg{$part_pkg->pkgpart} =
       $active_sth->fetchrow_arrayref->[0];
   }
-  $sortby = \*active_cust_pkg_sort;
+  $sortby = sub {
+    $num_active_cust_pkg{$b->pkgpart} <=> $num_active_cust_pkg{$a->pkgpart};
+  };
+
+  $suspended_sth = dbh->prepare(
+    'SELECT COUNT(*) FROM cust_pkg WHERE pkgpart = ?'.
+    ' AND ( cancel IS NULL OR cancel = 0 )'.
+    ' AND susp IS NOT NULL AND susp != 0'
+  ) or die dbh->errstr;
+
+  $canceled_sth = dbh->prepare(
+    'SELECT COUNT(*) FROM cust_pkg WHERE pkgpart = ?'.
+    ' AND cancel IS NOT NULL AND cancel != 0'
+  ) or die dbh->errstr;
+
 } else {
   $sortby = \*pkgpart_sort;
 }
@@ -63,8 +78,10 @@ print <<END;
         <TH><FONT SIZE=-1>Data</FONT></TH>
         <TH>Service</TH>
         <TH><FONT SIZE=-1>Quan.</FONT></TH>
-      </TR>
 END
+print '<TH><FONT SIZE=-1>Primary</FONT></TH>'
+   if dbdef->table('pkg_svc')->column('primary_svc');
+print '</TR>';
 
 foreach my $part_pkg ( sort $sortby @part_pkg ) {
   my($hashref)=$part_pkg->hashref;
@@ -100,8 +117,19 @@ END
     print "        <TD ROWSPAN=$rowspan>";
     print '<FONT COLOR="#00CC00"><B>'.
           $num_active_cust_pkg{$hashref->{'pkgpart'}}.
-          qq!</B></FONT>&nbsp;<A HREF="${p}search/cust_pkg.cgi?magic=active;pkgpart=$hashref->{pkgpart}">active</A>!;
-    # suspended/cancelled
+          qq!</B></FONT>&nbsp;<A HREF="${p}search/cust_pkg.cgi?magic=active;pkgpart=$hashref->{pkgpart}">active</A><BR>!;
+
+    $suspended_sth->execute( $part_pkg->pkgpart ) or die $suspended_sth->errstr;
+    my $num_suspended = $suspended_sth->fetchrow_arrayref->[0];
+    print '<FONT COLOR="#FF9900"><B>'. $num_suspended.
+          qq!</B></FONT>&nbsp;<A HREF="${p}search/cust_pkg.cgi?magic=suspended;pkgpart=$hashref->{pkgpart}">suspended</A><BR>!;
+
+    $canceled_sth->execute( $part_pkg->pkgpart ) or die $canceled_sth->errstr;
+    my $num_canceled = $canceled_sth->fetchrow_arrayref->[0];
+    print '<FONT COLOR="#FF0000"><B>'. $num_canceled.
+          qq!</B></FONT>&nbsp;<A HREF="${p}search/cust_pkg.cgi?magic=canceled;pkgpart=$hashref->{pkgpart}">canceled</A>!;
+
+
     print '</TD>';
   }
   print <<END;
@@ -117,7 +145,13 @@ END
     my($part_svc) = qsearchs('part_svc',{'svcpart'=> $svcpart });
     print $n,qq!<TD><A HREF="${p}edit/part_svc.cgi?$svcpart">!,
           $part_svc->getfield('svc'),"</A></TD><TD>",
-          $pkg_svc->getfield('quantity'),"</TD></TR>\n";
+          $pkg_svc->getfield('quantity'),"</TD>";
+    if ( dbdef->table('pkg_svc')->column('primary_svc') ) {
+      print '<TD>';
+      print 'PRIMARY' if $pkg_svc->primary_svc =~ /^Y/i;
+      print '</TD>';
+    }
+    print "</TR>\n";
     $n="<TR>";
   }
 
@@ -132,13 +166,8 @@ print <<END;
 </HTML>
 END
 
-
 sub pkgpart_sort {
   $a->pkgpart <=> $b->pkgpart;
 }
 
-sub active_cust_pkg_sort {
-  $num_active_cust_pkg{$b->pkgpart} <=> $num_active_cust_pkg{$a->pkgpart};
-}
-
 %>
index 084c21b..3f59abc 100755 (executable)
@@ -8,31 +8,57 @@ Where a customer heard about your service. Tracked for informational purposes.
 <A HREF="<%= $p %>edit/part_referral.cgi"><I>Add a new advertising source</I></A>
 <BR><BR>
 
+<%
+  my $today = timelocal(0, 0, 0, (localtime(time))[3..5] );
+  my %past;
+  tie %past, 'Tie::IxHash',
+    'Today'         =>        0,
+    'Past week'     =>   518400, # 60sec * 60min * 24hrs * 6days
+    'Past 30 days'  =>  2505600, # 60sec * 60min * 24hrs * 29days 
+    'Past 60 days'  =>  5097600, # 60sec * 60min * 24hrs * 29days 
+    'Past 90 days'  =>  7689600, # 60sec * 60min * 24hrs * 29days 
+    'Past 6 months' => 15724800, # 60sec * 60min * 24hrs * 182days 
+    'Past year'     => 31486000, # 60sec * 60min * 24hrs * 364days 
+    'Total'         => $today,
+  ;
+
+  my $sth = dbh->prepare("SELECT COUNT(*) FROM h_cust_main
+                            WHERE history_action = 'insert'
+                              AND refnum = ?
+                              AND history_date > ?         ")
+    or die dbh->errstr;
+%>
+
 <%= table() %>
 <TR>
-  <TH COLSPAN=2>Advertising source</TH>
+  <TH COLSPAN=2 ROWSPAN=2>Advertising source</TH>
+  <TH COLSPAN=<%= scalar(keys %past) %>>Customers</TH>
+</TR>
+<% for my $period ( keys %past ) { %>
+  <TH><FONT SIZE=-1><%= $period %></FONT></TH>
+<% } %>
 </TR>
 
 <%
 foreach my $part_referral ( sort { 
   $a->getfield('refnum') <=> $b->getfield('refnum')
 } qsearch('part_referral',{}) ) {
-  my($hashref)=$part_referral->hashref;
-  print <<END;
+%>
       <TR>
-        <TD><A HREF="${p}edit/part_referral.cgi?$hashref->{refnum}">
-          $hashref->{refnum}</A></TD>
-        <TD><A HREF="${p}edit/part_referral.cgi?$hashref->{refnum}">
-          $hashref->{referral}</A></TD>
+        <TD><A HREF="<%= $p %>edit/part_referral.cgi?<%= $part_referral->refnum %>">
+          <%= $part_referral->refnum %></A></TD>
+        <TD><A HREF="<%= $p %>edit/part_referral.cgi?<%= $part_referral->refnum %>">
+          <%= $part_referral->referral %></A></TD>
+        <% for my $period ( values %past ) {
+          $sth->execute($part_referral->refnum, $today-$period)
+            or die $sth->errstr;
+          my $number = $sth->fetchrow_arrayref->[0];
+        %>
+          <TD ALIGN="right"><%= $number %></TD>
+        <% } %>
       </TR>
-END
-
-}
+<% } %>
 
-print <<END;
     </TABLE>
   </BODY>
 </HTML>
-END
-
-%>
index 3fece29..ef0de13 100755 (executable)
@@ -13,6 +13,20 @@ my @part_svc =
     qsearch('part_svc', \%search );
 my $total = scalar(@part_svc);
 
+my %num_active_cust_svc = ();
+if ( $cgi->param('active') ) {
+  my $active_sth = dbh->prepare(
+    'SELECT COUNT(*) FROM cust_svc WHERE svcpart = ?'
+  ) or die dbh->errstr;
+  foreach my $part_svc ( @part_svc ) {
+    $active_sth->execute($part_svc->svcpart) or die $active_sth->errstr;
+    $num_active_cust_svc{$part_svc->svcpart} =
+      $active_sth->fetchrow_arrayref->[0];
+  }
+  @part_svc = sort { $num_active_cust_svc{$b->svcpart} <=>
+                     $num_active_cust_svc{$a->svcpart}     } @part_svc;
+}
+
 %>
 <%= header('Service Definition Listing', menubar( 'Main Menu' => $p) ) %>
 
@@ -45,6 +59,9 @@ function part_export_areyousure(href) {
   <TR>
     <TH COLSPAN=<%= $cgi->param('showdisabled') ? 2 : 3 %>>Service</TH>
     <TH>Table</TH>
+<% if ( $cgi->param('active') ) { %>
+    <TH><FONT SIZE=-1>Customer<BR>Services</FONT></TH>
+<% } %>
     <TH>Export</TH>
     <TH>Field</TH>
     <TH COLSPAN=2>Modifier</TH>
@@ -75,6 +92,11 @@ function part_export_areyousure(href) {
       <%= $hashref->{svc} %></A></TD>
     <TD ROWSPAN=<%= $rowspan %>>
       <%= $hashref->{svcdb} %></TD>
+<% if ( $cgi->param('active') ) { %>
+    <TD ROWSPAN=<%= $rowspan %>>
+      <FONT COLOR="#00CC00"><B><%= $num_active_cust_svc{$hashref->{svcpart}} %></B></FONT>&nbsp;<A HREF="<%=$p%>search/<%= $hashref->{svcdb} %>.cgi?svcpart=<%= $hashref->{svcpart} %>">active</A>
+    </TD>
+<% } %>
     <TD ROWSPAN=<%= $rowspan %>><%= itable() %>
 <%
 #  my @part_export =
index a9ec6fd..feee4ec 100644 (file)
@@ -18,7 +18,7 @@ my $p2 = popurl(2);
     <TD><B>Router name</B></TD>
     <TD><B>Address block(s)</B></TD>
   </TR>
-<% foreach $router (sort {$a->routernum <=> $b->routernum} @router) {
+<% foreach my $router (sort {$a->routernum <=> $b->routernum} @router) {
      my @addr_block = $router->addr_block;
 %>
   <TR>
index 8d35cb5..44cda81 100755 (executable)
@@ -1,4 +1,9 @@
 <!-- mason kludge -->
+<%
+  my $accounts_sth = dbh->prepare("SELECT COUNT(*) FROM svc_acct
+                                     WHERE popnum = ?           ")
+    or die dbh->errstr;
+%>
 <%= header('Access Number Listing', menubar( 'Main Menu' => $p )) %>
 Points of Presence<BR><BR>
 <A HREF="<%= $p %>edit/svc_acct_pop.cgi"><I>Add new Access Number</I></A><BR><BR>
@@ -10,6 +15,7 @@ Points of Presence<BR><BR>
         <TH>Area code</TH>
         <TH>Exchange</TH>
         <TH>Local</TH>
+        <TH>Accounts</TH>
       </TR>
 
 <%
@@ -18,32 +24,40 @@ foreach my $svc_acct_pop ( sort {
   $a->state cmp $b->state || $a->city cmp $b->city
     || $a->ac <=> $b->ac || $a->exch <=> $b->exch || $a->loc <=> $b->loc
 } qsearch('svc_acct_pop',{}) ) {
-  my($hashref)=$svc_acct_pop->hashref;
-  print <<END;
+
+  my $svc_acct_pop_link = $p . 'edit/svc_acct_pop.cgi?'. $svc_acct_pop->popnum;
+
+  $accounts_sth->execute($svc_acct_pop->popnum) or die $accounts_sth->errstr;
+  my $num_accounts = $accounts_sth->fetchrow_arrayref->[0];
+
+  my $svc_acct_link = $p. 'search/svc_acct.cgi?popnum='. $svc_acct_pop->popnum;
+
+%>
       <TR>
-        <TD><A HREF="${p}edit/svc_acct_pop.cgi?$hashref->{popnum}">
-          $hashref->{popnum}</A></TD>
-        <TD><A HREF="${p}edit/svc_acct_pop.cgi?$hashref->{popnum}">
-          $hashref->{city}</A></TD>
-        <TD><A HREF="${p}edit/svc_acct_pop.cgi?$hashref->{popnum}">
-          $hashref->{state}</A></TD>
-        <TD><A HREF="${p}edit/svc_acct_pop.cgi?$hashref->{popnum}">
-          $hashref->{ac}</A></TD>
-        <TD><A HREF="${p}edit/svc_acct_pop.cgi?$hashref->{popnum}">
-          $hashref->{exch}</A></TD>
-        <TD><A HREF="${p}edit/svc_acct_pop.cgi?$hashref->{popnum}">
-          $hashref->{loc}</A></TD>
+        <TD><A HREF="<%= $svc_acct_pop_link %>">
+          <%= $svc_acct_pop->popnum %></A></TD>
+        <TD><A HREF="<%= $svc_acct_pop_link %>">
+          <%= $svc_acct_pop->city %></A></TD>
+        <TD><A HREF="<%= $svc_acct_pop_link %>">
+          <%= $svc_acct_pop->state %></A></TD>
+        <TD><A HREF="<%= $svc_acct_pop_link %>">
+          <%= $svc_acct_pop->ac %></A></TD>
+        <TD><A HREF="<%= $svc_acct_pop_link %>">
+          <%= $svc_acct_pop->exch %></A></TD>
+        <TD><A HREF="<%= $svc_acct_pop_link %>">
+          <%= $svc_acct_pop->loc %></A></TD>
+        <TD>
+          <FONT COLOR="#00CC00"><B><%= $num_accounts %></B></FONT>
+            <% if ( $num_accounts ) { %><A HREF="<%= $svc_acct_link %>"><% } %>
+            active
+            <% if ( $num_accounts ) { %></A><% } %>
+        </TD>
       </TR>
-END
+<% } %>
 
-}
-
-print <<END;
       <TR>
       </TR>
     </TABLE>
   </BODY>
 </HTML>
-END
 
-%>
index c78a87f..1d6f8c4 100644 (file)
@@ -11,7 +11,7 @@
     <li>Credit card decline alerts: Customize the <a href="../config/config.cgi#declinetemplate">declinetemplate</a> configuration option and set the <a href="../config/config.cgi#emaildecline">emaildecline</a> configuration option.
     <li>Optional: Invoice template customization
       <ul>
-        <li>See the <a href="http://search.cpan.org/doc/MJD/Text-Template-1.42/Template.pm">Text::Template</a> documentation for details on the substitution language.
+        <li>See the <a href="http://search.cpan.org/~mjd/Text-Template/lib/Text/Template.pm">Text::Template</a> documentation for details on the substitution language.
         <li>You <b>must</b> call the invoice_lines() function at least once - pass it a number of lines, and it returns a list of array references, each of two elements: a service description column, and a price column.  Alternatively, call invoice_lines() with no arguments, and pagination will be disabled - all invoice line items will print on one page, with no padding (recommended for email invoices).
         <li>In addition, the following variables are available:
           <ul>
diff --git a/httemplate/docs/cvv2.html b/httemplate/docs/cvv2.html
new file mode 100644 (file)
index 0000000..fe8a17f
--- /dev/null
@@ -0,0 +1,25 @@
+<HTML>
+  <HEAD>
+    <TITLE>
+      CVV2 information
+    </TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+  The CVV2 number (also called CVC2 or CID) is a three- or four-digit
+  security code used to reduce credit card fraud.<BR><BR>
+  <TABLE BORDER=0 CELLSPACING=4>
+    <TR>
+      <TH>Visa / MasterCard / Discover</TH>
+      <TH>American Express</TH>
+    </TR>
+    <TR>
+      <TD>
+        <IMG BORDER=0 ALT="Visa/MasterCard/Discover" SRC="../images/cvv2.png">
+      </TD>
+      <TD>
+        <IMG BORDER=0 ALT="American Express" SRC="../images/cvv2_amex.png">
+      </TD>
+  </TABLE>
+    <CENTER><A HREF="javascript:close()">(close window)</A></CENTER>
+  </BODY>
+</HTML>
diff --git a/httemplate/docs/ieak.html b/httemplate/docs/ieak.html
new file mode 100644 (file)
index 0000000..00c5342
--- /dev/null
@@ -0,0 +1,75 @@
+<pre>
+this is incomplete
+mostly it should be merged into signup.html and fs_signup/ieak.template
+
+- download and install the IEAK from
+  http://www.microsoft.com/windows/ieak/default.asp
+
+- Good examples may be found in 
+  C:\Program Files\IEAK\toolkit\isp\server\ICW\signup\perl\signup08.pl
+  C:\Program Files\IEAK\toolkit\isp\server\ICW\reconfig\perl\reconfig04.pl
+  C:\Program Files\IEAK6\toolkit\isp\servless\basic\sample.ins
+  C:\Program Files\IEAK6\toolkit\isp\servless\advanced\4567.ins
+  C:\Program Files\IEAK6\toolkit\isp\servless\advanced\4568.ins
+  C:\Program Files\IEAK6\toolkit\isp\servless\advanced\7890.ins
+  C:\Program Files\IEAK6\toolkit\isp\servless\advanced\7891.ins
+
+- Full documentation on all the settings available in .INS files is
+  avaialble under Program Files | Microsoft IEAK 6 | IEAK Help 
+                  | Reference | Internet Settings (.ins) Files
+
+- Freeside will make the following substitutions before sending the file
+  to the user:
+
+  { $ac }         - area code of selected POP
+  { $exch }       - exchange of selected POP
+  { $loc }        - local part of selected POP
+  { $username }
+  { $password }
+  { $email_name } - first and last name
+  { $pkg }        - package name
+
+- Simple example follows:
+
+[Entry]
+Entry Name = IEAK Sample
+[Phone]
+Dial_As_Is = No
+Phone_Number = { $exch }{ $loc }
+Area_Code = { $ac }
+Country_Code = 1
+Country_Id = 1
+[Server]
+Type = PPP
+SW_Compress = Yes
+PW_Encrypt = Yes
+Negotiate_TCP/IP = Yes
+Disable_LCP = No
+[TCP/IP]
+Specity_IP_Address = No
+Specity_Server_Address = No
+IP_Header_Compress = Yes
+Gateway_On_Remote = Yes
+[User]
+Name = { $username }
+Passowrd = { $password }
+Display_Password = Yes
+[Internet_Mail]
+Email_Name = { $email_name }
+Email_Address = { $username }@example.com
+POP_Server = mail.example.com
+POP_Server_Port_Number = 110
+POP_Logon_Password = { $password }
+SMTP_Server = mail.example.com
+SMTP_Server_Port_Number = 25
+Install_Mail = 1
+[URL]
+Help_Page = http://www.ieaksample.net/helpdesk
+Home_Page = http://www.ieaksample.net
+Search_Page = http://www.ieaksample,net/search
+[Favorites]
+IEAK Sample \\ IEAK Sample Home Page.url = http://acme.ieaksample.net/
+[Branding]
+Window_Title = Internet Explorer from Acme Internet Services
+
+</pre>
index b57b06f..648cb98 100644 (file)
@@ -9,6 +9,7 @@
   <li><a href="upgrade7.html">Upgrading from 1.3.0 to 1.3.1</a>
   <li><a href="upgrade8.html">Upgrading from 1.3.1 to 1.4.0</a>
   <li><a href="upgrade9.html">Upgrading from 1.4.0 to 1.4.1</a>
+  <li><a href="upgrade-1.4.2.html">Upgrading from 1.4.1 to 1.4.2</a>
   <li><a href="upgrade10.html">Upgrading from 1.4.1 (or 1.4.2?) to 1.5.0</a>
 <!--
   <li><a href="config.html">Configuration files</a>
index 54614cc..ed306f2 100644 (file)
@@ -3,9 +3,10 @@
 </head>
 <body>
 <h1>Installation</h1>
+<i>Note: Install Freeside on a firewalled, private server, not a public (web, RADIUS, etc.) server.</i><br><br>
 Before installing, you need:
 <ul>
-  <li><a href="http://www.perl.com/">Perl</a>  Don't enable experimental features like threads or the PerlIO abstraction layer.
+  <li><a href="http://www.perl.com/">Perl</a>
   <li><a href="http://www.apache.org">Apache</a> (<a href="http://www.modssl.org/">mod_ssl</a> or <a href="http://www.apache-ssl.org">Apache-SSL</a> highly recommended)
   <li><a href="http://perl.apache.org/">mod_perl</a> (if compiling your own mod_perl, make sure you set the <a href="http://perl.apache.org/guide/install.html#EVERYTHING">EVERYTHING</a>=1 compile-time option)
   <li><a href="http://www.openssh.com/">SSH</a> (<a href="http://www.openssh.com//">OpenSSH</a> is recommended.  SSH Communications Security <a href="http://www.ssh.com/products/ssh/download.cfm">commercial SSH version 3</a> has been reported incompatible with Freeside.)
@@ -17,7 +18,7 @@ Before installing, you need:
 <!--       <li>MySQL has been reported to work. -->
          <b>MySQL's default <a href="http://www.mysql.com/doc/M/y/MyISAM.html">MyISAM</a> and <a href="http://www.mysql.com/doc/I/S/ISAM.html">ISAM</a> table types are not supported</b>.  If you want to use MySQL, you <b>must</b> use one of the new <a href="http://www.mysql.com/doc/T/a/Table_types.html">transaction-safe table types</a> such as <a href="http://www.mysql.com/doc/B/D/BDB.html">BDB</a> or <a href="http://www.mysql.com/doc/I/n/InnoDB.html">InnoDB</a>, and set it as the default table type using the <code>--default-table-type=BDB</code> or <code>--default-table-type=InnoDB</code> <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Command-line_options">mysqld command-line option</a> or by setting <code>default-table-type=BDB</code> or <code>default-table-type=InnoDB</code> in the <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Option_files">my.cnf option file</a>.
     </ul>
-  <li>Perl modules (<a href="http://theoryx5.uwinnipeg.ca/CPAN/perl/CPAN.html">CPAN</a> will query, download and build perl modules automatically)
+  <li>Perl modules (<a href="http://search.cpan.org/~andk/CPAN/lib/CPAN.pm">CPAN</a> will query, download and build perl modules automatically)
     <ul>
 <!--      <li><a href="http://search.cpan.org/search?dist=Array-PrintCols">Array-PrintCols</a>
       <li><a href="http://search.cpan.org/search?dist=Term-Query">Term-Query</a> (make test broken; install manually) -->
@@ -130,22 +131,23 @@ cp&nbsp;htetc/global.asa&nbsp;/usr/local/etc/freeside/asp-global/global.asa
 <font size="-1"><pre>
 PerlModule Apache::ASP
 &lt;Directory&nbsp;/usr/local/apache/htdocs/freeside-asp&gt;
-&lt;Files ~ (\.cgi)&gt;
-AddHandler perl-script .cgi
+&lt;Files ~ (\.cgi|\.html)&gt;
+SetHandler perl-script
 PerlHandler Apache::ASP
 &lt;/Files&gt;
 &lt;Perl&gt;
 $MLDBM::RemoveTaint = 1;
 &lt;/Perl&gt;
 PerlSetVar&nbsp;Global&nbsp;/usr/local/etc/freeside/asp-global/
-PerlSetVar Debug 2
+PerlSetVar&nbsp;Debug&nbsp;2
+PerlSetVar&nbsp;RequestBinaryRead&nbsp;Off
 &lt;/Directory&gt;
 </pre></font>
     </ul></td>
     <td><ul>
       <li>Run <tt>make masondocs</tt>
       <li>Copy <tt>masondocs/</tt> to your web server's document space. (For example: <tt>/usr/local/apache/htdocs/freeside-mason</tt>)
-      <li>Copy <tt>htetc/handler.pl</tt> to <tt>/usr/local/etc/freeside</tt> (use htetc/handler.pl-1.0x for Mason versions before 1.10).
+      <li>Copy <tt>htetc/handler.pl</tt> to <tt>/usr/local/etc/freeside</tt>
       <li>Edit <tt>handler.pl</tt> and:
       <ul>
         <li> set an appropriate <tt>comp_root</tt>, such as <tt>/usr/local/apache/htdocs/freeside-mason</tt>
@@ -156,8 +158,8 @@ PerlSetVar Debug 2
 <font size="-1"><pre>
 PerlModule HTML::Mason
 &lt;Directory&nbsp;/usr/local/apache/htdocs/freeside-mason&gt;
-&lt;Files ~ (\.cgi)&gt;
-AddHandler perl-script .cgi
+&lt;Files ~ (\.cgi|.html)&gt;
+SetHandler perl-script
 PerlHandler HTML::Mason
 &lt;/Files&gt;
 &lt;Perl&gt;
index 6787809..94efe53 100755 (executable)
@@ -3,7 +3,7 @@
 </head>
 <body>
   <h1>Importing legacy data</h1>
-<font size="+2">In most cases, legacy data import all cases will require writing custom code to deal with your particular legacy data.  The example scripts here will not work "out-of-the-box".  Importing your legacy data will most probably involve some hacking on the example scripts noted below.  Contributions to the import process are welcome.</font>
+<font size="+2">In almost all cases, legacy data import will require writing custom code to deal with your particular legacy data.  The example scripts here will probably <b>not</b> work "out-of-the-box", and are provided <b>as a starting point only</b>.</font>
 <br><br><i>Some import scripts may require installation of the <a href="http://search.cpan.org/search?dist=Array-PrintCols">Array-PrintCols</a> and <a href="http://search.cpan.org/search?dist=Term-Query">Term-Query</a> (make test broken; install manually) modules.</i><br>
 <ul>
   <li><a name="bind">bin/bind.import</a> - Import domain information from BIND named
index a59755e..b380317 100644 (file)
@@ -12,6 +12,9 @@
         <li>typenum - <a href="#agent_type">agent type</a>
         <li>prog - (unimplemented)
         <li>freq - (unimplemented)
+        <li>disabled - Disabled flag, empty or 'Y'
+        <li>username - Username for the Agent interface
+        <li>_password - Password for the Agent interface
       </ul>
     <li><a name="agent_type" href="man/FS/agent_type.html">agent_type</a> - Agent types define groups of packages that you can then assign to particular agents.
       <ul>
         <li><i>ship_fax</i>
         <li>payby - CARD, DCHK, CHEK, DCHK, LECB, BILL, or COMP
         <li>payinfo - card number, P.O.#, or comp issuer
+        <li>paycvv - Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
         <li>paydate - expiration date
         <li>payname - billing name (name on card)
         <li>tax - tax exempt, Y or null
         <li>taxclass
         <li>exempt_amount
         <li>taxname - if defined, printed on invoices instead of "Tax"
+        <li>setuptax - if 'Y', this tax does not apply to setup fees
+        <li>recurtax - if 'Y', this tax does not apply to recurring fees
       </ul>
     <li><a name="cust_tax_exempt" href="man/FS/cust_tax_exempt.html">cust_tax_exempt</a> - Tax exemption record
       <ul>
         <li>pkgpart - <a href="#part_pkg">Package definition</a>
         <li>svcpart - <a href="#part_svc">Service definition</a>
         <li>quantity - quantity of this service that this package includes
+        <li>primary_svc - blank or Y: primary service
       </ul>
     <li><a name="export_svc" href="man/FS/export_svc.html">export_svc</a>
       <ul>
       <ul>
         <li>svcnum - <a href="#cust_svc">primary key</a>
         <li>srcsvc - <a href="#svc_acct">svcnum of the source of this forward</a>
+        <li>src - literal source (username or full email address)
         <li>dstsvc - <a href="#svc_acct">svcnum of the destination of this forward</a>
-        <li>dst - foreign destination (email address) - forward not local to freeside
+        <li>dst - literal destination (username or full email address)
       </ul>
     <li><a name="domain_record" href="man/FS/domain_record.html">domain_record</a> - Domain zone detail
       <ul>
index 5168f47..97d7aa7 100644 (file)
@@ -12,7 +12,7 @@ webserver.  On this machine, install:
   <li><a href="http://search.cpan.org/search?dist=Text-Template">Text::Template</a>
   <li><a href="http://search.cpan.org/search?dist=Storable">Storable</a>
   <li><a href="http://search.cpan.org/search?dist=Business-CreditCard">Business-CreditCard</a>
-  <li><a href="http://www.sisd.com/useragent">HTTP::Headers::UserAgent</a> (version 2.0 or higher; not yet indexed correctly on CPAN)
+  <li><a href="http://search.cpan.org/search?dist=HTTP-BrowserDetect">HTTP::BrowserDetect</a>
 
   <li><a href="man/FS/SignupClient.html">FS::SignupClient</a> (copy the fs_signup/FS-SignupClient directory to the external machine, then: perl Makefile.PL; make; make install)
 </ul>
@@ -36,9 +36,7 @@ Then:
 Optional:
 <ul>
   <li>If you create a <b>/usr/local/freeside/ieak.template</b> file on the external machine, it will be sent to IE users with MIME type <i>application/x-Internet-signup</i>.  This file will be processed with <a href="http://search.cpan.org/doc/MJD/Text-Template-1.23/Template.pm">Text::Template</a> with the variables listed below available.
-  (an example file is included as <b>fs_signup/ieak.template</b>)  See the <a href="http://www.microsoft.com/windows/ieak/techinfo/deploy/60/en/toc.asp">IEAK documentation</a> for more information.
-  <li>If you create a <b>/usr/local/freeside/cck.template</b> file on the external machine, the variables defined will be sent to Netscape users with MIME type <i>application/x-netscape-autoconfigure-dialer-v2</i>.  This file will be processed with <a href="http://search.cpan.org/doc/MJD/Text-Template-1.23/Template.pm">Text::Template</a> with the variables listed below available.
-  (an example file is included as <b>fs_signup/cck.template</b>).  See the <a href="http://help.netscape.com/products/client/mc/acctproc4.html">Netscape documentation</a> for more information.
+  (an example file is included as <b>fs_signup/ieak.template</b>)  See the section on <a href="http://www.microsoft.com/windows/ieak/techinfo/deploy/60/en/INS.HTM">internet settings files</a> in the <a href="http://www.microsoft.com/windows/ieak/techinfo/deploy/60/en/toc.asp">IEAK documentation</a> for more information.
   <li>If you create a <b>/usr/local/freeside/success.html</b> file on the external machine, it will be used as the success HTML page.  Although template substiutions are available, a regular HTML file will work fine here, unlike signup.html.  An example file is included as <b>fs_signup/FS-SignupClient/cgi/success.html</b>
   <li>Variable substitutions available in <b>ieak.template</b>, <b>cck.template</b> and <b>success.html</b>:
     <ul>
diff --git a/httemplate/docs/upgrade-1.4.2.html b/httemplate/docs/upgrade-1.4.2.html
new file mode 100644 (file)
index 0000000..1feaa80
--- /dev/null
@@ -0,0 +1,20 @@
+<head>
+  <title>Upgrading to 1.4.2</title>
+</head>
+<body>
+<h1>Upgrading to 1.4.2 from 1.4.1</h1>
+<ul>
+  <li>If migrating from less than 1.4.1, see these <a href="upgrade9.html">instructions</a> first.
+  <li>Back  up your data and current Freeside installation.
+  <li>Install <a href="http://search.cpan.org/search?dist=Locale-SubCountry">Locale::SubCountry</a>
+  <li>Install <a href="http://search.cpan.org/search?dist=IPC-ShareLite">IPC::ShareLite</a>
+  <li>Install <a href="http://search.cpan.org/search?dist=HTML-Widgets-SelectLayers">HTML::Widgets::SelectLayers</a> 0.03.
+  <li>CGI.pm minimum version 2.47 is required.  You will probably need to install a current CGI.pm from CPAN if you are using Perl 5.005 or earlier.
+  <li>If using Apache::ASP, add <code>PerlSetVar RequestBinaryRead Off</code> to your Apache configuration and make sure you are using Apache::ASP minimum version 2.55.
+  <li>Run <code>make aspdocs</code> or <code>make masondocs</code>.
+  <li>Copy <code>aspdocs/</code> or <code>masondocs/</code> to your web server's document space.
+  <li>Run <code>make install-perl-modules</code>.
+  <li>The signup server and password server are deprecated in 1.4.2.  Their functionality has been incorperated into the self-service server.  Edit or reinstall your init script, and set the "signup_server-default_agentnum" and "signup_server-default_refnum" configuration options.  The FS::SignupClient interface is still available as a compatibility wrapper, so you should be able to continue to use your current signup.cgi.
+  <li>Optional: To use typeset invoices, install tetex and ghostscript, and copy conf/invoice_latex, conf/invoice_latexnotes, and conf/invoice_latexfooter to /usr/local/etc/freeside/conf.<datasrc>/
+  <li>Restart Apache and freeside-queued.
+</body>
index 4c2c17b..774babb 100644 (file)
@@ -1,9 +1,9 @@
 <pre>
 this is incomplete
 
-install DBIx::DBSchema 0.21
+install DBIx::DBSchema 0.22
 
-install NetAddr::IP and Chart::Base
+install NetAddr::IP, Chart::Base, IPC::ShareLite and Locale::SubCountry
 
 CREATE TABLE cust_bill_pkg_detail (
   detailnum serial,
@@ -62,14 +62,112 @@ CREATE TABLE svc_broadband (
   PRIMARY KEY (svcnum)
 );
 
-DELETE INDEX cust_bill_pkg1;
+CREATE TABLE acct_snarf (
+  snarfnum serial,
+  svcnum int NOT NULL,
+  machine varchar(255) NULL,
+  protocol varchar(80) NULL,
+  username varchar(80) NULL,
+  _password varchar(80) NULL,
+  PRIMARY KEY (snarfnum)
+);
+CREATE INDEX acct_snarf1 ON acct_snarf ( svcnum );
+
+CREATE TABLE svc_external (
+  svcnum int NOT NULL,
+  id int NOT NULL,
+  title varchar(80),
+  PRIMARY KEY (svcnum)
+);
+
+CREATE TABLE part_pkg_temp (
+    pkgpart serial NOT NULL,
+    pkg varchar(80) NOT NULL,
+    "comment" varchar(80) NOT NULL,
+    setup text NULL,
+    freq varchar(80) NOT NULL,
+    recur text NULL,
+    setuptax char(1) NULL,
+    recurtax char(1) NULL,
+    plan varchar(80) NULL,
+    plandata text NULL,
+    disabled char(1) NULL,
+    taxclass varchar(80) NULL,
+    PRIMARY KEY (pkgpart),
+);
+INSERT INTO part_pkg_temp SELECT * from part_pkg;
+DROP TABLE part_pkg;
+ALTER TABLE part_pkg_temp RENAME TO part_pkg;
+ALTER TABLE part_pkg DROP CONSTRAINT part_pkg_temp_pkey;
+ALTER TABLE part_pkg ADD PRIMARY KEY (pkgpart);
+CREATE INDEX part_pkg1 ON part_pkg(disabled);
+select setval('public.part_pkg_temp_pkgpart_seq', ( select max(pkgpart) from part_pkg) ); #?
+
+CREATE TABLE h_part_pkg_temp (
+    historynum serial NOT NULL,
+    history_date int,
+    history_user varchar(80) NOT NULL,
+    history_action varchar(80) NOT NULL,
+    pkgpart int NOT NULL,
+    pkg varchar(80) NOT NULL,
+    "comment" varchar(80) NOT NULL,
+    setup text NULL,
+    freq varchar(80) NOT NULL,
+    recur text NULL,
+    setuptax char(1) NULL,
+    recurtax char(1) NULL,
+    plan varchar(80) NULL,
+    plandata text NULL,
+    disabled char(1) NULL,
+    taxclass varchar(80) NULL,
+    PRIMARY KEY (historynum)
+);
+INSERT INTO h_part_pkg_temp SELECT * from h_part_pkg;
+DROP TABLE h_part_pkg;
+ALTER TABLE h_part_pkg_temp RENAME TO h_part_pkg;
+ALTER TABLE h_part_pkg DROP CONSTRAINT h_part_pkg_temp_pkey;
+ALTER TABLE h_part_pkg ADD PRIMARY KEY (historynum);
+CREATE INDEX h_part_pkg1 ON h_part_pkg(disabled);
+select setval('public.h_part_pkg_temp_historynum_seq', ( select max(historynum) from h_part_pkg) );
+
+
+DROP INDEX cust_bill_pkg1;
 
 ALTER TABLE cust_bill_pkg ADD itemdesc varchar(80) NULL;
 ALTER TABLE h_cust_bill_pkg ADD itemdesc varchar(80) NULL;
 ALTER TABLE cust_main_county ADD taxname varchar(80) NULL;
 ALTER TABLE h_cust_main_county ADD taxname varchar(80) NULL;
+ALTER TABLE cust_main_county ADD setuptax char(1) NULL;
+ALTER TABLE h_cust_main_county ADD setuptax char(1) NULL;
+ALTER TABLE cust_main_county ADD recurtax char(1) NULL;
+ALTER TABLE h_cust_main_county ADD recurtax char(1) NULL;
 ALTER TABLE cust_pkg ADD last_bill int NULL;
 ALTER TABLE h_cust_pkg ADD last_bill int NULL;
+ALTER TABLE agent ADD disabled char(1) NULL;
+ALTER TABLE h_agent ADD disabled char(1) NULL;
+ALTER TABLE agent ADD username varchar(80) NULL;
+ALTER TABLE h_agent ADD username varchar(80) NULL;
+ALTER TABLE agent ADD _password varchar(80) NULL;
+ALTER TABLE h_agent ADD _password varchar(80) NULL;
+ALTER TABLE cust_main ADD paycvv varchar(4) NULL;
+ALTER TABLE h_cust_main ADD paycvv varchar(4) NULL;
+ALTER TABLE part_referral ADD disabled char(1) NULL;
+ALTER TABLE h_part_referral ADD disabled char(1) NULL;
+CREATE INDEX part_referral1 ON part_referral ( disabled );
+ALTER TABLE pkg_svc ADD primary_svc char(1) NULL;
+ALTER TABLE h_pkg_svc ADD primary_svc char(1) NULL;
+ALTER TABLE svc_forward ADD src varchar(255) NULL;
+ALTER TABLE h_svc_forward ADD src varchar(255) NULL;
+
+On recent Pg versions:
+
+ALTER TABLE svc_forward ALTER COLUMN srcsvc DROP NOT NULL;
+ALTER TABLE h_svc_forward ALTER COLUMN srcsvc DROP NOT NULL;
+ALTER TABLE svc_forward ALTER COLUMN dstsvc DROP NOT NULL;
+ALTER TABLE h_svc_forward ALTER COLUMN dstsvc DROP NOT NULL;
+
+Or on Pg versions that don't support DROP NOT NULL (tested only on 7.2 so far):
+UPDATE pg_attribute SET attnotnull = FALSE WHERE ( attname = 'srcsvc' OR attname = 'dstsvc' ) AND ( attrelid = ( SELECT oid FROM pg_class WHERE relname = 'svc_forward' ) OR attrelid = ( SELECT oid FROM pg_class WHERE relname = 'h_svc_forward' ) );
 
 dump database, edit:
 - cust_main: increase otaker from 8 to 32
@@ -90,15 +188,17 @@ optionally:
   CREATE INDEX cust_main9 ON cust_main ( ship_daytime );
   CREATE INDEX cust_main10 ON cust_main ( ship_night );
   CREATE INDEX cust_main11 ON cust_main ( ship_fax );
+  CREATE INDEX agent2 ON agent ( disabled );
+  CREATE INDEX part_bill_event2 ON part_bill_event ( disabled );
 
   serial columns
 
 mandatory again:
 
 dbdef-create username
-create-history-tables username cust_bill_pkg_detail router part_svc_router addr_block svc_broadband
+create-history-tables username cust_bill_pkg_detail router part_svc_router addr_block svc_broadband acct_snarf svc_external
 dbdef-create username
 
-
+apache - fix <Files> sections to include .html also
 
 </pre>
index 24d1cce..6a8fd96 100644 (file)
@@ -21,6 +21,8 @@ CREATE INDEX part_pkg1 ON part_pkg ( disabled );
 CREATE INDEX part_svc1 ON part_svc ( disabled );
 CREATE INDEX cust_bill2 ON cust_bill ( _date );
 </pre>
-  <li>If you want to use ACH (electronic checks), you will need to make changes to your database.  The easiest way to make these changes is to dump your database (with pg_dump), change the payinfo field in the cust_pay, cust_refund, h_cust_pay and h_cust_refund tables from varchar(16) to varchar(80), reload the database from the dump, and run dbdef-create
+  <li>If you want to use ACH (electronic checks), you will need to make changes to your database.  The easiest way to make these changes is to dump your database (with pg_dump), change the payinfo field in the cust_pay, cust_refund, h_cust_pay and h_cust_refund tables from varchar(16) to varchar(80), reload the database from the dump.
+  <li>If you will be doing bind exports you should make additional changes to your database. Follow the directions above to dump the database and change the reczone and recdata fields in the domain_record and h_domain_record tables from varchar(80) to varchar(255).
+  <li>If you made changes to your db schema from a dump as listed above run dbdef-create.
   <li>Restart Apache and freeside-queued.
 </body>
index e44acba..d9b7579 100755 (executable)
@@ -1,6 +1,6 @@
 <!-- mason kludge -->
 <%
-# <!-- $Id: REAL_cust_pkg.cgi,v 1.5 2003-04-01 01:22:24 ivan Exp $ -->
+# <!-- $Id: REAL_cust_pkg.cgi,v 1.7 2003-11-19 12:21:09 ivan Exp $ -->
 
 my $error ='';
 my $pkgnum = '';
@@ -30,6 +30,15 @@ print header('Package Edit'); #, menubar(
 #  'Main Menu' => popurl(2)
 #));
 
+%>
+
+    <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
+
+<%
+
 #print info
 my($susp,$cancel,$expire)=(
   $cust_pkg->getfield('susp'),
@@ -45,6 +54,9 @@ print '<FORM NAME="formname" ACTION="process/REAL_cust_pkg.cgi" METHOD="POST">',
 print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: $error</FONT>!
   if $error;
 
+#my $format = "%c %z (%Z)";
+my $format = "%m/%d/%Y %T %z (%Z)";
+
 print ntable("#cccccc",2),
       '<TR><TD ALIGN="right">Package number</TD><TD BGCOLOR="#ffffff">',
       $pkgnum, '</TD></TR>',
@@ -55,23 +67,29 @@ print ntable("#cccccc",2),
       '<TR><TD ALIGN="right">Order taker</TD><TD BGCOLOR="#ffffff">',
       $otaker,  '</TD></TR>',
       '<TR><TD ALIGN="right">Setup date</TD><TD>'.
-      '<INPUT TYPE="text" NAME="setup" SIZE=32 VALUE="',
-      ( $setup ? time2str("%c %z (%Z)",$setup) : "" ), '"></TD></TR>';
+      '<INPUT TYPE="text" NAME="setup" SIZE=32 ID="setup_text" VALUE="',
+      ( $setup ? time2str($format, $setup) : "" ), '">'.
+      ' <IMG SRC="../images/calendar.png" ID="setup_button" STYLE="cursor: pointer" TITLE="Select date">'.
+      '</TD></TR>';
 
 print '<TR><TD ALIGN="right">Last bill date</TD><TD>',
-      '<INPUT TYPE="text" NAME="last_bill" SIZE=32 VALUE="',
+      '<INPUT TYPE="text" NAME="last_bill" SIZE=32 ID="last_bill_text" VALUE="',
       ( $cust_pkg->last_bill
-        ? time2str("%c %z (%Z)", $cust_pkg->last_bill)
+        ? time2str($format, $cust_pkg->last_bill)
         : ""                                          ),
-      '"></TD></TR>'
+      '">'.
+      ' <IMG SRC="../images/calendar.png" ID="last_bill_button" STYLE="cursor: pointer" TITLE="Select date">'.
+      '</TD></TR>'
   if $cust_pkg->dbdef_table->column('last_bill');
 
 print '<TR><TD ALIGN="right">Next bill date</TD><TD>',
-      '<INPUT TYPE="text" NAME="bill" SIZE=32 VALUE="',
-      ( $bill ? time2str("%c %z (%Z)",$bill) : "" ), '"></TD></TR>';
+      '<INPUT TYPE="text" NAME="bill" SIZE=32 ID="bill_text" VALUE="',
+      ( $bill ? time2str($format, $bill) : "" ), '">'.
+      ' <IMG SRC="../images/calendar.png" ID="bill_button" STYLE="cursor: pointer" TITLE="Select date">'.
+      '</TD></TR>';
 
 print '<TR><TD ALIGN="right">Suspension date</TD><TD BGCOLOR="#ffffff">',
-       time2str("%D",$susp), '</TD></TR>'
+       time2str($format, $susp), '</TD></TR>'
   if $susp;
 
 #print '<TR><TD ALIGN="right">Expiration date</TD><TD BGCOLOR="#ffffff">',
@@ -79,18 +97,34 @@ print '<TR><TD ALIGN="right">Suspension date</TD><TD BGCOLOR="#ffffff">',
 #  if $expire;
 print '<TR><TD ALIGN="right">Expiration date'.
       '</TD><TD>',
-      '<INPUT TYPE="text" NAME="expire" SIZE=32 VALUE="',
-      ( $expire ? time2str("%c %z (%Z)",$expire) : "" ), '">'.
+      '<INPUT TYPE="text" NAME="expire" SIZE=32 ID="expire_text" VALUE="',
+      ( $expire ? time2str($format, $expire) : "" ), '">'.
+      ' <IMG SRC="../images/calendar.png" ID="expire_button" STYLE="cursor: pointer" TITLE="Select date">'.
       '<BR><FONT SIZE=-1>(will <b>cancel</b> this package'.
       ' when the date is reached)</FONT>'.
       '</TD></TR>';
 
 print '<TR><TD ALIGN="right">Cancellation date</TD><TD BGCOLOR="#ffffff">',
-       time2str("%D",$cancel), '</TD></TR>'
+       time2str($format, $cancel), '</TD></TR>'
   if $cancel;
 
 %>
 </TABLE>
+<SCRIPT TYPE="text/javascript">
+<%
+  my @cal = qw( setup bill expire );
+  push @cal, 'last_bill'
+    if $cust_pkg->dbdef_table->column('last_bill');
+  foreach my $cal (@cal) {
+%>
+  Calendar.setup({
+    inputField: "<%= $cal %>_text",
+    ifFormat:   "%m/%d/%Y",
+    button:     "<%= $cal %>_button",
+    align:      "BR"
+  });
+<% } %>
+</SCRIPT>
 <BR><INPUT TYPE="submit" VALUE="Apply Changes">
 </FORM>
 </BODY>
index 449456c..8a1cb2a 100755 (executable)
@@ -16,59 +16,64 @@ if ( $cgi->param('error') ) {
 my $action = $agent->agentnum ? 'Edit' : 'Add';
 my $hashref = $agent->hashref;
 
-print header("$action Agent", menubar(
+%>
+
+<%= header("$action Agent", menubar(
   'Main Menu' => $p,
   'View all agents' => $p. 'browse/agent.cgi',
-));
+)) %>
 
-print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
-      "</FONT>"
-  if $cgi->param('error');
+<% if ( $cgi->param('error') ) { %>
+<FONT SIZE="+1" COLOR="#ff0000">Error: <%= $cgi->param('error') %></FONT>
+<% } %>
 
-print '<FORM ACTION="', popurl(1), 'process/agent.cgi" METHOD=POST>',
-      qq!<INPUT TYPE="hidden" NAME="agentnum" VALUE="$hashref->{agentnum}">!,
-      "Agent #", $hashref->{agentnum} ? $hashref->{agentnum} : "(NEW)";
+<FORM ACTION="<%=popurl(1)%>process/agent.cgi" METHOD=POST>
+<INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $hashref->{agentnum} %>">
+Agent #<%= $hashref->{agentnum} ? $hashref->{agentnum} : "(NEW)" %>
 
-print &ntable("#cccccc", 2, ''), <<END;
+<%= &ntable("#cccccc", 2, '') %>
 <TR>
   <TH ALIGN="right">Agent</TH>
-  <TD><INPUT TYPE="text" NAME="agent" SIZE=32 VALUE="$hashref->{agent}"></TD>
+  <TD><INPUT TYPE="text" NAME="agent" SIZE=32 VALUE="<%= $hashref->{agent} %>"></TD>
 </TR>
 <TR>
   <TH ALIGN="right">Agent type</TH>
   <TD><SELECT NAME="typenum" SIZE=1>
-END
 
-foreach my $agent_type (qsearch('agent_type',{})) {
-  print "<OPTION VALUE=". $agent_type->typenum;
-  print " SELECTED"
-    if $hashref->{typenum} && ( $hashref->{typenum} == $agent_type->typenum );
-  print ">", $agent_type->getfield('typenum'), ": ",
-        $agent_type->getfield('atype'),"\n";
-}
+<% foreach my $agent_type (qsearch('agent_type',{})) { %>
+  <OPTION VALUE="<%= $agent_type->typenum %>"<%= ( $hashref->{typenum} && ( $hashref->{typenum} == $agent_type->typenum ) ) ? ' SELECTED' : '' %>>
+  <%= $agent_type->getfield('typenum') %>: <%= $agent_type->getfield('atype') %>
+<% } %>
 
-print <<END;
 </SELECT></TD>
 </TR>
+<% if ( dbdef->table('agent')->column('disabled') ) { %>
+  <TR>
+    <TD ALIGN="right">Disable</TD>
+    <TD><INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<%= $hashref->{disabled} eq 'Y' ? ' CHECKED' : '' %>></TD>
+  </TR>
+<% } %>
 <TR>
   <TD ALIGN="right"><!--Frequency--></TD>
-  <TD><INPUT TYPE="hidden" NAME="freq" VALUE="$hashref->{freq}"></TD>
+  <TD><INPUT TYPE="hidden" NAME="freq" VALUE="<%= $hashref->{freq} %>"></TD>
 </TR>
 <TR>
   <TD ALIGN="right"><!--Program--></TD>
-  <TD><INPUT TYPE="hidden" NAME="prog" VALUE="$hashref->{prog}"></TD>
+  <TD><INPUT TYPE="hidden" NAME="prog" VALUE="<%= $hashref->{prog} %>"></TD>
 </TR>
+<% if ( dbdef->table('agent')->column('username') ) { %>
+  <TR>
+    <TD ALIGN="right">Agent interface username</TD>
+    <TD><INPUT TYPE="text" NAME="username" VALUE="<%= $hashref->{username} %>"></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Agent interface password</TD>
+    <TD><INPUT TYPE="text" NAME="_password" VALUE="<%= $hashref->{_password} %>"></TD>
+  </TR>
+<% } %>
 </TABLE>
-END
 
-print qq!<BR><INPUT TYPE="submit" VALUE="!,
-      $hashref->{agentnum} ? "Apply changes" : "Add agent",
-      qq!">!;
-
-print <<END;
+<BR><INPUT TYPE="submit" VALUE="<%= $hashref->{agentnum} ? "Apply changes" : "Add agent" %>">
     </FORM>
   </BODY>
 </HTML>
-END
-
-%>
index eeceed8..857e769 100755 (executable)
@@ -39,6 +39,12 @@ if ( $cgi->param('error') ) {
   $query =~ /^(\d+)$/;
   $custnum=$1;
   $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+  if ( $cust_main->dbdef_table->column('paycvv')
+       && length($cust_main->paycvv)             ) {
+    my $paycvv = $cust_main->paycvv;
+    $paycvv =~ s/./*/g;
+    $cust_main->paycvv($paycvv);
+  }
   $saved_pkgpart = 0;
   $username = '';
   $password = '';
@@ -61,7 +67,7 @@ my $action = $custnum ? 'Edit' : 'Add';
 # top
 
 my $p1 = popurl(1);
-print header("Customer $action", '');
+print header("Customer $action", '', ' onUnload="myclose()"');
 print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $error, "</FONT>"
   if $error;
 
@@ -75,9 +81,11 @@ print qq!<FORM ACTION="${p1}process/cust_main.cgi" METHOD=POST NAME="form1">!,
 
 my $r = qq!<font color="#ff0000">*</font>&nbsp;!;
 
-my @agents = qsearch( 'agent', {} );
+my %agent_search = dbdef->table('agent')->column('disabled')
+                     ? ( 'disabled' => '' ) : ();
+my @agents = qsearch( 'agent', \%agent_search );
 #die "No agents created!" unless @agents;
-eidiot "You have not created any agents.  You must create at least one agent before adding a customer.  Go to ". popurl(2). "browse/agent.cgi and create one or more agents." unless @agents;
+eidiot "You have not created any agents (or all agents are disabled).  You must create at least one agent before adding a customer.  Go to ". popurl(2). "browse/agent.cgi and create one or more agents." unless @agents;
 my $agentnum = $cust_main->agentnum || $agents[0]->agentnum; #default to first
 if ( scalar(@agents) == 1 ) {
   print qq!<INPUT TYPE="hidden" NAME="agentnum" VALUE="$agentnum">!;
@@ -183,8 +191,10 @@ END
 my $countrydefault = $conf->config('countrydefault') || 'US';
 $cust_main->country( $countrydefault ) unless $cust_main->country;
 
-$cust_main->state( $conf->config('statedefault') || 'CA' )
-  unless $cust_main->state || $cust_main->country ne 'US';
+my $statedefault = $conf->config('statedefault')
+                   || ($countrydefault eq 'US' ? 'CA' : '');
+$cust_main->state( $statedefault )
+  unless $cust_main->state || $cust_main->country ne $countrydefault;
 
 my($county_html, $state_html, $country_html) =
   FS::cust_main_county::regionselector( $cust_main->county,
@@ -281,8 +291,9 @@ END
   #false laziness with regular state
   $cust_main->ship_country( $countrydefault ) unless $cust_main->ship_country;
 
-  $cust_main->ship_state( $conf->config('statedefault') || 'CA' )
-    unless $cust_main->ship_state || $cust_main->ship_country ne 'US';
+  $cust_main->ship_state( $statedefault )
+    unless $cust_main->ship_state
+           || $cust_main->ship_country ne $countrydefault;
 
   my($ship_county_html, $ship_state_html, $ship_country_html) =
     FS::cust_main_county::regionselector( $cust_main->ship_county,
@@ -335,7 +346,9 @@ sub expselect {
     $return .= ">$_";
   }
   $return .= qq!</SELECT>/<SELECT NAME="$prefix!. qq!_year" SIZE="1">!;
-  for ( 2001 .. 2037 ) {
+  my @t = localtime;
+  my $thisYear = $t[5] + 1900;
+  for ( ($thisYear > $y && $y > 0 ? $y : $thisYear) .. 2037 ) {
     $return .= "<OPTION";
     $return .= " SELECTED" if $_ == $y;
     $return .= ">$_";
@@ -398,7 +411,19 @@ if ( $payby_default eq 'HIDE' ) {
   print qq!<TR><TD>Email invoice <INPUT TYPE="text" NAME="invoicing_list" VALUE="$invoicing_list"></TD></TR>!;
 
   print "<TR><TD>Billing type</TD></TR>",
-        "</TABLE>",
+        "</TABLE>", '<script language="JavaScript"><!--
+                       var mywindow = -1;
+                       function myopen(filename,windowname,properties) {
+                         myclose();
+                         mywindow = window.open(filename,windowname,properties);
+                       }
+                       function myclose() {
+                         if ( mywindow != -1 )
+                           mywindow.close();
+                         mywindow = -1;
+                       }
+
+                     //--></script>',
         &table("#cccccc"), "<TR>";
 
   my($payinfo, $payname)=(
@@ -416,6 +441,12 @@ if ( $payby_default eq 'HIDE' ) {
     'COMP' => qq!Complimentary<BR>${r}Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE=""><BR>${r}Exp !. expselect("COMP"),
 );
 
+  if ( $cust_main->dbdef_table->column('paycvv') ) {
+    foreach my $payby ( grep { exists $payby{$_} } qw(CARD DCRD) ) { #1.4/1.5 bs
+      $payby{$payby} .= qq!<BR>CVV2&nbsp;(<A HREF="javascript:myopen('../docs/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>)&nbsp;<INPUT TYPE="text" NAME=${payby}_paycvv VALUE="" SIZE=4 MAXLENGTH=4>!;
+    }
+  }
+
   my( $account, $aba ) = split('@', $payinfo);
 
   my %paybychecked = (
@@ -428,6 +459,15 @@ if ( $payby_default eq 'HIDE' ) {
     'COMP' => qq!Complimentary<BR>${r}Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE="$payinfo"><BR>${r}Exp !. expselect("COMP", $cust_main->paydate),
 );
 
+  if ( $cust_main->dbdef_table->column('paycvv') ) {
+    my $paycvv = $cust_main->paycvv;
+
+    foreach my $payby ( grep { exists $payby{$_} } qw(CARD DCRD) ) { #1.4/1.5 bs
+      $paybychecked{$payby} .= qq!<BR>CVV2&nbsp;(<A HREF="javascript:myopen('../docs/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>)&nbsp;<INPUT TYPE="text" NAME=${payby}_paycvv VALUE="$paycvv" SIZE=4 MAXLENGTH=4>!;
+    }
+  }
+
+
   $cust_main->payby($payby_default) unless $cust_main->payby;
   for (qw(CARD DCRD CHEK DCHK LECB BILL COMP)) {
     print qq!<TD VALIGN=TOP><INPUT TYPE="radio" NAME="payby" VALUE="$_"!;
index f3d2882..4bcfcbe 100755 (executable)
@@ -16,12 +16,26 @@ print qq!<FORM ACTION="!, popurl(1),
         <TH><FONT SIZE=-1>State</FONT></TH>
         <TH><FONT SIZE=-1>County</FONT></TH>
         <TH><FONT SIZE=-1>Taxclass</FONT><BR><FONT SIZE=-2>(per-package classification)</FONT></TH>
-        <TH><FONT SIZE=-1>Tax name</FONT><BR><FONT SIZE=-2>(printed on invoices)</FONT></TH>
+END
+
+if ( dbdef->table('cust_main_county')->column('taxname') ) {
+  print '<TH><FONT SIZE=-1>Tax name</FONT><BR><FONT SIZE=-2>(printed on invoices)</FONT></TH>';
+}
+
+print <<END;
         <TH><FONT SIZE=-1>Tax</FONT></TH>
         <TH><FONT SIZE=-1>Exempt<BR>per<BR>month</TH>
-      </TR>
 END
 
+if ( dbdef->table('cust_main_county')->column('setuptax') ) {
+  print '<TH><FONT SIZE=-1>Setup<BR>fee<BR>exempt</TH>';
+}
+if ( dbdef->table('cust_main_county')->column('recurtax') ) {
+  print '<TH><FONT SIZE=-1>Recurring<BR>fee<BR>exempt</TH>';
+}
+
+print '</TR>';
+
 foreach my $cust_main_county ( sort {    $a->country cmp $b->country
                                       or $a->state   cmp $b->state
                                       or $a->county  cmp $b->county
@@ -48,11 +62,26 @@ END
     , "</TD>";
 
   print qq!<TD><INPUT TYPE="text" NAME="taxname!, $hashref->{taxnum},
-        qq!" VALUE="!, $hashref->{taxname}, qq!"></TD>!;
-  print qq!<TD><INPUT TYPE="text" NAME="tax!, $hashref->{taxnum},
-        qq!" VALUE="!, $hashref->{tax}, qq!" SIZE=6 MAXLENGTH=6>%</TD>!;
-  print qq!<TD>\$<INPUT TYPE="text" NAME="exempt_amount!, $hashref->{taxnum},
-        qq!" VALUE="!, $hashref->{exempt_amount}||0, qq!" SIZE=6></TD>!;
+        qq!" VALUE="!, $hashref->{taxname}, qq!"></TD>!
+    if dbdef->table('cust_main_county')->column('taxname');
+
+  print qq!<TD><TABLE><TR><TD><INPUT TYPE="text" NAME="tax!, $hashref->{taxnum},
+        qq!" VALUE="!, $hashref->{tax}, qq!" SIZE=6 MAXLENGTH=6></TD><TD>%</TD></TR></TABLE></TD>!;
+  print qq!<TD><TABLE><TR><TD>\$</TD><TD><INPUT TYPE="text" NAME="exempt_amount!, $hashref->{taxnum},
+        qq!" VALUE="!, $hashref->{exempt_amount}||0, qq!" SIZE=6></TD></TR></TABLE></TD>!;
+
+  print qq!<TD><INPUT TYPE="checkbox" NAME="setuptax!. $hashref->{taxnum}.
+        '" VALUE="Y"'.
+        ( $hashref->{setuptax} =~ /^Y$/i ? ' CHECKED' : '' ).
+        '></TD>'
+    if dbdef->table('cust_main_county')->column('setuptax');
+
+  print qq!<TD><INPUT TYPE="checkbox" NAME="recurtax!. $hashref->{taxnum}.
+        '" VALUE="Y"'.
+        ( $hashref->{recurtax} =~ /^Y$/i ? ' CHECKED' : '' ).
+        '></TD>'
+    if dbdef->table('cust_main_county')->column('recurtax');
+
   print '</TR>';
 
 }
index 6426eed..48ed791 100755 (executable)
@@ -20,7 +20,7 @@ if ( $query && $query =~ /^(\d+)$/ ) {
 } else {
   $part_bill_event ||= new FS::part_bill_event {};
 }
-$action ||= $part_bill_event->pkgpart ? 'Edit' : 'Add';
+$action ||= $part_bill_event->eventpart ? 'Edit' : 'Add';
 my $hashref = $part_bill_event->hashref;
 
 print header("$action Invoice Event Definition", menubar(
index cc60f1a..4d0c739 100644 (file)
@@ -68,9 +68,10 @@ my $widget = new HTML::Widgets::SelectLayers(
         $html .= '</SELECT>';
       } elsif ( $type eq 'textarea' ) {
         $html .= qq!<TEXTAREA NAME="$option" COLS=80 ROWS=8 WRAP="virtual">!.
-                 qq!$value</TEXTAREA>!;
+                 encode_entities($value). '</TEXTAREA>';
       } elsif ( $type eq 'text' ) {
-        $html .= qq!<INPUT TYPE="text" NAME="$option" VALUE="$value" SIZE=64>!;
+        $html .= qq!<INPUT TYPE="text" NAME="$option" VALUE="!.
+                 encode_entities($value). '" SIZE=64>';
       } elsif ( $type eq 'checkbox' ) {
         $html .= qq!<INPUT TYPE="checkbox" NAME="$option" VALUE="1"!;
         $html .= ' CHECKED' if $value;
index dee3562..8416b35 100755 (executable)
@@ -66,12 +66,52 @@ print '<FORM NAME="dummy">';
 #print qq!<INPUT TYPE="hidden" NAME="pkgpart" VALUE="$hashref->{pkgpart}">!,
 print "Package Part #", $hashref->{pkgpart} ? $hashref->{pkgpart} : "(NEW)";
 
-print ntable("#cccccc",2), <<END;
-<TR><TD ALIGN="right">Package (customer-visible)</TD><TD><INPUT TYPE="text" NAME="pkg" SIZE=32 VALUE="$hashref->{pkg}"></TD></TR>
-<TR><TD ALIGN="right">Comment (customer-hidden)</TD><TD><INPUT TYPE="text" NAME="comment" SIZE=32 VALUE="$hashref->{comment}"></TD></TR>
-<TR><TD ALIGN="right">Frequency (months) of recurring fee</TD><TD><INPUT TYPE="text" NAME="freq" VALUE="$hashref->{freq}" SIZE=3>&nbsp;&nbsp;<I>0=no recurring fee, 1=monthly, 3=quarterly, 12=yearly</TD></TR>
-<TR><TD ALIGN="right">Setup fee tax exempt</TD><TD>
-END
+#false laziness w/view/cust_main.cgi
+my %freq;
+tie %freq, 'Tie::IxHash', 
+  '0'  => '(no recurring fee)',
+  '1d' => 'daily',
+  '1w' => 'weekly',
+  '2w' => 'biweekly (every 2 weeks)',
+  '1'  => 'monthly',
+  '2'  => 'bimonthly (every 2 months)',
+  '3'  => 'quarterly (every 3 months)',
+  '6'  => 'semiannually (every 6 months)',
+  '12' => 'annually',
+  '24' => 'biannually (every 2 years)',
+;
+if ( $part_pkg->dbdef_table->column('freq')->type =~ /(int)/i ) {
+  delete $freq{$_} foreach grep { ! /^\d+$/ } keys %freq;
+}
+
+%>
+<%= ntable("#cccccc",2) %>
+  <TR>
+    <TD ALIGN="right">Package (customer-visible)</TD>
+    <TD>
+      <INPUT TYPE="text" NAME="pkg" SIZE=32 VALUE="<%= $part_pkg->pkg %>">
+    </TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Comment (customer-hidden)</TD>
+    <TD>
+      <INPUT TYPE="text" NAME="comment" SIZE=32 VALUE="<%=$part_pkg->comment%>">
+    </TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Recurring fee frequency </TD>
+    <TD>
+      <SELECT NAME="freq">
+        <% foreach my $freq ( keys %freq ) { %>
+          <OPTION VALUE="<%= $freq %>"<%= $freq eq $part_pkg->freq ? ' SELECTED' : '' %>><%= $freq{$freq} %>
+        <% } %>
+      </SELECT>
+    </TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Setup fee tax exempt</TD>
+    <TD>
+<%
 
 print '<INPUT TYPE="checkbox" NAME="setuptax" VALUE="Y"';
 print ' CHECKED' if $hashref->{setuptax} eq "Y";
@@ -112,9 +152,11 @@ print ' CHECKED' if $hashref->{disabled} eq "Y";
 print '>';
 print '</TD></TR></TABLE>';
 
-my $thead =  "\n\n". ntable('#cccccc', 2). <<END;
-<TR><TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Quan.</FONT></TH><TH BGCOLOR="#dcdcdc">Service</TH></TR>
-END
+my $thead =  "\n\n". ntable('#cccccc', 2).
+             '<TR><TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Quan.</FONT></TH>';
+$thead .=  '<TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Primary</FONT></TH>'
+  if dbdef->table('pkg_svc')->column('primary_svc');
+$thead .= '<TH BGCOLOR="#dcdcdc">Service</TH></TR>';
 
 #unless ( $cgi->param('clone') ) {
 #dunno why...
@@ -136,9 +178,10 @@ foreach my $part_svc ( @part_svc ) {
     'pkgpart'  => $pkgpart,
     'svcpart'  => $svcpart,
   } ) || new FS::pkg_svc ( {
-    'pkgpart'  => $pkgpart,
-    'svcpart'  => $svcpart,
-    'quantity' => 0,
+    'pkgpart'     => $pkgpart,
+    'svcpart'     => $svcpart,
+    'quantity'    => 0,
+    'primary_svc' => '',
   });
   #? #next unless $pkg_svc;
 
@@ -150,7 +193,13 @@ foreach my $part_svc ( @part_svc ) {
     print '<TR>'; # if $count == 0 ;
     print qq!<TD><INPUT TYPE="text" NAME="pkg_svc$svcpart" SIZE=4 MAXLENGTH=3 VALUE="!,
           $cgi->param("pkg_svc$svcpart") || $pkg_svc->quantity || 0,
-          qq!"></TD><TD><A HREF="part_svc.cgi?!,$part_svc->svcpart,
+          qq!"></TD>!;
+    if ( dbdef->table('pkg_svc')->column('primary_svc') ) {
+      print qq!<TD><INPUT TYPE="radio" NAME="pkg_svc_primary" VALUE="$svcpart"!;
+      print ' CHECKED' if $pkg_svc->primary_svc =~ /^Y/i;
+      print '></TD>';
+    }
+    print qq!<TD><A HREF="part_svc.cgi?!,$part_svc->svcpart,
           qq!">!, $part_svc->getfield('svc'), "</A></TD></TR>";
 #    print "</TABLE></TD><TD>$thead" if ++$count == int(scalar(@part_svc) / 2);
     $count+=1;
@@ -399,6 +448,71 @@ tie my %plans, 'Tie::IxHash',
     'recur' => '\'my $last_bill = $cust_pkg->last_bill; my $hours = $cust_pkg->seconds_since_sqlradacct($last_bill, $sdate ) / 3600 - \' + what.recur_included_hours.value + \'; $hours = 0 if $hours < 0; my $input = $cust_pkg->attribute_since_sqlradacct($last_bill, $sdate, \"AcctInputOctets\" ) / 1048576; my $output = $cust_pkg->attribute_since_sqlradacct($last_bill, $sdate, \"AcctOutputOctets\" ) / 1048576; my $total = $input + $output - \' + what.recur_included_total.value + \'; $total = 0 if $total < 0; my $input = $input - \' + what.recur_included_input.value + \'; $input = 0 if $input < 0; my $output = $output - \' + what.recur_included_output.value + \'; $output = 0 if $output < 0; my $totalcharge = sprintf(\"%.2f\", \' + what.recur_total_charge.value + \' * $total); my $hourscharge = sprintf(\"%.2f\", \' + what.recur_hourly_charge.value + \' * $hours); push @details, \"Last month\\\'s excess data \". sprintf(\"%.1f\", $total). \" megs: \\\$$totalcharge\", \"Last month\\\'s excess time \". sprintf(\"%.1f\", $hours). \" hours: \\\$$hourscharge\"; \' + what.recur_flat.value + \' + $hourscharge + \' + what.recur_input_charge.value + \' * $input + \' + what.recur_output_charge.value + \' * $output + $totalcharge ;\'',
   },
 
+  'sql_generic' => {
+    'name' => 'Base charge plus a metered rate from a configurable SQL query',
+    'fields' => {
+      'setup_fee' => { 'name' => 'Setup fee for this package',
+                       'default' => 0,
+                     },
+      'recur_flat' => { 'name' => 'Base monthly charge for this package',
+                        'default' => 0,
+                      },
+      'recur_included' => { 'name' => 'Units included',
+                            'default' => 0,
+                          },
+      'recur_unit_charge' => { 'name' => 'Additional charge per unit',
+                               'default' => 0,
+                             },
+      'datasrc' => { 'name' => 'DBI data source',
+                     'default' => '',
+                   },
+      'db_username' => { 'name' => 'Database username',
+                         'default' => '',
+                       },
+      'db_password' => { 'name' => 'Database username',
+                         'default' => '',
+                       },
+      'query' => { 'name' => 'SQL query',
+                   'default' => '',
+                 },
+    },
+    'fieldorder' => [qw( setup_fee recur_flat recur_included recur_unit_charge datasrc db_username db_password query )],
+    'setup' => 'what.setup_fee.value',
+   # 'recur' => '\'my $dbh = DBI->connect(\"\' + what.datasrc.value + \'\", \"\' + what.db_username.value + \'\") or die $DBI::errstr; \'',
+   'recur' => '\'my $dbh = DBI->connect(\"\' + what.datasrc.value + \'\", \"\' + what.db_username.value + \'\", \"\' + what.db_password.value + \'\" ) or die $DBI::errstr; my $sth = $dbh->prepare(\"\' + what.query.value + \'\") or die $dbh->errstr; my $units = 0; foreach my $cust_svc ( grep { $_->part_svc->svcdb eq \"svc_domain\" } $cust_pkg->cust_svc ) { my $domain = $cust_svc->svc_x->domain; $sth->execute($domain) or die $sth->errstr; $units += $sth->fetchrow_arrayref->[0]; } $units -= \' + what.recur_included.value + \'; $units = 0 if $units < 0; \' + what.recur_flat.value + \' + $units * \' + what.recur_unit_charge.value + \';\'',
+    #'recur' => '\'my $dbh = DBI->connect("\' + what.datasrc.value + \'", "\' + what.db_username.value + \'", "\' what.db_password.value + \'" ) or die $DBI::errstr; my $sth = $dbh->prepare("\' + what.query.value + \'") or die $dbh->errstr; my $units = 0; foreach my $cust_svc ( grep { $_->part_svc->svcdb eq "svc_domain" } $cust_pkg->cust_svc ) { my $domain = $cust_svc->svc_x->domain; $sth->execute($domain) or die $sth->errstr; $units += $sth->fetchrow_arrayref->[0]; } $units -= \' + what.recur_included.value + \'; $units = 0 if $units < 0; \' + what.recur_flat.value + \' + $units * \' + what.recur_unit_charge + \';\'',
+  },
+
+
+
+  'sql_external' => {
+    'name' => 'Base charge plus additional fees for external services from a configurable SQL query',
+    'fields' => {
+      'setup_fee' => { 'name' => 'Setup fee for this package',
+                       'default' => 0,
+                     },
+      'recur_flat' => { 'name' => 'Base monthly charge for this package',
+                        'default' => 0,
+                      },
+      'datasrc' => { 'name' => 'DBI data source',
+                     'default' => '',
+                   },
+      'db_username' => { 'name' => 'Database username',
+                         'default' => '',
+                       },
+      'db_password' => { 'name' => 'Database password',
+                         'default' => '',
+                       },
+      'query' => { 'name' => 'SQL query',
+                   'default' => '',
+                 },
+    },
+    'fieldorder' => [qw( setup_fee recur_flat datasrc db_username db_password query )],
+    'setup' => 'what.setup_fee.value',
+    'recur' => q!'my $dbh = DBI->connect("' + what.datasrc.value + '", "' + what.db_username.value + '", "' + what.db_password.value + '" ) or die $DBI::errstr; my $sth = $dbh->prepare("' + what.query.value + '") or die $dbh->errstr; my $price = ' + what.recur_flat.value + '; foreach my $cust_svc ( grep { $_->part_svc->svcdb eq "svc_external" } $cust_pkg->cust_svc ){ my $id = $cust_svc->svc_x->id; $sth->execute($id) or die $sth->errstr; $price += $sth->fetchrow_arrayref->[0]; } $price;'!,
+
+  },
+
 ;
 
 my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
@@ -413,6 +527,10 @@ if ( $conf->exists('enable_taxclasses') ) {
   push @fixups, 'taxclass'; #hidden
 }
 
+my @form_radio = ();
+if ( dbdef->table('pkg_svc')->column('primary_svc') ) {
+  push @form_radio, 'pkg_svc_primary';
+}
 
 my $widget = new HTML::Widgets::SelectLayers(
   'selected_layer' => $part_pkg->plan,
@@ -421,7 +539,8 @@ my $widget = new HTML::Widgets::SelectLayers(
   'form_action'    => 'process/part_pkg.cgi',
   'form_text'      => [ qw(pkg comment freq clone pkgnum pkgpart), @fixups ],
   'form_checkbox'  => [ qw(setuptax recurtax disabled) ],
-  'form_select'    => [ @form_select ],
+  'form_radio'     => \@form_radio,
+  'form_select'    => \@form_select,
   'fixup_callback' => sub {
                         #my $ = @_;
                         my $html = '';
index 45e0b6f..6868ffd 100755 (executable)
@@ -53,6 +53,7 @@ Services are items you offer to your customers.
     <LI>svc_forward - mail forwarding
     <LI>svc_www - Virtual domain website
     <LI>svc_broadband - Broadband/High-speed Internet service
+    <LI>svc_external - Externally-tracked service
 <!--   <LI>svc_charge - One-time charges (Partially unimplemented)
        <LI>svc_wo - Work orders (Partially unimplemented)
 -->
@@ -129,9 +130,13 @@ my %defs = (
     'ip_addr' => 'IP address.  Leave blank for automatic assignment.',
     'blocknum' => 'Address block.',
   },
+  'svc_external' => {
+    #'id' => '',
+    #'title' => '',
+  },
 );
 
-  foreach $svcdb (keys(%defs)) {
+  foreach my $svcdb (grep dbdef->table($_), keys %defs ) {
     my $self = "FS::$svcdb"->new;
     $vfields{$svcdb} = {};
     foreach my $field ($self->virtual_fields) { # svc_Common::virtual_fields with a null svcpart returns all of them
@@ -151,9 +156,9 @@ my %defs = (
   
   my @dbs = $hashref->{svcdb}
              ? ( $hashref->{svcdb} )
-             : qw( svc_acct svc_domain svc_forward svc_www svc_broadband );
+             : qw( svc_acct svc_domain svc_forward svc_www svc_broadband svc_external );
 
-  tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } @dbs;
+  tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } grep dbdef->table($_), @dbs;
   my $widget = new HTML::Widgets::SelectLayers(
     #'selected_layer' => $p_svcdb,
     'selected_layer' => $hashref->{svcdb} || 'svc_acct',
@@ -174,7 +179,7 @@ my %defs = (
       $html .= '<BR><BR>'. table().
                table(). "<TR><TH COLSPAN=$columns>Exports</TH></TR><TR>";
       foreach my $part_export ( @part_export ) {
-        $html .= '<TD><INPUT TYPE="checbox"'.
+        $html .= '<TD><INPUT TYPE="checkbox"'.
                  ' NAME="exportnum'. $part_export->exportnum. '"  VALUE="1" ';
         $html .= 'CHECKED'
           if ( $clone || $part_svc->svcpart ) #null svcpart search causing error
index 8183828..25c346e 100755 (executable)
@@ -19,6 +19,8 @@ if ( $payby ) {
   $cgi->param('paydate',
     $cgi->param( $payby. '_month' ). '-'. $cgi->param( $payby. '_year' ) );
   $cgi->param('payname', $cgi->param( $payby. '_payname' ) );
+  $cgi->param('paycvv', $cgi->param( $payby. '_paycvv' ) )
+    if defined $cgi->param( $payby. '_paycvv' );
 }
 
 $cgi->param('otaker', &getotaker );
@@ -27,6 +29,7 @@ my @invoicing_list = split( /\s*\,\s*/, $cgi->param('invoicing_list') );
 push @invoicing_list, 'POST' if $cgi->param('invoicing_list_POST');
 $cgi->param('invoicing_list', join(',', @invoicing_list) );
 
+
 #create new record object
 
 my $new = new FS::cust_main ( {
@@ -113,6 +116,11 @@ if ( $new->custnum eq '' ) {
 } else { #create old record object
   my $old = qsearchs( 'cust_main', { 'custnum' => $new->custnum } ); 
   $error ||= "Old record not found!" unless $old;
+  if ( defined dbdef->table('cust_main')->column('paycvv')
+       && length($old->paycvv)
+       && $new->paycvv =~ /^\s*\*+\s*$/ ) {
+    $new->paycvv($old->paycvv);
+  }
   $error ||= $new->replace($old, \@invoicing_list);
 }
 
index 8e67140..5da9dea 100755 (executable)
@@ -3,8 +3,8 @@
 my($query) = $cgi->keywords;
 $query =~ /^(\d+)$/ or die "Illegal taxnum!";
 my $taxnum = $1;
-my $cust_main_county = qsearchs('cust_main_county',{'taxnum'=>$taxnum})
-  or die ("Unknown taxnum!");
+my $cust_main_county = qsearchs('cust_main_county', { 'taxnum' => $taxnum } )
+  or die "Unknown taxnum $taxnum";
 
 #really should do this in a .pm & start transaction
 
index 6d80ad5..9287ed1 100755 (executable)
@@ -7,11 +7,15 @@ foreach ( grep { /^tax\d+$/ } $cgi->param ) {
     or die "Couldn't find taxnum $taxnum!";
   next unless    $old->tax           != $cgi->param("tax$taxnum")
               || $old->exempt_amount != $cgi->param("exempt_amount$taxnum")
-              || $old->taxname       ne $cgi->param("taxname$taxnum");
+              || $old->taxname       ne $cgi->param("taxname$taxnum")
+              || $old->setuptax      ne $cgi->param("setuptax$taxnum")
+              || $old->recurtax      ne $cgi->param("recurtax$taxnum");
   my %hash = $old->hash;
   $hash{tax} = $cgi->param("tax$taxnum");
   $hash{exempt_amount} = $cgi->param("exempt_amount$taxnum");
   $hash{taxname} = $cgi->param("taxname$taxnum");
+  $hash{setuptax} = $cgi->param("setuptax$taxnum");
+  $hash{recurtax} = $cgi->param("recurtax$taxnum");
   my $new = new FS::cust_main_county \%hash;
   my $error = $new->replace($old);
   if ( $error ) {
index d489426..7eada7b 100755 (executable)
@@ -62,16 +62,24 @@ if ( $error ) {
 
 foreach my $part_svc (qsearch('part_svc',{})) {
   my $quantity = $cgi->param('pkg_svc'. $part_svc->svcpart) || 0;
+  my $primary_svc =
+    $cgi->param('pkg_svc_primary') == $part_svc->svcpart ? 'Y' : '';
   my $old_pkg_svc = qsearchs('pkg_svc', {
     'pkgpart' => $pkgpart,
     'svcpart' => $part_svc->svcpart,
   } );
   my $old_quantity = $old_pkg_svc ? $old_pkg_svc->quantity : 0;
-  next unless $old_quantity != $quantity; #!here
+  my $old_primary_svc =
+    ( $old_pkg_svc && $old_pkg_svc->dbdef_table->column('primary_svc') )
+      ? $old_pkg_svc->primary_svc
+      : '';
+  next unless $old_quantity != $quantity || $old_primary_svc ne $primary_svc;
+
   my $new_pkg_svc = new FS::pkg_svc( {
-    'pkgpart'  => $pkgpart,
-    'svcpart'  => $part_svc->svcpart,
-    'quantity' => $quantity, 
+    'pkgpart'     => $pkgpart,
+    'svcpart'     => $part_svc->svcpart,
+    'quantity'    => $quantity, 
+    'primary_svc' => $primary_svc,
   } );
   if ( $old_pkg_svc ) {
     my $myerror = $new_pkg_svc->replace($old_pkg_svc);
index 7e40c48..a2fa46d 100644 (file)
@@ -1,8 +1,5 @@
 <%
 
-use FS::UID qw(dbh);
-
-my $dbh = dbh;
 local $FS::UID::AutoCommit=0;
 
 sub check {
@@ -10,7 +7,7 @@ sub check {
   if($error) {
     $cgi->param('error', $error);
     print $cgi->redirect(popurl(3) . "edit/router.cgi?". $cgi->query_string);
-    $dbh->rollback;
+    dbh->rollback;
     exit;
   }
 }
@@ -38,7 +35,7 @@ check($error);
 
 if ($old) {
   @old_psr = $old->part_svc_router;
-  foreach $psr (@old_psr) {
+  foreach my $psr (@old_psr) {
     if($cgi->param('svcpart_'.$psr->svcpart) eq 'ON') {
       # do nothing
     } else {
@@ -64,7 +61,7 @@ foreach($cgi->param) {
 
 
 # Yay, everything worked!
-$dbh->commit or die $dbh->errstr;
+dbh->commit or die dbh->errstr;
 print $cgi->redirect(popurl(3). "browse/router.cgi");
 
 %>
diff --git a/httemplate/edit/process/svc_external.cgi b/httemplate/edit/process/svc_external.cgi
new file mode 100755 (executable)
index 0000000..728cd21
--- /dev/null
@@ -0,0 +1,29 @@
+<%
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum =$1;
+
+my $old = qsearchs('svc_external',{'svcnum'=>$svcnum}) if $svcnum;
+
+my $new = new FS::svc_external ( {
+  map {
+    ($_, scalar($cgi->param($_)));
+  } ( fields('svc_external'), qw( pkgnum svcpart ) )
+} );
+
+my $error = '';
+if ( $svcnum ) {
+  $error = $new->replace($old);
+} else {
+  $error = $new->insert;
+  $svcnum = $new->getfield('svcnum');
+} 
+
+if ($error) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "svc_external.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "view/svc_external.cgi?$svcnum");
+}
+
+%>
index 5c365a2..a573c65 100755 (executable)
@@ -22,6 +22,8 @@ print header("$action Router", menubar(
   'View all routers' => "${p}browse/router.cgi",
 ));
 
+my $p3 = popurl(3);
+
 if($cgi->param('error')) {
 %> <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
 <% } %>
@@ -31,6 +33,7 @@ if($cgi->param('error')) {
   <INPUT TYPE="hidden" NAME="redirect_ok" VALUE="<%=$p3%>/browse/router.cgi">
   <INPUT TYPE="hidden" NAME="redirect_error" VALUE="<%=$p3%>/edit/router.cgi">
   <INPUT TYPE="hidden" NAME="routernum" VALUE="<%=$routernum%>">
+  <INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$router->svcnum%>">
     Router #<%=$routernum or "(NEW)"%>
 
 <BR><BR>Name <INPUT TYPE="text" NAME="routername" SIZE=32 VALUE="<%=$router->routername%>">
@@ -49,12 +52,14 @@ foreach my $field ($router->virtual_fields) {
 </TABLE>
 
 
-
+<%
+unless ($router->svcnum) {
+%>
 <BR><BR>Select the service types available on this router<BR>
 <%
 
-foreach my $part_svc ( qsearch('part_svc', { svcdb    => 'svc_broadband',
-                                             disabled => '' }) ) {
+  foreach my $part_svc ( qsearch('part_svc', { svcdb    => 'svc_broadband',
+                                               disabled => '' }) ) {
   %>
   <BR>
   <INPUT TYPE="checkbox" NAME="svcpart_<%=$part_svc->svcpart%>"<%=
@@ -64,6 +69,8 @@ foreach my $part_svc ( qsearch('part_svc', { svcdb    => 'svc_broadband',
     <%=$part_svc->svcpart%>: <%=$part_svc->svc%></A>
   <% } %>
 
+<% } %>
+
   <BR><BR><INPUT TYPE="submit" VALUE="Apply changes">
   </FORM>
 </BODY></HTML>
index 44606d9..f1b8b80 100755 (executable)
@@ -281,7 +281,7 @@ if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
 }
 print '</TD></TR>';
 
-foreach $field ($svc_acct->virtual_fields) {
+foreach my $field ($svc_acct->virtual_fields) {
   if ( $part_svc->part_svc_column($field)->columnflag ne 'F' ) {
     # If the flag is X, it won't even show up in $svc_acct->virtual_fields.
     print $svc_acct->pvf($field)->widget('HTML', 'edit', 
index 6828791..db27b32 100644 (file)
@@ -157,7 +157,7 @@ Service #<B><%=$svcnum ? $svcnum : "(NEW)"%></B><BR><BR>
 <% } %>
 
 <%
-foreach $field ($svc_broadband->virtual_fields) {
+foreach my $field ($svc_broadband->virtual_fields) {
   if ( $part_svc->part_svc_column($field)->columnflag ne 'F' ) {
     print $svc_broadband->pvf($field)->widget('HTML', 'edit',
         $svc_broadband->getfield($field));
diff --git a/httemplate/edit/svc_external.cgi b/httemplate/edit/svc_external.cgi
new file mode 100644 (file)
index 0000000..bcfc85e
--- /dev/null
@@ -0,0 +1,105 @@
+<!-- mason kludge -->
+<%
+
+my( $svcnum,  $pkgnum, $svcpart, $part_svc, $svc_external );
+if ( $cgi->param('error') ) {
+  $svc_external = new FS::svc_external ( {
+    map { $_, scalar($cgi->param($_)) } fields('svc_external')
+  } );
+  $svcnum = $svc_external->svcnum;
+  $pkgnum = $cgi->param('pkgnum');
+  $svcpart = $cgi->param('svcpart');
+  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+  die "No part_svc entry!" unless $part_svc;
+} else {
+  my($query) = $cgi->keywords;
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $svcnum=$1;
+    $svc_external=qsearchs('svc_external',{'svcnum'=>$svcnum})
+      or die "Unknown (svc_external) svcnum!";
+
+    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+      or die "Unknown (cust_svc) svcnum!";
+
+    $pkgnum=$cust_svc->pkgnum;
+    $svcpart=$cust_svc->svcpart;
+  
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+  } else { #adding
+
+    foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
+      $pkgnum=$1 if /^pkgnum(\d+)$/;
+      $svcpart=$1 if /^svcpart(\d+)$/;
+    }
+    $svc_external = new FS::svc_external { svcpart => $svcpart };
+
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+    $svcnum='';
+
+    #set fixed and default fields from part_svc
+    foreach my $part_svc_column (
+      grep { $_->columnflag } $part_svc->all_part_svc_column
+    ) {
+      $svc_external->setfield( $part_svc_column->columnname,
+                               $part_svc_column->columnvalue,
+                             );
+    }
+
+  }
+}
+my $action = $svc_external->svcnum ? 'Edit' : 'Add';
+
+my $p1 = popurl(1);
+print header("External service $action", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/svc_external.cgi" METHOD=POST>!;
+
+#display
+
+#svcnum
+print qq!<INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">!;
+print qq!Service #<B>!, $svcnum ? $svcnum : "(NEW)", "</B><BR><BR>";
+
+#pkgnum
+print qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!;
+#svcpart
+print qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!;
+
+my($id,$title)=(
+  $svc_external->id,
+  $svc_external->title,
+);
+
+print &ntable("#cccccc",2),
+      '<TR><TD ALIGN="right">External ID</TD><TD>'.
+      qq!<INPUT TYPE="text" NAME="id" VALUE="$id">!.
+      '</TD></TR>'.
+      '<TR><TD ALIGN="right">Title</TD><TD>'.
+      qq!<INPUT TYPE="text" NAME="title" VALUE="$title">!.
+      '</TD></TR>';
+
+foreach my $field ($svc_external->virtual_fields) {
+  if ( $part_svc->part_svc_column($field)->columnflag ne 'F' ) {
+    # If the flag is X, it won't even show up in $svc_acct->virtual_fields.
+    print $svc_external->pvf($field)->widget('HTML', 'edit', 
+        $svc_external->getfield($field));
+  }
+}
+
+%>
+
+</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">
+    </FORM>
+  </BODY>
+</HTML>
+
index 0d815b9..2e6c5f1 100755 (executable)
@@ -58,20 +58,17 @@ if ( $cgi->param('error') ) {
 my $action = $svc_forward->svcnum ? 'Edit' : 'Add';
 
 my %email;
+
+#starting with those currently attached
+foreach my $method (qw( srcsvc_acct dstsvc_acct )) {
+  my $svc_acct = $svc_forward->$method();
+  $email{$svc_acct->svcnum} = $svc_acct->email if $svc_acct;
+}
+
 if ($pkgnum) {
 
   #find all possible user svcnums (and emails)
 
-  #starting with those currently attached
-  if ( $svc_forward->srcsvc ) {
-    my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_forward->srcsvc } );
-    $email{$svc_forward->srcsvc} = $svc_acct->email;
-  }
-  if ( $svc_forward->dstsvc ) {
-    my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_forward->dstsvc } );
-    $email{$svc_forward->dstsvc} = $svc_acct->email;
-  }
-
   #and including the rest for this customer
   my($u_part_svc,@u_acct_svcparts);
   foreach $u_part_svc ( qsearch('part_svc',{'svcdb'=>'svc_acct'}) ) {
@@ -98,15 +95,7 @@ if ($pkgnum) {
     }
   }
 
-} elsif ( $action eq 'Edit' ) {
-
-  my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$svc_forward->srcsvc});
-  $email{$svc_forward->srcsvc} = $svc_acct->email;
-
-  $svc_acct=qsearchs('svc_acct',{'svcnum'=>$svc_forward->dstsvc});
-  $email{$svc_forward->dstsvc} = $svc_acct->email;
-
-} else {
+} elsif ( $action eq 'Add' ) {
   die "\$action eq Add, but \$pkgnum is null!\n";
 }
 
@@ -115,6 +104,7 @@ my($srcsvc,$dstsvc,$dst)=(
   $svc_forward->dstsvc,
   $svc_forward->dst,
 );
+my $src = $svc_forward->dbdef_table->column('src') ? $svc_forward->src : '';
 
 #display
 
@@ -130,46 +120,54 @@ my($srcsvc,$dstsvc,$dst)=(
 Service #<%= $svcnum ? "<B>$svcnum</B>" : " (NEW)" %><BR>
 Service: <B><%= $part_svc->svc %></B><BR><BR>
 
-<FORM NAME="dummy">
+<FORM ACTION="process/svc_forward.cgi" METHOD="POST">
+<INPUT TYPE="hidden" NAME="svcnum" VALUE="<%= $svcnum %>">
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<%= $pkgnum %>">
+<INPUT TYPE="hidden" NAME="svcpart" VALUE="<%= $svcpart %>">
+
+<SCRIPT TYPE="text/javascript">
+function srcchanged(what) {
+  if ( what.options[what.selectedIndex].value == 0 ) {
+    what.form.src.disabled = false;
+  } else {
+    what.form.src.disabled = true;
+  }
+}
+function dstchanged(what) {
+  if ( what.options[what.selectedIndex].value == 0 ) {
+    what.form.dst.disabled = false;
+  } else {
+    what.form.dst.disabled = true;
+  }
+}
+</SCRIPT>
 
 <%= ntable("#cccccc",2) %>
-<TR><TD ALIGN="right">Email to</TD><TD><SELECT NAME="srcsvc" SIZE=1>
+<TR><TD ALIGN="right">Email to</TD>
+<TD><SELECT NAME="srcsvc" SIZE=1 onChange="srcchanged(this)">
 <% foreach $_ (keys %email) { %>
   <OPTION<%= $_ eq $srcsvc ? " SELECTED" : "" %> VALUE="<%= $_ %>"><%= $email{$_} %></OPTION>
 <% } %>
-</SELECT></TD></TR>
-
-<%
-  tie my %tied_email, 'Tie::IxHash',
-    ''  => 'SELECT DESTINATION',
-    %email,
-    '0' => '(other email address)';
-  my $widget = new HTML::Widgets::SelectLayers(
-    'selected_layer' => $dstsvc,
-    'options'        => \%tied_email,
-    'form_name'      => 'dummy',
-    'form_action'    => 'process/svc_forward.cgi',
-    'form_select'    => ['srcsvc'],
-    'html_between'   => '</TD></TR></TABLE>',
-    'layer_callback' => sub {
-      my $layer = shift;
-      my $html = qq!<INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">!.
-                 qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!.
-                 qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!.
-                 qq!<INPUT TYPE="hidden" NAME="dstsvc" VALUE="$layer">!;
-      if ( $layer eq '0' ) {
-        $html .= ntable("#cccccc",2).
-                 '<TR><TD ALIGN="right">Destination email</TD>'.
-                 qq!<TD><INPUT TYPE="text" NAME="dst" VALUE="$dst"></TD>!.
-                 '</TR></TABLE>';
-      }
-      $html .= '<BR><INPUT TYPE="submit" VALUE="Submit">';
-      $html;
-    },
-  );
-%>
+<% if ( $svc_forward->dbdef_table->column('src') ) { %>
+  <OPTION <%= $src ? 'SELECTED' : '' %> VALUE="0">(other email address)</OPTION>
+<% } %>
+</SELECT>
+<% if ( $svc_forward->dbdef_table->column('src') ) { %>
+<INPUT TYPE="text" NAME="src" VALUE="<%= $src %>" <%= ( $src || !scalar(%email) ) ? '' : 'DISABLED' %>>
+<% } %>
+</TD></TR>
 
 <TR><TD ALIGN="right">Forwards to</TD>
-<TD><%= $widget->html %>
+<TD><SELECT NAME="dstsvc" SIZE=1 onChange="dstchanged(this)">
+<% foreach $_ (keys %email) { %>
+  <OPTION<%= $_ eq $dstsvc ? " SELECTED" : "" %> VALUE="<%= $_ %>"><%= $email{$_} %></OPTION>
+<% } %>
+<OPTION <%= $dst ? 'SELECTED' : '' %> VALUE="0">(other email address)</OPTION>
+</SELECT>
+<INPUT TYPE="text" NAME="dst" VALUE="<%= $dst %>" <%= ( $dst || !scalar(%email) ) ? '' : 'DISABLED' %>>
+</TD></TR>
+    </TABLE>
+<BR><INPUT TYPE="submit" VALUE="Submit">
+</FORM>
   </BODY>
 </HTML>
index 043af61..ec5169e 100644 (file)
@@ -167,7 +167,7 @@ foreach $_ (keys %username) {
 }
 print "</SELECT></TD></TR>";
 
-foreach $field ($svc_www->virtual_fields) {
+foreach my $field ($svc_www->virtual_fields) {
   if ( $part_svc->part_svc_column($field)->columnflag ne 'F' ) {
     # If the flag is X, it won't even show up in $svc_acct->virtual_fields.
     print $svc_www->pvf($field)->widget('HTML', 'edit', 
diff --git a/httemplate/elements/calendar-en.js b/httemplate/elements/calendar-en.js
new file mode 100644 (file)
index 0000000..e9291e1
--- /dev/null
@@ -0,0 +1,114 @@
+// ** I18N
+
+// Calendar EN language
+// Author: Mihai Bazon, <mishoo@infoiasi.ro>
+// Encoding: any
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible.  We strongly believe that
+// Unicode is the answer to a real internationalized world.  Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ "Sunday");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary.  We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+//   Calendar._SDN_len = N; // short day name length
+//   Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Sun",
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat",
+ "Sun");
+
+// full month names
+Calendar._MN = new Array
+("January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "About the calendar";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2003\n" + // don't translate this this ;-)
+"For latest version visit: http://dynarch.com/mishoo/calendar.epl\n" +
+"Distributed under GNU LGPL.  See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "Prev. year (hold for menu)";
+Calendar._TT["PREV_MONTH"] = "Prev. month (hold for menu)";
+Calendar._TT["GO_TODAY"] = "Go Today";
+Calendar._TT["NEXT_MONTH"] = "Next month (hold for menu)";
+Calendar._TT["NEXT_YEAR"] = "Next year (hold for menu)";
+Calendar._TT["SEL_DATE"] = "Select date";
+Calendar._TT["DRAG_TO_MOVE"] = "Drag to move";
+Calendar._TT["PART_TODAY"] = " (today)";
+Calendar._TT["MON_FIRST"] = "Display Monday first";
+Calendar._TT["SUN_FIRST"] = "Display Sunday first";
+Calendar._TT["CLOSE"] = "Close";
+Calendar._TT["TODAY"] = "Today";
+Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+
+Calendar._TT["WK"] = "wk";
diff --git a/httemplate/elements/calendar-setup.js b/httemplate/elements/calendar-setup.js
new file mode 100644 (file)
index 0000000..0dc3caa
--- /dev/null
@@ -0,0 +1,163 @@
+/*  Copyright Mihai Bazon, 2002, 2003  |  http://dynarch.com/mishoo/
+ * ---------------------------------------------------------------------------
+ *
+ * The DHTML Calendar
+ *
+ * Details and latest version at:
+ * http://dynarch.com/mishoo/calendar.epl
+ *
+ * This script is distributed under the GNU Lesser General Public License.
+ * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html
+ *
+ * This file defines helper functions for setting up the calendar.  They are
+ * intended to help non-programmers get a working calendar on their site
+ * quickly.  This script should not be seen as part of the calendar.  It just
+ * shows you what one can do with the calendar, while in the same time
+ * providing a quick and simple method for setting it up.  If you need
+ * exhaustive customization of the calendar creation process feel free to
+ * modify this code to suit your needs (this is recommended and much better
+ * than modifying calendar.js itself).
+ */
+
+// $Id: calendar-setup.js,v 1.3 2003-11-07 10:53:35 ivan Exp $
+
+/**
+ *  This function "patches" an input field (or other element) to use a calendar
+ *  widget for date selection.
+ *
+ *  The "params" is a single object that can have the following properties:
+ *
+ *    prop. name   | description
+ *  -------------------------------------------------------------------------------------------------
+ *   inputField    | the ID of an input field to store the date
+ *   displayArea   | the ID of a DIV or other element to show the date
+ *   button        | ID of a button or other element that will trigger the calendar
+ *   eventName     | event that will trigger the calendar, without the "on" prefix (default: "click")
+ *   ifFormat      | date format that will be stored in the input field
+ *   daFormat      | the date format that will be used to display the date in displayArea
+ *   singleClick   | (true/false) wether the calendar is in single click mode or not (default: true)
+ *   mondayFirst   | (true/false) if true Monday is the first day of week, Sunday otherwise (default: true)
+ *   align         | alignment (default: "Bl"); if you don't know what's this see the calendar documentation
+ *   range         | array with 2 elements.  Default: [1900, 2999] -- the range of years available
+ *   weekNumbers   | (true/false) if it's true (default) the calendar will display week numbers
+ *   flat          | null or element ID; if not null the calendar will be a flat calendar having the parent with the given ID
+ *   flatCallback  | function that receives a JS Date object and returns an URL to point the browser to (for flat calendar)
+ *   disableFunc   | function that receives a JS Date object and should return true if that date has to be disabled in the calendar
+ *   onSelect      | function that gets called when a date is selected.  You don't _have_ to supply this (the default is generally okay)
+ *   onClose       | function that gets called when the calendar is closed.  [default]
+ *   onUpdate      | function that gets called after the date is updated in the input field.  Receives a reference to the calendar.
+ *   date          | the date that the calendar will be initially displayed to
+ *   showsTime     | default: false; if true the calendar will include a time selector
+ *   timeFormat    | the time format; can be "12" or "24", default is "12"
+ *
+ *  None of them is required, they all have default values.  However, if you
+ *  pass none of "inputField", "displayArea" or "button" you'll get a warning
+ *  saying "nothing to setup".
+ */
+Calendar.setup = function (params) {
+       function param_default(pname, def) { if (typeof params[pname] == "undefined") { params[pname] = def; } };
+
+       param_default("inputField",     null);
+       param_default("displayArea",    null);
+       param_default("button",         null);
+       param_default("eventName",      "click");
+       param_default("ifFormat",       "%Y/%m/%d");
+       param_default("daFormat",       "%Y/%m/%d");
+       param_default("singleClick",    true);
+       param_default("disableFunc",    null);
+       param_default("dateStatusFunc", params["disableFunc"]); // takes precedence if both are defined
+       param_default("mondayFirst",    true);
+       param_default("align",          "Bl");
+       param_default("range",          [1900, 2999]);
+       param_default("weekNumbers",    true);
+       param_default("flat",           null);
+       param_default("flatCallback",   null);
+       param_default("onSelect",       null);
+       param_default("onClose",        null);
+       param_default("onUpdate",       null);
+       param_default("date",           null);
+       param_default("showsTime",      false);
+       param_default("timeFormat",     "24");
+
+       var tmp = ["inputField", "displayArea", "button"];
+       for (var i in tmp) {
+               if (typeof params[tmp[i]] == "string") {
+                       params[tmp[i]] = document.getElementById(params[tmp[i]]);
+               }
+       }
+       if (!(params.flat || params.inputField || params.displayArea || params.button)) {
+               alert("Calendar.setup:\n  Nothing to setup (no fields found).  Please check your code");
+               return false;
+       }
+
+       function onSelect(cal) {
+               if (cal.params.flat) {
+                       if (typeof cal.params.flatCallback == "function") {
+                               cal.params.flatCallback(cal);
+                       } else {
+                               alert("No flatCallback given -- doing nothing.");
+                       }
+                       return false;
+               }
+               if (cal.params.inputField) {
+                       cal.params.inputField.value = cal.date.print(cal.params.ifFormat);
+               }
+               if (cal.params.displayArea) {
+                       cal.params.displayArea.innerHTML = cal.date.print(cal.params.daFormat);
+               }
+               if (cal.params.singleClick && cal.dateClicked) {
+                       cal.callCloseHandler();
+               }
+               if (typeof cal.params.onUpdate == "function") {
+                       cal.params.onUpdate(cal);
+               }
+       };
+
+       if (params.flat != null) {
+               params.flat = document.getElementById(params.flat);
+               if (!params.flat) {
+                       alert("Calendar.setup:\n  Flat specified but can't find parent.");
+                       return false;
+               }
+               var cal = new Calendar(params.mondayFirst, params.date, params.onSelect || onSelect);
+               cal.showsTime = params.showsTime;
+               cal.time24 = (params.timeFormat == "24");
+               cal.params = params;
+               cal.weekNumbers = params.weekNumbers;
+               cal.setRange(params.range[0], params.range[1]);
+               cal.setDateStatusHandler(params.dateStatusFunc);
+               cal.create(params.flat);
+               cal.show();
+               return false;
+       }
+
+       var triggerEl = params.button || params.displayArea || params.inputField;
+       triggerEl["on" + params.eventName] = function() {
+               var dateEl = params.inputField || params.displayArea;
+               var dateFmt = params.inputField ? params.ifFormat : params.daFormat;
+               var mustCreate = false;
+               var cal = window.calendar;
+               if (!window.calendar) {
+                       window.calendar = cal = new Calendar(params.mondayFirst,
+                                                            params.date,
+                                                            params.onSelect || onSelect,
+                                                            params.onClose || function(cal) { cal.hide(); });
+                       cal.showsTime = params.showsTime;
+                       cal.time24 = (params.timeFormat == "24");
+                       cal.weekNumbers = params.weekNumbers;
+                       mustCreate = true;
+               } else {
+                       cal.hide();
+               }
+               cal.setRange(params.range[0], params.range[1]);
+               cal.params = params;
+               cal.setDateStatusHandler(params.dateStatusFunc);
+               cal.setDateFormat(dateFmt);
+               if (mustCreate)
+                       cal.create();
+               cal.parseDate(dateEl.value || dateEl.innerHTML);
+               cal.refresh();
+               cal.showAtElement(params.displayArea || params.inputField, params.align);
+               return false;
+       };
+};
diff --git a/httemplate/elements/calendar-win2k-2.css b/httemplate/elements/calendar-win2k-2.css
new file mode 100644 (file)
index 0000000..9727d1b
--- /dev/null
@@ -0,0 +1,263 @@
+/* The main calendar widget.  DIV containing a table. */
+
+.calendar {
+  position: relative;
+  display: none;
+  border-top: 2px solid #fff;
+  border-right: 2px solid #000;
+  border-bottom: 2px solid #000;
+  border-left: 2px solid #fff;
+  font-size: 11px;
+  color: #000;
+  cursor: default;
+  background: #d4c8d0;
+  font-family: tahoma,verdana,sans-serif;
+}
+
+.calendar table {
+  border-top: 1px solid #000;
+  border-right: 1px solid #fff;
+  border-bottom: 1px solid #fff;
+  border-left: 1px solid #000;
+  font-size: 11px;
+  color: #000;
+  cursor: default;
+  background: #d4c8d0;
+  font-family: tahoma,verdana,sans-serif;
+}
+
+/* Header part -- contains navigation buttons and day names. */
+
+.calendar .button { /* "<<", "<", ">", ">>" buttons have this class */
+  text-align: center;
+  padding: 1px;
+  border-top: 1px solid #fff;
+  border-right: 1px solid #000;
+  border-bottom: 1px solid #000;
+  border-left: 1px solid #fff;
+}
+
+.calendar .nav {
+  background: transparent url(menuarrow.gif) no-repeat 100% 100%;
+}
+
+.calendar thead .title { /* This holds the current "month, year" */
+  font-weight: bold;
+  padding: 1px;
+  border: 1px solid #000;
+  background: #847880;
+  color: #fff;
+  text-align: center;
+}
+
+.calendar thead .headrow { /* Row <TR> containing navigation buttons */
+}
+
+.calendar thead .daynames { /* Row <TR> containing the day names */
+}
+
+.calendar thead .name { /* Cells <TD> containing the day names */
+  border-bottom: 1px solid #000;
+  padding: 2px;
+  text-align: center;
+  background: #f4e8f0;
+}
+
+.calendar thead .weekend { /* How a weekend day name shows in header */
+  color: #f00;
+}
+
+.calendar thead .hilite { /* How do the buttons in header appear when hover */
+  border-top: 2px solid #fff;
+  border-right: 2px solid #000;
+  border-bottom: 2px solid #000;
+  border-left: 2px solid #fff;
+  padding: 0px;
+  background-color: #e4d8e0;
+}
+
+.calendar thead .active { /* Active (pressed) buttons in header */
+  padding: 2px 0px 0px 2px;
+  border-top: 1px solid #000;
+  border-right: 1px solid #fff;
+  border-bottom: 1px solid #fff;
+  border-left: 1px solid #000;
+  background-color: #c4b8c0;
+}
+
+/* The body part -- contains all the days in month. */
+
+.calendar tbody .day { /* Cells <TD> containing month days dates */
+  width: 2em;
+  text-align: right;
+  padding: 2px 4px 2px 2px;
+}
+
+.calendar table .wn {
+  padding: 2px 3px 2px 2px;
+  border-right: 1px solid #000;
+  background: #f4e8f0;
+}
+
+.calendar tbody .rowhilite td {
+  background: #e4d8e0;
+}
+
+.calendar tbody .rowhilite td.wn {
+  background: #d4c8d0;
+}
+
+.calendar tbody td.hilite { /* Hovered cells <TD> */
+  padding: 1px 3px 1px 1px;
+  border-top: 1px solid #fff;
+  border-right: 1px solid #000;
+  border-bottom: 1px solid #000;
+  border-left: 1px solid #fff;
+}
+
+.calendar tbody td.active { /* Active (pressed) cells <TD> */
+  padding: 2px 2px 0px 2px;
+  border-top: 1px solid #000;
+  border-right: 1px solid #fff;
+  border-bottom: 1px solid #fff;
+  border-left: 1px solid #000;
+}
+
+.calendar tbody td.selected { /* Cell showing selected date */
+  font-weight: bold;
+  border-top: 1px solid #000;
+  border-right: 1px solid #fff;
+  border-bottom: 1px solid #fff;
+  border-left: 1px solid #000;
+  padding: 2px 2px 0px 2px;
+  background: #e4d8e0;
+}
+
+.calendar tbody td.weekend { /* Cells showing weekend days */
+  color: #f00;
+}
+
+.calendar tbody td.today { /* Cell showing today date */
+  font-weight: bold;
+  color: #00f;
+}
+
+.calendar tbody .disabled { color: #999; }
+
+.calendar tbody .emptycell { /* Empty cells (the best is to hide them) */
+  visibility: hidden;
+}
+
+.calendar tbody .emptyrow { /* Empty row (some months need less than 6 rows) */
+  display: none;
+}
+
+/* The footer part -- status bar and "Close" button */
+
+.calendar tfoot .footrow { /* The <TR> in footer (only one right now) */
+}
+
+.calendar tfoot .ttip { /* Tooltip (status bar) cell <TD> */
+  background: #f4e8f0;
+  padding: 1px;
+  border: 1px solid #000;
+  background: #847880;
+  color: #fff;
+  text-align: center;
+}
+
+.calendar tfoot .hilite { /* Hover style for buttons in footer */
+  border-top: 1px solid #fff;
+  border-right: 1px solid #000;
+  border-bottom: 1px solid #000;
+  border-left: 1px solid #fff;
+  padding: 1px;
+  background: #e4d8e0;
+}
+
+.calendar tfoot .active { /* Active (pressed) style for buttons in footer */
+  padding: 2px 0px 0px 2px;
+  border-top: 1px solid #000;
+  border-right: 1px solid #fff;
+  border-bottom: 1px solid #fff;
+  border-left: 1px solid #000;
+}
+
+/* Combo boxes (menus that display months/years for direct selection) */
+
+.combo {
+  position: absolute;
+  display: none;
+  width: 4em;
+  top: 0px;
+  left: 0px;
+  cursor: default;
+  border-top: 1px solid #fff;
+  border-right: 1px solid #000;
+  border-bottom: 1px solid #000;
+  border-left: 1px solid #fff;
+  background: #e4d8e0;
+  font-size: smaller;
+  padding: 1px;
+}
+
+.combo .label,
+.combo .label-IEfix {
+  text-align: center;
+  padding: 1px;
+}
+
+.combo .label-IEfix {
+  width: 4em;
+}
+
+.combo .active {
+  background: #d4c8d0;
+  padding: 0px;
+  border-top: 1px solid #000;
+  border-right: 1px solid #fff;
+  border-bottom: 1px solid #fff;
+  border-left: 1px solid #000;
+}
+
+.combo .hilite {
+  background: #408;
+  color: #fea;
+}
+
+.calendar td.time {
+  border-top: 1px solid #000;
+  padding: 1px 0px;
+  text-align: center;
+  background-color: #f4f0e8;
+}
+
+.calendar td.time .hour,
+.calendar td.time .minute,
+.calendar td.time .ampm {
+  padding: 0px 3px 0px 4px;
+  border: 1px solid #889;
+  font-weight: bold;
+  background-color: #fff;
+}
+
+.calendar td.time .ampm {
+  text-align: center;
+}
+
+.calendar td.time .colon {
+  padding: 0px 2px 0px 3px;
+  font-weight: bold;
+}
+
+.calendar td.time span.hilite {
+  border-color: #000;
+  background-color: #766;
+  color: #fff;
+}
+
+.calendar td.time span.active {
+  border-color: #f00;
+  background-color: #000;
+  color: #0f0;
+}
diff --git a/httemplate/elements/calendar.js b/httemplate/elements/calendar.js
new file mode 100644 (file)
index 0000000..3c028cc
--- /dev/null
@@ -0,0 +1,1641 @@
+/*  Copyright Mihai Bazon, 2002, 2003  |  http://dynarch.com/mishoo/
+ * ------------------------------------------------------------------
+ *
+ * The DHTML Calendar, version 0.9.5 "Your favorite time, bis"
+ *
+ * Details and latest version at:
+ * http://dynarch.com/mishoo/calendar.epl
+ *
+ * This script is distributed under the GNU Lesser General Public License.
+ * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html
+ */
+
+// $Id: calendar.js,v 1.3 2003-11-07 10:53:35 ivan Exp $
+
+/** The Calendar object constructor. */
+Calendar = function (mondayFirst, dateStr, onSelected, onClose) {
+       // member variables
+       this.activeDiv = null;
+       this.currentDateEl = null;
+       this.getDateStatus = null;
+       this.timeout = null;
+       this.onSelected = onSelected || null;
+       this.onClose = onClose || null;
+       this.dragging = false;
+       this.hidden = false;
+       this.minYear = 1970;
+       this.maxYear = 2050;
+       this.dateFormat = Calendar._TT["DEF_DATE_FORMAT"];
+       this.ttDateFormat = Calendar._TT["TT_DATE_FORMAT"];
+       this.isPopup = true;
+       this.weekNumbers = true;
+       this.mondayFirst = mondayFirst;
+       this.dateStr = dateStr;
+       this.ar_days = null;
+       this.showsTime = false;
+       this.time24 = true;
+       // HTML elements
+       this.table = null;
+       this.element = null;
+       this.tbody = null;
+       this.firstdayname = null;
+       // Combo boxes
+       this.monthsCombo = null;
+       this.yearsCombo = null;
+       this.hilitedMonth = null;
+       this.activeMonth = null;
+       this.hilitedYear = null;
+       this.activeYear = null;
+       // Information
+       this.dateClicked = false;
+
+       // one-time initializations
+       if (typeof Calendar._SDN == "undefined") {
+               // table of short day names
+               if (typeof Calendar._SDN_len == "undefined")
+                       Calendar._SDN_len = 3;
+               var ar = new Array();
+               for (var i = 8; i > 0;) {
+                       ar[--i] = Calendar._DN[i].substr(0, Calendar._SDN_len);
+               }
+               Calendar._SDN = ar;
+               // table of short month names
+               if (typeof Calendar._SMN_len == "undefined")
+                       Calendar._SMN_len = 3;
+               ar = new Array();
+               for (var i = 12; i > 0;) {
+                       ar[--i] = Calendar._MN[i].substr(0, Calendar._SMN_len);
+               }
+               Calendar._SMN = ar;
+       }
+};
+
+// ** constants
+
+/// "static", needed for event handlers.
+Calendar._C = null;
+
+/// detect a special case of "web browser"
+Calendar.is_ie = ( /msie/i.test(navigator.userAgent) &&
+                  !/opera/i.test(navigator.userAgent) );
+
+/// detect Opera browser
+Calendar.is_opera = /opera/i.test(navigator.userAgent);
+
+/// detect KHTML-based browsers
+Calendar.is_khtml = /Konqueror|Safari|KHTML/i.test(navigator.userAgent);
+
+// BEGIN: UTILITY FUNCTIONS; beware that these might be moved into a separate
+//        library, at some point.
+
+Calendar.getAbsolutePos = function(el) {
+       var SL = 0, ST = 0;
+       var is_div = /^div$/i.test(el.tagName);
+       if (is_div && el.scrollLeft)
+               SL = el.scrollLeft;
+       if (is_div && el.scrollTop)
+               ST = el.scrollTop;
+       var r = { x: el.offsetLeft - SL, y: el.offsetTop - ST };
+       if (el.offsetParent) {
+               var tmp = Calendar.getAbsolutePos(el.offsetParent);
+               r.x += tmp.x;
+               r.y += tmp.y;
+       }
+       return r;
+};
+
+Calendar.isRelated = function (el, evt) {
+       var related = evt.relatedTarget;
+       if (!related) {
+               var type = evt.type;
+               if (type == "mouseover") {
+                       related = evt.fromElement;
+               } else if (type == "mouseout") {
+                       related = evt.toElement;
+               }
+       }
+       while (related) {
+               if (related == el) {
+                       return true;
+               }
+               related = related.parentNode;
+       }
+       return false;
+};
+
+Calendar.removeClass = function(el, className) {
+       if (!(el && el.className)) {
+               return;
+       }
+       var cls = el.className.split(" ");
+       var ar = new Array();
+       for (var i = cls.length; i > 0;) {
+               if (cls[--i] != className) {
+                       ar[ar.length] = cls[i];
+               }
+       }
+       el.className = ar.join(" ");
+};
+
+Calendar.addClass = function(el, className) {
+       Calendar.removeClass(el, className);
+       el.className += " " + className;
+};
+
+Calendar.getElement = function(ev) {
+       if (Calendar.is_ie) {
+               return window.event.srcElement;
+       } else {
+               return ev.currentTarget;
+       }
+};
+
+Calendar.getTargetElement = function(ev) {
+       if (Calendar.is_ie) {
+               return window.event.srcElement;
+       } else {
+               return ev.target;
+       }
+};
+
+Calendar.stopEvent = function(ev) {
+       ev || (ev = window.event);
+       if (Calendar.is_ie) {
+               ev.cancelBubble = true;
+               ev.returnValue = false;
+       } else {
+               ev.preventDefault();
+               ev.stopPropagation();
+       }
+       return false;
+};
+
+Calendar.addEvent = function(el, evname, func) {
+       if (el.attachEvent) { // IE
+               el.attachEvent("on" + evname, func);
+       } else if (el.addEventListener) { // Gecko / W3C
+               el.addEventListener(evname, func, true);
+       } else {
+               el["on" + evname] = func;
+       }
+};
+
+Calendar.removeEvent = function(el, evname, func) {
+       if (el.detachEvent) { // IE
+               el.detachEvent("on" + evname, func);
+       } else if (el.removeEventListener) { // Gecko / W3C
+               el.removeEventListener(evname, func, true);
+       } else {
+               el["on" + evname] = null;
+       }
+};
+
+Calendar.createElement = function(type, parent) {
+       var el = null;
+       if (document.createElementNS) {
+               // use the XHTML namespace; IE won't normally get here unless
+               // _they_ "fix" the DOM2 implementation.
+               el = document.createElementNS("http://www.w3.org/1999/xhtml", type);
+       } else {
+               el = document.createElement(type);
+       }
+       if (typeof parent != "undefined") {
+               parent.appendChild(el);
+       }
+       return el;
+};
+
+// END: UTILITY FUNCTIONS
+
+// BEGIN: CALENDAR STATIC FUNCTIONS
+
+/** Internal -- adds a set of events to make some element behave like a button. */
+Calendar._add_evs = function(el) {
+       with (Calendar) {
+               addEvent(el, "mouseover", dayMouseOver);
+               addEvent(el, "mousedown", dayMouseDown);
+               addEvent(el, "mouseout", dayMouseOut);
+               if (is_ie) {
+                       addEvent(el, "dblclick", dayMouseDblClick);
+                       el.setAttribute("unselectable", true);
+               }
+       }
+};
+
+Calendar.findMonth = function(el) {
+       if (typeof el.month != "undefined") {
+               return el;
+       } else if (typeof el.parentNode.month != "undefined") {
+               return el.parentNode;
+       }
+       return null;
+};
+
+Calendar.findYear = function(el) {
+       if (typeof el.year != "undefined") {
+               return el;
+       } else if (typeof el.parentNode.year != "undefined") {
+               return el.parentNode;
+       }
+       return null;
+};
+
+Calendar.showMonthsCombo = function () {
+       var cal = Calendar._C;
+       if (!cal) {
+               return false;
+       }
+       var cal = cal;
+       var cd = cal.activeDiv;
+       var mc = cal.monthsCombo;
+       if (cal.hilitedMonth) {
+               Calendar.removeClass(cal.hilitedMonth, "hilite");
+       }
+       if (cal.activeMonth) {
+               Calendar.removeClass(cal.activeMonth, "active");
+       }
+       var mon = cal.monthsCombo.getElementsByTagName("div")[cal.date.getMonth()];
+       Calendar.addClass(mon, "active");
+       cal.activeMonth = mon;
+       var s = mc.style;
+       s.display = "block";
+       if (cd.navtype < 0)
+               s.left = cd.offsetLeft + "px";
+       else
+               s.left = (cd.offsetLeft + cd.offsetWidth - mc.offsetWidth) + "px";
+       s.top = (cd.offsetTop + cd.offsetHeight) + "px";
+};
+
+Calendar.showYearsCombo = function (fwd) {
+       var cal = Calendar._C;
+       if (!cal) {
+               return false;
+       }
+       var cal = cal;
+       var cd = cal.activeDiv;
+       var yc = cal.yearsCombo;
+       if (cal.hilitedYear) {
+               Calendar.removeClass(cal.hilitedYear, "hilite");
+       }
+       if (cal.activeYear) {
+               Calendar.removeClass(cal.activeYear, "active");
+       }
+       cal.activeYear = null;
+       var Y = cal.date.getFullYear() + (fwd ? 1 : -1);
+       var yr = yc.firstChild;
+       var show = false;
+       for (var i = 12; i > 0; --i) {
+               if (Y >= cal.minYear && Y <= cal.maxYear) {
+                       yr.firstChild.data = Y;
+                       yr.year = Y;
+                       yr.style.display = "block";
+                       show = true;
+               } else {
+                       yr.style.display = "none";
+               }
+               yr = yr.nextSibling;
+               Y += fwd ? 2 : -2;
+       }
+       if (show) {
+               var s = yc.style;
+               s.display = "block";
+               if (cd.navtype < 0)
+                       s.left = cd.offsetLeft + "px";
+               else
+                       s.left = (cd.offsetLeft + cd.offsetWidth - yc.offsetWidth) + "px";
+               s.top = (cd.offsetTop + cd.offsetHeight) + "px";
+       }
+};
+
+// event handlers
+
+Calendar.tableMouseUp = function(ev) {
+       var cal = Calendar._C;
+       if (!cal) {
+               return false;
+       }
+       if (cal.timeout) {
+               clearTimeout(cal.timeout);
+       }
+       var el = cal.activeDiv;
+       if (!el) {
+               return false;
+       }
+       var target = Calendar.getTargetElement(ev);
+       ev || (ev = window.event);
+       Calendar.removeClass(el, "active");
+       if (target == el || target.parentNode == el) {
+               Calendar.cellClick(el, ev);
+       }
+       var mon = Calendar.findMonth(target);
+       var date = null;
+       if (mon) {
+               date = new Date(cal.date);
+               if (mon.month != date.getMonth()) {
+                       date.setMonth(mon.month);
+                       cal.setDate(date);
+                       cal.dateClicked = false;
+                       cal.callHandler();
+               }
+       } else {
+               var year = Calendar.findYear(target);
+               if (year) {
+                       date = new Date(cal.date);
+                       if (year.year != date.getFullYear()) {
+                               date.setFullYear(year.year);
+                               cal.setDate(date);
+                               cal.dateClicked = false;
+                               cal.callHandler();
+                       }
+               }
+       }
+       with (Calendar) {
+               removeEvent(document, "mouseup", tableMouseUp);
+               removeEvent(document, "mouseover", tableMouseOver);
+               removeEvent(document, "mousemove", tableMouseOver);
+               cal._hideCombos();
+               _C = null;
+               return stopEvent(ev);
+       }
+};
+
+Calendar.tableMouseOver = function (ev) {
+       var cal = Calendar._C;
+       if (!cal) {
+               return;
+       }
+       var el = cal.activeDiv;
+       var target = Calendar.getTargetElement(ev);
+       if (target == el || target.parentNode == el) {
+               Calendar.addClass(el, "hilite active");
+               Calendar.addClass(el.parentNode, "rowhilite");
+       } else {
+               if (typeof el.navtype == "undefined" || (el.navtype != 50 && (el.navtype == 0 || Math.abs(el.navtype) > 2)))
+                       Calendar.removeClass(el, "active");
+               Calendar.removeClass(el, "hilite");
+               Calendar.removeClass(el.parentNode, "rowhilite");
+       }
+       ev || (ev = window.event);
+       if (el.navtype == 50 && target != el) {
+               var pos = Calendar.getAbsolutePos(el);
+               var w = el.offsetWidth;
+               var x = ev.clientX;
+               var dx;
+               var decrease = true;
+               if (x > pos.x + w) {
+                       dx = x - pos.x - w;
+                       decrease = false;
+               } else
+                       dx = pos.x - x;
+
+               if (dx < 0) dx = 0;
+               var range = el._range;
+               var current = el._current;
+               var count = Math.floor(dx / 10) % range.length;
+               for (var i = range.length; --i >= 0;)
+                       if (range[i] == current)
+                               break;
+               while (count-- > 0)
+                       if (decrease) {
+                               if (!(--i in range))
+                                       i = range.length - 1;
+                       } else if (!(++i in range))
+                               i = 0;
+               var newval = range[i];
+               el.firstChild.data = newval;
+
+               cal.onUpdateTime();
+       }
+       var mon = Calendar.findMonth(target);
+       if (mon) {
+               if (mon.month != cal.date.getMonth()) {
+                       if (cal.hilitedMonth) {
+                               Calendar.removeClass(cal.hilitedMonth, "hilite");
+                       }
+                       Calendar.addClass(mon, "hilite");
+                       cal.hilitedMonth = mon;
+               } else if (cal.hilitedMonth) {
+                       Calendar.removeClass(cal.hilitedMonth, "hilite");
+               }
+       } else {
+               if (cal.hilitedMonth) {
+                       Calendar.removeClass(cal.hilitedMonth, "hilite");
+               }
+               var year = Calendar.findYear(target);
+               if (year) {
+                       if (year.year != cal.date.getFullYear()) {
+                               if (cal.hilitedYear) {
+                                       Calendar.removeClass(cal.hilitedYear, "hilite");
+                               }
+                               Calendar.addClass(year, "hilite");
+                               cal.hilitedYear = year;
+                       } else if (cal.hilitedYear) {
+                               Calendar.removeClass(cal.hilitedYear, "hilite");
+                       }
+               } else if (cal.hilitedYear) {
+                       Calendar.removeClass(cal.hilitedYear, "hilite");
+               }
+       }
+       return Calendar.stopEvent(ev);
+};
+
+Calendar.tableMouseDown = function (ev) {
+       if (Calendar.getTargetElement(ev) == Calendar.getElement(ev)) {
+               return Calendar.stopEvent(ev);
+       }
+};
+
+Calendar.calDragIt = function (ev) {
+       var cal = Calendar._C;
+       if (!(cal && cal.dragging)) {
+               return false;
+       }
+       var posX;
+       var posY;
+       if (Calendar.is_ie) {
+               posY = window.event.clientY + document.body.scrollTop;
+               posX = window.event.clientX + document.body.scrollLeft;
+       } else {
+               posX = ev.pageX;
+               posY = ev.pageY;
+       }
+       cal.hideShowCovered();
+       var st = cal.element.style;
+       st.left = (posX - cal.xOffs) + "px";
+       st.top = (posY - cal.yOffs) + "px";
+       return Calendar.stopEvent(ev);
+};
+
+Calendar.calDragEnd = function (ev) {
+       var cal = Calendar._C;
+       if (!cal) {
+               return false;
+       }
+       cal.dragging = false;
+       with (Calendar) {
+               removeEvent(document, "mousemove", calDragIt);
+               removeEvent(document, "mouseover", stopEvent);
+               removeEvent(document, "mouseup", calDragEnd);
+               tableMouseUp(ev);
+       }
+       cal.hideShowCovered();
+};
+
+Calendar.dayMouseDown = function(ev) {
+       var el = Calendar.getElement(ev);
+       if (el.disabled) {
+               return false;
+       }
+       var cal = el.calendar;
+       cal.activeDiv = el;
+       Calendar._C = cal;
+       if (el.navtype != 300) with (Calendar) {
+               if (el.navtype == 50)
+                       el._current = el.firstChild.data;
+               addClass(el, "hilite active");
+               addEvent(document, "mouseover", tableMouseOver);
+               addEvent(document, "mousemove", tableMouseOver);
+               addEvent(document, "mouseup", tableMouseUp);
+       } else if (cal.isPopup) {
+               cal._dragStart(ev);
+       }
+       if (el.navtype == -1 || el.navtype == 1) {
+               if (cal.timeout) clearTimeout(cal.timeout);
+               cal.timeout = setTimeout("Calendar.showMonthsCombo()", 250);
+       } else if (el.navtype == -2 || el.navtype == 2) {
+               if (cal.timeout) clearTimeout(cal.timeout);
+               cal.timeout = setTimeout((el.navtype > 0) ? "Calendar.showYearsCombo(true)" : "Calendar.showYearsCombo(false)", 250);
+       } else {
+               cal.timeout = null;
+       }
+       return Calendar.stopEvent(ev);
+};
+
+Calendar.dayMouseDblClick = function(ev) {
+       Calendar.cellClick(Calendar.getElement(ev), ev || window.event);
+       if (Calendar.is_ie) {
+               document.selection.empty();
+       }
+};
+
+Calendar.dayMouseOver = function(ev) {
+       var el = Calendar.getElement(ev);
+       if (Calendar.isRelated(el, ev) || Calendar._C || el.disabled) {
+               return false;
+       }
+       if (el.ttip) {
+               if (el.ttip.substr(0, 1) == "_") {
+                       var date = null;
+                       with (el.calendar.date) {
+                               date = new Date(getFullYear(), getMonth(), el.caldate);
+                       }
+                       el.ttip = date.print(el.calendar.ttDateFormat) + el.ttip.substr(1);
+               }
+               el.calendar.tooltips.firstChild.data = el.ttip;
+       }
+       if (el.navtype != 300) {
+               Calendar.addClass(el, "hilite");
+               if (el.caldate) {
+                       Calendar.addClass(el.parentNode, "rowhilite");
+               }
+       }
+       return Calendar.stopEvent(ev);
+};
+
+Calendar.dayMouseOut = function(ev) {
+       with (Calendar) {
+               var el = getElement(ev);
+               if (isRelated(el, ev) || _C || el.disabled) {
+                       return false;
+               }
+               removeClass(el, "hilite");
+               if (el.caldate) {
+                       removeClass(el.parentNode, "rowhilite");
+               }
+               el.calendar.tooltips.firstChild.data = _TT["SEL_DATE"];
+               return stopEvent(ev);
+       }
+};
+
+/**
+ *  A generic "click" handler :) handles all types of buttons defined in this
+ *  calendar.
+ */
+Calendar.cellClick = function(el, ev) {
+       var cal = el.calendar;
+       var closing = false;
+       var newdate = false;
+       var date = null;
+       if (typeof el.navtype == "undefined") {
+               Calendar.removeClass(cal.currentDateEl, "selected");
+               Calendar.addClass(el, "selected");
+               closing = (cal.currentDateEl == el);
+               if (!closing) {
+                       cal.currentDateEl = el;
+               }
+               cal.date.setDate(el.caldate);
+               date = cal.date;
+               newdate = true;
+               // a date was clicked
+               cal.dateClicked = true;
+       } else {
+               if (el.navtype == 200) {
+                       Calendar.removeClass(el, "hilite");
+                       cal.callCloseHandler();
+                       return;
+               }
+               date = (el.navtype == 0) ? new Date() : new Date(cal.date);
+               // unless "today" was clicked, we assume no date was clicked so
+               // the selected handler will know not to close the calenar when
+               // in single-click mode.
+               // cal.dateClicked = (el.navtype == 0);
+               cal.dateClicked = false;
+               var year = date.getFullYear();
+               var mon = date.getMonth();
+               function setMonth(m) {
+                       var day = date.getDate();
+                       var max = date.getMonthDays(m);
+                       if (day > max) {
+                               date.setDate(max);
+                       }
+                       date.setMonth(m);
+               };
+               switch (el.navtype) {
+                   case 400:
+                       Calendar.removeClass(el, "hilite");
+                       var text = Calendar._TT["ABOUT"];
+                       if (typeof text != "undefined") {
+                               text += cal.showsTime ? Calendar._TT["ABOUT_TIME"] : "";
+                       } else {
+                               // FIXME: this should be removed as soon as lang files get updated!
+                               text = "Help and about box text is not translated into this language.\n" +
+                                       "If you know this language and you feel generous please update\n" +
+                                       "the corresponding file in \"lang\" subdir to match calendar-en.js\n" +
+                                       "and send it back to <mishoo@infoiasi.ro> to get it into the distribution  ;-)\n\n" +
+                                       "Thank you!\n" +
+                                       "http://dynarch.com/mishoo/calendar.epl\n";
+                       }
+                       alert(text);
+                       return;
+                   case -2:
+                       if (year > cal.minYear) {
+                               date.setFullYear(year - 1);
+                       }
+                       break;
+                   case -1:
+                       if (mon > 0) {
+                               setMonth(mon - 1);
+                       } else if (year-- > cal.minYear) {
+                               date.setFullYear(year);
+                               setMonth(11);
+                       }
+                       break;
+                   case 1:
+                       if (mon < 11) {
+                               setMonth(mon + 1);
+                       } else if (year < cal.maxYear) {
+                               date.setFullYear(year + 1);
+                               setMonth(0);
+                       }
+                       break;
+                   case 2:
+                       if (year < cal.maxYear) {
+                               date.setFullYear(year + 1);
+                       }
+                       break;
+                   case 100:
+                       cal.setMondayFirst(!cal.mondayFirst);
+                       return;
+                   case 50:
+                       var range = el._range;
+                       var current = el.firstChild.data;
+                       for (var i = range.length; --i >= 0;)
+                               if (range[i] == current)
+                                       break;
+                       if (ev && ev.shiftKey) {
+                               if (!(--i in range))
+                                       i = range.length - 1;
+                       } else if (!(++i in range))
+                               i = 0;
+                       var newval = range[i];
+                       el.firstChild.data = newval;
+                       cal.onUpdateTime();
+                       return;
+                   case 0:
+                       // TODAY will bring us here
+                       if ((typeof cal.getDateStatus == "function") && cal.getDateStatus(date, date.getFullYear(), date.getMonth(), date.getDate())) {
+                               // remember, "date" was previously set to new
+                               // Date() if TODAY was clicked; thus, it
+                               // contains today date.
+                               return false;
+                       }
+                       break;
+               }
+               if (!date.equalsTo(cal.date)) {
+                       cal.setDate(date);
+                       newdate = true;
+               }
+       }
+       if (newdate) {
+               cal.callHandler();
+       }
+       if (closing) {
+               Calendar.removeClass(el, "hilite");
+               cal.callCloseHandler();
+       }
+};
+
+// END: CALENDAR STATIC FUNCTIONS
+
+// BEGIN: CALENDAR OBJECT FUNCTIONS
+
+/**
+ *  This function creates the calendar inside the given parent.  If _par is
+ *  null than it creates a popup calendar inside the BODY element.  If _par is
+ *  an element, be it BODY, then it creates a non-popup calendar (still
+ *  hidden).  Some properties need to be set before calling this function.
+ */
+Calendar.prototype.create = function (_par) {
+       var parent = null;
+       if (! _par) {
+               // default parent is the document body, in which case we create
+               // a popup calendar.
+               parent = document.getElementsByTagName("body")[0];
+               this.isPopup = true;
+       } else {
+               parent = _par;
+               this.isPopup = false;
+       }
+       this.date = this.dateStr ? new Date(this.dateStr) : new Date();
+
+       var table = Calendar.createElement("table");
+       this.table = table;
+       table.cellSpacing = 0;
+       table.cellPadding = 0;
+       table.calendar = this;
+       Calendar.addEvent(table, "mousedown", Calendar.tableMouseDown);
+
+       var div = Calendar.createElement("div");
+       this.element = div;
+       div.className = "calendar";
+       if (this.isPopup) {
+               div.style.position = "absolute";
+               div.style.display = "none";
+       }
+       div.appendChild(table);
+
+       var thead = Calendar.createElement("thead", table);
+       var cell = null;
+       var row = null;
+
+       var cal = this;
+       var hh = function (text, cs, navtype) {
+               cell = Calendar.createElement("td", row);
+               cell.colSpan = cs;
+               cell.className = "button";
+               if (navtype != 0 && Math.abs(navtype) <= 2)
+                       cell.className += " nav";
+               Calendar._add_evs(cell);
+               cell.calendar = cal;
+               cell.navtype = navtype;
+               if (text.substr(0, 1) != "&") {
+                       cell.appendChild(document.createTextNode(text));
+               }
+               else {
+                       // FIXME: dirty hack for entities
+                       cell.innerHTML = text;
+               }
+               return cell;
+       };
+
+       row = Calendar.createElement("tr", thead);
+       var title_length = 6;
+       (this.isPopup) && --title_length;
+       (this.weekNumbers) && ++title_length;
+
+       hh("?", 1, 400).ttip = Calendar._TT["INFO"];
+       this.title = hh("", title_length, 300);
+       this.title.className = "title";
+       if (this.isPopup) {
+               this.title.ttip = Calendar._TT["DRAG_TO_MOVE"];
+               this.title.style.cursor = "move";
+               hh("&#x00d7;", 1, 200).ttip = Calendar._TT["CLOSE"];
+       }
+
+       row = Calendar.createElement("tr", thead);
+       row.className = "headrow";
+
+       this._nav_py = hh("&#x00ab;", 1, -2);
+       this._nav_py.ttip = Calendar._TT["PREV_YEAR"];
+
+       this._nav_pm = hh("&#x2039;", 1, -1);
+       this._nav_pm.ttip = Calendar._TT["PREV_MONTH"];
+
+       this._nav_now = hh(Calendar._TT["TODAY"], this.weekNumbers ? 4 : 3, 0);
+       this._nav_now.ttip = Calendar._TT["GO_TODAY"];
+
+       this._nav_nm = hh("&#x203a;", 1, 1);
+       this._nav_nm.ttip = Calendar._TT["NEXT_MONTH"];
+
+       this._nav_ny = hh("&#x00bb;", 1, 2);
+       this._nav_ny.ttip = Calendar._TT["NEXT_YEAR"];
+
+       // day names
+       row = Calendar.createElement("tr", thead);
+       row.className = "daynames";
+       if (this.weekNumbers) {
+               cell = Calendar.createElement("td", row);
+               cell.className = "name wn";
+               cell.appendChild(document.createTextNode(Calendar._TT["WK"]));
+       }
+       for (var i = 7; i > 0; --i) {
+               cell = Calendar.createElement("td", row);
+               cell.appendChild(document.createTextNode(""));
+               if (!i) {
+                       cell.navtype = 100;
+                       cell.calendar = this;
+                       Calendar._add_evs(cell);
+               }
+       }
+       this.firstdayname = (this.weekNumbers) ? row.firstChild.nextSibling : row.firstChild;
+       this._displayWeekdays();
+
+       var tbody = Calendar.createElement("tbody", table);
+       this.tbody = tbody;
+
+       for (i = 6; i > 0; --i) {
+               row = Calendar.createElement("tr", tbody);
+               if (this.weekNumbers) {
+                       cell = Calendar.createElement("td", row);
+                       cell.appendChild(document.createTextNode(""));
+               }
+               for (var j = 7; j > 0; --j) {
+                       cell = Calendar.createElement("td", row);
+                       cell.appendChild(document.createTextNode(""));
+                       cell.calendar = this;
+                       Calendar._add_evs(cell);
+               }
+       }
+
+       if (this.showsTime) {
+               row = Calendar.createElement("tr", tbody);
+               row.className = "time";
+
+               cell = Calendar.createElement("td", row);
+               cell.className = "time";
+               cell.colSpan = 2;
+               cell.innerHTML = "&nbsp;";
+
+               cell = Calendar.createElement("td", row);
+               cell.className = "time";
+               cell.colSpan = this.weekNumbers ? 4 : 3;
+
+               (function(){
+                       function makeTimePart(className, init, range_start, range_end) {
+                               var part = Calendar.createElement("span", cell);
+                               part.className = className;
+                               part.appendChild(document.createTextNode(init));
+                               part.calendar = cal;
+                               part.ttip = Calendar._TT["TIME_PART"];
+                               part.navtype = 50;
+                               part._range = [];
+                               if (typeof range_start != "number")
+                                       part._range = range_start;
+                               else {
+                                       for (var i = range_start; i <= range_end; ++i) {
+                                               var txt;
+                                               if (i < 10 && range_end >= 10) txt = '0' + i;
+                                               else txt = '' + i;
+                                               part._range[part._range.length] = txt;
+                                       }
+                               }
+                               Calendar._add_evs(part);
+                               return part;
+                       };
+                       var hrs = cal.date.getHours();
+                       var mins = cal.date.getMinutes();
+                       var t12 = !cal.time24;
+                       var pm = (hrs > 12);
+                       if (t12 && pm) hrs -= 12;
+                       var H = makeTimePart("hour", hrs, t12 ? 1 : 0, t12 ? 12 : 23);
+                       var span = Calendar.createElement("span", cell);
+                       span.appendChild(document.createTextNode(":"));
+                       span.className = "colon";
+                       var M = makeTimePart("minute", mins, 0, 59);
+                       var AP = null;
+                       cell = Calendar.createElement("td", row);
+                       cell.className = "time";
+                       cell.colSpan = 2;
+                       if (t12)
+                               AP = makeTimePart("ampm", pm ? "pm" : "am", ["am", "pm"]);
+                       else
+                               cell.innerHTML = "&nbsp;";
+
+                       cal.onSetTime = function() {
+                               var hrs = this.date.getHours();
+                               var mins = this.date.getMinutes();
+                               var pm = (hrs > 12);
+                               if (pm && t12) hrs -= 12;
+                               H.firstChild.data = (hrs < 10) ? ("0" + hrs) : hrs;
+                               M.firstChild.data = (mins < 10) ? ("0" + mins) : mins;
+                               if (t12)
+                                       AP.firstChild.data = pm ? "pm" : "am";
+                       };
+
+                       cal.onUpdateTime = function() {
+                               var date = this.date;
+                               var h = parseInt(H.firstChild.data, 10);
+                               if (t12) {
+                                       if (/pm/i.test(AP.firstChild.data) && h < 12)
+                                               h += 12;
+                                       else if (/am/i.test(AP.firstChild.data) && h == 12)
+                                               h = 0;
+                               }
+                               var d = date.getDate();
+                               var m = date.getMonth();
+                               var y = date.getFullYear();
+                               date.setHours(h);
+                               date.setMinutes(parseInt(M.firstChild.data, 10));
+                               date.setFullYear(y);
+                               date.setMonth(m);
+                               date.setDate(d);
+                               this.dateClicked = false;
+                               this.callHandler();
+                       };
+               })();
+       } else {
+               this.onSetTime = this.onUpdateTime = function() {};
+       }
+
+       var tfoot = Calendar.createElement("tfoot", table);
+
+       row = Calendar.createElement("tr", tfoot);
+       row.className = "footrow";
+
+       cell = hh(Calendar._TT["SEL_DATE"], this.weekNumbers ? 8 : 7, 300);
+       cell.className = "ttip";
+       if (this.isPopup) {
+               cell.ttip = Calendar._TT["DRAG_TO_MOVE"];
+               cell.style.cursor = "move";
+       }
+       this.tooltips = cell;
+
+       div = Calendar.createElement("div", this.element);
+       this.monthsCombo = div;
+       div.className = "combo";
+       for (i = 0; i < Calendar._MN.length; ++i) {
+               var mn = Calendar.createElement("div");
+               mn.className = Calendar.is_ie ? "label-IEfix" : "label";
+               mn.month = i;
+               mn.appendChild(document.createTextNode(Calendar._SMN[i]));
+               div.appendChild(mn);
+       }
+
+       div = Calendar.createElement("div", this.element);
+       this.yearsCombo = div;
+       div.className = "combo";
+       for (i = 12; i > 0; --i) {
+               var yr = Calendar.createElement("div");
+               yr.className = Calendar.is_ie ? "label-IEfix" : "label";
+               yr.appendChild(document.createTextNode(""));
+               div.appendChild(yr);
+       }
+
+       this._init(this.mondayFirst, this.date);
+       parent.appendChild(this.element);
+};
+
+/** keyboard navigation, only for popup calendars */
+Calendar._keyEvent = function(ev) {
+       if (!window.calendar) {
+               return false;
+       }
+       (Calendar.is_ie) && (ev = window.event);
+       var cal = window.calendar;
+       var act = (Calendar.is_ie || ev.type == "keypress");
+       if (ev.ctrlKey) {
+               switch (ev.keyCode) {
+                   case 37: // KEY left
+                       act && Calendar.cellClick(cal._nav_pm);
+                       break;
+                   case 38: // KEY up
+                       act && Calendar.cellClick(cal._nav_py);
+                       break;
+                   case 39: // KEY right
+                       act && Calendar.cellClick(cal._nav_nm);
+                       break;
+                   case 40: // KEY down
+                       act && Calendar.cellClick(cal._nav_ny);
+                       break;
+                   default:
+                       return false;
+               }
+       } else switch (ev.keyCode) {
+           case 32: // KEY space (now)
+               Calendar.cellClick(cal._nav_now);
+               break;
+           case 27: // KEY esc
+               act && cal.hide();
+               break;
+           case 37: // KEY left
+           case 38: // KEY up
+           case 39: // KEY right
+           case 40: // KEY down
+               if (act) {
+                       var date = cal.date.getDate() - 1;
+                       var el = cal.currentDateEl;
+                       var ne = null;
+                       var prev = (ev.keyCode == 37) || (ev.keyCode == 38);
+                       switch (ev.keyCode) {
+                           case 37: // KEY left
+                               (--date >= 0) && (ne = cal.ar_days[date]);
+                               break;
+                           case 38: // KEY up
+                               date -= 7;
+                               (date >= 0) && (ne = cal.ar_days[date]);
+                               break;
+                           case 39: // KEY right
+                               (++date < cal.ar_days.length) && (ne = cal.ar_days[date]);
+                               break;
+                           case 40: // KEY down
+                               date += 7;
+                               (date < cal.ar_days.length) && (ne = cal.ar_days[date]);
+                               break;
+                       }
+                       if (!ne) {
+                               if (prev) {
+                                       Calendar.cellClick(cal._nav_pm);
+                               } else {
+                                       Calendar.cellClick(cal._nav_nm);
+                               }
+                               date = (prev) ? cal.date.getMonthDays() : 1;
+                               el = cal.currentDateEl;
+                               ne = cal.ar_days[date - 1];
+                       }
+                       Calendar.removeClass(el, "selected");
+                       Calendar.addClass(ne, "selected");
+                       cal.date.setDate(ne.caldate);
+                       cal.callHandler();
+                       cal.currentDateEl = ne;
+               }
+               break;
+           case 13: // KEY enter
+               if (act) {
+                       cal.callHandler();
+                       cal.hide();
+               }
+               break;
+           default:
+               return false;
+       }
+       return Calendar.stopEvent(ev);
+};
+
+/**
+ *  (RE)Initializes the calendar to the given date and style (if mondayFirst is
+ *  true it makes Monday the first day of week, otherwise the weeks start on
+ *  Sunday.
+ */
+Calendar.prototype._init = function (mondayFirst, date) {
+       var today = new Date();
+       var year = date.getFullYear();
+       if (year < this.minYear) {
+               year = this.minYear;
+               date.setFullYear(year);
+       } else if (year > this.maxYear) {
+               year = this.maxYear;
+               date.setFullYear(year);
+       }
+       this.mondayFirst = mondayFirst;
+       this.date = new Date(date);
+       var month = date.getMonth();
+       var mday = date.getDate();
+       var no_days = date.getMonthDays();
+       date.setDate(1);
+       var wday = date.getDay();
+       var MON = mondayFirst ? 1 : 0;
+       var SAT = mondayFirst ? 5 : 6;
+       var SUN = mondayFirst ? 6 : 0;
+       if (mondayFirst) {
+               wday = (wday > 0) ? (wday - 1) : 6;
+       }
+       var iday = 1;
+       var row = this.tbody.firstChild;
+       var MN = Calendar._SMN[month];
+       var hasToday = ((today.getFullYear() == year) && (today.getMonth() == month));
+       var todayDate = today.getDate();
+       var week_number = date.getWeekNumber();
+       var ar_days = new Array();
+       for (var i = 0; i < 6; ++i) {
+               if (iday > no_days) {
+                       row.className = "emptyrow";
+                       row = row.nextSibling;
+                       continue;
+               }
+               var cell = row.firstChild;
+               if (this.weekNumbers) {
+                       cell.className = "day wn";
+                       cell.firstChild.data = week_number;
+                       cell = cell.nextSibling;
+               }
+               ++week_number;
+               row.className = "daysrow";
+               for (var j = 0; j < 7; ++j) {
+                       cell.className = "day";
+                       if ((!i && j < wday) || iday > no_days) {
+                               // cell.className = "emptycell";
+                               cell.innerHTML = "&nbsp;";
+                               cell.disabled = true;
+                               cell = cell.nextSibling;
+                               continue;
+                       }
+                       cell.disabled = false;
+                       cell.firstChild.data = iday;
+                       if (typeof this.getDateStatus == "function") {
+                               date.setDate(iday);
+                               var status = this.getDateStatus(date, year, month, iday);
+                               if (status === true) {
+                                       cell.className += " disabled";
+                                       cell.disabled = true;
+                               } else {
+                                       if (/disabled/i.test(status))
+                                               cell.disabled = true;
+                                       cell.className += " " + status;
+                               }
+                       }
+                       if (!cell.disabled) {
+                               ar_days[ar_days.length] = cell;
+                               cell.caldate = iday;
+                               cell.ttip = "_";
+                               if (iday == mday) {
+                                       cell.className += " selected";
+                                       this.currentDateEl = cell;
+                               }
+                               if (hasToday && (iday == todayDate)) {
+                                       cell.className += " today";
+                                       cell.ttip += Calendar._TT["PART_TODAY"];
+                               }
+                               if (wday == SAT || wday == SUN) {
+                                       cell.className += " weekend";
+                               }
+                       }
+                       ++iday;
+                       ((++wday) ^ 7) || (wday = 0);
+                       cell = cell.nextSibling;
+               }
+               row = row.nextSibling;
+       }
+       this.ar_days = ar_days;
+       this.title.firstChild.data = Calendar._MN[month] + ", " + year;
+       this.onSetTime();
+       // PROFILE
+       // this.tooltips.firstChild.data = "Generated in " + ((new Date()) - today) + " ms";
+};
+
+/**
+ *  Calls _init function above for going to a certain date (but only if the
+ *  date is different than the currently selected one).
+ */
+Calendar.prototype.setDate = function (date) {
+       if (!date.equalsTo(this.date)) {
+               this._init(this.mondayFirst, date);
+       }
+};
+
+/**
+ *  Refreshes the calendar.  Useful if the "disabledHandler" function is
+ *  dynamic, meaning that the list of disabled date can change at runtime.
+ *  Just * call this function if you think that the list of disabled dates
+ *  should * change.
+ */
+Calendar.prototype.refresh = function () {
+       this._init(this.mondayFirst, this.date);
+};
+
+/** Modifies the "mondayFirst" parameter (EU/US style). */
+Calendar.prototype.setMondayFirst = function (mondayFirst) {
+       this._init(mondayFirst, this.date);
+       this._displayWeekdays();
+};
+
+/**
+ *  Allows customization of what dates are enabled.  The "unaryFunction"
+ *  parameter must be a function object that receives the date (as a JS Date
+ *  object) and returns a boolean value.  If the returned value is true then
+ *  the passed date will be marked as disabled.
+ */
+Calendar.prototype.setDateStatusHandler = Calendar.prototype.setDisabledHandler = function (unaryFunction) {
+       this.getDateStatus = unaryFunction;
+};
+
+/** Customization of allowed year range for the calendar. */
+Calendar.prototype.setRange = function (a, z) {
+       this.minYear = a;
+       this.maxYear = z;
+};
+
+/** Calls the first user handler (selectedHandler). */
+Calendar.prototype.callHandler = function () {
+       if (this.onSelected) {
+               this.onSelected(this, this.date.print(this.dateFormat));
+       }
+};
+
+/** Calls the second user handler (closeHandler). */
+Calendar.prototype.callCloseHandler = function () {
+       if (this.onClose) {
+               this.onClose(this);
+       }
+       this.hideShowCovered();
+};
+
+/** Removes the calendar object from the DOM tree and destroys it. */
+Calendar.prototype.destroy = function () {
+       var el = this.element.parentNode;
+       el.removeChild(this.element);
+       Calendar._C = null;
+       window.calendar = null;
+};
+
+/**
+ *  Moves the calendar element to a different section in the DOM tree (changes
+ *  its parent).
+ */
+Calendar.prototype.reparent = function (new_parent) {
+       var el = this.element;
+       el.parentNode.removeChild(el);
+       new_parent.appendChild(el);
+};
+
+// This gets called when the user presses a mouse button anywhere in the
+// document, if the calendar is shown.  If the click was outside the open
+// calendar this function closes it.
+Calendar._checkCalendar = function(ev) {
+       if (!window.calendar) {
+               return false;
+       }
+       var el = Calendar.is_ie ? Calendar.getElement(ev) : Calendar.getTargetElement(ev);
+       for (; el != null && el != calendar.element; el = el.parentNode);
+       if (el == null) {
+               // calls closeHandler which should hide the calendar.
+               window.calendar.callCloseHandler();
+               return Calendar.stopEvent(ev);
+       }
+};
+
+/** Shows the calendar. */
+Calendar.prototype.show = function () {
+       var rows = this.table.getElementsByTagName("tr");
+       for (var i = rows.length; i > 0;) {
+               var row = rows[--i];
+               Calendar.removeClass(row, "rowhilite");
+               var cells = row.getElementsByTagName("td");
+               for (var j = cells.length; j > 0;) {
+                       var cell = cells[--j];
+                       Calendar.removeClass(cell, "hilite");
+                       Calendar.removeClass(cell, "active");
+               }
+       }
+       this.element.style.display = "block";
+       this.hidden = false;
+       if (this.isPopup) {
+               window.calendar = this;
+               Calendar.addEvent(document, "keydown", Calendar._keyEvent);
+               Calendar.addEvent(document, "keypress", Calendar._keyEvent);
+               Calendar.addEvent(document, "mousedown", Calendar._checkCalendar);
+       }
+       this.hideShowCovered();
+};
+
+/**
+ *  Hides the calendar.  Also removes any "hilite" from the class of any TD
+ *  element.
+ */
+Calendar.prototype.hide = function () {
+       if (this.isPopup) {
+               Calendar.removeEvent(document, "keydown", Calendar._keyEvent);
+               Calendar.removeEvent(document, "keypress", Calendar._keyEvent);
+               Calendar.removeEvent(document, "mousedown", Calendar._checkCalendar);
+       }
+       this.element.style.display = "none";
+       this.hidden = true;
+       this.hideShowCovered();
+};
+
+/**
+ *  Shows the calendar at a given absolute position (beware that, depending on
+ *  the calendar element style -- position property -- this might be relative
+ *  to the parent's containing rectangle).
+ */
+Calendar.prototype.showAt = function (x, y) {
+       var s = this.element.style;
+       s.left = x + "px";
+       s.top = y + "px";
+       this.show();
+};
+
+/** Shows the calendar near a given element. */
+Calendar.prototype.showAtElement = function (el, opts) {
+       var self = this;
+       var p = Calendar.getAbsolutePos(el);
+       if (!opts || typeof opts != "string") {
+               this.showAt(p.x, p.y + el.offsetHeight);
+               return true;
+       }
+       this.element.style.display = "block";
+       Calendar.continuation_for_the_fucking_khtml_browser = function() {
+               var w = self.element.offsetWidth;
+               var h = self.element.offsetHeight;
+               self.element.style.display = "none";
+               var valign = opts.substr(0, 1);
+               var halign = "l";
+               if (opts.length > 1) {
+                       halign = opts.substr(1, 1);
+               }
+               // vertical alignment
+               switch (valign) {
+                   case "T": p.y -= h; break;
+                   case "B": p.y += el.offsetHeight; break;
+                   case "C": p.y += (el.offsetHeight - h) / 2; break;
+                   case "t": p.y += el.offsetHeight - h; break;
+                   case "b": break; // already there
+               }
+               // horizontal alignment
+               switch (halign) {
+                   case "L": p.x -= w; break;
+                   case "R": p.x += el.offsetWidth; break;
+                   case "C": p.x += (el.offsetWidth - w) / 2; break;
+                   case "r": p.x += el.offsetWidth - w; break;
+                   case "l": break; // already there
+               }
+               self.showAt(p.x, p.y);
+       };
+       if (Calendar.is_khtml)
+               setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()", 10);
+       else
+               Calendar.continuation_for_the_fucking_khtml_browser();
+};
+
+/** Customizes the date format. */
+Calendar.prototype.setDateFormat = function (str) {
+       this.dateFormat = str;
+};
+
+/** Customizes the tooltip date format. */
+Calendar.prototype.setTtDateFormat = function (str) {
+       this.ttDateFormat = str;
+};
+
+/**
+ *  Tries to identify the date represented in a string.  If successful it also
+ *  calls this.setDate which moves the calendar to the given date.
+ */
+Calendar.prototype.parseDate = function (str, fmt) {
+       var y = 0;
+       var m = -1;
+       var d = 0;
+       var a = str.split(/\W+/);
+       if (!fmt) {
+               fmt = this.dateFormat;
+       }
+       var b = [];
+       fmt.replace(/(%.)/g, function(str, par) {
+               return b[b.length] = par;
+       });
+       var i = 0, j = 0;
+       var hr = 0;
+       var min = 0;
+       for (i = 0; i < a.length; ++i) {
+               if (b[i] == "%a" || b[i] == "%A") {
+                       continue;
+               }
+               if (b[i] == "%d" || b[i] == "%e") {
+                       d = parseInt(a[i], 10);
+               }
+               if (b[i] == "%m") {
+                       m = parseInt(a[i], 10) - 1;
+               }
+               if (b[i] == "%Y" || b[i] == "%y") {
+                       y = parseInt(a[i], 10);
+                       (y < 100) && (y += (y > 29) ? 1900 : 2000);
+               }
+               if (b[i] == "%b" || b[i] == "%B") {
+                       for (j = 0; j < 12; ++j) {
+                               if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { m = j; break; }
+                       }
+               } else if (/%[HIkl]/.test(b[i])) {
+                       hr = parseInt(a[i], 10);
+               } else if (/%[pP]/.test(b[i])) {
+                       if (/pm/i.test(a[i]) && hr < 12)
+                               hr += 12;
+               } else if (b[i] == "%M") {
+                       min = parseInt(a[i], 10);
+               }
+       }
+       if (y != 0 && m != -1 && d != 0) {
+               this.setDate(new Date(y, m, d, hr, min, 0));
+               return;
+       }
+       y = 0; m = -1; d = 0;
+       for (i = 0; i < a.length; ++i) {
+               if (a[i].search(/[a-zA-Z]+/) != -1) {
+                       var t = -1;
+                       for (j = 0; j < 12; ++j) {
+                               if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { t = j; break; }
+                       }
+                       if (t != -1) {
+                               if (m != -1) {
+                                       d = m+1;
+                               }
+                               m = t;
+                       }
+               } else if (parseInt(a[i], 10) <= 12 && m == -1) {
+                       m = a[i]-1;
+               } else if (parseInt(a[i], 10) > 31 && y == 0) {
+                       y = parseInt(a[i], 10);
+                       (y < 100) && (y += (y > 29) ? 1900 : 2000);
+               } else if (d == 0) {
+                       d = a[i];
+               }
+       }
+       if (y == 0) {
+               var today = new Date();
+               y = today.getFullYear();
+       }
+       if (m != -1 && d != 0) {
+               this.setDate(new Date(y, m, d, hr, min, 0));
+       }
+};
+
+Calendar.prototype.hideShowCovered = function () {
+       var self = this;
+       Calendar.continuation_for_the_fucking_khtml_browser = function() {
+               function getVisib(obj){
+                       var value = obj.style.visibility;
+                       if (!value) {
+                               if (document.defaultView && typeof (document.defaultView.getComputedStyle) == "function") { // Gecko, W3C
+                                       if (!Calendar.is_khtml)
+                                               value = document.defaultView.
+                                                       getComputedStyle(obj, "").getPropertyValue("visibility");
+                                       else
+                                               value = '';
+                               } else if (obj.currentStyle) { // IE
+                                       value = obj.currentStyle.visibility;
+                               } else
+                                       value = '';
+                       }
+                       return value;
+               };
+
+               var tags = new Array("applet", "iframe", "select");
+               var el = self.element;
+
+               var p = Calendar.getAbsolutePos(el);
+               var EX1 = p.x;
+               var EX2 = el.offsetWidth + EX1;
+               var EY1 = p.y;
+               var EY2 = el.offsetHeight + EY1;
+
+               for (var k = tags.length; k > 0; ) {
+                       var ar = document.getElementsByTagName(tags[--k]);
+                       var cc = null;
+
+                       for (var i = ar.length; i > 0;) {
+                               cc = ar[--i];
+
+                               p = Calendar.getAbsolutePos(cc);
+                               var CX1 = p.x;
+                               var CX2 = cc.offsetWidth + CX1;
+                               var CY1 = p.y;
+                               var CY2 = cc.offsetHeight + CY1;
+
+                               if (self.hidden || (CX1 > EX2) || (CX2 < EX1) || (CY1 > EY2) || (CY2 < EY1)) {
+                                       if (!cc.__msh_save_visibility) {
+                                               cc.__msh_save_visibility = getVisib(cc);
+                                       }
+                                       cc.style.visibility = cc.__msh_save_visibility;
+                               } else {
+                                       if (!cc.__msh_save_visibility) {
+                                               cc.__msh_save_visibility = getVisib(cc);
+                                       }
+                                       cc.style.visibility = "hidden";
+                               }
+                       }
+               }
+       };
+       if (Calendar.is_khtml)
+               setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()", 10);
+       else
+               Calendar.continuation_for_the_fucking_khtml_browser();
+};
+
+/** Internal function; it displays the bar with the names of the weekday. */
+Calendar.prototype._displayWeekdays = function () {
+       var MON = this.mondayFirst ? 0 : 1;
+       var SUN = this.mondayFirst ? 6 : 0;
+       var SAT = this.mondayFirst ? 5 : 6;
+       var cell = this.firstdayname;
+       for (var i = 0; i < 7; ++i) {
+               cell.className = "day name";
+               if (!i) {
+                       cell.ttip = this.mondayFirst ? Calendar._TT["SUN_FIRST"] : Calendar._TT["MON_FIRST"];
+                       cell.navtype = 100;
+                       cell.calendar = this;
+                       Calendar._add_evs(cell);
+               }
+               if (i == SUN || i == SAT) {
+                       Calendar.addClass(cell, "weekend");
+               }
+               cell.firstChild.data = Calendar._SDN[i + 1 - MON];
+               cell = cell.nextSibling;
+       }
+};
+
+/** Internal function.  Hides all combo boxes that might be displayed. */
+Calendar.prototype._hideCombos = function () {
+       this.monthsCombo.style.display = "none";
+       this.yearsCombo.style.display = "none";
+};
+
+/** Internal function.  Starts dragging the element. */
+Calendar.prototype._dragStart = function (ev) {
+       if (this.dragging) {
+               return;
+       }
+       this.dragging = true;
+       var posX;
+       var posY;
+       if (Calendar.is_ie) {
+               posY = window.event.clientY + document.body.scrollTop;
+               posX = window.event.clientX + document.body.scrollLeft;
+       } else {
+               posY = ev.clientY + window.scrollY;
+               posX = ev.clientX + window.scrollX;
+       }
+       var st = this.element.style;
+       this.xOffs = posX - parseInt(st.left);
+       this.yOffs = posY - parseInt(st.top);
+       with (Calendar) {
+               addEvent(document, "mousemove", calDragIt);
+               addEvent(document, "mouseover", stopEvent);
+               addEvent(document, "mouseup", calDragEnd);
+       }
+};
+
+// BEGIN: DATE OBJECT PATCHES
+
+/** Adds the number of days array to the Date object. */
+Date._MD = new Array(31,28,31,30,31,30,31,31,30,31,30,31);
+
+/** Constants used for time computations */
+Date.SECOND = 1000 /* milliseconds */;
+Date.MINUTE = 60 * Date.SECOND;
+Date.HOUR   = 60 * Date.MINUTE;
+Date.DAY    = 24 * Date.HOUR;
+Date.WEEK   =  7 * Date.DAY;
+
+/** Returns the number of days in the current month */
+Date.prototype.getMonthDays = function(month) {
+       var year = this.getFullYear();
+       if (typeof month == "undefined") {
+               month = this.getMonth();
+       }
+       if (((0 == (year%4)) && ( (0 != (year%100)) || (0 == (year%400)))) && month == 1) {
+               return 29;
+       } else {
+               return Date._MD[month];
+       }
+};
+
+/** Returns the number of day in the year. */
+Date.prototype.getDayOfYear = function() {
+       var now = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0);
+       var then = new Date(this.getFullYear(), 0, 1, 0, 0, 0);
+       var time = now - then;
+       return Math.floor(time / Date.DAY);
+};
+
+/** Returns the number of the week in year, as defined in ISO 8601. */
+Date.prototype.getWeekNumber = function() {
+       var now = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0);
+       var then = new Date(this.getFullYear(), 0, 1, 0, 0, 0);
+       var time = now - then;
+       var day = then.getDay(); // 0 means Sunday
+       if (day == 0) day = 7;
+       (day > 4) && (day -= 4) || (day += 3);
+       return Math.round(((time / Date.DAY) + day) / 7);
+};
+
+/** Checks dates equality (ignores time) */
+Date.prototype.equalsTo = function(date) {
+       return ((this.getFullYear() == date.getFullYear()) &&
+               (this.getMonth() == date.getMonth()) &&
+               (this.getDate() == date.getDate()) &&
+               (this.getHours() == date.getHours()) &&
+               (this.getMinutes() == date.getMinutes()));
+};
+
+/** Prints the date in a string according to the given format. */
+Date.prototype.print = function (str) {
+       var m = this.getMonth();
+       var d = this.getDate();
+       var y = this.getFullYear();
+       var wn = this.getWeekNumber();
+       var w = this.getDay();
+       var s = {};
+       var hr = this.getHours();
+       var pm = (hr >= 12);
+       var ir = (pm) ? (hr - 12) : hr;
+       var dy = this.getDayOfYear();
+       if (ir == 0)
+               ir = 12;
+       var min = this.getMinutes();
+       var sec = this.getSeconds();
+       s["%a"] = Calendar._SDN[w]; // abbreviated weekday name [FIXME: I18N]
+       s["%A"] = Calendar._DN[w]; // full weekday name
+       s["%b"] = Calendar._SMN[m]; // abbreviated month name [FIXME: I18N]
+       s["%B"] = Calendar._MN[m]; // full month name
+       // FIXME: %c : preferred date and time representation for the current locale
+       s["%C"] = 1 + Math.floor(y / 100); // the century number
+       s["%d"] = (d < 10) ? ("0" + d) : d; // the day of the month (range 01 to 31)
+       s["%e"] = d; // the day of the month (range 1 to 31)
+       // FIXME: %D : american date style: %m/%d/%y
+       // FIXME: %E, %F, %G, %g, %h (man strftime)
+       s["%H"] = (hr < 10) ? ("0" + hr) : hr; // hour, range 00 to 23 (24h format)
+       s["%I"] = (ir < 10) ? ("0" + ir) : ir; // hour, range 01 to 12 (12h format)
+       s["%j"] = (dy < 100) ? ((dy < 10) ? ("00" + dy) : ("0" + dy)) : dy; // day of the year (range 001 to 366)
+       s["%k"] = hr;           // hour, range 0 to 23 (24h format)
+       s["%l"] = ir;           // hour, range 1 to 12 (12h format)
+       s["%m"] = (m < 9) ? ("0" + (1+m)) : (1+m); // month, range 01 to 12
+       s["%M"] = (min < 10) ? ("0" + min) : min; // minute, range 00 to 59
+       s["%n"] = "\n";         // a newline character
+       s["%p"] = pm ? "PM" : "AM";
+       s["%P"] = pm ? "pm" : "am";
+       // FIXME: %r : the time in am/pm notation %I:%M:%S %p
+       // FIXME: %R : the time in 24-hour notation %H:%M
+       s["%s"] = Math.floor(this.getTime() / 1000);
+       s["%S"] = (sec < 10) ? ("0" + sec) : sec; // seconds, range 00 to 59
+       s["%t"] = "\t";         // a tab character
+       // FIXME: %T : the time in 24-hour notation (%H:%M:%S)
+       s["%U"] = s["%W"] = s["%V"] = (wn < 10) ? ("0" + wn) : wn;
+       s["%u"] = w + 1;        // the day of the week (range 1 to 7, 1 = MON)
+       s["%w"] = w;            // the day of the week (range 0 to 6, 0 = SUN)
+       // FIXME: %x : preferred date representation for the current locale without the time
+       // FIXME: %X : preferred time representation for the current locale without the date
+       s["%y"] = ('' + y).substr(2, 2); // year without the century (range 00 to 99)
+       s["%Y"] = y;            // year with the century
+       s["%%"] = "%";          // a literal '%' character
+       var re = Date._msh_formatRegexp;
+       if (typeof re == "undefined") {
+               var tmp = "";
+               for (var i in s)
+                       tmp += tmp ? ("|" + i) : i;
+               Date._msh_formatRegexp = re = new RegExp("(" + tmp + ")", 'g');
+       }
+       return str.replace(re, function(match, par) { return s[par]; });
+};
+
+// END: DATE OBJECT PATCHES
+
+// global object that remembers the calendar
+window.calendar = null;
diff --git a/httemplate/elements/calendar_stripped.js b/httemplate/elements/calendar_stripped.js
new file mode 100644 (file)
index 0000000..029496a
--- /dev/null
@@ -0,0 +1,12 @@
+/*  Copyright Mihai Bazon, 2002, 2003  |  http://dynarch.com/mishoo/
+ * ------------------------------------------------------------------
+ *
+ * The DHTML Calendar, version 0.9.5 "Your favorite time, bis"
+ *
+ * Details and latest version at:
+ * http://dynarch.com/mishoo/calendar.epl
+ *
+ * This script is distributed under the GNU Lesser General Public License.
+ * Read the entire license text here: http://www.gnu.org/licenses/lgpl.html
+ */
+ Calendar=function(mondayFirst,dateStr,onSelected,onClose){this.activeDiv=null;this.currentDateEl=null;this.getDateStatus=null;this.timeout=null;this.onSelected=onSelected||null;this.onClose=onClose||null;this.dragging=false;this.hidden=false;this.minYear=1970;this.maxYear=2050;this.dateFormat=Calendar._TT["DEF_DATE_FORMAT"];this.ttDateFormat=Calendar._TT["TT_DATE_FORMAT"];this.isPopup=true;this.weekNumbers=true;this.mondayFirst=mondayFirst;this.dateStr=dateStr;this.ar_days=null;this.showsTime=false;this.time24=true;this.table=null;this.element=null;this.tbody=null;this.firstdayname=null;this.monthsCombo=null;this.yearsCombo=null;this.hilitedMonth=null;this.activeMonth=null;this.hilitedYear=null;this.activeYear=null;this.dateClicked=false;if(typeof Calendar._SDN=="undefined"){if(typeof Calendar._SDN_len=="undefined")Calendar._SDN_len=3;var ar=new Array();for(var i=8;i>0;){ar[--i]=Calendar._DN[i].substr(0,Calendar._SDN_len);}Calendar._SDN=ar;if(typeof Calendar._SMN_len=="undefined")Calendar._SMN_len=3;ar=new Array();for(var i=12;i>0;){ar[--i]=Calendar._MN[i].substr(0,Calendar._SMN_len);}Calendar._SMN=ar;}};Calendar._C=null;Calendar.is_ie=(/msie/i.test(navigator.userAgent)&&!/opera/i.test(navigator.userAgent));Calendar.is_opera=/opera/i.test(navigator.userAgent);Calendar.is_khtml=/Konqueror|Safari|KHTML/i.test(navigator.userAgent);Calendar.getAbsolutePos=function(el){var SL=0,ST=0;var is_div=/^div$/i.test(el.tagName);if(is_div&&el.scrollLeft)SL=el.scrollLeft;if(is_div&&el.scrollTop)ST=el.scrollTop;var r={x:el.offsetLeft-SL,y:el.offsetTop-ST};if(el.offsetParent){var tmp=Calendar.getAbsolutePos(el.offsetParent);r.x+=tmp.x;r.y+=tmp.y;}return r;};Calendar.isRelated=function(el,evt){var related=evt.relatedTarget;if(!related){var type=evt.type;if(type=="mouseover"){related=evt.fromElement;}else if(type=="mouseout"){related=evt.toElement;}}while(related){if(related==el){return true;}related=related.parentNode;}return false;};Calendar.removeClass=function(el,className){if(!(el&&el.className)){return;}var cls=el.className.split(" ");var ar=new Array();for(var i=cls.length;i>0;){if(cls[--i]!=className){ar[ar.length]=cls[i];}}el.className=ar.join(" ");};Calendar.addClass=function(el,className){Calendar.removeClass(el,className);el.className+=" "+className;};Calendar.getElement=function(ev){if(Calendar.is_ie){return window.event.srcElement;}else{return ev.currentTarget;}};Calendar.getTargetElement=function(ev){if(Calendar.is_ie){return window.event.srcElement;}else{return ev.target;}};Calendar.stopEvent=function(ev){ev||(ev=window.event);if(Calendar.is_ie){ev.cancelBubble=true;ev.returnValue=false;}else{ev.preventDefault();ev.stopPropagation();}return false;};Calendar.addEvent=function(el,evname,func){if(el.attachEvent){el.attachEvent("on"+evname,func);}else if(el.addEventListener){el.addEventListener(evname,func,true);}else{el["on"+evname]=func;}};Calendar.removeEvent=function(el,evname,func){if(el.detachEvent){el.detachEvent("on"+evname,func);}else if(el.removeEventListener){el.removeEventListener(evname,func,true);}else{el["on"+evname]=null;}};Calendar.createElement=function(type,parent){var el=null;if(document.createElementNS){el=document.createElementNS("http://www.w3.org/1999/xhtml",type);}else{el=document.createElement(type);}if(typeof parent!="undefined"){parent.appendChild(el);}return el;};Calendar._add_evs=function(el){with(Calendar){addEvent(el,"mouseover",dayMouseOver);addEvent(el,"mousedown",dayMouseDown);addEvent(el,"mouseout",dayMouseOut);if(is_ie){addEvent(el,"dblclick",dayMouseDblClick);el.setAttribute("unselectable",true);}}};Calendar.findMonth=function(el){if(typeof el.month!="undefined"){return el;}else if(typeof el.parentNode.month!="undefined"){return el.parentNode;}return null;};Calendar.findYear=function(el){if(typeof el.year!="undefined"){return el;}else if(typeof el.parentNode.year!="undefined"){return el.parentNode;}return null;};Calendar.showMonthsCombo=function(){var cal=Calendar._C;if(!cal){return false;}var cal=cal;var cd=cal.activeDiv;var mc=cal.monthsCombo;if(cal.hilitedMonth){Calendar.removeClass(cal.hilitedMonth,"hilite");}if(cal.activeMonth){Calendar.removeClass(cal.activeMonth,"active");}var mon=cal.monthsCombo.getElementsByTagName("div")[cal.date.getMonth()];Calendar.addClass(mon,"active");cal.activeMonth=mon;var s=mc.style;s.display="block";if(cd.navtype<0)s.left=cd.offsetLeft+"px";else s.left=(cd.offsetLeft+cd.offsetWidth-mc.offsetWidth)+"px";s.top=(cd.offsetTop+cd.offsetHeight)+"px";};Calendar.showYearsCombo=function(fwd){var cal=Calendar._C;if(!cal){return false;}var cal=cal;var cd=cal.activeDiv;var yc=cal.yearsCombo;if(cal.hilitedYear){Calendar.removeClass(cal.hilitedYear,"hilite");}if(cal.activeYear){Calendar.removeClass(cal.activeYear,"active");}cal.activeYear=null;var Y=cal.date.getFullYear()+(fwd?1:-1);var yr=yc.firstChild;var show=false;for(var i=12;i>0;--i){if(Y>=cal.minYear&&Y<=cal.maxYear){yr.firstChild.data=Y;yr.year=Y;yr.style.display="block";show=true;}else{yr.style.display="none";}yr=yr.nextSibling;Y+=fwd?2:-2;}if(show){var s=yc.style;s.display="block";if(cd.navtype<0)s.left=cd.offsetLeft+"px";else s.left=(cd.offsetLeft+cd.offsetWidth-yc.offsetWidth)+"px";s.top=(cd.offsetTop+cd.offsetHeight)+"px";}};Calendar.tableMouseUp=function(ev){var cal=Calendar._C;if(!cal){return false;}if(cal.timeout){clearTimeout(cal.timeout);}var el=cal.activeDiv;if(!el){return false;}var target=Calendar.getTargetElement(ev);ev||(ev=window.event);Calendar.removeClass(el,"active");if(target==el||target.parentNode==el){Calendar.cellClick(el,ev);}var mon=Calendar.findMonth(target);var date=null;if(mon){date=new Date(cal.date);if(mon.month!=date.getMonth()){date.setMonth(mon.month);cal.setDate(date);cal.dateClicked=false;cal.callHandler();}}else{var year=Calendar.findYear(target);if(year){date=new Date(cal.date);if(year.year!=date.getFullYear()){date.setFullYear(year.year);cal.setDate(date);cal.dateClicked=false;cal.callHandler();}}}with(Calendar){removeEvent(document,"mouseup",tableMouseUp);removeEvent(document,"mouseover",tableMouseOver);removeEvent(document,"mousemove",tableMouseOver);cal._hideCombos();_C=null;return stopEvent(ev);}};Calendar.tableMouseOver=function(ev){var cal=Calendar._C;if(!cal){return;}var el=cal.activeDiv;var target=Calendar.getTargetElement(ev);if(target==el||target.parentNode==el){Calendar.addClass(el,"hilite active");Calendar.addClass(el.parentNode,"rowhilite");}else{if(typeof el.navtype=="undefined"||(el.navtype!=50&&(el.navtype==0||Math.abs(el.navtype)>2)))Calendar.removeClass(el,"active");Calendar.removeClass(el,"hilite");Calendar.removeClass(el.parentNode,"rowhilite");}ev||(ev=window.event);if(el.navtype==50&&target!=el){var pos=Calendar.getAbsolutePos(el);var w=el.offsetWidth;var x=ev.clientX;var dx;var decrease=true;if(x>pos.x+w){dx=x-pos.x-w;decrease=false;}else dx=pos.x-x;if(dx<0)dx=0;var range=el._range;var current=el._current;var count=Math.floor(dx/10)%range.length;for(var i=range.length;--i>=0;)if(range[i]==current)break;while(count-->0)if(decrease){if(!(--i in range))i=range.length-1;}else if(!(++i in range))i=0;var newval=range[i];el.firstChild.data=newval;cal.onUpdateTime();}var mon=Calendar.findMonth(target);if(mon){if(mon.month!=cal.date.getMonth()){if(cal.hilitedMonth){Calendar.removeClass(cal.hilitedMonth,"hilite");}Calendar.addClass(mon,"hilite");cal.hilitedMonth=mon;}else if(cal.hilitedMonth){Calendar.removeClass(cal.hilitedMonth,"hilite");}}else{if(cal.hilitedMonth){Calendar.removeClass(cal.hilitedMonth,"hilite");}var year=Calendar.findYear(target);if(year){if(year.year!=cal.date.getFullYear()){if(cal.hilitedYear){Calendar.removeClass(cal.hilitedYear,"hilite");}Calendar.addClass(year,"hilite");cal.hilitedYear=year;}else if(cal.hilitedYear){Calendar.removeClass(cal.hilitedYear,"hilite");}}else if(cal.hilitedYear){Calendar.removeClass(cal.hilitedYear,"hilite");}}return Calendar.stopEvent(ev);};Calendar.tableMouseDown=function(ev){if(Calendar.getTargetElement(ev)==Calendar.getElement(ev)){return Calendar.stopEvent(ev);}};Calendar.calDragIt=function(ev){var cal=Calendar._C;if(!(cal&&cal.dragging)){return false;}var posX;var posY;if(Calendar.is_ie){posY=window.event.clientY+document.body.scrollTop;posX=window.event.clientX+document.body.scrollLeft;}else{posX=ev.pageX;posY=ev.pageY;}cal.hideShowCovered();var st=cal.element.style;st.left=(posX-cal.xOffs)+"px";st.top=(posY-cal.yOffs)+"px";return Calendar.stopEvent(ev);};Calendar.calDragEnd=function(ev){var cal=Calendar._C;if(!cal){return false;}cal.dragging=false;with(Calendar){removeEvent(document,"mousemove",calDragIt);removeEvent(document,"mouseover",stopEvent);removeEvent(document,"mouseup",calDragEnd);tableMouseUp(ev);}cal.hideShowCovered();};Calendar.dayMouseDown=function(ev){var el=Calendar.getElement(ev);if(el.disabled){return false;}var cal=el.calendar;cal.activeDiv=el;Calendar._C=cal;if(el.navtype!=300)with(Calendar){if(el.navtype==50)el._current=el.firstChild.data;addClass(el,"hilite active");addEvent(document,"mouseover",tableMouseOver);addEvent(document,"mousemove",tableMouseOver);addEvent(document,"mouseup",tableMouseUp);}else if(cal.isPopup){cal._dragStart(ev);}if(el.navtype==-1||el.navtype==1){if(cal.timeout)clearTimeout(cal.timeout);cal.timeout=setTimeout("Calendar.showMonthsCombo()",250);}else if(el.navtype==-2||el.navtype==2){if(cal.timeout)clearTimeout(cal.timeout);cal.timeout=setTimeout((el.navtype>0)?"Calendar.showYearsCombo(true)":"Calendar.showYearsCombo(false)",250);}else{cal.timeout=null;}return Calendar.stopEvent(ev);};Calendar.dayMouseDblClick=function(ev){Calendar.cellClick(Calendar.getElement(ev),ev||window.event);if(Calendar.is_ie){document.selection.empty();}};Calendar.dayMouseOver=function(ev){var el=Calendar.getElement(ev);if(Calendar.isRelated(el,ev)||Calendar._C||el.disabled){return false;}if(el.ttip){if(el.ttip.substr(0,1)=="_"){var date=null;with(el.calendar.date){date=new Date(getFullYear(),getMonth(),el.caldate);}el.ttip=date.print(el.calendar.ttDateFormat)+el.ttip.substr(1);}el.calendar.tooltips.firstChild.data=el.ttip;}if(el.navtype!=300){Calendar.addClass(el,"hilite");if(el.caldate){Calendar.addClass(el.parentNode,"rowhilite");}}return Calendar.stopEvent(ev);};Calendar.dayMouseOut=function(ev){with(Calendar){var el=getElement(ev);if(isRelated(el,ev)||_C||el.disabled){return false;}removeClass(el,"hilite");if(el.caldate){removeClass(el.parentNode,"rowhilite");}el.calendar.tooltips.firstChild.data=_TT["SEL_DATE"];return stopEvent(ev);}};Calendar.cellClick=function(el,ev){var cal=el.calendar;var closing=false;var newdate=false;var date=null;if(typeof el.navtype=="undefined"){Calendar.removeClass(cal.currentDateEl,"selected");Calendar.addClass(el,"selected");closing=(cal.currentDateEl==el);if(!closing){cal.currentDateEl=el;}cal.date.setDate(el.caldate);date=cal.date;newdate=true;cal.dateClicked=true;}else{if(el.navtype==200){Calendar.removeClass(el,"hilite");cal.callCloseHandler();return;}date=(el.navtype==0)?new Date():new Date(cal.date);cal.dateClicked=false;var year=date.getFullYear();var mon=date.getMonth();function setMonth(m){var day=date.getDate();var max=date.getMonthDays(m);if(day>max){date.setDate(max);}date.setMonth(m);};switch(el.navtype){case 400:Calendar.removeClass(el,"hilite");var text=Calendar._TT["ABOUT"];if(typeof text!="undefined"){text+=cal.showsTime?Calendar._TT["ABOUT_TIME"]:"";}else{text="Help and about box text is not translated into this language.\n"+"If you know this language and you feel generous please update\n"+"the corresponding file in \"lang\" subdir to match calendar-en.js\n"+"and send it back to <mishoo@infoiasi.ro> to get it into the distribution  ;-)\n\n"+"Thank you!\n"+"http://dynarch.com/mishoo/calendar.epl\n";}alert(text);return;case-2:if(year>cal.minYear){date.setFullYear(year-1);}break;case-1:if(mon>0){setMonth(mon-1);}else if(year-->cal.minYear){date.setFullYear(year);setMonth(11);}break;case 1:if(mon<11){setMonth(mon+1);}else if(year<cal.maxYear){date.setFullYear(year+1);setMonth(0);}break;case 2:if(year<cal.maxYear){date.setFullYear(year+1);}break;case 100:cal.setMondayFirst(!cal.mondayFirst);return;case 50:var range=el._range;var current=el.firstChild.data;for(var i=range.length;--i>=0;)if(range[i]==current)break;if(ev&&ev.shiftKey){if(!(--i in range))i=range.length-1;}else if(!(++i in range))i=0;var newval=range[i];el.firstChild.data=newval;cal.onUpdateTime();return;case 0:if((typeof cal.getDateStatus=="function")&&cal.getDateStatus(date,date.getFullYear(),date.getMonth(),date.getDate())){return false;}break;}if(!date.equalsTo(cal.date)){cal.setDate(date);newdate=true;}}if(newdate){cal.callHandler();}if(closing){Calendar.removeClass(el,"hilite");cal.callCloseHandler();}};Calendar.prototype.create=function(_par){var parent=null;if(!_par){parent=document.getElementsByTagName("body")[0];this.isPopup=true;}else{parent=_par;this.isPopup=false;}this.date=this.dateStr?new Date(this.dateStr):new Date();var table=Calendar.createElement("table");this.table=table;table.cellSpacing=0;table.cellPadding=0;table.calendar=this;Calendar.addEvent(table,"mousedown",Calendar.tableMouseDown);var div=Calendar.createElement("div");this.element=div;div.className="calendar";if(this.isPopup){div.style.position="absolute";div.style.display="none";}div.appendChild(table);var thead=Calendar.createElement("thead",table);var cell=null;var row=null;var cal=this;var hh=function(text,cs,navtype){cell=Calendar.createElement("td",row);cell.colSpan=cs;cell.className="button";if(navtype!=0&&Math.abs(navtype)<=2)cell.className+=" nav";Calendar._add_evs(cell);cell.calendar=cal;cell.navtype=navtype;if(text.substr(0,1)!="&"){cell.appendChild(document.createTextNode(text));}else{cell.innerHTML=text;}return cell;};row=Calendar.createElement("tr",thead);var title_length=6;(this.isPopup)&&--title_length;(this.weekNumbers)&&++title_length;hh("?",1,400).ttip=Calendar._TT["INFO"];this.title=hh("",title_length,300);this.title.className="title";if(this.isPopup){this.title.ttip=Calendar._TT["DRAG_TO_MOVE"];this.title.style.cursor="move";hh("&#x00d7;",1,200).ttip=Calendar._TT["CLOSE"];}row=Calendar.createElement("tr",thead);row.className="headrow";this._nav_py=hh("&#x00ab;",1,-2);this._nav_py.ttip=Calendar._TT["PREV_YEAR"];this._nav_pm=hh("&#x2039;",1,-1);this._nav_pm.ttip=Calendar._TT["PREV_MONTH"];this._nav_now=hh(Calendar._TT["TODAY"],this.weekNumbers?4:3,0);this._nav_now.ttip=Calendar._TT["GO_TODAY"];this._nav_nm=hh("&#x203a;",1,1);this._nav_nm.ttip=Calendar._TT["NEXT_MONTH"];this._nav_ny=hh("&#x00bb;",1,2);this._nav_ny.ttip=Calendar._TT["NEXT_YEAR"];row=Calendar.createElement("tr",thead);row.className="daynames";if(this.weekNumbers){cell=Calendar.createElement("td",row);cell.className="name wn";cell.appendChild(document.createTextNode(Calendar._TT["WK"]));}for(var i=7;i>0;--i){cell=Calendar.createElement("td",row);cell.appendChild(document.createTextNode(""));if(!i){cell.navtype=100;cell.calendar=this;Calendar._add_evs(cell);}}this.firstdayname=(this.weekNumbers)?row.firstChild.nextSibling:row.firstChild;this._displayWeekdays();var tbody=Calendar.createElement("tbody",table);this.tbody=tbody;for(i=6;i>0;--i){row=Calendar.createElement("tr",tbody);if(this.weekNumbers){cell=Calendar.createElement("td",row);cell.appendChild(document.createTextNode(""));}for(var j=7;j>0;--j){cell=Calendar.createElement("td",row);cell.appendChild(document.createTextNode(""));cell.calendar=this;Calendar._add_evs(cell);}}if(this.showsTime){row=Calendar.createElement("tr",tbody);row.className="time";cell=Calendar.createElement("td",row);cell.className="time";cell.colSpan=2;cell.innerHTML="&nbsp;";cell=Calendar.createElement("td",row);cell.className="time";cell.colSpan=this.weekNumbers?4:3;(function(){function makeTimePart(className,init,range_start,range_end){var part=Calendar.createElement("span",cell);part.className=className;part.appendChild(document.createTextNode(init));part.calendar=cal;part.ttip=Calendar._TT["TIME_PART"];part.navtype=50;part._range=[];if(typeof range_start!="number")part._range=range_start;else{for(var i=range_start;i<=range_end;++i){var txt;if(i<10&&range_end>=10)txt='0'+i;else txt=''+i;part._range[part._range.length]=txt;}}Calendar._add_evs(part);return part;};var hrs=cal.date.getHours();var mins=cal.date.getMinutes();var t12=!cal.time24;var pm=(hrs>12);if(t12&&pm)hrs-=12;var H=makeTimePart("hour",hrs,t12?1:0,t12?12:23);var span=Calendar.createElement("span",cell);span.appendChild(document.createTextNode(":"));span.className="colon";var M=makeTimePart("minute",mins,0,59);var AP=null;cell=Calendar.createElement("td",row);cell.className="time";cell.colSpan=2;if(t12)AP=makeTimePart("ampm",pm?"pm":"am",["am","pm"]);else cell.innerHTML="&nbsp;";cal.onSetTime=function(){var hrs=this.date.getHours();var mins=this.date.getMinutes();var pm=(hrs>12);if(pm&&t12)hrs-=12;H.firstChild.data=(hrs<10)?("0"+hrs):hrs;M.firstChild.data=(mins<10)?("0"+mins):mins;if(t12)AP.firstChild.data=pm?"pm":"am";};cal.onUpdateTime=function(){var date=this.date;var h=parseInt(H.firstChild.data,10);if(t12){if(/pm/i.test(AP.firstChild.data)&&h<12)h+=12;else if(/am/i.test(AP.firstChild.data)&&h==12)h=0;}var d=date.getDate();var m=date.getMonth();var y=date.getFullYear();date.setHours(h);date.setMinutes(parseInt(M.firstChild.data,10));date.setFullYear(y);date.setMonth(m);date.setDate(d);this.dateClicked=false;this.callHandler();};})();}else{this.onSetTime=this.onUpdateTime=function(){};}var tfoot=Calendar.createElement("tfoot",table);row=Calendar.createElement("tr",tfoot);row.className="footrow";cell=hh(Calendar._TT["SEL_DATE"],this.weekNumbers?8:7,300);cell.className="ttip";if(this.isPopup){cell.ttip=Calendar._TT["DRAG_TO_MOVE"];cell.style.cursor="move";}this.tooltips=cell;div=Calendar.createElement("div",this.element);this.monthsCombo=div;div.className="combo";for(i=0;i<Calendar._MN.length;++i){var mn=Calendar.createElement("div");mn.className=Calendar.is_ie?"label-IEfix":"label";mn.month=i;mn.appendChild(document.createTextNode(Calendar._SMN[i]));div.appendChild(mn);}div=Calendar.createElement("div",this.element);this.yearsCombo=div;div.className="combo";for(i=12;i>0;--i){var yr=Calendar.createElement("div");yr.className=Calendar.is_ie?"label-IEfix":"label";yr.appendChild(document.createTextNode(""));div.appendChild(yr);}this._init(this.mondayFirst,this.date);parent.appendChild(this.element);};Calendar._keyEvent=function(ev){if(!window.calendar){return false;}(Calendar.is_ie)&&(ev=window.event);var cal=window.calendar;var act=(Calendar.is_ie||ev.type=="keypress");if(ev.ctrlKey){switch(ev.keyCode){case 37:act&&Calendar.cellClick(cal._nav_pm);break;case 38:act&&Calendar.cellClick(cal._nav_py);break;case 39:act&&Calendar.cellClick(cal._nav_nm);break;case 40:act&&Calendar.cellClick(cal._nav_ny);break;default:return false;}}else switch(ev.keyCode){case 32:Calendar.cellClick(cal._nav_now);break;case 27:act&&cal.hide();break;case 37:case 38:case 39:case 40:if(act){var date=cal.date.getDate()-1;var el=cal.currentDateEl;var ne=null;var prev=(ev.keyCode==37)||(ev.keyCode==38);switch(ev.keyCode){case 37:(--date>=0)&&(ne=cal.ar_days[date]);break;case 38:date-=7;(date>=0)&&(ne=cal.ar_days[date]);break;case 39:(++date<cal.ar_days.length)&&(ne=cal.ar_days[date]);break;case 40:date+=7;(date<cal.ar_days.length)&&(ne=cal.ar_days[date]);break;}if(!ne){if(prev){Calendar.cellClick(cal._nav_pm);}else{Calendar.cellClick(cal._nav_nm);}date=(prev)?cal.date.getMonthDays():1;el=cal.currentDateEl;ne=cal.ar_days[date-1];}Calendar.removeClass(el,"selected");Calendar.addClass(ne,"selected");cal.date.setDate(ne.caldate);cal.callHandler();cal.currentDateEl=ne;}break;case 13:if(act){cal.callHandler();cal.hide();}break;default:return false;}return Calendar.stopEvent(ev);};Calendar.prototype._init=function(mondayFirst,date){var today=new Date();var year=date.getFullYear();if(year<this.minYear){year=this.minYear;date.setFullYear(year);}else if(year>this.maxYear){year=this.maxYear;date.setFullYear(year);}this.mondayFirst=mondayFirst;this.date=new Date(date);var month=date.getMonth();var mday=date.getDate();var no_days=date.getMonthDays();date.setDate(1);var wday=date.getDay();var MON=mondayFirst?1:0;var SAT=mondayFirst?5:6;var SUN=mondayFirst?6:0;if(mondayFirst){wday=(wday>0)?(wday-1):6;}var iday=1;var row=this.tbody.firstChild;var MN=Calendar._SMN[month];var hasToday=((today.getFullYear()==year)&&(today.getMonth()==month));var todayDate=today.getDate();var week_number=date.getWeekNumber();var ar_days=new Array();for(var i=0;i<6;++i){if(iday>no_days){row.className="emptyrow";row=row.nextSibling;continue;}var cell=row.firstChild;if(this.weekNumbers){cell.className="day wn";cell.firstChild.data=week_number;cell=cell.nextSibling;}++week_number;row.className="daysrow";for(var j=0;j<7;++j){cell.className="day";if((!i&&j<wday)||iday>no_days){cell.innerHTML="&nbsp;";cell.disabled=true;cell=cell.nextSibling;continue;}cell.disabled=false;cell.firstChild.data=iday;if(typeof this.getDateStatus=="function"){date.setDate(iday);var status=this.getDateStatus(date,year,month,iday);if(status===true){cell.className+=" disabled";cell.disabled=true;}else{if(/disabled/i.test(status))cell.disabled=true;cell.className+=" "+status;}}if(!cell.disabled){ar_days[ar_days.length]=cell;cell.caldate=iday;cell.ttip="_";if(iday==mday){cell.className+=" selected";this.currentDateEl=cell;}if(hasToday&&(iday==todayDate)){cell.className+=" today";cell.ttip+=Calendar._TT["PART_TODAY"];}if(wday==SAT||wday==SUN){cell.className+=" weekend";}}++iday;((++wday)^ 7)||(wday=0);cell=cell.nextSibling;}row=row.nextSibling;}this.ar_days=ar_days;this.title.firstChild.data=Calendar._MN[month]+", "+year;this.onSetTime();};Calendar.prototype.setDate=function(date){if(!date.equalsTo(this.date)){this._init(this.mondayFirst,date);}};Calendar.prototype.refresh=function(){this._init(this.mondayFirst,this.date);};Calendar.prototype.setMondayFirst=function(mondayFirst){this._init(mondayFirst,this.date);this._displayWeekdays();};Calendar.prototype.setDateStatusHandler=Calendar.prototype.setDisabledHandler=function(unaryFunction){this.getDateStatus=unaryFunction;};Calendar.prototype.setRange=function(a,z){this.minYear=a;this.maxYear=z;};Calendar.prototype.callHandler=function(){if(this.onSelected){this.onSelected(this,this.date.print(this.dateFormat));}};Calendar.prototype.callCloseHandler=function(){if(this.onClose){this.onClose(this);}this.hideShowCovered();};Calendar.prototype.destroy=function(){var el=this.element.parentNode;el.removeChild(this.element);Calendar._C=null;window.calendar=null;};Calendar.prototype.reparent=function(new_parent){var el=this.element;el.parentNode.removeChild(el);new_parent.appendChild(el);};Calendar._checkCalendar=function(ev){if(!window.calendar){return false;}var el=Calendar.is_ie?Calendar.getElement(ev):Calendar.getTargetElement(ev);for(;el!=null&&el!=calendar.element;el=el.parentNode);if(el==null){window.calendar.callCloseHandler();return Calendar.stopEvent(ev);}};Calendar.prototype.show=function(){var rows=this.table.getElementsByTagName("tr");for(var i=rows.length;i>0;){var row=rows[--i];Calendar.removeClass(row,"rowhilite");var cells=row.getElementsByTagName("td");for(var j=cells.length;j>0;){var cell=cells[--j];Calendar.removeClass(cell,"hilite");Calendar.removeClass(cell,"active");}}this.element.style.display="block";this.hidden=false;if(this.isPopup){window.calendar=this;Calendar.addEvent(document,"keydown",Calendar._keyEvent);Calendar.addEvent(document,"keypress",Calendar._keyEvent);Calendar.addEvent(document,"mousedown",Calendar._checkCalendar);}this.hideShowCovered();};Calendar.prototype.hide=function(){if(this.isPopup){Calendar.removeEvent(document,"keydown",Calendar._keyEvent);Calendar.removeEvent(document,"keypress",Calendar._keyEvent);Calendar.removeEvent(document,"mousedown",Calendar._checkCalendar);}this.element.style.display="none";this.hidden=true;this.hideShowCovered();};Calendar.prototype.showAt=function(x,y){var s=this.element.style;s.left=x+"px";s.top=y+"px";this.show();};Calendar.prototype.showAtElement=function(el,opts){var self=this;var p=Calendar.getAbsolutePos(el);if(!opts||typeof opts!="string"){this.showAt(p.x,p.y+el.offsetHeight);return true;}this.element.style.display="block";Calendar.continuation_for_the_fucking_khtml_browser=function(){var w=self.element.offsetWidth;var h=self.element.offsetHeight;self.element.style.display="none";var valign=opts.substr(0,1);var halign="l";if(opts.length>1){halign=opts.substr(1,1);}switch(valign){case "T":p.y-=h;break;case "B":p.y+=el.offsetHeight;break;case "C":p.y+=(el.offsetHeight-h)/2;break;case "t":p.y+=el.offsetHeight-h;break;case "b":break;}switch(halign){case "L":p.x-=w;break;case "R":p.x+=el.offsetWidth;break;case "C":p.x+=(el.offsetWidth-w)/2;break;case "r":p.x+=el.offsetWidth-w;break;case "l":break;}self.showAt(p.x,p.y);};if(Calendar.is_khtml)setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()",10);else Calendar.continuation_for_the_fucking_khtml_browser();};Calendar.prototype.setDateFormat=function(str){this.dateFormat=str;};Calendar.prototype.setTtDateFormat=function(str){this.ttDateFormat=str;};Calendar.prototype.parseDate=function(str,fmt){var y=0;var m=-1;var d=0;var a=str.split(/\W+/);if(!fmt){fmt=this.dateFormat;}var b=[];fmt.replace(/(%.)/g,function(str,par){return b[b.length]=par;});var i=0,j=0;var hr=0;var min=0;for(i=0;i<a.length;++i){if(b[i]=="%a"||b[i]=="%A"){continue;}if(b[i]=="%d"||b[i]=="%e"){d=parseInt(a[i],10);}if(b[i]=="%m"){m=parseInt(a[i],10)-1;}if(b[i]=="%Y"||b[i]=="%y"){y=parseInt(a[i],10);(y<100)&&(y+=(y>29)?1900:2000);}if(b[i]=="%b"||b[i]=="%B"){for(j=0;j<12;++j){if(Calendar._MN[j].substr(0,a[i].length).toLowerCase()==a[i].toLowerCase()){m=j;break;}}}else if(/%[HIkl]/.test(b[i])){hr=parseInt(a[i],10);}else if(/%[pP]/.test(b[i])){if(/pm/i.test(a[i])&&hr<12)hr+=12;}else if(b[i]=="%M"){min=parseInt(a[i],10);}}if(y!=0&&m!=-1&&d!=0){this.setDate(new Date(y,m,d,hr,min,0));return;}y=0;m=-1;d=0;for(i=0;i<a.length;++i){if(a[i].search(/[a-zA-Z]+/)!=-1){var t=-1;for(j=0;j<12;++j){if(Calendar._MN[j].substr(0,a[i].length).toLowerCase()==a[i].toLowerCase()){t=j;break;}}if(t!=-1){if(m!=-1){d=m+1;}m=t;}}else if(parseInt(a[i],10)<=12&&m==-1){m=a[i]-1;}else if(parseInt(a[i],10)>31&&y==0){y=parseInt(a[i],10);(y<100)&&(y+=(y>29)?1900:2000);}else if(d==0){d=a[i];}}if(y==0){var today=new Date();y=today.getFullYear();}if(m!=-1&&d!=0){this.setDate(new Date(y,m,d,hr,min,0));}};Calendar.prototype.hideShowCovered=function(){var self=this;Calendar.continuation_for_the_fucking_khtml_browser=function(){function getVisib(obj){var value=obj.style.visibility;if(!value){if(document.defaultView&&typeof(document.defaultView.getComputedStyle)=="function"){if(!Calendar.is_khtml)value=document.defaultView. getComputedStyle(obj,"").getPropertyValue("visibility");else value='';}else if(obj.currentStyle){value=obj.currentStyle.visibility;}else value='';}return value;};var tags=new Array("applet","iframe","select");var el=self.element;var p=Calendar.getAbsolutePos(el);var EX1=p.x;var EX2=el.offsetWidth+EX1;var EY1=p.y;var EY2=el.offsetHeight+EY1;for(var k=tags.length;k>0;){var ar=document.getElementsByTagName(tags[--k]);var cc=null;for(var i=ar.length;i>0;){cc=ar[--i];p=Calendar.getAbsolutePos(cc);var CX1=p.x;var CX2=cc.offsetWidth+CX1;var CY1=p.y;var CY2=cc.offsetHeight+CY1;if(self.hidden||(CX1>EX2)||(CX2<EX1)||(CY1>EY2)||(CY2<EY1)){if(!cc.__msh_save_visibility){cc.__msh_save_visibility=getVisib(cc);}cc.style.visibility=cc.__msh_save_visibility;}else{if(!cc.__msh_save_visibility){cc.__msh_save_visibility=getVisib(cc);}cc.style.visibility="hidden";}}}};if(Calendar.is_khtml)setTimeout("Calendar.continuation_for_the_fucking_khtml_browser()",10);else Calendar.continuation_for_the_fucking_khtml_browser();};Calendar.prototype._displayWeekdays=function(){var MON=this.mondayFirst?0:1;var SUN=this.mondayFirst?6:0;var SAT=this.mondayFirst?5:6;var cell=this.firstdayname;for(var i=0;i<7;++i){cell.className="day name";if(!i){cell.ttip=this.mondayFirst?Calendar._TT["SUN_FIRST"]:Calendar._TT["MON_FIRST"];cell.navtype=100;cell.calendar=this;Calendar._add_evs(cell);}if(i==SUN||i==SAT){Calendar.addClass(cell,"weekend");}cell.firstChild.data=Calendar._SDN[i+1-MON];cell=cell.nextSibling;}};Calendar.prototype._hideCombos=function(){this.monthsCombo.style.display="none";this.yearsCombo.style.display="none";};Calendar.prototype._dragStart=function(ev){if(this.dragging){return;}this.dragging=true;var posX;var posY;if(Calendar.is_ie){posY=window.event.clientY+document.body.scrollTop;posX=window.event.clientX+document.body.scrollLeft;}else{posY=ev.clientY+window.scrollY;posX=ev.clientX+window.scrollX;}var st=this.element.style;this.xOffs=posX-parseInt(st.left);this.yOffs=posY-parseInt(st.top);with(Calendar){addEvent(document,"mousemove",calDragIt);addEvent(document,"mouseover",stopEvent);addEvent(document,"mouseup",calDragEnd);}};Date._MD=new Array(31,28,31,30,31,30,31,31,30,31,30,31);Date.SECOND=1000;Date.MINUTE=60*Date.SECOND;Date.HOUR=60*Date.MINUTE;Date.DAY=24*Date.HOUR;Date.WEEK=7*Date.DAY;Date.prototype.getMonthDays=function(month){var year=this.getFullYear();if(typeof month=="undefined"){month=this.getMonth();}if(((0==(year%4))&&((0!=(year%100))||(0==(year%400))))&&month==1){return 29;}else{return Date._MD[month];}};Date.prototype.getDayOfYear=function(){var now=new Date(this.getFullYear(),this.getMonth(),this.getDate(),0,0,0);var then=new Date(this.getFullYear(),0,1,0,0,0);var time=now-then;return Math.floor(time/Date.DAY);};Date.prototype.getWeekNumber=function(){var now=new Date(this.getFullYear(),this.getMonth(),this.getDate(),0,0,0);var then=new Date(this.getFullYear(),0,1,0,0,0);var time=now-then;var day=then.getDay();if(day==0)day=7;(day>4)&&(day-=4)||(day+=3);return Math.round(((time/Date.DAY)+day)/7);};Date.prototype.equalsTo=function(date){return((this.getFullYear()==date.getFullYear())&&(this.getMonth()==date.getMonth())&&(this.getDate()==date.getDate())&&(this.getHours()==date.getHours())&&(this.getMinutes()==date.getMinutes()));};Date.prototype.print=function(str){var m=this.getMonth();var d=this.getDate();var y=this.getFullYear();var wn=this.getWeekNumber();var w=this.getDay();var s={};var hr=this.getHours();var pm=(hr>=12);var ir=(pm)?(hr-12):hr;var dy=this.getDayOfYear();if(ir==0)ir=12;var min=this.getMinutes();var sec=this.getSeconds();s["%a"]=Calendar._SDN[w];s["%A"]=Calendar._DN[w];s["%b"]=Calendar._SMN[m];s["%B"]=Calendar._MN[m];s["%C"]=1+Math.floor(y/100);s["%d"]=(d<10)?("0"+d):d;s["%e"]=d;s["%H"]=(hr<10)?("0"+hr):hr;s["%I"]=(ir<10)?("0"+ir):ir;s["%j"]=(dy<100)?((dy<10)?("00"+dy):("0"+dy)):dy;s["%k"]=hr;s["%l"]=ir;s["%m"]=(m<9)?("0"+(1+m)):(1+m);s["%M"]=(min<10)?("0"+min):min;s["%n"]="\n";s["%p"]=pm?"PM":"AM";s["%P"]=pm?"pm":"am";s["%s"]=Math.floor(this.getTime()/1000);s["%S"]=(sec<10)?("0"+sec):sec;s["%t"]="\t";s["%U"]=s["%W"]=s["%V"]=(wn<10)?("0"+wn):wn;s["%u"]=w+1;s["%w"]=w;s["%y"]=(''+y).substr(2,2);s["%Y"]=y;s["%%"]="%";var re=Date._msh_formatRegexp;if(typeof re=="undefined"){var tmp="";for(var i in s)tmp+=tmp?("|"+i):i;Date._msh_formatRegexp=re=new RegExp("("+tmp+")",'g');}return str.replace(re,function(match,par){return s[par];});};window.calendar=null;
\ No newline at end of file
diff --git a/httemplate/elements/header.html b/httemplate/elements/header.html
new file mode 100644 (file)
index 0000000..581bbab
--- /dev/null
@@ -0,0 +1,19 @@
+<%
+  my($title, $menubar) = @_;
+  my $etc = @_ ? shift : ''; #$etc is for things like onLoad= etc.
+%>
+    <HTML>
+      <HEAD>
+        <TITLE>
+          <%= $title %>
+        </TITLE>
+        <META HTTP-Equiv="Cache-Control" Content="no-cache">
+        <META HTTP-Equiv="Pragma" Content="no-cache">
+        <META HTTP-Equiv="Expires" Content="0"> 
+      </HEAD>
+      <BODY BGCOLOR="#e8e8e8"<%= $etc %>>
+          <FONT SIZE=7>
+            <%= $title %>
+          </FONT>
+          <BR><BR>
+          <%= $menubar ? "$menubar<BR><BR>" : '' %>
diff --git a/httemplate/elements/menubar.html b/httemplate/elements/menubar.html
new file mode 100644 (file)
index 0000000..87a5031
--- /dev/null
@@ -0,0 +1,8 @@
+<%
+  my($item, $url, @html);
+  while (@_) {
+    ($item, $url) = splice(@_,0,2);
+    push @html, qq!<A HREF="$url">$item</A>!;
+  }
+%>
+<%= join(' | ', @html) %>
diff --git a/httemplate/elements/pager.html b/httemplate/elements/pager.html
new file mode 100644 (file)
index 0000000..db9ff83
--- /dev/null
@@ -0,0 +1,42 @@
+<%
+
+  my %opt = @_;
+
+  my $pager = '';
+  if ( $opt{'total'} != $opt{'num_rows'} && $opt{'maxrecords'} ) {
+    unless ( $opt{'offset'} == 0 ) {
+      $cgi->param('offset', $opt{'offset'} - $opt{'maxrecords'});
+%>
+
+      <A HREF="<%= $cgi->self_url %>"><B><FONT SIZE="+1">Previous</FONT></B></A>
+
+<%
+    }
+    my $page = 0;
+    for ( my $poff = 0; $poff < $opt{'total'}; $poff += $opt{'maxrecords'} ) {
+      $page++;
+      if ( $opt{'offset'} == $poff ) {
+%>
+
+        <FONT SIZE="+2"><%= $page %></FONT>
+
+<%
+      } else {
+        $cgi->param('offset', $poff);
+%>
+
+        <A HREF="<%= $cgi->self_url %>">$page</A>
+
+<%
+      }
+    }
+    unless ( $opt{'offset'} + $opt{'maxrecords'} > $opt{'total'} ) {
+      $cgi->param('offset', $opt{'offset'} + $opt{'maxrecords'});
+%>
+
+      <A HREF="<%= $cgi->self_url %>"><B><FONT SIZE="+1">Next</FONT></B></A>
+
+<%
+    }
+  }
+%>
diff --git a/httemplate/elements/table.html b/httemplate/elements/table.html
new file mode 100644 (file)
index 0000000..3b61087
--- /dev/null
@@ -0,0 +1,8 @@
+<%
+   my $color = shift;
+   if ( $color ) {
+%>
+    <TABLE BGCOLOR="<%= $color %>" BORDER=1 WIDTH="100%" CELLSPACING=0 CELLPADDING=2 BORDERCOLOR="#999999">
+<% } else { %>
+    <TABLE BORDER=1 CELLSPACING=0 CELLPADDING=2 BORDERCOLOR="#999999">
+<% } %>
index 944019a..76f1bd7 100755 (executable)
@@ -62,14 +62,14 @@ while ( $syear < $eyear || ( $syear == $eyear && $smonth < $emonth ) ) {
   my $refunded = $refunded_sth->fetchrow_arrayref->[0] || 0;
 
     #horrible local kludge that doesn't even really work right
-    my $expenses_sql = "SELECT SUM(cust_bill_pay.amount) FROM cust_bill_pay, cust_bill WHERE cust_bill_pay.invnum = cust_bill.invnum AND cust_bill_pay._date >= $speriod AND cust_bill_pay._date < $eperiod AND 0 < ( select count(*) from cust_bill_pkg, cust_pkg, part_pkg WHERE cust_bill.invnum = cust_bill_pkg.invnum AND cust_pkg.pkgnum = cust_bill_pkg.pkgnum AND cust_pkg.pkgpart = part_pkg.pkgpart AND LOWER(part_pkg.pkg) LIKE 'expense _%' )";
+    my $expenses_sql2 = "SELECT SUM(cust_bill_pay.amount) FROM cust_bill_pay, cust_bill WHERE cust_bill_pay.invnum = cust_bill.invnum AND cust_bill_pay._date >= $speriod AND cust_bill_pay._date < $eperiod AND 0 < ( select count(*) from cust_bill_pkg, cust_pkg, part_pkg WHERE cust_bill.invnum = cust_bill_pkg.invnum AND cust_pkg.pkgnum = cust_bill_pkg.pkgnum AND cust_pkg.pkgpart = part_pkg.pkgpart AND LOWER(part_pkg.pkg) LIKE 'expense _%' )";
 
-#    my $expenses_sql = "SELECT SUM(cust_bill_pay.amount) FROM cust_bill_pay, cust_bill_pkg, cust_bill, cust_pkg, part_pkg WHERE cust_bill_pay.invnum = cust_bill.invnum AND cust_bill.invnum = cust_bill_pkg.invnum AND cust_bill_pay._date >= $speriod AND cust_bill_pay._date < $eperiod AND cust_pkg.pkgnum = cust_bill_pkg.pkgnum AND cust_pkg.pkgpart = part_pkg.pkgpart AND LOWER(part_pkg.pkg) LIKE 'expense _%'";
-    my $expenses_sth = dbh->prepare($expenses_sql) or die dbh->errstr;
-    $expenses_sth->execute or die $expenses_sth->errstr;
-    my $expenses = $expenses_sth->fetchrow_arrayref->[0] || 0;
+#    my $expenses_sql2 = "SELECT SUM(cust_bill_pay.amount) FROM cust_bill_pay, cust_bill_pkg, cust_bill, cust_pkg, part_pkg WHERE cust_bill_pay.invnum = cust_bill.invnum AND cust_bill.invnum = cust_bill_pkg.invnum AND cust_bill_pay._date >= $speriod AND cust_bill_pay._date < $eperiod AND cust_pkg.pkgnum = cust_bill_pkg.pkgnum AND cust_pkg.pkgpart = part_pkg.pkgpart AND LOWER(part_pkg.pkg) LIKE 'expense _%'";
+    my $expenses_sth2 = dbh->prepare($expenses_sql2) or die dbh->errstr;
+    $expenses_sth2->execute or die $expenses_sth2->errstr;
+    my $expenses2 = $expenses_sth2->fetchrow_arrayref->[0] || 0;
 
-  push @{$data{cash}}, $paid-$refunded-$expenses;
+  push @{$data{cash}}, $paid-$refunded-$expenses2;
 
 }
 
@@ -101,7 +101,7 @@ my @data = ( \@labels,
 #$chart->cgi_png(\@data);
 
 http_header('Content-Type' => 'image/png' );
-$Response->{ContentType} = 'image/png';
+#$Response->{ContentType} = 'image/png';
 
 $chart->_set_colors();
 
index e24157c..de8f6ee 100644 (file)
@@ -1,4 +1,4 @@
-<!-- mason kludge %>
+<!-- mason kludge -->
 <%
 
 #my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
@@ -32,9 +32,9 @@ my $emonth = $cgi->param('emonth') || $curmon+1;
   Cashflow (payments - refunds)<BR>
 <BR>
 From <SELECT NAME="smonth">
-<% my @m = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
-   foreach my $m ( 1..12 ) { %>
-<OPTION VALUE="<%= $m %>"<%= $m == $smonth ? ' SELECTED' : '' %>><%= $m[$m-1] %>
+<% my @mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); %>
+<% foreach my $mon ( 1..12 ) { %>
+<OPTION VALUE="<%= $mon %>"<%= $mon == $smonth ? ' SELECTED' : '' %>><%= $mon[$mon-1] %>
 <% } %>
 </SELECT>
 <SELECT NAME="syear">
@@ -43,8 +43,8 @@ From <SELECT NAME="smonth">
 <% } %>
 </SELECT>
  to <SELECT NAME="emonth">
-<% foreach my $m ( 1..12 ) { %>
-<OPTION VALUE="<%= $m %>"<%= $m == $emonth ? ' SELECTED' : '' %>><%= $m[$m-1] %>
+<% foreach my $mon ( 1..12 ) { %>
+<OPTION VALUE="<%= $mon %>"<%= $mon == $emonth ? ' SELECTED' : '' %>><%= $mon[$mon-1] %>
 <% } %>
 </SELECT>
 <SELECT NAME="eyear">
diff --git a/httemplate/images/calendar.png b/httemplate/images/calendar.png
new file mode 100644 (file)
index 0000000..1632661
Binary files /dev/null and b/httemplate/images/calendar.png differ
diff --git a/httemplate/images/cvv2.png b/httemplate/images/cvv2.png
new file mode 100644 (file)
index 0000000..4610dcb
Binary files /dev/null and b/httemplate/images/cvv2.png differ
diff --git a/httemplate/images/cvv2_amex.png b/httemplate/images/cvv2_amex.png
new file mode 100644 (file)
index 0000000..21c36a0
Binary files /dev/null and b/httemplate/images/cvv2_amex.png differ
index 1895f29..d39f8b0 100644 (file)
@@ -11,8 +11,8 @@
     </td><td>
       <font color="#ff0000" size=7>freeside main menu</font>
     </td><td align=right valign=bottom>
-      version 1.5.0pre2
-      <BR><A HREF="http://www.sisd.com/freeside">Freeside home page</A>
+       version %%%VERSION%%%
+      <BR><A HREF="http://www.sisd.com/freeside">Freeside&nbsp;home&nbsp;page</A>
       <BR><A HREF="docs/">Documentation</A>
     </td></tr>
   </table>
@@ -34,7 +34,8 @@
         <FORM ACTION="search/cust_main.cgi" METHOD="POST"><INPUT TYPE="hidden" NAME="phone_on" VALUE="1">Phone # <INPUT TYPE="text" NAME="phone_text"><INPUT TYPE="submit" VALUE="Search"></FORM>
         <BR><FORM ACTION="search/svc_acct.cgi" METHOD="POST">Username <INPUT TYPE="text" NAME="username"><SELECT NAME="username_type"><OPTION VALUE="All">(all)</OPTION><OPTION>Fuzzy</OPTION><OPTION>Substring</OPTION><OPTION SELECTED>Exact</OPTION></SELECT><INPUT TYPE="submit" VALUE="Search"> or <A HREF="search/svc_acct.cgi?username">all accounts by username</A> or <A HREF="search/svc_acct.cgi?uid">uid</A></FORM>
         <BR><FORM ACTION="search/svc_domain.cgi" METHOD="POST">Domain <INPUT TYPE="text" NAME="domain"><INPUT TYPE="submit" VALUE="Search"> or <A HREF="search/svc_domain.cgi?domain">all domains</A></FORM>
-<!--        <LI><A HREF="search/svc_forward.html">mail forwards (by ?)</A>-->
+        <BR><A HREF="search/svc_forward.cgi?svcnum">all mail forwards by svcnum</A><BR>
+
       <BR>
     </TD></TR>
     </TABLE>
@@ -56,7 +57,7 @@
       <BR><FORM ACTION="search/cust_main.cgi" METHOD="POST">Credit card # <INPUT TYPE="hidden" NAME="card_on" VALUE="1"><INPUT TYPE="text" NAME="card"><INPUT TYPE="submit" VALUE="Search"></FORM>
       <FORM ACTION="search/cust_bill.cgi" METHOD="POST">Invoice # <INPUT TYPE="text" NAME="invnum" SIZE="8"><INPUT TYPE="submit" VALUE="Search"></FORM>
       <FORM ACTION="search/cust_pay.cgi" METHOD="POST">Check # <INPUT TYPE="text" NAME="payinfo" SIZE="8"><INPUT TYPE="hidden" NAME="payby" VALUE="BILL"><INPUT TYPE="submit" VALUE="Search"></FORM>
-      <BR><A HREF="browse/cust_pay_batch.cgi">View pending credit card batch</A>      <BR><BR><A HREF="search/cust_pkg.html">Packages (by next bill date range)</A>
+      <BR><A HREF="browse/cust_pay_batch.cgi">View pending credit card batch</A>      <BR><BR><A HREF="search/cust_pkg_report.cgi">Packages (by next bill date range)</A>
       <BR><BR>Invoice reports
             <UL>
               <LI><a href="search/cust_bill_event.html">Invoice event errors (failed credit cards)</a>
               <LI>all invoices (<A HREF="search/cust_bill.cgi?invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?date">by date</A>) (<A HREF="search/cust_bill.cgi?custnum">by customer number</A>)
             </UL>
       <A HREF="search/report_cust_pay.html">Payment report (by type and/or date range)</A>
-      <BR><BR>Financial reports
+      <BR><BR><A HREF="search/report_receivables.cgi">Accounts Receivable Aging Summary</A>
+      <BR><BR><A HREF="search/report_prepaid_income.html">Prepaid Income (Unearned Revenue) Report</A>
+      <BR><BR>(old) Financial reports (being rewritten)
             <UL>
-              <LI> <A HREF="search/report_receivables.cgi">current receivables</A>
               <LI> <A HREF="search/report_tax.html">tax reports</A>
               <LI> <A HREF="search/report_cc.html">credit card receipts</A>
               <LI> <A HREF="search/report_credit.html">credit memos</A>
       Packages
       <UL>
         <LI><A HREF="search/cust_pkg.cgi?pkgnum">all packages (by package number)</A>
-        <LI><A HREF="search/cust_pkg.cgi?SUSP_pkgnum">suspended packages (by package number)</A>
+        <LI><A HREF="search/cust_pkg.cgi?magic=suspended">suspended packages (by package number)</A>
         <LI><A HREF="search/cust_pkg.cgi?APKG_pkgnum">packages with unconfigured services (by package number)</A>
-        <LI><A HREF="search/cust_pkg.html">packages (by next bill date range)</A>
-      </UL>
-      <A HREF="browse/part_pkg.cgi?active=1">Package definitions (by number of active packages)</A>
-      <BR><BR>Invoices
-      <UL>
-        <LI><a href="search/cust_bill_event.html">Invoice event errors (failed credit cards)</a>
-        <LI>open invoices (<A HREF="search/cust_bill.cgi?OPEN_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN_custnum">by customer number</A>)
-        <LI>30 day open invoices (<A HREF="search/cust_bill.cgi?OPEN30_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN30_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN30_custnum">by customer number</A>)
-        <LI>60 day open invoices (<A HREF="search/cust_bill.cgi?OPEN60_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN60_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN60_custnum">by customer number</A>)
-        <LI>90 day open invoices (<A HREF="search/cust_bill.cgi?OPEN90_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN90_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN90_custnum">by customer number</A>)
-        <LI>120 day open invoices (<A HREF="search/cust_bill.cgi?OPEN120_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN120_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN120_custnum">by customer number</A>)
-        <LI>all invoices (<A HREF="search/cust_bill.cgi?invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?date">by date</A>) (<A HREF="search/cust_bill.cgi?custnum">by customer number</A>)
+        <LI><A HREF="search/cust_pkg_report.cgi">packages (by next bill date range)</A>
       </UL>
-    <A HREF="search/report_cust_pay.html">Payment Report (by type and/or date range)</A>
-    <BR><BR>Financial reports
-            <UL>
-              <LI> <A HREF="search/report_receivables.cgi">current receivables</A>
-              <LI> <A HREF="search/report_tax.html">tax reports</A>
-              <LI> <A HREF="search/report_cc.html">credit card receipts</A>
-              <LI> <A HREF="search/report_credit.html">credit memos</A>
-            </UL>
+      <A HREF="browse/part_pkg.cgi?active=1">Package definitions (by number of active packages)</A><BR><BR>
+      <A HREF="browse/part_svc.cgi?active=1">Service definitions (by number of active services)</A><BR><BR>
     Customers
       <UL>
         <LI><A HREF="search/cust_main-otaker.cgi">Search customers by order-taker</A>
       </UL>
-    <FORM ACTION="search/sql.cgi" METHOD="POST">SQL query: <TT>SELECT </TT><INPUT TYPE="text" NAME="sql" SIZE=32><INPUT TYPE="submit" VALUE="Query"></FORM>
+    <FORM ACTION="search/sql.html" METHOD="POST">SQL query: <TT>SELECT </TT><INPUT TYPE="text" NAME="sql" SIZE=32><INPUT TYPE="submit" VALUE="Query"></FORM>
 
     <BR>
     </TD></TR>
       <BR><A HREF="browse/queue.cgi">View pending job queue</A>
       <BR><A HREF="misc/cust_main-import.cgi">Batch import customers from CSV file</A>
       <BR><A HREF="misc/cust_main-import_charges.cgi">Batch import charges from CSV file</A>
+      <BR><A HREF="misc/dump.cgi">Download database dump</A>
       <BR><BR><CENTER><HR WIDTH="94%" NOSHADE></CENTER><BR>
       <A NAME="config" HREF="config/config-view.cgi">Configuration</a><!-- - <font size="+2" color="#ff0000">start here</font> -->
       <BR><BR><A NAME="admin">Administration</a>
           <LI><A HREF="browse/cust_main_county.cgi">View/Edit locales and tax rates</A>
             - Change tax rates, or break down a country into states, or a state
               into counties and assign different tax rates to each.
-          <LI><A HREF="browse/svc_acct_pop.cgi">View/Edit Access Numbers</A>
+          <LI><A HREF="browse/svc_acct_pop.cgi">View/Edit access numbers</A>
             - Points of Presence 
           <LI><A HREF="browse/part_bill_event.cgi">View/Edit invoice events</A> - Actions for overdue invoices
          <LI><A HREF="browse/msgcat.cgi">View/Edit message catalog</A> - Change error messages and other customizable labels.
index 526e128..257c338 100755 (executable)
@@ -7,8 +7,8 @@ my $custnum = $1;
 
 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
 
-my $error = $cust_main->cancel;
-eidiot($error) if $error;
+my @errors = $cust_main->cancel;
+eidiot(join(' / ', @errors)) if scalar(@errors);
 
 #print $cgi->redirect($p. "view/cust_main.cgi?". $cust_main->custnum);
 print $cgi->redirect($p);
diff --git a/httemplate/misc/delete-cust_credit.cgi b/httemplate/misc/delete-cust_credit.cgi
new file mode 100755 (executable)
index 0000000..30de04d
--- /dev/null
@@ -0,0 +1,16 @@
+<%
+
+#untaint crednum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal crednum";
+my $crednum = $1;
+
+my $cust_credit = qsearchs('cust_credit',{'crednum'=>$crednum});
+my $custnum = $cust_credit->custnum;
+
+my $error = $cust_credit->delete;
+eidiot($error) if $error;
+
+print $cgi->redirect($p. "view/cust_main.cgi?". $custnum);
+
+%>
diff --git a/httemplate/misc/download-batch.cgi b/httemplate/misc/download-batch.cgi
new file mode 100644 (file)
index 0000000..306ef5d
--- /dev/null
@@ -0,0 +1,16 @@
+<%
+
+#http_header('Content-Type' => 'text/comma-separated-values' ); #IE chokes
+http_header('Content-Type' => 'text/plain' );
+
+for my $cust_pay_batch ( sort { $a->paybatchnum <=> $b->paybatchnum }
+                              qsearch('cust_pay_batch', {} )
+) {
+
+$cust_pay_batch->exp =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+my( $mon, $y ) = ( $2, $1 );
+$mon = "0$mon" if $mon < 10;
+my $exp = "$mon$y";
+
+%>,,,,<%= $cust_pay_batch->cardnum %>,<%= $exp %>,<%= $cust_pay_batch->amount %>,<%= $cust_pay_batch->paybatchnum %>
+<% } %>
diff --git a/httemplate/misc/dump.cgi b/httemplate/misc/dump.cgi
new file mode 100644 (file)
index 0000000..dc1323b
--- /dev/null
@@ -0,0 +1,19 @@
+<%
+  if ( driver_name =~ /^Pg$/ ) {
+    my $dbname = (split(':', datasrc))[2];
+    if ( $dbname =~ /[;=]/ ) {
+      my %elements = map { /^(\w+)=(.*)$/; $1=>$2 } split(';', $dbname);
+      $dbname = $elements{'dbname'};
+    }
+    open(DUMP,"pg_dump $dbname |");
+  } else {
+    eidiot "don't (yet) know how to dump ". driver_name. " databases\n";
+  }
+
+  http_header('Content-Type' => 'text/plain' );
+
+  while (<DUMP>) {
+    print $_;
+  }
+  close DUMP;
+%>
diff --git a/httemplate/misc/email-invoice.cgi b/httemplate/misc/email-invoice.cgi
new file mode 100755 (executable)
index 0000000..7ab1613
--- /dev/null
@@ -0,0 +1,23 @@
+<%
+
+my $conf = new FS::Conf;
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d*)$/;
+my $invnum = $1;
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+die "Can't find invoice!\n" unless $cust_bill;
+
+my $error = send_email(
+  'from'    => $conf->config('invoice_from'),
+  'to'      => [ grep { $_ ne 'POST' } $cust_bill->cust_main->invoicing_list ],
+  'subject' => 'Invoice',
+  'body'    => [ $cust_bill->print_text ],
+);
+eidiot($error) if $error;
+
+my $custnum = $cust_bill->getfield('custnum');
+print $cgi->redirect("${p}view/cust_main.cgi?$custnum");
+
+%>
index a5500bf..144f615 100755 (executable)
@@ -11,13 +11,19 @@ my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
 die "Can't find invoice!\n" unless $cust_bill;
 
         open(LPR,"|$lpr") or die "Can't open $lpr: $!";
-        print LPR $cust_bill->print_text; #( date )
+
+        if ( $conf->exists('invoice_latex') ) {
+          print LPR $cust_bill->print_ps; #( date )
+        } else {
+          print LPR $cust_bill->print_text; #( date )
+        }
+
         close LPR
           or die $! ? "Error closing $lpr: $!"
                        : "Exit status $? from $lpr";
 
 my $custnum = $cust_bill->getfield('custnum');
 
-print $cgi->redirect(popurl(2). "view/cust_main.cgi?$custnum#history");
+print $cgi->redirect("${p}view/cust_main.cgi?$custnum");
 
 %>
index 2939c8f..59d236f 100644 (file)
@@ -116,8 +116,8 @@ function SafeOnsubmit() {
   #hashmaker widget
   sub hashmaker {
     my($name, $from, $to, $labelfrom, $labelto) = @_;
-    $fromsize = scalar(@$from);
-    $tosize = scalar(@$to);
+    my $fromsize = scalar(@$from);
+    my $tosize = scalar(@$to);
     "<TABLE><TR><TH>$labelfrom</TH><TH>$labelto</TH></TR><TR><TD>".
         qq!<SELECT NAME="${name}_from" SIZE=$fromsize>\n!.
         join("\n", map { qq!<OPTION VALUE="$_">$_</OPTION>! } sort { $a cmp $b } @$from ).
diff --git a/httemplate/misc/upload-batch.cgi b/httemplate/misc/upload-batch.cgi
new file mode 100644 (file)
index 0000000..cc53466
--- /dev/null
@@ -0,0 +1,29 @@
+<%
+
+  my $fh = $cgi->upload('batch_results');
+  my $filename = $cgi->param('batch_results');
+  $filename =~ /^.*[\/\\]([^\/\\]+)$/ or die;
+  my $paybatch = $1;
+
+  my $error = defined($fh)
+    ? FS::cust_pay_batch::import_results( {
+        'filehandle' => $fh,
+        'format'     => $cgi->param('format'),
+        'paybatch'   => $paybatch,
+      } )
+    : 'No file';
+
+  if ( $error ) {
+    %>
+    <!-- mason kludge -->
+    <%
+    eidiot($error);
+#    $cgi->param('error', $error);
+#    print $cgi->redirect( "${p}cust_main-import.cgi
+  } else {
+    %>
+    <!-- mason kludge -->
+    <%= header('Batch results upload sucessful') %> <%
+  }
+%>
+
index b76f66b..ec952ea 100644 (file)
@@ -7,7 +7,7 @@ $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/;
 my $beginning = str2time($1) || 0;
 
 $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/;
-my $ending = str2time($1) + 86400;
+my $ending = str2time($1) + 86399;
 
 my @cust_bill_event =
   sort { $a->_date <=> $b->_date }
index 9f39db9..d48f1d0 100755 (executable)
@@ -7,27 +7,28 @@
       Quick payment entry
     </FONT>
     <BR><BR>
+    <A HREF="../">Main Menu</A><BR><BR>
     <FORM ACTION="cust_main.cgi" METHOD="post">
       <INPUT TYPE="hidden" NAME="quickpay" VALUE="yes">
       <INPUT TYPE="checkbox" NAME="last_on" CHECKED> Search for <B>last name</B>: 
       <INPUT TYPE="text" NAME="last_text">
       using search method: <SELECT NAME="last_type">
-        <OPTION SELECTED>All
+        <OPTION>All
         <OPTION>Fuzzy
         <OPTION>Substring
-        <OPTION>Exact
+        <OPTION SELECTED>Exact
       </SELECT>
 
       <P><INPUT TYPE="checkbox" NAME="company_on" CHECKED> Search for <B>company</B>: 
       <INPUT TYPE="text" NAME="company_text">
       using search methods: <SELECT NAME="company_type">
-        <OPTION SELECTED>All
+        <OPTION>All
         <OPTION>Fuzzy
         <OPTION>Substring
-        <OPTION>Exact
+        <OPTION SELECTED>Exact
       </SELECT>
 
-      <P><INPUT TYPE="submit" VALUE="Search"> Note: Fuzzy searching can take a while.  Please be patient.
+      <P><INPUT TYPE="submit" VALUE="Search">
 
     </FORM>
 
index 5b39a09..50d367e 100755 (executable)
@@ -50,6 +50,7 @@ my $total = 0;
 my(@cust_main, $sortby, $orderby);
 if ( $cgi->param('browse')
      || $cgi->param('otaker_on')
+     || $cgi->param('agentnum_on')
 ) {
 
   my %search = ();
@@ -73,6 +74,9 @@ if ( $cgi->param('browse')
     if ( $cgi->param('otaker_on') ) {
       $cgi->param('otaker') =~ /^(\w{1,32})$/ or eidiot "Illegal otaker\n";
       $search{otaker} = $1;
+    } elsif ( $cgi->param('agentnum_on') ) {
+      $cgi->param('agentnum') =~ /^(\d+)$/ or eidiot "Illegal agentnum\n";
+      $search{agentnum} = $1;
     } else {
       die "unknown query...";
     }
@@ -112,6 +116,7 @@ if ( $cgi->param('browse')
                AND (temp1_$$.count > 0
                        OR temp2_$$.count = 0 )
        ";
+
     } else {
        $ncancelled = "
           0 < ( SELECT COUNT(*) FROM cust_pkg
@@ -124,15 +129,32 @@ if ( $cgi->param('browse')
                        WHERE cust_pkg.custnum = cust_main.custnum
                    )
        ";
-    }
-
+     }
+   }
+
+  my $cancelled = '';
+  if ( $cgi->param('cancelled') ) {
+    $cancelled = "
+      0 = ( SELECT COUNT(*) FROM cust_pkg
+                   WHERE cust_pkg.custnum = cust_main.custnum
+                      AND ( cust_pkg.cancel IS NULL
+                            OR cust_pkg.cancel = 0
+                          )
+          )
+        AND 0 < ( SELECT COUNT(*) FROM cust_pkg
+                    WHERE cust_pkg.custnum = cust_main.custnum
+                )
+    ";
   }
 
   #EWWWWWW
   my $qual = join(' AND ',
             map { "$_ = ". dbh->quote($search{$_}) } keys %search );
 
-  if ( $ncancelled ) {
+  if ( $cancelled ) {
+    $qual .= ' AND ' if $qual;
+    $qual .= $cancelled;
+  } elsif ( $ncancelled ) {
     $qual .= ' AND ' if $qual;
     $qual .= $ncancelled;
   }
@@ -150,21 +172,22 @@ if ( $cgi->param('browse')
 
   $total = $sth->fetchrow_arrayref->[0];
 
-  if ( $ncancelled ) {
+  my $rqual = $cancelled || $ncancelled;
+  if ( $rqual ) {
     if ( %search ) {
-      $ncancelled = " AND $ncancelled";
+      $rqual = " AND $rqual";
     } else {
-      $ncancelled = " WHERE $ncancelled";
+      $rqual = " WHERE $rqual";
     }
   }
 
   my @just_cust_main;
   if ( driver_name eq 'mysql' ) {
     @just_cust_main = qsearch('cust_main', \%search, 'cust_main.*',
-                              ",temp1_$$,temp2_$$ $ncancelled $orderby $limit");
+                              ",temp1_$$,temp2_$$ $rqual $orderby $limit");
   } else {
     @just_cust_main = qsearch('cust_main', \%search, '',   
-                              "$ncancelled $orderby $limit" );
+                              "$rqual $orderby $limit" );
   }
   if ( driver_name eq 'mysql' ) {
     my $sql = "DROP TABLE temp1_$$,temp2_$$;";
@@ -213,9 +236,12 @@ if ( $cgi->param('browse')
   }
 
   @cust_main = grep { $_->ncancelled_pkgs || ! $_->all_pkgs } @cust_main
-    if $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me
-       || ( $conf->exists('hidecancelledcustomers')
-             && ! $cgi->param('showcancelledcustomers') );
+    if ! $cgi->param('cancelled')
+       && (
+         $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me
+         || ( $conf->exists('hidecancelledcustomers')
+               && ! $cgi->param('showcancelledcustomers') )
+       );
 
   my %saw = ();
   @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
@@ -277,19 +303,22 @@ if ( scalar(@cust_main) == 1 && ! $cgi->param('referral_custnum') ) {
     }
   }
   #end pager
-  
-  if ( $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me
-       || ( $conf->exists('hidecancelledcustomers')
-            && ! $cgi->param('showcancelledcustomers')
-          )
-     ) {
-    $cgi->param('showcancelledcustomers', 1);
-    $cgi->param('offset', 0);
-    print qq!( <a href="!. $cgi->self_url. qq!">show cancelled customers</a> )!;
-  } else {
-    $cgi->param('showcancelledcustomers', 0);
-    $cgi->param('offset', 0);
-    print qq!( <a href="!. $cgi->self_url. qq!">hide cancelled customers</a> )!;
+
+  unless ( $cgi->param('cancelled') ) {
+    if ( $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me
+         || ( $conf->exists('hidecancelledcustomers')
+              && ! $cgi->param('showcancelledcustomers')
+            )
+       ) {
+      $cgi->param('showcancelledcustomers', 1);
+      $cgi->param('offset', 0);
+      print qq!( <a href="!. $cgi->self_url. qq!">show!;
+    } else {
+      $cgi->param('showcancelledcustomers', 0);
+      $cgi->param('offset', 0);
+      print qq!( <a href="!. $cgi->self_url. qq!">hide!;
+    }
+    print ' cancelled customers</a> )';
   }
   if ( $cgi->param('referral_custnum') ) {
     $cgi->param('referral_custnum') =~ /^(\d+)$/
index e4dba01..51dd3b3 100755 (executable)
@@ -5,27 +5,53 @@ my @cust_pay;
 if ( $cgi->param('magic') && $cgi->param('magic') eq '_date' ) {
 
   my %search;
+  my @search;
+
   if ( $cgi->param('payby') ) {
-    $cgi->param('payby') =~ /^(CARD|CHEK|BILL)$/
+    $cgi->param('payby') =~ /^(CARD|CHEK|BILL)(-(VisaMC|Amex|Discover))?$/
       or die "illegal payby ". $cgi->param('payby');
     $search{'payby'} = $1;
+    if ( $3 ) {
+      if ( $3 eq 'VisaMC' ) {
+        #avoid posix regexes for portability
+        push @search, " (    substring(payinfo from 1 for 1) = '4'  ".
+                      "   OR substring(payinfo from 1 for 2) = '51' ".
+                      "   OR substring(payinfo from 1 for 2) = '52' ".
+                      "   OR substring(payinfo from 1 for 2) = '53' ".
+                      "   OR substring(payinfo from 1 for 2) = '54' ".
+                      "   OR substring(payinfo from 1 for 2) = '54' ".
+                      "   OR substring(payinfo from 1 for 2) = '55' ".
+                      " ) ";
+      } elsif ( $3 eq 'Amex' ) {
+        push @search, " (    substring(payinfo from 1 for 2 ) = '34' ".
+                      "   OR substring(payinfo from 1 for 2 ) = '37' ".
+                      " ) ";
+      } elsif ( $3 eq 'Discover' ) {
+        push @search, " substring(payinfo from 1 for 4 ) = '6011' ";
+      } else {
+        die "unknown card type $3";
+      }
+    }
   }
 
   #false laziness with cust_pkg.cgi
-  my $range = '';
   if ( $cgi->param('beginning')
        && $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/ ) {
     my $beginning = str2time($1);
-    $range = " WHERE _date >= $beginning ";
+    push @search, "_date >= $beginning ";
   }
   if ( $cgi->param('ending')
             && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) {
-    my $ending = str2time($1) + 86400;
-    $range .= ( $range ? ' AND ' : ' WHERE ' ). " _date <= $ending ";
+    my $ending = str2time($1) + 86399;
+    push @search, " _date <= $ending ";
+  }
+  my $search;
+  if ( @search ) {
+    $search = ( scalar(keys %search) ? ' AND ' : ' WHERE ' ).
+              join(' AND ', @search);
   }
-  $range =~ s/^\s*WHERE/ AND/ if scalar(keys %search) ;
 
-  @cust_pay = qsearch('cust_pay', \%search, '', $range );
+  @cust_pay = qsearch('cust_pay', \%search, '', $search );
 
   $sortby = \*date_sort;
 
index 8b2fd0c..45420f4 100755 (executable)
@@ -29,10 +29,18 @@ if ( $cgi->param('magic') && $cgi->param('magic') eq 'bill' ) {
   }
   if ( $cgi->param('ending')
             && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) {
-    my $ending = str2time($1) + 86400;
+    my $ending = str2time($1) + 86399;
     $range .= ( $range ? ' AND ' : ' WHERE ' ). " bill <= $ending ";
   }
 
+  $range .= ( $range ? 'AND ' : ' WHERE ' ). '( cancel IS NULL OR cancel = 0 )';
+
+  if ( $cgi->param('agentnum') =~ /^(\d+)$/ and $1 ) {
+    $range .= ( $range ? 'AND ' : ' WHERE ' ). 
+              "$1 = ( SELECT agentnum FROM cust_main".
+                    " WHERE cust_main.custnum = cust_pkg.custnum )";
+  }
+
   #false laziness with below
   my $statement = "SELECT COUNT(*) FROM cust_pkg $range";
   warn $statement;
@@ -46,10 +54,21 @@ if ( $cgi->param('magic') && $cgi->param('magic') eq 'bill' ) {
 } else {
 
   my $qual = '';
-  if ( $cgi->param('magic') && $cgi->param('magic') eq 'active' ) {
+  if ( $cgi->param('magic') &&
+       $cgi->param('magic') =~ /^(active|suspended|canceled)$/
+  ) {
 
-    $qual = 'WHERE ( susp IS NULL OR susp = 0 )'.
-            ' AND ( cancel IS NULL OR cancel = 0)';
+    if ( $cgi->param('magic') eq 'active' ) {
+      $qual = 'WHERE ( susp IS NULL OR susp = 0 )'.
+              ' AND ( cancel IS NULL OR cancel = 0)';
+    } elsif ( $cgi->param('magic') eq 'suspended' ) {
+      $qual = 'WHERE susp IS NOT NULL AND susp != 0'.
+              ' AND ( cancel IS NULL OR cancel = 0)';
+    } elsif ( $cgi->param('magic') eq 'canceled' ) {
+      $qual = 'WHERE cancel IS NOT NULL AND cancel != 0';
+    } else {
+      die "guru meditation #420";
+    }
 
     $sortby = \*pkgnum_sort;
 
@@ -61,12 +80,6 @@ if ( $cgi->param('magic') && $cgi->param('magic') eq 'bill' ) {
 
     $sortby=\*pkgnum_sort;
 
-  } elsif ( $query eq 'SUSP_pkgnum' ) {
-
-    $sortby=\*pkgnum_sort;
-
-    $qual = 'WHERE susp IS NOT NULL AND susp != 0';
-
   } elsif ( $query eq 'APKG_pkgnum' ) {
   
     $sortby=\*pkgnum_sort;
diff --git a/httemplate/search/cust_pkg.html b/httemplate/search/cust_pkg.html
deleted file mode 100755 (executable)
index bb0a540..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<HTML>
-  <HEAD>
-    <TITLE>Packages</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-      <H1>Packages</H1>
-    </CENTER>
-    <HR>
-    <FORM ACTION="cust_pkg.cgi" METHOD="post">
-    <INPUT TYPE="hidden" NAME="magic" VALUE="bill">
-      Return <B>packages</B> with next bill date: 
-      from <INPUT TYPE="text" NAME="beginning"> <i>m/d/y</i>
-      to <INPUT TYPE="text" NAME="ending"> <i>m/d/y</i>
-
-      <P><INPUT TYPE="submit" VALUE="Get Report">
-
-    </FORM>
-
-  <HR>
-
-  </BODY>
-</HTML>
-
diff --git a/httemplate/search/cust_pkg_report.cgi b/httemplate/search/cust_pkg_report.cgi
new file mode 100755 (executable)
index 0000000..b316745
--- /dev/null
@@ -0,0 +1,63 @@
+<HTML>
+  <HEAD>
+    <TITLE>Packages</TITLE>
+    <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <H1>Packages</H1>
+    <FORM ACTION="cust_pkg.cgi" METHOD="post">
+    <INPUT TYPE="hidden" NAME="magic" VALUE="bill">
+      Return packages with next bill date:<BR><BR>
+      <TABLE>
+        <TR>
+          <TD ALIGN="right">From: </TD>
+          <TD><INPUT TYPE="text" NAME="beginning" ID="beginning_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="beginning_button" STYLE="cursor: pointer" TITLE="Select date"><BR><I>m/d/y</I></TD>
+<SCRIPT TYPE="text/javascript">
+  Calendar.setup({
+    inputField: "beginning_text",
+    ifFormat:   "%m/%d/%Y",
+    button:     "beginning_button",
+    align:      "BR"
+  });
+</SCRIPT>
+        </TR>
+        <TR>
+          <TD ALIGN="right">To: </TD>
+          <TD><INPUT TYPE="text" NAME="ending" ID="ending_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="ending_button" STYLE="cursor: pointer" TITLE="Select date"><BR><I>m/d/y</I></TD>
+<SCRIPT TYPE="text/javascript">
+  Calendar.setup({
+    inputField: "ending_text",
+    ifFormat:   "%m/%d/%Y",
+    button:     "ending_button",
+    align:      "BR"
+  });
+</SCRIPT>
+        </TR>
+<% my %agent_search = dbdef->table('agent')->column('disabled')
+                        ? ( 'disabled' => '' ) : ();
+   my @agents = qsearch( 'agent', \%agent_search );
+   if ( scalar(@agents) == 1 ) {
+%>
+     <INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agents[0]->agentnum %>">
+<% } else { %>
+
+        <TR>
+          <TD ALIGN="right">Agent: </TD>
+          <TD><SELECT NAME="agentnum"><OPTION VALUE="">(all)
+          <% foreach my $agent ( sort { $a->agent cmp $b->agent; } @agents) { %>
+            <OPTION VALUE="<%= $agent->agentnum %>"><%= $agent->agent %>
+          <% } %>
+          </TD>
+        </TR>
+<% } %>
+      </TABLE>
+      <BR><INPUT TYPE="submit" VALUE="Get Report">
+
+    </FORM>
+
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html
new file mode 100644 (file)
index 0000000..fbedcaa
--- /dev/null
@@ -0,0 +1,59 @@
+<%
+
+  my %opt = @_;
+  unless (exists($opt{'count_query'}) && length($opt{'count_query'})) {
+    ( $opt{'count_query'} = $opt{'query'} ) =~
+      s/^\s*SELECT\s*(.*)\s+FROM\s/SELECT COUNT(*) FROM /i;
+  }
+
+  my $conf = new FS::Conf;
+  my $maxrecords = $conf->config('maxsearchrecordsperpage');
+
+  my $limit = $maxrecords ? "LIMIT $maxrecords" : '';
+
+  my $offset = $cgi->param('offset') || 0;
+  $limit .= " OFFSET $offset" if $offset;
+
+  my $count_sth = dbh->prepare($opt{'count_query'})
+    or die "Error preparing $opt{'count_query'}: ". dbh->errstr;
+  $count_sth->execute
+    or die "Error executing $opt{'count_query'}: ". $count_sth->errstr;
+  my $total = $count_sth->fetchrow_arrayref->[0];
+
+  my $sth = dbh->prepare("$opt{'query'} $limit")
+    or die "Error preparing $opt{'query'}: ". dbh->errstr;
+  $sth->execute
+    or die "Error executing $opt{'query'}: ". $sth->errstr;
+
+  #can get # of rows without fetching them all?
+  my $rows = $sth->fetchall_arrayref;
+
+%>
+<!-- mason kludge -->
+<% my $pager = include ( '/elements/pager.html',
+                           'offset'     => $offset,
+                           'num_rows'   => scalar(@$rows),
+                           'total'      => $total,
+                           'maxrecords' => $maxrecords,
+                       );
+%>
+
+<%= $total %> total <%= $opt{'name'} %><BR><BR><%= $pager %>
+<%= include( '/elements/table.html' ) %>
+  <TR>
+  <% foreach ( @{$sth->{NAME}} ) { %>
+       <TH><%= $_ %></TH>
+  <% } %>
+  </TR>
+  <% foreach my $row ( @$rows ) { %>
+       <TR>
+       <% foreach ( @$row ) { %>
+            <TD><%= $_ %></TD>
+       <% } %>
+       </TR>
+  <% } %>
+
+</TABLE>
+<%= $pager %>
+</BODY>
+</HTML>
index 8653dcc..595a7b1 100755 (executable)
@@ -1,23 +1,43 @@
 <HTML>
   <HEAD>
     <TITLE>Credit Card Receipt Report Criteria</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-      <H1>Credit Card Receipt Report Criteria</H1>
-    </CENTER>
-    <HR>
+    <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <H1>Credit Card Receipt Report Criteria</H1>
     <FORM ACTION="report_cc.cgi" METHOD="post">
       Return <B>credit card receipt report</B> for period: 
-      from <INPUT TYPE="text" NAME="beginning"> <i>m/d/y</i>
-      to <INPUT TYPE="text" NAME="ending"> <i>m/d/y</i>
+    <TABLE>
+      <TR>
+        <TD ALIGN="right">From: </TD>
+        <TD><INPUT TYPE="text" NAME="beginning" ID="beginning_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="beginning_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+  Calendar.setup({
+    inputField: "beginning_text",
+    ifFormat:   "%m/%d/%Y",
+    button:     "beginning_button",
+    align:      "BR"
+  });
+</SCRIPT>
+      </TR>
+        <TD ALIGN="right">To: </TD>
+        <TD><INPUT TYPE="text" NAME="ending" ID="ending_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="ending_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+  Calendar.setup({
+    inputField: "ending_text",
+    ifFormat:   "%m/%d/%Y",
+    button:     "ending_button",
+    align:      "BR"
+  });
+</SCRIPT>
+      </TR>
+    </TABLE>
 
-      <P><INPUT TYPE="submit" VALUE="Get Report">
+      <BR><INPUT TYPE="submit" VALUE="Get Report">
 
     </FORM>
-
-  <HR>
-
   </BODY>
 </HTML>
 
index df9b958..11cb32e 100755 (executable)
@@ -1,23 +1,43 @@
 <HTML>
   <HEAD>
     <TITLE>In House Credit Report Criteria</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-      <H1>In House Credit Report Criteria</H1>
-    </CENTER>
-    <HR>
+    <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <H1>In House Credit Report Criteria</H1>
     <FORM ACTION="report_credit.cgi" METHOD="post">
       Return <B>in house credit report</B> for period: 
-      from <INPUT TYPE="text" NAME="beginning"> <i>m/d/y</i>
-      to <INPUT TYPE="text" NAME="ending"> <i>m/d/y</i>
+    <TABLE>
+      <TR>
+        <TD ALIGN="right">From: </TD>
+        <TD><INPUT TYPE="text" NAME="beginning" ID="beginning_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="beginning_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+  Calendar.setup({
+    inputField: "beginning_text",
+    ifFormat:   "%m/%d/%Y",
+    button:     "beginning_button",
+    align:      "BR"
+  });
+</SCRIPT>
+      </TR>
+        <TD ALIGN="right">To: </TD>
+        <TD><INPUT TYPE="text" NAME="ending" ID="ending_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="ending_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+  Calendar.setup({
+    inputField: "ending_text",
+    ifFormat:   "%m/%d/%Y",
+    button:     "ending_button",
+    align:      "BR"
+  });
+</SCRIPT>
+      </TR>
+    </TABLE>
 
-      <P><INPUT TYPE="submit" VALUE="Get Report">
+    <BR><INPUT TYPE="submit" VALUE="Get Report">
 
     </FORM>
-
-  <HR>
-
   </BODY>
 </HTML>
 
index 93053e1..1b30685 100644 (file)
@@ -1,24 +1,54 @@
 <HTML>
   <HEAD>
     <TITLE>Payment report criteria</TITLE>
+    <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
   </HEAD>
-  <BODY>
-    <CENTER>
-      <H1>Payment report criteria</H1>
-    </CENTER>
-    <HR>
+  <BODY BGCOLOR="#e8e8e8">
+    <H1>Payment report criteria</H1>
     <FORM ACTION="cust_pay.cgi" METHOD="post">
     <INPUT TYPE="hidden" NAME="magic" VALUE="_date">
-      Return <SELECT NAME="payby">
-        <OPTION VALUE="">all</OPTION>
-        <OPTION VALUE="CARD">credit card</OPTION>
-        <OPTION VALUE="CHEK">electronic check (ACH)</OPTION>
-        <OPTION VALUE="BILL">check/cash</OPTION>
-      </SELECT> payments for period<BR>
-      from <INPUT TYPE="text" NAME="beginning"> <i>m/d/y</i>
-      to <INPUT TYPE="text" NAME="ending"> <i>m/d/y</i>
-      <P><INPUT TYPE="submit" VALUE="Get Report">
+    <TABLE>
+      <TR>
+        <TD ALIGN="right">Payments of type: </TD>
+        <TD><SELECT NAME="payby">
+              <OPTION VALUE="">all</OPTION>
+              <OPTION VALUE="CARD">credit card (all)</OPTION>
+              <OPTION VALUE="CARD-VisaMC">credit card (Visa/MasterCard)</OPTION>
+              <OPTION VALUE="CARD-Amex">credit card (American Express)</OPTION>
+              <OPTION VALUE="CARD-Discover">credit card (Discover)</OPTION>
+              <OPTION VALUE="CHEK">electronic check / ACH</OPTION>
+              <OPTION VALUE="BILL">check / cash</OPTION>
+            </SELECT>
+        </TD>
+      </TR>
+      <TR>
+        <TD ALIGN="right">From: </TD>
+        <TD><INPUT TYPE="text" NAME="beginning" ID="beginning_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="beginning_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+  Calendar.setup({
+    inputField: "beginning_text",
+    ifFormat:   "%m/%d/%Y",
+    button:     "beginning_button",
+    align:      "BR"
+  });
+</SCRIPT>
+      </TR>
+        <TD ALIGN="right">To: </TD>
+        <TD><INPUT TYPE="text" NAME="ending" ID="ending_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="ending_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+  Calendar.setup({
+    inputField: "ending_text",
+    ifFormat:   "%m/%d/%Y",
+    button:     "ending_button",
+    align:      "BR"
+  });
+</SCRIPT>
+      </TR>
+    </TABLE>
+    <BR><INPUT TYPE="submit" VALUE="Get Report">
     </FORM>
-  <HR>
   </BODY>
 </HTML>
diff --git a/httemplate/search/report_prepaid_income.cgi b/httemplate/search/report_prepaid_income.cgi
new file mode 100644 (file)
index 0000000..eb8bbb5
--- /dev/null
@@ -0,0 +1,75 @@
+<!-- mason kludge -->
+<%
+
+  #doesn't yet deal with daily/weekly packages
+
+  #needs to be re-written in sql for efficiency
+
+  my $now = $cgi->param('date') && str2time($cgi->param('date')) || time;
+  $now =~ /^(\d+)$/ or die "unparsable date?";
+  $now = $1;
+
+  my %prepaid;
+
+  my @cust_bill_pkg =
+    grep { $_->cust_pkg && $_->cust_pkg->part_pkg->freq !~ /^([01]|\d+[dw])$/ }
+      qsearch( 'cust_bill_pkg', {
+                                  'recur' => { op=>'!=', value=>0 },
+                                  'edate' => { op=>'>', value=>$now },
+                                }, );
+
+  foreach my $cust_bill_pkg ( @cust_bill_pkg ) {
+
+    #conceptual false laziness w/texas tax exempt_amount stuff in
+    #FS::cust_main::bill
+
+    my $freq = $cust_bill_pkg->cust_pkg->part_pkg->freq;
+    my $per_month = sprintf("%.2f", $cust_bill_pkg->recur / $freq);
+
+    my($mon, $year) = (localtime($cust_bill_pkg->sdate) )[4,5];
+    $mon+=2; $year+=1900;
+
+    foreach my $which_month ( 2 .. $freq ) {
+      until ( $mon < 13 ) { $mon -= 12; $year++; }
+      $prepaid{"$year-$mon"} += $per_month;
+      $mon++;
+    }
+
+  }
+
+  my @mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
+
+%>
+
+<%= header( 'Prepaid Income (Unearned Revenue) Report',
+            menubar( 'Main Menu'=>$p, ) )               %>
+<%= table() %>
+<%
+
+  my $total = 0;
+
+  my ($now_mon, $now_year) = (localtime($now))[4,5];
+  $now_mon+=2; $now_year+=1900;
+  until ( $now_mon < 13 ) { $now_mon -= 12; $now_year++; }
+
+  my $subseq = 0;
+  for my $year ( $now_year .. 2037 ) {
+    for my $mon ( ( $subseq++ ? 1 : $now_mon ) .. 12 ) {
+      if ( $prepaid{"$year-$mon"} ) {
+        $total += $prepaid{"$year-$mon"};
+        %> <TR><TD ALIGN="right"><%= $mon[$mon-1]. ' '. $year %></TD>
+               <TD ALIGN="right">
+                 <%= sprintf("%.2f", $prepaid{"$year-$mon"} ) %>
+               </TD>
+           </TR>
+        <%
+      }
+    }
+
+  }
+
+%>
+<TR><TH>Total</TH><TD ALIGN="right"><%= sprintf("%.2f", $total) %></TD></TR>
+</TABLE>
+</BODY>
+</HTML>
diff --git a/httemplate/search/report_prepaid_income.html b/httemplate/search/report_prepaid_income.html
new file mode 100644 (file)
index 0000000..b85a481
--- /dev/null
@@ -0,0 +1,17 @@
+<HTML>
+  <HEAD>
+    <TITLE>Prepaid Income (Unearned Revenue) Report</TITLE>
+    <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <H1>Prepaid Income (Unearned Revenue) Report</H1>
+    <FORM ACTION="report_prepaid_income.cgi" METHOD="post">
+    Prepaid income (unearned revenue) as of <INPUT TYPE="text" NAME="date" VALUE="now">
+    <INPUT TYPE="submit" VALUE="Generate report">
+  </BODY>
+</HTML>
+    <TABLE>
+
index fdd3779..ad353a1 100755 (executable)
 <!-- mason kludge -->
 <%
 
-my $user = getotaker;
+  my $charged = <<END;
+  sum( charged
+       - coalesce(
+           ( select sum(amount) from cust_bill_pay
+             where cust_bill.invnum = cust_bill_pay.invnum )
+           ,0
+         )
+       - coalesce(
+           ( select sum(amount) from cust_credit_bill
+             where cust_bill.invnum = cust_credit_bill.invnum )
+           ,0
+         )
 
-print header('Current Receivables Report Results');
+     )
+END
 
-open (REPORT, "freeside-receivables-report -v $user |");
+  my $owed_cols = <<END;
+       coalesce(
+         ( select $charged from cust_bill
+           where cust_bill._date > extract(epoch from now())-2592000
+             and cust_main.custnum = cust_bill.custnum
+         )
+         ,0
+       ) as owed_0_30,
 
-print '<PRE>';
-while(<REPORT>) {
-  print $_;
-}
-print '</PRE>';
+       coalesce(
+         ( select $charged from cust_bill
+           where cust_bill._date >  extract(epoch from now())-5184000
+             and cust_bill._date <= extract(epoch from now())-2592000
+             and cust_main.custnum = cust_bill.custnum
+         )
+         ,0
+       ) as owed_30_60,
 
-print '</BODY></HTML>';
+       coalesce(
+         ( select $charged from cust_bill
+           where cust_bill._date >  extract(epoch from now())-7776000
+             and cust_bill._date <= extract(epoch from now())-5184000
+             and cust_main.custnum = cust_bill.custnum
+         )
+         ,0
+       ) as owed_60_90,
 
-%>
+       coalesce(
+         ( select $charged from cust_bill
+           where cust_bill._date <= extract(epoch from now())-7776000
+             and cust_main.custnum = cust_bill.custnum
+         )
+         ,0
+       ) as owed_90_plus,
+
+       coalesce(
+         ( select $charged from cust_bill
+           where cust_main.custnum = cust_bill.custnum
+         )
+         ,0
+       ) as owed_total
+END
+
+  my $recurring = <<END;
+        0 < ( select freq from part_pkg
+                where cust_pkg.pkgpart = part_pkg.pkgpart )
+END
+
+  my $packages_cols = <<END;
+
+       ( select count(*) from cust_pkg
+           where cust_main.custnum = cust_pkg.custnum
+             and $recurring
+             and ( cancel = 0 or cancel is null )
+       ) as uncancelled_pkgs,
+
+       ( select count(*) from cust_pkg
+           where cust_main.custnum = cust_pkg.custnum
+             and $recurring
+             and ( cancel = 0 or cancel is null )
+             and ( susp = 0 or susp is null )
+       ) as active_pkgs
+
+END
+
+  my $sql = <<END;
 
+select *, $owed_cols, $packages_cols from cust_main
+where 0 <
+  coalesce(
+           ( select $charged from cust_bill
+             where cust_main.custnum = cust_bill.custnum
+           )
+           ,0
+         )
+
+order by lower(company), lower(last)
+
+END
+
+  my $total_sql = "select $owed_cols";
+
+  my $sth = dbh->prepare($sql) or die dbh->errstr;
+  $sth->execute or die $sth->errstr;
+
+  my $total_sth = dbh->prepare($total_sql) or die dbh->errstr;
+  $total_sth->execute or die $total_sth->errstr;
+
+%>
+<%= header('Accounts Receivable Aging Summary', menubar( 'Main Menu'=>$p, ) ) %>
+<%= table() %>
+  <TR>
+    <TH>Customer</TH>
+    <TH>Status</TH>
+    <TH>0-30</TH>
+    <TH>30-60</TH>
+    <TH>60-90</TH>
+    <TH>90+</TH>
+    <TH>Total</TH>
+  </TR>
+<% while ( my $row = $sth->fetchrow_hashref() ) {
+     my $status = 'Cancelled';
+     my $statuscol = 'FF0000';
+     if ( $row->{uncancelled_pkgs} ) {
+       $status = 'Suspended';
+       $statuscol = 'FF9900';
+       if ( $row->{active_pkgs} ) {
+         $status = 'Active';
+         $statuscol = '00CC00';
+       }
+     }
+%>
+  <TR>
+    <TD><A HREF="<%= $p %>view/cust_main.cgi?<%= $row->{'custnum'} %>"><%= $row->{'custnum'} %>:
+        <%= $row->{'company'} ? $row->{'company'}. ' (' : '' %><%= $row->{'last'}. ', '. $row->{'first'} %><%= $row->{'company'} ? ')' : '' %></A>
+    </TD>
+    <TD><B><FONT SIZE=-1 COLOR="#<%= $statuscol %>"><%= $status %></FONT></B></TD>
+    <TD ALIGN="right">$<%= sprintf("%.2f", $row->{'owed_0_30'} ) %></TD>
+    <TD ALIGN="right">$<%= sprintf("%.2f", $row->{'owed_30_60'} ) %></TD>
+    <TD ALIGN="right">$<%= sprintf("%.2f", $row->{'owed_60_90'} ) %></TD>
+    <TD ALIGN="right">$<%= sprintf("%.2f", $row->{'owed_90_plus'} ) %></TD>
+    <TD ALIGN="right"><B>$<%= sprintf("%.2f", $row->{'owed_total'} ) %></B></TD>
+  </TR>
+<% } %>
+<% my $row = $total_sth->fetchrow_hashref(); %>
+  <TR>
+    <TD COLSPAN=6>&nbsp;</TD>
+  </TR>
+  <TR>
+    <TD COLSPAN=2><I>Total</I></TD>
+    <TD ALIGN="right"><I>$<%= sprintf("%.2f", $row->{'owed_0_30'} ) %></TD>
+    <TD ALIGN="right"><I>$<%= sprintf("%.2f", $row->{'owed_30_60'} ) %></TD>
+    <TD ALIGN="right"><I>$<%= sprintf("%.2f", $row->{'owed_60_90'} ) %></TD>
+    <TD ALIGN="right"><I>$<%= sprintf("%.2f", $row->{'owed_90_plus'} ) %></TD>
+    <TD ALIGN="right"><I><B>$<%= sprintf("%.2f", $row->{'owed_total'} ) %></B></I></TD>
+  </TR>
+</TABLE>
+</BODY>
+</HTML>
index 7bf681b..7bc35e3 100755 (executable)
@@ -1,23 +1,43 @@
 <HTML>
   <HEAD>
     <TITLE>Tax Report Criteria</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-      <H1>Tax Report Criteria</H1>
-    </CENTER>
-    <HR>
+    <LINK REL="stylesheet" TYPE="text/css" HREF="../elements/calendar-win2k-2.css" TITLE="win2k-2">
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar_stripped.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
+    <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <H1>Tax Report Criteria</H1>
     <FORM ACTION="report_tax.cgi" METHOD="post">
-      Return <B>tax report</B> for period: 
-      from <INPUT TYPE="text" NAME="beginning"> <i>m/d/y</i>
-      to <INPUT TYPE="text" NAME="ending"> <i>m/d/y</i>
+      Return <B>tax report</B> for period:
+    <TABLE>
+      <TR>
+        <TD ALIGN="right">From: </TD>
+        <TD><INPUT TYPE="text" NAME="beginning" ID="beginning_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="beginning_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+  Calendar.setup({
+    inputField: "beginning_text",
+    ifFormat:   "%m/%d/%Y",
+    button:     "beginning_button",
+    align:      "BR"
+  });
+</SCRIPT>
+      </TR>
+        <TD ALIGN="right">To: </TD>
+        <TD><INPUT TYPE="text" NAME="ending" ID="ending_text" VALUE="" SIZE=11 MAXLENGTH=10> <IMG SRC="../images/calendar.png" ID="ending_button" STYLE="cursor: pointer" TITLE="Select date"><BR><i>m/d/y</i></TD>
+<SCRIPT TYPE="text/javascript">
+  Calendar.setup({
+    inputField: "ending_text",
+    ifFormat:   "%m/%d/%Y",
+    button:     "ending_button",
+    align:      "BR"
+  });
+</SCRIPT>
+      </TR>
+    </TABLE>
 
-      <P><INPUT TYPE="submit" VALUE="Get Report">
+      <BR><INPUT TYPE="submit" VALUE="Get Report">
 
     </FORM>
-
-  <HR>
-
   </BODY>
 </HTML>
 
diff --git a/httemplate/search/sql.cgi b/httemplate/search/sql.cgi
deleted file mode 100755 (executable)
index b83ef03..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-<%
-
-my $conf = new FS::Conf;
-my $maxrecords = $conf->config('maxsearchrecordsperpage');
-
-my $limit = '';
-$limit .= "LIMIT $maxrecords" if $maxrecords;
-
-my $offset = $cgi->param('offset') || 0;
-$limit .= " OFFSET $offset" if $offset;
-
-my $total;
-
-my $sql = $cgi->param('sql');
-$sql =~ s/^\s*SELECT//i;
-
-my $count_sql = $sql;
-$count_sql =~ s/^(.*)\s+FROM\s/COUNT(*) FROM /i;
-
-my $sth = dbh->prepare("SELECT $count_sql")
-  or eidiot dbh->errstr. " doing $count_sql\n";
-$sth->execute or eidiot "Error executing \"$count_sql\": ". $sth->errstr;
-
-$total = $sth->fetchrow_arrayref->[0];
-
-my $sth = dbh->prepare("SELECT $sql $limit")
-  or eidiot dbh->errstr. " doing $sql\n";
-$sth->execute or eidiot "Error executing \"$sql\": ". $sth->errstr;
-my $rows = $sth->fetchall_arrayref;
-
-%>
-<!-- mason kludge -->
-<%
-
-  #begin pager
-  my $pager = '';
-  if ( $total != scalar(@$rows) && $maxrecords ) {
-    unless ( $offset == 0 ) {
-      $cgi->param('offset', $offset - $maxrecords);
-      $pager .= '<A HREF="'. $cgi->self_url.
-                '"><B><FONT SIZE="+1">Previous</FONT></B></A> ';
-    }
-    my $poff;
-    my $page;
-    for ( $poff = 0; $poff < $total; $poff += $maxrecords ) {
-      $page++;
-      if ( $offset == $poff ) {
-        $pager .= qq!<FONT SIZE="+2">$page</FONT> !;
-      } else {
-        $cgi->param('offset', $poff);
-        $pager .= qq!<A HREF="!. $cgi->self_url. qq!">$page</A> !;
-      }
-    }
-    unless ( $offset + $maxrecords > $total ) {
-      $cgi->param('offset', $offset + $maxrecords);
-      $pager .= '<A HREF="'. $cgi->self_url.
-                '"><B><FONT SIZE="+1">Next</FONT></B></A> ';
-    }
-  }
-  #end pager
-
-  print header('Query Results', menubar('Main Menu'=>$p) ).
-        "$total total rows<BR><BR>$pager". table().
-        "<TR>";
-  print "<TH>$_</TH>" foreach @{$sth->{NAME}};
-  print "</TR>";
-
-  foreach $row ( @$rows ) {
-    print "<TR>";
-    print "<TD>$_</TD>" foreach @$row;
-    print "</TR>";
-  }
-
-  print "</TABLE>$pager</BODY></HTML>";
-
-%>
diff --git a/httemplate/search/sql.html b/httemplate/search/sql.html
new file mode 100644 (file)
index 0000000..7d7fc08
--- /dev/null
@@ -0,0 +1,12 @@
+<%= include( '/elements/header.html', 'Query Results',
+               include( '/elements/menubar.html', 'Main Menu' => $p )
+    )
+%>
+
+<%= include( 'elements/search.html',
+               'name'  => 'rows',
+               'query' => 'SELECT '. ( $cgi->param('sql')
+                                       || eidiot('Empty query') ),
+    )
+%>
+
index e43f4f7..1e4a03d 100755 (executable)
@@ -46,14 +46,31 @@ if ( $query eq 'svcnum' ) {
   $orderby = "ORDER BY ${tblname}username";
 } elsif ( $query eq 'uid' ) {
   $sortby=\*uid_sort;
-  $orderby = ( $unlinked ? 'AND' : 'WHERE' ).
+  $orderby = ( $unlinked ? ' AND' : ' WHERE' ).
              " ${tblname}uid IS NOT NULL ORDER BY ${tblname}uid";
+} elsif ( $cgi->param('popnum') =~ /^(\d+)$/ ) {
+  $unlinked .= ( $unlinked ? 'AND' : 'WHERE' ).
+               " popnum = $1";
+  $sortby=\*username_sort;
+  $orderby = "ORDER BY ${tblname}username";
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+  $unlinked .= ( $unlinked ? ' AND' : ' WHERE' ).
+               " $1 = ( SELECT svcpart FROM cust_svc ".
+               "        WHERE cust_svc.svcnum = svc_acct.svcnum ) ";
+  $sortby=\*uid_sort;
+  #$sortby=\*svcnum_sort;
 } else {
   $sortby=\*uid_sort;
   @svc_acct = @{&usernamesearch};
 }
 
-if ( $query eq 'svcnum' || $query eq 'username' || $query eq 'uid' ) {
+
+if (    $query eq 'svcnum'
+     || $query eq 'username'
+     || $query eq 'uid'
+     || $cgi->param('popnum') =~ /^(\d+)$/
+     || $cgi->param('svcpart') =~ /^(\d+)$/
+   ) {
 
   my $statement = "SELECT COUNT(*) FROM svc_acct $unlinked";
   my $sth = dbh->prepare($statement)
index c0acf11..948b1d9 100755 (executable)
@@ -23,6 +23,13 @@ if ( $query eq 'svcnum' ) {
       'svcnum' => $_->svcnum,
       'pkgnum' => '',
     }), qsearch('svc_domain',{});
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+  @svc_domain =
+    qsearch( 'svc_domain', {}, '',
+               " WHERE $1 = ( SELECT svcpart FROM cust_svc ".
+               "              WHERE cust_svc.svcnum = svc_domain.svcnum ) "
+    );
+  $sortby=\*svcnum_sort;
 } else {
   $cgi->param('domain') =~ /^([\w\-\.]+)$/; 
   my($domain)=$1;
diff --git a/httemplate/search/svc_forward.cgi b/httemplate/search/svc_forward.cgi
new file mode 100755 (executable)
index 0000000..10094bc
--- /dev/null
@@ -0,0 +1,79 @@
+<%
+
+my $conf = new FS::Conf;
+
+my($query)=$cgi->keywords;
+$query ||= ''; #to avoid use of unitialized value errors
+my(@svc_forward,$sortby);
+if ( $query eq 'svcnum' ) {
+  $sortby=\*svcnum_sort;
+  @svc_forward=qsearch('svc_forward',{});
+} else {
+  eidiot('unimplemented');
+}
+
+if ( scalar(@svc_forward) == 1 ) {
+  print $cgi->redirect(popurl(2). "view/svc_forward.cgi?". $svc_forward[0]->svcnum);
+  #exit;
+} elsif ( scalar(@svc_forward) == 0 ) {
+%>
+<!-- mason kludge -->
+<%
+  eidiot "No matching forwards found!\n";
+} else {
+%>
+<!-- mason kludge -->
+<%
+  my $total = scalar(@svc_forward);
+  print header("Mail forward Search Results",''), <<END;
+
+    $total matching mail forwards found
+    <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
+      <TR>
+        <TH>Service #<BR><FONT SIZE=-1>(click to view forward)</FONT></TH>
+        <TH>Mail to<BR><FONT SIZE=-1>(click to view account)</FONT></TH>
+        <TH>Forwards to<BR><FONT SIZE=-1>(click to view account)</FONT></TH>
+      </TR>
+END
+
+  foreach my $svc_forward (
+    sort $sortby (@svc_forward)
+  ) {
+    my $svcnum = $svc_forward->svcnum;
+
+    my $src = $svc_forward->src;
+    $src = "<I>(anything)</I>$src" if $src =~ /^@/;
+    if ( $svc_forward->srcsvc_acct ) {
+      $src = qq!<A HREF="${p}view/svc_acct.cgi?!. $svc_forward->srcsvc. '">'.
+             $svc_forward->srcsvc_acct->email. '</A>';
+    }
+
+    my $dst = $svc_forward->dst;
+    if ( $svc_forward->dstsvc_acct ) {
+      $dst = qq!<A HREF="${p}view/svc_acct.cgi?!. $svc_forward->dstsvc. '">'.
+             $svc_forward->dstsvc_acct->email. '</A>';
+    }
+
+    print <<END;
+      <TR>
+        <TD><A HREF="${p}view/svc_forward.cgi?$svcnum">$svcnum</A></TD>
+        <TD>$src</TD>
+        <TD>$dst</TD>
+      </TR>
+END
+
+  }
+  print <<END;
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+}
+
+sub svcnum_sort {
+  $a->getfield('svcnum') <=> $b->getfield('svcnum');
+}
+
+%>
diff --git a/httemplate/view/cust_bill-pdf.cgi b/httemplate/view/cust_bill-pdf.cgi
new file mode 100755 (executable)
index 0000000..7aa6910
--- /dev/null
@@ -0,0 +1,13 @@
+<%
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $invnum = $1;
+
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+die "Invoice #$invnum not found!" unless $cust_bill;
+
+http_header('Content-Type' => 'application/pdf' );
+%>
+<%= $cust_bill->print_pdf %>
diff --git a/httemplate/view/cust_bill-ps.cgi b/httemplate/view/cust_bill-ps.cgi
new file mode 100755 (executable)
index 0000000..03340a1
--- /dev/null
@@ -0,0 +1,13 @@
+<%
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $invnum = $1;
+
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+die "Invoice #$invnum not found!" unless $cust_bill;
+
+http_header('Content-Type' => 'application/postscript' );
+%>
+<%= $cust_bill->print_ps %>
index 53d7bc0..50ee8b3 100755 (executable)
@@ -20,7 +20,20 @@ print header('Invoice View', menubar(
 print qq!<A HREF="${p}edit/cust_pay.cgi?$invnum">Enter payments (check/cash) against this invoice</A> | !
   if $cust_bill->owed > 0;
 
-print qq!<A HREF="${p}misc/print-invoice.cgi?$invnum">Reprint this invoice</A>!.      '<BR><BR>';
+print qq!<A HREF="${p}misc/print-invoice.cgi?$invnum">Reprint this invoice</A>!;
+if ( grep { $_ ne 'POST' } $cust_bill->cust_main->invoicing_list ) {
+  print qq! | <A HREF="${p}misc/email-invoice.cgi?$invnum">!.
+        qq!Re-email this invoice</A>!;
+}
+
+print '<BR><BR>';
+
+my $conf = new FS::Conf;
+if ( $conf->exists('invoice_latex') ) {
+  print menubar(
+    'View typeset invoice' => "${p}view/cust_bill-pdf.cgi?$invnum",
+  ), '<BR><BR>';
+}
 
 #false laziness with search/cust_bill_event.cgi
 
index c36c9e2..ee5f869 100755 (executable)
@@ -76,7 +76,7 @@ print '<TD VALIGN="top">';
 
   print "Billing address", &ntable("#cccccc"), "<TR><TD>",
         &ntable("#cccccc",2),
-    '<TR><TD ALIGN="right">Contact name</TD>',
+    '<TR><TD ALIGN="right">Contact&nbsp;name</TD>',
       '<TD COLSPAN=3 BGCOLOR="#ffffff">',
       $cust_main->last, ', ', $cust_main->first,
       '</TD>';
@@ -105,8 +105,8 @@ print '</TR>',
           $cust_main->country,
           '</TD></TR>',
   ;
-  my $daytime_label = FS::Msgcat::_gettext('daytime') || 'Day Phone';
-  my $night_label = FS::Msgcat::_gettext('night') || 'Night Phone';
+  my $daytime_label = FS::Msgcat::_gettext('daytime') || 'Day&nbsp;Phone';
+  my $night_label = FS::Msgcat::_gettext('night') || 'Night&nbsp;Phone';
   print '<TR><TD ALIGN="right">'. $daytime_label.
           '</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
           $cust_main->daytime || '&nbsp', '</TD></TR>',
@@ -166,7 +166,7 @@ print '</TD>';
 print '<TD VALIGN="top">';
 
   print &ntable("#cccccc"), "<TR><TD>", &ntable("#cccccc",2),
-        '<TR><TD ALIGN="right">Customer number</TD><TD BGCOLOR="#ffffff">',
+        '<TR><TD ALIGN="right">Customer&nbsp;number</TD><TD BGCOLOR="#ffffff">',
         $custnum, '</TD></TR>',
   ;
 
@@ -184,13 +184,13 @@ print '<TD VALIGN="top">';
     my $referral = qsearchs('part_referral', {
       'refnum' => $cust_main->refnum
     } );
-    print '<TR><TD ALIGN="right">Advertising source</TD><TD BGCOLOR="#ffffff">',
+    print '<TR><TD ALIGN="right">Advertising&nbsp;source</TD><TD BGCOLOR="#ffffff">',
           $referral->refnum, ": ", $referral->referral, '</TD></TR>';
   }
   print '<TR><TD ALIGN="right">Order taker</TD><TD BGCOLOR="#ffffff">',
     $cust_main->otaker, '</TD></TR>';
 
-  print '<TR><TD ALIGN="right">Referring Customer</TD><TD BGCOLOR="#ffffff">';
+  print '<TR><TD ALIGN="right">Referring&nbsp;Customer</TD><TD BGCOLOR="#ffffff">';
   my $referring_cust_main = '';
   if ( $cust_main->referral_custnum
        && ( $referring_cust_main =
@@ -220,22 +220,22 @@ if ( $conf->config('payby-default') ne 'HIDE' ) {
   print "Billing information (",
        qq!<A HREF="!, popurl(2), qq!misc/bill.cgi?$custnum">!, "Bill now</A>)",
         &ntable("#cccccc"), "<TR><TD>", &ntable("#cccccc",2),
-        '<TR><TD ALIGN="right">Tax exempt</TD><TD BGCOLOR="#ffffff">',
+        '<TR><TD ALIGN="right">Tax&nbsp;exempt</TD><TD BGCOLOR="#ffffff">',
         $cust_main->tax ? 'yes' : 'no',
         '</TD></TR>',
-        '<TR><TD ALIGN="right">Postal invoices</TD><TD BGCOLOR="#ffffff">',
+        '<TR><TD ALIGN="right">Postal&nbsp;invoices</TD><TD BGCOLOR="#ffffff">',
         ( grep { $_ eq 'POST' } @invoicing_list ) ? 'yes' : 'no',
         '</TD></TR>',
-        '<TR><TD ALIGN="right">Email invoices</TD><TD BGCOLOR="#ffffff">',
+        '<TR><TD ALIGN="right">Email&nbsp;invoices</TD><TD BGCOLOR="#ffffff">',
         join(', ', grep { $_ ne 'POST' } @invoicing_list ) || 'no',
         '</TD></TR>',
-        '<TR><TD ALIGN="right">Billing type</TD><TD BGCOLOR="#ffffff">',
+        '<TR><TD ALIGN="right">Billing&nbsp;type</TD><TD BGCOLOR="#ffffff">',
   ;
 
   if ( $cust_main->payby eq 'CARD' || $cust_main->payby eq 'DCRD' ) {
     my $payinfo = $cust_main->payinfo;
     $payinfo = 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4));
-    print 'Credit card ',
+    print 'Credit&nbsp;card&nbsp;',
           ( $cust_main->payby eq 'CARD' ? '(automatic)' : '(on-demand)' ),
           '</TD></TR>',
           '<TR><TD ALIGN="right">Card number</TD><TD BGCOLOR="#ffffff">',
@@ -247,7 +247,7 @@ if ( $conf->config('payby-default') ne 'HIDE' ) {
     ;
   } elsif ( $cust_main->payby eq 'CHEK' || $cust_main->payby eq 'DCHK') {
     my( $account, $aba ) = split('@', $cust_main->payinfo );
-    print 'Electronic check',
+    print 'Electronic&nbsp;check&nbsp;',
           ( $cust_main->payby eq 'CHEK' ? '(automatic)' : '(on-demand)' ),
           '</TD></TR>',
           '<TR><TD ALIGN="right">Account number</TD><TD BGCOLOR="#ffffff">',
@@ -260,7 +260,7 @@ if ( $conf->config('payby-default') ne 'HIDE' ) {
   } elsif ( $cust_main->payby eq 'LECB' ) {
     $cust_main->payinfo =~ /^(\d{3})(\d{3})(\d{4})$/;
     my $payinfo = "$1-$2-$3";
-    print 'Phone bill billing</TD></TR>',
+    print 'Phone&nbsp;bill&nbsp;billing</TD></TR>',
           '<TR><TD ALIGN="right">Phone number</TD><TD BGCOLOR="#ffffff">',
           $payinfo, '</TD></TR>',
     ;
@@ -276,7 +276,7 @@ if ( $conf->config('payby-default') ne 'HIDE' ) {
     ;
   } elsif ( $cust_main->payby eq 'COMP' ) {
     print 'Complimentary</TD></TR>',
-          '<TR><TD ALIGN="right">Authorized by</TD><TD BGCOLOR="#ffffff">',
+          '<TR><TD ALIGN="right">Authorized&nbsp;by</TD><TD BGCOLOR="#ffffff">',
           $cust_main->payinfo, '</TD></TR>',
           '<TR><TD ALIGN="right">Expiration</TD><TD BGCOLOR="#ffffff">',
           $cust_main->paydate, '</TD></TR>',
@@ -370,7 +370,7 @@ print qq!<BR><A NAME="cust_pkg">Packages</A> !,
 
 #get package info
 
-my $packages = get_packages($cust_main);
+my $packages = get_packages($cust_main, $conf);
 
 if ( @$packages ) {
 %>
@@ -401,27 +401,30 @@ foreach my $pkg (sort pkgsort_pkgnum_cancel @$packages) {
     <%=$pkg->{pkg}%> - <%=$pkg->{comment}%> (&nbsp;<%=pkg_details_link($pkg)%>&nbsp;)<BR>
 <% unless ($pkg->{cancel}) { %>
     (&nbsp;<%=pkg_change_link($pkg)%>&nbsp;)
-    (&nbsp;<%=pkg_dates_link($pkg)%>&nbsp;|&nbsp;<%=pkg_customize_link($pkg)%>&nbsp;)
+    (&nbsp;<%=pkg_dates_link($pkg)%>&nbsp;|&nbsp;<%=pkg_customize_link($pkg,$custnum)%>&nbsp;)
 <% } %>
   </TD>
 <%
   #foreach (qw(setup last_bill next_bill susp expire cancel)) {
-  #  print qq!  <TD ROWSPAN=$rowspan>! . pkg_datestr($pkg,$_) . qq!</TD>\n!;
+  #  print qq!  <TD ROWSPAN=$rowspan>! . pkg_datestr($pkg,$_,$conf) . qq!</TD>\n!;
   #}
   print "<TD ROWSPAN=$rowspan>". &itable('');
 
-  #move
-  my %freq = (
-    1 => 'monthly',
-    2 => 'bi-monthly',
-    3 => 'quarterly',
-    6 => 'semi-annually',
-    12 => 'annually',
-    24 => 'bi-annually',
-    36 => 'tri-annually',
-  );
-
   sub freq {
+
+    #false laziness w/edit/part_pkg.cgi
+    my %freq = ( #move this
+      '1d' => 'daily',
+      '1w' => 'weekly',
+      '2w' => 'biweekly (every 2 weeks)',
+      '1'  => 'monthly',
+      '2'  => 'bimonthly (every 2 months)',
+      '3'  => 'quarterly (every 3 months)',
+      '6'  => 'semiannually (every 6 months)',
+      '12' => 'annually',
+      '24' => 'biannually (every 2 years)',
+    );
+
     my $freq = shift;
     exists $freq{$freq} ? $freq{$freq} : "every&nbsp;$freq&nbsp;months";
   }
@@ -431,17 +434,17 @@ foreach my $pkg (sort pkgsort_pkgnum_cancel @$packages) {
   if ( $pkg->{cancel} ) { #status: cancelled
 
     print '<TR><TD><FONT COLOR="#ff0000"><B>Cancelled&nbsp;</B></FONT></TD>'.
-          '<TD>'. pkg_datestr($pkg,'cancel'). '</TD></TR>';
+          '<TD>'. pkg_datestr($pkg,'cancel',$conf). '</TD></TR>';
     unless ( $pkg->{setup} ) {
       print '<TR><TD COLSPAN=2>Never billed</TD></TR>';
     } else {
       print "<TR><TD>Setup&nbsp;</TD><TD>".
-            pkg_datestr($pkg, 'setup'). '</TD></TR>';
+            pkg_datestr($pkg, 'setup',$conf). '</TD></TR>';
       print "<TR><TD>Last&nbsp;bill&nbsp;</TD><TD>".
-            pkg_datestr($pkg, 'last_bill'). '</TD></TR>'
+            pkg_datestr($pkg, 'last_bill',$conf). '</TD></TR>'
         if $pkg->{'last_bill'};
       print "<TR><TD>Suspended&nbsp;</TD><TD>".
-            pkg_datestr($pkg, 'susp'). '</TD></TR>'
+            pkg_datestr($pkg, 'susp',$conf). '</TD></TR>'
         if $pkg->{'susp'};
     }
 
@@ -449,19 +452,19 @@ foreach my $pkg (sort pkgsort_pkgnum_cancel @$packages) {
 
     if ( $pkg->{susp} ) { #status: suspended
       print '<TR><TD><FONT COLOR="#FF9900"><B>Suspended</B>&nbsp;</FONT></TD>'.
-            '<TD>'. pkg_datestr($pkg,'susp'). '</TD></TR>';
+            '<TD>'. pkg_datestr($pkg,'susp',$conf). '</TD></TR>';
       unless ( $pkg->{setup} ) {
         print '<TR><TD COLSPAN=2>Never billed</TD></TR>';
       } else {
         print "<TR><TD>Setup&nbsp;</TD><TD>". 
-              pkg_datestr($pkg, 'setup'). '</TD></TR>';
+              pkg_datestr($pkg, 'setup',$conf). '</TD></TR>';
       }
       print "<TR><TD>Last&nbsp;bill&nbsp;</TD><TD>".
-            pkg_datestr($pkg, 'last_bill'). '</TD></TR>'
+            pkg_datestr($pkg, 'last_bill',$conf). '</TD></TR>'
         if $pkg->{'last_bill'};
       # next bill ??
       print "<TR><TD>Expires&nbsp;</TD><TD>".
-            pkg_datestr($pkg, 'expire'). '</TD></TR>'
+            pkg_datestr($pkg, 'expire',$conf). '</TD></TR>'
         if $pkg->{'expire'};
       print '<TR><TD COLSPAN=2>(&nbsp;'. pkg_unsuspend_link($pkg).
             '&nbsp;|&nbsp;'. pkg_cancel_link($pkg). '&nbsp;)</TD></TR>';
@@ -484,24 +487,24 @@ foreach my $pkg (sort pkgsort_pkgnum_cancel @$packages) {
         unless ( $pkg->{freq} ) {
           print "<TR><TD COLSPAN=2>One-time&nbsp;charge</TD></TR>".
                 '<TR><TD>Billed&nbsp;</TD><TD>'.
-                pkg_datestr($pkg,'setup'). '</TD></TR>';
+                pkg_datestr($pkg,'setup',$conf). '</TD></TR>';
         } else {
           print '<TR><TD COLSPAN=2><FONT COLOR="#00CC00"><B>Active</B></FONT>'.
                 ',&nbsp;billed&nbsp;'. freq($pkg->{freq}). '</TD></TR>'.
                 '<TR><TD>Setup&nbsp;</TD><TD>'.
-                pkg_datestr($pkg, 'setup'). '</TD></TR>';
+                pkg_datestr($pkg, 'setup',$conf). '</TD></TR>';
         }
 
       }
 
       print "<TR><TD>Last&nbsp;bill&nbsp;</TD><TD>".
-            pkg_datestr($pkg, 'last_bill'). '</TD></TR>'
+            pkg_datestr($pkg, 'last_bill',$conf). '</TD></TR>'
         if $pkg->{'last_bill'};
       print "<TR><TD>Next&nbsp;bill&nbsp;</TD><TD>".
-            pkg_datestr($pkg, 'next_bill'). '</TD></TR>'
+            pkg_datestr($pkg, 'next_bill',$conf). '</TD></TR>'
         if $pkg->{'next_bill'};
       print "<TR><TD>Expires&nbsp;</TD><TD>".
-            pkg_datestr($pkg, 'expire'). '</TD></TR>'
+            pkg_datestr($pkg, 'expire',$conf). '</TD></TR>'
         if $pkg->{'expire'};
       if ( $pkg->{freq} ) {
         print '<TR><TD COLSPAN=2>(&nbsp;'. pkg_suspend_link($pkg).
@@ -551,6 +554,11 @@ function cust_pay_unapply_areyousure(href) {
  == true)
         window.location.href = href;
 }
+function cust_credit_areyousure(href) {
+    if (confirm("Are you sure you want to delete this credit?")
+ == true)
+        window.location.href = href;
+}
 </SCRIPT>
 END
 
@@ -625,9 +633,13 @@ if ( $conf->config('payby-default') ne 'HIDE' ) {
         $cust_credit->reason,
         time2str("%D", $cust_credit_bill->_date),
       );
+      my $delete =
+        $cust_credit->closed !~ /^Y/i && $conf->exists('deletecredits')
+          ? qq! (<A HREF="javascript:cust_credit_areyousure('${p}misc/delete-cust_credit.cgi?!. $cust_credit->crednum. qq!')">delete</A>)!
+          : '';
       push @history,
         "$date\tCredit #$crednum: $reason<BR>".
-        "(applied to invoice #$invnum on $app_date)\t\t\t$amount\t";
+        "(applied to invoice #$invnum on $app_date)$delete\t\t\t$amount\t";
     }
   }
   
@@ -656,12 +668,16 @@ if ( $conf->config('payby-default') ne 'HIDE' ) {
              qsearch('cust_credit',{'custnum'=>$custnum});
   foreach my $credit (@credits) {
     my($cref)=$credit->hashref;
+    my $delete =
+      $credit->closed !~ /^Y/i && $conf->exists('deletecredits')
+        ? qq! (<A HREF="javascript:cust_credit_areyousure('${p}misc/delete-cust_credit.cgi?!. $credit->crednum. qq!')">delete</A>)!
+        : '';
     push @history,
       $cref->{_date} . "\t" .
       qq!<A HREF="! . popurl(2). qq!edit/cust_credit_bill.cgi?!. $cref->{crednum} . qq!">!.
       '<b><font size="+1" color="#ff0000">Unapplied credit #' .
       $cref->{crednum} . "</font></b></A>: ".
-      $cref->{reason} . "\t\t\t" . $credit->credited . "\t";
+      $cref->{reason} . "$delete\t\t\t" . $credit->credited . "\t";
   }
   
   my(@refunds)=qsearch('cust_refund',{'custnum'=> $custnum } );
@@ -767,52 +783,79 @@ sub keyfield_numerically { (split(/\t/,$a))[0] <=> (split(/\t/,$b))[0]; }
 
 
 sub get_packages {
+  my $cust_main = shift or return undef;
+  my $conf = shift;
+  
+  my @packages = ();
+  
+  foreach my $cust_pkg (
+    $conf->exists('hidecancelledpackages')
+      ? $cust_main->ncancelled_pkgs
+      : $cust_main->all_pkgs
+  ) { 
+  
+    my $part_pkg = $cust_pkg->part_pkg;
+  
+    my %pkg = ();
+    $pkg{pkgnum} = $cust_pkg->pkgnum;
+    $pkg{pkg} = $part_pkg->pkg;
+    $pkg{pkgpart} = $part_pkg->pkgpart;
+    $pkg{comment} = $part_pkg->getfield('comment');
+    $pkg{freq} = $part_pkg->freq;
+    $pkg{setup} = $cust_pkg->getfield('setup');
+    $pkg{last_bill} = $cust_pkg->getfield('last_bill');
+    $pkg{next_bill} = $cust_pkg->getfield('bill');
+    $pkg{susp} = $cust_pkg->getfield('susp');
+    $pkg{expire} = $cust_pkg->getfield('expire');
+    $pkg{cancel} = $cust_pkg->getfield('cancel');
+  
+    my %svcparts = ();
 
-my $cust_main = shift or return undef;
-
-my @packages = ();
-
-foreach my $cust_pkg (($conf->exists('hidecancelledpackages') ? ($cust_main->ncancelled_pkgs)
-                                                              : ($cust_main->all_pkgs))) { 
-
-  my $part_pkg = $cust_pkg->part_pkg;
-
-  my %pkg = ();
-  $pkg{pkgnum} = $cust_pkg->pkgnum;
-  $pkg{pkg} = $part_pkg->pkg;
-  $pkg{pkgpart} = $part_pkg->pkgpart;
-  $pkg{comment} = $part_pkg->getfield('comment');
-  $pkg{freq} = $part_pkg->freq;
-  $pkg{setup} = $cust_pkg->getfield('setup');
-  $pkg{last_bill} = $cust_pkg->getfield('last_bill');
-  $pkg{next_bill} = $cust_pkg->getfield('bill');
-  $pkg{susp} = $cust_pkg->getfield('susp');
-  $pkg{expire} = $cust_pkg->getfield('expire');
-  $pkg{cancel} = $cust_pkg->getfield('cancel');
-
-  $pkg{svcparts} = []; 
-
-  foreach my $pkg_svc (qsearch('pkg_svc', { 'pkgpart' => $part_pkg->pkgpart })) {
-
-    next if ($pkg_svc->quantity == 0);
-
-    my $part_svc = qsearchs('part_svc', { 'svcpart' => $pkg_svc->svcpart });
-
-    my $svcpart = {};
-    $svcpart->{svcpart} = $part_svc->svcpart;
-    $svcpart->{svc} = $part_svc->svc;
-    $svcpart->{svcdb} = $part_svc->svcdb;
-    $svcpart->{quantity} = $pkg_svc->quantity;
-    $svcpart->{count} = 0;
+    foreach my $pkg_svc (
+      qsearch('pkg_svc', { 'pkgpart' => $part_pkg->pkgpart })
+    ) {
+  
+      next if ($pkg_svc->quantity == 0);
+  
+      my $part_svc = qsearchs('part_svc', { 'svcpart' => $pkg_svc->svcpart });
+  
+      my $svcpart = {};
+      $svcpart->{svcpart} = $part_svc->svcpart;
+      $svcpart->{svc} = $part_svc->svc;
+      $svcpart->{svcdb} = $part_svc->svcdb;
+      $svcpart->{quantity} = $pkg_svc->quantity;
+      $svcpart->{count} = 0;
+  
+      $svcpart->{services} = [];
 
-    $svcpart->{services} = [];
+      $svcparts{$svcpart->{svcpart}} = $svcpart;
 
-    foreach my $cust_svc (qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum,
-                                                'svcpart' => $part_svc->svcpart } )) {
+    }
 
-      my $svc = {};
-      $svc->{svcnum} = $cust_svc->svcnum;
-      $svc->{label} = ($cust_svc->label)[1];
+    foreach my $cust_svc (
+      qsearch( 'cust_svc', {
+                             'pkgnum' => $cust_pkg->pkgnum,
+                             #'svcpart' => $part_svc->svcpart,
+                           }
+      )
+    ) {
+
+      warn "svcnum ". $cust_svc->svcnum. " / svcpart ". $cust_svc->svcpart. "\n";
+      my $svc = {
+        'svcnum' => $cust_svc->svcnum,
+        'label'  => ($cust_svc->label)[1],
+      };
+
+      #false laziness with above, to catch extraneous services.  whole
+      #damn thing should be OO...
+      my $svcpart = ( $svcparts{$cust_svc->svcpart} ||= {
+        'svcpart'  => $cust_svc->svcpart,
+        'svc'      => $cust_svc->part_svc->svc,
+        'svcdb'    => $cust_svc->part_svc->svcdb,
+        'quantity' => 0,
+        'count'    => 0,
+        'services' => [],
+      } );
 
       push @{$svcpart->{services}}, $svc;
 
@@ -820,29 +863,27 @@ foreach my $cust_pkg (($conf->exists('hidecancelledpackages') ? ($cust_main->nca
 
     }
 
-    push @{$pkg{svcparts}}, $svcpart;
+    $pkg{svcparts} = [ values %svcparts ];
 
+    push @packages, \%pkg;
+  
   }
-
-  push @packages, \%pkg;
-
-}
-
-return \@packages;
+  
+  return \@packages;
 
 }
 
 sub svc_link {
 
- my ($svcpart, $svc) = (shift,shift) or return '';
- return qq!<A HREF="${p}view/$svcpart->{svcdb}.cgi?$svc->{svcnum}">$svcpart->{svc}</A>!;
 my ($svcpart, $svc) = (shift,shift) or return '';
 return qq!<A HREF="${p}view/$svcpart->{svcdb}.cgi?$svc->{svcnum}">$svcpart->{svc}</A>!;
 
 }
 
 sub svc_label_link {
 
- my ($svcpart, $svc) = (shift,shift) or return '';
- return qq!<A HREF="${p}view/$svcpart->{svcdb}.cgi?$svc->{svcnum}">$svc->{label}</A>!;
 my ($svcpart, $svc) = (shift,shift) or return '';
 return qq!<A HREF="${p}view/$svcpart->{svcdb}.cgi?$svc->{svcnum}">$svc->{label}</A>!;
 
 }
 
@@ -875,7 +916,7 @@ sub pkgsort_pkgnum_cancel {
 }
 
 sub pkg_datestr {
-  my($pkg, $field) = @_ or return '';
+  my($pkg, $field, $conf) = @_ or return '';
   return '&nbsp;' unless $pkg->{$field};
   my $format = $conf->exists('pkg_showtimes')
                ? '<B>%D</B>&nbsp;<FONT SIZE=-3>%l:%M:%S%P&nbsp;%z</FONT>'
@@ -916,6 +957,7 @@ sub pkg_dates_link {
 
 sub pkg_customize_link {
   my $pkg = shift or return '';
+  my $custnum = shift;
   return qq!<A HREF="${p}edit/part_pkg.cgi?keywords=$custnum;clone=$pkg->{pkgpart};pkgnum=$pkg->{pkgnum}">Customize</A>!;
 }
 
index 4e01db3..58591fc 100755 (executable)
@@ -57,7 +57,9 @@ function areyousure(href) {
 <%
 
 #if ( $cust_pkg && $cust_pkg->part_pkg->plan eq 'sqlradacct_hour' ) {
-if ( $part_svc->part_export('sqlradius') ) {
+if (    $part_svc->part_export('sqlradius')
+     || $part_svc->part_export('sqlradius_withdomain')
+) {
 
   my $last_bill;
   my %plandata;
@@ -74,9 +76,9 @@ if ( $part_svc->part_export('sqlradius') ) {
   }
 
   my $seconds = $svc_acct->seconds_since_sqlradacct( $last_bill, time );
-  my $h = int($seconds/3600);
-  my $m = int( ($seconds%3600) / 60 );
-  my $s = $seconds%60;
+  my $hour = int($seconds/3600);
+  my $min = int( ($seconds%3600) / 60 );
+  my $sec = $seconds%60;
 
   my $input = $svc_acct->attribute_since_sqlradacct(
     $last_bill, time, 'AcctInputOctets'
@@ -86,7 +88,7 @@ if ( $part_svc->part_export('sqlradius') ) {
   ) / 1048576;
 
   if ( $seconds ) {
-    print "Online <B>$h</B>h <B>$m</B>m <B>$s</B>s";
+    print "Online <B>$hour</B>h <B>$min</B>m <B>$sec</B>s";
   } else {
     print 'Has not logged on';
   }
index 677a4b0..a4ec756 100644 (file)
@@ -37,10 +37,9 @@ my (
      $svc_broadband->getfield('speed_up'),
      $svc_broadband->getfield('ip_addr')
    );
+%>
 
-
-
-print header('Broadband Service View', menubar(
+<%=header('Broadband Service View', menubar(
   ( ( $custnum )
     ? ( "View this package (#$pkgnum)" => "${p}view/cust_pkg.cgi?$pkgnum",
         "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
@@ -49,29 +48,96 @@ print header('Broadband Service View', menubar(
           "${p}misc/cancel-unaudited.cgi?$svcnum" )
   ),
   "Main menu" => $p,
-)).
-      qq!<A HREF="${p}edit/svc_broadband.cgi?$svcnum">Edit this information</A><BR>!.
-      ntable("#cccccc"). '<TR><TD>'. ntable("#cccccc",2).
-      qq!<TR><TD ALIGN="right">Service number</TD>!.
-        qq!<TD BGCOLOR="#ffffff">$svcnum</TD></TR>!.
-      qq!<TR><TD ALIGN="right">Router</TD>!.
-        qq!<TD BGCOLOR="#ffffff">$routernum: $routername</TD></TR>!.
-      qq!<TR><TD ALIGN="right">Download Speed</TD>!.
-        qq!<TD BGCOLOR="#ffffff">$speed_down</TD></TR>!.
-      qq!<TR><TD ALIGN="right">Upload Speed</TD>!.
-        qq!<TD BGCOLOR="#ffffff">$speed_up</TD></TR>!.
-      qq!<TR><TD ALIGN="right">IP Address</TD>!.
-        qq!<TD BGCOLOR="#ffffff">$ip_addr</TD></TR>!.
-      '</TD></TR><TR ROWSPAN="1"><TD></TD></TR>';
+))
+%>
 
+<A HREF="<%=${p}%>edit/svc_broadband.cgi?<%=$svcnum%>">Edit this information</A>
+<BR>
+<%=ntable("#cccccc")%>
+  <TR>
+    <TD>
+      <%=ntable("#cccccc",2)%>
+        <TR>
+          <TD ALIGN="right">Service number</TD>
+          <TD BGCOLOR="#ffffff"><%=$svcnum%></TD>
+        </TR>
+        <TR>
+          <TD ALIGN="right">Router</TD>
+          <TD BGCOLOR="#ffffff"><%=$routernum%>: <%=$routername%></TD>
+        </TR>
+        <TR>
+          <TD ALIGN="right">Download Speed</TD>
+          <TD BGCOLOR="#ffffff"><%=$speed_down%></TD>
+        </TR>
+        <TR>
+          <TD ALIGN="right">Upload Speed</TD>
+          <TD BGCOLOR="#ffffff"><%=$speed_up%></TD>
+        </TR>
+        <TR>
+          <TD ALIGN="right">IP Address</TD>
+          <TD BGCOLOR="#ffffff"><%=$ip_addr%></TD>
+        </TR>
+        <TR COLSPAN="2"><TD></TD></TR>
+
+<%
 foreach (sort { $a cmp $b } $svc_broadband->virtual_fields) {
-  print $svc_broadband->pvf($_)->widget('HTML', 'view', 
-      $svc_broadband->getfield($_)), "\n";
+  print $svc_broadband->pvf($_)->widget('HTML', 'view',
+                                        $svc_broadband->getfield($_)), "\n";
 }
 
-print '</TABLE>';
+%>
+      </TABLE>
+    </TD>
+  </TR>
+</TABLE>
 
-print '<BR>'. joblisting({'svcnum'=>$svcnum}, 1).
-      '</BODY></HTML>'
-;
+<BR>
+<%=ntable("#cccccc", 2)%>
+<%
+  my $sb_router = qsearchs('router', { svcnum => $svcnum });
+  if ($sb_router) {
+  %>
+  <B>Router associated: <%=$sb_router->routername%> </B>
+  <A HREF="<%=popurl(2)%>edit/router.cgi?<%=$sb_router->routernum%>">
+    (details)
+  </A>
+  <BR>
+  <% my @addr_block;
+     if (@addr_block = $sb_router->addr_block) {
+     %>
+  <B>Address space </B>
+  <A HREF="<%=popurl(2)%>browse/addr_block.cgi">
+    (edit)
+  </A>
+  <BR>
+  <%   print ntable("#cccccc", 1);
+       foreach (@addr_block) { %>
+    <TR>
+      <TD><%=$_->ip_gateway%>/<%=$_->ip_netmask%></TD>
+    </TR>
+    <% } %>
+  </TABLE>
+  <% } else { %>
+  <B>No address space allocated.</B>
+    <% } %>
+  <BR>
+  <%
+  } else {
 %>
+
+<FORM METHOD="GET" ACTION="<%=popurl(2)%>edit/router.cgi">
+  <INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$svcnum%>">
+Add router named 
+  <INPUT TYPE="text" NAME="routername" SIZE="32" VALUE="Broadband router (<%=$svcnum%>)">
+  <INPUT TYPE="submit" VALUE="Add router">
+</FORM>
+
+<%
+}
+%>
+
+<BR>
+<%=joblisting({'svcnum'=>$svcnum}, 1)%>
+  </BODY>
+</HTML>
+
diff --git a/httemplate/view/svc_external.cgi b/httemplate/view/svc_external.cgi
new file mode 100644 (file)
index 0000000..e5c977f
--- /dev/null
@@ -0,0 +1,52 @@
+<!-- mason kludge -->
+<%
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_external = qsearchs( 'svc_external', { 'svcnum' => $svcnum } )
+  or die "svc_external: Unknown svcnum $svcnum";
+
+#false laziness w/all svc_*.cgi
+my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $svcnum } );
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+  $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $pkgnum } );
+  $custnum = $cust_pkg->custnum;
+} else {
+  $cust_pkg = '';
+  $custnum = '';
+}
+#eofalse
+
+%>
+
+<%= header('External Service View', menubar(
+  ( ( $custnum )
+    ? ( "View this package (#$pkgnum)" => "${p}view/cust_pkg.cgi?$pkgnum",
+        "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+      )                                                                       
+    : ( "Cancel this (unaudited) external service" =>
+          "${p}misc/cancel-unaudited.cgi?$svcnum" )
+  ),
+  "Main menu" => $p,
+)) %>
+
+<A HREF="<%=$p%>edit/svc_external.cgi?<%=$svcnum%>">Edit this information</A><BR>
+<%= ntable("#cccccc") %><TR><TD><%= ntable("#cccccc",2) %>
+
+<TR><TD ALIGN="right">Service number</TD>
+  <TD BGCOLOR="#ffffff"><%= $svcnum %></TD></TR>
+<TR><TD ALIGN="right">External ID</TD>
+  <TD BGCOLOR="#ffffff"><%= $svc_external->id %></TD></TR>
+<TR><TD ALIGN="right">Title</TD>
+  <TD BGCOLOR="#ffffff"><%= $svc_external->title %></TD></TR>
+
+<% foreach (sort { $a cmp $b } $svc_external->virtual_fields) { %>
+  <%= $svc_external->pvf($_)->widget('HTML', 'view', $svc_external->getfield($_)) %>
+<% } %>
+
+</TABLE></TD></TR></TABLE>
+<BR><%= joblisting({'svcnum'=>$svcnum}, 1) %>
+</BODY></HTML>
index 5d619de..6509724 100755 (executable)
@@ -28,7 +28,7 @@ print header('Mail Forward View', menubar(
     ? ( "View this package (#$pkgnum)" => "${p}view/cust_pkg.cgi?$pkgnum",
         "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
       )
-    : ( "Cancel this (unaudited) account" =>
+    : ( "Cancel this (unaudited) mail forward" =>
           "${p}misc/cancel-unaudited.cgi?$svcnum" )
   ),
   "Main menu" => $p,
@@ -39,16 +39,25 @@ my($srcsvc,$dstsvc,$dst) = (
   $svc_forward->dstsvc,
   $svc_forward->dst,
 );
+my $src = $svc_forward->dbdef_table->column('src') ? $svc_forward->src : '';
+
 my $svc = $part_svc->svc;
-my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$srcsvc})
-  or die "Corrupted database: no svc_acct.svcnum matching srcsvc $srcsvc";
-my $source = $svc_acct->email;
+
+my $source;
+if ($srcsvc) {
+  my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$srcsvc})
+    or die "Corrupted database: no svc_acct.svcnum matching srcsvc $srcsvc";
+  $source = $svc_acct->email;
+} else {
+  $source = $src;
+}
+
 my $destination;
 if ($dstsvc) {
   my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$dstsvc})
     or die "Corrupted database: no svc_acct.svcnum matching dstsvc $dstsvc";
   $destination = $svc_acct->email;
-}else{
+} else {
   $destination = $dst;
 }
 
index 68cba8d..1ca7dff 100644 (file)
@@ -7,16 +7,8 @@ QUEUED_USER=%%%QUEUED_USER%%%
 
 FREESIDE_PATH="%%%FREESIDE_PATH%%%"
 
-PASSWD_USER=%%%PASSWD_USER%%%
-PASSWD_MACHINE=%%%PASSWD_MACHINE%%%
-
-SIGNUP_USER=%%%SIGNUP_USER%%%
-SIGNUP_MACHINE=%%%SIGNUP_MACHINE%%%
-SIGNUP_AGENTNUM=%%%SIGNUP_AGENTNUM%%%
-SIGNUP_REFNUM=%%%SIGNUP_REFNUM%%%
-
 SELFSERVICE_USER=%%%SELFSERVICE_USER%%%
-SELFSERVICE_MACHINE=%%%SELFSERVICE_MACHINE%%%
+SELFSERVICE_MACHINES="%%%SELFSERVICE_MACHINES%%%"
 
 case "$1" in
   start)
@@ -25,17 +17,11 @@ case "$1" in
         freeside-queued $QUEUED_USER
         echo "done."
 
-        echo -n "Starting fs_passwd_server: "
-        su freeside -c "$FREESIDE_PATH/fs_passwd/fs_passwd_server $PASSWD_USER $PASSWD_MACHINE" &
-        echo "done."
-
-        echo -n "Starting fs_signup_server: "
-        su freeside -c "$FREESIDE_PATH/fs_signup/fs_signup_server $SIGNUP_USER $SIGNUP_MACHINE $SIGNUP_AGENTNUM $SIGNUP_REFNUM" &
-        echo "done."
-
-        echo -n "Starting freeside-selfservice-server: "
-        freeside-selfservice-server $SELFSERVICE_USER $SELFSERVICE_MACHINE
-        echo "done."
+        for MACHINE in $SELFSERVICE_MACHINES; do
+          echo -n "Starting freeside-selfservice-server to $MACHINE: "
+          freeside-selfservice-server $SELFSERVICE_USER $MACHINE
+          echo "done."
+        done
 
         ;;
   stop)
@@ -44,17 +30,19 @@ case "$1" in
         kill `cat /var/run/freeside-queued.pid`
         echo "done."
 
-        echo -n "Stopping fs_passwd_server: "
-        killall fs_passwd_server
-        echo "done."
+        if [ -e /var/run/freeside-selfservice-server.$SELFSERVICE_USER.pid ]
+        then
+          echo -n "Stopping (old) freeside-selfservice-server: "
+          kill `cat /var/run/freeside-selfservice-server.$SELFSERVICE_USER.pid`
+          rm /var/run/freeside-selfservice-server.$SELFSERVICE_USER.pid
+        fi
 
-        echo -n "Stopping fs_signup_server: "
-        killall fs_signup_server
-        echo "done."
+        for MACHINE in $SELFSERVICE_MACHINES; do
+          echo -n "Stopping freeside-selfservice-server to $MACHINE: "
+          kill `cat /var/run/freeside-selfservice-server.$SELFSERVICE_USER.$MACHINE.pid`
+          echo "done."
+        done
 
-        echo -n "Stopping freeside-selfservice-server: "
-        kill `cat /var/run/freeside-selfservice-server.pid`
-        echo "done."
         ;;
 
   restart)
diff --git a/install/debian/3.0/INSTALL b/install/debian/3.0/INSTALL
new file mode 100644 (file)
index 0000000..96991e8
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+echo "deb http://pouncequick.420.am/~ivan/freeside-woody/ ./" >>/etc/apt/sources.list
+
+apt-get update
+apt-get install screen zsh libapache-mod-ssl libapache-mod-perl rsync \
+        postgresql cvs fsh \
+        liburi-perl libhtml-tagset-perl libnet-perl liblocale-codes-perl \
+        libnet-whois-perl libwww-perl libbusiness-creditcard-perl \
+        libmailtools-perl libtimedate-perl libdate-manip-perl \
+        libfile-counterfile-perl libfreezethaw-perl libstring-approx-perl \
+        libtext-template-perl libdbi-perl libdbd-pg-perl \
+        libdbix-datasource-perl libdbix-dbschema-perl libnet-ssh-perl \
+        libstring-shellquote-perl libnet-scp-perl libapache-asp-perl \
+        libtie-ixhash-perl libtime-duration-perl \
+        libhtml-widgets-selectlayers-perl libstorable-perl \
+        libapache-dbi-perl libcache-cache-perl libdbd-mysql-perl
+
+useradd freeside
+su postgres -c "createuser -P freeside"
+
+su freeside -c "createdb freeside"
+
+#?
+cd ../../..
+make install-perl-modules
+make create-config
+freeside-adduser -c -h /usr/local/etc/freeside/htpasswd ivan
+su freeside -c 'freeside-setup ivan'
+su freeside -c '/home/ivan/freeside/bin/populate-msgcat ivan'
+make deploy
index 1e04a42..019c5e1 100644 (file)
@@ -41,3 +41,4 @@ devel/p5-Tie-IxHash
     #www/p5-HTML-Widgets-SelectLayers
 devel/p5-Storable
 www/p5-Apache-DBI
+devel/p5-Cache-Cache
diff --git a/install/openbsd/INSTALL b/install/openbsd/INSTALL
new file mode 100644 (file)
index 0000000..1beef92
--- /dev/null
@@ -0,0 +1,54 @@
+#!/bin/sh
+
+DIR=`pwd`
+
+#cd /usr/ports
+#cvs -q -d anoncvs@anoncvs6.usa.openbsd.org:/cvs up -r OPENBSD_`uname -r | perl -pe 's/\./_/g;'` -Pd
+
+for a in `grep -v '^ *#' $DIR/ports`
+do cd /usr/ports/$a
+  make install
+done
+
+for a in `grep -v '^ *#' $DIR/cpan`
+do perl -MCPAN -e "install $a"
+done
+
+#from /usr/local/share/doc/postgresql/README.OpenBSD
+useradd -c "PostgreSQL Admin User" -g =uid -m -d /var/postgresql -s /bin/sh postgresql
+
+su -l postgresql -c 'mkdir /var/postgresql/data'
+su -l postgresql -c 'initdb -D /var/postgresql/data'
+
+cat <<END >>/etc/rc.local
+if [ -x /usr/local/bin/pg_ctl ]; then
+        su -l postgresql -c "/usr/local/bin/pg_ctl start \
+                -D /var/postgresql/data -l /var/postgresql/logfile \
+                -o '-D /var/postgresql/data'"
+        echo -n ' postgresql'
+fi
+END
+
+cat <<END >>/etc/rc.shutdown
+if [ -f /var/postgresql/data/postmaster.pid ]; then
+        su -l postgresql -c "/usr/local/bin/pg_ctl stop -m fast \
+                -D /var/postgresql/data"
+        rm -f /var/postgresql/data/postmaster.pid
+fi
+
+su -l postgresql -c "/usr/local/bin/pg_ctl start \
+        -D /var/postgresql/data -l /var/postgresql/logfile \
+        -o '-D /var/postgresql/data'"
+
+useradd -c "Freeside" -g =uid -m freeside
+su -l postgresql -c 'createuser -P freeside'
+su -l freeside -c 'createdb freeside'
+
+#?
+cd ../..
+make install-perl-modules
+make create-config
+make deploy
+
+#edit apache config, etc.
+
diff --git a/install/openbsd/cpan b/install/openbsd/cpan
new file mode 100644 (file)
index 0000000..4304b72
--- /dev/null
@@ -0,0 +1,15 @@
+DBIx::DBSchema
+Time::Duration
+Business::CreditCard
+String::ShellQuote
+Net::SSH
+HTML::Mason
+HTML::Widgets::SelectLayers
+DBIx::DataSource
+Date::Manip
+String::Approx
+Tie::IxHash
+Date::Parse
+File::CounterFile
+Net::SCP
+Mail::Internet
diff --git a/install/openbsd/ports b/install/openbsd/ports
new file mode 100644 (file)
index 0000000..3e17d82
--- /dev/null
@@ -0,0 +1,24 @@
+shells/zsh
+misc/screen
+#www/apache13-modssl
+www/mod_perl
+net/rsync
+databases/postgresql
+converters/p5-MIME-Base64
+security/p5-Digest-MD5
+security/p5-MD5
+www/p5-HTML-Tagset
+www/p5-HTML-Parser
+net/p5-libnet
+misc/p5-Locale-Codes
+net/p5-Net-Whois
+www/p5-libwww
+#mail/p5-Mail-Tools
+devel/p5-FreezeThaw
+textproc/p5-Text-Template
+databases/p5-DBI
+databases/p5-DBD-Pg
+#databases/p5-DBD-Msql-Mysql
+www/p5-Apache-ASP
+devel/p5-Storable
+www/p5-Apache-DBI
index 4c07f88..6befc05 100644 (file)
@@ -1,6 +1,6 @@
 #!/bin/sh
 
-wget ftp://apt-rpm.tuxfamily.org/apt/redhat/7.3/en/i386/RPMS.extra/apt-*i386.rpm
+wget --passive-ftp ftp://apt-rpm.tuxfamily.org/apt/redhat/7.3/en/i386/RPMS.extra/apt-*i386.rpm
 rpm -i apt*i386.rpm
 cp sources.list /etc/apt/
 apt-get update; apt-get update
@@ -11,4 +11,23 @@ perl -MCPAN -e"install Locale::Country, Net::Whois, Business::CreditCard, \
                        String::Approx, Text::Template, DBIx::DataSource, \
                        DBIx::DBSchema, Net::SSH, String::ShellQuote, \
                        Net::SCP, Apache::ASP, Tie::IxHash, Time::Duration, \
-                       HTML::Widgets::SelectLayers,  Apache::DBI"
+                       HTML::Widgets::SelectLayers, Apache::DBI, Cache::Cache"
+
+useradd freeside
+
+chkconfig postgresql on
+/etc/init.d/postgresql start
+
+su postgres -c "createuser -P freeside"
+
+su freeside -c "createdb freeside"
+
+#?
+cd ../..
+make install-perl-modules
+make create-config
+freeside-adduser -c -h /usr/local/etc/freeside/htpasswd ivan
+su freeside -c 'freeside-setup ivan'
+su freeside -c '/home/ivan/freeside/bin/populate-msgcat ivan'
+make deploy
+
diff --git a/install/redhat/9/INSTALL b/install/redhat/9/INSTALL
new file mode 100644 (file)
index 0000000..ee3cba9
--- /dev/null
@@ -0,0 +1,65 @@
+#!/bin/sh
+
+
+wget --passive-ftp --continue ftp://apt-rpm.tuxfamily.org/apt/redhat/9/en/i386/RPMS.extra/apt-*i386.rpm
+rpm -i apt*i386.rpm
+cp sources.list /etc/apt/
+apt-get update; apt-get update
+#apt-get install apache mod_ssl mod_perl perl-CGI perl-CPAN perl-DBD-MySQL perl-DBD-Pg perl-DBI perl-DateManip perl-Digest-MD5 perl-HTML-Parser perl-HTML-Tagset perl-MIME-Base64 perl-Storable perl-TimeDate perl-URI perl-libnet perl-libwww-perl perl-suidperl rsync postgresql postgresql-docs postgresql-libs postgresql-server screen zsh lftp cvs #openssh
+
+apt-get install perl-Devel-Symdump perl-BSD-Resource rpm-build gdbm-devel expat-devel openssl-devel krb5-devel db4-devel
+
+wget --passive-ftp --continue http://reb00t.com/linux/RPMS/redhat-9/apache/apache-1.3.28-0.n0i.src.rpm http://reb00t.com/linux/RPMS/redhat-9/mm/mm-1.2.1-0.n0i.i686.rpm http://reb00t.com/linux/RPMS/redhat-9/mm/mm-devel-1.2.1-0.n0i.i686.rpm
+rpm -i mm-1.2.1-0.n0i.i686.rpm mm-devel-1.2.1-0.n0i.i686.rpm apache-1.3.28-0.n0i.src.rpm
+
+install -d /usr/src/redhat
+for a in BUILD RPMS SOURCES SPECS SRPMS; do install -d /usr/src/redhat/$a; done
+for a in athlon i386 i486 i586 i686 noarch; do install -d /usr/src/redhat/RPMS/$a; done
+
+cd /usr/src/redhat/SPECS  
+rpmbuild -ba apache.spec
+
+cd /usr/src/redhat/RPMS/i386
+rpm -i apache-1.3.28-0.n0i.i386.rpm
+
+apt-get install perl-CGI perl-CPAN perl-DBD-MySQL perl-DBD-Pg perl-DBI perl-DateManip perl-HTML-Parser perl-HTML-Tagset perl-TimeDate perl-URI perl-libwww-perl perl-suidperl rsync postgresql postgresql-docs postgresql-libs postgresql-server screen zsh lftp cvs gcc gd #openssh
+
+wget --passive-ftp --continue http://atrpms.physik.fu-berlin.de/dist/rh9/perl-GD/perl-GD-2.11-7.rh9.at.i386.rpm http://atrpms.physik.fu-berlin.de/dist/rh9/atrpms/atrpms-45-1.rh9.at.noarch.rpm http://atrpms.physik.fu-berlin.de/dist/rh9/yum/yum-2.0.4-28.rh9.at.noarch.rpm http://atrpms.physik.fu-berlin.de/dist/rh9/gd/gd-2.0.15-1_6.rh9.at.i386.rpm http://atrpms.physik.fu-berlin.de/dist/rh9/atrpms/atrpms-package-config-45-1.rh9.at.noarch.rpm
+
+cp /etc/apt/apt.conf /etc/apt/apt.conf.real
+
+rpm -i --replacefiles atrpms-package-config-45-1.rh9.at.noarch.rpm yum-2.0.4-28.rh9.at.noarch.rpm atrpms-45-1.rh9.at.noarch.rpm gd-2.0.15-1_6.rh9.at.i386.rpm perl-GD-2.11-7.rh9.at.i386.rpm
+
+mv /etc/apt/apt.conf.real /etc/apt/apt.conf
+
+perl -MCPAN -e"install Locale::Country, Net::Whois, Business::CreditCard, \
+                       Mail::Internet, File::CounterFile, FreezeThaw, \
+                       String::Approx, Text::Template, DBIx::DataSource, \
+                       DBIx::DBSchema, Net::SSH, String::ShellQuote, \
+                       Net::SCP, Apache::ASP, Tie::IxHash, Time::Duration, \
+                       HTML::Widgets::SelectLayers, Apache::DBI, Cache::Cache \
+                       Test::Pod NetAddr::IP IPC::ShareLite Chart::LinesPoints"
+
+echo 'OPTIONS="-DHAVE_PERL -DHAVE_SSL"' >>/etc/sysconfig/apache
+
+#remove perl & ssl LoadModule lines from /etc/httpd/conf/httpd.conf
+#as they're statically linked
+
+/usr/sbin/useradd freeside
+
+/sbin/chkconfig postgresql on
+/etc/init.d/postgresql start
+
+su postgres -c "createuser -P freeside"
+
+su freeside -c "createdb freeside"
+
+#?
+cd ../../..
+make install-perl-modules
+make create-config
+freeside-adduser -c -h /usr/local/etc/freeside/htpasswd ivan
+su freeside -c 'freeside-setup ivan'
+su freeside -c '/home/ivan/freeside/bin/populate-msgcat ivan'
+make deploy
+
diff --git a/install/redhat/9/sources.list b/install/redhat/9/sources.list
new file mode 100644 (file)
index 0000000..f6f21f4
--- /dev/null
@@ -0,0 +1,2 @@
+rpm ftp://apt-rpm.tuxfamily.org/apt redhat/9/en/i386 os updates extra
+rpm-src ftp://apt-rpm.tuxfamily.org/apt redhat/9/en/i386 os updates extra
diff --git a/rt/README b/rt/README
deleted file mode 100755 (executable)
index 7c5e4d4..0000000
--- a/rt/README
+++ /dev/null
@@ -1,300 +0,0 @@
-# BEGIN LICENSE BLOCK
-# 
-# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
-# 
-# (Except where explictly superceded by other copyright notices)
-# 
-# This work is made available to you under the terms of Version 2 of
-# the GNU General Public License. A copy of that license should have
-# been provided with this software, but in any event can be snarfed
-# from www.gnu.org.
-# 
-# This work is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
-# 
-# Unless otherwise specified, all modifications, corrections or
-# extensions to this work which alter its source code become the
-# property of Best Practical Solutions, LLC when submitted for
-# inclusion in the work.
-# 
-# 
-# END LICENSE BLOCK
-RT is an enterprise-grade issue tracking system. It allows
-organizations to keep track of their to-do lists, who is working
-on which tasks, what's already been done, and when tasks were
-completed. It is available under the terms of version 2 of the GNU
-General Public License (GPL), so it doesn't cost anything to set
-up and use.
-
-
-        Jesse Vincent
-        Best Practical Solutions, LLC
-        March 2003
-
-REQUIRED PACKAGES:
-------------------
-
-o   Perl 5.8.0 or later (http://www.perl.com).
-
-       (If you intend to use the FastCGI or SpeedyCGI support, you 
-        need to make sure that perl has been built with support for 
-        setgid perl scripts.)`
-
-    Perl 5.6.1 is currently deprecated and will be officially desupported
-    in a future release
-
-o   A DB backend; MySQL is recommended ( http://www.mysql.com ) 
-        Currently supported:  Mysql 4.0.13 or later. 
-                              Postgres 7.2 or later.
-
-                              Mysql 3.23.46 or newer with support for InnoDB 
-                             is currently deprecated and will be officially
-                             desupported in a future release.
-
-o   Apache version 1.3.x or 2.x (http://httpd.apache.org) 
-    with mod_perl -- (http://perl.apache.org ) 
-    or a webserver with FastCGI support (www.fastcgi.com)
-
-        mod_perl 2.0 isn't quite ready for prime_time just yet;
-        Best Practical Solutions strongly recommends that sites use 
-        Apache 1.3 or FastCGI.
-
-        Compiling mod_perl on Apache 1.3.x as a DSO has been known 
-         to have massive stability problems and is not recommended.
-
-        mod_perl 1.x must be build with EVERYTHING=1
-
-        RT's FastCGI handler runs setgid to the 'rt' group to
-        protect RT's database password.  You may need to install
-        a special  "suidperl" package or reconfigure your perl
-        setup to support "setuid scripts" if you intend to use RT
-        with FastCGI.
-
-        Debian GNU/* 3.0+: the package which installs suidperl is
-         called perl-suid, and should work without any tweaking.
-
-        FreeBSD 4.2+: the package is called sperl, and should
-         install a suidperl that just works 
-
-        Conectiva Linux 6.0+: suidperl is installed by default when 
-         perl is installed, but the program /bin/suidperl is not setuid. 
-         You must use chmod to make it setuid.
-
-
-
-o    Various and sundry perl modules
-       A tool included with RT takes care of the installation of
-       most of these automatically during the install process.
-
-       The tool supplied with RT uses Perl's CPAN system
-       (http://www.cpan.org) to install modules. Some operating
-       systems package all or some of the modules required and
-       you may be better off installing the modules that way.
-
-
-GENERAL INSTALLATION
---------------------
-
-This is a rough guide to installing RT. For more detail, you'll want 
-to read 'Chapter 2: Installing' in RT's manual, available at
-http://www.bestpractical.com/rt 
-
-1   Unpack this distribution SOMWHERE OTHER THAN where you want to install RT
-
-        Granted, you've already got it open. To do this cleanly:
-
-                tar xzvf rt.tar.gz -C /tmp
-
-2   Run the "configure" script. 
-
-        ./configure --help to see the list of options
-        ./configure (with the flags you want)
-
-3   Satisfy RT's myriad dependencies. 
-
-3.1   Check for compliance:
-        
-   perl sbin/rt-test-dependencies \ 
-                --with-<databasename> --with-<web-environment>
-
-        databasename is one of: mysql, postgres
-        web-environment is one of: fastcgi, modperl1, modperl2
-
-3.2   If there are unsatisfied dependencies, install them by hand or run:
-
-        perl sbin/rt-test-dependencies \
-                --with-<databasename> --with-<web-environment> --install
-        
-
-3.3   Check to make sure everything was installed properly:
-
-        perl sbin/rt-test-dependencies \
-                --with-<databasename> --with-<web-environment>
-
-4   Create a group called 'rt'
-
-5a  FOR A NEW INSTALLATION: 
-        
-        As root, type:
-                 make install        (replace "make" with the local name for 
-                                 Make, if you need to)
-
-                       
-                 make initialize-database 
-
-
-        If the make fails, type:
-                make dropdb 
-        and start over from step 5a
-
-5b  FOR UPGRADING: (Within the RT 3.0.x series)
-
-        As root, type: 
-                make upgrade     (replace "make" with the local name for 
-                                  Make, if you need to)
-
-        This will build new binaries, config files and libraries without
-        overwriting your RT database. 
-        
-        It may then instruct you to update your RT system database objects 
-
-6   Edit etc/RT_SiteConfig.pm in your RT installation directory, by specifying
-    any values you need to change from the defaults in etc/RT_Config.pm
-
-7   Configure the email and web gateways, as described below. 
-
-8   Stop and start your webserver, so it picks up your configuration changes.
-
-    NOTE: root's password for the web interface is "password" 
-    (without the quotes.)  Not changing this is a SECURITY risk
-    
-9   Configure RT per the instructions in RT's manual.
-
-    Until you do this, RT will not be able to send or receive email,
-    nor will it be more than marginally functional.  This is not an
-    optional step.
-
-
-THE WEB INTERFACE
------------------
-
-RT's web interface is based around HTML::Mason, which works best with the mod_perl
-perl interpreter within Apache httpd.  Alternatively, support for the FastCGI
-(and plain CGI) interface is also provided as 'bin/mason_handler.fcgi'.
-
-Apache 
-        You'll need to add a few lines to your httpd.conf telling it about RT:
-
-<VirtualHost your.ip.address>
-    ServerName your.rt.server.hostname
-    DocumentRoot /opt/rt3/share/html
-    AddDefaultCharset UTF-8
-
-    # this line applies to Apache2+mod_perl2 only
-    PerlModule Apache2 Apache::compat
-
-    PerlModule Apache::DBI
-    PerlRequire /opt/rt3/bin/webmux.pl
-
-    # this section applies to Apache 1 only
-    <Location />
-        SetHandler perl-script
-        PerlHandler RT::Mason
-    </Location>
-
-    # this section applies to Apache2+mod_perl2 only
-    <FilesMatch "\.html$">
-        SetHandler perl-script
-        PerlHandler RT::Mason
-    </FilesMatch>
-    <LocationMatch "/Attachment/">
-        SetHandler perl-script
-        PerlHandler RT::Mason
-    </LocationMatch>
-    <LocationMatch "/REST/">
-        SetHandler perl-script
-        PerlHandler RT::Mason
-    </LocationMatch>
-</VirtualHost>
-
-
-
-SETTING UP THE MAIL GATEWAY 
----------------------------
-
-An alias for the initial queue will need to be made in either your
-global mail aliases file (if you are using NIS) or locally on your
-machine.
-Add the following lines to /etc/aliases (or your local equivalent) :
-
-rt:         "|/opt/rt3/bin/rt-mailgate --queue general --action correspond --url http://localhost/"
-rt-comment: "|/opt/rt3/bin/rt-mailgate --queue general --action comment --url http://localhost/"
-                                            |                |             |
-                            <queue-name>----/                |             |
-                                                             |             |
-               <correspond or comment depending on whether   |             |
-               the mail should be resent to the requestor>---/             |
-                                                                           |
-                                            <URL for RT's web interface>---/
-
-
-BUGS
-----
-
-To report a bug, send email to rt-3.0-bugs@fsck.com.
-
-GETTING HELP
-------------
-
-If RT is mission-critical for you or if you use it heavily, we recommend that
-you purchase a commercial support contract.  Details on support contracts
-are available at http://www.bestpractical.com.
-
-If you're interested in having RT extended or customized or would like more
-information about commercial support options, please send email to 
-<sales@bestpractical.com> to discuss rates and availability.
-
-
-RT-USERS MAILINGLIST
---------------------
-
-To keep up to date on the latest RT tips, techniques and extensions,
-you probably want to join the rt-users mailing list.  Send a message to:
-
-         rt-users-request@lists.fsck.com 
-
-With the body of the message consisting of only the word:
-
-        subscribe
-
-If you're interested in hacking on RT, you'll want to subscribe to
-rt-devel@lists.fsck.com.  Subscribe to it with instructions similar to
-those above.
-
-Address questions about the stable release to the rt-users list, and
-questions about the development version to the rt-devel list.  If you feel
-your questions are best not asked publicly, send them personally to
-<jesse@bestpractical.com>.
-
-
-RT WEBSITE
-----------
-
-For current information about RT, check out the RT website at 
-        http://www.bestpractical.com/  
-
-You'll find screenshots, a pointer to the current version of RT, contributed 
-patches, and lots of other great stuff.
-
-
-TROUBLESHOOTING
----------------
-
-If the solution to the problem you're running into isn't obvious and you've 
-checked the FAQ, feel free to send mail to rt-users@fsck.com (for released 
-versions of RT) or rt-devel@fsck.com (for development versions).
-
-Thanks!