svc_hardware revision number, #16266
authormark <mark>
Thu, 16 Feb 2012 02:13:00 +0000 (02:13 +0000)
committermark <mark>
Thu, 16 Feb 2012 02:13:00 +0000 (02:13 +0000)
FS/FS/Schema.pm
FS/FS/hardware_class.pm
FS/FS/hardware_type.pm
httemplate/browse/hardware_class.html
httemplate/edit/elements/svc_Common.html
httemplate/edit/hardware_type.html
httemplate/elements/select-hardware_type.html
httemplate/elements/select-tiered.html [new file with mode: 0644]
httemplate/search/report_svc_hardware.html
httemplate/search/svc_hardware.cgi

index 9de1b7f..483c5e0 100644 (file)
@@ -2126,12 +2126,13 @@ sub tables_hashref {
 
     'hardware_type' => {
       'columns' => [
-        'typenum',  'serial', '',      '', '', '',
-        'classnum',    'int', '',      '', '', '',
-        'model',   'varchar', '', $char_d, '', '',
+        'typenum',  'serial',     '',      '', '', '',
+        'classnum',    'int',     '',      '', '', '',
+        'model',   'varchar',     '', $char_d, '', '',
+        'revision','varchar', 'NULL', $char_d, '', '',
       ],
       'primary_key' => 'typenum',
-      'unique' => [ ],
+      'unique' => [ [ 'classnum', 'model', 'revision' ] ],
       'index'  => [ ],
     },
 
index 073a97f..791653b 100644 (file)
@@ -112,7 +112,11 @@ Returns all L<FS::hardware_type> objects belonging to this class.
 
 sub hardware_type {
   my $self = shift;
-  return qsearch('hardware_type', { 'classnum' => $self->classnum });
+  qsearch({
+      table   => 'hardware_type',
+      hashref => { 'classnum' => $self->classnum },
+      order_by=> 'ORDER BY model, revision',
+  })
 }
 
 =back
index ba19fcb..f19a3f6 100644 (file)
@@ -39,6 +39,8 @@ to which this device type belongs.
 
 =item model - descriptive model name or number
 
+=item revision - revision name/number, subordinate to model
+
 =back
 
 =head1 METHODS
@@ -102,6 +104,7 @@ sub check {
     $self->ut_numbern('typenum')
     || $self->ut_foreign_key('classnum', 'hardware_class', 'classnum')
     || $self->ut_text('model')
+    || $self->ut_textn('revision')
   ;
   return $error if $error;
 
@@ -119,6 +122,17 @@ sub hardware_class {
   return qsearchs('hardware_class', { 'classnum' => $self->classnum });
 }
 
+=item description
+
+Returns the model and revision number.
+
+=cut
+
+sub description {
+  my $self = shift;
+  $self->model . ($self->revision ? ' '.$self->revision : '');
+}
+
 =back
 
 =head1 SEE ALSO
index aef0fa3..0bf314e 100644 (file)
@@ -33,7 +33,17 @@ my $types_sub = sub {
   my $hardware_class = shift;
   my @rows = map { 
       my $type_link = $p.'edit/hardware_type.html?'.$_->typenum;
-      [ { 'data' => $_->model, 'link' => $type_link }, ] 
+      my $num_svcs = FS::svc_hardware->count("typenum = ".$_->typenum);
+      $num_svcs = $num_svcs > 0 ? 
+        mt('<B>[_1]</B> [numerate,_1,service]',$num_svcs) : '';
+      my $search_link = $p.'search/svc_hardware.cgi?typenum='.$_->typenum;
+
+      [ 
+        { 'data' => $_->model, 'link' => $type_link },
+        { 'data' => $_->revision, 'link' => $type_link },
+        { 'data' => $num_svcs, 'link' => $search_link, 'size' => -1 }
+      ]
+
     } $hardware_class->hardware_type;
 
   \@rows;
index 0955d49..38716f0 100644 (file)
                      $f->{'hashref'}     = {
                                             'classnum'=>$columndef->columnvalue
                                            };
-                     $f->{'empty_label'} = 'Select hardware type';
                    }
 
                    if (    $f->{'type'} eq 'select-svc_pbx'
index 09a2724..7174401 100644 (file)
@@ -6,6 +6,7 @@
                                'typenum'  => 'Type number',
                                'model'    => 'Device model',
                                'classnum' => 'Hardware class',
+                               'revision' => 'Revision',
                              },
                  'viewall_url' => $p.'browse/hardware_class.html',
            )
@@ -22,7 +23,8 @@ my @fields = (
     disable_empty => 1,
     name_col => 'classname',
   },
-  'model',
+  { field => 'model', size => 50, },
+  { field => 'revision', size => 50, },
 );
 
 </%init>
