summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorivan <ivan>2006-07-12 00:20:23 +0000
committerivan <ivan>2006-07-12 00:20:23 +0000
commit1053db7f76169cbbc87840539959a4c362aff242 (patch)
tree1d1895ce43bb5910a8de5e3ead26b2e179ed268e
parenta8665e44dbd99bd864e48231928405a31cedce5f (diff)
svc_phone service and CDR billing from imported CDRs
-rw-r--r--FS/FS/AccessRight.pm2
-rw-r--r--FS/FS/Record.pm2
-rw-r--r--FS/FS/Schema.pm12
-rw-r--r--FS/FS/cdr.pm83
-rw-r--r--FS/FS/cust_svc.pm24
-rw-r--r--FS/FS/h_svc_phone.pm33
-rw-r--r--FS/FS/part_pkg/voip_cdr.pm146
-rw-r--r--FS/FS/part_svc.pm10
-rw-r--r--FS/FS/svc_phone.pm146
-rw-r--r--FS/MANIFEST3
-rw-r--r--htetc/handler.pl1
-rw-r--r--httemplate/edit/elements/edit.html68
-rw-r--r--httemplate/edit/elements/svc_Common.html98
-rwxr-xr-xhttemplate/edit/part_svc.cgi25
-rw-r--r--httemplate/edit/process/elements/process.html15
-rw-r--r--httemplate/edit/process/elements/svc_Common.html14
-rw-r--r--httemplate/edit/process/svc_phone.html4
-rw-r--r--httemplate/edit/svc_phone.cgi11
-rw-r--r--httemplate/elements/menu.html22
-rw-r--r--httemplate/misc/cdr-import.html3
-rw-r--r--httemplate/search/cdr.html6
-rw-r--r--httemplate/search/report_cdr.html2
-rw-r--r--httemplate/search/svc_phone.cgi94
-rw-r--r--httemplate/view/elements/svc_Common.html116
-rw-r--r--httemplate/view/svc_phone.cgi10
25 files changed, 831 insertions, 119 deletions
diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm
index f04779a..797a12a 100644
--- a/FS/FS/AccessRight.pm
+++ b/FS/FS/AccessRight.pm
@@ -128,6 +128,8 @@ assigned to users and/or groups.
'List packages',
'List services',
+ 'List rating data',
+
'Financial reports',
'Job queue', # these are not currently agent-virtualized
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index 9a99aeb..41e0eba 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -1727,7 +1727,7 @@ sub _quote {
( $nullable ? ' NULL' : ' NOT NULL' ).
")\n" if $DEBUG > 2;
- if ( $value eq '' && $column_type =~ /^int/ ) {
+ if ( $value eq '' && $column_type =~ /^(int|numeric)/ ) {
if ( $nullable ) {
'NULL';
} else {
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 7219274..3e1d68f 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1538,6 +1538,18 @@ sub tables_hashref {
'index' => [],
},
+ 'svc_phone' => {
+ 'columns' => [
+ 'svcnum', 'int', '', '', '', '',
+ 'countrycode', 'varchar', '', 3, '', '',
+ 'phonenum', 'varchar', '', 15, '', '', #12 ?
+ 'pin', 'varchar', 'NULL', $char_d, '', '',
+ ],
+ 'primary_key' => 'svcnum',
+ 'unique' => [],
+ 'index' => [ [ 'countrycode', 'phonenum' ] ],
+ },
+
};
#'new_table' => {
diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm
index 5eb0cf3..2f47170 100644
--- a/FS/FS/cdr.pm
+++ b/FS/FS/cdr.pm
@@ -4,6 +4,7 @@ use strict;
use vars qw( @ISA );
use Date::Parse;
use Date::Format;
+use Time::Local;
use FS::UID qw( dbh );
use FS::Record qw( qsearch qsearchs );
use FS::cdr_type;
@@ -224,6 +225,17 @@ sub check {
# ;
# return $error if $error;
+ $self->calldate( $self->startdate_sql )
+ if !$self->calldate && $self->startdate;
+
+ unless ( $self->charged_party ) {
+ if ( $self->dst =~ /^(\+?1)?8[02-8]{2}/ ) {
+ $self->charged_party($self->dst);
+ } else {
+ $self->charged_party($self->src);
+ }
+ }
+
#check the foreign keys even?
#do we want to outright *reject* the CDR?
my $error =
@@ -252,7 +264,7 @@ error, otherwise returns false.
sub set_status_and_rated_price {
my($self, $status, $rated_price) = @_;
- $self->status($status);
+ $self->freesidestatus($status);
$self->rated_price($rated_price);
$self->replace();
}
@@ -267,6 +279,20 @@ sub calldate_unix {
str2time(shift->calldate);
}
+=item startdate_sql
+
+Parses the startdate in UNIX timestamp format and returns a string in SQL
+format.
+
+=cut
+
+sub startdate_sql {
+ my($sec,$min,$hour,$mday,$mon,$year) = localtime(shift->startdate);
+ $mon++;
+ $year += 1900;
+ "$year-$mon-$mday $hour:$min:$sec";
+}
+
=item cdr_carrier
Returns the FS::cdr_carrier object associated with this CDR, or false if no
@@ -420,6 +446,8 @@ sub downstream_csv {
=cut
+my($tmp_mday, $tmp_mon, $tmp_year);
+
my %import_formats = (
'asterisk' => [
'accountcode',
@@ -465,7 +493,42 @@ my %import_formats = (
'quantity',
'carrierid',
'upstream_rateid',
- ]
+ ],
+ 'ams' => [
+
+ # Date
+ sub { my($cdr, $date) = @_;
+ $date =~ /^(\d{1,2})\/(\d{1,2})\/(\d\d(\d\d)?)$/
+ or die "unparsable date: $date"; #maybe we shouldn't die...
+ #$cdr->startdate( timelocal(0, 0, 0 ,$2, $1-1, $3) );
+ ($tmp_mday, $tmp_mon, $tmp_year) = ( $2, $1-1, $3 );
+ },
+
+ # Time
+ sub { my($cdr, $time) = @_;
+ #my($sec, $min, $hour, $mday, $mon, $year)= localtime($cdr->startdate);
+ $time =~ /^(\d{1,2}):(\d{1,2}):(\d{1,2})$/
+ or die "unparsable time: $time"; #maybe we shouldn't die...
+ #$cdr->startdate( timelocal($3, $2, $1 ,$mday, $mon, $year) );
+ $cdr->startdate(
+ timelocal($3, $2, $1 ,$tmp_mday, $tmp_mon, $tmp_year)
+ );
+ },
+
+ # Source_Number
+ 'src',
+
+ # Terminating_Number
+ 'dst',
+
+ # Duration
+ sub { my($cdr, $min) = @_;
+ my $sec = sprintf('%.0f', $min * 60 );
+ $cdr->billsec( $sec );
+ $cdr->duration( $sec );
+ },
+
+ ],
);
sub batch_import {
@@ -494,10 +557,20 @@ sub batch_import {
my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
-
+
+ if ( $format eq 'ams' ) { # and other formats with a header too?
+
+ }
+
+ my $body = 0;
my $line;
while ( defined($line=<$fh>) ) {
+ #skip header...
+ if ( ! $body++ && $format eq 'ams' && $line =~ /^[\w\, ]+$/ ) {
+ next;
+ }
+
$csv->parse($line) or do {
$dbh->rollback if $oldAutoCommit;
return "can't parse: ". $csv->error_input();
@@ -506,6 +579,10 @@ sub batch_import {
my @columns = $csv->fields();
#warn join('-',@columns);
+ if ( $format eq 'ams' ) {
+ @columns = map { s/^ +//; $_; } @columns;
+ }
+
my @later = ();
my %cdr =
map {
diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index e7afa77..8914e8c 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -13,6 +13,7 @@ use FS::svc_acct;
use FS::svc_domain;
use FS::svc_forward;
use FS::svc_broadband;
+use FS::svc_phone;
use FS::svc_external;
use FS::domain_record;
use FS::part_export;
@@ -277,6 +278,10 @@ Returns a list consisting of:
- The table name (i.e. svc_domain) for this service
- svcnum
+Usage example:
+
+ my($label, $value, $svcdb) = $cust_svc->label;
+
=cut
sub label {
@@ -315,6 +320,8 @@ sub _svc_label {
$tag = $domain_record->zone;
} elsif ( $svcdb eq 'svc_broadband' ) {
$tag = $svc_x->ip_addr;
+ } elsif ( $svcdb eq 'svc_phone' ) {
+ $tag = $svc_x->phonenum; #XXX format it better
} elsif ( $svcdb eq 'svc_external' ) {
my $conf = new FS::Conf;
if ( $conf->config('svc_external-display_type') eq 'artera_turbo' ) {
@@ -586,30 +593,29 @@ sub get_cdrs_for_update {
my $default_prefix = $options{'default_prefix'};
- #Currently CDRs are associated with svc_acct services via a DID in the
- #username. This part is rather tenative and still subject to change...
- #return () unless $self->svc_x->isa('FS::svc_acct');
- return () unless $self->part_svc->svcdb eq 'svc_acct';
- my $number = $self->svc_x->username;
+ #CDRs are now associated with svc_phone services via svc_phone.phonenum
+ #return () unless $self->svc_x->isa('FS::svc_phone');
+ return () unless $self->part_svc->svcdb eq 'svc_phone';
+ my $number = $self->svc_x->phonenum;
my @cdrs =
- qsearch(
+ qsearch( {
'table' => 'cdr',
'hashref' => { 'freesidestatus' => '',
'charged_party' => $number
},
'extra_sql' => 'FOR UPDATE',
- );
+ } );
if ( length($default_prefix) ) {
push @cdrs,
- qsearch(
+ qsearch( {
'table' => 'cdr',
'hashref' => { 'freesidestatus' => '',
'charged_party' => "$default_prefix$number",
},
'extra_sql' => 'FOR UPDATE',
- );
+ } );
}
@cdrs;
diff --git a/FS/FS/h_svc_phone.pm b/FS/FS/h_svc_phone.pm
new file mode 100644
index 0000000..95898c7
--- /dev/null
+++ b/FS/FS/h_svc_phone.pm
@@ -0,0 +1,33 @@
+package FS::h_svc_phone;
+
+use strict;
+use vars qw( @ISA );
+use FS::h_Common;
+use FS::svc_phone;
+
+@ISA = qw( FS::h_Common FS::svc_phone );
+
+sub table { 'h_svc_phone' };
+
+=head1 NAME
+
+FS::h_svc_phone - Historical phone number objects
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+An FS::h_svc_phone object represents a historical phone number.
+FS::h_svc_phone inherits from FS::h_Common and FS::svc_phone.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::h_Common>, L<FS::svc_phone>, L<FS::Record>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm
index 15af77b..500a1b0 100644
--- a/FS/FS/part_pkg/voip_cdr.pm
+++ b/FS/FS/part_pkg/voip_cdr.pm
@@ -102,9 +102,8 @@ sub calc_recur {
my $downstream_cdr = '';
- # also look for a specific domain??? (username@telephonedomain)
foreach my $cust_svc (
- grep { $_->part_svc->svcdb eq 'svc_acct' } $cust_pkg->cust_svc
+ grep { $_->part_svc->svcdb eq 'svc_phone' } $cust_pkg->cust_svc
) {
foreach my $cdr (
@@ -125,78 +124,85 @@ sub calc_recur {
)
{
- die "rating_method 'prefix' not yet supported";
-
-# ###
-# # look up rate details based on called station id
-# ###
-#
-# my $dest = $cdr->dst;
-#
-# #remove non-phone# stuff and whitespace
-# $dest =~ s/\s//g;
+ ###
+ # look up rate details based on called station id
+ # (or calling station id for toll free calls)
+ ###
+
+ my( $to_or_from, $number );
+ if ( $cdr->dst =~ /^(\+?1)?8[02-8]{2}/ ) { #tollfree call
+ $to_or_from = 'from';
+ $number = $cdr->src;
+ } else { #regular call
+ $to_or_from = 'to';
+ $number = $cdr->dst;
+ }
+
+ #remove non-phone# stuff and whitespace
+ $number =~ s/\s//g;
# my $proto = '';
# $dest =~ s/^(\w+):// and $proto = $1; #sip:
# my $siphost = '';
# $dest =~ s/\@(.*)$// and $siphost = $1; # @10.54.32.1, @sip.example.com
-#
-# #determine the country code
-# my $countrycode;
-# if ( $dest =~ /^011(((\d)(\d))(\d))(\d+)$/
-# || $dest =~ /^\+(((\d)(\d))(\d))(\d+)$/
-# )
-# {
-#
-# my( $three, $two, $one, $u1, $u2, $rest ) = ( $1,$2,$3,$4,$5,$6 );
-# #first look for 1 digit country code
-# if ( qsearch('rate_prefix', { 'countrycode' => $one } ) ) {
-# $countrycode = $one;
-# $dest = $u1.$u2.$rest;
-# } elsif ( qsearch('rate_prefix', { 'countrycode' => $two } ) ) { #or 2
-# $countrycode = $two;
-# $dest = $u2.$rest;
-# } else { #3 digit country code
-# $countrycode = $three;
-# $dest = $rest;
-# }
-#
-# } else {
-# $countrycode = '1';
-# $dest =~ s/^1//;# if length($dest) > 10;
-# }
-#
-# warn "rating call to +$countrycode $dest\n" if $DEBUG;
-# $pretty_destnum = "+$countrycode $dest";
-#
-# #find a rate prefix, first look at most specific (4 digits) then 3, etc.,
-# # finally trying the country code only
-# my $rate_prefix = '';
-# for my $len ( reverse(1..6) ) {
-# $rate_prefix = qsearchs('rate_prefix', {
-# 'countrycode' => $countrycode,
-# #'npa' => { op=> 'LIKE', value=> substr($dest, 0, $len) }
-# 'npa' => substr($dest, 0, $len),
-# } ) and last;
-# }
-# $rate_prefix ||= qsearchs('rate_prefix', {
-# 'countrycode' => $countrycode,
-# 'npa' => '',
-# });
-#
-# die "Can't find rate for call to +$countrycode $dest\n"
-# unless $rate_prefix;
-#
-# $regionnum = $rate_prefix->regionnum;
-# $rate_detail = qsearchs('rate_detail', {
-# 'ratenum' => $ratenum,
-# 'dest_regionnum' => $regionnum,
-# } );
-#
-# $rate_region = $rate_prefix->rate_region;
-#
-# warn " found rate for regionnum $regionnum ".
-# "and rate detail $rate_detail\n"
-# if $DEBUG;
+
+ #determine the country code
+ my $countrycode;
+ if ( $number =~ /^011(((\d)(\d))(\d))(\d+)$/
+ || $number =~ /^\+(((\d)(\d))(\d))(\d+)$/
+ )
+ {
+
+ my( $three, $two, $one, $u1, $u2, $rest ) = ( $1,$2,$3,$4,$5,$6 );
+ #first look for 1 digit country code
+ if ( qsearch('rate_prefix', { 'countrycode' => $one } ) ) {
+ $countrycode = $one;
+ $number = $u1.$u2.$rest;
+ } elsif ( qsearch('rate_prefix', { 'countrycode' => $two } ) ) { #or 2
+ $countrycode = $two;
+ $number = $u2.$rest;
+ } else { #3 digit country code
+ $countrycode = $three;
+ $number = $rest;
+ }
+
+ } else {
+ $countrycode = '1';
+ $number =~ s/^1//;# if length($number) > 10;
+ }
+
+ warn "rating call $to_or_from +$countrycode $number\n" if $DEBUG;
+ $pretty_destnum = "+$countrycode $number";
+
+ #find a rate prefix, first look at most specific (4 digits) then 3, etc.,
+ # finally trying the country code only
+ my $rate_prefix = '';
+ for my $len ( reverse(1..6) ) {
+ $rate_prefix = qsearchs('rate_prefix', {
+ 'countrycode' => $countrycode,
+ #'npa' => { op=> 'LIKE', value=> substr($number, 0, $len) }
+ 'npa' => substr($number, 0, $len),
+ } ) and last;
+ }
+ $rate_prefix ||= qsearchs('rate_prefix', {
+ 'countrycode' => $countrycode,
+ 'npa' => '',
+ });
+
+ #
+ die "Can't find rate for call $to_or_from +$countrycode $\numbern"
+ unless $rate_prefix;
+
+ $regionnum = $rate_prefix->regionnum;
+ $rate_detail = qsearchs('rate_detail', {
+ 'ratenum' => $ratenum,
+ 'dest_regionnum' => $regionnum,
+ } );
+
+ $rate_region = $rate_prefix->rate_region;
+
+ warn " found rate for regionnum $regionnum ".
+ "and rate detail $rate_detail\n"
+ if $DEBUG;
} elsif ( $self->option('rating_method') eq 'upstream' ) {
diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm
index 7f79194..2587347 100644
--- a/FS/FS/part_svc.pm
+++ b/FS/FS/part_svc.pm
@@ -347,7 +347,6 @@ and replace methods.
sub check {
my $self = shift;
- my $recref = $self->hashref;
my $error;
$error=
@@ -358,8 +357,9 @@ sub check {
;
return $error if $error;
- my @fields = eval { fields( $recref->{svcdb} ) }; #might die
- return "Unknown svcdb!" unless @fields;
+ my @fields = eval { fields( $self->svcdb ) }; #might die
+ return "Unknown svcdb: ". $self->svcdb. " (Error: $@)"
+ unless @fields;
$self->SUPER::check;
}
@@ -549,7 +549,9 @@ sub process {
@fields;
} grep defined( dbdef->table($_) ),
- qw( svc_acct svc_domain svc_forward svc_www svc_broadband )
+ qw( svc_acct svc_domain svc_forward svc_www svc_broadband
+ svc_phone svc_external
+ )
)
} );
diff --git a/FS/FS/svc_phone.pm b/FS/FS/svc_phone.pm
new file mode 100644
index 0000000..fca3369
--- /dev/null
+++ b/FS/FS/svc_phone.pm
@@ -0,0 +1,146 @@
+package FS::svc_phone;
+
+use strict;
+use vars qw( @ISA );
+#use FS::Record qw( qsearch qsearchs );
+use FS::svc_Common;
+
+@ISA = qw( FS::svc_Common );
+
+=head1 NAME
+
+FS::svc_phone - Object methods for svc_phone records
+
+=head1 SYNOPSIS
+
+ use FS::svc_phone;
+
+ $record = new FS::svc_phone \%hash;
+ $record = new FS::svc_phone { '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_phone object represents a phone number. FS::svc_phone inherits
+from FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item svcnum - primary key
+
+=item countrycode -
+
+=item phonenum -
+
+=item pin -
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new phone number. To add the number 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
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'svc_phone'; }
+
+=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 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 phone number. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('svcnum')
+ || $self->ut_numbern('countrycode')
+ || $self->ut_number('phonenum')
+ || $self->ut_numbern('pin')
+ ;
+ return $error if $error;
+
+ $self->countrycode(1) unless $self->countrycode;
+
+ $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;
+
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 3c315fc..42b6165 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -347,3 +347,6 @@ FS/ConfDefaults.pm
t/ConfDefaults.t
FS/m2name_Common.pm
FS/CurrentUser.pm
+FS/svc_phone.pm
+t/svc_phone.t
+FS/h_svc_phone.pm
diff --git a/htetc/handler.pl b/htetc/handler.pl
index 1dfa137..ef3aed7 100644
--- a/htetc/handler.pl
+++ b/htetc/handler.pl
@@ -186,6 +186,7 @@ sub handler
use FS::access_groupagent;
use FS::access_right;
use FS::AccessRight;
+ use FS::svc_phone;
if ( %%%RT_ENABLED%%% ) {
eval '
diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html
index 6fa2b3b..f79cc0b 100644
--- a/httemplate/edit/elements/edit.html
+++ b/httemplate/edit/elements/edit.html
@@ -9,25 +9,35 @@
# 'column' => 'Label',
# }
#
- # listref - each item is a literal column name (or method) or (notyet) coderef
+ # listref - each item is a literal column name (or method) or hashref
+ # or (notyet) coderef
# if not specified all columns (except for the primary key) will be editable
# 'fields' => [
+ # 'columname',
+ # { 'field' => 'another_columname',
+ # 'type' => 'text', #text, fixed, hidden
+ # },
# ]
#
# 'menubar' => '', #menubar arrayref
#
# #run when re-displaying with an error
- # 'error_callback' => sub { my $cgi, $object = @_; },
+ # 'error_callback' => sub { my( $cgi, $object ) = @_; },
#
# #run when editing
- # 'edit_callback' => sub { my $cgi, $object = @_; },
+ # 'edit_callback' => sub { my( $cgi, $object ) = @_; },
+ #
+ # # returns a hashref for the new object
+ # 'new_hashref_callback'
#
# #run when adding
- # 'new_callback' => sub { my $cgi, $object = @_; },
+ # 'new_callback' => sub { my( $cgi, $object ) = @_; },
+ #
+ # #XXX describe
+ # 'field_callback' => sub { },
#
- # #uninmplemented
- # #'html_table_bottom' => '', #string or listref of additinal HTML to
- # # #add before </TABLE>
+ # #string or coderef of additional HTML to add before </TABLE>
+ # 'html_table_bottom' => '',
#
# 'viewall_dir' => '', #'search' or 'browse', defaults to 'search'
#
@@ -64,13 +74,19 @@
my( $query ) = $cgi->keywords;
$query =~ /^(\d+)$/;
$object = qsearchs( $table, { $pkey => $1 } );
+ warn "$table $pkey => $1"
+ if $opt{'debug'};
&{$opt{'edit_callback'}}($cgi, $object)
if $opt{'edit_callback'};
} else { #adding
- $object = $class->new( {} );
+ my $hashref = $opt{'new_hashref_callback'}
+ ? &{$opt{'new_hashref_callback'}}
+ : {};
+
+ $object = $class->new( $hashref );
&{$opt{'new_callback'}}($cgi, $object)
if $opt{'new_callback'};
@@ -113,16 +129,15 @@
<%= ntable("#cccccc",2) %>
-<% foreach my $f ( @$fields ) {
+<% foreach my $f ( map { ref($_) ? $_ : {'field'=>$_} }
+ @$fields
+ ) {
- my( $field, $type);
- if ( ref($f) ) {
- $field = $f->{'field'},
- $type = $f->{'type'} || 'text',
- } else {
- $field = $f;
- $type = 'text';
- }
+ &{ $opt{'field_callback'} }( $f )
+ if $opt{'field_callback'};
+
+ my $field = $f->{'field'};
+ my $type = $f->{'type'} ||= 'text';
%>
@@ -137,16 +152,29 @@
<%
#eventually more options for <SELECT>, etc. fields
+ if ( $type eq 'fixed' ) {
%>
- <TD>
- <INPUT TYPE="<%= $type %>" NAME="<%= $field %>" VALUE="<%= $object->$field() %>">
- <TD>
+ <TD BGCOLOR="#dddddd"><%= $f->{'value'} %></TD>
+ <INPUT TYPE="hidden" NAME="<%= $field %>" VALUE="<%= $f->{'value'} %>">
+
+ <% } else { %>
+
+ <TD>
+ <INPUT TYPE="<%= $type %>" NAME="<%= $field %>" VALUE="<%= $object->$field() %>">
+ <TD>
+
+ <% } %>
</TR>
<% } %>
+<%= ref( $opt{'html_table_bottom'} )
+ ? &{ $opt{'html_table_bottom'} }( $object )
+ : $opt{'html_table_bottom'}
+%>
+
</TABLE>
<%= ref( $opt{'html_bottom'} )
diff --git a/httemplate/edit/elements/svc_Common.html b/httemplate/edit/elements/svc_Common.html
new file mode 100644
index 0000000..c113ad6
--- /dev/null
+++ b/httemplate/edit/elements/svc_Common.html
@@ -0,0 +1,98 @@
+<%
+
+ my %opt = @_;
+
+ #my( $svcnum, $pkgnum, $svcpart, $part_svc );
+ my( $pkgnum, $svcpart, $part_svc );
+
+ #get & untaint pkgnum & svcpart
+ my($query) = $cgi->keywords; #they're not proper cgi params
+ if ( $query =~ /^pkgnum(\d+)-svcpart(\d+)$/ ) {
+ $pkgnum = $1;
+ $svcpart = $2;
+ $cgi->delete_all(); #so the standard edit.html treats this correctly as new
+ }
+
+%><%= include( 'edit.html',
+
+ 'menubar' => [],
+
+ 'error_callback' => sub {
+ my( $cgi, $svc_x ) = @_;
+ #$svcnum = $svc_x->svcnum;
+ $pkgnum = $cgi->param('pkgnum');
+ $svcpart = $cgi->param('svcpart');
+
+ $part_svc = qsearchs( 'part_svc', { svcpart=>$svcpart });
+ die "No part_svc entry!" unless $part_svc;
+ },
+
+ 'edit_callback' => sub {
+ my( $cgi, $svc_x ) = @_;
+ #$svcnum = $svc_x->svcnum;
+ my $cust_svc = $svc_x->cust_svc
+ 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;
+ },
+
+ 'new_hash_callback' => sub {
+ #my( $cgi, $svc_x ) = @_;
+
+ { svcpart => $svcpart };
+
+ },
+
+ 'new_callback' => sub {
+ my( $cgi, $svc_x ) = @_;;
+
+ $part_svc = qsearchs( 'part_svc', { svcpart=>$svcpart });
+ die "No part_svc entry!" unless $part_svc;
+
+ #$svcnum='';
+
+ $svc_x->set_default_and_fixed;
+
+ },
+
+ 'field_callback' => sub {
+ my $f = shift;
+ my $columndef = $part_svc->part_svc_column($f->{'field'});
+ my $flag = $columndef->columnflag;
+ if ( $flag eq 'F' ) {
+ $f->{'type'} = 'fixed';
+ $f->{'value'} = $columndef->columnvalue;
+ }
+ },
+
+ 'html_table_bottom' => sub {
+ my $svc_x = shift;
+ my $html = '';
+ foreach my $field ($svc_x->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.
+ $html .=
+ $svc_x->pvf($field)->widget( 'HTML',
+ 'edit',
+ $svc_x->getfield($field)
+ );
+ }
+ }
+ $html;
+ },
+
+ 'html_bottom' => sub {
+ qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!.
+ qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!;
+ },
+
+ 'debug' => 1,
+
+ %opt #pass through/override params
+ )
+%>
diff --git a/httemplate/edit/part_svc.cgi b/httemplate/edit/part_svc.cgi
index 0298a54..489a233 100755
--- a/httemplate/edit/part_svc.cgi
+++ b/httemplate/edit/part_svc.cgi
@@ -39,11 +39,12 @@ Disable new orders <INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<%= $hashref-
<INPUT TYPE="hidden" NAME="svcpart" VALUE="<%= $hashref->{svcpart} %>">
<BR>
Service definitions are the templates for items you offer to your customers.
-<UL><LI>svc_acct - Accounts - anything with a username (Mailboxes, PPP accounts, shell accounts, etc.)
+<UL><LI>svc_acct - Accounts - anything with a username (Mailboxes, PPP accounts, shell accounts, RADIUS entries for broadband, etc.)
<LI>svc_domain - Domains
<LI>svc_forward - mail forwarding
<LI>svc_www - Virtual domain website
<LI>svc_broadband - Broadband/High-speed Internet service (always-on)
+ <LI>svc_phone - Customer phone numbers
<LI>svc_external - Externally-tracked service
<!-- <LI>svc_charge - One-time charges (Partially unimplemented)
<LI>svc_wo - Work orders (Partially unimplemented)
@@ -60,6 +61,7 @@ that field.
#pry need to eventually create stuff that's shared amount UIs
my $conf = new FS::Conf;
my %defs = (
+
'svc_acct' => {
'dir' => 'Home directory',
'uid' => 'UID (set to fixed and blank for no UIDs)',
@@ -111,14 +113,17 @@ my %defs = (
disable_inventory => 1,
},
},
+
'svc_domain' => {
'domain' => 'Domain',
},
+
'svc_forward' => {
'srcsvc' => 'service from which mail is to be forwarded',
'dstsvc' => 'service to which mail is to be forwarded',
'dst' => 'someone@another.domain.com to use when dstsvc is 0',
},
+
# 'svc_charge' => {
# 'amount' => 'amount',
# },
@@ -126,20 +131,36 @@ my %defs = (
# 'worker' => 'Worker',
# '_date' => 'Date',
# },
+
'svc_www' => {
#'recnum' => '',
#'usersvc' => '',
},
+
'svc_broadband' => {
'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
'ip_addr' => 'IP address. Leave blank for automatic assignment.',
'blocknum' => 'Address block.',
},
+
+ 'svc_phone' => {
+ 'countrycode' => { desc => 'Country code',
+ type => 'text',
+ disable_inventory => 1,
+ },
+ 'phonenum' => 'Phone number',
+ 'pin' => { desc => 'Personal Identification Number',
+ type => 'text',
+ disable_inventory => 1,
+ },
+ },
+
'svc_external' => {
#'id' => '',
#'title' => '',
},
+
);
my %vfields;
@@ -195,7 +216,7 @@ my %defs = (
my @dbs = $hashref->{svcdb}
? ( $hashref->{svcdb} )
- : qw( svc_acct svc_domain svc_forward svc_www svc_broadband svc_external );
+ : qw( svc_acct svc_domain svc_forward svc_www svc_broadband svc_phone svc_external );
tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } grep dbdef->table($_), @dbs;
my $widget = new HTML::Widgets::SelectLayers(
diff --git a/httemplate/edit/process/elements/process.html b/httemplate/edit/process/elements/process.html
index a6e3b50..7cae78b 100644
--- a/httemplate/edit/process/elements/process.html
+++ b/httemplate/edit/process/elements/process.html
@@ -5,6 +5,7 @@
###
##req
##
+ #
# 'table' =>
#
# #? 'primary_key' => #required when the dbdef doesn't know...???
@@ -13,7 +14,14 @@
###
##opt
###
+ #
# 'viewall_dir' => '', #'search' or 'browse', defaults to 'search'
+ # OR
+ # 'redirect' => 'view/table.cgi?', # value of primary key is appended
+ #
+ # 'edit_ext' => 'html', #defaults to 'html', you might want 'cgi' while the
+ # #naming is still inconsistent
+ #
# 'process_m2m' => { 'link_table' => 'link_table_name',
# 'target_table' => 'target_table_name',
# },
@@ -65,9 +73,14 @@
);
}
+ # XXX print?!?!
+
if ( $error ) {
$cgi->param('error', $error);
- print $cgi->redirect(popurl(2). "$table.html?". $cgi->query_string );
+ my $edit_ext = $opt{'edit_ext'} || 'html';
+ print $cgi->redirect(popurl(2). "$table.$edit_ext?". $cgi->query_string );
+ } elsif ( $opt{'redirect'} ) {
+ print $cgi->redirect( $opt{'redirect'}. $pkeyvalue );
} else {
print $cgi->redirect( popurl(3).
( $opt{'viewall_dir'} || 'search' ).
diff --git a/httemplate/edit/process/elements/svc_Common.html b/httemplate/edit/process/elements/svc_Common.html
new file mode 100644
index 0000000..1f8f831
--- /dev/null
+++ b/httemplate/edit/process/elements/svc_Common.html
@@ -0,0 +1,14 @@
+<%
+
+ my %opt = @_;
+ my $table = $opt{'table'};
+ $opt{'fields'} ||= [ fields($table) ];
+ push @{ $opt{'fields'} }, qw( pkgnum svcpart );
+
+%><%= include( 'process.html',
+ 'edit_ext' => 'cgi',
+ 'redirect' => popurl(3)."view/$table.cgi?",
+ %opt,
+ )
+%>
+
diff --git a/httemplate/edit/process/svc_phone.html b/httemplate/edit/process/svc_phone.html
new file mode 100644
index 0000000..c1d4b75
--- /dev/null
+++ b/httemplate/edit/process/svc_phone.html
@@ -0,0 +1,4 @@
+<%= include( 'elements/svc_Common.html',
+ 'table' => 'svc_phone',
+ )
+%>
diff --git a/httemplate/edit/svc_phone.cgi b/httemplate/edit/svc_phone.cgi
new file mode 100644
index 0000000..77b4975
--- /dev/null
+++ b/httemplate/edit/svc_phone.cgi
@@ -0,0 +1,11 @@
+<%= include( 'elements/svc_Common.html',
+ 'name' => 'Phone number',
+ 'table' => 'svc_phone',
+ 'fields' => [qw( countrycode phonenum )], #pin
+ 'labels' => {
+ 'countrycode' => 'Country code',
+ 'phonenum' => 'Phone number',
+ 'pin' => 'PIN',
+ },
+ )
+%>
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index 05db0f6..8c62d97 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -69,6 +69,10 @@
#'Unlinked domain' => [ $fsurl.'search/svc_acct.cgi?UN_uid', 'Pre-Freeside domains without a customer record' ],
;
+ tie my %report_services_phone, 'Tie::IxHash',
+ 'All phone numbers' => [ $fsurl.'search/svc_phone.cgi?svcnum', '' ],
+ ;
+
tie my %report_services_external, 'Tie::IxHash',
'All external services' => [ $fsurl.'search/svc_external.cgi?id', '' ],
;
@@ -85,6 +89,7 @@
$report_services{'Mail forwards'} = [ \%report_services_forward, 'Mail forwards', ];
$report_services{'Virtual hosts'} = [ \%report_services_www, 'Virtual hosting', ];
$report_services{'Broadband services'} = [ \%report_services_broadband, 'Fixed (username-less) broadband services', ];
+ $report_services{'Phone numbers'} = [ \%report_services_phone, 'Telephone numbers', ];
$report_services{'External services'} = [ \%report_services_external, 'External services', ];
tie my %report_packages, 'Tie::IxHash';
@@ -97,6 +102,10 @@
$report_packages{'Customer packages with unconfigured services'} = [ $fsurl.'search/cust_pkg.cgi?APKG_pkgnum', 'List packages which have provisionable services' ];
$report_packages{'Advanced package reports'} = [ $fsurl.'search/report_cust_pkg.html', 'by agent, date range, status, package definition' ];
+ tie my %report_rating, 'Tie::IxHash',
+ 'Call Detail Records (CDRs)' => [ $fsurl.'search/report_cdr.html', '' ],
+ ;
+
tie my %report_financial, 'Tie::IxHash',
'Sales, Credits and Receipts' => [ $fsurl.'graph/report_money_time.html', 'Sales, credits and receipts summary graph' ],
'Sales Report' => [ $fsurl.'graph/report_cust_bill_pkg.html', 'Sales report and graph (by agent, package class and/or date range)' ],
@@ -108,20 +117,23 @@
;
tie my %report_menu, 'Tie::IxHash';
- $report_menu{'Customers'} = [ \%report_customers, 'Customer reports' ]
+ $report_menu{'Customers'} = [ \%report_customers, 'Customer reports' ]
if $curuser->access_right('List customers');
- $report_menu{'Invoices'} = [ \%report_invoices, 'Invoice reports' ]
+ $report_menu{'Invoices'} = [ \%report_invoices, 'Invoice reports' ]
if $curuser->access_right('List invoices');
- $report_menu{'Packages'} = [ \%report_packages, 'Package reports' ]
+ $report_menu{'Packages'} = [ \%report_packages, 'Package reports' ]
if $curuser->access_right('List packages');
- $report_menu{'Services'} = [ \%report_services, 'Services reports' ]
+ $report_menu{'Services'} = [ \%report_services, 'Services reports' ]
if $curuser->access_right('List services');
- $report_menu{'Financial'} = [ \%report_financial, 'Financial reports' ]
+ $report_menu{'Rating data'} = [ \%report_rating, 'Rating reports' ]
+ if $curuser->access_right('List rating data');
+ $report_menu{'Financial'} = [ \%report_financial, 'Financial reports' ]
if $curuser->access_right('Financial reports');
tie my %tools_importing, 'Tie::IxHash',
'Import customers from CSV file' => [ $fsurl.'misc/cust_main-import.cgi', '' ],
'Import one-time charges from CSV file' => [ $fsurl.'misc/cust_main-import_charges.cgi', '' ],
+ 'Import Call Detail Records (CDRs) from CSV file' => [ $fsurl.'misc/cdr-import.html', '' ],
;
tie my %tools_exporting, 'Tie::IxHash',
diff --git a/httemplate/misc/cdr-import.html b/httemplate/misc/cdr-import.html
index dc17332..93de6e4 100644
--- a/httemplate/misc/cdr-import.html
+++ b/httemplate/misc/cdr-import.html
@@ -2,8 +2,9 @@
<FORM ACTION="process/cdr-import.html" METHOD="POST" ENCTYPE="multipart/form-data">
Import a CSV file containing Call Detail Records (CDRs).<BR><BR>
CDR Format: <SELECT NAME="format">
-<!-- <OPTION VALUE="asterisk">Asterisk</OPTION> -->
+<OPTION VALUE="asterisk">Asterisk (untested)</OPTION>
<OPTION VALUE="unitel">Unitel/RSLCOM</OPTION>
+<OPTION VALUE="ams">AMS</OPTION>
</SELECT><BR><BR>
Filename: <INPUT TYPE="file" NAME="csvfile"><BR><BR>
diff --git a/httemplate/search/cdr.html b/httemplate/search/cdr.html
index ec847e4..e3d6043 100644
--- a/httemplate/search/cdr.html
+++ b/httemplate/search/cdr.html
@@ -11,13 +11,15 @@ if ( $cgi->param('freesidestatus') eq 'NULL' ) {
my $title = "Unprocessed $title";
$hashref->{'freesidestatus'} = ''; # Record.pm will take care of it
- $count_query .= " AND ( freesidestatus IS NULL OR freesidestatus = '' )";
+ #$count_query .= " AND ( freesidestatus IS NULL OR freesidestatus = '' )";
+ $count_query .= " WHERE ( freesidestatus IS NULL OR freesidestatus = '' )";
} elsif ( $cgi->param('freesidestatus') =~ /^([\w ]+)$/ ) {
my $title = "Processed $title";
$hashref->{'freesidestatus'} = $1;
- $count_query .= " AND freesidestatus = '$1'";
+ #$count_query .= " AND freesidestatus = '$1'";
+ $count_query .= " WHERE freesidestatus = '$1'";
}
diff --git a/httemplate/search/report_cdr.html b/httemplate/search/report_cdr.html
index 924e28b..6febe6c 100644
--- a/httemplate/search/report_cdr.html
+++ b/httemplate/search/report_cdr.html
@@ -4,7 +4,7 @@
Status: <SELECT NAME="freesidestatus">
<OPTION VALUE="">(all)
<OPTION VALUE="NULL">unprocessed
- <OPTION VALUE="done"">processed
+ <OPTION VALUE="done">processed
</SELECT><BR>
<INPUT TYPE="submit" VALUE="Search Call Detail Records">
diff --git a/httemplate/search/svc_phone.cgi b/httemplate/search/svc_phone.cgi
new file mode 100644
index 0000000..a68a13e
--- /dev/null
+++ b/httemplate/search/svc_phone.cgi
@@ -0,0 +1,94 @@
+<%
+
+my $conf = new FS::Conf;
+
+my($query)=$cgi->keywords;
+$query ||= ''; #to avoid use of unitialized value errors
+
+my $orderby = 'ORDER BY svcnum';
+my %svc_phone = ();
+my @extra_sql = ();
+if ( $query eq 'svcnum' ) {
+ #$orderby = 'ORDER BY svcnum';
+} elsif ( $query eq 'phonenum' ) {
+ $orderby = 'ORDER BY phonenum';
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+ #$orderby = 'ORDER BY svcnum';
+ push @extra_sql, "svcpart = $1";
+} else {
+ $cgi->param('phonenum') =~ /^([\d\- ]+)$/;
+ ( $svc_phone{'phonenum'} = $1 ) =~ s/\D//g;
+}
+
+my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
+ ' LEFT JOIN part_svc USING ( svcpart ) '.
+ ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN cust_main USING ( custnum ) ';
+
+#here is the agent virtualization
+push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my $extra_sql = '';
+if ( @extra_sql ) {
+ $extra_sql = ( keys(%svc_phone) ? ' AND ' : ' WHERE ' ).
+ join(' AND ', @extra_sql );
+}
+
+my $count_query = "SELECT COUNT(*) FROM svc_phone $addl_from ";
+if ( keys %svc_phone ) {
+ $count_query .= ' WHERE '.
+ join(' AND ', map "$_ = ". dbh->quote($svc_phone{$_}),
+ keys %svc_phone
+ );
+}
+$count_query .= $extra_sql;
+
+my $sql_query = {
+ 'table' => 'svc_phone',
+ 'hashref' => \%svc_phone,
+ 'select' => join(', ',
+ 'svc_phone.*',
+ 'part_svc.svc',
+ 'cust_main.custnum',
+ FS::UI::Web::cust_sql_fields(),
+ ),
+ 'extra_sql' => "$extra_sql $orderby",
+ 'addl_from' => $addl_from,
+};
+
+my $link = [ "${p}view/svc_phone.cgi?", 'svcnum' ];
+
+#smaller false laziness w/svc_*.cgi here
+my $link_cust = sub {
+ my $svc_x = shift;
+ $svc_x->custnum ? [ "${p}view/cust_main.cgi?", 'custnum' ] : '';
+};
+
+%><%= include( 'elements/search.html',
+ 'title' => "Phone number search results",
+ 'name' => 'phone numbers',
+ 'query' => $sql_query,
+ 'count_query' => $count_query,
+ 'redirect' => $link,
+ 'header' => [ '#',
+ 'Service',
+ 'Country code',
+ 'Phone number',
+ FS::UI::Web::cust_header(),
+ ],
+ 'fields' => [ 'svcnum',
+ 'svc',
+ 'countrycode',
+ 'phonenum',
+ \&FS::UI::Web::cust_fields,
+ ],
+ 'links' => [ $link,
+ $link,
+ $link,
+ $link,
+ ( map { $link_cust }
+ FS::UI::Web::cust_header()
+ ),
+ ],
+ )
+%>
diff --git a/httemplate/view/elements/svc_Common.html b/httemplate/view/elements/svc_Common.html
new file mode 100644
index 0000000..0f103e3
--- /dev/null
+++ b/httemplate/view/elements/svc_Common.html
@@ -0,0 +1,116 @@
+<%
+
+ # options example...
+ #
+ # 'table' => 'svc_something'
+ #
+ # 'labels' => {
+ # 'column' => 'Label',
+ # },
+ #
+ # listref - each item is a literal column name (or method) or (notyet) coderef
+ # if not specified all columns (except for the primary key) will be viewable
+ # 'fields' => [
+ # ]
+
+ my(%opt) = @_;
+
+ my $table = $opt{'table'};
+
+ my $fields = $opt{'fields'}
+ #|| [ grep { $_ ne 'svcnum' } dbdef->table($table)->columns ];
+ || [ grep { $_ ne 'svcnum' } fields($table) ];
+
+ my($query) = $cgi->keywords;
+ $query =~ /^(\d+)$/;
+ my $svcnum = $1;
+ my $svc_x = qsearchs( $opt{'table'}, { 'svcnum' => $svcnum } )
+ or die "Unknown svcnum $svcnum in ". $opt{'table'}. " table\n";
+
+ my $cust_svc = $svc_x->cust_svc;
+ my($label, $value, $svcdb) = $cust_svc->label;
+
+ my $pkgnum = $cust_svc->pkgnum;
+
+ my($cust_pkg, $custnum);
+ if ($pkgnum) {
+ $cust_pkg = $cust_svc->cust_pkg;
+ $custnum = $cust_pkg->custnum;
+ } else {
+ $cust_pkg = '';
+ $custnum = '';
+ }
+
+%>
+
+<% if ( $custnum ) { %>
+
+ <%= include("/elements/header.html","View $label: $value", menubar(
+ "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+ )) %>
+
+ <%= include( '/elements/small_custview.html', $custnum, '', 1 ) %>
+ <BR>
+
+<% } else { %>
+
+ <SCRIPT>
+ function areyousure(href) {
+ if (confirm("Permanently delete this <%= $label %>?") == true)
+ window.location.href = href;
+ }
+ </SCRIPT>
+
+ <%= include("/elements/header.html","View $label: $value", menubar(
+ "Cancel this (unaudited) $label" =>
+ "javascript:areyousure(\'${p}misc/cancel-unaudited.cgi?$svcnum\')"
+ )) %>
+
+<% } %>
+
+Service #<B><%= $svcnum %></B>
+| <A HREF="<%=$p%>edit/<%= $opt{'table'} %>.cgi?<%=$svcnum%>">Edit this <%= $label %></A>
+<BR>
+
+<%= ntable("#cccccc") %><TR><TD><%= ntable("#cccccc",2) %>
+
+<% foreach my $f ( @$fields ) {
+
+ my( $field, $type);
+ if ( ref($f) ) {
+ $field = $f->{'field'},
+ $type = $f->{'type'} || 'text',
+ } else {
+ $field = $f;
+ $type = 'text';
+ }
+%>
+
+ <TR>
+ <TD ALIGN="right">
+ <%= ( $opt{labels} && exists $opt{labels}->{$field} )
+ ? $opt{labels}->{$field}
+ : $field
+ %>
+ </TD>
+
+ <%
+ #eventually more options for <SELECT>, etc. fields
+ %>
+
+ <TD BGCOLOR="#ffffff"><%= $svc_x->$field %><TD>
+
+ </TR>
+
+<% } %>
+
+<% foreach (sort { $a cmp $b } $svc_x->virtual_fields) { %>
+ <%= $svc_x->pvf($_)->widget('HTML', 'view', $svc_x->getfield($_)) %>
+<% } %>
+
+</TABLE></TD></TR></TABLE>
+
+<BR>
+<%= joblisting({'svcnum'=>$svcnum}, 1) %>
+
+<%= include('/elements/footer.html') %>
diff --git a/httemplate/view/svc_phone.cgi b/httemplate/view/svc_phone.cgi
new file mode 100644
index 0000000..8de7cc8
--- /dev/null
+++ b/httemplate/view/svc_phone.cgi
@@ -0,0 +1,10 @@
+<%= include('elements/svc_Common.html',
+ 'table' => 'svc_phone',
+ 'fields' => [qw( countrycode phonenum )], #pin
+ 'labels' => {
+ 'countrycode' => 'Country code',
+ 'phonenum' => 'Phone number',
+ 'pin' => 'PIN',
+ },
+ )
+%>