summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/Schema.pm9
-rw-r--r--FS/FS/hardware_class.pm6
-rw-r--r--FS/FS/hardware_type.pm14
-rw-r--r--httemplate/browse/hardware_class.html12
-rw-r--r--httemplate/edit/elements/svc_Common.html1
-rw-r--r--httemplate/edit/hardware_type.html4
-rw-r--r--httemplate/elements/select-hardware_type.html40
-rw-r--r--httemplate/elements/select-tiered.html191
-rwxr-xr-xhttemplate/search/report_svc_hardware.html23
-rw-r--r--httemplate/search/svc_hardware.cgi30
10 files changed, 292 insertions, 38 deletions
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 9de1b7f..483c5e0 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -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' => [ ],
},
diff --git a/FS/FS/hardware_class.pm b/FS/FS/hardware_class.pm
index 073a97f..791653b 100644
--- a/FS/FS/hardware_class.pm
+++ b/FS/FS/hardware_class.pm
@@ -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
diff --git a/FS/FS/hardware_type.pm b/FS/FS/hardware_type.pm
index ba19fcb..f19a3f6 100644
--- a/FS/FS/hardware_type.pm
+++ b/FS/FS/hardware_type.pm
@@ -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
diff --git a/httemplate/browse/hardware_class.html b/httemplate/browse/hardware_class.html
index aef0fa3..0bf314e 100644
--- a/httemplate/browse/hardware_class.html
+++ b/httemplate/browse/hardware_class.html
@@ -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;
diff --git a/httemplate/edit/elements/svc_Common.html b/httemplate/edit/elements/svc_Common.html
index 0955d49..38716f0 100644
--- a/httemplate/edit/elements/svc_Common.html
+++ b/httemplate/edit/elements/svc_Common.html
@@ -109,7 +109,6 @@
$f->{'hashref'} = {
'classnum'=>$columndef->columnvalue
};
- $f->{'empty_label'} = 'Select hardware type';
}
if ( $f->{'type'} eq 'select-svc_pbx'
diff --git a/httemplate/edit/hardware_type.html b/httemplate/edit/hardware_type.html
index 09a2724..7174401 100644
--- a/httemplate/edit/hardware_type.html
+++ b/httemplate/edit/hardware_type.html
@@ -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>
diff --git a/httemplate/elements/select-hardware_type.html b/httemplate/elements/select-hardware_type.html
index ae07798..126576d 100644
--- a/httemplate/elements/select-hardware_type.html
+++ b/httemplate/elements/select-hardware_type.html
@@ -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
index 0000000..35f9e5a
--- /dev/null
+++ b/httemplate/elements/select-tiered.html
@@ -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>
diff --git a/httemplate/search/report_svc_hardware.html b/httemplate/search/report_svc_hardware.html
index 07a6241..61ba4ab 100755
--- a/httemplate/search/report_svc_hardware.html
+++ b/httemplate/search/report_svc_hardware.html
@@ -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',
);
}
diff --git a/httemplate/search/svc_hardware.cgi b/httemplate/search/svc_hardware.cgi
index 2ff868e..7dd0058 100644
--- a/httemplate/search/svc_hardware.cgi
+++ b/httemplate/search/svc_hardware.cgi
@@ -7,6 +7,7 @@
'header' => [ '#',
'Service',
'Device type',
+ '', #revision
'Serial #',
'Hardware addr.',
'IP addr.',
@@ -16,21 +17,22 @@
'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' ];