diff options
author | ivan <ivan> | 2006-07-12 00:20:23 +0000 |
---|---|---|
committer | ivan <ivan> | 2006-07-12 00:20:23 +0000 |
commit | 1053db7f76169cbbc87840539959a4c362aff242 (patch) | |
tree | 1d1895ce43bb5910a8de5e3ead26b2e179ed268e | |
parent | a8665e44dbd99bd864e48231928405a31cedce5f (diff) |
svc_phone service and CDR billing from imported CDRs
-rw-r--r-- | FS/FS/AccessRight.pm | 2 | ||||
-rw-r--r-- | FS/FS/Record.pm | 2 | ||||
-rw-r--r-- | FS/FS/Schema.pm | 12 | ||||
-rw-r--r-- | FS/FS/cdr.pm | 83 | ||||
-rw-r--r-- | FS/FS/cust_svc.pm | 24 | ||||
-rw-r--r-- | FS/FS/h_svc_phone.pm | 33 | ||||
-rw-r--r-- | FS/FS/part_pkg/voip_cdr.pm | 146 | ||||
-rw-r--r-- | FS/FS/part_svc.pm | 10 | ||||
-rw-r--r-- | FS/FS/svc_phone.pm | 146 | ||||
-rw-r--r-- | FS/MANIFEST | 3 | ||||
-rw-r--r-- | htetc/handler.pl | 1 | ||||
-rw-r--r-- | httemplate/edit/elements/edit.html | 68 | ||||
-rw-r--r-- | httemplate/edit/elements/svc_Common.html | 98 | ||||
-rwxr-xr-x | httemplate/edit/part_svc.cgi | 25 | ||||
-rw-r--r-- | httemplate/edit/process/elements/process.html | 15 | ||||
-rw-r--r-- | httemplate/edit/process/elements/svc_Common.html | 14 | ||||
-rw-r--r-- | httemplate/edit/process/svc_phone.html | 4 | ||||
-rw-r--r-- | httemplate/edit/svc_phone.cgi | 11 | ||||
-rw-r--r-- | httemplate/elements/menu.html | 22 | ||||
-rw-r--r-- | httemplate/misc/cdr-import.html | 3 | ||||
-rw-r--r-- | httemplate/search/cdr.html | 6 | ||||
-rw-r--r-- | httemplate/search/report_cdr.html | 2 | ||||
-rw-r--r-- | httemplate/search/svc_phone.cgi | 94 | ||||
-rw-r--r-- | httemplate/view/elements/svc_Common.html | 116 | ||||
-rw-r--r-- | httemplate/view/svc_phone.cgi | 10 |
25 files changed, 831 insertions, 119 deletions
diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index f04779a07..797a12a4d 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 9a99aebe7..41e0eba51 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 7219274e6..3e1d68f1b 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 5eb0cf393..2f47170fb 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 e7afa77ea..8914e8c73 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 000000000..95898c7b0 --- /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 15af77b4f..500a1b0a4 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 7f791947a..258734735 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 000000000..fca33690d --- /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 3c315fc97..42b616575 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 1dfa1376e..ef3aed7e7 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 6fa2b3b6e..f79cc0b24 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 000000000..c113ad645 --- /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 0298a5461..489a2339c 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 a6e3b50e3..7cae78bfc 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 000000000..1f8f8315e --- /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 000000000..c1d4b7547 --- /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 000000000..77b4975a1 --- /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 05db0f659..8c62d9778 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 dc1733249..93de6e43f 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 ec847e41f..e3d6043e8 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 924e28bd7..6febe6c4a 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 000000000..a68a13e39 --- /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 000000000..0f103e3e0 --- /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 000000000..8de7cc8e7 --- /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', + }, + ) +%> |