From: ivan Date: Wed, 12 Jul 2006 00:20:23 +0000 (+0000) Subject: svc_phone service and CDR billing from imported CDRs X-Git-Tag: BEFORE_FINAL_MASONIZE~86 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=1053db7f76169cbbc87840539959a4c362aff242 svc_phone service and CDR billing from imported CDRs --- 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, L, L, 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 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). + +=item unsuspend + +Called by the unsuspend method of FS::cust_pkg (see L). + +=item cancel + +Called by the cancel method of FS::cust_pkg (see L). + +=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, L, L, L, +L, 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 + # #string or coderef of additional HTML to add before + # '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 - + <%= $f->{'value'} %> + + + <% } else { %> + + + + + + <% } %> <% } %> +<%= ref( $opt{'html_table_bottom'} ) + ? &{ $opt{'html_table_bottom'} }( $object ) + : $opt{'html_table_bottom'} +%> + <%= 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!!. + qq!!; + }, + + '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
Service definitions are the templates for items you offer to your customers. -
  • svc_acct - Accounts - anything with a username (Mailboxes, PPP accounts, shell accounts, etc.) +
    • svc_acct - Accounts - anything with a username (Mailboxes, PPP accounts, shell accounts, RADIUS entries for broadband, etc.)
    • svc_domain - Domains
    • svc_forward - mail forwarding
    • svc_www - Virtual domain website
    • svc_broadband - Broadband/High-speed Internet service (always-on) +
    • svc_phone - Customer phone numbers
    • svc_external - Externally-tracked service + +

      Filename:

      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:
      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 ) %> +
      + +<% } else { %> + + + + <%= include("/elements/header.html","View $label: $value", menubar( + "Cancel this (unaudited) $label" => + "javascript:areyousure(\'${p}misc/cancel-unaudited.cgi?$svcnum\')" + )) %> + +<% } %> + +Service #<%= $svcnum %> +| Edit this <%= $label %> +
      + +<%= ntable("#cccccc") %><%= 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'; + } +%> + + + + <%= ( $opt{labels} && exists $opt{labels}->{$field} ) + ? $opt{labels}->{$field} + : $field + %> + + + <% + #eventually more options for