index ae07798..126576d 100644 (file)
@@ -1,14 +1,36 @@
-<% include( '/elements/select-table.html',
-                 'table'         => 'hardware_type',
-                 'name_col'      => 'model',
-                 'hashref'       => $hashref,
-                 %opt,
-             )
-%>
+<& /elements/select-tiered.html, tiers => [
+  {
+    field     => 'classnum',
+    table     => 'hardware_class',
+    hashref   => ($classnum ? { classnum => $classnum } : {}),
+    name_col  => 'classname',
+    empty_label => '(all)',
+  },
+  {
+    field     => 'model',
+    table     => 'hardware_type',
+    select    => 'classnum, model',
+    name_col  => 'model',
+    value_col => 'model',
+    link_col  => 'classnum',
+    hashref   => $hashref,
+    extra_sql => 'GROUP BY classnum, model',
+    empty_label => '(all)',
+  },
+  {
+    table     => 'hardware_type',
+    name_col  => 'revision',
+    value_col => 'typenum',
+    link_col  => 'model',
+    empty_label => $opt{'empty_label'},
+  },
+],
+  field => 'typenum',
+  %opt,
+&>
 <%init>
 my %opt = @_;
-my $classnum = delete $opt{'classnum'};
 my $hashref = $opt{'hashref'} || {};
-$hashref->{'classnum'} = $classnum if $classnum;
+my $classnum = $hashref->{classnum};
 
 </%init>
