Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan-debian@420.am>
Thu, 15 Mar 2012 20:56:48 +0000 (13:56 -0700)
committerIvan Kohler <ivan-debian@420.am>
Thu, 15 Mar 2012 20:56:48 +0000 (13:56 -0700)
25 files changed:
FS/FS/ClientAPI/MyAccount.pm
FS/FS/Schema.pm
FS/FS/cdr.pm
FS/FS/detail_format.pm
FS/FS/part_event.pm
FS/FS/part_event/Condition/signupdate_day.pm [new file with mode: 0644]
FS/FS/router.pm
FS/FS/svc_acct.pm
FS/FS/svc_broadband.pm
FS/FS/svc_hardware.pm
httemplate/browse/part_event.html
httemplate/browse/router.cgi
httemplate/docs/credits.html
httemplate/docs/license.html
httemplate/edit/process/svc_broadband.cgi
httemplate/edit/router.cgi
httemplate/edit/svc_broadband.cgi
httemplate/edit/svc_hardware.cgi
httemplate/elements/masked_input_1.1.js [new file with mode: 0644]
httemplate/elements/tr-input-mac_addr.html [new file with mode: 0644]
httemplate/elements/tr-input-mask.html [new file with mode: 0644]
httemplate/elements/tr-select-router_block_ip.html
httemplate/view/part_event-targets.html
httemplate/view/svc_broadband.cgi
httemplate/view/svc_hardware.cgi

index 7d177f9..acd0c6e 100644 (file)
@@ -2485,7 +2485,7 @@ sub myaccount_passwd {
       unless $svc_acct->check_password($p->{'old_password'});
   }
 
-  $svc_acct->_password($p->{'new_password'});
+  $svc_acct->set_password($p->{'new_password'});
   my $error = $svc_acct->replace();
 
   my($label, $value) = $svc_acct->cust_svc->label;
