svc_phone service and CDR billing from imported CDRs
authorivan <ivan>
Wed, 12 Jul 2006 00:20:23 +0000 (00:20 +0000)
committerivan <ivan>
Wed, 12 Jul 2006 00:20:23 +0000 (00:20 +0000)
25 files changed:
FS/FS/AccessRight.pm
FS/FS/Record.pm
FS/FS/Schema.pm
FS/FS/cdr.pm
FS/FS/cust_svc.pm
FS/FS/h_svc_phone.pm [new file with mode: 0644]
FS/FS/part_pkg/voip_cdr.pm
FS/FS/part_svc.pm
FS/FS/svc_phone.pm [new file with mode: 0644]
FS/MANIFEST
htetc/handler.pl
httemplate/edit/elements/edit.html
httemplate/edit/elements/svc_Common.html [new file with mode: 0644]
httemplate/edit/part_svc.cgi
httemplate/edit/process/elements/process.html
httemplate/edit/process/elements/svc_Common.html [new file with mode: 0644]
httemplate/edit/process/svc_phone.html [new file with mode: 0644]
httemplate/edit/svc_phone.cgi [new file with mode: 0644]
httemplate/elements/menu.html
httemplate/misc/cdr-import.html
httemplate/search/cdr.html
httemplate/search/report_cdr.html
httemplate/search/svc_phone.cgi [new file with mode: 0644]
httemplate/view/elements/svc_Common.html [new file with mode: 0644]
httemplate/view/svc_phone.cgi [new file with mode: 0644]

index f04779a..797a12a 100644 (file)
@@ -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
index 9a99aeb..41e0eba 100644 (file)
@@ -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 {
index 7219274..3e1d68f 100644 (file)
@@ -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' => {
index 5eb0cf3..2f47170 100644 (file)
@@ -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 {
index e7afa77..8914e8c 100644 (file)
@@ -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 (file)
index 0000000..95898c7
--- /dev/null
@@ -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;
+
index 15af77b..500a1b0 100644 (file)
@@ -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' ) {
 
index 7f79194..2587347 100644 (file)
@@ -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 (file)
index 0000000..fca3369
--- /dev/null
@@ -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;
+
index 3c315fc..42b6165 100644 (file)
@@ -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
index 1dfa137..ef3aed7 100644 (file)
@@ -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 '
index 6fa2b3b..f79cc0b 100644 (file)
@@ -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'
   #
     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'};
 
 <%= 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';
 
 %>
 
 
     <%
       #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 (file)
index 0000000..c113ad6
--- /dev/null
@@ -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
+             )
+%>
index 0298a54..489a233 100755 (executable)
@@ -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(
index a6e3b50..7cae78b 100644 (file)
@@ -5,6 +5,7 @@
   ###
   ##req
   ##
+  #
   # 'table' => 
   #
   # #? 'primary_key' => #required when the dbdef doesn't know...???
   ###
   ##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',
   #                  },
                                  );
   }
 
+  # 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 (file)
index 0000000..1f8f831
--- /dev/null
@@ -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 (file)
index 0000000..c1d4b75
--- /dev/null
@@ -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 (file)
index 0000000..77b4975
--- /dev/null
@@ -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',
+                             },
+           )
+%>
index 05db0f6..8c62d97 100644 (file)
     #'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';
   $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)' ],
   ;
 
   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',
index dc17332..93de6e4 100644 (file)
@@ -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>
index ec847e4..e3d6043 100644 (file)
@@ -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'";
 
 }
 
index 924e28b..6febe6c 100644 (file)
@@ -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 (file)
index 0000000..a68a13e
--- /dev/null
@@ -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 (file)
index 0000000..0f103e3
--- /dev/null
@@ -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 (file)
index 0000000..8de7cc8
--- /dev/null
@@ -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',
+                          },
+           )
+%>