diff --git a/httemplate/elements/select-tiered.html b/httemplate/elements/select-tiered.html
new file mode 100644 (file)
index 0000000..35f9e5a
--- /dev/null
@@ -0,0 +1,191 @@
+<%doc>
+Usage:
+
+<& /elements/select-tiered.html,
+  tiers       => [
+    { table => 'table1', ... }, # most select-table options are supported
+    { table => 'table2', ..., link_col = 't2num' }, # foreign key in table1
+  ],
+  prefix      => '', # to avoid name conflicts
+  curr_value  => 42, # in the last table
+  field       => 'fieldname', # NAME attribute of the last element
+&>
+
+This creates a group of SELECT elements (similar to select-table.html) for
+drill-down navigation of data with one-to-many relationships.
+
+'tiers' is required, and must be an arrayref of hashes, each describing one
+tier of selection (from most general to most specific).  Each tier can 
+contain the following:
+- table, select, addl_from, hashref, extra_sql: as in FS::Record::qsearch.
+- records, an arrayref of exact records.  Either this or "table" must be 
+  provided.
+- field: the NAME attribute of the select element.  Optional.
+- name_col: the column/method name to obtain the record's text label in the 
+  select element.
+- value_col: the column/method name to obtain the record's value, which is
+  sent on form submission.  Defaults to the primary key.
+- link_col: the column/method name to associate the record to the value_col
+  of a record in the previous table's value_col.  (That is, the foreign key.)
+- empty_label: the label to use for an option with the logical meaning of 
+  "all of these" and a value of ''.
+- curr_value: the currently selected value.  This will constrain the current
+  values of preceding tiers.
+- multiple: set to true for a multiple-style selector.  This should work but 
+  isn't fully tested.
+- after: an HTML string to be inserted after the select element, before 
+  the next one.  By default there's nothing between them.
+
+For convenience, "curr_value" and "field" can be passed as part of the 
+main argument list, and will be applied to the last tier.
+
+</%doc>
+% $i = 0;
+% foreach my $tier (@$tiers) {
+%   my $onchange;
+%   $onchange="onchange='${pre}select_change(this, $i)'"
+%     if $i < scalar(@$tiers) - 1;
+<SELECT 
+  NAME="<% $tier->{field} %>"
+  ID="<% $pre."select_".$i %>"
+  <%$onchange%>
+  <% $tier->{multiple} ? 'MULTIPLE' : '' %>
+  >
+%   if ( $i == 0 ) {
+%     my $options = $tiers_by_key->[0]->{''};
+%     foreach ( sort keys %$options ) {
+  <OPTION VALUE="<%$_ |h%>"><% $options->{$_} |h%></OPTION>
+%     }
+%   }
+%   $i++;
+</SELECT>
+<% $tier->{after} %>
+% } #foreach $tier
+<SCRIPT type="text/javascript">
+% my $json = JSON->new->canonical; #sort
+var <% $pre %>tiers = <% $json->encode($tiers_by_key) %>;
+var <% $pre %>curr_values = <% $json->encode($curr_values) %>;
+function <% $pre %>select_change(select_this, i) {
+
+  i++; // operate on the next tier selector
+  var next_options = new Object; // use like a hash
+  // slight hack here: empty_label implies not multiple, so if the 'all'
+  // option is selected, it will be the "value" property of the select.
+  var all = (select_this.value == '');
+  // combine all of the options of this one
+  for (var j = 0; j < select_this.options.length; j++) {
+    var this_opt = select_this.options[j];
+    if ( this_opt.selected || all ) {
+      for (var next_key in <% $pre %>tiers[i][this_opt.value]) {
+        next_options[next_key] = <% $pre %>tiers[i][this_opt.value][next_key];
+      } // for next_key
+    } // if selected
+  } // for this_opt
+
+  var select_next = document.getElementById('<% $pre."select_" %>' + i);
+  select_next.options.length = 0; // clear it
+  for (var next_key in next_options) {
+    var o = document.createElement('OPTION');
+    o.value = next_key;
+    o.text = next_options[next_key];
+
+    if ( next_key == '' ) {
+      select_next.add(o, select_next.options[0]); //insert at top
+    } else {
+      select_next.add(o, null); //append
+    }
+    // then select it if we're selecting them all, or if it's the only one,
+    // or if it's the current value at that tier
+    o.selected = select_next.multiple
+              || (next_options.length == 1)
+              || (next_key == <% $pre %>curr_values[i])
+              ;
+  }
+  if ( i < <% scalar(@$tiers) - 1 %> ) {
+    <% $pre %>select_change(select_next, i);
+  }
+  return;
+}
+<% $pre %>select_change(document.getElementById('<% $pre %>select_0'), 0);
+</SCRIPT>
+<%init>
+my %opt = @_;
+my $pre = $opt{prefix} || '';
+my $tiers = $opt{tiers} or die "no tiers defined";
+
+my $i;
+for( $i = 0; $i < @$tiers; $i++ ) {
+  my $tier = $tiers->[$i];
+  my $key = $tier->{value_col};
+  my $name_col = $tier->{name_col};
+  if ( !exists($tier->{records}) ) {
+    # minor false laziness w/ select-table
+    my $dbdef_table = dbdef->table($tier->{table})
+      or die "can't find dbdef for ".$tier->{table}." table\n";
+    $key ||= $dbdef_table->primary_key;
+    my $hashref = $tier->{hashref} || {};
+    my $select = $tier->{select} || '*';
+    # we don't yet support agent_virt
+    $tier->{records} = [ qsearch({
+        'select'    => $select, # the real magic
+        'table'     => $tier->{table},
+        'addl_from' => $tier->{addl_from},
+        'hashref'   => $hashref,
+        'extra_sql' => $tier->{extra_sql},
+    }) ];
+  }
+
+  # set up options
+  my %children_of;
+  if ( $i == 0 ) {
+    $children_of{''} = {
+      map { $_->$key => $_->$name_col } @{ $tier->{records} }
+    };
+  }
+  else {
+    my $link_col = $tier->{link_col} 
+      or die "no link_col in '".$tier->{table}."' tier\n";
+    # %children_of maps the option values in the previous tier 
+    # to hashes of their linked options in this tier. 
+    foreach my $rec (@{ $tier->{records} }) {
+      $children_of{ $rec->$link_col } ||= {};
+      $children_of{ $rec->$link_col }->{ $rec->$key } = $rec->$name_col;
+    }
+  }
+
+  if ( defined $tier->{empty_label} ) {
+    foreach my $key (keys %children_of) {
+      # only create "all" options if there are multiple choices
+      if ( scalar(keys %{ $children_of{$key} }) > 1 ) {
+        $children_of{$key}->{''} = $tier->{empty_label};
+      }
+    }
+  }
+  $tier->{by_key} = \%children_of;
+}
+
+$i = scalar(@$tiers) - 1;
+$tiers->[$i]->{curr_value} ||= $opt{curr_value};
+$tiers->[$i]->{field} ||= $opt{field};
+
+# We expect the usual case to be $opt{curr_value}, i.e.
+# current value in the last tier.  So trace it backward.
+while($i >= 1) {
+  my $curr_value = $tiers->[$i]->{curr_value};
+  last if !defined($curr_value);
+
+  my $tier = $tiers->[$i];
+  foreach my $key ( %{ $tier->{by_key} } ) {
+    my $options = $tier->{by_key}->{$key};
+    if ( exists( $options->{$curr_value} ) ) {
+      warn "tier $i curr_value ($curr_value) found under key $key\n";
+      $tiers->[$i-1]->{curr_value} = $key;
+      last;
+    }
+  }
+  $i--;
+}
+
+my $tiers_by_key = [ map { $_->{by_key} } @$tiers ];
+my $curr_values = [ map { $_->{curr_value} || '' } @$tiers ];
+</%init>
index 07a6241..61ba4ab 100755 (executable)
@@ -7,15 +7,19 @@
       <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Search options</FONT></TH>
     </TR>
 