@@ -2626,7 +2626,7 @@ sub process_reset_passwd {
   my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
     or return { 'error' => "Service not found" };
 
-  $svc_acct->_password($p->{'new_password'});
+  $svc_acct->set_password($p->{'new_password'});
   my $error = $svc_acct->replace();
 
   my($label, $value) = $svc_acct->cust_svc->label;
index 1112f52..a36d2dc 100644 (file)
@@ -2484,7 +2484,7 @@ sub tables_hashref {
         'routername', 'varchar', '', $char_d, '', '', 
         'svcnum', 'int', 'NULL', '', '', '', 
         'agentnum',   'int', 'NULL', '', '', '', 
-        'auto_addr', 'char', 'NULL', 1, '', '',
+        'manual_addr', 'char', 'NULL', 1, '', '',
       ],
       'primary_key' => 'routernum',
       'unique'      => [],
index 1769fe9..3a6b01b 100644 (file)
@@ -152,7 +152,7 @@ following fields are currently supported:
 
 =item svcnum - Link to customer service (see L<FS::cust_svc>)
 
-=item freesidestatus - NULL, processing-tiered, rated, done
+=item freesidestatus - NULL, processing-tiered, rated, done, skipped, no-charge, failed
 
 =item freesiderewritestatus - NULL, done, skipped
 
@@ -545,7 +545,7 @@ sub rate_prefix {
                                          );
   if ( $reason ) {
     warn "not charging for CDR ($reason)\n" if $DEBUG;
-    return $self->set_status_and_rated_price( 'rated',
+    return $self->set_status_and_rated_price( 'skipped',
                                               0,
                                               $opt{'svcnum'},
                                             );
index f70acc6..88cc02f 100644 (file)
@@ -235,6 +235,7 @@ sub duration {
   my $cdr = shift;
   my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
   my $sec = $object->rated_seconds if $object;
+  $sec ||= 0;
   # XXX termination objects don't have rated_granularity so this may 
   # result in inbound CDRs being displayed as min/sec when they shouldn't.
   # Should probably fix this.
index 31d2afd..62f16fa 100644 (file)
@@ -253,7 +253,7 @@ sub templatename {
   }
 }
 
-=item targets
+=item targets OPTIONS
 
 Returns all objects (of type C<FS::eventtable>, for this object's 
 C<eventtable>) eligible for processing under this event, as of right now.
@@ -268,7 +268,8 @@ but can be useful when configuring events.
 
 sub targets {
   my $self = shift;
-  my $time = time; # $opt{'time'}?
+  my %opt = @_;
+  my $time = $opt{'time'} || time;
 
   my $eventpart = $self->eventpart;
   $eventpart =~ /^\d+$/ or die "bad eventpart $eventpart";
@@ -305,8 +306,8 @@ sub targets {
   });
   my @tested_objects;
   foreach my $object ( @objects ) {
-    my $cust_event = $self->new_cust_event($object, 'time' => $time);
-    next unless $cust_event->test_conditions;
+    my $cust_event = $self->new_cust_event($object);
+    next unless $cust_event->test_conditions('time' => $time);
 
     $object->set('cust_event', $cust_event);
     push @tested_objects, $object;
diff --git a/FS/FS/part_event/Condition/signupdate_day.pm b/FS/FS/part_event/Condition/signupdate_day.pm
new file mode 100644 (file)
index 0000000..dbe9e60
--- /dev/null
@@ -0,0 +1,54 @@
+package FS::part_event::Condition::signupdate_day;
+
+use strict;
+use Tie::IxHash;
+
+use base qw( FS::part_event::Condition );
+
+sub description {
+  "Customer signed up on the same day of month as today";
+}
+
+sub option_fields {
+  (
+    'delay' => { label  => 'Delay additional days',
+                 type   => 'text',
+                 value  => '0',
+               },
+  );
+}
+
+sub condition {
+  my( $self, $object, %opt ) = @_;
+
+  my $cust_main = $self->cust_main($object);
+
+  my ($today) = (localtime($opt{'time'}))[3];
+
+  my $delay = $self->option('delay') || 0;
+  my $signupday = ((localtime($cust_main->signupdate + $delay * 86400))[3] - 1)
+                   % 28 + 1;
+  
+  $today == $signupday;
+}
+
+sub condition_sql {
+  my( $class, $table, %opt ) = @_;
+  my $mday;
+  if ( $opt{'driver_name'} eq 'Pg' ) {
+    $mday = sub{ "EXTRACT( DAY FROM TO_TIMESTAMP($_[0]) )::INTEGER" };
+  }
+  elsif ( $opt{'driver_name'} eq 'mysql' ) {
+    $mday = sub{ "DAY( FROM_UNIXTIME($_[0]) )" };
+  }
+  else {
+    return 'true';
+  }
+
+  my $delay = $class->condition_sql_option_integer('delay', 
+    $opt{'driver_name'}); # returns 0 for null
+  $mday->($opt{'time'}) . ' = '.
+    '(' . $mday->("cust_main.signupdate + $delay * 86400") . ' - 1) % 28 + 1';
+}
+
+1;
index 99373e5..6fa44b4 100755 (executable)
@@ -40,8 +40,9 @@ fields are currently supported:
 
 =item svcnum - svcnum of the owning FS::svc_broadband, if appropriate
 
-=item auto_addr - flag to automatically assign IP addresses to services
-linked to this router ('Y' or null).
+=item manual_addr - set to 'Y' to allow services linked to this router 
+to have any IP address, rather than one in an address block belonging 
+to the router.
 
 =back
 
@@ -86,7 +87,7 @@ sub check {
   my $error =
     $self->ut_numbern('routernum')
     || $self->ut_text('routername')
-    || $self->ut_enum('auto_addr', [ '', 'Y' ])
+    || $self->ut_enum('manual_addr', [ '', 'Y' ])
     || $self->ut_agentnum_acl('agentnum', 'Broadband global configuration')
   ;
   return $error if $error;
@@ -146,7 +147,7 @@ sub addr_block {
 
 sub auto_addr_block {
   my $self = shift;
-  return () if !$self->auto_addr;
+  return () if $self->manual_addr;
   return qsearch('addr_block', { routernum => $self->routernum,
                                  manual_flag => '' });
 }
index 139f927..e67db43 100644 (file)
@@ -2524,7 +2524,8 @@ sub check_password {
 
   if ( $self->_password_encoding eq 'ldap' ) {
 
-    my $auth = from_rfc2307 Authen::Passphrase $self->_password;
+    $password =~ s/^{PLAIN}/{CLEARTEXT}/;
+    my $auth = from_rfc2307 Authen::Passphrase $password;
     return $auth->match($check_password);
 
   } elsif ( $self->_password_encoding eq 'crypt' ) {
index 1096200..212a4bf 100755 (executable)
@@ -135,7 +135,7 @@ sub table_info {
 
 sub table { 'svc_broadband'; }
 
-sub table_dupcheck_fields { ( 'mac_addr' ); }
+sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
 
 =item search HASHREF
 
@@ -406,7 +406,13 @@ sub check {
   }
   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
 
-  if ($self->routernum) {
+  if ( $conf->exists('auto_router') and $self->ip_addr and !$self->routernum ) {
+    # assign_router is guaranteed to provide a router that's legal
+    # for this agent and svcpart
+    my $error = $self->_check_ip_addr || $self->assign_router;
+    return $error if $error;
+  }
+  elsif ($self->routernum) {
     return "Router ".$self->routernum." does not provide this service"
       unless qsearchs('part_svc_router', { 
         svcpart => $svcpart,
@@ -417,16 +423,19 @@ sub check {
     return "Router ".$self->routernum." does not serve this customer"
       if $router->agentnum and $router->agentnum != $agentnum;
 
-    if ( $router->auto_addr ) {
+    if ( $router->manual_addr ) {
+      $self->blocknum('');
+    }
+    else {
       my $addr_block = $self->addr_block;
       unless ( $addr_block and $addr_block->manual_flag ) {
         my $error = $self->assign_ip_addr;
         return $error if $error;
       }
     }
-    else {
-      $self->blocknum('');
-    }
+    my $error = $self->_check_ip_addr;
+    return $error if $error;
   } # if $self->routernum
 
   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
@@ -440,15 +449,12 @@ sub check {
     }
   }
 
-  $error = $self->_check_ip_addr;
-  return $error if $error;
-
   $self->SUPER::check;
 }
 
 =item assign_ip_addr
 
-Assign an address block matching the selected router, and the selected block
+Assign an IP address matching the selected router, and the selected block
 if there is one.
 
 =cut
@@ -469,6 +475,7 @@ sub assign_ip_addr {
   else { 
     return '';
   }
+#warn "assigning ip address in blocks\n".join("\n",map{$_->cidr} @blocks)."\n";
 
   foreach my $block ( @blocks ) {
     if ( $self->ip_addr and $block->NetAddr->contains($self->NetAddr) ) {
@@ -487,6 +494,29 @@ sub assign_ip_addr {
   }
 }
 
+=item assign_router
+
+Assign an address block and router matching the selected IP address.
+Does nothing if IP address is null.
+
+=cut
+
+sub assign_router {
+  my $self = shift;
+  return '' if !$self->ip_addr;
+  #warn "assigning router/block for ".$self->ip_addr."\n";
+  foreach my $router ($self->allowed_routers) {
+    foreach my $block ($router->addr_block) {
+      if ( $block->NetAddr->contains($self->NetAddr) ) {
+        $self->blocknum($block->blocknum);
+        $self->routernum($block->routernum);
+        return '';
+      }
+    }
+  }
+  return $self->ip_addr.' is not in an allowed block.';
+}
+
 sub _check_ip_addr {
   my $self = shift;
 
@@ -494,6 +524,9 @@ sub _check_ip_addr {
     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); 
     return 'IP address required';
   }
+  else {
+    return 'Cannot parse address: '.$self->ip_addr unless $self->NetAddr;
+  }
 #  if (my $dup = qsearchs('svc_broadband', {
 #        ip_addr => $self->ip_addr,
 #        svcnum  => {op=>'!=', value => $self->svcnum}
@@ -506,10 +539,17 @@ sub _check_ip_addr {
 sub _check_duplicate {
   my $self = shift;
 
-  return "MAC already in use"
-    if ( $self->mac_addr &&
-         scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
-       );
+  $self->lock_table;
+
+  my @dup;
+  @dup = $self->find_duplicates('global', 'ip_addr');
+  if ( @dup ) {
+    return "IP address in use (svcnum ".$dup[0]->svcnum.")";
+  }
+  @dup = $self->find_duplicates('global', 'mac_addr');
+  if ( @dup ) {
+    return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
+  }
 
   '';
 }
@@ -558,8 +598,15 @@ Returns a list of allowed FS::router objects.
 sub allowed_routers {
   my $self = shift;
   my $svcpart = $self->svcnum ? $self->cust_svc->svcpart : $self->svcpart;
-  map { $_->router } qsearch('part_svc_router', 
+  my @r = map { $_->router } qsearch('part_svc_router', 
     { svcpart => $self->cust_svc->svcpart });
+  if ( $self->cust_main ) {
+    my $agentnum = $self->cust_main->agentnum;
+    return grep { !$_->agentnum or $_->agentnum == $agentnum } @r;
+  }
+  else {
+    return @r;
+  }
 }
 
 =back
index b4eb8cc..22e6275 100644 (file)
@@ -164,7 +164,7 @@ sub check {
   if ( $conf->exists('svc_hardware-check_mac_addr') ) {
     $hw_addr = uc($hw_addr);
     $hw_addr =~ /^[0-9A-F]{12}$/ 
-      or return "Illegal (MAC address) ".$self->getfield('hw_addr');
+      or return "Illegal (MAC address) '".$self->getfield('hw_addr')."'";
   }
   $self->setfield('hw_addr', $hw_addr);
 
index 6be2860..0399643 100644 (file)
@@ -45,7 +45,8 @@ my $link = [ $p.'edit/part_event.html?', 'eventpart' ];
 my $event_sub = sub {
   my $part_event = shift;
   my $onclick = include('/elements/popup_link_onclick.html',
-    action      => $p.'view/part_event-targets.html?'.$part_event->eventpart,
+    action      => $p.'view/part_event-targets.html?eventpart='.
+                    $part_event->eventpart,
     actionlabel => 'Event query - '.$part_event->event,
     width       => 650,
     height      => 420,
index 21047d7..ef8ad31 100644 (file)
@@ -17,7 +17,7 @@
                                                                shift->addr_block
                                                  );
                                            },
-                                       sub { shift->auto_addr ? 'Automatic' : 'Manual' },
+                                       sub { shift->manual_addr ? 'Manual' : 'Automatic' },
                                        sub { 'Delete' },
                                      ],
                 'links'           => [ [ "${p2}edit/router.cgi?", 'routernum' ],
index 7b3b0b9..9bb1dec 100644 (file)
@@ -61,6 +61,7 @@ Joe Camadine<BR>
 Chris Cappuccio<BR>
 Rebecca Cardennis<BR>
 Shane Chrisp<BR>
+Kendall Conrad<BR>
 Luke Crawford<BR>
 Brad Dameron<BR>
 Dave Denney<BR>
index e0e40b7..fab8cd0 100644 (file)
@@ -112,6 +112,11 @@ terms as Perl (GPL/Artistic).
 Contains code derived from HTML::GoogleMapsV3 by David Peters, licensed under
 the same terms as Perl (GPL/Artistic).
 
+<P>
+Contains the Masked Input JavaScript library by Kendall Conrad, licensed under
+a <a href="http://creativecommons.org/licenses/by-sa/3.0/us/">Creative Commons 
+Attribution-ShareAlike 3.0 United States</a> license.
+
 <!-- artwork -->
 
 <P>
index 4184f5f..31def25 100644 (file)
@@ -13,6 +13,9 @@ die "access denied"
 
 sub precheck {
   my $cgi = shift;
+  if ( !defined($cgi->param('ip_addr')) ) {
+    $cgi->param('ip_addr', $cgi->param('prev_ip_addr') || '');
+  }
   $cgi->param("usergroup", [ $cgi->param('usergroup') ]);
   ''
 }
index 6672d5d..fdcd7b3 100755 (executable)
@@ -7,13 +7,13 @@
                         'routername' => 'Name',
                         'svc_part'   => 'Service',
                         'agentnum'   => 'Agent',
-                        'auto_addr'  => 'Assign IP addresses automatically',
+                        'manual_addr'  => 'Assign IP addresses manually',
                       },
      'fields'      => [
                         { 'field'=>'routername', 'type'=>'text', 'size'=>32 },
                         { 'field'=>'agentnum',   'type'=>'select-agent' },
                         { 'field'=>'svcnum',     'type'=>'hidden' },
-                        { 'field'=>'auto_addr','type'=>'checkbox','value'=>'Y'},
+                        { 'field'=>'manual_addr','type'=>'checkbox','value'=>'Y'},
                       ],
      'error_callback' => $callback,
      'edit_callback'  => $callback,
index 8fccb1f..b266928 100644 (file)
@@ -102,9 +102,10 @@ END
 my @fields = (
   qw( description speed_down speed_up ),
   { field=>'sectornum', type=>'select-tower_sector', },
-  { field=>'routernum', type=>'select-router_block_ip', },
-  qw( mac_addr latitude longitude altitude vlan_profile 
-      performance_profile authkey plan_id ),
+  { field=>'routernum', type=>'select-router_block_ip' },
+  { field=>'mac_addr' , type=>'input-mac_addr' },
+    qw( latitude longitude altitude vlan_profile 
+    performance_profile authkey plan_id )
 );
 
 if ( $conf->exists('svc_broadband-radius') ) {
@@ -115,8 +116,6 @@ if ( $conf->exists('svc_broadband-radius') ) {
   }
 }
 
-my $fixedblock = '';
-
 my $part_svc;
 
 my $svc_edit_callback = sub {
@@ -124,8 +123,6 @@ my $svc_edit_callback = sub {
 
   $part_svc = $part_svc_x; #for field_callback to use
 
-  $opt->{'labels'}{'block_label'} = 'Block';
-
   my ($nas_export) = $part_svc->part_export('broadband_nas');
   #can we assume there's only one of these per part_svc?
   if ( $nas_export ) {
@@ -173,60 +170,13 @@ my $field_callback = sub {
                             ? 'fixed'
                             : 'hidden';
     $fieldref->{'value'} = $columndef->columnvalue;
-    $fixedblock = $fieldref->{value}
-      if $fieldref->{field} eq 'blocknum';
-
+    
     if ( $fieldref->{field} eq 'usergroup' ) {
       $fieldref->{'formatted_value'} = 
         [ $object->radius_groups('long_description') ];
     }
   }
 
-  if ($object->svcnum) { 
-
-    $fieldref->{type} = 'hidden'
-      if $fieldref->{field} eq 'blocknum';
-      
-    $fieldref->{value} = $object->addr_block->label
-      if $fieldref->{field} eq 'block_label' && $object->addr_block;
-
-  } else { 
-
-    if ($fieldref->{field} eq 'block_label') {
-      if ($fixedblock && $object->addr_block) {
-        $object->blocknum($fixedblock);
-        $fieldref->{value} = $object->addr_block->label;
-      }else{
-        $fieldref->{type} = 'hidden';
-      }
-    }
-
-    if ($fieldref->{field} eq 'blocknum') {
-      if ( $fixedblock or $conf->exists('auto_router') ) {
-        $fieldref->{type} = 'hidden';
-        $fieldref->{value} = $fixedblock;
-        return;
-      }
-
-      my $cust_pkg = qsearchs( 'cust_pkg', {pkgnum => $cgi->param('pkgnum')} );
-      die "No cust_pkg entry!" unless $cust_pkg;
-
-      $object->svcpart($part_svc->svcpart);
-      my @addr_block =
-        grep {  ! $_->agentnum
-               || $cust_pkg->cust_main->agentnum == $_->agentnum
-               && $FS::CurrentUser::CurrentUser->agentnum($_->agentnum)
-             }
-        map { $_->addr_block } $object->allowed_routers;
-      my @options = map { $_->blocknum } 
-                    sort { $a->label cmp $b->label } @addr_block;
-      my %option_labels = map { ( $_->blocknum => $_->label ) } @addr_block;
-      $fieldref->{type}    = 'select';
-      $fieldref->{options} = \@options;
-      $fieldref->{labels}  = \%option_labels;
-    }
-
-  }
 }; 
 
 </%init>
index dcf83de..d9cd4cd 100644 (file)
@@ -26,7 +26,8 @@ my @fields = (
   },
   {
     field => 'hw_addr',
-    type  => 'text',
+    type  => $conf->exists('svc_hardware-check_mac_addr') ? 
+              'input-mac_addr' : 'text',
     label => 'Hardware address',
   },
   {
diff --git a/httemplate/elements/masked_input_1.1.js b/httemplate/elements/masked_input_1.1.js
new file mode 100644 (file)
index 0000000..05efa77
--- /dev/null
@@ -0,0 +1,195 @@
+/***********************************************************************
+                       Masked Input version 1.1
+************************************************************************
+Author: Kendall Conrad
+Home page: http://www.angelwatt.com/coding/masked_input.php
+Created:  2008-12-16
+Modified: 2010-04-14
+Description:
+License: This work is licensed under a Creative Commons Attribution-Share Alike
+  3.0 United States License http://creativecommons.org/licenses/by-sa/3.0/us/
+
+Argument pieces:
+- elm:        [req] text input node to apply the mask on
+- format:     [req] string format for the mask
+- allowed:    [opt, '0123456789'] string with chars allowed to be typed
+- sep:        [opt, '\/:-'] string of char(s) used as separators in mask
+- typeon:     [opt, '_YMDhms'] string of chars in mask that can be typed on
+- onbadkey:   [opt, null] function to run when user types a unallowed key
+- badkeywait: [opt, 0] used with onbadkey. Indicates how long (in ms) to lock
+  text input for onbadkey function to run
+***********************************************************************/
+function MaskedInput(args)
+{
+  if (args['elm'] === null || args['format'] === null) { return false; }
+  var el     = args['elm'],
+    format   = args['format'],
+    allowed  = args['allowed']    || '0123456789',
+    sep      = args['separator']  || '\/:-',
+    open     = args['typeon']     || '_YMDhms',
+    onbadkey = args['onbadkey']   || function(){},
+    badwait  = args['badkeywait'] || 0;
+  
+  var locked = false, hold = 0;
+  el.value = format;
+  // Assign events
+  el.onkeydown  = KeyHandlerDown;  //
+  el.onkeypress = KeyHandlerPress; // add event handlers to element
+  el.onkeyup    = KeyHandlerUp;    //
+
+  function GetKey(code)
+  {
+    code = code || window.event, ch = '';
+    var keyCode = code.which, evt = code.type;
+    if (keyCode == null) { keyCode = code.keyCode; }
+    if (keyCode === null) { return ''; } // no key, no play
+    // deal with special keys
+    switch (keyCode) {
+    case 8:  ch = 'bksp'; break;
+    case 46: // handle del and . both being 46
+      ch = (evt == 'keydown') ? 'del' : '.'; break;
+    case 16: ch = 'shift'; break;//shift
+    case 0:/*CRAP*/ case 9:/*TAB*/ case 13:/*ENTER*/
+      ch = 'etc'; break;
+    case 37: case 38: case 39: case 40: // arrow keys
+      ch = (!code.shiftKey &&
+           (code.charCode != 39 && code.charCode !== undefined)) ?
+        'etc' : String.fromCharCode(keyCode);
+      break;
+    // default to thinking it's a character or digit
+    default: ch = String.fromCharCode(keyCode);
+    }
+    return ch;
+  }
+  function KeyHandlerDown(e)
+  {
+    e = e || event;
+    if (locked) { return false; }
+    var key = GetKey(e);
+    if (el.value == '') { el.value = format; SetTextCursor(el,0); }
+    // Only do update for bksp del
+    if (key == 'bksp' || key == 'del') { Update(key); return false; }
+    else if (key == 'etc' || key == 'shift') { return true; }
+    else { return true; }    
+  }
+  function KeyHandlerPress(e)
+  {
+    e = e || event;
+    if (locked) { return false; }
+    var key = GetKey(e);
+    // Check if modifier key is being pressed; command
+    if (key=='etc' || e.metaKey || e.ctrlKey || e.altKey) { return true; }
+    if (key != 'bksp' && key != 'del' && key != 'etc' && key != 'shift') {
+      if (!GoodOnes(key)) { return false; }
+      return Update(key);
+    }
+    else { return false; }
+  }
+  function KeyHandlerUp(e) { hold = 0; }
+  function Update(key)
+  {
+    var p = GetTextCursor(el), c = el.value, val = '';
+    // Handle keys now
+    switch (true) {
+    case (allowed.indexOf(key) != -1):
+      if (++p > format.length) { return false; } // if text csor at end
+      // Handle cases where user places csor before separator
+      while (sep.indexOf(c.charAt(p-1)) != -1 && p <= format.length) { p++; }
+      val = c.substr(0, p-1) + key + c.substr(p);
+      // Move csor up a spot if next char is a separator char
+      if (allowed.indexOf(c.charAt(p)) == -1
+          && open.indexOf(c.charAt(p)) == -1) { p++; }
+      break;
+    case (key=='bksp'): // backspace
+      if (--p < 0) return false; // at start of field
+      // If previous char is a separator, move a little more
+      while (allowed.indexOf(c.charAt(p)) == -1
+             && open.indexOf(c.charAt(p)) == -1
+             && p > 1) { p--; }
+      val = c.substr(0, p) + format.substr(p,1) + c.substr(p+1);
+      break;
+    case (key=='del'): // forward delete
+      if (p >= c.length) { return false; } // at end of field
+      // If next char is a separator and not the end of the text field
+      while (sep.indexOf(c.charAt(p)) != -1
+             && c.charAt(p) != '') { p++; }
+      val = c.substr(0, p) + format.substr(p,1) + c.substr(p+1);
+      p++; // Move position forward
+      break;
+    case (key=='etc'): return true; // Catch other allowed chars
+    default: return false;   // Ignore the rest
+    }
+    el.value = '';        // blank it first (Firefox issue)
+    el.value = val;       // put updated value back in
+    SetTextCursor(el, p); // Set the text cursor
+    return false;
+  }
+  function GetTextCursor(node)
+  {
+    try {
+      if (node.selectionStart >= 0) { return node.selectionStart; }
+      else if (document.selection) {// IE
+        var ntxt = node.value; // getting starting text
+        var rng = document.selection.createRange();
+        rng.text = '|%|';
+        var start = node.value.indexOf('|%|');
+        rng.moveStart('character', -3);
+        rng.text = '';
+        // put starting text back in,
+        // fixes issue if all text was highlighted
+        node.value = ntxt;
+        return start;
+      } return -1;
+    } catch(e) { return false; }
+  }
+  function SetTextCursor(node, pos)
+  {
+    try {
+      if (node.selectionStart) {
+        node.focus();
+        node.setSelectionRange(pos,pos);
+      }
+      else if (node.createTextRange) { // IE
+        var rng = node.createTextRange();
+        rng.move('character', pos);
+        rng.select();
+      }
+    } catch(e) { return false; }
+  }
+  function GoodOnes(k)
+  {
+    if (allowed.indexOf(k) == -1 && k!='bksp' && k!='del' && k!='etc') {
+      var p = GetTextCursor(el); // Need to ensure cursor position not lost
+      locked = true; onbadkey();
+      // Hold lock long enough for onbadkey function to run
+      setTimeout(function(){locked=false; SetTextCursor(el,p);}, badwait);
+      return false;
+    } return true;
+  }
+  function resetField() {
+    el.value = format;
+  }
+  function setAllowed(a) {
+    allowed = a;
+    resetField();
+  }
+  function setFormat(f) {
+    format = f;
+    resetField();
+  }
+  function setSeparator(s) {
+    sep = s;
+    resetField();
+  }
+  function setTypeon(t) {
+    open = t;
+    resetField();
+  }
+  return {
+    resetField:resetField,
+    setAllowed:setAllowed,
+    setFormat:setFormat,
+    setSeparator:setSeparator,
+    setTypeon:setTypeon
+  }
+}
diff --git a/httemplate/elements/tr-input-mac_addr.html b/httemplate/elements/tr-input-mac_addr.html
new file mode 100644 (file)
index 0000000..d768d4e
--- /dev/null
@@ -0,0 +1,11 @@
+<& /elements/tr-input-mask.html, 
+  format => '__:__:__:__:__:__',
+  allowed => '0123456789ABCDEFabcdef',
+  %opt,
+&>
+<%init>
+my %opt = @_;
+my $value = length($opt{curr_value}) ? $opt{curr_value} : $opt{value};
+$value =~ s/\W//g;
+$opt{curr_value} = join(':', $value =~ /../g);
+</%init>
diff --git a/httemplate/elements/tr-input-mask.html b/httemplate/elements/tr-input-mask.html
new file mode 100644 (file)
index 0000000..33725b9
--- /dev/null
@@ -0,0 +1,41 @@
+% if ( !$init ) {
+<script type="text/javascript" src="<%$p%>elements/masked_input_1.1.js">
+</script>
+% $init++;
+% }
+<& /elements/tr-input-text.html, id => $id, @_ &>
+<script type="text/javascript">
+MaskedInput({
+  elm: document.getElementById('<%$id%>'),
+  format: '<% $opt{format} %>',
+  <% $opt{allowed} ? "allowed: '$opt{allowed}'," : '' %>
+  <% $opt{typeon}  ? "typeon:  '$opt{typeon}',"  : '' %>
+});
+document.getElementById('<%$id%>').value = <% $value |js_string %>;
+</script>
+<%shared>
+my $init = 0;
+</%shared>
+<%init>
+my %opt = @_;
+# must have a DOM id
+my $id = $opt{id} || sprintf('input%04d',int(rand(10000)));
+my $value = length($opt{curr_value}) ? $opt{curr_value} : $opt{value} || '';
+</%init>
+<%doc>
+Set up a text input field with input masking.
+
+<& /elements/tr-input-mask.html,
+  format    => '____-__-__',
+  #typeon   => '_YMDhms',    # which characters in the format represent blanks
+  #allowed  => '0123456789', # characters allowed in the blanks
+  ... all other options as for tr-input-text.html
+&>
+
+Note that the value sent on form submission will contain the mask 
+separators, and if value/curr_value is passed, it should also be 
+formatted to fit the mask.
+
+Uses masked_input_1.1.js by Kendall Conrad, available under a Creative Commons
+Attribution-ShareAlike license.
+</%doc>
index 45d1dac..ed8fe81 100644 (file)
@@ -1,14 +1,22 @@
 <script type="text/javascript">
-var auto_addr_routernum = <% encode_json(\%auto_addr_routernum) %>;
-function hide_if_auto_addr(obj, i) {
+var manual_addr_routernum = <% encode_json(\%manual_addr_routernum) %>;
+var ip_addr_curr_value = <% $opt{'ip_addr'} |js_string %>;
+function lock_ip_addr(obj, i) {
   var routernum = obj.value;
   var select_blocknum = document.getElementsByName('blocknum')[0];
-  var label_auto_addr = document.getElementById('label_auto_addr');
   var input_ip_addr = document.getElementById('input_ip_addr');
-  var auto = ( auto_addr_routernum[routernum] == 'Y' );
-  select_blocknum.style.display = auto ? '' : 'none';
-  label_auto_addr.style.display = auto ? '' : 'none';
-  input_ip_addr.style.display = !auto ? '' : 'none';
+  if ( manual_addr_routernum[routernum] == 'Y' ) {
+%# enable ip_addr, default it to its previous value, and hide block selection
+    select_blocknum.style.display = 'none';
+    input_ip_addr.value = ip_addr_curr_value;
+    input_ip_addr.disabled = false;
+  }
+  else {
+%# the reverse
+    select_blocknum.style.display = '';
+    input_ip_addr.disabled = true;
+    input_ip_addr.value = '(automatic)';
+  }
 }
 </script>
 <& /elements/tr-td-label.html, label => ($opt{'label'} || 'Router') &>
@@ -19,7 +27,7 @@ function hide_if_auto_addr(obj, i) {
     records   => \@routers,
     name_col  => 'routername',
     value_col => 'routernum',
-    onchange  => 'hide_if_auto_addr',
+    onchange  => 'lock_ip_addr',
     curr_value=> $opt{'routernum'},
   },
   {
@@ -44,17 +52,18 @@ function hide_if_auto_addr(obj, i) {
 % }
 % else {
   <input type="text" id="input_ip_addr" name="ip_addr" 
-    style="display:none" value="<% $opt{'ip_addr'} |h%>">
+  value="<% $opt{'ip_addr'} |h%>">
 % }
-  <span id="label_auto_addr"><% $opt{'ip_addr'} || '' %> 
-  <i>(automatic)</i></span>
 </td> </tr>
+<input type="hidden" name="prev_ip_addr" value="<% $opt{'ip_addr'} |h%>">
 <script type="text/javascript">
-hide_if_auto_addr(document.getElementsByName('routernum')[0],0);
+lock_ip_addr(document.getElementsByName('routernum')[0],0);
 </script>
 <%init>
+
 my %opt = @_;
 my @routers;
+my $conf = FS::Conf->new;
 
 my $svc_x = $opt{'object'};
 if ( $svc_x ) {
@@ -91,5 +100,17 @@ else {
   @routers = qsearch('router', {});
 }
 
-my %auto_addr_routernum = map { $_->routernum, $_->auto_addr } @routers;
+my %manual_addr_routernum = map { $_->routernum, $_->manual_addr } @routers;
+
+if ( $conf->exists('auto_router') ) {
+  # Then show an "(automatic)" router, with no blocks.  manual_addr is on
+  # so that the ip_addr field will be unlocked.
+  unshift @routers, FS::router->new({
+      'routernum'   => '',
+      'routername'  => '(automatic)',
+      'manual_addr' => 'Y',
+  });
+  $manual_addr_routernum{''} = 'Y';
+}
+
 </%init>
index c5faccf..2029fd4 100644 (file)
@@ -3,6 +3,16 @@
        'title'   => 'Event query - '.$part_event->event,
      }
 &>
+<FORM STYLE="display:inline" ACTION=<%$cgi->url%> METHOD="GET">
+When event is run on <& /elements/input-date-field.html, {
+  'name'    => 'date',
+  'value'   => $time,
+  'format'  => FS::Conf->new->config('date_format') || '%m/%d/%Y',
+} &>
+<INPUT TYPE="hidden" NAME="eventpart" VALUE="<%$eventpart%>">
+<INPUT TYPE="submit" VALUE="Refresh">
+</FORM>
+<BR><BR>
 % if ( $objects > 0 ) {
   <% emt("[quant,_1,$label]", $objects) %>
 %   if ( $part_event->eventtable ne 'cust_main' ) {
@@ -18,8 +28,8 @@
 
 %   my @rowcolors = ('ffffff','eeeeee');
 %   my $row = 0;
-  <TR style="background-color:#<% $rowcolors[$row++ % 2] %>">
 %   foreach my $object (@targets) {
+  <TR style="background-color:#<% $rowcolors[$row++ % 2] %>">
 %     # now works for all eventtables, including cust_pkg
 %     my $link = $p . 'view/' . $part_event->eventtable . '.cgi?' .
 %        $object->$pkey;
@@ -65,12 +75,14 @@ die "access denied"
   unless $curuser->access_right('Edit billing events')
         || $curuser->access_right('Edit global billing events');
 
-my ($eventpart) = $cgi->keywords;
+my ($eventpart) = $cgi->param('eventpart');
 $eventpart =~ /^\d+$/ or die 'illegal eventpart';
 
+my $time = parse_datetime($cgi->param('date')) || time;
+
 my $part_event = FS::part_event->by_key($eventpart)
   or die "Event definition $eventpart not found.\n";
-my @targets = $part_event->targets;
+my @targets = $part_event->targets('time' => $time);
 my $total = @targets;
 
 # in imitation of search/elements/search-html.html
index 961374e..131582f 100644 (file)
@@ -17,7 +17,6 @@ my %labels = map { $_ => ( ref($fields->{$_})
 #my %labels = ();
 
 $labels{'description'} = emt('Description');
-$labels{'router'} = emt('Router');
 $labels{'speed_down'} = emt('Download Speed');
 $labels{'speed_up'} = emt('Upload Speed');
 $labels{'ip_addr'} = emt('IP Address');
@@ -32,7 +31,7 @@ my @fields = (
   'speed_up',
   { field => 'ip_addr', value => \&ip_addr },
   { field => 'sectornum', value => \&sectornum },
-  'mac_addr',
+  { field => 'mac_addr', value => \&mac_addr },
   #'latitude',
   #'longitude',
   { field => 'coordinates', value => \&coordinates },
@@ -67,6 +66,11 @@ sub ip_addr {
   $out;
 }
 
+sub mac_addr {
+  my $svc = shift;
+  join(':', $svc->mac_addr =~ /../g);
+}
+
 sub usergroup {
   my $svc = shift;
   my $usergroup = $svc->usergroup;
index 1d88235..725358c 100644 (file)
@@ -6,6 +6,7 @@
 %>
 <%init>
 
+my $conf = new FS::Conf;
 my $fields = FS::svc_hardware->table_info->{'fields'};
 my %labels = map { $_ =>  ( ref($fields->{$_})
                              ? $fields->{$_}{'label'}
@@ -24,5 +25,22 @@ my $note =   { field => 'note',
                type  => 'text',
                value => sub { encode_entities($_[0]->note) }
              };
-my @fields = ($model, qw( serial hw_addr ip_addr smartcard ), $status, $note );
+my $hw_addr ={ field => 'hw_addr',
+               type  => 'text',
+               value => sub { 
+                my $hw_addr = $_[0]->hw_addr;
+                $conf->exists('svc_hardware-check_mac_addr') ?
+                  join(':', $hw_addr =~ /../g) : $hw_addr
+                },
+              };
+
+my @fields = (
+  $model,
+  'serial',
+  $hw_addr,
+  'ip_addr',
+  'smartcard',
+  $status,
+  $note,
+);
 </%init>