-    <TR><TD>
-    <% include('/elements/selectlayers.html',
-                  'field'           => 'classnum',
-                  'label'           => '',
-                  'options'         => \@classnums,
-                  'labels'          => \%class_labels,
-                  'layer_callback'  => \&layer_callback,
-                  'html_between'    => '</TD><TD>',
-              ) %>
+    <& /elements/tr-td-label.html, label => 'Device type' &>
+%#    <% include('/elements/selectlayers.html',
+%#                  'field'           => 'classnum',
+%#                  'label'           => '',
+%#                  'options'         => \@classnums,
+%#                  'labels'          => \%class_labels,
+%#                  'layer_callback'  => \&layer_callback,
+%#                  'html_between'    => '</TD><TD>',
+%#              ) %>
+    <TD>
+      <& /elements/select-hardware_type.html,
+          'empty_label' => '(all)'
+       &>
     </TD></TR>
 
     <% include('/elements/tr-input-text.html',
@@ -71,6 +75,7 @@ sub layer_callback {
   include('/elements/select-hardware_type.html',
               'field'       => 'classnum'.$classnum.'typenum',
               'classnum'    => $classnum,
+              'prefix'      => $classnum,
               'empty_label' => 'any',
           );
 }
index 2ff868e..7dd0058 100644 (file)
@@ -7,6 +7,7 @@
             'header'            => [ '#',
                                      'Service',
                                      'Device type',
+                                     '', #revision
                                      'Serial #',
                                      'Hardware addr.',
                                      'IP addr.',
             'fields'            => [ 'svcnum',
                                      'svc',
                                      'model',
+                                     'revision',
                                      'serial',
                                      'hw_addr',
                                      'ip_addr',
                                      'smartcard',
                                      \&FS::UI::Web::cust_fields,
                                    ],
-            'links'             => [ ($link_svc) x 7,
+            'links'             => [ ($link_svc) x 8,
                                      ( map { $_ ne 'Cust. Status' ? 
                                                 $link_cust : '' }
                                        FS::UI::Web::cust_header() )
                                    ],
-            'align'             => 'rllllll' . FS::UI::Web::cust_aligns(),
-            'color'             => [ ('') x 7,
+            'align'             => 'rlllllll' . FS::UI::Web::cust_aligns(),
+            'color'             => [ ('') x 8,
                                       FS::UI::Web::cust_colors() ],
-            'style'             => [ $svc_cancel_style, ('') x 6,
+            'style'             => [ $svc_cancel_style, ('') x 7,
                                       FS::UI::Web::cust_styles() ],
             )
 %>
@@ -39,7 +41,6 @@
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('List services');
 
-
 my $addl_from = '
  LEFT JOIN cust_svc  USING ( svcnum  )
  LEFT JOIN part_svc  USING ( svcpart )
@@ -66,9 +67,9 @@ if ( $cgi->param('hw_addr') =~ /^(\S+)$/ ) {
   push @extra_sql, "hw_addr LIKE '%$hw_addr%'";
 }
 
-my $ip = NetAddr::IP->new($cgi->param('ip_addr'));
-if ( $ip ) {
-  push @extra_sql, "ip_addr = '".lc($ip->addr)."'";
+if ( $cgi->param('ip_addr') ) {
+  my $ip = NetAddr::IP->new($cgi->param('ip_addr'));
+  push @extra_sql, "ip_addr = '".lc($ip->addr)."'" if $ip;
 }
 
 if ( lc($cgi->param('smartcard')) =~ /^(\w+)$/ ) {
@@ -81,9 +82,14 @@ if ( $cgi->param('statusnum') =~ /^(\d+)$/ ) {
 
 if ( $cgi->param('classnum') =~ /^(\d+)$/ ) {
   push @extra_sql, "hardware_type.classnum = $1";
-  if ( $cgi->param('classnum'.$1.'typenum') =~ /^(\d+)$/ ) {
-    push @extra_sql, "svc_hardware.typenum = $1";
-  }
+}
+
+if ( $cgi->param('model') =~ /^([\w\s]+)$/ ) {
+  push @extra_sql, "hardware_type.model = '$1'";
+}
+
+if ( $cgi->param('typenum') =~ /^(\d+)$/ ) {
+  push @extra_sql, "svc_hardware.typenum = $1";
 }
 
 if ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
@@ -103,6 +109,7 @@ my $sql_query = {
                     'part_svc.svc',
                     'cust_main.custnum',
                     'hardware_type.model',
+                    'hardware_type.revision',
                     'cust_pkg.cancel',
                     FS::UI::Web::cust_sql_fields(),
                  ),
@@ -111,7 +118,6 @@ my $sql_query = {
   'order_by'  => "ORDER BY $orderby",
   'addl_from' => $addl_from,
 };
-
 my $count_query = "SELECT COUNT(*) FROM svc_hardware $addl_from $extra_sql";
 my $link_svc = [ $p.'view/svc_hardware.cgi?', 'svcnum' ];
 my $link_cust = [ $p.'view/cust_main.cgi?', 'custnum' ];