From: Ivan Kohler Date: Thu, 25 Apr 2013 11:15:41 +0000 (-0700) Subject: Merge branch 'patch-18' of https://github.com/gjones2/Freeside X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=f3e0ac2b009c4edd5692cb587ff709dac2223ebe;hp=e006b70d3b27db3f607471852bbe13c0281d520e Merge branch 'patch-18' of https://github.com/gjones2/Freeside --- diff --git a/FS/FS.pm b/FS/FS.pm index 2d963b54f..d8bc33347 100644 --- a/FS/FS.pm +++ b/FS/FS.pm @@ -3,7 +3,7 @@ package FS; use strict; use vars qw($VERSION); -$VERSION = '3.0git'; +$VERSION = '3.1git'; #find missing entries in this file with: # for a in `ls *pm | cut -d. -f1`; do grep 'L' ../FS.pm >/dev/null || echo "missing $a" ; done @@ -231,6 +231,8 @@ L - Package class class L - Package definition class +L - Package definition localization class + L - Package definition link class L - Tax class class diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 66624e179..bfb39b4ad 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -162,6 +162,7 @@ tie my %rights, 'Tie::IxHash', 'Recharge customer service', #NEW 'Unprovision customer service', 'Change customer service', #NEWNEW + 'Edit password', 'Edit usage', #NEW 'Edit home dir', #NEW 'Edit www config', #NEW @@ -182,6 +183,7 @@ tie my %rights, 'Tie::IxHash', 'Unvoid invoices', 'Delete invoices', 'View customer tax exemptions', #yow + 'Edit customer tax exemptions', #NEWNEW 'Add customer tax adjustment', #new, but no need to phase in 'View customer batched payments', #NEW 'View customer pending payments', #NEW @@ -212,6 +214,7 @@ tie my %rights, 'Tie::IxHash', ### 'Customer credit and refund rights' => [ 'Post credit', + 'Credit line items', #NEWNEWNEW 'Apply credit', #NEWNEW { rightname=>'Unapply credit', desc=>'Enable "unapplication" of unclosed credits.' }, #aka unapplycredits { rightname=>'Delete credit', desc=>'Enable deletion of unclosed credits. Be very careful! Only delete credits that were data-entry errors, not adjustments.' }, #aka. deletecredits Optionally specify one or more comma-separated email addresses to be notified when a credit is deleted. @@ -293,6 +296,7 @@ tie my %rights, 'Tie::IxHash', 'Services: Hardware', 'Services: Hardware: Advanced search', 'Services: Phone numbers', + 'Services: Phone numbers: Advanced search', 'Services: PBXs', 'Services: Ports', 'Services: Mailing lists', @@ -301,6 +305,8 @@ tie my %rights, 'Tie::IxHash', 'Usage: Call Detail Records (CDRs)', 'Usage: Unrateable CDRs', 'Usage: Time worked', + { rightname=>'Employees: Commission Report', global=>1 }, + { rightname=>'Employees: Audit Report', global=>1 }, #{ rightname => 'List customers of all agents', global=>1 }, ], @@ -339,6 +345,8 @@ tie my %rights, 'Tie::IxHash', 'Edit package definitions', { rightname=>'Edit global package definitions', global=>1 }, + 'Bulk edit package definitions', + 'Edit billing events', { rightname=>'Edit global billing events', global=>1 }, diff --git a/FS/FS/ClientAPI/Bulk.pm b/FS/FS/ClientAPI/Bulk.pm deleted file mode 100644 index ec617df76..000000000 --- a/FS/FS/ClientAPI/Bulk.pm +++ /dev/null @@ -1,384 +0,0 @@ -package FS::ClientAPI::Bulk; - -use strict; - -use vars qw( $DEBUG $cache ); -use Date::Parse; -use FS::Record qw( qsearchs ); -use FS::Conf; -use FS::ClientAPI_SessionCache; -use FS::cust_main; -use FS::cust_pkg; -use FS::cust_svc; -use FS::svc_acct; -use FS::svc_external; -use FS::cust_recon; -use Data::Dumper; - -$DEBUG = 1; - -sub _cache { - $cache ||= new FS::ClientAPI_SessionCache ( { - 'namespace' => 'FS::ClientAPI::Agent', #yes, share session_ids - } ); -} - -sub _izoom_ftp_row_fixup { - my $hash = shift; - - my @addr_fields = qw( address1 address2 city state zip ); - my @fields = ( qw( agent_custid username _password first last ), - @addr_fields, - map { "ship_$_" } @addr_fields ); - - $hash->{$_} =~ s/[&\/\*'"]/_/g foreach @fields; - - #$hash->{action} = '' if $hash->{action} eq 'R'; #unsupported for ftp - - $hash->{refnum} = 1; #ahem - $hash->{country} = 'US'; - $hash->{ship_country} = 'US'; - $hash->{payby} = 'LECB'; - $hash->{payinfo} = $hash->{daytime}; - $hash->{ship_fax} = '' if ( !$hash->{sms} || $hash->{sms} eq 'F' ); - - my $has_ship = - grep { $hash->{"ship_$_"} && - (! $hash->{$_} || $hash->{"ship_$_"} ne $hash->{$_} ) - } - ( @addr_fields, 'fax' ); - - if ( $has_ship ) { - foreach ( @addr_fields, qw( first last ) ) { - $hash->{"ship_$_"} = $hash->{$_} unless $hash->{"ship_$_"}; - } - } - - delete $hash->{sms}; - - ''; - -}; - -sub _izoom_ftp_result { - my ($hash, $error) = @_; - my $cust_main = - qsearchs( 'cust_main', { 'agent_custid' => $hash->{agent_custid}, - 'agentnum' => $hash->{agentnum} - } - ); - - my $custnum = $cust_main ? $cust_main->custnum : ''; - my @response = ( $hash->{action}, $hash->{agent_custid}, $custnum ); - - if ( $error ) { - push @response, ( 'ERROR', $error ); - } else { - push @response, ( 'OK', 'OK' ); - } - - join( ',', @response ); - -} - -sub _izoom_ftp_badaction { - "Invalid action: $_[0] record: @_ "; -} - -sub _izoom_soap_row_fixup { _izoom_ftp_row_fixup(@_) }; - -sub _izoom_soap_result { - my ($hash, $error) = @_; - - if ( $hash->{action} eq 'R' ) { - if ( $error ) { - return "Please check errors:\n $error"; # odd extra space - } else { - return join(' ', "Everything ok.", $hash->{pkg}, $hash->{adjourn} ); - } - } - - my $pkg = $hash->{pkg} || $hash->{saved_pkg} || ''; - if ( $error ) { - return join(' ', $hash->{agent_custid}, $error ); - } else { - return join(' ', $hash->{agent_custid}, $pkg, $hash->{adjourn} ); - } - -} - -sub _izoom_soap_badaction { - "Unknown action '$_[13]' "; -} - -my %format = ( - 'izoom-ftp' => { - 'fields' => [ qw ( action agent_custid username _password - daytime ship_fax sms first last - address1 address2 city state zip - pkg adjourn ship_address1 ship_address2 - ship_city ship_state ship_zip ) ], - 'fixup' => sub { _izoom_ftp_row_fixup(@_) }, - 'result' => sub { _izoom_ftp_result(@_) }, - 'action' => sub { _izoom_ftp_badaction(@_) }, - }, - 'izoom-soap' => { - 'fields' => [ qw ( agent_custid username _password - daytime first last address1 address2 - city state zip pkg action adjourn - ship_fax sms ship_address1 ship_address2 - ship_city ship_state ship_zip ) ], - 'fixup' => sub { _izoom_soap_row_fixup(@_) }, - 'result' => sub { _izoom_soap_result(@_) }, - 'action' => sub { _izoom_soap_badaction(@_) }, - }, -); - -sub processrow { - my $p = shift; - - my $session = _cache->get($p->{'session_id'}) - or return { 'error' => "Can't resume session" }; #better error message - - my $conf = new FS::Conf; - my $format = $conf->config('selfservice-bulk_format', $session->{agentnum}) - || 'izoom-soap'; - my ( @row ) = @{ $p->{row} }; - - warn "processrow called with '". join("' '", @row). "'\n" if $DEBUG; - - return { 'error' => "unknown format: $format" } - unless exists $format{$format}; - - return { 'error' => "Invalid record record length: ". scalar(@row). - "record: @row " #sic - } - unless scalar(@row) == scalar(@{$format{$format}{fields}}); - - my %hash = ( 'agentnum' => $session->{agentnum} ); - my $error; - - foreach my $field ( @{ $format{ $format }{ fields } } ) { - $hash{$field} = shift @row; - } - - $error ||= &{ $format{ $format }{ fixup } }( \%hash ); - - # put in the fixup routine? - if ( 'R' eq $hash{action} ) { - warn "processing reconciliation\n" if $DEBUG; - $error ||= process_recon($hash{agentnum}, $hash{agent_custid}); - } elsif ( 'P' eq $hash{action} ) { - # do nothing - } elsif( 'D' eq $hash{action} ) { - $hash{promo_pkg} = 'disk-1-'. $session->{agent}; - } elsif ( 'S' eq $hash{action} ) { - $hash{promo_pkg} = 'disk-2-'. $session->{agent}; - $hash{saved_pkg} = $hash{pkg}; - $hash{pkg} = ''; - } else { - $error ||= &{ $format{ $format }{ action } }( @row ); - } - - warn "processing provision\n" if ($DEBUG && !$error && $hash{action} ne 'R'); - $error ||= provision( %hash ) unless $hash{action} eq 'R'; - - my $result = &{ $format{ $format }{ result } }( \%hash, $error ); - - warn "processrow returning '". join("' '", $result, $error). "'\n" - if $DEBUG; - - return { 'error' => $error, 'message' => $result }; - -} - -sub provision { - my %args = ( @_ ); - - delete $args{action}; - - my $cust_main = - qsearchs( 'cust_main', - { map { $_ => $args{$_} } qw ( agent_custid agentnum ) }, - ); - - unless ( $cust_main ) { - $cust_main = new FS::cust_main { %args }; - my $error = $cust_main->insert; - return $error if $error; - } - - my @pkgs = grep { $_->part_pkg->freq } $cust_main->ncancelled_pkgs; - if ( scalar(@pkgs) > 1 ) { - return "Invalid account, should not be more then one active package ". #sic - "but found: ". scalar(@pkgs). " packages."; - } - - my $part_pkg = qsearchs( 'part_pkg', { 'pkg' => $args{pkg} } ) - or return "Unknown pkgpart: $args{pkg}" - if $args{pkg}; - - - my $create_package = $args{pkg}; - if ( scalar(@pkgs) && $create_package ) { - my $pkg = pop(@pkgs); - - if ( $part_pkg->pkgpart != $pkg->pkgpart ) { - my @cust_bill_pkg = $pkg->cust_bill_pkg(); - if ( 1 == scalar(@cust_bill_pkg) ) { - my $cbp= pop(@cust_bill_pkg); - my $cust_bill = $cbp->cust_bill; - $cust_bill->delete(); #really? wouldn't a credit be better? - } - $pkg->cancel(); - } else { - $create_package = ''; - $pkg->setfield('adjourn', str2time($args{adjourn})); - my $error = $pkg->replace(); - return $error if $error; - } - } - - if ( $create_package ) { - my $cust_pkg = new FS::cust_pkg ( { - 'pkgpart' => $part_pkg->pkgpart, - 'adjourn' => str2time( $args{adjourn} ), - } ); - - my $svcpart = $part_pkg->svcpart('svc_acct'); - - my $svc_acct = new FS::svc_acct ( { - 'svcpart' => $svcpart, - 'username' => $args{username}, - '_password' => $args{_password}, - } ); - - my $error = $cust_main->order_pkg( cust_pkg => $cust_pkg, - svcs => [ $svc_acct ], - ); - return $error if $error; - } - - if ( $args{promo_pkg} ) { - my $part_pkg = - qsearchs( 'part_pkg', { 'promo_code' => $args{promo_pkg} } ) - or return "unknown pkgpart: $args{promo_pkg}"; - - my $svcpart = $part_pkg->svcpart('svc_external') - or return "unknown svcpart: svc_external"; - - my $cust_pkg = new FS::cust_pkg ( { - 'svcpart' => $svcpart, - 'pkgpart' => $part_pkg->pkgpart, - } ); - - my $svc_ext = new FS::svc_external ( { 'svcpart' => $svcpart } ); - - my $ticket_subject = 'Send setup disk to customer '. $cust_main->custnum; - my $error = $cust_main->order_pkg ( cust_pkg => $cust_pkg, - svcs => [ $svc_ext ], - noexport => 1, - ticket_subject => $ticket_subject, - ticket_queue => "disk-$args{agentnum}", - ); - return $error if $error; - } - - my $error = $cust_main->bill(); - return $error if $error; -} - -sub process_recon { - my ( $agentnum, $id ) = @_; - my @recs = split /;/, $id; - my $err = ''; - foreach my $rec ( @recs ) { - my @record = split /,/, $rec; - my $result = process_recon_record(@record, $agentnum); - $err .= "$result\n" if $result; - } - return $err; -} - -sub process_recon_record { - my ( $agent_custid, $username, $_password, $daytime, $first, $last, $address1, $address2, $city, $state, $zip, $pkg, $adjourn, $agentnum) = @_; - - warn "process_recon_record called with '". join("','", @_). "'\n" if $DEBUG; - - my ($cust_pkg, $package); - - my $cust_main = - qsearchs( 'cust_main', - { 'agent_custid' => $agent_custid, 'agentnum' => $agentnum }, - ); - - my $comments = ''; - if ( $cust_main ) { - my @cust_pkg = grep { $_->part_pkg->freq } $cust_main->ncancelled_pkgs; - if ( scalar(@cust_pkg) == 1) { - $cust_pkg = pop(@cust_pkg); - $package = $cust_pkg->part_pkg->pkg; - $comments = "$agent_custid wrong package, expected: $pkg found: $package" - if ( $pkg ne $package ); - } else { - $comments = "invalid account, should be one active package but found: ". - scalar(@cust_pkg). " packages."; - } - } else { - $comments = - "Customer not found agent_custid=$agent_custid, agentnum=$agentnum"; - } - - my $cust_recon = new FS::cust_recon( { - 'recondate' => time, - 'agentnum' => $agentnum, - 'first' => $first, - 'last' => $last, - 'address1' => $address1, - 'address2' => $address2, - 'city' => $city, - 'state' => $state, - 'zip' => $zip, - 'custnum' => $cust_main ? $cust_main->custnum : '', #really? - 'status' => $cust_main ? $cust_main->status : '', - 'pkg' => $package, - 'adjourn' => $cust_pkg ? $cust_pkg->adjourn : '', - 'agent_custid' => $agent_custid, # redundant? - 'agent_pkg' => $pkg, - 'agent_adjourn' => str2time($adjourn), - 'comments' => $comments, - } ); - - warn Dumper($cust_recon) if $DEBUG; - my $error = $cust_recon->insert; - return $error if $error; - - warn "process_recon_record returning $comments\n" if $DEBUG; - - $comments; - -} - -sub check_username { - my $p = shift; - - my $session = _cache->get($p->{'session_id'}) - or return { 'error' => "Can't resume session" }; #better error message - - my $svc_domain = qsearchs( 'svc_domain', { 'domain' => $p->{domain} } ) - or return { 'error' => 'Unknown domain '. $p->{domain} }; - - my $svc_acct = qsearchs( 'svc_acct', { 'username' => $p->{user}, - 'domsvc' => $svc_domain->svcnum, - }, - ); - - return { 'error' => $p->{user}. '@'. $p->{domain}. " alerady in use" } # sic - if $svc_acct; - - return { 'error' => '', - 'message' => $p->{user}. '@'. $p->{domain}. " is free" - }; -} - -1; diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index b02852b59..01e0ebc33 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -45,12 +45,12 @@ use FS::payby; use FS::acct_rt_transaction; use FS::msg_template; -$DEBUG = 0; +$DEBUG = 1; $me = '[FS::ClientAPI::MyAccount]'; use vars qw( @cust_main_editable_fields @location_editable_fields ); @cust_main_editable_fields = qw( - first last daytime night fax mobile + first last company daytime night fax mobile locale payby payinfo payname paystart_month paystart_year payissue payip ss paytype paystate stateid stateid_state @@ -121,6 +121,7 @@ sub skin_info { font title_color title_align title_size menu_bgcolor menu_fontsize ) ), + 'menu_disable' => [ $conf->config('selfservice-menu_disable',$agentnum) ], ( map { $_ => $conf->exists("selfservice-$_", $agentnum ) } qw( menu_skipblanks menu_skipheadings menu_nounderline no_logo ) ), @@ -635,11 +636,12 @@ sub billing_history { push @history, { 'type' => 'Line item', - 'description' => $_->desc. ( $_->sdate && $_->edate - ? ' '. time2str('%d-%b-%Y', $_->sdate). - ' To '. time2str('%d-%b-%Y', $_->edate) - : '' - ), + 'description' => $_->desc( $cust_main->locale ). + ( $_->sdate && $_->edate + ? ' '. time2str('%d-%b-%Y', $_->sdate). + ' To '. time2str('%d-%b-%Y', $_->edate) + : '' + ), 'amount' => sprintf('%.2f', $_->setup + $_->recur ), 'date' => $cust_bill->_date, 'date_pretty' => time2str('%m/%d/%Y', $cust_bill->_date ), @@ -1583,10 +1585,13 @@ sub list_pkgs { my $primary_cust_svc = $_->primary_cust_svc; +{ $_->hash, $_->part_pkg->hash, - pkg_label => $_->pkg_label, + pkg_label => $_->pkg_locale, status => $_->status, part_svc => - [ map $_->hashref, $_->available_part_svc ], + [ map { $_->hashref } + grep { $_->selfservice_access ne 'hidden' } + $_->available_part_svc + ], cust_svc => [ map { my $ref = { $_->hash, label => [ $_->label ], @@ -1600,7 +1605,9 @@ sub list_pkgs { $ref->{svchash}->{svcpart} = $_->part_svc->svcpart if $_->part_svc->svcdb eq 'svc_phone'; # hack $ref; - } $_->cust_svc + } + grep { $_->part_svc->selfservice_access ne 'hidden' } + $_->cust_svc ], primary_cust_svc => $primary_cust_svc @@ -1637,15 +1644,26 @@ sub list_svcs { } my @cust_svc = (); + my @cust_pkg_usage = (); #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) { foreach my $cust_pkg ( $p->{'ncancelled'} ? $cust_main->ncancelled_pkgs : $cust_main->unsuspended_pkgs ) { next if $pkgnum && $cust_pkg->pkgnum != $pkgnum; push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context + push @cust_pkg_usage, $cust_pkg->cust_pkg_usage; } @cust_svc = grep { $_->part_svc->selfservice_access ne 'hidden' } @cust_svc; + my %usage_pools; + foreach (@cust_pkg_usage) { + my $part = $_->part_pkg_usage; + my $tag = $part->description . ($part->shared ? 1 : 0); + my $row = $usage_pools{$tag} + ||= [ $part->description, 0, 0, $part->shared ? 1 : 0 ]; + $row->[1] += $_->minutes; # minutes remaining + $row->[2] += $part->minutes; # minutes total + } if ( $p->{'svcdb'} ) { my $svcdb = ref($p->{'svcdb'}) eq 'HASH' @@ -1681,7 +1699,7 @@ sub list_svcs { 'svcdb' => $svcdb, 'label' => $label, 'value' => $value, - 'pkg_label' => $cust_pkg->pkg_label, + 'pkg_label' => $cust_pkg->pkg_locale, 'pkg_status' => $cust_pkg->status, 'readonly' => ($part_svc->selfservice_access eq 'readonly'), ); @@ -1717,7 +1735,34 @@ sub list_svcs { } else { $hash{'name'} = $cust_main->name; } + } elsif ( $svcdb eq 'svc_phone' ) { + # could potentially show lots of things... + $hash{'outbound'} = 1; + $hash{'inbound'} = 0; + if ( $part_pkg->plan eq 'voip_inbound' ) { + $hash{'outbound'} = 0; + $hash{'inbound'} = 1; + } elsif ( $part_pkg->option('selfservice_inbound_format') + or $conf->config('selfservice-default_inbound_cdr_format') + ) { + $hash{'inbound'} = 1; + } + foreach (qw(inbound outbound)) { + # hmm...we can't filter by status here, because there might + # not be cdr_terminations at all. have to go by date. + # find all since the last bill date. + # XXX cdr types? we are going to need them. + if ( $hash{$_} ) { + my $sum_cdr = $svc_x->sum_cdrs( + 'inbound' => ( $_ eq 'inbound' ? 1 : 0 ), + 'begin' => ($cust_pkg->last_bill || 0), + 'nonzero' => 1, + ); + $hash{$_} = $sum_cdr->hashref; + } + } } + # elsif ( $svcdb eq 'svc_phone' || $svcdb eq 'svc_port' ) { # %hash = ( # %hash, @@ -1728,6 +1773,11 @@ sub list_svcs { } @cust_svc ], + 'usage_pools' => [ + map { $usage_pools{$_} } + sort { $a cmp $b } + keys %usage_pools + ], }; } @@ -1782,8 +1832,14 @@ sub svc_status_hash { } -sub set_svc_status_hash { - my $p = shift; +sub set_svc_status_hash { _svc_method_X(shift, 'export_setstatus') } +sub set_svc_status_listadd { _svc_method_X(shift, 'export_setstatus_listadd') } +sub set_svc_status_listdel { _svc_method_X(shift, 'export_setstatus_listdel') } +sub set_svc_status_vacationadd { _svc_method_X(shift, 'export_setstatus_vacationadd') } +sub set_svc_status_vacationdel { _svc_method_X(shift, 'export_setstatus_vacationdel') } + +sub _svc_method_X { + my( $p, $method ) = @_; my($context, $session, $custnum) = _custoragent_session_custnum($p); return { 'error' => $session } if $context eq 'error'; @@ -1792,16 +1848,15 @@ sub set_svc_status_hash { my $svc_x = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_acct') or return { 'error' => "Service not found" }; - warn "set_svc_status_hash ". join(' / ', map "$_=>".$p->{$_}, keys %$p ) + warn "$method ". join(' / ', map "$_=>".$p->{$_}, keys %$p ) if $DEBUG; - my $error = $svc_x->export_setstatus($p); #$p? returns error? + my $error = $svc_x->$method($p); #$p? returns error? return { 'error' => $error } if $error; return {}; #? { 'error' => '' } } - sub acct_forward_info { my $p = shift; @@ -1985,7 +2040,7 @@ sub _list_cdr_usage { # we have to return the results all at once... my($svc_phone, $begin, $end, %opt) = @_; map [ $_->downstream_csv(%opt, 'keeparray' => 1) ], - $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, ); + $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, %opt ); } sub list_cdr_usage { @@ -2015,18 +2070,21 @@ sub _usage_details { my %callback_opt; my $header = []; if ( $svcdb eq 'svc_phone' ) { - my $format = $cust_pkg->part_pkg->option('output_format') || ''; - $format = '' if $format =~ /^sum_/; - # sensible default if there is no format or it's a summary format - if ( $cust_pkg->part_pkg->plan eq 'voip_inbound' ) { - $format ||= 'source_default'; + my $conf = FS::Conf->new; + my $format = ''; + if ( $p->{inbound} ) { + $format = $cust_pkg->part_pkg->option('selfservice_inbound_format') + || $conf->config('selfservice-default_inbound_cdr_format') + || 'source_default'; $callback_opt{inbound} = 1; + } else { + $format = $cust_pkg->part_pkg->option('selfservice_format') + || $conf->config('selfservice-default_cdr_format') + || 'default'; } - else { - $format ||= 'default'; - } - + $callback_opt{format} = $format; + $callback_opt{use_clid} = 1; $header = [ split(',', FS::cdr::invoice_header($format) ) ]; } @@ -2085,6 +2143,7 @@ sub _usage_details { 'svcnum' => $p->{svcnum}, 'beginning' => $p->{beginning}, 'ending' => $p->{ending}, + 'inbound' => $p->{inbound}, 'previous' => ($previous > $start) ? $previous : $start, 'next' => ($next < $end) ? $next : $end, 'header' => $header, diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm index b7dcdbb64..1dbb20bc7 100644 --- a/FS/FS/ClientAPI/Signup.pm +++ b/FS/FS/ClientAPI/Signup.pm @@ -524,20 +524,13 @@ sub new_customer { my $template_cust = qsearchs('cust_main', { 'custnum' => $template_custnum } ); return { 'error' => 'Configuration error' } unless $template_cust; - #XXX Copy template customer's locations $cust_main = new FS::cust_main ( { 'agentnum' => $agentnum, 'refnum' => $packet->{refnum} || $conf->config('signup_server-default_refnum'), ( map { $_ => $template_cust->$_ } qw( - last first company address1 address2 - city county state zip country - daytime night fax - - ship_last ship_first ship_company ship_address1 ship_address2 - ship_city ship_county ship_state ship_zip ship_country - ship_daytime ship_night ship_fax + last first company daytime night fax ) ), @@ -555,6 +548,9 @@ sub new_customer { } ); + $bill_hash = { $template_cust->bill_location->location_hash }; + $ship_hash = { $template_cust->ship_location->location_hash }; + } else { $cust_main = new FS::cust_main ( { @@ -777,13 +773,15 @@ sub new_customer { # " new customer: $bill_error" # if $bill_error; - $bill_error = $cust_main->realtime_collect( - method => FS::payby->payby2bop( $packet->{payby} ), - depend_jobnum => $placeholder->jobnum, - selfservice => 1, - ); - #warn "$me error collecting from new customer: $bill_error" - # if $bill_error; + unless ( $packet->{payby} eq 'PREPAY' ) { + $bill_error = $cust_main->realtime_collect( + method => FS::payby->payby2bop( $packet->{payby} ), + depend_jobnum => $placeholder->jobnum, + selfservice => 1, + ); + #warn "$me error collecting from new customer: $bill_error" + # if $bill_error; + } if ($bill_error && ref($bill_error) eq 'HASH') { return { 'error' => '_collect', diff --git a/FS/FS/ClientAPI_XMLRPC.pm b/FS/FS/ClientAPI_XMLRPC.pm index 7dd20c652..d720db268 100644 --- a/FS/FS/ClientAPI_XMLRPC.pm +++ b/FS/FS/ClientAPI_XMLRPC.pm @@ -129,6 +129,10 @@ sub ss2clientapi { 'svc_status_html' => 'MyAccount/svc_status_html', 'svc_status_hash' => 'MyAccount/svc_status_hash', 'set_svc_status_hash' => 'MyAccount/set_svc_status_hash', + 'set_svc_status_listadd' => 'MyAccount/set_svc_status_listadd', + 'set_svc_status_listdel' => 'MyAccount/set_svc_status_listdel', + 'set_svc_status_vacationadd'=> 'MyAccount/set_svc_status_vacationadd', + 'set_svc_status_vacationdel'=> 'MyAccount/set_svc_status_vacationdel', 'acct_forward_info' => 'MyAccount/acct_forward_info', 'process_acct_forward' => 'MyAccount/process_acct_forward', 'list_dsl_devices' => 'MyAccount/list_dsl_devices', diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index d11916faf..6a19ff475 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -717,6 +717,18 @@ my %batch_gateway_options = ( }, ); +my @cdr_formats = ( + '' => '', + 'default' => 'Default', + 'source_default' => 'Default with source', + 'accountcode_default' => 'Default plus accountcode', + 'description_default' => 'Default with description field as destination', + 'basic' => 'Basic', + 'simple' => 'Simple', + 'simple2' => 'Simple with source', + 'accountcode_simple' => 'Simple with accountcode', +); + # takes the reason class (C, R, S) as an argument sub reason_type_options { my $reason_class = shift; @@ -985,6 +997,14 @@ sub reason_type_options { }, { + 'key' => 'currency', + 'section' => 'billing', + 'description' => 'Currency', + 'type' => 'select', + 'select_enum' => [ '', qw( USD AUD CAD DKK EUR GBP ILS JPY NZD XAF ) ], + }, + + { 'key' => 'business-batchpayment-test_transaction', 'section' => 'billing', 'description' => 'Turns on the Business::BatchPayment test_mode flag. Note that not all gateway modules support this flag; if yours does not, using the batch gateway will fail.', @@ -1509,8 +1529,18 @@ and customer address. Include units.', 'section' => 'invoicing', 'description' => 'Split invoice into sections and label according to package category when enabled.', 'type' => 'checkbox', + 'per_agent' => 1, }, + #quotations seem broken-ish with sections ATM? + #{ + # 'key' => 'quotation_sections', + # 'section' => 'invoicing', + # 'description' => 'Split quotations into sections and label according to package category when enabled.', + # 'type' => 'checkbox', + # 'per_agent' => 1, + #}, + { 'key' => 'usage_class_as_a_section', 'section' => 'invoicing', @@ -1608,6 +1638,7 @@ and customer address. Include units.', 'section' => 'required', 'description' => 'Print command for paper invoices, for example `lpr -h\'', 'type' => 'text', + 'per_agent' => 1, }, { @@ -2053,7 +2084,7 @@ and customer address. Include units.', 'key' => 'locale', 'section' => 'UI', 'description' => 'Default locale', - 'type' => 'select', + 'type' => 'select-sub', 'options_sub' => sub { map { $_ => FS::Locales->description($_) } FS::Locales->locales; }, @@ -3525,7 +3556,7 @@ and customer address. Include units.', 'section' => 'billing', 'description' => 'Default format for batches.', 'type' => 'select', - 'select_enum' => [ 'csv-td_canada_trust-merchant_pc_batch', + 'select_enum' => [ 'NACHA', 'csv-td_canada_trust-merchant_pc_batch', 'csv-chase_canada-E-xactBatch', 'BoM', 'PAP', 'paymentech', 'ach-spiritone', 'RBC' ] @@ -3587,9 +3618,9 @@ and customer address. Include units.', 'section' => 'billing', 'description' => 'Fixed (unchangeable) format for electronic check batches.', 'type' => 'select', - 'select_enum' => [ 'csv-td_canada_trust-merchant_pc_batch', 'BoM', 'PAP', - 'paymentech', 'ach-spiritone', 'RBC', 'td_eft1464', - 'eft_canada' + 'select_enum' => [ 'NACHA', 'csv-td_canada_trust-merchant_pc_batch', 'BoM', + 'PAP', 'paymentech', 'ach-spiritone', 'RBC', + 'td_eft1464', 'eft_canada' ] }, @@ -3643,13 +3674,6 @@ and customer address. Include units.', }, { - 'key' => 'batch-manual_approval', - 'section' => 'billing', - 'description' => 'Allow manual batch closure, which will approve all payments that do not yet have a status. This is not advised, but is needed for payment processors that provide a report of rejected rather than approved payments.', - 'type' => 'checkbox', - }, - - { 'key' => 'batchconfig-eft_canada', 'section' => 'billing', 'description' => 'Configuration for EFT Canada batching, four lines: 1. SFTP username, 2. SFTP password, 3. Transaction code, 4. Number of days to delay process date.', @@ -3658,6 +3682,34 @@ and customer address. Include units.', }, { + 'key' => 'batchconfig-nacha-destination', + 'section' => 'billing', + 'description' => 'Configuration for NACHA batching, Destination (9 digit transit routing number).', + 'type' => 'text', + }, + + { + 'key' => 'batchconfig-nacha-destination_name', + 'section' => 'billing', + 'description' => 'Configuration for NACHA batching, Destination (Bank Name, up to 23 characters).', + 'type' => 'text', + }, + + { + 'key' => 'batchconfig-nacha-origin', + 'section' => 'billing', + 'description' => 'Configuration for NACHA batching, Origin (your 10-digit company number, IRS tax ID recommended).', + 'type' => 'text', + }, + + { + 'key' => 'batch-manual_approval', + 'section' => 'billing', + 'description' => 'Allow manual batch closure, which will approve all payments that do not yet have a status. This is not advised unless needed for specific payment processors that provide a report of rejected rather than approved payments.', + 'type' => 'checkbox', + }, + + { 'key' => 'batch-spoolagent', 'section' => 'billing', 'description' => 'Store payment batches per-agent.', @@ -3722,20 +3774,6 @@ and customer address. Include units.', }, { - 'key' => 'cust_main-skeleton_tables', - 'section' => '', - 'description' => 'Tables which will have skeleton records inserted into them for each customer. Syntax for specifying tables is unfortunately a tricky perl data structure for now.', - 'type' => 'textarea', - }, - - { - 'key' => 'cust_main-skeleton_custnum', - 'section' => '', - 'description' => 'Customer number specifying the source data to copy into skeleton tables for new customers.', - 'type' => 'text', - }, - - { 'key' => 'cust_main-enable_birthdate', 'section' => 'UI', 'description' => 'Enable tracking of a birth date with each customer record', @@ -3785,6 +3823,13 @@ and customer address. Include units.', 'type' => 'checkbox', }, + { + 'key' => 'fuzzy-fuzziness', + 'section' => 'UI', + 'description' => 'Set the "fuzziness" of fuzzy searching (see the String::Approx manpage for details). Defaults to 10%', + 'type' => 'text', + }, + { 'key' => 'pkg_referral', 'section' => '', 'description' => 'Enable package-specific advertising sources.', @@ -3916,6 +3961,19 @@ and customer address. Include units.', }, { + 'key' => 'cust_bill-line_item-date_style-non_monthly', + 'section' => 'billing', + 'description' => 'If set, override cust_bill-line_item-date_style for non-monthly charges.', + 'type' => 'select', + 'select_hash' => [ '' => 'Default', + 'start_end' => 'STARTDATE-ENDDATE', + 'month_of' => 'Month of MONTHNAME', + 'X_month' => 'DATE_DESC MONTHNAME', + ], + 'per_agent' => 1, + }, + + { 'key' => 'cust_bill-line_item-date_description', 'section' => 'billing', 'description' => 'Text to display for "DATE_DESC" when using cust_bill-line_item-date_style DATE_DESC MONTHNAME.', @@ -3954,7 +4012,7 @@ and customer address. Include units.', 'type' => 'select', 'multiple' => 1, 'select_hash' => [ - 'address1' => 'Billing address', + #'address1' => 'Billing address', ], }, @@ -4128,7 +4186,7 @@ and customer address. Include units.', { 'key' => 'census_year', 'section' => 'UI', - 'description' => 'The year to use in census tract lookups', + 'description' => 'The year to use in census tract lookups. NOTE: you need to select 2012 for Year 2010 Census tract codes. A selection of 2011 or 2010 provides Year 2000 Census tract codes. Use the freeside-censustract-update tool if exisitng customers need to be changed.', 'type' => 'select', 'select_enum' => [ qw( 2012 2011 2010 ) ], }, @@ -4462,6 +4520,31 @@ and customer address. Include units.', }, { + 'key' => 'selfservice-menu_disable', + 'section' => 'self-service', + 'description' => 'Disable the selected menu entries in the self-service menu', + 'type' => 'selectmultiple', + 'select_enum' => [ #false laziness w/myaccount_menu.html + 'Overview', + 'Purchase', + 'Purchase additional package', + 'Recharge my account with a credit card', + 'Recharge my account with a check', + 'Recharge my account with a prepaid card', + 'View my usage', + 'Create a ticket', + 'Setup my services', + 'Change my information', + 'Change billing address', + 'Change service address', + 'Change payment information', + 'Change password(s)', + 'Logout', + ], + 'per_agent' => 1, + }, + + { 'key' => 'selfservice-menu_skipblanks', 'section' => 'self-service', 'description' => 'Skip blank (spacer) entries in the self-service menu', @@ -4547,23 +4630,6 @@ and customer address. Include units.', }, { - 'key' => 'selfservice-bulk_format', - 'section' => 'deprecated', - 'description' => 'Parameter arrangement for selfservice bulk features', - 'type' => 'select', - 'select_enum' => [ '', 'izoom-soap', 'izoom-ftp' ], - 'per_agent' => 1, - }, - - { - 'key' => 'selfservice-bulk_ftp_dir', - 'section' => 'deprecated', - 'description' => 'Enable bulk ftp provisioning in this folder', - 'type' => 'text', - 'per_agent' => 1, - }, - - { 'key' => 'signup-no_company', 'section' => 'self-service', 'description' => "Don't display a field for company name on signup.", @@ -4706,6 +4772,13 @@ and customer address. Include units.', }, { + 'key' => 'cdr-taqua-callerid_rewrite', + 'section' => 'telephony', + 'description' => 'For the Taqua CDR format, pull Caller ID blocking information from secondary CDRs.', + 'type' => 'checkbox', + }, + + { 'key' => 'cdr-asterisk_australia_rewrite', 'section' => 'telephony', 'description' => 'For Asterisk CDRs, assign CDR type numbers based on Australian conventions.', @@ -4713,6 +4786,13 @@ and customer address. Include units.', }, { + 'key' => 'cdr-gsm_tap3-sender', + 'section' => 'telephony', + 'description' => 'GSM TAP3 Sender network (5 letter code)', + 'type' => 'text', + }, + + { 'key' => 'cust_pkg-show_autosuspend', 'section' => 'UI', 'description' => 'Show package auto-suspend dates. Use with caution for now; can slow down customer view for large insallations.', @@ -4772,7 +4852,7 @@ and customer address. Include units.', { 'key' => 'svc_broadband-manage_link', 'section' => 'UI', - 'description' => 'URL for svc_broadband "Manage Device" link. The following substitutions are available: $ip_addr.', + 'description' => 'URL for svc_broadband "Manage Device" link. The following substitutions are available: $ip_addr and $mac_addr.', 'type' => 'text', }, @@ -4871,7 +4951,7 @@ and customer address. Include units.', { 'key' => 'pkg-balances', 'section' => 'billing', - 'description' => 'Enable experimental package balances. Not recommended for general use.', + 'description' => 'Enable per-package balances.', 'type' => 'checkbox', }, @@ -5125,6 +5205,13 @@ and customer address. Include units.', }, { + 'key' => 'invoice_payment_details', + 'section' => 'invoicing', + 'description' => 'When displaying payments on an invoice, show the payment method used, including the check or credit card number. Credit card numbers will be masked.', + 'type' => 'checkbox', + }, + + { 'key' => 'cust_main-status_module', 'section' => 'UI', 'description' => 'Which module to use for customer status display. The "Classic" module (the default) considers accounts with cancelled recurring packages but un-cancelled one-time charges Inactive. The "Recurring" module considers those customers Cancelled. Similarly for customers with suspended recurring packages but one-time charges.', #other differences? @@ -5146,6 +5233,13 @@ and customer address. Include units.', 'type' => 'checkbox', }, + { + 'key' => 'username-exclamation', + 'section' => 'username', + 'description' => 'Allow the exclamation character (!) in usernames.', + 'type' => 'checkbox', + }, + { 'key' => 'ie-compatibility_mode', 'section' => 'UI', @@ -5260,6 +5354,19 @@ and customer address. Include units.', $cdr_type ? $cdr_type->cdrtypename : ''; }, }, + + { + 'key' => 'cdr-minutes_priority', + 'section' => 'telephony', + 'description' => 'Priority rule for assigning included minutes to CDRs.', + 'type' => 'select', + 'select_hash' => [ + '' => 'No specific order', + 'time' => 'Chronological', + 'rate_high' => 'Highest rate first', + 'rate_low' => 'Lowest rate first', + ], + }, { 'key' => 'brand-agent', @@ -5283,6 +5390,22 @@ and customer address. Include units.', }, { + 'key' => 'selfservice-default_cdr_format', + 'section' => 'self-service', + 'description' => 'Format for showing outbound CDRs in self-service. The per-package option overrides this.', + 'type' => 'select', + 'select_hash' => \@cdr_formats, + }, + + { + 'key' => 'selfservice-default_inbound_cdr_format', + 'section' => 'self-service', + 'description' => 'Format for showing inbound CDRs in self-service. The per-package option overrides this. Leave blank to avoid showing these CDRs.', + 'type' => 'select', + 'select_hash' => \@cdr_formats, + }, + + { 'key' => 'logout-timeout', 'section' => 'UI', 'description' => 'If set, automatically log users out of the backoffice after this many minutes.', @@ -5307,6 +5430,13 @@ and customer address. Include units.', 'type' => 'text', }, + { + 'key' => 'report-cust_pay-select_time', + 'section' => 'UI', + 'description' => 'Enable time selection on payment and refund reports.', + 'type' => 'checkbox', + }, + { key => "apacheroot", section => "deprecated", description => "DEPRECATED", type => "text" }, { key => "apachemachine", section => "deprecated", description => "DEPRECATED", type => "text" }, { key => "apachemachines", section => "deprecated", description => "DEPRECATED", type => "text" }, diff --git a/FS/FS/Cron/bill.pm b/FS/FS/Cron/bill.pm index 6e110e852..98ce8fa73 100644 --- a/FS/FS/Cron/bill.pm +++ b/FS/FS/Cron/bill.pm @@ -201,7 +201,8 @@ sub bill_where { # generate where_pkg/where_event search clause ### - my $billtime = day_end($time); + my $conf = new FS::Conf; + my $billtime = $conf->exists('next-bill-ignore-time') ? day_end($time) : $time; # select * from cust_main where my $where_pkg = <<"END"; diff --git a/FS/FS/Cron/upload.pm b/FS/FS/Cron/upload.pm index 628c6801b..03ed366e2 100644 --- a/FS/FS/Cron/upload.pm +++ b/FS/FS/Cron/upload.pm @@ -470,7 +470,7 @@ sub spool_upload { } -=item send_report CONFIG PARAMS +=item prepare_report CONFIG PARAMS Retrieves the config value named CONFIG, parses it as a Text::Template, extracts "to" and "subject" headers, and returns a hash that can be passed diff --git a/FS/FS/L10N/en_us.pm b/FS/FS/L10N/en_us.pm index 6ad136be0..ed936a5d4 100644 --- a/FS/FS/L10N/en_us.pm +++ b/FS/FS/L10N/en_us.pm @@ -1,6 +1,8 @@ package FS::L10N::en_us; -use base qw(FS::L10N); +use base qw(FS::L10N::DBI); -our %Lexicon = ( _AUTO=>1 ); +#prevents english "translation" via FS::L10N::DBI, FS::Msgcat::_gettext already +# does the same sort of fallback +#our %Lexicon = ( _AUTO=>1 ); 1; diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 2bc1596f2..1553a42df 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -77,7 +77,7 @@ if ( -e $addl_handler_use_file ) { use HTML::TableExtract qw(tree); use HTML::FormatText; use HTML::Defang; - use JSON; + use JSON::XS; # use XMLRPC::Transport::HTTP; # use XMLRPC::Lite; # for XMLRPC::Serializer use MIME::Base64; @@ -160,6 +160,7 @@ if ( -e $addl_handler_use_file ) { use FS::cust_credit; use FS::cust_credit_bill; use FS::cust_main; + use FS::h_cust_main; use FS::cust_main::Search qw(smart_search); use FS::cust_main::Import; use FS::cust_main_county; @@ -332,6 +333,12 @@ if ( -e $addl_handler_use_file ) { use FS::GeocodeCache; use FS::log; use FS::log_context; + use FS::part_pkg_usage_class; + use FS::cust_pkg_usage; + use FS::part_pkg_usage_class; + use FS::part_pkg_usage; + use FS::cdr_cust_pkg_usage; + use FS::part_pkg_msgcat; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm index 096ec8a4c..de9fb522f 100644 --- a/FS/FS/Misc.pm +++ b/FS/FS/Misc.pm @@ -699,7 +699,8 @@ sub generate_ps { open(POSTSCRIPT, "<$file.ps") or die "can't open $file.ps: $! (error in LaTeX template?)\n"; - unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex"); + unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex") + unless $FS::CurrentUser::CurrentUser->option('save_tmp_typesetting'); my $ps = ''; @@ -757,7 +758,8 @@ sub generate_pdf { open(PDF, "<$file.pdf") or die "can't open $file.pdf: $! (error in LaTeX template?)\n"; - unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex"); + unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex") + unless $FS::CurrentUser::CurrentUser->option('save_tmp_typesetting'); my $pdf = ''; while () { @@ -800,16 +802,32 @@ sub _pslatex { } -=item do_print ARRAYREF +=item do_print ARRAYREF [, OPTION => VALUE ... ] Sends the lines in ARRAYREF to the printer. +Options available are: + +=over 4 + +=item agentnum + +Uses this agent's 'lpr' configuration setting override instead of the global +value. + +=item lpr + +Uses this command instead of the configured lpr command (overrides both the +global value and agentnum). + =cut sub do_print { - my $data = shift; + my( $data, %opt ) = @_; - my $lpr = $conf->config('lpr'); + my $lpr = ( exists($opt{'lpr'}) && $opt{'lpr'} ) + ? $opt{'lpr'} + : $conf->config('lpr', $opt{'agentnum'} ); my $outerr = ''; run3 $lpr, $data, \$outerr, \$outerr; diff --git a/FS/FS/Misc/DateTime.pm b/FS/FS/Misc/DateTime.pm index 9c12e6408..2fff90647 100644 --- a/FS/FS/Misc/DateTime.pm +++ b/FS/FS/Misc/DateTime.pm @@ -2,8 +2,8 @@ package FS::Misc::DateTime; use base qw( Exporter ); use vars qw( @EXPORT_OK ); -use POSIX; use Carp; +use Time::Local; use Date::Parse; use DateTime::Format::Natural; use FS::Conf; @@ -49,7 +49,7 @@ sub parse_datetime { #carp "WARNING: can't parse date: ". $parser->error; #return ''; #huh, very common, we still need the "partially" (fully enough for our purposes) parsed date. - $dt->epoch; + return $dt->epoch; } } else { return str2time($string, $tz); @@ -59,24 +59,17 @@ sub parse_datetime { =item day_end TIME -If the next-bill-ignore-time configuration setting is turned off, just -returns the passed-in value. - -If the next-bill-ignore-time configuration setting is turned on, parses TIME -as an integer UNIX timestamp and returns a new timestamp with the same date but -23:59:59 for the time. +Parses TIME as an integer UNIX timestamp and returns a new timestamp with the +same date but 23:59:59 for the time. =cut sub day_end { my $time = shift; - my $conf = new FS::Conf; - return $time unless $conf->exists('next-bill-ignore-time'); - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time); - mktime(59,59,23,$mday,$mon,$year,$wday,$yday,$isdst); + timelocal(59,59,23,$mday,$mon,$year); } =back diff --git a/FS/FS/Misc/Geo.pm b/FS/FS/Misc/Geo.pm index 5cb10b2fb..2ad8311d0 100644 --- a/FS/FS/Misc/Geo.pm +++ b/FS/FS/Misc/Geo.pm @@ -81,7 +81,7 @@ sub get_censustract_ffiec { my($zip5, $zip4) = split('-',$location->{zip}); - $year ||= '2011'; #2012 per http://transition.fcc.gov/form477/techfaqs.html soon/now? + $year ||= '2012'; my @ffiec_args = ( __VIEWSTATE => $viewstate, __EVENTVALIDATION => $eventvalidation, diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index ca68c3596..bdf3bcf3a 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -458,7 +458,13 @@ sub qsearch { # grep defined( $record->{$_} ) && $record->{$_} ne '', @fields # ) or croak "Error executing \"$statement\": ". $sth->errstr; - $sth->execute or croak "Error executing \"$statement\": ". $sth->errstr; + my $ok = $sth->execute; + if (!$ok) { + my $error = "Error executing \"$statement\""; + $error .= ' (' . join(', ', map {"'$_'"} @value) . ')' if @value; + $error .= ': '. $sth->errstr; + croak $error; + } my $table = $stable[0]; my $pkey = ''; @@ -1451,6 +1457,7 @@ sub process_batch_import { format_sep_chars => $opt->{format_sep_chars}, format_fixedlength_formats => $opt->{format_fixedlength_formats}, format_xml_formats => $opt->{format_xml_formats}, + format_asn_formats => $opt->{format_asn_formats}, format_row_callbacks => $opt->{format_row_callbacks}, #per-import job => $job, @@ -1533,8 +1540,9 @@ sub batch_import { my $file = $param->{file}; my $params = $param->{params} || {}; - my( $type, $header, $sep_char, $fixedlength_format, - $xml_format, $row_callback, @fields ); + my( $type, $header, $sep_char, + $fixedlength_format, $xml_format, $asn_format, + $row_callback, @fields ); my $postinsert_callback = ''; $postinsert_callback = $param->{'postinsert_callback'} @@ -1572,6 +1580,11 @@ sub batch_import { ? $param->{'format_xml_formats'}{ $param->{'format'} } : ''; + $asn_format = + $param->{'format_asn_formats'} + ? $param->{'format_asn_formats'}{ $param->{'format'} } + : ''; + $row_callback = $param->{'format_row_callbacks'} ? $param->{'format_row_callbacks'}{ $param->{'format'} } @@ -1611,11 +1624,12 @@ sub batch_import { my $count; my $parser; my @buffer = (); + my $asn_header_buffer; if ( $type eq 'csv' || $type eq 'fixedlength' ) { if ( $type eq 'csv' ) { - my %attr = (); + my %attr = ( 'binary' => 1, ); $attr{sep_char} = $sep_char if $sep_char; $parser = new Text::CSV_XS \%attr; @@ -1652,7 +1666,9 @@ sub batch_import { $count++; $row = $header || 0; + } elsif ( $type eq 'xml' ) { + # FS::pay_batch eval "use XML::Simple;"; die $@ if $@; @@ -1668,6 +1684,26 @@ sub batch_import { $rows = $rows->{$_} foreach @$xmlrow; $rows = [ $rows ] if ref($rows) ne 'ARRAY'; $count = @buffer = @$rows; + + } elsif ( $type eq 'asn.1' ) { + + eval "use Convert::ASN1"; + die $@ if $@; + + my $asn = Convert::ASN1->new; + $asn->prepare( $asn_format->{'spec'} ) or die $asn->error; + + $parser = $asn->find( $asn_format->{'macro'} ) or die $asn->error; + + my $data = slurp($file); + my $asn_output = $parser->decode( $data ) + or die "No ". $asn_format->{'macro'}. " found\n"; + + $asn_header_buffer = &{ $asn_format->{'header_buffer'} }( $asn_output ); + + my $rows = &{ $asn_format->{'arrayref'} }( $asn_output ); + $count = @buffer = @$rows; + } else { die "Unknown file type $type\n"; } @@ -1711,6 +1747,7 @@ sub batch_import { while (1) { my @columns = (); + my %hash = %$params; if ( $type eq 'csv' ) { last unless scalar(@buffer); @@ -1747,16 +1784,27 @@ sub batch_import { #warn $z++. ": $_\n" for @columns; } elsif ( $type eq 'xml' ) { + # $parser = [ 'Column0Key', 'Column1Key' ... ] last unless scalar(@buffer); my $row = shift @buffer; @columns = @{ $row }{ @$parser }; + + } elsif ( $type eq 'asn.1' ) { + + last unless scalar(@buffer); + my $row = shift @buffer; + &{ $asn_format->{row_callback} }( $row, $asn_header_buffer ) + if $asn_format->{row_callback}; + foreach my $key ( keys %{ $asn_format->{map} } ) { + $hash{$key} = &{ $asn_format->{map}{$key} }( $row, $asn_header_buffer ); + } + } else { die "Unknown file type $type\n"; } my @later = (); - my %hash = %$params; foreach my $field ( @fields ) { @@ -2051,11 +2099,18 @@ is an error, returns the error, otherwise returns false. sub ut_money { my($self,$field)=@_; - $self->setfield($field, 0) if $self->getfield($field) eq ''; - $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{2})?\s*$/ - or return "Illegal (money) $field: ". $self->getfield($field); - #$self->setfield($field, "$1$2$3" || 0); - $self->setfield($field, ( ($1||''). ($2||''). ($3||'') ) || 0); + + if ( $self->getfield($field) eq '' ) { + $self->setfield($field, 0); + } elsif ( $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{1})\s*$/ ) { + #handle one decimal place without barfing out + $self->setfield($field, ( ($1||''). ($2||''). ($3.'0') ) || 0); + } elsif ( $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{2})?\s*$/ ) { + $self->setfield($field, ( ($1||''). ($2||''). ($3||'') ) || 0); + } else { + return "Illegal (money) $field: ". $self->getfield($field); + } + ''; } @@ -2466,10 +2521,29 @@ sub ut_name { # warn "ut_name allowed alphanumerics: +(sort grep /\w/, map { chr() } 0..255), "\n"; $self->getfield($field) =~ /^([\w \,\.\-\']+)$/ or return gettext('illegal_name'). " $field: ". $self->getfield($field); - $self->setfield($field,$1); + my $name = $1; + $name =~ s/^\s+//; + $name =~ s/\s+$//; + $name =~ s/\s+/ /g; + $self->setfield($field, $name); ''; } +=item ut_namen COLUMN + +Check/untaint proper names; allows alphanumerics, spaces and the following +punctuation: , . - ' + +May not be null. + +=cut + +sub ut_namen { + my( $self, $field ) = @_; + return $self->setfield($field, '') if $self->getfield($field) =~ /^$/; + $self->ut_name($field); +} + =item ut_zip COLUMN Check/untaint zip codes. diff --git a/FS/FS/Report/FCC_477.pm b/FS/FS/Report/FCC_477.pm index 49bb8a852..fd088148b 100644 --- a/FS/FS/Report/FCC_477.pm +++ b/FS/FS/Report/FCC_477.pm @@ -22,26 +22,26 @@ Documentation. =cut @upload = qw( - <200kpbs - 200-768kpbs + <200kbps + 200-768kbps 768kbps-1.5mbps 1.5-3mpbs 3-6mbps 6-10mbps 10-25mbps 25-100mbps - >100bmps + >100mbps ); @download = qw( - 200-768kpbs + 200-768kbps 768kbps-1.5mbps - 1.5-3mpbs + 1.5-3mbps 3-6mbps 6-10mbps 10-25mbps 25-100mbps - >100bmps + >100mbps ); @technology = ( diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index cbcd27b46..eb73ccbc8 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -772,7 +772,7 @@ sub tables_hashref { 'format', 'char', 'NULL', 1, '', '', 'classnum', 'int', 'NULL', '', '', '', 'duration', 'int', 'NULL', '', 0, '', - 'phonenum', 'varchar', 'NULL', 15, '', '', + 'phonenum', 'varchar', 'NULL', 25, '', '', 'accountcode', 'varchar', 'NULL', 20, '', '', 'startdate', @date_type, '', '', 'regionname', 'varchar', 'NULL', $char_d, '', '', @@ -875,7 +875,7 @@ sub tables_hashref { 'format', 'char', 'NULL', 1, '', '', 'classnum', 'int', 'NULL', '', '', '', 'duration', 'int', 'NULL', '', 0, '', - 'phonenum', 'varchar', 'NULL', 15, '', '', + 'phonenum', 'varchar', 'NULL', 25, '', '', 'accountcode', 'varchar', 'NULL', 20, '', '', 'startdate', @date_type, '', '', 'regionname', 'varchar', 'NULL', $char_d, '', '', @@ -1080,6 +1080,7 @@ sub tables_hashref { 'locale', 'varchar', 'NULL', 16, '', '', 'calling_list_exempt', 'char', 'NULL', 1, '', '', 'invoice_noemail', 'char', 'NULL', 1, '', '', + 'message_noemail', 'char', 'NULL', 1, '', '', 'bill_locationnum', 'int', 'NULL', '', '', '', 'ship_locationnum', 'int', 'NULL', '', '', '', ], @@ -1225,6 +1226,8 @@ sub tables_hashref { 'quotation_pkg' => { 'columns' => [ 'quotationpkgnum', 'serial', '', '', '', '', + 'quotationnum', 'int', 'NULL', '', '', '', #shouldn't be null, + # but history... 'pkgpart', 'int', '', '', '', '', 'locationnum', 'int', 'NULL', '', '', '', 'start_date', @date_type, '', '', @@ -1688,13 +1691,14 @@ sub tables_hashref { 'zip', 'varchar', 'NULL', 10, '', '', 'country', 'char', '', 2, '', '', # 'trancode', 'int', '', '', '', '' - 'payby', 'char', '', 4, '', '', # CARD/BILL/COMP, should be - 'payinfo', 'varchar', '', 512, '', '', + 'payby', 'char', '', 4, '', '', + 'payinfo', 'varchar', 'NULL', 512, '', '', #'exp', @date_type, '', '' - 'exp', 'varchar', 'NULL', 11, '', '', + 'exp', 'varchar', 'NULL', 11, '', '', 'payname', 'varchar', 'NULL', $char_d, '', '', 'amount', @money_type, '', '', - 'status', 'varchar', 'NULL', $char_d, '', '', + 'status', 'varchar', 'NULL', $char_d, '', '', + 'error_message', 'varchar', 'NULL', $char_d, '', '', ], 'primary_key' => 'paybatchnum', 'unique' => [], @@ -1717,6 +1721,7 @@ sub tables_hashref { 'custnum', 'int', '', '', '', '', 'pkgpart', 'int', '', '', '', '', 'pkgbatch', 'varchar', 'NULL', $char_d, '', '', + 'contactnum', 'int', 'NULL', '', '', '', 'locationnum', 'int', 'NULL', '', '', '', 'otaker', 'varchar', 'NULL', 32, '', '', 'usernum', 'int', 'NULL', '', '', '', @@ -1738,6 +1743,8 @@ sub tables_hashref { 'change_pkgnum', 'int', 'NULL', '', '', '', 'change_pkgpart', 'int', 'NULL', '', '', '', 'change_locationnum', 'int', 'NULL', '', '', '', + 'main_pkgnum', 'int', 'NULL', '', '', '', + 'pkglinknum', 'int', 'NULL', '', '', '', 'manual_flag', 'char', 'NULL', 1, '', '', 'no_auto', 'char', 'NULL', 1, '', '', 'quantity', 'int', 'NULL', '', '', '', @@ -1812,6 +1819,30 @@ sub tables_hashref { 'index' => [ [ 'pkgnum' ], [ 'discountnum' ], [ 'usernum' ], ], }, + 'cust_pkg_usage' => { + 'columns' => [ + 'pkgusagenum', 'serial', '', '', '', '', + 'pkgnum', 'int', '', '', '', '', + 'minutes', 'int', '', '', '', '', + 'pkgusagepart', 'int', '', '', '', '', + ], + 'primary_key' => 'pkgusagenum', + 'unique' => [], + 'index' => [ [ 'pkgnum' ], [ 'pkgusagepart' ] ], + }, + + 'cdr_cust_pkg_usage' => { + 'columns' => [ + 'cdrusagenum', 'bigserial', '', '', '', '', + 'acctid', 'bigint', '', '', '', '', + 'pkgusagenum', 'int', '', '', '', '', + 'minutes', 'int', '', '', '', '', + ], + 'primary_key' => 'cdrusagenum', + 'unique' => [], + 'index' => [ [ 'pkgusagenum' ], [ 'acctid' ] ], + }, + 'cust_bill_pkg_discount' => { 'columns' => [ 'billpkgdiscountnum', 'serial', '', '', '', '', @@ -1981,6 +2012,19 @@ sub tables_hashref { ], }, + 'part_pkg_msgcat' => { + 'columns' => [ + 'pkgpartmsgnum', 'serial', '', '', '', '', + 'pkgpart', 'int', '', '', '', '', + 'locale', 'varchar', '', 16, '', '', + 'pkg', 'varchar', '', $char_d, '', '', #longer/no limit? + 'comment', 'varchar', 'NULL', 2*$char_d, '', '', #longer/no limit? + ], + 'primary_key' => 'pkgpartmsgnum', + 'unique' => [ [ 'pkgpart', 'locale' ] ], + 'index' => [], + }, + 'part_pkg_link' => { 'columns' => [ 'pkglinknum', 'serial', '', '', '', '', @@ -2109,7 +2153,8 @@ sub tables_hashref { 'preserve', 'char', 'NULL', 1, '', '', 'selfservice_access', 'varchar', 'NULL', $char_d, '', '', 'classnum', 'int', 'NULL', '', '', '', - ], + 'restrict_edit_password','char', 'NULL', 1, '', '', +], 'primary_key' => 'svcpart', 'unique' => [], 'index' => [ [ 'disabled' ] ], @@ -2257,6 +2302,7 @@ sub tables_hashref { 'cgp_sendmdnmode', 'varchar', 'NULL', $char_d, '', '',#SendMDNMode #mail #XXX RPOP settings + # ], 'primary_key' => 'svcnum', #'unique' => [ [ 'username', 'domsvc' ] ], @@ -2683,9 +2729,10 @@ sub tables_hashref { 'columns' => [ 'exportnum', 'serial', '', '', '', '', 'exportname', 'varchar', 'NULL', $char_d, '', '', - 'machine', 'varchar', 'NULL', $char_d, '', '', + 'machine', 'varchar', 'NULL', $char_d, '', '', 'exporttype', 'varchar', '', $char_d, '', '', 'nodomain', 'char', 'NULL', 1, '', '', + 'default_machine','int', 'NULL', '', '', '', ], 'primary_key' => 'exportnum', 'unique' => [], @@ -2862,22 +2909,28 @@ sub tables_hashref { 'svc_broadband' => { 'columns' => [ - 'svcnum', 'int', '', '', '', '', - 'description', 'varchar', 'NULL', $char_d, '', '', - 'routernum', 'int', 'NULL', '', '', '', - 'blocknum', 'int', 'NULL', '', '', '', - 'sectornum', 'int', 'NULL', '', '', '', - 'speed_up', 'int', 'NULL', '', '', '', - 'speed_down', 'int', 'NULL', '', '', '', - 'ip_addr', 'varchar', 'NULL', 15, '', '', - 'mac_addr', 'varchar', 'NULL', 12, '', '', - 'authkey', 'varchar', 'NULL', 32, '', '', - 'latitude', 'decimal', 'NULL', '10,7', '', '', - 'longitude', 'decimal', 'NULL', '10,7', '', '', - 'altitude', 'decimal', 'NULL', '', '', '', - 'vlan_profile', 'varchar', 'NULL', $char_d, '', '', - 'performance_profile', 'varchar', 'NULL', $char_d, '', '', - 'plan_id', 'varchar', 'NULL', $char_d, '', '', + 'svcnum', 'int', '', '', '', '', + 'description', 'varchar', 'NULL', $char_d, '', '', + 'routernum', 'int', 'NULL', '', '', '', + 'blocknum', 'int', 'NULL', '', '', '', + 'sectornum', 'int', 'NULL', '', '', '', + 'speed_up', 'int', 'NULL', '', '', '', + 'speed_down', 'int', 'NULL', '', '', '', + 'ip_addr', 'varchar', 'NULL', 15, '', '', + 'mac_addr', 'varchar', 'NULL', 12, '', '', + 'authkey', 'varchar', 'NULL', 32, '', '', + 'latitude', 'decimal', 'NULL', '10,7', '', '', + 'longitude', 'decimal', 'NULL', '10,7', '', '', + 'altitude', 'decimal', 'NULL', '', '', '', + 'vlan_profile', 'varchar', 'NULL', $char_d, '', '', + 'performance_profile', 'varchar', 'NULL', $char_d, '', '', + 'plan_id', 'varchar', 'NULL', $char_d, '', '', + 'radio_serialnum', 'varchar', 'NULL', $char_d, '', '', + 'radio_location', 'varchar', 'NULL', 2*$char_d, '', '', + 'poe_location', 'varchar', 'NULL', 2*$char_d, '', '', + 'rssi', 'int', 'NULL', '', '', '', + 'suid', 'int', 'NULL', '', '', '', + 'shared_svcnum', 'int', 'NULL', '', '', '', ], 'primary_key' => 'svcnum', 'unique' => [ [ 'ip_addr' ], [ 'mac_addr' ] ], @@ -3016,6 +3069,32 @@ sub tables_hashref { 'index' => [ [ 'disabled' ] ], }, + 'part_pkg_usage' => { + 'columns' => [ + 'pkgusagepart', 'serial', '', '', '', '', + 'pkgpart', 'int', '', '', '', '', + 'minutes', 'int', '', '', '', '', + 'priority', 'int', 'NULL', '', '', '', + 'shared', 'char', 'NULL', 1, '', '', + 'rollover', 'char', 'NULL', 1, '', '', + 'description', 'varchar', 'NULL', $char_d, '', '', + ], + 'primary_key' => 'pkgusagepart', + 'unique' => [], + 'index' => [ [ 'pkgpart' ] ], + }, + + 'part_pkg_usage_class' => { + 'columns' => [ + 'num', 'serial', '', '', '', '', + 'pkgusagepart', 'int', '', '', '', '', + 'classnum', 'int','NULL', '', '', '', + ], + 'primary_key' => 'num', + 'unique' => [ [ 'pkgusagepart', 'classnum' ] ], + 'index' => [], + }, + 'rate' => { 'columns' => [ 'ratenum', 'serial', '', '', '', '', @@ -3053,6 +3132,7 @@ sub tables_hashref { 'columns' => [ 'regionnum', 'serial', '', '', '', '', 'regionname', 'varchar', '', $char_d, '', '', + 'exact_match', 'char', 'NULL', 1, '', '', ], 'primary_key' => 'regionnum', 'unique' => [], @@ -3333,6 +3413,12 @@ sub tables_hashref { 'quantity', 'int', 'NULL', '', '', '', 'upstream_rateid', 'int', 'NULL', '', '', '', + + ### + # more fields, for GSM imports + ### + 'servicecode', 'int', 'NULL', '', '', '', + 'quantity_able', 'int', 'NULL', '', '', '', ### #and now for our own fields @@ -3341,8 +3427,9 @@ sub tables_hashref { 'cdrtypenum', 'int', 'NULL', '', '', '', 'charged_party', 'varchar', 'NULL', $char_d, '', '', + 'charged_party_imsi', 'varchar', 'NULL', $char_d, '', '', - 'upstream_price', 'decimal', 'NULL', '10,4', '', '', + 'upstream_price', 'decimal', 'NULL', '10,5', '', '', 'upstream_src_regionname', 'varchar', 'NULL', $char_d, '', '', 'upstream_dst_regionname', 'varchar', 'NULL', $char_d, '', '', @@ -3357,7 +3444,7 @@ sub tables_hashref { 'rated_classnum', 'int', 'NULL', '', '', '', 'rated_ratename', 'varchar', 'NULL', $char_d, '', '', - 'carrierid', 'int', 'NULL', '', '', '', + 'carrierid', 'bigint', 'NULL', '', '', '', # service it was matched to 'svcnum', 'int', 'NULL', '', '', '', @@ -3590,7 +3677,8 @@ sub tables_hashref { 'columns' => [ 'svcnum', 'int', '', '', '', '', 'countrycode', 'varchar', '', 3, '', '', - 'phonenum', 'varchar', '', 15, '', '', #12 ? + 'phonenum', 'varchar', '', 25, '', '', #12 ? + 'sim_imsi', 'varchar', 'NULL', 15, '', '', 'pin', 'varchar', 'NULL', $char_d, '', '', 'sip_password', 'varchar', 'NULL', $char_d, '', '', 'phone_name', 'varchar', 'NULL', $char_d, '', '', diff --git a/FS/FS/TemplateItem_Mixin.pm b/FS/FS/TemplateItem_Mixin.pm index 6d7ea26bc..8b0e16a2d 100644 --- a/FS/FS/TemplateItem_Mixin.pm +++ b/FS/FS/TemplateItem_Mixin.pm @@ -52,10 +52,10 @@ line item, and for generic taxes, simply returns "Tax". =cut sub desc { - my $self = shift; + my( $self, $locale ) = @_; if ( $self->pkgnum > 0 ) { - $self->itemdesc || $self->part_pkg->pkg; + $self->itemdesc || $self->part_pkg->pkg_locale($locale); } else { my $desc = $self->itemdesc || 'Tax'; $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/; @@ -271,10 +271,12 @@ sub cust_bill_pkg_display { } else { my $hashref = { 'billpkgnum' => $self->billpkgnum }; $hashref->{type} = $type if defined($type); + + my $order_by = $self->display_table_orderby || 'billpkgdisplaynum'; @result = qsearch ({ 'table' => $self->display_table, - 'hashref' => { 'billpkgnum' => $self->billpkgnum }, - 'order_by' => 'ORDER BY billpkgdisplaynum', + 'hashref' => $hashref, + 'order_by' => "ORDER BY $order_by", }); } diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index adab9d5e2..2e78f12f4 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -122,7 +122,9 @@ sub print_latex { UNLINK => 0, ) or die "can't open temp file: $!\n"; - my $agentnum = $self->cust_main->agentnum; + my $cust_main = $self->cust_main; + my $prospect_main = $self->prospect_main; + my $agentnum = $cust_main ? $cust_main->agentnum : $prospect_main->agentnum; if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) { print $lh $conf->config_binary("logo_${template}.eps", $agentnum) @@ -363,14 +365,6 @@ sub print_generic { my $date_format = $date_formats{$format}; - my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}' - }, - 'html' => sub { return ''. shift(). '' - }, - 'template' => sub { shift }, - ); - my $embolden_function = $embolden_functions{$format}; - my %newline_tokens = ( 'latex' => '\\\\', 'html' => '
', 'template' => "\n", @@ -584,16 +578,20 @@ sub print_generic { #my $balance_due = $self->owed + $pr_total - $cr_total; my $balance_due = $self->owed + $pr_total; - # the customer's current balance as shown on the invoice before this one - $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) ); + #these are used on the summary page only + + # the customer's current balance as shown on the invoice before this one + $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) ); - # the change in balance from that invoice to this one - $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) ); + # the change in balance from that invoice to this one + $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) ); - # the sum of amount owed on all previous invoices - $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total); + # the sum of amount owed on all previous invoices + # ($pr_total is used elsewhere but not as $previous_balance) + $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total); # the sum of amount owed on all invoices + # (this is used in the summary & on the payment coupon) $invoice_data{'balance'} = sprintf("%.2f", $balance_due); # info from customer's last invoice before this one, for some @@ -727,10 +725,11 @@ sub print_generic { my $adjusttotal = 0; - my $adjust_section = { 'description' => - $self->mt('Credits, Payments, and Adjustments'), - 'subtotal' => 0, # adjusted below - }; + my $adjust_section = { + 'description' => $self->mt('Credits, Payments, and Adjustments'), + 'adjust_section' => 1, + 'subtotal' => 0, # adjusted below + }; my $adjust_weight = _pkg_category($adjust_section->{description}) ? _pkg_category($adjust_section->{description})->weight : 0; @@ -738,7 +737,7 @@ sub print_generic { $adjust_section->{'sort_weight'} = $adjust_weight; my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y'; - my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum); + my $multisection = $conf->exists($tc.'_sections', $cust_main->agentnum); $invoice_data{'multisection'} = $multisection; my $late_sections = []; my $extra_sections = []; @@ -936,6 +935,7 @@ sub print_generic { $detail->{'sdate'} = $line_item->{'sdate'}; $detail->{'edate'} = $line_item->{'edate'}; $detail->{'seconds'} = $line_item->{'seconds'}; + $detail->{'svc_label'} = $line_item->{'svc_label'}; push @detail_items, $detail; push @buf, ( [ $detail->{'description'}, @@ -1033,9 +1033,33 @@ sub print_generic { $money_char. sprintf("%10.2f",$self->charged) ]; push @buf,['','']; - # calculate total, possibly including total owed on previous - # invoices - { + + ### + # Totals + ### + + my %embolden_functions = ( + 'latex' => sub { return '\textbf{'. shift(). '}' }, + 'html' => sub { return ''. shift(). '' }, + 'template' => sub { shift }, + ); + my $embolden_function = $embolden_functions{$format}; + + if ( $self->can('_items_total') ) { # quotations + + $self->_items_total(\@total_items); + + foreach ( @total_items ) { + $_->{'total_item'} = &$embolden_function( $_->{'total_item'} ); + $_->{'total_amount'} = &$embolden_function( $other_money_char. + $_->{'total_amount'} + ); + } + + } else { #normal invoice case + + # calculate total, possibly including total owed on previous + # invoices my $total = {}; my $item = 'Total'; $item = $conf->config('previous_balance-exclude_from_total') @@ -1066,126 +1090,128 @@ sub print_generic { sprintf( '%10.2f', $amount ) ]; push @buf,['','']; - } - # if we're showing previous invoices, also show previous - # credits and payments - if ( $self->enable_previous - and $self->can('_items_credits') - and $self->can('_items_payments') ) - { - #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments - - # credits - my $credittotal = 0; - foreach my $credit ( $self->_items_credits('trim_len'=>60) ) { + # if we're showing previous invoices, also show previous + # credits and payments + if ( $self->enable_previous + and $self->can('_items_credits') + and $self->can('_items_payments') ) + { + #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments + + # credits + my $credittotal = 0; + foreach my $credit ( $self->_items_credits('trim_len'=>60) ) { + + my $total; + $total->{'total_item'} = &$escape_function($credit->{'description'}); + $credittotal += $credit->{'amount'}; + $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'}; + $adjusttotal += $credit->{'amount'}; + if ( $multisection ) { + my $money = $old_latex ? '' : $money_char; + push @detail_items, { + ext_description => [], + ref => '', + quantity => '', + description => &$escape_function($credit->{'description'}), + amount => $money. $credit->{'amount'}, + product_code => '', + section => $adjust_section, + }; + } else { + push @total_items, $total; + } - my $total; - $total->{'total_item'} = &$escape_function($credit->{'description'}); - $credittotal += $credit->{'amount'}; - $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'}; - $adjusttotal += $credit->{'amount'}; - if ( $multisection ) { - my $money = $old_latex ? '' : $money_char; - push @detail_items, { - ext_description => [], - ref => '', - quantity => '', - description => &$escape_function($credit->{'description'}), - amount => $money. $credit->{'amount'}, - product_code => '', - section => $adjust_section, - }; - } else { - push @total_items, $total; } + $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal); - } - $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal); - - #credits (again) - foreach my $credit ( $self->_items_credits('trim_len'=>32) ) { - push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ]; - } + #credits (again) + foreach my $credit ( $self->_items_credits('trim_len'=>32) ) { + push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ]; + } - # payments - my $paymenttotal = 0; - foreach my $payment ( $self->_items_payments ) { - my $total = {}; - $total->{'total_item'} = &$escape_function($payment->{'description'}); - $paymenttotal += $payment->{'amount'}; - $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'}; - $adjusttotal += $payment->{'amount'}; + # payments + my $paymenttotal = 0; + foreach my $payment ( $self->_items_payments ) { + my $total = {}; + $total->{'total_item'} = &$escape_function($payment->{'description'}); + $paymenttotal += $payment->{'amount'}; + $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'}; + $adjusttotal += $payment->{'amount'}; + if ( $multisection ) { + my $money = $old_latex ? '' : $money_char; + push @detail_items, { + ext_description => [], + ref => '', + quantity => '', + description => &$escape_function($payment->{'description'}), + amount => $money. $payment->{'amount'}, + product_code => '', + section => $adjust_section, + }; + }else{ + push @total_items, $total; + } + push @buf, [ $payment->{'description'}, + $money_char. sprintf("%10.2f", $payment->{'amount'}), + ]; + } + $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal); + if ( $multisection ) { - my $money = $old_latex ? '' : $money_char; - push @detail_items, { - ext_description => [], - ref => '', - quantity => '', - description => &$escape_function($payment->{'description'}), - amount => $money. $payment->{'amount'}, - product_code => '', - section => $adjust_section, - }; - }else{ - push @total_items, $total; + $adjust_section->{'subtotal'} = $other_money_char. + sprintf('%.2f', $adjusttotal); + push @sections, $adjust_section + unless $adjust_section->{sort_weight}; } - push @buf, [ $payment->{'description'}, - $money_char. sprintf("%10.2f", $payment->{'amount'}), - ]; - } - $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal); - - if ( $multisection ) { - $adjust_section->{'subtotal'} = $other_money_char. - sprintf('%.2f', $adjusttotal); - push @sections, $adjust_section - unless $adjust_section->{sort_weight}; - } - # create Balance Due message - { - my $total; - $total->{'total_item'} = &$embolden_function($self->balance_due_msg); - $total->{'total_amount'} = - &$embolden_function( - $other_money_char. sprintf('%.2f', $summarypage - ? $self->charged + - $self->billing_balance - : $self->owed + $pr_total - ) - ); - if ( $multisection && !$adjust_section->{sort_weight} ) { - $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '. - $total->{'total_amount'}; - }else{ - push @total_items, $total; + # create Balance Due message + { + my $total; + $total->{'total_item'} = &$embolden_function($self->balance_due_msg); + $total->{'total_amount'} = + &$embolden_function( + $other_money_char. sprintf('%.2f', #why? $summarypage + # ? $self->charged + + # $self->billing_balance + # : + $self->owed + $pr_total + ) + ); + if ( $multisection && !$adjust_section->{sort_weight} ) { + $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '. + $total->{'total_amount'}; + }else{ + push @total_items, $total; + } + push @buf,['','-----------']; + push @buf,[$self->balance_due_msg, $money_char. + sprintf("%10.2f", $balance_due ) ]; } - push @buf,['','-----------']; - push @buf,[$self->balance_due_msg, $money_char. - sprintf("%10.2f", $balance_due ) ]; - } - if ( $conf->exists('previous_balance-show_credit') - and $cust_main->balance < 0 ) { - my $credit_total = { - 'total_item' => &$embolden_function($self->credit_balance_msg), - 'total_amount' => &$embolden_function( - $other_money_char. sprintf('%.2f', -$cust_main->balance) - ), - }; - if ( $multisection ) { - $adjust_section->{'posttotal'} .= $newline_token . - $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'}; - } - else { - push @total_items, $credit_total; + if ( $conf->exists('previous_balance-show_credit') + and $cust_main->balance < 0 ) { + my $credit_total = { + 'total_item' => &$embolden_function($self->credit_balance_msg), + 'total_amount' => &$embolden_function( + $other_money_char. sprintf('%.2f', -$cust_main->balance) + ), + }; + if ( $multisection ) { + $adjust_section->{'posttotal'} .= $newline_token . + $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'}; + } + else { + push @total_items, $credit_total; + } + push @buf,['','-----------']; + push @buf,[$self->credit_balance_msg, $money_char. + sprintf("%10.2f", -$cust_main->balance ) ]; } - push @buf,['','-----------']; - push @buf,[$self->credit_balance_msg, $money_char. - sprintf("%10.2f", -$cust_main->balance ) ]; } - } + + } #end of default total adding ! can('_items_total') if ( $multisection ) { if ( $conf->exists('svc_phone_sections') @@ -2042,6 +2068,11 @@ separate quantities, for some reason). =cut +sub _items_nontax { + my $self = shift; + grep { $_->pkgnum } $self->cust_bill_pkg; +} + sub _items_pkg { my $self = shift; my %options = @_; @@ -2049,7 +2080,7 @@ sub _items_pkg { warn "$me _items_pkg searching for all package line items\n" if $DEBUG > 1; - my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg; + my @cust_bill_pkg = $self->_items_nontax; warn "$me _items_pkg filtering line items\n" if $DEBUG > 1; @@ -2150,6 +2181,7 @@ sub _items_cust_bill_pkg { my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style # and location labels + my $locale = $cust_main->locale; my @b = (); my ($s, $r, $u) = ( undef, undef, undef ); @@ -2194,7 +2226,7 @@ sub _items_cust_bill_pkg { my $type = $display->type; - my $desc = $cust_bill_pkg->desc; + my $desc = $cust_bill_pkg->desc( $cust_main->locale ); $desc = substr($desc, 0, $maxlength). '...' if $format eq 'latex' && length($desc) > $maxlength; @@ -2260,13 +2292,16 @@ sub _items_cust_bill_pkg { || $cust_bill_pkg->recur_show_zero; my @d = (); + my $svc_label; unless ( $cust_pkg->part_pkg->hide_svc_detail || $cust_bill_pkg->hidden ) { - push @d, map &{$escape_function}($_), - $cust_pkg->h_labels_short($self->_date, undef, 'I') + my @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date, undef, 'I'); + push @d, @svc_labels unless $cust_bill_pkg->pkgpart_override; #don't redisplay services + $svc_label = $svc_labels[0]; if ( ! $cust_pkg->locationnum or $cust_pkg->locationnum != $cust_main->ship_locationnum ) { @@ -2296,6 +2331,7 @@ sub _items_cust_bill_pkg { unit_amount => $cust_bill_pkg->unitsetup, quantity => $cust_bill_pkg->quantity, ext_description => \@d, + svc_label => ($svc_label || ''), }; }; @@ -2318,16 +2354,25 @@ sub _items_cust_bill_pkg { my $description = ($is_summary && $type && $type eq 'U') ? "Usage charges" : $desc; + my $part_pkg = $cust_pkg->part_pkg; + #pry be a bit more efficient to look some of this conf stuff up # outside the loop unless ( $conf->exists('disable_line_item_date_ranges') - || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1) + || $part_pkg->option('disable_line_item_date_ranges',1) + || ! $cust_bill_pkg->sdate + || ! $cust_bill_pkg->edate ) { my $time_period; - my $date_style = $conf->config( 'cust_bill-line_item-date_style', + my $date_style = ''; + $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly', + $cust_main->agentnum + ) + if $part_pkg && $part_pkg->freq !~ /^1m?$/; + $date_style ||= $conf->config( 'cust_bill-line_item-date_style', $cust_main->agentnum - ); + ); if ( defined($date_style) && $date_style eq 'month_of' ) { $time_period = time2str('The month of %B', $cust_bill_pkg->sdate); } elsif ( defined($date_style) && $date_style eq 'X_month' ) { @@ -2345,6 +2390,7 @@ sub _items_cust_bill_pkg { my @d = (); my @seconds = (); # for display of usage info + my $svc_label = ''; #at least until cust_bill_pkg has "past" ranges in addition to #the "future" sdate/edate ones... see #3032 @@ -2353,7 +2399,7 @@ sub _items_cust_bill_pkg { push @dates, $prev->sdate if $prev; push @dates, undef if !$prev; - unless ( $cust_pkg->part_pkg->hide_svc_detail + unless ( $part_pkg->hide_svc_detail || $cust_bill_pkg->itemdesc || $cust_bill_pkg->hidden || $is_summary && $type && $type eq 'U' @@ -2363,11 +2409,11 @@ sub _items_cust_bill_pkg { warn "$me _items_cust_bill_pkg adding service details\n" if $DEBUG > 1; - push @d, map &{$escape_function}($_), - $cust_pkg->h_labels_short(@dates, 'I') - #$cust_bill_pkg->edate, - #$cust_bill_pkg->sdate) + my @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date, undef, 'I'); + push @d, @svc_labels unless $cust_bill_pkg->pkgpart_override; #don't redisplay services + $svc_label = $svc_labels[0]; warn "$me _items_cust_bill_pkg done adding service details\n" if $DEBUG > 1; @@ -2450,6 +2496,7 @@ sub _items_cust_bill_pkg { quantity => $cust_bill_pkg->quantity, %item_dates, ext_description => \@d, + svc_label => ($svc_label || ''), }; $r->{'seconds'} = \@seconds if grep {defined $_} @seconds; } diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm index c2ea0a61c..c8ad430b2 100644 --- a/FS/FS/UI/Web.pm +++ b/FS/FS/UI/Web.pm @@ -6,7 +6,7 @@ use Exporter; use Carp qw( confess ); use HTML::Entities; use FS::Conf; -use FS::Misc::DateTime qw( parse_datetime ); +use FS::Misc::DateTime qw( parse_datetime day_end ); use FS::Record qw(dbdef); use FS::cust_main; # are sql_balance and sql_date_balance in the right module? @@ -32,16 +32,16 @@ sub parse_beginning_ending { my $beginning = 0; if ( $cgi->param($prefix.'begin') =~ /^(\d+)$/ ) { $beginning = $1; - } elsif ( $cgi->param($prefix.'beginning') =~ /^([ 0-9\-\/]{1,64})$/ ) { + } elsif ( $cgi->param($prefix.'beginning') =~ /^([ 0-9\-\/\:]{1,64})$/ ) { $beginning = parse_datetime($1) || 0; } my $ending = 4294967295; #2^32-1 if ( $cgi->param($prefix.'end') =~ /^(\d+)$/ ) { $ending = $1 - 1; - } elsif ( $cgi->param($prefix.'ending') =~ /^([ 0-9\-\/]{1,64})$/ ) { - #probably need an option to turn off the + 86399 - $ending = parse_datetime($1) + 86399; + } elsif ( $cgi->param($prefix.'ending') =~ /^([ 0-9\-\/\:]{1,64})$/ ) { + $ending = parse_datetime($1); + $ending = day_end($ending) unless $ending =~ /:/; } ( $beginning, $ending ); @@ -235,20 +235,20 @@ sub cust_header { '(service) Name' => 'ship_contact', '(bill) Company' => 'company', '(service) Company' => 'ship_company', - 'Address 1' => 'address1', - 'Address 2' => 'address2', - 'City' => 'city', - 'State' => 'state', - 'Zip' => 'zip', + 'Address 1' => 'bill_address1', + 'Address 2' => 'bill_address2', + 'City' => 'bill_city', + 'State' => 'bill_state', + 'Zip' => 'bill_zip', 'Country' => 'country_full', 'Day phone' => 'daytime', # XXX should use msgcat, but how? 'Night phone' => 'night', # XXX should use msgcat, but how? 'Fax number' => 'fax', - '(bill) Address 1' => 'address1', - '(bill) Address 2' => 'address2', - '(bill) City' => 'city', - '(bill) State' => 'state', - '(bill) Zip' => 'zip', + '(bill) Address 1' => 'bill_address1', + '(bill) Address 2' => 'bill_address2', + '(bill) City' => 'bill_city', + '(bill) State' => 'bill_state', + '(bill) Zip' => 'bill_zip', '(bill) Country' => 'country_full', '(bill) Day phone' => 'daytime', # XXX should use msgcat, but how? '(bill) Night phone' => 'night', # XXX should use msgcat, but how? @@ -335,17 +335,21 @@ setting is supplied, the cust-fields configuration value. sub cust_sql_fields { my @fields = qw( last first company ); - push @fields, map "ship_$_", @fields; - push @fields, 'country'; +# push @fields, map "ship_$_", @fields; cust_header(@_); #inefficientish, but tiny lists and only run once per page - my @add_fields = qw( address1 address2 city state zip daytime night fax ); - push @fields, - grep { my $field = $_; grep { $_ eq $field } @cust_fields } - ( @add_fields, ( map "ship_$_", @add_fields ), 'payby' ); - + my @location_fields; + foreach my $field (qw( address1 address2 city state zip )) { + foreach my $pre ('bill_','ship_') { + if ( grep { $_ eq $pre.$field } @cust_fields ) { + push @location_fields, $pre.'location.'.$field.' AS '.$pre.$field; + } + } + } + + push @fields, 'payby' if grep { $_ eq 'payby'} @cust_fields; push @fields, 'agent_custid'; my @extra_fields = (); @@ -353,7 +357,71 @@ sub cust_sql_fields { push @extra_fields, FS::cust_main->balance_sql . " AS current_balance"; } - map("cust_main.$_", @fields), @extra_fields; + map("cust_main.$_", @fields), @location_fields, @extra_fields; +} + +=item join_cust_main [ TABLE[.CUSTNUM] ] [ LOCATION_TABLE[.LOCATIONNUM] ] + +Returns an SQL join phrase for the FROM clause so that the fields listed +in L will be available. Currently joins to cust_main +itself, as well as cust_location (under the aliases 'bill_location' and +'ship_location') if address fields are needed. L should have +been called already. + +All of these will be left joins; if you want to exclude rows with no linked +cust_main record (or bill_location/ship_location), you can do so in the +WHERE clause. + +TABLE is the table containing the custnum field. If CUSTNUM (a field name +in that table) is specified, that field will be joined to cust_main.custnum. +Otherwise, this function will assume the field is named "custnum". If the +argument isn't present at all, the join will just say "USING (custnum)", +which might work. + +As a special case, if TABLE is 'cust_main', only the joins to cust_location +will be returned. + +LOCATION_TABLE is an optional table name to use for joining ship_location, +in case your query also includes package information and you want the +"service address" columns to reflect package addresses. + +=cut + +sub join_cust_main { + my ($cust_table, $location_table) = @_; + my ($custnum, $locationnum); + ($cust_table, $custnum) = split(/\./, $cust_table); + $custnum ||= 'custnum'; + ($location_table, $locationnum) = split(/\./, $location_table); + $locationnum ||= 'locationnum'; + + my $sql = ''; + if ( $cust_table ) { + $sql = " LEFT JOIN cust_main ON (cust_main.custnum = $cust_table.$custnum)" + unless $cust_table eq 'cust_main'; + } else { + $sql = " LEFT JOIN cust_main USING (custnum)"; + } + + if ( !@cust_fields or grep /^bill_/, @cust_fields ) { + + $sql .= ' LEFT JOIN cust_location bill_location'. + ' ON (bill_location.locationnum = cust_main.bill_locationnum)'; + + } + + if ( !@cust_fields or grep /^ship_/, @cust_fields ) { + + if (!$location_table) { + $location_table = 'cust_main'; + $locationnum = 'ship_locationnum'; + } + + $sql .= ' LEFT JOIN cust_location ship_location'. + " ON (ship_location.locationnum = $location_table.$locationnum) "; + } + + $sql; } =item cust_fields OBJECT [ CUST_FIELDS_VALUE ] @@ -404,23 +472,26 @@ sub cust_fields_subs { my $unlinked_warn = 0; return map { my $f = $_; - if( $unlinked_warn++ ) { + if ( $unlinked_warn++ ) { + sub { my $record = shift; - if( $record->custnum ) { - $record->$f(@_); - } - else { + if ( $record->custnum ) { + encode_entities( $record->$f(@_) ); + } else { '(unlinked)' }; - } - } - else { + }; + + } else { + sub { my $record = shift; - $record->$f(@_) if $record->custnum; - } + $record->custnum ? encode_entities( $record->$f(@_) ) : ''; + }; + } + } @cust_fields; } @@ -510,7 +581,7 @@ use vars qw($DEBUG); use Carp; use Storable qw(nfreeze); use MIME::Base64; -use JSON; +use JSON::XS; use FS::UID qw(getotaker); use FS::Record qw(qsearchs); use FS::queue; @@ -655,10 +726,7 @@ sub job_status { @return = ( 'error', $job ? $job->statustext : $jobnum ); } - #to_json(\@return); #waiting on deb 5.0 for new JSON.pm? - #silence the warning though - my $to_json = JSON->can('to_json') || JSON->can('objToJson'); - &$to_json(\@return); + encode_json \@return; } diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index fea53a235..cda3198eb 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -294,6 +294,9 @@ sub upgrade_data { #insert default tower_sector if not present 'tower' => [], + #repair improperly deleted services + 'cust_svc' => [], + #routernum/blocknum 'svc_broadband' => [], diff --git a/FS/FS/access_right.pm b/FS/FS/access_right.pm index 397b456ce..d370ba5d1 100644 --- a/FS/FS/access_right.pm +++ b/FS/FS/access_right.pm @@ -198,6 +198,10 @@ sub _upgrade_data { # class method 'New prospect' => 'Generate quotation', 'Delete invoices' => 'Void invoices', 'List invoices' => 'List quotations', + 'Post credit' => 'Credit line items', + #'View customer tax exemptions' => 'Edit customer tax exemptions', + 'Edit customer' => 'Edit customer tax exemptions', + 'Edit package definitions' => 'Bulk edit package definitions', 'List services' => [ 'Services: Accounts', 'Services: Domains', @@ -218,12 +222,17 @@ sub _upgrade_data { # class method 'Services: Accounts' => 'Services: Accounts: Advanced search', 'Services: Wireless broadband services' => 'Services: Wireless broadband services: Advanced search', 'Services: Hardware' => 'Services: Hardware: Advanced search', + 'Services: Phone numbers' => 'Services: Phone numbers: Advanced search', 'List rating data' => [ 'Usage: RADIUS sessions', 'Usage: Call Detail Records (CDRs)', 'Usage: Unrateable CDRs', ], - ; + 'Provision customer service' => [ 'Edit password' ], + 'Financial reports' => [ 'Employees: Commission Report', + 'Employees: Audit Report', + ], +; foreach my $old_acl ( keys %onetime ) { diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index fdec921ee..3ebe6c420 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -11,6 +11,7 @@ use Date::Parse; use Date::Format; use Time::Local; use List::Util qw( first min ); +use Text::CSV_XS; use FS::UID qw( dbh ); use FS::Conf; use FS::Record qw( qsearch qsearchs ); @@ -325,6 +326,10 @@ sub check { $self->billsec( $self->enddate - $self->answerdate ); } + if ( ! $self->enddate && $self->startdate && $self->duration ) { + $self->enddate( $self->startdate + $self->duration ); + } + $self->set_charged_party; #check the foreign keys even? @@ -421,12 +426,25 @@ sub set_charged_party { Sets the status to the provided string. If there is an error, returns the error, otherwise returns false. +If status is being changed from 'rated' to some other status, also removes +any usage allocations to this CDR. + =cut sub set_status { my($self, $status) = @_; + my $old_status = $self->freesidestatus; $self->freesidestatus($status); - $self->replace; + my $error = $self->replace; + if ( $old_status eq 'rated' and $status ne 'done' ) { + # deallocate any usage + foreach (qsearch('cdr_cust_pkg_usage', {acctid => $self->acctid})) { + my $cust_pkg_usage = $_->cust_pkg_usage; + $cust_pkg_usage->set('minutes', $cust_pkg_usage->minutes + $_->minutes); + $error ||= $cust_pkg_usage->replace || $_->delete; + } + } + $error; } =item set_status_and_rated_price STATUS RATED_PRICE [ SVCNUM [ OPTION => VALUE ... ] ] @@ -573,7 +591,7 @@ reference of the number of included minutes and will be decremented by the rated minutes of this CDR. region_group_included_minutes_hashref is required for prefix price plans which -have included minues (otehrwise unused/ignored). It should be set to an empty +have included minues (otherwise unused/ignored). It should be set to an empty hashref at the start of a month's rating and then preserved across CDRs. =cut @@ -598,6 +616,7 @@ our %interval_cache = (); # for timed rates sub rate_prefix { my( $self, %opt ) = @_; my $part_pkg = $opt{'part_pkg'} or return "No part_pkg specified"; + my $cust_pkg = $opt{'cust_pkg'}; my $da_rewrote = 0; # this will result in those CDRs being marked as done... is that @@ -625,7 +644,34 @@ sub rate_prefix { ); } + if ( $part_pkg->option_cacheable('skip_same_customer') + and ! $self->is_tollfree ) { + my ($dst_countrycode, $dst_number) = $self->parse_number( + column => 'dst', + international_prefix => $part_pkg->option_cacheable('international_prefix'), + domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'), + ); + my $dst_same_cust = FS::Record->scalar_sql( + 'SELECT COUNT(svc_phone.svcnum) AS count '. + 'FROM cust_pkg ' . + 'JOIN cust_svc USING (pkgnum) ' . + 'JOIN svc_phone USING (svcnum) ' . + 'WHERE svc_phone.countrycode = ' . dbh->quote($dst_countrycode) . + ' AND svc_phone.phonenum = ' . dbh->quote($dst_number) . + ' AND cust_pkg.custnum = ' . $cust_pkg->custnum, + ); + if ( $dst_same_cust > 0 ) { + warn "not charging for CDR (same source and destination customer)\n" if $DEBUG; + return $self->set_status_and_rated_price( 'skipped', + 0, + $opt{'svcnum'}, + ); + } + } + + + ### # look up rate details based on called station id # (or calling station id for toll free calls) @@ -823,11 +869,6 @@ sub rate_prefix { $seconds_left -= $charge_sec; - my $included_min = $opt{'region_group_included_min_hashref'} || {}; - - $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included - unless exists $included_min->{$regionnum}{$ratetimenum}; - my $granularity = $rate_detail->sec_granularity; my $minutes; @@ -845,20 +886,40 @@ sub rate_prefix { $seconds += $charge_sec; + if ( $rate_detail->min_included ) { + # the old, kind of deprecated way to do this + my $included_min = $opt{'region_group_included_min_hashref'} || {}; - my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0; + # by default, set the included minutes for this region/time to + # what's in the rate_detail + $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included + unless exists $included_min->{$regionnum}{$ratetimenum}; - ${$opt{region_group_included_min}} -= $minutes - if $region_group && $rate_detail->region_group; + # the way that doesn't work + #my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0; + + #${$opt{region_group_included_min}} -= $minutes + # if $region_group && $rate_detail->region_group; + + if ( $included_min->{$regionnum}{$ratetimenum} > $minutes ) { + $charge_sec = 0; + $included_min->{$regionnum}{$ratetimenum} -= $minutes; + } else { + $charge_sec -= ($included_min->{$regionnum}{$ratetimenum} * 60); + $included_min->{$regionnum}{$ratetimenum} = 0; + } + } else { + # the new way! + my $applied_min = $cust_pkg->apply_usage( + 'cdr' => $self, + 'rate_detail' => $rate_detail, + 'minutes' => $minutes + ); + # for now, usage pools deal only in whole minutes + $charge_sec -= $applied_min * 60; + } - $included_min->{$regionnum}{$ratetimenum} -= $minutes; - if ( - $included_min->{$regionnum}{$ratetimenum} <= 0 - && ( ${$opt{region_group_included_min}} <= 0 - || ! $rate_detail->region_group - ) - ) - { + if ( $charge_sec > 0 ) { #NOW do connection charges here... right? #my $conn_seconds = min($seconds_left, $rate_detail->conn_sec); @@ -871,16 +932,9 @@ sub rate_prefix { } #should preserve (display?) this - my $charge_min = 0 - $included_min->{$regionnum}{$ratetimenum} - ( $conn_seconds / 60 ); - $included_min->{$regionnum}{$ratetimenum} = 0; + my $charge_min = ( $charge_sec - $conn_seconds ) / 60; $charge += ($rate_detail->min_charge * $charge_min) if $charge_min > 0; #still not rounded - } elsif ( ${$opt{region_group_included_min}} > 0 - && $region_group - && $rate_detail->region_group - ) - { - $included_min->{$regionnum}{$ratetimenum} = 0 } # choose next rate_detail @@ -1168,6 +1222,8 @@ sub export_formats { length($price) ? ($opt{money_char} . $price) : ''; }; + my $src_sub = sub { $_[0]->clid || $_[0]->src }; + %export_formats = ( 'simple' => [ sub { time2str($date_format, shift->calldate_unix ) }, #DATE @@ -1182,7 +1238,7 @@ sub export_formats { sub { time2str($date_format, shift->calldate_unix ) }, #DATE sub { time2str('%r', shift->calldate_unix ) }, #TIME #'userfield', #USER - 'src', #called from + $src_sub, #called from 'dst', #NUMBER_DIALED $duration_sub, #DURATION #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE @@ -1191,7 +1247,7 @@ sub export_formats { 'accountcode_simple' => [ sub { time2str($date_format, shift->calldate_unix ) }, #DATE sub { time2str('%r', shift->calldate_unix ) }, #TIME - 'src', #called from + $src_sub, #called from 'accountcode', #NUMBER_DIALED $duration_sub, #DURATION $price_sub, @@ -1199,14 +1255,14 @@ sub export_formats { 'sum_duration' => [ # for summary formats, the CDR is a fictitious object containing the # total billsec and the phone number of the service - 'src', + $src_sub, sub { my($cdr, %opt) = @_; $opt{ratename} }, sub { my($cdr, %opt) = @_; $opt{count} }, sub { my($cdr, %opt) = @_; int($opt{seconds}/60).'m' }, $price_sub, ], 'sum_count' => [ - 'src', + $src_sub, sub { my($cdr, %opt) = @_; $opt{ratename} }, sub { my($cdr, %opt) = @_; $opt{count} }, $price_sub, @@ -1240,7 +1296,7 @@ sub export_formats { $price_sub, ], ); - $export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ]; + $export_formats{'source_default'} = [ $src_sub, @{ $export_formats{'default'} }, ]; $export_formats{'accountcode_default'} = [ @{ $export_formats{'default'} }[0,1], 'accountcode', @@ -1248,7 +1304,7 @@ sub export_formats { ]; my @default = @{ $export_formats{'default'} }; $export_formats{'description_default'} = - [ 'src', @default[0..2], + [ $src_sub, @default[0..2], sub { my($cdr, %opt) = @_; $cdr->description }, @default[4,5] ]; @@ -1286,8 +1342,6 @@ sub downstream_csv { #$opt{'money_char'} ||= $conf->config('money_char') || '$'; $opt{'money_char'} ||= FS::Conf->new->config('money_char') || '$'; - eval "use Text::CSV_XS;"; - die $@ if $@; my $csv = new Text::CSV_XS; my @columns = @@ -1578,6 +1632,11 @@ my %import_options = ( keys %cdr_info }, + 'format_asn_formats' => + { map { $_ => $cdr_info{$_}->{'asn_format'}; } + keys %cdr_info + }, + 'format_row_callbacks' => { map { $_ => $cdr_info{$_}->{'row_callback'}; } keys %cdr_info }, diff --git a/FS/FS/cdr/asterisk_skip_clid.pm b/FS/FS/cdr/asterisk_skip_clid.pm new file mode 100644 index 000000000..1a105b399 --- /dev/null +++ b/FS/FS/cdr/asterisk_skip_clid.pm @@ -0,0 +1,45 @@ +package FS::cdr::asterisk_skip_clid; + +use strict; +use vars qw(@ISA %info); +use FS::cdr qw(_cdr_date_parser_maker); + +@ISA = qw(FS::cdr); + +#http://www.the-asterisk-book.com/unstable/funktionen-cdr.html +my %amaflags = ( + DEFAULT => 0, + OMIT => 1, #asterisk 1.4+ + IGNORE => 1, #asterisk 1.2 + BILLING => 2, #asterisk 1.4+ + BILL => 2, #asterisk 1.2 + DOCUMENTATION => 3, + #? '' => 0, +); + +%info = ( + 'name' => 'Asterisk (skip Caller ID)', + 'weight' => 11, + 'import_fields' => [ + 'accountcode', + 'src', + 'dst', + 'dcontext', + 'SKIP_clid', + 'channel', + 'dstchannel', + 'lastapp', + 'lastdata', + _cdr_date_parser_maker('startdate'), + _cdr_date_parser_maker('answerdate'), + _cdr_date_parser_maker('enddate'), + 'duration', + 'billsec', + 'disposition', + sub { my($cdr, $amaflags) = @_; $cdr->amaflags($amaflags{$amaflags}); }, + 'uniqueid', + 'userfield', + ], +); + +1; diff --git a/FS/FS/cdr/gsm_tap3_12.pm b/FS/FS/cdr/gsm_tap3_12.pm new file mode 100644 index 000000000..275e7b35c --- /dev/null +++ b/FS/FS/cdr/gsm_tap3_12.pm @@ -0,0 +1,2079 @@ +package FS::cdr::gsm_tap3_12; +use base qw( FS::cdr ); + +use strict; +use vars qw( %info %TZ ); +use Time::Local; +#use Data::Dumper; + +#false laziness w/huawei_softx3000.pm +%TZ = ( + '+0000' => 'XXX-0', + '+0100' => 'XXX-1', + '+0200' => 'XXX-2', + '+0300' => 'XXX-3', + '+0400' => 'XXX-4', + '+0500' => 'XXX-5', + '+0600' => 'XXX-6', + '+0700' => 'XXX-7', + '+0800' => 'XXX-8', + '+0900' => 'XXX-9', + '+1000' => 'XXX-10', + '+1100' => 'XXX-11', + '+1200' => 'XXX-12', + '-0000' => 'XXX+0', + '-0100' => 'XXX+1', + '-0200' => 'XXX+2', + '-0300' => 'XXX+3', + '-0400' => 'XXX+4', + '-0500' => 'XXX+5', + '-0600' => 'XXX+6', + '-0700' => 'XXX+7', + '-0800' => 'XXX+8', + '-0900' => 'XXX+9', + '-1000' => 'XXX+10', + '-1100' => 'XXX+11', + '-1200' => 'XXX+12', +); + +%info = ( + 'name' => 'GSM TAP3 release 12', + 'weight' => 50, + 'type' => 'asn.1', + 'import_fields' => [], + 'asn_format' => { + 'spec' => _asn_spec(), + 'macro' => 'TransferBatch', #XXX & skip the Notification ones? + 'header_buffer' => sub { + my $TransferBatch = shift; + + my $networkInfo = $TransferBatch->{networkInfo}; + + my $recEntityInfo = $networkInfo->{recEntityInfo}; + my %recEntity = map { $_->{recEntityCode} => $_->{recEntityId} } @$recEntityInfo; + + my $utcTimeOffsetInfo = $networkInfo->{utcTimeOffsetInfo}; + my %utcTimeOffset = map { $_->{utcTimeOffsetCode} => $_->{utcTimeOffset} } @$utcTimeOffsetInfo; + + { recEntity => \%recEntity, + utcTimeOffset => \%utcTimeOffset, + tapDecimalPlaces => $TransferBatch->{accountingInfo}{tapDecimalPlaces}, + }; + }, + 'arrayref' => sub { shift->{'callEventDetails'}; }, + 'map' => { + 'startdate' => sub { my($row, $buffer) = @_; + my $callinfo = $row->{mobileOriginatedCall}{basicCallInformation}; + my $timestamp = $callinfo->{callEventStartTimeStamp}; + + my $localTimeStamp = $timestamp->{localTimeStamp}; + $localTimeStamp =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/ + or die "unparsable timestamp: $localTimeStamp\n"; #. Dumper($callinfo); + my($year, $mon, $day, $hour, $min, $sec) = ($1, $2, $3, $4, $5, $6); + + my $utcTimeOffsetCode = $timestamp->{utcTimeOffsetCode}; + my $utcTimeOffset = $buffer->{utcTimeOffset}{ $utcTimeOffsetCode }; + local($ENV{TZ}) = $TZ{ $utcTimeOffset }; + + timelocal($sec, $min, $hour, $day, $mon-1, $year); + }, + 'duration' => sub { shift->{mobileOriginatedCall}{basicCallInformation}{totalCallEventDuration} }, + 'billsec' => sub { shift->{mobileOriginatedCall}{basicCallInformation}{totalCallEventDuration} }, #same.. + 'src' => sub { shift->{mobileOriginatedCall}{basicCallInformation}{chargeableSubscriber}{simChargeableSubscriber}{msisdn} }, + 'charged_party_imsi' => sub { shift->{mobileOriginatedCall}{basicCallInformation}{chargeableSubscriber}{simChargeableSubscriber}{imsi} }, + 'dst' => sub { shift->{mobileOriginatedCall}{basicCallInformation}{destination}{calledNumber} }, #dialledDigits? + 'carrierid' => sub { my( $row, $buffer ) = @_; + my $recEntityCode = $row->{mobileOriginatedCall}{locationInformation}{networkLocation}{recEntityCode}; + $buffer->{recEntity}{ $recEntityCode }; + }, + 'userfield' => sub { shift->{mobileOriginatedCall}{operatorSpecInformation}[0] }, + 'servicecode' => sub { shift->{mobileOriginatedCall}{basicServiceUsedList}[0]{basicService}{serviceCode}{teleServiceCode} }, + 'upstream_price' => sub { my($row, $buffer) = @_; + sprintf('%.'.$buffer->{tapDecimalPlaces}.'f', + $row->{mobileOriginatedCall}{basicServiceUsedList}[0]{chargeInformationList}[0]{chargeDetailList}[0]{charge} + / ( 10 ** $buffer->{tapDecimalPlaces} ) + ) + }, + 'calltypenum' => sub { shift->{mobileOriginatedCall}{basicServiceUsedList}[0]{chargeInformationList}[0]{callTypeGroup}{callTypelevel1} }, + 'quantity' => sub { shift->{mobileOriginatedCall}{basicServiceUsedList}[0]{chargeInformationList}[0]{chargedUnits} }, + 'quantity_able' => sub { shift->{mobileOriginatedCall}{basicServiceUsedList}[0]{chargeInformationList}[0]{chargeableUnits} }, + }, + }, +); + +#accepts qsearch parameters as a hash or list of name/value pairs, but not +#old-style qsearch('cdr', { field=>'value' }) + +use Date::Format; +use FS::Conf; +sub tap3_12_export { + my %qsearch = (); + if ( ref($_[0]) eq 'HASH' ) { + %qsearch = %{ $_[0] }; + } else { + %qsearch = @_; + } + + #if these get huge we might need to get a count and do a paged search + my @cdrs = qsearch({ 'table'=>'cdr', %qsearch, 'order_by'=>'calldate ASC' }); + + my $conf = new FS::Conf; + + eval "use Convert::ASN1"; + die $@ if $@; + + my $asn = Convert::ASN1->new; + $asn->prepare( _asn_spec() ) or die $asn->error; + + my $TransferBatch = $asn->find('TransferBatch') or die $asn->error; + + my %hash = _TransferBatch(); #static information etc. + + my $now = time; + my $utcTimeOffset = time2str('%z', $now); + + ### + # accountingInfo + ### + + #mandatory + $hash{localCurrency} = $conf->config('currency') || 'USD'; + + ### + # batchControlInfo + ### + + #optional + $hash{batchControlInfo}->{fileCreationTimeStamp} = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $now), + 'utcTimeOffset' => $utcTimeOffset, + }; + + #The timestamp used to select calls for transfer. All call records available prior to the timestamp are transferred. + # This gives an indication to the HPMN as to how ‘up-to-date’ the information is. + $hash{batchControlInfo}->{transferCutOffTimeStamp} = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $cdrs[-1]->calldate_unix ), + 'utcTimeOffset' => $utcTimeOffset, + }; + + #The date and time at which the file was made available to the Recipient PMN. + # Physically this will normally be the timestamp when the file transfer + # commenced to the Recipient PMN, i.e. start of push, however on some systems + # this will be the timestamp when the file was made available to be pulled. + $hash{batchControlInfo}->{fileAvailableTimeStamp} = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $now), + 'utcTimeOffset' => $utcTimeOffset, + }; + + # A unique identifier used to determine the network which is the Sender of the data. + # The full list of codes in use is given in TADIG PRD TD.13: PMN Naming Conventions. + $hash{batchControlInfo}->{sender} = $conf->config('cdr-gsm_tap3-sender') || 'ZZZZZ'; #reserved: Y*, ZO-ZZ + + #XXX customer or agent field of some sort + # A unique identifier used to determine which network the data is being sent to, + # i.e. the Recipient. + # Derivation: GSM Association PRD TD.13: PMN Naming Conventions. + $hash{batchControlInfo}->{recipient} = 'GNQHT'; + + #XXX + #A unique reference which identifies each TAP Data Interchange sent by one PMN to another, specific, PMN. + # The sequence commences at 1 and is incremented by one for each subsequent TAP Data Interchange sent by the Sender PMN to a particular Recipient PMN. + # Separate sequence numbering must be used for Test Data and Chargeable Data. Having reached the maximum value (99999) the number must recycle to 1. + $hash{batchControlInfo}->{fileSequenceNumber} = '00178'; + + ### + # networkInfo + ### + + $hash{networkInfo}->{utcTimeOffsetInfo}[0]{utcTimeOffset} = $utcTimeOffset; + + #XXX recording entity IDs, referenced by recEntityCode + #$hash->{networkInfo}->{recEntityInfo}[0]{recEntityId} = '340010100'; + #$hash->{networkInfo}->{recEntityInfo}[1]{recEntityId} = '240556000000'; + + ### + # auditControlInfo + ### + + #mandatory + $hash{auditControlInfo}->{callEventDetailsCount} = scalar(@cdrs); + + #these two are optional + $hash{auditControlInfo}->{earliestCallTimeStamp} = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $cdrs[0]->calldate_unix), + 'utcTimeOffset' => $utcTimeOffset, + }; + $hash{auditControlInfo}->{latestCallTimeStamp} = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $cdrs[-1]->calldate_unix), + 'utcTimeOffset' => $utcTimeOffset, + }; + + #mandatory + my $totalCharge = 0; + $totalCharge += $_->rated_price foreach @cdrs; + $hash{totalCharge} = sprintf('%.5f', $totalCharge); + + ### + # callEventDetails + ### + + $hash{callEventDetails} = [ map tap3_12_export_cdr($_), @cdrs ]; + + ### + + $TransferBatch->encode( \%hash ); + +} + +sub _TransferBatch { + + #accounting related information + 'accountingInfo' => { + #mandatory + #'localCurrency' => 'USD', + 'tapDecimalPlaces' => 5, + 'currencyConversionInfo' => [ + { + 'numberOfDecimalPlaces' => 5, + 'exchangeRate' => 152549, #XXX ??? "exchange rate +VAT" ? + 'exchangeRateCode' => 1 + } + ], + #optional: may conditionally include taxation and discounting tables, and, optionally, TAP currency + }, + + 'batchControlInfo' => { + #mandatory + 'specificationVersionNumber' => 3, + 'releaseVersionNumber' => 12, + + #'sender' => 'MDGTM', + #'recipient' => 'GNQHT', + #'fileSequenceNumber' => '00178', + + #'transferCutOffTimeStamp' => { + # 'localTimeStamp' => '20121230050222', + # 'utcTimeOffset' => '+0300' + # }, + #'fileAvailableTimeStamp' => { + # 'localTimeStamp' => '20121230035052', + # 'utcTimeOffset' => '+0100' + # } + + #optional + #'fileCreationTimeStamp' => { + # 'localTimeStamp' => '20121230050222', + # 'utcTimeOffset' => '+0300' + # }, + + #optional: file type indicator which will only be present where the file represents test data + #optional: RAP File Sequence Number (used where the batch has previously been returned with a fatal error and is now being resubmitted) (not fileSequenceNumber?) + + #optional: beyond the scope of TAP and has been bilaterally agreed + #'operatorSpecInformation' => [ + # '', # '|File proc MTH LUXMA: 1285348027|' Operator Specific Information + # # probably just leave out + # ], + + + }, + + #Network Information is a group of related information which pertains to the Sender PMN + 'networkInfo' => { + #must be present where Recording Entity Codes are present within the TAP file + 'recEntityInfo' => [ + { + 'recEntityCode' => 1, + 'recEntityType' => 1, #MSC + #'recEntityId' => '340010100', + }, + { + 'recEntityCode' => 2, + 'recEntityType' => 2, #SMSC + #'recEntityId' => '240556000000', + }, + ], + #mandatory + 'utcTimeOffsetInfo' => [ + { + 'utcTimeOffsetCode' => 1, + #'utcTimeOffset' => '+0300', + } + ] + }, + + #identifies the end of the Transfer Batch + 'auditControlInfo' => { + #mandatory + #'callEventDetailsCount' => 4, + 'totalTaxValue' => 0, + 'totalDiscountValue' => 0, + #'totalCharge' => 50474, + + #these two are optional + #'earliestCallTimeStamp' => { + # 'localTimeStamp' => '20121229102501', + # 'utcTimeOffset' => '+0300' + # }, + #'latestCallTimeStamp' => { + # 'localTimeStamp' => '20121229102807', + # 'utcTimeOffset' => '+0300' + # } + #optional: beyond the scope of TAP and has been bilaterally agreed + #'operatorSpecInformation' => [ + # '', + # ], + }, +} + +sub tap3_12_export_cdr { + my $self = shift; + + #one of Mobile Originated Call, Mobile Terminated Call, Mobile Session, Messaging Event, Supplementary Service Event, Service Centre Usage, GPRS Call, Content Transaction or Location Service + # Each occurrence must have no more than one of these present + + { #either tele or bearer service usage originated by the mobile subscription (others?) + 'mobileOriginatedCall' => { + + #identifies the Network Location, which includes the MSC responsible for handling + # the call and, where appropriate, the Geographical Location of the mobile + 'locationInformation' => { + 'networkLocation' => { + 'recEntityCode' => $self->carrierid, #XXX Recording Entity (per 2.5, from "Reference Tables") + } + }, + + #Operator Specific Information: beyond the scope of TAP and has been bilaterally agreed + 'operatorSpecInformation' => [ + $self->userfield, ##'|Seq: 178 Loc: 1|' + ], + + #The type of service used together with all related charging information + 'basicServiceUsedList' => [ + { + #identifies the actual Basic Service used + 'basicService' => { + #one of Teleservice Code or Bearer Service Code as determined by the service type used + 'serviceCode' => { + #XXX + #00 All teleservices + #10 All Speech transmission services + #11 Telephony + #12 Emergency calls + #20 All SMS Services + #21 Short Message MT/PP + #22 Short Message MO/PP + #60 All Fax Services + #61 Facsimile Group 3 & alternative speech + #62 Automatic Facsimile Group 3 + #63 Automatic Facsimile Group 4 + #70 All data teleservices (compound) + #80 All teleservices except SMS (compound) + #90 All voice group call services + #91 Voice group call + #92 Voice broadcast call + 'teleServiceCode' => $self->servicecode, #'11' + + #Bearer Service Code + # Must be present within group Service Code where the type of service used + # was a bearer service. Must not be present when the type of service used + # was a tele service and, therefore, Teleservice Code is present. + # Group Bearer Codes, identifiable by the description ‘All’, should only + # be used where details of the specific services affected are not + # available from the network. + #00 All Bearer Services + #20 All Data Circuit Asynchronous Services + #21 Duplex Asynch. 300bps data circuit + #22 Duplex Asynch. 1200bps data circuit + #23 Duplex Asynch. 1200/75bps data circuit + #24 Duplex Asynch. 2400bps data circuit + #25 Duplex Asynch. 4800bps data circuit + #26 Duplex Asynch. 9600bps data circuit + #27 General Data Circuit Asynchronous Service + #30 All Data Circuit Synchronous Services + #32 Duplex Synch. 1200bps data circuit + #34 Duplex Synch. 2400bps data circuit + #35 Duplex Synch. 4800bps data circuit + #36 Duplex Synch. 9600bps data circuit + #37 General Data Circuit Synchronous Service + #40 All Dedicated PAD Access Services + #41 Duplex Asynch. 300bps PAD access + #42 Duplex Asynch. 1200bps PAD access + #43 Duplex Asynch. 1200/75bps PAD access + #44 Duplex Asynch. 2400bps PAD access + #45 Duplex Asynch. 4800bps PAD access + #46 Duplex Asynch. 9600bps PAD access + #47 General PAD Access Service + #50 All Dedicated Packet Access Services + #54 Duplex Synch. 2400bps PAD access + #55 Duplex Synch. 4800bps PAD access + #56 Duplex Synch. 9600bps PAD access + #57 General Packet Access Service + #60 All Alternat Speech/Asynchronous Services + #70 All Alternate Speech/Synchronous Services + #80 All Speech followed by Data Asynchronous Services + #90 All Speech followed by Data Synchronous Services + #A0 All Data Circuit Asynchronous Services (compound) + #B0 All Data Circuit Synchronous Services (compound) + #C0 All Asynchronous Services (compound) + } + #conditionally also contain the following for UMTS: Transparency Indicator, Fixed Network User + # Rate, User Protocol Indicator, Guaranteed Bit Rate and Maximum Bit Rate + }, + + #Charge information is provided for all chargeable elements except within Messaging Event and Mobile Session call events + # must contain Charged Item and at least one occurrence of Charge Detail + 'chargeInformationList' => [ + { + #XXX + #mandatory + # the charging principle applied and the unitisation of Chargeable Units. It + # is not intended to identify the service used. + #A: Call set up attempt + #C: Content + #D: Duration based charge + #E: Event based charge + #F: Fixed (one-off) charge + #L: Calendar (for example daily usage charge) + #V: Volume (outgoing) based charge + #W: Volume (incoming) based charge + #X: Volume (total volume) based charge + #(?? fields to be used as a basis for the calculation of the correct Charge + # A: Chargeable Units (if present) + # D,V,W,X: Chargeable Units + # C: Depends on the content + # E: Not Applicable + # F: Not Applicable + # L: Call Event Start Timestamp) + 'chargedItem' => 'D', + + # the IOT used by the VPMN to price the call + 'callTypeGroup' => { + + #The highest category call type in respect of the destination of the call + #0: Unknown/Not Applicable + #1: National + #2: International + #10: HGGSN/HP-GW + #11: VGGSN/VP-GW + #12: Other GGSN/Other P-GW + #100: WLAN + 'callTypeLevel1' => $self->calltypenum, + + #the sub category of Call Type Level 1 + #0: Unknown/Not Applicable + #1: Mobile + #2: PSTN + #3: Non Geographic + #4: Premium Rate + #5: Satellite destination + #6: Forwarded call + #7: Non forwarded call + #10: Broadband + #11: Narrowband + #12: Conversational + #13: Streaming + #14: Interactive + #15: Background + 'callTypeLevel2' => 0, + + #the sub category of Call Type Level 2 + 'callTypeLevel3' => 0, + }, + + #mandatory, at least one occurence must be present + #A repeating group detailing the Charge and/or charge element + # Note that, where a Charge has been levied, even where that Charge is zero, + # there must be one occurance, and only one, with a Charge Type of '00' + 'chargeDetailList' => [ + { + #mandatory + # after discounts have been deducted but before any tax is added + 'charge' => $self->rated_price * 100000, #XXX numberOfDecimalPlaces + + #mandatory + # the type of charge represented + #00: Total charge for Charge Information (the invoiceable value) + #01: Airtime charge + #02: reserved + #03: Toll charge + #04: Directory assistance + #05–20: reserved + #21: VPMN surcharge + #50: Total charge for Charge Information according to the published IOT + # Note that the use of value 50 is only for use by bilateral agreement, use without + # bilateral agreement can be treated as per reserved values, that is ‘out of range’ + #69–99: reserved + 'chargeType' => '00', + + #conditional + # the number of units which are chargeable within the Charge Detail, this may not + # correspond to the number of rounded units charged. + # The item Charged Item defines what the units represent. + 'chargeableUnits' => $self->quantity_able, + + #optional + # the rounded number of units which are actually charged for + 'chargedUnits' => $self->quantity, + } + ], + 'exchangeRateCode' => 1, #from header + } + ] + } + ], + + #MO Basic Call Information provides the basic detail of who made the call and where to in respect of mobile originated traffic. + 'basicCallInformation' => { + #mandatory + # the identification of the chargeable subscriber. + # The group must contain either the IMSI or the MIN of the Chargeable Subscriber, but not both. + 'chargeableSubscriber' => { + 'simChargeableSubscriber' => { + 'msisdn' => $self->charged_party, #src + 'imsi' => $self->charged_party_imsi, + } + }, + # the start of the call event + 'callEventStartTimeStamp' => { + 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $self->startdate), + 'utcTimeOffsetCode' => 1 + }, + + # the actual total duration of a call event as a number of seconds + 'totalCallEventDuration' => $self->duration, + + #conditional + # the number dialled by the subscriber (Called Number) + # or the SMSC Address in case of SMS usage or in cases involving supplementary services + # such as call forwarding or transfer etc., the number to which the call is routed + 'destination' => { + #the international representation of the destination + 'calledNumber' => $self->dst, + + #the actual digits as dialled by the subscriber, i.e. unmodified, in establishing a call + # This will contain ‘+’ and ‘#’ where appropriate. + #'dialledDigits' => '322221350' + }, + } + } + }; + +} + +sub _asn_spec { + <<'END'; +-- +-- +-- The following ASN.1 specification defines the abstract syntax for +-- +-- Data Record Format Version 03 +-- Release 12 +-- +-- The specification is structured as follows: +-- (1) structure of the Tap batch +-- (2) definition of the individual Tap ‘records’ +-- (3) Tap data items and groups of data items used within (2) +-- (4) Common, non-Tap data types +-- (5) Tap data items for content charging +-- +-- It is mainly a translation from the logical structure +-- diagrams. Where appropriate, names used within the +-- logical structure diagrams have been shortened. +-- For repeating data items the name as used within the logical +-- structure have been extended by adding ‘list’ or ‘table’ +-- (in some instances). +-- + + +-- TAP-0312 DEFINITIONS IMPLICIT TAGS ::= + +-- BEGIN + +-- +-- Structure of a Tap batch +-- + +DataInterChange ::= CHOICE +{ + transferBatch TransferBatch, + notification Notification, +... +} + +-- Batch Control Information must always, both logically and physically, +-- be the first group/item within Transfer Batch – this ensures that the +-- TAP release version can be readily identified. Any new groups/items +-- required may be inserted at any point after Batch Control Information + +TransferBatch ::= [APPLICATION 1] SEQUENCE +{ + batchControlInfo BatchControlInfo OPTIONAL, -- *m.m. + accountingInfo AccountingInfo OPTIONAL, + networkInfo NetworkInfo OPTIONAL, -- *m.m. + messageDescriptionInfo MessageDescriptionInfoList OPTIONAL, + callEventDetails CallEventDetailList OPTIONAL, -- *m.m. + auditControlInfo AuditControlInfo OPTIONAL, -- *m.m. +... +} + +Notification ::= [APPLICATION 2] SEQUENCE +{ + sender Sender OPTIONAL, -- *m.m. + recipient Recipient OPTIONAL, -- *m.m. + fileSequenceNumber FileSequenceNumber OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + fileCreationTimeStamp FileCreationTimeStamp OPTIONAL, + fileAvailableTimeStamp FileAvailableTimeStamp OPTIONAL, -- *m.m. + transferCutOffTimeStamp TransferCutOffTimeStamp OPTIONAL, -- *m.m. + specificationVersionNumber SpecificationVersionNumber OPTIONAL, -- *m.m. + releaseVersionNumber ReleaseVersionNumber OPTIONAL, -- *m.m. + fileTypeIndicator FileTypeIndicator OPTIONAL, + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +CallEventDetailList ::= [APPLICATION 3] SEQUENCE OF CallEventDetail + +CallEventDetail ::= CHOICE +{ + mobileOriginatedCall MobileOriginatedCall, + mobileTerminatedCall MobileTerminatedCall, + supplServiceEvent SupplServiceEvent, + serviceCentreUsage ServiceCentreUsage, + gprsCall GprsCall, + contentTransaction ContentTransaction, + locationService LocationService, + messagingEvent MessagingEvent, + mobileSession MobileSession, +... +} + +-- +-- Structure of the individual Tap records +-- + +BatchControlInfo ::= [APPLICATION 4] SEQUENCE +{ + sender Sender OPTIONAL, -- *m.m. + recipient Recipient OPTIONAL, -- *m.m. + fileSequenceNumber FileSequenceNumber OPTIONAL, -- *m.m. + fileCreationTimeStamp FileCreationTimeStamp OPTIONAL, + transferCutOffTimeStamp TransferCutOffTimeStamp OPTIONAL, -- *m.m. + fileAvailableTimeStamp FileAvailableTimeStamp OPTIONAL, -- *m.m. + specificationVersionNumber SpecificationVersionNumber OPTIONAL, -- *m.m. + releaseVersionNumber ReleaseVersionNumber OPTIONAL, -- *m.m. + fileTypeIndicator FileTypeIndicator OPTIONAL, + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +AccountingInfo ::= [APPLICATION 5] SEQUENCE +{ + taxation TaxationList OPTIONAL, + discounting DiscountingList OPTIONAL, + localCurrency LocalCurrency OPTIONAL, -- *m.m. + tapCurrency TapCurrency OPTIONAL, + currencyConversionInfo CurrencyConversionList OPTIONAL, + tapDecimalPlaces TapDecimalPlaces OPTIONAL, -- *m.m. +... +} + +NetworkInfo ::= [APPLICATION 6] SEQUENCE +{ + utcTimeOffsetInfo UtcTimeOffsetInfoList OPTIONAL, -- *m.m. + recEntityInfo RecEntityInfoList OPTIONAL, +... +} + +MessageDescriptionInfoList ::= [APPLICATION 8] SEQUENCE OF MessageDescriptionInformation + +MobileOriginatedCall ::= [APPLICATION 9] SEQUENCE +{ + basicCallInformation MoBasicCallInformation OPTIONAL, -- *m.m. + locationInformation LocationInformation OPTIONAL, -- *m.m. + equipmentIdentifier ImeiOrEsn OPTIONAL, + basicServiceUsedList BasicServiceUsedList OPTIONAL, -- *m.m. + supplServiceCode SupplServiceCode OPTIONAL, + thirdPartyInformation ThirdPartyInformation OPTIONAL, + camelServiceUsed CamelServiceUsed OPTIONAL, + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +MobileTerminatedCall ::= [APPLICATION 10] SEQUENCE +{ + basicCallInformation MtBasicCallInformation OPTIONAL, -- *m.m. + locationInformation LocationInformation OPTIONAL, -- *m.m. + equipmentIdentifier ImeiOrEsn OPTIONAL, + basicServiceUsedList BasicServiceUsedList OPTIONAL, -- *m.m. + camelServiceUsed CamelServiceUsed OPTIONAL, + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + + +SupplServiceEvent ::= [APPLICATION 11] SEQUENCE +{ + chargeableSubscriber ChargeableSubscriber OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + locationInformation LocationInformation OPTIONAL, -- *m.m. + equipmentIdentifier ImeiOrEsn OPTIONAL, + supplServiceUsed SupplServiceUsed OPTIONAL, -- *m.m. + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + + +ServiceCentreUsage ::= [APPLICATION 12] SEQUENCE +{ + basicInformation ScuBasicInformation OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + servingNetwork ServingNetwork OPTIONAL, + recEntityCode RecEntityCode OPTIONAL, -- *m.m. + chargeInformation ChargeInformation OPTIONAL, -- *m.m. + scuChargeType ScuChargeType OPTIONAL, -- *m.m. + scuTimeStamps ScuTimeStamps OPTIONAL, -- *m.m. + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +GprsCall ::= [APPLICATION 14] SEQUENCE +{ + gprsBasicCallInformation GprsBasicCallInformation OPTIONAL, -- *m.m. + gprsLocationInformation GprsLocationInformation OPTIONAL, -- *m.m. + equipmentIdentifier ImeiOrEsn OPTIONAL, + gprsServiceUsed GprsServiceUsed OPTIONAL, -- *m.m. + camelServiceUsed CamelServiceUsed OPTIONAL, + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +ContentTransaction ::= [APPLICATION 17] SEQUENCE +{ + contentTransactionBasicInfo ContentTransactionBasicInfo OPTIONAL, -- *m.m. + chargedPartyInformation ChargedPartyInformation OPTIONAL, -- *m.m. + servingPartiesInformation ServingPartiesInformation OPTIONAL, -- *m.m. + contentServiceUsed ContentServiceUsedList OPTIONAL, -- *m.m. + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +LocationService ::= [APPLICATION 297] SEQUENCE +{ + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + recEntityCode RecEntityCode OPTIONAL, -- *m.m. + callReference CallReference OPTIONAL, + trackingCustomerInformation TrackingCustomerInformation OPTIONAL, + lCSSPInformation LCSSPInformation OPTIONAL, + trackedCustomerInformation TrackedCustomerInformation OPTIONAL, + locationServiceUsage LocationServiceUsage OPTIONAL, -- *m.m. + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +MessagingEvent ::= [APPLICATION 433] SEQUENCE +{ + messagingEventService MessagingEventService OPTIONAL, -- *m.m. + chargedParty ChargedParty OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + simToolkitIndicator SimToolkitIndicator OPTIONAL, + geographicalLocation GeographicalLocation OPTIONAL, + eventReference EventReference OPTIONAL, -- *m.m. + + recEntityCodeList RecEntityCodeList OPTIONAL, -- *m.m. + networkElementList NetworkElementList OPTIONAL, + locationArea LocationArea OPTIONAL, + cellId CellId OPTIONAL, + serviceStartTimestamp ServiceStartTimestamp OPTIONAL, -- *m.m. + nonChargedParty NonChargedParty OPTIONAL, + exchangeRateCode ExchangeRateCode OPTIONAL, + callTypeGroup CallTypeGroup OPTIONAL, -- *m.m. + charge Charge OPTIONAL, -- *m.m. + taxInformationList TaxInformationList OPTIONAL, + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +MobileSession ::= [APPLICATION 434] SEQUENCE +{ + mobileSessionService MobileSessionService OPTIONAL, -- *m.m. + chargedParty ChargedParty OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + simToolkitIndicator SimToolkitIndicator OPTIONAL, + geographicalLocation GeographicalLocation OPTIONAL, + locationArea LocationArea OPTIONAL, + cellId CellId OPTIONAL, + eventReference EventReference OPTIONAL, -- *m.m. + + recEntityCodeList RecEntityCodeList OPTIONAL, -- *m.m. + serviceStartTimestamp ServiceStartTimestamp OPTIONAL, -- *m.m. + causeForTerm CauseForTerm OPTIONAL, + totalCallEventDuration TotalCallEventDuration OPTIONAL, -- *m.m. + nonChargedParty NonChargedParty OPTIONAL, + sessionChargeInfoList SessionChargeInfoList OPTIONAL, -- *m.m. + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +AuditControlInfo ::= [APPLICATION 15] SEQUENCE +{ + earliestCallTimeStamp EarliestCallTimeStamp OPTIONAL, + latestCallTimeStamp LatestCallTimeStamp OPTIONAL, + totalCharge TotalCharge OPTIONAL, -- *m.m. + totalChargeRefund TotalChargeRefund OPTIONAL, + totalTaxRefund TotalTaxRefund OPTIONAL, + totalTaxValue TotalTaxValue OPTIONAL, -- *m.m. + totalDiscountValue TotalDiscountValue OPTIONAL, -- *m.m. + totalDiscountRefund TotalDiscountRefund OPTIONAL, + totalAdvisedChargeValueList TotalAdvisedChargeValueList OPTIONAL, + callEventDetailsCount CallEventDetailsCount OPTIONAL, -- *m.m. + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + + +-- +-- Tap data items and groups of data items +-- + +AccessPointNameNI ::= [APPLICATION 261] AsciiString --(SIZE(1..63)) + +AccessPointNameOI ::= [APPLICATION 262] AsciiString --(SIZE(1..37)) + +ActualDeliveryTimeStamp ::= [APPLICATION 302] DateTime + +AddressStringDigits ::= BCDString + +AdvisedCharge ::= [APPLICATION 349] Charge + +AdvisedChargeCurrency ::= [APPLICATION 348] Currency + +AdvisedChargeInformation ::= [APPLICATION 351] SEQUENCE +{ + paidIndicator PaidIndicator OPTIONAL, + paymentMethod PaymentMethod OPTIONAL, + advisedChargeCurrency AdvisedChargeCurrency OPTIONAL, + advisedCharge AdvisedCharge OPTIONAL, -- *m.m. + commission Commission OPTIONAL, +... +} + +AgeOfLocation ::= [APPLICATION 396] INTEGER + +BasicService ::= [APPLICATION 36] SEQUENCE +{ + serviceCode BasicServiceCode OPTIONAL, -- *m.m. + transparencyIndicator TransparencyIndicator OPTIONAL, + fnur Fnur OPTIONAL, + userProtocolIndicator UserProtocolIndicator OPTIONAL, + guaranteedBitRate GuaranteedBitRate OPTIONAL, + maximumBitRate MaximumBitRate OPTIONAL, +... +} + +BasicServiceCode ::= [APPLICATION 426] CHOICE +{ + teleServiceCode TeleServiceCode, + bearerServiceCode BearerServiceCode, +... +} + +BasicServiceCodeList ::= [APPLICATION 37] SEQUENCE OF BasicServiceCode + +BasicServiceUsed ::= [APPLICATION 39] SEQUENCE +{ + basicService BasicService OPTIONAL, -- *m.m. + chargingTimeStamp ChargingTimeStamp OPTIONAL, + chargeInformationList ChargeInformationList OPTIONAL, -- *m.m. + hSCSDIndicator HSCSDIndicator OPTIONAL, +... +} + +BasicServiceUsedList ::= [APPLICATION 38] SEQUENCE OF BasicServiceUsed + +BearerServiceCode ::= [APPLICATION 40] HexString --(SIZE(2)) + +EventReference ::= [APPLICATION 435] AsciiString + + +CalledNumber ::= [APPLICATION 407] AddressStringDigits + +CalledPlace ::= [APPLICATION 42] AsciiString + +CalledRegion ::= [APPLICATION 46] AsciiString + +CallEventDetailsCount ::= [APPLICATION 43] INTEGER + +CallEventStartTimeStamp ::= [APPLICATION 44] DateTime + +CallingNumber ::= [APPLICATION 405] AddressStringDigits + +CallOriginator ::= [APPLICATION 41] SEQUENCE +{ + callingNumber CallingNumber OPTIONAL, + clirIndicator ClirIndicator OPTIONAL, + sMSOriginator SMSOriginator OPTIONAL, +... +} + +CallReference ::= [APPLICATION 45] OCTET STRING --(SIZE(1..8)) + +CallTypeGroup ::= [APPLICATION 258] SEQUENCE +{ + callTypeLevel1 CallTypeLevel1 OPTIONAL, -- *m.m. + callTypeLevel2 CallTypeLevel2 OPTIONAL, -- *m.m. + callTypeLevel3 CallTypeLevel3 OPTIONAL, -- *m.m. +... +} + +CallTypeLevel1 ::= [APPLICATION 259] INTEGER + +CallTypeLevel2 ::= [APPLICATION 255] INTEGER + +CallTypeLevel3 ::= [APPLICATION 256] INTEGER + +CamelDestinationNumber ::= [APPLICATION 404] AddressStringDigits + +CamelInvocationFee ::= [APPLICATION 422] AbsoluteAmount + +CamelServiceKey ::= [APPLICATION 55] INTEGER + +CamelServiceLevel ::= [APPLICATION 56] INTEGER + +CamelServiceUsed ::= [APPLICATION 57] SEQUENCE +{ + camelServiceLevel CamelServiceLevel OPTIONAL, + camelServiceKey CamelServiceKey OPTIONAL, -- *m.m. + defaultCallHandling DefaultCallHandlingIndicator OPTIONAL, + exchangeRateCode ExchangeRateCode OPTIONAL, + taxInformation TaxInformationList OPTIONAL, + discountInformation DiscountInformation OPTIONAL, + camelInvocationFee CamelInvocationFee OPTIONAL, + threeGcamelDestination ThreeGcamelDestination OPTIONAL, + cseInformation CseInformation OPTIONAL, +... +} + +CauseForTerm ::= [APPLICATION 58] INTEGER + +CellId ::= [APPLICATION 59] INTEGER + +Charge ::= [APPLICATION 62] AbsoluteAmount + +ChargeableSubscriber ::= [APPLICATION 427] CHOICE +{ + simChargeableSubscriber SimChargeableSubscriber, + minChargeableSubscriber MinChargeableSubscriber, +... +} + +ChargeableUnits ::= [APPLICATION 65] INTEGER + +ChargeDetail ::= [APPLICATION 63] SEQUENCE +{ + chargeType ChargeType OPTIONAL, -- *m.m. + charge Charge OPTIONAL, -- *m.m. + chargeableUnits ChargeableUnits OPTIONAL, + chargedUnits ChargedUnits OPTIONAL, + chargeDetailTimeStamp ChargeDetailTimeStamp OPTIONAL, +... +} + +ChargeDetailList ::= [APPLICATION 64] SEQUENCE OF ChargeDetail + +ChargeDetailTimeStamp ::= [APPLICATION 410] ChargingTimeStamp + +ChargedItem ::= [APPLICATION 66] AsciiString --(SIZE(1)) + +ChargedParty ::= [APPLICATION 436] SEQUENCE +{ + imsi Imsi OPTIONAL, -- *m.m. + msisdn Msisdn OPTIONAL, + publicUserId PublicUserId OPTIONAL, + homeBid HomeBid OPTIONAL, + homeLocationDescription HomeLocationDescription OPTIONAL, + imei Imei OPTIONAL, +... +} + +ChargedPartyEquipment ::= [APPLICATION 323] SEQUENCE +{ + equipmentIdType EquipmentIdType OPTIONAL, -- *m.m. + equipmentId EquipmentId OPTIONAL, -- *m.m. +... +} + +ChargedPartyHomeIdentification ::= [APPLICATION 313] SEQUENCE +{ + homeIdType HomeIdType OPTIONAL, -- *m.m. + homeIdentifier HomeIdentifier OPTIONAL, -- *m.m. +... +} + +ChargedPartyHomeIdList ::= [APPLICATION 314] SEQUENCE OF + ChargedPartyHomeIdentification + +ChargedPartyIdentification ::= [APPLICATION 309] SEQUENCE +{ + chargedPartyIdType ChargedPartyIdType OPTIONAL, -- *m.m. + chargedPartyIdentifier ChargedPartyIdentifier OPTIONAL, -- *m.m. +... +} + +ChargedPartyIdentifier ::= [APPLICATION 287] AsciiString + +ChargedPartyIdList ::= [APPLICATION 310] SEQUENCE OF ChargedPartyIdentification + +ChargedPartyIdType ::= [APPLICATION 305] INTEGER + +ChargedPartyInformation ::= [APPLICATION 324] SEQUENCE +{ + chargedPartyIdList ChargedPartyIdList OPTIONAL, -- *m.m. + chargedPartyHomeIdList ChargedPartyHomeIdList OPTIONAL, + chargedPartyLocationList ChargedPartyLocationList OPTIONAL, + chargedPartyEquipment ChargedPartyEquipment OPTIONAL, +... +} + +ChargedPartyLocation ::= [APPLICATION 320] SEQUENCE +{ + locationIdType LocationIdType OPTIONAL, -- *m.m. + locationIdentifier LocationIdentifier OPTIONAL, -- *m.m. +... +} + +ChargedPartyLocationList ::= [APPLICATION 321] SEQUENCE OF ChargedPartyLocation + +ChargedPartyStatus ::= [APPLICATION 67] INTEGER + +ChargedUnits ::= [APPLICATION 68] INTEGER + +ChargeInformation ::= [APPLICATION 69] SEQUENCE +{ + chargedItem ChargedItem OPTIONAL, -- *m.m. + exchangeRateCode ExchangeRateCode OPTIONAL, + callTypeGroup CallTypeGroup OPTIONAL, + chargeDetailList ChargeDetailList OPTIONAL, -- *m.m. + taxInformation TaxInformationList OPTIONAL, + discountInformation DiscountInformation OPTIONAL, +... +} + +ChargeInformationList ::= [APPLICATION 70] SEQUENCE OF ChargeInformation + +ChargeRefundIndicator ::= [APPLICATION 344] INTEGER + +ChargeType ::= [APPLICATION 71] NumberString --(SIZE(2..3)) + +ChargingId ::= [APPLICATION 72] INTEGER + +ChargingPoint ::= [APPLICATION 73] AsciiString --(SIZE(1)) + +ChargingTimeStamp ::= [APPLICATION 74] DateTime + +ClirIndicator ::= [APPLICATION 75] INTEGER + +Commission ::= [APPLICATION 350] Charge + +CompletionTimeStamp ::= [APPLICATION 76] DateTime + +ContentChargingPoint ::= [APPLICATION 345] INTEGER + +ContentProvider ::= [APPLICATION 327] SEQUENCE +{ + contentProviderIdType ContentProviderIdType OPTIONAL, -- *m.m. + contentProviderIdentifier ContentProviderIdentifier OPTIONAL, -- *m.m. +... +} + +ContentProviderIdentifier ::= [APPLICATION 292] AsciiString + +ContentProviderIdList ::= [APPLICATION 328] SEQUENCE OF ContentProvider + +ContentProviderIdType ::= [APPLICATION 291] INTEGER + +ContentProviderName ::= [APPLICATION 334] AsciiString + +ContentServiceUsed ::= [APPLICATION 352] SEQUENCE +{ + contentTransactionCode ContentTransactionCode OPTIONAL, -- *m.m. + contentTransactionType ContentTransactionType OPTIONAL, -- *m.m. + objectType ObjectType OPTIONAL, + transactionDescriptionSupp TransactionDescriptionSupp OPTIONAL, + transactionShortDescription TransactionShortDescription OPTIONAL, -- *m.m. + transactionDetailDescription TransactionDetailDescription OPTIONAL, + transactionIdentifier TransactionIdentifier OPTIONAL, -- *m.m. + transactionAuthCode TransactionAuthCode OPTIONAL, + dataVolumeIncoming DataVolumeIncoming OPTIONAL, + dataVolumeOutgoing DataVolumeOutgoing OPTIONAL, + totalDataVolume TotalDataVolume OPTIONAL, + chargeRefundIndicator ChargeRefundIndicator OPTIONAL, + contentChargingPoint ContentChargingPoint OPTIONAL, + chargeInformationList ChargeInformationList OPTIONAL, + advisedChargeInformation AdvisedChargeInformation OPTIONAL, +... +} + +ContentServiceUsedList ::= [APPLICATION 285] SEQUENCE OF ContentServiceUsed + +ContentTransactionBasicInfo ::= [APPLICATION 304] SEQUENCE +{ + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + orderPlacedTimeStamp OrderPlacedTimeStamp OPTIONAL, + requestedDeliveryTimeStamp RequestedDeliveryTimeStamp OPTIONAL, + actualDeliveryTimeStamp ActualDeliveryTimeStamp OPTIONAL, + totalTransactionDuration TotalTransactionDuration OPTIONAL, + transactionStatus TransactionStatus OPTIONAL, +... +} + +ContentTransactionCode ::= [APPLICATION 336] INTEGER + +ContentTransactionType ::= [APPLICATION 337] INTEGER + +CseInformation ::= [APPLICATION 79] OCTET STRING --(SIZE(1..40)) + +CurrencyConversion ::= [APPLICATION 106] SEQUENCE +{ + exchangeRateCode ExchangeRateCode OPTIONAL, -- *m.m. + numberOfDecimalPlaces NumberOfDecimalPlaces OPTIONAL, -- *m.m. + exchangeRate ExchangeRate OPTIONAL, -- *m.m. +... +} + +CurrencyConversionList ::= [APPLICATION 80] SEQUENCE OF CurrencyConversion + +CustomerIdentifier ::= [APPLICATION 364] AsciiString + +CustomerIdType ::= [APPLICATION 363] INTEGER + +DataVolume ::= INTEGER + +DataVolumeIncoming ::= [APPLICATION 250] DataVolume + +DataVolumeOutgoing ::= [APPLICATION 251] DataVolume + +-- +-- The following datatypes are used to denote timestamps. +-- Each timestamp consists of a local timestamp and a +-- corresponding UTC time offset. +-- Except for the timestamps used within the Batch Control +-- Information and the Audit Control Information +-- the UTC time offset is identified by a code referencing +-- the UtcTimeOffsetInfo. +-- + +-- +-- We start with the “short” datatype referencing the +-- UtcTimeOffsetInfo. +-- + +DateTime ::= SEQUENCE +{ + -- + -- Local timestamps are noted in the format + -- + -- CCYYMMDDhhmmss + -- + -- where CC = century (‘19’, ‘20’,...) + -- YY = year (‘00’ – ‘99’) + -- MM = month (‘01’, ‘02’, ... , ‘12’) + -- DD = day (‘01’, ‘02’, ... , ‘31’) + -- hh = hour (‘00’, ‘01’, ... , ‘23’) + -- mm = minutes (‘00’, ‘01’, ... , ‘59’) + -- ss = seconds (‘00’, ‘01’, ... , ‘59’) + -- + localTimeStamp LocalTimeStamp OPTIONAL, -- *m.m. + utcTimeOffsetCode UtcTimeOffsetCode OPTIONAL, -- *m.m. +... +} + +-- +-- The following version is the “long” datatype +-- containing the UTC time offset directly. +-- + +DateTimeLong ::= SEQUENCE +{ + localTimeStamp LocalTimeStamp OPTIONAL, -- *m.m. + utcTimeOffset UtcTimeOffset OPTIONAL, -- *m.m. +... +} + +DefaultCallHandlingIndicator ::= [APPLICATION 87] INTEGER + +DepositTimeStamp ::= [APPLICATION 88] DateTime + +Destination ::= [APPLICATION 89] SEQUENCE +{ + calledNumber CalledNumber OPTIONAL, + dialledDigits DialledDigits OPTIONAL, + calledPlace CalledPlace OPTIONAL, + calledRegion CalledRegion OPTIONAL, + sMSDestinationNumber SMSDestinationNumber OPTIONAL, +... +} + +DestinationNetwork ::= [APPLICATION 90] NetworkId + +DialledDigits ::= [APPLICATION 279] AsciiString + +Discount ::= [APPLICATION 412] DiscountValue + +DiscountableAmount ::= [APPLICATION 423] AbsoluteAmount + +DiscountApplied ::= [APPLICATION 428] CHOICE +{ + fixedDiscountValue FixedDiscountValue, + discountRate DiscountRate, +... +} + +DiscountCode ::= [APPLICATION 91] INTEGER + +DiscountInformation ::= [APPLICATION 96] SEQUENCE +{ + discountCode DiscountCode OPTIONAL, -- *m.m. + discount Discount OPTIONAL, + discountableAmount DiscountableAmount OPTIONAL, +... +} + +Discounting ::= [APPLICATION 94] SEQUENCE +{ + discountCode DiscountCode OPTIONAL, -- *m.m. + discountApplied DiscountApplied OPTIONAL, -- *m.m. +... +} + +DiscountingList ::= [APPLICATION 95] SEQUENCE OF Discounting + +DiscountRate ::= [APPLICATION 92] PercentageRate + +DiscountValue ::= AbsoluteAmount + +DistanceChargeBandCode ::= [APPLICATION 98] AsciiString --(SIZE(1)) + +EarliestCallTimeStamp ::= [APPLICATION 101] DateTimeLong + +ElementId ::= [APPLICATION 437] AsciiString + +ElementType ::= [APPLICATION 438] INTEGER + +EquipmentId ::= [APPLICATION 290] AsciiString + +EquipmentIdType ::= [APPLICATION 322] INTEGER + +Esn ::= [APPLICATION 103] NumberString + +ExchangeRate ::= [APPLICATION 104] INTEGER + +ExchangeRateCode ::= [APPLICATION 105] Code + +FileAvailableTimeStamp ::= [APPLICATION 107] DateTimeLong + +FileCreationTimeStamp ::= [APPLICATION 108] DateTimeLong + +FileSequenceNumber ::= [APPLICATION 109] NumberString --(SIZE(5)) + +FileTypeIndicator ::= [APPLICATION 110] AsciiString --(SIZE(1)) + +FixedDiscountValue ::= [APPLICATION 411] DiscountValue + +Fnur ::= [APPLICATION 111] INTEGER + +GeographicalLocation ::= [APPLICATION 113] SEQUENCE +{ + servingNetwork ServingNetwork OPTIONAL, + servingBid ServingBid OPTIONAL, + servingLocationDescription ServingLocationDescription OPTIONAL, +... +} + +GprsBasicCallInformation ::= [APPLICATION 114] SEQUENCE +{ + gprsChargeableSubscriber GprsChargeableSubscriber OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + gprsDestination GprsDestination OPTIONAL, -- *m.m. + callEventStartTimeStamp CallEventStartTimeStamp OPTIONAL, -- *m.m. + totalCallEventDuration TotalCallEventDuration OPTIONAL, -- *m.m. + causeForTerm CauseForTerm OPTIONAL, + partialTypeIndicator PartialTypeIndicator OPTIONAL, + pDPContextStartTimestamp PDPContextStartTimestamp OPTIONAL, + networkInitPDPContext NetworkInitPDPContext OPTIONAL, + chargingId ChargingId OPTIONAL, -- *m.m. +... +} + +GprsChargeableSubscriber ::= [APPLICATION 115] SEQUENCE +{ + chargeableSubscriber ChargeableSubscriber OPTIONAL, + pdpAddress PdpAddress OPTIONAL, + networkAccessIdentifier NetworkAccessIdentifier OPTIONAL, +... +} + +GprsDestination ::= [APPLICATION 116] SEQUENCE +{ + accessPointNameNI AccessPointNameNI OPTIONAL, -- *m.m. + accessPointNameOI AccessPointNameOI OPTIONAL, +... +} + +GprsLocationInformation ::= [APPLICATION 117] SEQUENCE +{ + gprsNetworkLocation GprsNetworkLocation OPTIONAL, -- *m.m. + homeLocationInformation HomeLocationInformation OPTIONAL, + geographicalLocation GeographicalLocation OPTIONAL, +... +} + +GprsNetworkLocation ::= [APPLICATION 118] SEQUENCE +{ + recEntity RecEntityCodeList OPTIONAL, -- *m.m. + locationArea LocationArea OPTIONAL, + cellId CellId OPTIONAL, +... +} + +GprsServiceUsed ::= [APPLICATION 121] SEQUENCE +{ + iMSSignallingContext IMSSignallingContext OPTIONAL, + dataVolumeIncoming DataVolumeIncoming OPTIONAL, -- *m.m. + dataVolumeOutgoing DataVolumeOutgoing OPTIONAL, -- *m.m. + chargeInformationList ChargeInformationList OPTIONAL, -- *m.m. +... +} + +GsmChargeableSubscriber ::= [APPLICATION 286] SEQUENCE +{ + imsi Imsi OPTIONAL, + msisdn Msisdn OPTIONAL, +... +} + +GuaranteedBitRate ::= [APPLICATION 420] OCTET STRING --(SIZE (1)) + +HomeBid ::= [APPLICATION 122] Bid + +HomeIdentifier ::= [APPLICATION 288] AsciiString + +HomeIdType ::= [APPLICATION 311] INTEGER + +HomeLocationDescription ::= [APPLICATION 413] LocationDescription + +HomeLocationInformation ::= [APPLICATION 123] SEQUENCE +{ + homeBid HomeBid OPTIONAL, -- *m.m. + homeLocationDescription HomeLocationDescription OPTIONAL, -- *m.m. +... +} + +HorizontalAccuracyDelivered ::= [APPLICATION 392] INTEGER + +HorizontalAccuracyRequested ::= [APPLICATION 385] INTEGER + +HSCSDIndicator ::= [APPLICATION 424] AsciiString --(SIZE(1)) + +Imei ::= [APPLICATION 128] BCDString --(SIZE(7..8)) + +ImeiOrEsn ::= [APPLICATION 429] CHOICE +{ + imei Imei, + esn Esn, +... +} + +Imsi ::= [APPLICATION 129] BCDString --(SIZE(3..8)) + +IMSSignallingContext ::= [APPLICATION 418] INTEGER + +InternetServiceProvider ::= [APPLICATION 329] SEQUENCE +{ + ispIdType IspIdType OPTIONAL, -- *m.m. + ispIdentifier IspIdentifier OPTIONAL, -- *m.m. +... +} + +InternetServiceProviderIdList ::= [APPLICATION 330] SEQUENCE OF InternetServiceProvider + +IspIdentifier ::= [APPLICATION 294] AsciiString + +IspIdType ::= [APPLICATION 293] INTEGER + +ISPList ::= [APPLICATION 378] SEQUENCE OF InternetServiceProvider + +NetworkIdType ::= [APPLICATION 331] INTEGER + +NetworkIdentifier ::= [APPLICATION 295] AsciiString + +Network ::= [APPLICATION 332] SEQUENCE +{ + networkIdType NetworkIdType OPTIONAL, -- *m.m. + networkIdentifier NetworkIdentifier OPTIONAL, -- *m.m. +... +} + +NetworkList ::= [APPLICATION 333] SEQUENCE OF Network + +LatestCallTimeStamp ::= [APPLICATION 133] DateTimeLong + +LCSQosDelivered ::= [APPLICATION 390] SEQUENCE +{ + lCSTransactionStatus LCSTransactionStatus OPTIONAL, + horizontalAccuracyDelivered HorizontalAccuracyDelivered OPTIONAL, + verticalAccuracyDelivered VerticalAccuracyDelivered OPTIONAL, + responseTime ResponseTime OPTIONAL, + positioningMethod PositioningMethod OPTIONAL, + trackingPeriod TrackingPeriod OPTIONAL, + trackingFrequency TrackingFrequency OPTIONAL, + ageOfLocation AgeOfLocation OPTIONAL, +... +} + +LCSQosRequested ::= [APPLICATION 383] SEQUENCE +{ + lCSRequestTimestamp LCSRequestTimestamp OPTIONAL, -- *m.m. + horizontalAccuracyRequested HorizontalAccuracyRequested OPTIONAL, + verticalAccuracyRequested VerticalAccuracyRequested OPTIONAL, + responseTimeCategory ResponseTimeCategory OPTIONAL, + trackingPeriod TrackingPeriod OPTIONAL, + trackingFrequency TrackingFrequency OPTIONAL, +... +} + +LCSRequestTimestamp ::= [APPLICATION 384] DateTime + +LCSSPIdentification ::= [APPLICATION 375] SEQUENCE +{ + contentProviderIdType ContentProviderIdType OPTIONAL, -- *m.m. + contentProviderIdentifier ContentProviderIdentifier OPTIONAL, -- *m.m. +... +} + +LCSSPIdentificationList ::= [APPLICATION 374] SEQUENCE OF LCSSPIdentification + +LCSSPInformation ::= [APPLICATION 373] SEQUENCE +{ + lCSSPIdentificationList LCSSPIdentificationList OPTIONAL, -- *m.m. + iSPList ISPList OPTIONAL, + networkList NetworkList OPTIONAL, +... +} + +LCSTransactionStatus ::= [APPLICATION 391] INTEGER + +LocalCurrency ::= [APPLICATION 135] Currency + +LocalTimeStamp ::= [APPLICATION 16] NumberString --(SIZE(14)) + +LocationArea ::= [APPLICATION 136] INTEGER + +LocationDescription ::= AsciiString + +LocationIdentifier ::= [APPLICATION 289] AsciiString + +LocationIdType ::= [APPLICATION 315] INTEGER + +LocationInformation ::= [APPLICATION 138] SEQUENCE +{ + networkLocation NetworkLocation OPTIONAL, -- *m.m. + homeLocationInformation HomeLocationInformation OPTIONAL, + geographicalLocation GeographicalLocation OPTIONAL, +... +} + +LocationServiceUsage ::= [APPLICATION 382] SEQUENCE +{ + lCSQosRequested LCSQosRequested OPTIONAL, -- *m.m. + lCSQosDelivered LCSQosDelivered OPTIONAL, + chargingTimeStamp ChargingTimeStamp OPTIONAL, + chargeInformationList ChargeInformationList OPTIONAL, -- *m.m. +... +} + +MaximumBitRate ::= [APPLICATION 421] OCTET STRING --(SIZE (1)) + +Mdn ::= [APPLICATION 253] NumberString + +MessageDescription ::= [APPLICATION 142] AsciiString + +MessageDescriptionCode ::= [APPLICATION 141] Code + +MessageDescriptionInformation ::= [APPLICATION 143] SEQUENCE +{ + messageDescriptionCode MessageDescriptionCode OPTIONAL, -- *m.m. + messageDescription MessageDescription OPTIONAL, -- *m.m. +... +} + +MessageStatus ::= [APPLICATION 144] INTEGER + +MessageType ::= [APPLICATION 145] INTEGER + +MessagingEventService ::= [APPLICATION 439] INTEGER + +Min ::= [APPLICATION 146] NumberString --(SIZE(2..15)) + +MinChargeableSubscriber ::= [APPLICATION 254] SEQUENCE +{ + min Min OPTIONAL, -- *m.m. + mdn Mdn OPTIONAL, +... +} + +MoBasicCallInformation ::= [APPLICATION 147] SEQUENCE +{ + chargeableSubscriber ChargeableSubscriber OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + destination Destination OPTIONAL, + destinationNetwork DestinationNetwork OPTIONAL, + callEventStartTimeStamp CallEventStartTimeStamp OPTIONAL, -- *m.m. + totalCallEventDuration TotalCallEventDuration OPTIONAL, -- *m.m. + simToolkitIndicator SimToolkitIndicator OPTIONAL, + causeForTerm CauseForTerm OPTIONAL, +... +} + +MobileSessionService ::= [APPLICATION 440] INTEGER + +Msisdn ::= [APPLICATION 152] BCDString --(SIZE(1..9)) + +MtBasicCallInformation ::= [APPLICATION 153] SEQUENCE +{ + chargeableSubscriber ChargeableSubscriber OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + callOriginator CallOriginator OPTIONAL, + originatingNetwork OriginatingNetwork OPTIONAL, + callEventStartTimeStamp CallEventStartTimeStamp OPTIONAL, -- *m.m. + totalCallEventDuration TotalCallEventDuration OPTIONAL, -- *m.m. + simToolkitIndicator SimToolkitIndicator OPTIONAL, + causeForTerm CauseForTerm OPTIONAL, +... +} + +NetworkAccessIdentifier ::= [APPLICATION 417] AsciiString + +NetworkElement ::= [APPLICATION 441] SEQUENCE +{ +elementType ElementType OPTIONAL, -- *m.m. +elementId ElementId OPTIONAL, -- *m.m. +... +} + +NetworkElementList ::= [APPLICATION 442] SEQUENCE OF NetworkElement + +NetworkId ::= AsciiString --(SIZE(1..6)) + +NetworkInitPDPContext ::= [APPLICATION 245] INTEGER + +NetworkLocation ::= [APPLICATION 156] SEQUENCE +{ + recEntityCode RecEntityCode OPTIONAL, -- *m.m. + callReference CallReference OPTIONAL, + locationArea LocationArea OPTIONAL, + cellId CellId OPTIONAL, +... +} + +NonChargedNumber ::= [APPLICATION 402] AsciiString + +NonChargedParty ::= [APPLICATION 443] SEQUENCE +{ + nonChargedPartyNumber NonChargedPartyNumber OPTIONAL, + nonChargedPublicUserId NonChargedPublicUserId OPTIONAL, +... +} + +NonChargedPartyNumber ::= [APPLICATION 444] AddressStringDigits + +NonChargedPublicUserId ::= [APPLICATION 445] AsciiString + +NumberOfDecimalPlaces ::= [APPLICATION 159] INTEGER + +ObjectType ::= [APPLICATION 281] INTEGER + +OperatorSpecInfoList ::= [APPLICATION 162] SEQUENCE OF OperatorSpecInformation + +OperatorSpecInformation ::= [APPLICATION 163] AsciiString + +OrderPlacedTimeStamp ::= [APPLICATION 300] DateTime + +OriginatingNetwork ::= [APPLICATION 164] NetworkId + +PacketDataProtocolAddress ::= [APPLICATION 165] AsciiString + +PaidIndicator ::= [APPLICATION 346] INTEGER + +PartialTypeIndicator ::= [APPLICATION 166] AsciiString --(SIZE(1)) + +PaymentMethod ::= [APPLICATION 347] INTEGER + +PdpAddress ::= [APPLICATION 167] PacketDataProtocolAddress + +PDPContextStartTimestamp ::= [APPLICATION 260] DateTime + +PlmnId ::= [APPLICATION 169] AsciiString --(SIZE(5)) + +PositioningMethod ::= [APPLICATION 395] INTEGER + +PriorityCode ::= [APPLICATION 170] INTEGER + +PublicUserId ::= [APPLICATION 446] AsciiString + +RapFileSequenceNumber ::= [APPLICATION 181] FileSequenceNumber + +RecEntityCode ::= [APPLICATION 184] Code + +RecEntityCodeList ::= [APPLICATION 185] SEQUENCE OF RecEntityCode + +RecEntityId ::= [APPLICATION 400] AsciiString + +RecEntityInfoList ::= [APPLICATION 188] SEQUENCE OF RecEntityInformation + +RecEntityInformation ::= [APPLICATION 183] SEQUENCE +{ + recEntityCode RecEntityCode OPTIONAL, -- *m.m. + recEntityType RecEntityType OPTIONAL, -- *m.m. + recEntityId RecEntityId OPTIONAL, -- *m.m. +... +} + +RecEntityType ::= [APPLICATION 186] INTEGER + +Recipient ::= [APPLICATION 182] PlmnId + +ReleaseVersionNumber ::= [APPLICATION 189] INTEGER + +RequestedDeliveryTimeStamp ::= [APPLICATION 301] DateTime + +ResponseTime ::= [APPLICATION 394] INTEGER + +ResponseTimeCategory ::= [APPLICATION 387] INTEGER + +ScuBasicInformation ::= [APPLICATION 191] SEQUENCE +{ + chargeableSubscriber ScuChargeableSubscriber OPTIONAL, -- *m.m. + chargedPartyStatus ChargedPartyStatus OPTIONAL, -- *m.m. + nonChargedNumber NonChargedNumber OPTIONAL, -- *m.m. + clirIndicator ClirIndicator OPTIONAL, + originatingNetwork OriginatingNetwork OPTIONAL, + destinationNetwork DestinationNetwork OPTIONAL, +... +} + +ScuChargeType ::= [APPLICATION 192] SEQUENCE +{ + messageStatus MessageStatus OPTIONAL, -- *m.m. + priorityCode PriorityCode OPTIONAL, -- *m.m. + distanceChargeBandCode DistanceChargeBandCode OPTIONAL, + messageType MessageType OPTIONAL, -- *m.m. + messageDescriptionCode MessageDescriptionCode OPTIONAL, -- *m.m. +... +} + +ScuTimeStamps ::= [APPLICATION 193] SEQUENCE +{ + depositTimeStamp DepositTimeStamp OPTIONAL, -- *m.m. + completionTimeStamp CompletionTimeStamp OPTIONAL, -- *m.m. + chargingPoint ChargingPoint OPTIONAL, -- *m.m. +... +} + +ScuChargeableSubscriber ::= [APPLICATION 430] CHOICE +{ + gsmChargeableSubscriber GsmChargeableSubscriber, + minChargeableSubscriber MinChargeableSubscriber, +... +} + +Sender ::= [APPLICATION 196] PlmnId + +ServiceStartTimestamp ::= [APPLICATION 447] DateTime + +ServingBid ::= [APPLICATION 198] Bid + +ServingLocationDescription ::= [APPLICATION 414] LocationDescription + +ServingNetwork ::= [APPLICATION 195] AsciiString + +ServingPartiesInformation ::= [APPLICATION 335] SEQUENCE +{ + contentProviderName ContentProviderName OPTIONAL, -- *m.m. + contentProviderIdList ContentProviderIdList OPTIONAL, + internetServiceProviderIdList InternetServiceProviderIdList OPTIONAL, + networkList NetworkList OPTIONAL, +... +} + +SessionChargeInfoList ::= [APPLICATION 448] SEQUENCE OF SessionChargeInformation + +SessionChargeInformation ::= [APPLICATION 449] SEQUENCE +{ +chargedItem ChargedItem OPTIONAL, -- *m.m. +exchangeRateCode ExchangeRateCode OPTIONAL, + callTypeGroup CallTypeGroup OPTIONAL, -- *m.m. + chargeDetailList ChargeDetailList OPTIONAL, -- *m.m. + taxInformationList TaxInformationList OPTIONAL, +... +} + +SimChargeableSubscriber ::= [APPLICATION 199] SEQUENCE +{ + imsi Imsi OPTIONAL, -- *m.m. + msisdn Msisdn OPTIONAL, +... +} + +SimToolkitIndicator ::= [APPLICATION 200] AsciiString --(SIZE(1)) + +SMSDestinationNumber ::= [APPLICATION 419] AsciiString + +SMSOriginator ::= [APPLICATION 425] AsciiString + +SpecificationVersionNumber ::= [APPLICATION 201] INTEGER + +SsParameters ::= [APPLICATION 204] AsciiString --(SIZE(1..40)) + +SupplServiceActionCode ::= [APPLICATION 208] INTEGER + +SupplServiceCode ::= [APPLICATION 209] HexString --(SIZE(2)) + +SupplServiceUsed ::= [APPLICATION 206] SEQUENCE +{ + supplServiceCode SupplServiceCode OPTIONAL, -- *m.m. + supplServiceActionCode SupplServiceActionCode OPTIONAL, -- *m.m. + ssParameters SsParameters OPTIONAL, + chargingTimeStamp ChargingTimeStamp OPTIONAL, + chargeInformation ChargeInformation OPTIONAL, + basicServiceCodeList BasicServiceCodeList OPTIONAL, +... +} + +TapCurrency ::= [APPLICATION 210] Currency + +TapDecimalPlaces ::= [APPLICATION 244] INTEGER + +TaxableAmount ::= [APPLICATION 398] AbsoluteAmount + +Taxation ::= [APPLICATION 216] SEQUENCE +{ + taxCode TaxCode OPTIONAL, -- *m.m. + taxType TaxType OPTIONAL, -- *m.m. + taxRate TaxRate OPTIONAL, + chargeType ChargeType OPTIONAL, + taxIndicator TaxIndicator OPTIONAL, +... +} + +TaxationList ::= [APPLICATION 211] SEQUENCE OF Taxation + +TaxCode ::= [APPLICATION 212] INTEGER + +TaxIndicator ::= [APPLICATION 432] AsciiString --(SIZE(1)) + +TaxInformation ::= [APPLICATION 213] SEQUENCE +{ + taxCode TaxCode OPTIONAL, -- *m.m. + taxValue TaxValue OPTIONAL, -- *m.m. + taxableAmount TaxableAmount OPTIONAL, +... +} + +TaxInformationList ::= [APPLICATION 214] SEQUENCE OF TaxInformation + +-- The TaxRate item is of a fixed length to ensure that the full 5 +-- decimal places is provided. + +TaxRate ::= [APPLICATION 215] NumberString --(SIZE(7)) + +TaxType ::= [APPLICATION 217] AsciiString --(SIZE(2)) + +TaxValue ::= [APPLICATION 397] AbsoluteAmount + +TeleServiceCode ::= [APPLICATION 218] HexString --(SIZE(2)) + +ThirdPartyInformation ::= [APPLICATION 219] SEQUENCE +{ + thirdPartyNumber ThirdPartyNumber OPTIONAL, + clirIndicator ClirIndicator OPTIONAL, +... +} + +ThirdPartyNumber ::= [APPLICATION 403] AddressStringDigits + +ThreeGcamelDestination ::= [APPLICATION 431] CHOICE +{ + camelDestinationNumber CamelDestinationNumber, + gprsDestination GprsDestination, +... +} + +TotalAdvisedCharge ::= [APPLICATION 356] AbsoluteAmount + +TotalAdvisedChargeRefund ::= [APPLICATION 357] AbsoluteAmount + +TotalAdvisedChargeValue ::= [APPLICATION 360] SEQUENCE +{ + advisedChargeCurrency AdvisedChargeCurrency OPTIONAL, + totalAdvisedCharge TotalAdvisedCharge OPTIONAL, -- *m.m. + totalAdvisedChargeRefund TotalAdvisedChargeRefund OPTIONAL, + totalCommission TotalCommission OPTIONAL, + totalCommissionRefund TotalCommissionRefund OPTIONAL, +... +} + +TotalAdvisedChargeValueList ::= [APPLICATION 361] SEQUENCE OF TotalAdvisedChargeValue + +TotalCallEventDuration ::= [APPLICATION 223] INTEGER + +TotalCharge ::= [APPLICATION 415] AbsoluteAmount + +TotalChargeRefund ::= [APPLICATION 355] AbsoluteAmount + +TotalCommission ::= [APPLICATION 358] AbsoluteAmount + +TotalCommissionRefund ::= [APPLICATION 359] AbsoluteAmount + +TotalDataVolume ::= [APPLICATION 343] DataVolume + +TotalDiscountRefund ::= [APPLICATION 354] AbsoluteAmount + +TotalDiscountValue ::= [APPLICATION 225] AbsoluteAmount + +TotalTaxRefund ::= [APPLICATION 353] AbsoluteAmount + +TotalTaxValue ::= [APPLICATION 226] AbsoluteAmount + +TotalTransactionDuration ::= [APPLICATION 416] TotalCallEventDuration + +TrackedCustomerEquipment ::= [APPLICATION 381] SEQUENCE +{ + equipmentIdType EquipmentIdType OPTIONAL, -- *m.m. + equipmentId EquipmentId OPTIONAL, -- *m.m. +... +} + +TrackedCustomerHomeId ::= [APPLICATION 377] SEQUENCE +{ + homeIdType HomeIdType OPTIONAL, -- *m.m. + homeIdentifier HomeIdentifier OPTIONAL, -- *m.m. +... +} + +TrackedCustomerHomeIdList ::= [APPLICATION 376] SEQUENCE OF TrackedCustomerHomeId + +TrackedCustomerIdentification ::= [APPLICATION 372] SEQUENCE +{ + customerIdType CustomerIdType OPTIONAL, -- *m.m. + customerIdentifier CustomerIdentifier OPTIONAL, -- *m.m. +... +} + +TrackedCustomerIdList ::= [APPLICATION 370] SEQUENCE OF TrackedCustomerIdentification + +TrackedCustomerInformation ::= [APPLICATION 367] SEQUENCE +{ + trackedCustomerIdList TrackedCustomerIdList OPTIONAL, -- *m.m. + trackedCustomerHomeIdList TrackedCustomerHomeIdList OPTIONAL, + trackedCustomerLocList TrackedCustomerLocList OPTIONAL, + trackedCustomerEquipment TrackedCustomerEquipment OPTIONAL, +... +} + +TrackedCustomerLocation ::= [APPLICATION 380] SEQUENCE +{ + locationIdType LocationIdType OPTIONAL, -- *m.m. + locationIdentifier LocationIdentifier OPTIONAL, -- *m.m. +... +} + +TrackedCustomerLocList ::= [APPLICATION 379] SEQUENCE OF TrackedCustomerLocation + +TrackingCustomerEquipment ::= [APPLICATION 371] SEQUENCE +{ + equipmentIdType EquipmentIdType OPTIONAL, -- *m.m. + equipmentId EquipmentId OPTIONAL, -- *m.m. +... +} + +TrackingCustomerHomeId ::= [APPLICATION 366] SEQUENCE +{ + homeIdType HomeIdType OPTIONAL, -- *m.m. + homeIdentifier HomeIdentifier OPTIONAL, -- *m.m. +... +} + +TrackingCustomerHomeIdList ::= [APPLICATION 365] SEQUENCE OF TrackingCustomerHomeId + +TrackingCustomerIdentification ::= [APPLICATION 362] SEQUENCE +{ + customerIdType CustomerIdType OPTIONAL, -- *m.m. + customerIdentifier CustomerIdentifier OPTIONAL, -- *m.m. +... +} + +TrackingCustomerIdList ::= [APPLICATION 299] SEQUENCE OF TrackingCustomerIdentification + +TrackingCustomerInformation ::= [APPLICATION 298] SEQUENCE +{ + trackingCustomerIdList TrackingCustomerIdList OPTIONAL, -- *m.m. + trackingCustomerHomeIdList TrackingCustomerHomeIdList OPTIONAL, + trackingCustomerLocList TrackingCustomerLocList OPTIONAL, + trackingCustomerEquipment TrackingCustomerEquipment OPTIONAL, +... +} + +TrackingCustomerLocation ::= [APPLICATION 369] SEQUENCE +{ + locationIdType LocationIdType OPTIONAL, -- *m.m. + locationIdentifier LocationIdentifier OPTIONAL, -- *m.m. +... +} + +TrackingCustomerLocList ::= [APPLICATION 368] SEQUENCE OF TrackingCustomerLocation + +TrackingFrequency ::= [APPLICATION 389] INTEGER + +TrackingPeriod ::= [APPLICATION 388] INTEGER + +TransactionAuthCode ::= [APPLICATION 342] AsciiString + +TransactionDescriptionSupp ::= [APPLICATION 338] INTEGER + +TransactionDetailDescription ::= [APPLICATION 339] AsciiString + +TransactionIdentifier ::= [APPLICATION 341] AsciiString + +TransactionShortDescription ::= [APPLICATION 340] AsciiString + +TransactionStatus ::= [APPLICATION 303] INTEGER + +TransferCutOffTimeStamp ::= [APPLICATION 227] DateTimeLong + +TransparencyIndicator ::= [APPLICATION 228] INTEGER + +UserProtocolIndicator ::= [APPLICATION 280] INTEGER + +UtcTimeOffset ::= [APPLICATION 231] AsciiString --(SIZE(5)) + +UtcTimeOffsetCode ::= [APPLICATION 232] Code + +UtcTimeOffsetInfo ::= [APPLICATION 233] SEQUENCE +{ + utcTimeOffsetCode UtcTimeOffsetCode OPTIONAL, -- *m.m. + utcTimeOffset UtcTimeOffset OPTIONAL, -- *m.m. +... +} + +UtcTimeOffsetInfoList ::= [APPLICATION 234] SEQUENCE OF UtcTimeOffsetInfo + +VerticalAccuracyDelivered ::= [APPLICATION 393] INTEGER + +VerticalAccuracyRequested ::= [APPLICATION 386] INTEGER + + +-- +-- Tagged common data types +-- + +-- +-- The AbsoluteAmount data type is used to +-- encode absolute revenue amounts. +-- The accuracy of all absolute amount values is defined +-- by the value of TapDecimalPlaces within the group +-- AccountingInfo for the entire TAP batch. +-- Note, that only amounts greater than or equal to zero are allowed. +-- The decimal number representing the amount is +-- derived from the encoded integer +-- value by division by 10^TapDecimalPlaces. +-- for example for TapDecimalPlaces = 3 the following values +-- will be derived: +-- 0 represents 0.000 +-- 12 represents 0.012 +-- 1234 represents 1.234 +-- for TapDecimalPlaces = 5 the following values will be +-- derived: +-- 0 represents 0.00000 +-- 1234 represents 0.01234 +-- 123456 represents 1.23456 +-- This data type is used to encode (total) +-- charges, (total) discount values and +-- (total) tax values. +-- +AbsoluteAmount ::= INTEGER + +Bid ::= AsciiString --(SIZE(5)) + +Code ::= INTEGER + +-- +-- Non-tagged common data types +-- +-- +-- Recommended common data types to be used for file encoding: +-- +-- The following definitions should be used for TAP file creation instead of +-- the default specifications (OCTET STRING) +-- +-- AsciiString ::= VisibleString +-- +-- Currency ::= VisibleString +-- +-- HexString ::= VisibleString +-- +-- NumberString ::= NumericString +-- +-- AsciiString contains visible ISO 646 characters. +-- Leading and trailing spaces must be discarded during processing. +-- An AsciiString cannot contain only spaces. + +AsciiString ::= OCTET STRING + +-- +-- The BCDString data type (Binary Coded Decimal String) is used to represent +-- several digits from 0 through 9, a, b, c, d, e. +-- Two digits are encoded per octet. The four leftmost bits of the octet represent +-- the first digit while the four remaining bits represent the following digit. +-- A single f must be used as a filler when the total number of digits to be +-- encoded is odd. +-- No other filler is allowed. + +BCDString ::= OCTET STRING + + +-- +-- The currency codes from ISO 4217 +-- are used to identify a currency +-- +Currency ::= OCTET STRING + +-- +-- HexString contains ISO 646 characters from 0 through 9, A, B, C, D, E, F. +-- + +HexString ::= OCTET STRING + +-- +-- NumberString contains ISO 646 characters from 0 through 9. +-- + +NumberString ::= OCTET STRING + + +-- +-- The PercentageRate data type is used to +-- encode percentage rates with an accuracy of 2 decimal places. +-- This data type is used to encode discount rates. +-- The decimal number representing the percentage +-- rate is obtained by dividing the integer value by 100 +-- Examples: +-- +-- 1500 represents 15.00 percent +-- 1 represents 0.01 percent +-- +PercentageRate ::= INTEGER + + +-- END +END +} + +1; diff --git a/FS/FS/cdr/huawei_softx3000.pm b/FS/FS/cdr/huawei_softx3000.pm new file mode 100644 index 000000000..e66af43a9 --- /dev/null +++ b/FS/FS/cdr/huawei_softx3000.pm @@ -0,0 +1,2689 @@ +package FS::cdr::huawei_softx3000; +use base qw( FS::cdr ); + +use strict; +use vars qw( %info %TZ ); +use subs qw( ts24008_number TimeStamp ); +use Time::Local; +use FS::Record qw( qsearch ); +use FS::cdr_calltype; + +#false laziness w/gsm_tap3_12.pm +%TZ = ( + '+0000' => 'XXX-0', + '+0100' => 'XXX-1', + '+0200' => 'XXX-2', + '+0300' => 'XXX-3', + '+0400' => 'XXX-4', + '+0500' => 'XXX-5', + '+0600' => 'XXX-6', + '+0700' => 'XXX-7', + '+0800' => 'XXX-8', + '+0900' => 'XXX-9', + '+1000' => 'XXX-10', + '+1100' => 'XXX-11', + '+1200' => 'XXX-12', + '-0000' => 'XXX+0', + '-0100' => 'XXX+1', + '-0200' => 'XXX+2', + '-0300' => 'XXX+3', + '-0400' => 'XXX+4', + '-0500' => 'XXX+5', + '-0600' => 'XXX+6', + '-0700' => 'XXX+7', + '-0800' => 'XXX+8', + '-0900' => 'XXX+9', + '-1000' => 'XXX+10', + '-1100' => 'XXX+11', + '-1200' => 'XXX+12', +); + +%info = ( + 'name' => 'Huawei SoftX3000', #V100R006C05 ? + 'weight' => 160, + 'type' => 'asn.1', + 'import_fields' => [], + 'asn_format' => { + 'spec' => _asn_spec(), + 'macro' => 'CallEventDataFile', + 'header_buffer' => sub { + #my $CallEventDataFile = shift; + + my %cdr_calltype = ( map { $_->calltypename => $_->calltypenum } + qsearch('cdr_calltype', {}) + ); + + { cdr_calltype => \%cdr_calltype, + }; + + }, + 'arrayref' => sub { shift->{'callEventRecords'} }, + 'row_callback' => sub { + my( $row, $buffer ) = @_; + my @keys = keys %$row; + $buffer->{'key'} = $keys[0]; + }, + 'map' => { + 'src' => huawei_field('callingNumber', ts24008_number, ), + + 'dst' => huawei_field('calledNumber', ts24008_number, ), + + 'startdate' => huawei_field(['answerTime','deliveryTime'], TimeStamp), + 'answerdate' => huawei_field(['answerTime','deliveryTime'], TimeStamp), + 'enddate' => huawei_field('releaseTime', TimeStamp), + 'duration' => huawei_field('callDuration'), + 'billsec' => huawei_field('callDuration'), + #'disposition' => #diagnostics? + #'accountcode' + #'charged_party' => # 0 or 1, do something with this? + 'calltypenum' => sub { + my($rec, $buf) = @_; + my $key = $buf->{key}; + $buf->{'cdr_calltype'}{ $key }; + }, + #'carrierid' => + }, + + }, +); + +sub huawei_field { + my $field = shift; + my $decode = $_[0] ? shift : ''; + return sub { + my($rec, $buf) = @_; + + my $key = $buf->{key}; + + $field = ref($field) ? $field : [ $field ]; + my $value = ''; + foreach my $f (@$field) { + $value = $rec->{$key}{$f} and last; + } + + $decode + ? &{ $decode }( $value ) + : $value; + + }; +} + +sub ts24008_number { + # This type contains the binary coded decimal representation of + # a directory number e.g. calling/called/connected/translated number. + # The encoding of the octet string is in accordance with the + # the elements "Calling party BCD number", "Called party BCD number" + # and "Connected number" defined in TS 24.008. + # This encoding includes type of number and number plan information + # together with a BCD encoded digit string. + # It may also contain both a presentation and screening indicator + # (octet 3a). + # For the avoidance of doubt, this field does not include + # octets 1 and 2, the element name and length, as this would be + # redundant. + # + #type id (per TS 24.008 page 490): + # low nybble: "numbering plan identification" + # high nybble: "type of number" + # 0 unknown + # 1 international + # 2 national + # 3 network specific + # 4 dedicated access, short code + # 5 reserved + # 6 reserved + # 7 reserved for extension + # (bit 8 "extension") + return sub { + my( $type_id, $value ) = unpack 'Ch*', shift; + $value =~ s/f$//; # If the called party BCD number contains an odd number + # of digits, bits 5 to 8 of the last octet shall be + # filled with an end mark coded as "1111". + $value; + }; +} + +sub TimeStamp { + # The contents of this field are a compact form of the UTCTime format + # containing local time plus an offset to universal time. Binary coded + # decimal encoding is employed for the digits to reduce the storage and + # transmission overhead + # e.g. YYMMDDhhmmssShhmm + # where + # YY = Year 00 to 99 BCD encoded + # MM = Month 01 to 12 BCD encoded + # DD = Day 01 to 31 BCD encoded + # hh = hour 00 to 23 BCD encoded + # mm = minute 00 to 59 BCD encoded + # ss = second 00 to 59 BCD encoded + # S = Sign 0 = "+", "-" ASCII encoded + # hh = hour 00 to 23 BCD encoded + # mm = minute 00 to 59 BCD encoded + return sub { + my($year, $mon, $day, $hour, $min, $sec, $tz_sign, $tz_hour, $tz_min, $dst)= + unpack 'H2H2H2H2H2H2AH2H2C', shift; + #warn "$year/$mon/$day $hour:$min:$sec $tz_sign$tz_hour$tz_min $dst\n"; + return 0 unless $year; #y2100 bug + local($ENV{TZ}) = $TZ{ "$tz_sign$tz_hour$tz_min" }; + timelocal($sec, $min, $hour, $day, $mon-1, $year); + }; +} + +sub _asn_spec { + <<'END'; + +--DEFINITIONS IMPLICIT TAGS ::= + +--BEGIN + +-------------------------------------------------------------------------------- +-- +-- CALL AND EVENT RECORDS +-- +------------------------------------------------------------------------------ +--Font: verdana 8 + +CallEventRecord ::= CHOICE +{ + moCallRecord [0] MOCallRecord, + mtCallRecord [1] MTCallRecord, + roamingRecord [2] RoamingRecord, + incGatewayRecord [3] IncGatewayRecord, + outGatewayRecord [4] OutGatewayRecord, + transitRecord [5] TransitCallRecord, + moSMSRecord [6] MOSMSRecord, + mtSMSRecord [7] MTSMSRecord, + ssActionRecord [10] SSActionRecord, + hlrIntRecord [11] HLRIntRecord, + commonEquipRecord [14] CommonEquipRecord, + recTypeExtensions [15] ManagementExtensions, + termCAMELRecord [16] TermCAMELRecord, + mtLCSRecord [17] MTLCSRecord, + moLCSRecord [18] MOLCSRecord, + niLCSRecord [19] NILCSRecord, + forwardCallRecord [100] MOCallRecord +} + +MOCallRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedIMEI [2] IMEI OPTIONAL, + servedMSISDN [3] MSISDN OPTIONAL, + callingNumber [4] CallingNumber OPTIONAL, + calledNumber [5] CalledNumber OPTIONAL, + translatedNumber [6] TranslatedNumber OPTIONAL, + connectedNumber [7] ConnectedNumber OPTIONAL, + roamingNumber [8] RoamingNumber OPTIONAL, + recordingEntity [9] RecordingEntity OPTIONAL, + mscIncomingROUTE [10] ROUTE OPTIONAL, + mscOutgoingROUTE [11] ROUTE OPTIONAL, + location [12] LocationAreaAndCell OPTIONAL, + changeOfLocation [13] SEQUENCE OF LocationChange OPTIONAL, + basicService [14] BasicServiceCode OPTIONAL, + transparencyIndicator [15] TransparencyInd OPTIONAL, + changeOfService [16] SEQUENCE OF ChangeOfService OPTIONAL, + supplServicesUsed [17] SEQUENCE OF SuppServiceUsed OPTIONAL, + aocParameters [18] AOCParameters OPTIONAL, + changeOfAOCParms [19] SEQUENCE OF AOCParmChange OPTIONAL, + msClassmark [20] Classmark OPTIONAL, + changeOfClassmark [21] ChangeOfClassmark OPTIONAL, + seizureTime [22] TimeStamp OPTIONAL, + answerTime [23] TimeStamp OPTIONAL, + releaseTime [24] TimeStamp OPTIONAL, + callDuration [25] CallDuration OPTIONAL, + radioChanRequested [27] RadioChanRequested OPTIONAL, + radioChanUsed [28] TrafficChannel OPTIONAL, + changeOfRadioChan [29] ChangeOfRadioChannel OPTIONAL, + causeForTerm [30] CauseForTerm OPTIONAL, + diagnostics [31] Diagnostics OPTIONAL, + callReference [32] CallReference OPTIONAL, + sequenceNumber [33] SequenceNumber OPTIONAL, + additionalChgInfo [34] AdditionalChgInfo OPTIONAL, + recordExtensions [35] ManagementExtensions OPTIONAL, + gsm-SCFAddress [36] Gsm-SCFAddress OPTIONAL, + serviceKey [37] ServiceKey OPTIONAL, + networkCallReference [38] NetworkCallReference OPTIONAL, + mSCAddress [39] MSCAddress OPTIONAL, + cAMELInitCFIndicator [40] CAMELInitCFIndicator OPTIONAL, + defaultCallHandling [41] DefaultCallHandling OPTIONAL, + fnur [45] Fnur OPTIONAL, + aiurRequested [46] AiurRequested OPTIONAL, + speechVersionSupported [49] SpeechVersionIdentifier OPTIONAL, + speechVersionUsed [50] SpeechVersionIdentifier OPTIONAL, + numberOfDPEncountered [51] INTEGER OPTIONAL, + levelOfCAMELService [52] LevelOfCAMELService OPTIONAL, + freeFormatData [53] FreeFormatData OPTIONAL, + cAMELCallLegInformation [54] SEQUENCE OF CAMELInformation OPTIONAL, + freeFormatDataAppend [55] BOOLEAN OPTIONAL, + defaultCallHandling-2 [56] DefaultCallHandling OPTIONAL, + gsm-SCFAddress-2 [57] Gsm-SCFAddress OPTIONAL, + serviceKey-2 [58] ServiceKey OPTIONAL, + freeFormatData-2 [59] FreeFormatData OPTIONAL, + freeFormatDataAppend-2 [60] BOOLEAN OPTIONAL, + systemType [61] SystemType OPTIONAL, + rateIndication [62] RateIndication OPTIONAL, + partialRecordType [69] PartialRecordType OPTIONAL, + guaranteedBitRate [70] GuaranteedBitRate OPTIONAL, + maximumBitRate [71] MaximumBitRate OPTIONAL, + modemType [139] ModemType OPTIONAL, + classmark3 [140] Classmark3 OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + callingChargeAreaCode [145] ChargeAreaCode OPTIONAL, + calledChargeAreaCode [146] ChargeAreaCode OPTIONAL, + mscOutgoingCircuit [166] MSCCIC OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + callEmlppPriority [170] EmlppPriority OPTIONAL, + callerDefaultEmlppPriority [171] EmlppPriority OPTIONAL, + eaSubscriberInfo [174] EASubscriberInfo OPTIONAL, + selectedCIC [175] SelectedCIC OPTIONAL, + optimalRoutingFlag [177] NULL OPTIONAL, + optimalRoutingLateForwardFlag [178] NULL OPTIONAL, + optimalRoutingEarlyForwardFlag [179] NULL OPTIONAL, + portedflag [180] PortedFlag OPTIONAL, + calledIMSI [181] IMSI OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + changeOfglobalAreaID [189] SEQUENCE OF ChangeOfglobalAreaID OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + intermediatemccmnc [193] MCCMNC OPTIONAL, + lastmccmnc [194] MCCMNC OPTIONAL, + cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL, + cUGInterlockCode [196] CUGInterlockCode OPTIONAL, + cUGOutgoingAccessUsed [197] CUGOutgoingAccessUsed OPTIONAL, + cUGIndex [198] CUGIndex OPTIONAL, + interactionWithIP [199] InteractionWithIP OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + setupTime [201] TimeStamp OPTIONAL, + alertingTime [202] TimeStamp OPTIONAL, + voiceIndicator [203] VoiceIndicator OPTIONAL, + bCategory [204] BCategory OPTIONAL, + callType [205] CallType OPTIONAL +} + +--at moc callingNumber is the same as served msisdn except basic msisdn != calling number such as MSP service + +MTCallRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedIMEI [2] IMEI OPTIONAL, + servedMSISDN [3] CalledNumber OPTIONAL, + callingNumber [4] CallingNumber OPTIONAL, + connectedNumber [5] ConnectedNumber OPTIONAL, + recordingEntity [6] RecordingEntity OPTIONAL, + mscIncomingROUTE [7] ROUTE OPTIONAL, + mscOutgoingROUTE [8] ROUTE OPTIONAL, + location [9] LocationAreaAndCell OPTIONAL, + changeOfLocation [10] SEQUENCE OF LocationChange OPTIONAL, + basicService [11] BasicServiceCode OPTIONAL, + transparencyIndicator [12] TransparencyInd OPTIONAL, + changeOfService [13] SEQUENCE OF ChangeOfService OPTIONAL, + supplServicesUsed [14] SEQUENCE OF SuppServiceUsed OPTIONAL, + aocParameters [15] AOCParameters OPTIONAL, + changeOfAOCParms [16] SEQUENCE OF AOCParmChange OPTIONAL, + msClassmark [17] Classmark OPTIONAL, + changeOfClassmark [18] ChangeOfClassmark OPTIONAL, + seizureTime [19] TimeStamp OPTIONAL, + answerTime [20] TimeStamp OPTIONAL, + releaseTime [21] TimeStamp OPTIONAL, + callDuration [22] CallDuration OPTIONAL, + radioChanRequested [24] RadioChanRequested OPTIONAL, + radioChanUsed [25] TrafficChannel OPTIONAL, + changeOfRadioChan [26] ChangeOfRadioChannel OPTIONAL, + causeForTerm [27] CauseForTerm OPTIONAL, + diagnostics [28] Diagnostics OPTIONAL, + callReference [29] CallReference OPTIONAL, + sequenceNumber [30] SequenceNumber OPTIONAL, + additionalChgInfo [31] AdditionalChgInfo OPTIONAL, + recordExtensions [32] ManagementExtensions OPTIONAL, + networkCallReference [33] NetworkCallReference OPTIONAL, + mSCAddress [34] MSCAddress OPTIONAL, + fnur [38] Fnur OPTIONAL, + aiurRequested [39] AiurRequested OPTIONAL, + speechVersionSupported [42] SpeechVersionIdentifier OPTIONAL, + speechVersionUsed [43] SpeechVersionIdentifier OPTIONAL, + gsm-SCFAddress [44] Gsm-SCFAddress OPTIONAL, + serviceKey [45] ServiceKey OPTIONAL, + systemType [46] SystemType OPTIONAL, + rateIndication [47] RateIndication OPTIONAL, + partialRecordType [54] PartialRecordType OPTIONAL, + guaranteedBitRate [55] GuaranteedBitRate OPTIONAL, + maximumBitRate [56] MaximumBitRate OPTIONAL, + initialCallAttemptFlag [137] NULL OPTIONAL, + ussdCallBackFlag [138] NULL OPTIONAL, + modemType [139] ModemType OPTIONAL, + classmark3 [140] Classmark3 OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + callingChargeAreaCode [145]ChargeAreaCode OPTIONAL, + calledChargeAreaCode [146]ChargeAreaCode OPTIONAL, + defaultCallHandling [150] DefaultCallHandling OPTIONAL, + freeFormatData [151] FreeFormatData OPTIONAL, + freeFormatDataAppend [152] BOOLEAN OPTIONAL, + numberOfDPEncountered [153] INTEGER OPTIONAL, + levelOfCAMELService [154] LevelOfCAMELService OPTIONAL, + roamingNumber [160] RoamingNumber OPTIONAL, + mscIncomingCircuit [166] MSCCIC OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + callEmlppPriority [170] EmlppPriority OPTIONAL, + calledDefaultEmlppPriority [171] EmlppPriority OPTIONAL, + eaSubscriberInfo [174] EASubscriberInfo OPTIONAL, + selectedCIC [175] SelectedCIC OPTIONAL, + optimalRoutingFlag [177] NULL OPTIONAL, + portedflag [180] PortedFlag OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + changeOfglobalAreaID [189] SEQUENCE OF ChangeOfglobalAreaID OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + intermediatemccmnc [193] MCCMNC OPTIONAL, + lastmccmnc [194] MCCMNC OPTIONAL, + cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL, + cUGInterlockCode [196] CUGInterlockCode OPTIONAL, + cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL, + cUGIndex [198] CUGIndex OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + redirectingnumber [201] RedirectingNumber OPTIONAL, + redirectingcounter [202] RedirectingCounter OPTIONAL, + setupTime [203] TimeStamp OPTIONAL, + alertingTime [204] TimeStamp OPTIONAL, + calledNumber [205] CalledNumber OPTIONAL, + voiceIndicator [206] VoiceIndicator OPTIONAL, + bCategory [207] BCategory OPTIONAL, + callType [208] CallType OPTIONAL +} + +RoamingRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedMSISDN [2] MSISDN OPTIONAL, + callingNumber [3] CallingNumber OPTIONAL, + roamingNumber [4] RoamingNumber OPTIONAL, + recordingEntity [5] RecordingEntity OPTIONAL, + mscIncomingROUTE [6] ROUTE OPTIONAL, + mscOutgoingROUTE [7] ROUTE OPTIONAL, + basicService [8] BasicServiceCode OPTIONAL, + transparencyIndicator [9] TransparencyInd OPTIONAL, + changeOfService [10] SEQUENCE OF ChangeOfService OPTIONAL, + supplServicesUsed [11] SEQUENCE OF SuppServiceUsed OPTIONAL, + seizureTime [12] TimeStamp OPTIONAL, + answerTime [13] TimeStamp OPTIONAL, + releaseTime [14] TimeStamp OPTIONAL, + callDuration [15] CallDuration OPTIONAL, + causeForTerm [17] CauseForTerm OPTIONAL, + diagnostics [18] Diagnostics OPTIONAL, + callReference [19] CallReference OPTIONAL, + sequenceNumber [20] SequenceNumber OPTIONAL, + recordExtensions [21] ManagementExtensions OPTIONAL, + networkCallReference [22] NetworkCallReference OPTIONAL, + mSCAddress [23] MSCAddress OPTIONAL, + partialRecordType [30] PartialRecordType OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + callingChargeAreaCode [145] ChargeAreaCode OPTIONAL, + calledChargeAreaCode [146] ChargeAreaCode OPTIONAL, + mscOutgoingCircuit [166] MSCCIC OPTIONAL, + mscIncomingCircuit [167] MSCCIC OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + callEmlppPriority [170] EmlppPriority OPTIONAL, + eaSubscriberInfo [174] EASubscriberInfo OPTIONAL, + selectedCIC [175] SelectedCIC OPTIONAL, + optimalRoutingFlag [177] NULL OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL, + cUGInterlockCode [196] CUGInterlockCode OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL +} + +TermCAMELRecord ::= SET +{ + recordtype [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedMSISDN [2] MSISDN OPTIONAL, + recordingEntity [3] RecordingEntity OPTIONAL, + interrogationTime [4] TimeStamp OPTIONAL, + destinationRoutingAddress [5] DestinationRoutingAddress OPTIONAL, + gsm-SCFAddress [6] Gsm-SCFAddress OPTIONAL, + serviceKey [7] ServiceKey OPTIONAL, + networkCallReference [8] NetworkCallReference OPTIONAL, + mSCAddress [9] MSCAddress OPTIONAL, + defaultCallHandling [10] DefaultCallHandling OPTIONAL, + recordExtensions [11] ManagementExtensions OPTIONAL, + calledNumber [12] CalledNumber OPTIONAL, + callingNumber [13] CallingNumber OPTIONAL, + mscIncomingROUTE [14] ROUTE OPTIONAL, + mscOutgoingROUTE [15] ROUTE OPTIONAL, + seizureTime [16] TimeStamp OPTIONAL, + answerTime [17] TimeStamp OPTIONAL, + releaseTime [18] TimeStamp OPTIONAL, + callDuration [19] CallDuration OPTIONAL, + causeForTerm [21] CauseForTerm OPTIONAL, + diagnostics [22] Diagnostics OPTIONAL, + callReference [23] CallReference OPTIONAL, + sequenceNumber [24] SequenceNumber OPTIONAL, + numberOfDPEncountered [25] INTEGER OPTIONAL, + levelOfCAMELService [26] LevelOfCAMELService OPTIONAL, + freeFormatData [27] FreeFormatData OPTIONAL, + cAMELCallLegInformation [28] SEQUENCE OF CAMELInformation OPTIONAL, + freeFormatDataAppend [29] BOOLEAN OPTIONAL, + mscServerIndication [30] BOOLEAN OPTIONAL, + defaultCallHandling-2 [31] DefaultCallHandling OPTIONAL, + gsm-SCFAddress-2 [32] Gsm-SCFAddress OPTIONAL, + serviceKey-2 [33] ServiceKey OPTIONAL, + freeFormatData-2 [34] FreeFormatData OPTIONAL, + freeFormatDataAppend-2 [35] BOOLEAN OPTIONAL, + partialRecordType [42] PartialRecordType OPTIONAL, + basicService [130] BasicServiceCode OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL +} + +IncGatewayRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + callingNumber [1] CallingNumber OPTIONAL, + calledNumber [2] CalledNumber OPTIONAL, + recordingEntity [3] RecordingEntity OPTIONAL, + mscIncomingROUTE [4] ROUTE OPTIONAL, + mscOutgoingROUTE [5] ROUTE OPTIONAL, + seizureTime [6] TimeStamp OPTIONAL, + answerTime [7] TimeStamp OPTIONAL, + releaseTime [8] TimeStamp OPTIONAL, + callDuration [9] CallDuration OPTIONAL, + causeForTerm [11] CauseForTerm OPTIONAL, + diagnostics [12] Diagnostics OPTIONAL, + callReference [13] CallReference OPTIONAL, + sequenceNumber [14] SequenceNumber OPTIONAL, + recordExtensions [15] ManagementExtensions OPTIONAL, + partialRecordType [22] PartialRecordType OPTIONAL, + iSDN-BC [23] ISDN-BC OPTIONAL, + lLC [24] LLC OPTIONAL, + hLC [25] HLC OPTIONAL, + basicService [130] BasicServiceCode OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + rateIndication [159] RateIndication OPTIONAL, + roamingNumber [160] RoamingNumber OPTIONAL, + mscIncomingCircuit [167] MSCCIC OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + callEmlppPriority [170] EmlppPriority OPTIONAL, + eaSubscriberInfo [174] EASubscriberInfo OPTIONAL, + selectedCIC [175] SelectedCIC OPTIONAL, + cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL, + cUGInterlockCode [196] CUGInterlockCode OPTIONAL, + cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL, + mscIncomingRouteAttribute [198] RouteAttribute OPTIONAL, + mscOutgoingRouteAttribute [199] RouteAttribute OPTIONAL, + networkCallReference [200] NetworkCallReference OPTIONAL, + setupTime [201] TimeStamp OPTIONAL, + alertingTime [202] TimeStamp OPTIONAL, + voiceIndicator [203] VoiceIndicator OPTIONAL, + bCategory [204] BCategory OPTIONAL, + callType [205] CallType OPTIONAL +} + +OutGatewayRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + callingNumber [1] CallingNumber OPTIONAL, + calledNumber [2] CalledNumber OPTIONAL, + recordingEntity [3] RecordingEntity OPTIONAL, + mscIncomingROUTE [4] ROUTE OPTIONAL, + mscOutgoingROUTE [5] ROUTE OPTIONAL, + seizureTime [6] TimeStamp OPTIONAL, + answerTime [7] TimeStamp OPTIONAL, + releaseTime [8] TimeStamp OPTIONAL, + callDuration [9] CallDuration OPTIONAL, + causeForTerm [11] CauseForTerm OPTIONAL, + diagnostics [12] Diagnostics OPTIONAL, + callReference [13] CallReference OPTIONAL, + sequenceNumber [14] SequenceNumber OPTIONAL, + recordExtensions [15] ManagementExtensions OPTIONAL, + partialRecordType [22] PartialRecordType OPTIONAL, + basicService [130] BasicServiceCode OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + rateIndication [159] RateIndication OPTIONAL, + roamingNumber [160] RoamingNumber OPTIONAL, + mscOutgoingCircuit [166] MSCCIC OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + eaSubscriberInfo [174] EASubscriberInfo OPTIONAL, + selectedCIC [175] SelectedCIC OPTIONAL, + callEmlppPriority [170] EmlppPriority OPTIONAL, + cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL, + cUGInterlockCode [196] CUGInterlockCode OPTIONAL, + cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL, + mscIncomingRouteAttribute [198] RouteAttribute OPTIONAL, + mscOutgoingRouteAttribute [199] RouteAttribute OPTIONAL, + networkCallReference [200] NetworkCallReference OPTIONAL, + setupTime [201] TimeStamp OPTIONAL, + alertingTime [202] TimeStamp OPTIONAL, + voiceIndicator [203] VoiceIndicator OPTIONAL, + bCategory [204] BCategory OPTIONAL, + callType [205] CallType OPTIONAL +} + +TransitCallRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + recordingEntity [1] RecordingEntity OPTIONAL, + mscIncomingROUTE [2] ROUTE OPTIONAL, + mscOutgoingROUTE [3] ROUTE OPTIONAL, + callingNumber [4] CallingNumber OPTIONAL, + calledNumber [5] CalledNumber OPTIONAL, + isdnBasicService [6] BasicService OPTIONAL, + seizureTime [7] TimeStamp OPTIONAL, + answerTime [8] TimeStamp OPTIONAL, + releaseTime [9] TimeStamp OPTIONAL, + callDuration [10] CallDuration OPTIONAL, + causeForTerm [12] CauseForTerm OPTIONAL, + diagnostics [13] Diagnostics OPTIONAL, + callReference [14] CallReference OPTIONAL, + sequenceNumber [15] SequenceNumber OPTIONAL, + recordExtensions [16] ManagementExtensions OPTIONAL, + partialRecordType [23] PartialRecordType OPTIONAL, + basicService [130] BasicServiceCode OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + rateIndication [159] RateIndication OPTIONAL, + mscOutgoingCircuit [166] MSCCIC OPTIONAL, + mscIncomingCircuit [167] MSCCIC OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + callEmlppPriority [170] EmlppPriority OPTIONAL, + eaSubscriberInfo [174] EASubscriberInfo OPTIONAL, + selectedCIC [175] SelectedCIC OPTIONAL, + cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL, + cUGInterlockCode [196] CUGInterlockCode OPTIONAL, + cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL, + mscIncomingRouteAttribute [198] RouteAttribute OPTIONAL, + mscOutgoingRouteAttribute [199] RouteAttribute OPTIONAL, + networkCallReference [200] NetworkCallReference OPTIONAL, + setupTime [201] TimeStamp OPTIONAL, + alertingTime [202] TimeStamp OPTIONAL, + voiceIndicator [203] VoiceIndicator OPTIONAL, + bCategory [204] BCategory OPTIONAL, + callType [205] CallType OPTIONAL +} + +MOSMSRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedIMEI [2] IMEI OPTIONAL, + servedMSISDN [3] MSISDN OPTIONAL, + msClassmark [4] Classmark OPTIONAL, + serviceCentre [5] AddressString OPTIONAL, + recordingEntity [6] RecordingEntity OPTIONAL, + location [7] LocationAreaAndCell OPTIONAL, + messageReference [8] MessageReference OPTIONAL, + originationTime [9] TimeStamp OPTIONAL, + smsResult [10] SMSResult OPTIONAL, + recordExtensions [11] ManagementExtensions OPTIONAL, + destinationNumber [12] SmsTpDestinationNumber OPTIONAL, + cAMELSMSInformation [13] CAMELSMSInformation OPTIONAL, + systemType [14] SystemType OPTIONAL, + basicService [130] BasicServiceCode OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + classmark3 [140] Classmark3 OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + smsUserDataType [195] SmsUserDataType OPTIONAL, + smstext [196] SMSTEXT OPTIONAL, + maximumNumberOfSMSInTheConcatenatedSMS [197] MaximumNumberOfSMSInTheConcatenatedSMS OPTIONAL, + concatenatedSMSReferenceNumber [198] ConcatenatedSMSReferenceNumber OPTIONAL, + sequenceNumberOfTheCurrentSMS [199] SequenceNumberOfTheCurrentSMS OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + callReference [201] CallReference OPTIONAL +} + +MTSMSRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + serviceCentre [1] AddressString OPTIONAL, + servedIMSI [2] IMSI OPTIONAL, + servedIMEI [3] IMEI OPTIONAL, + servedMSISDN [4] MSISDN OPTIONAL, + msClassmark [5] Classmark OPTIONAL, + recordingEntity [6] RecordingEntity OPTIONAL, + location [7] LocationAreaAndCell OPTIONAL, + deliveryTime [8] TimeStamp OPTIONAL, + smsResult [9] SMSResult OPTIONAL, + recordExtensions [10] ManagementExtensions OPTIONAL, + systemType [11] SystemType OPTIONAL, + cAMELSMSInformation [12] CAMELSMSInformation OPTIONAL, + basicService [130] BasicServiceCode OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + classmark3 [140] Classmark3 OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + smsUserDataType [195] SmsUserDataType OPTIONAL, + smstext [196] SMSTEXT OPTIONAL, + maximumNumberOfSMSInTheConcatenatedSMS [197] MaximumNumberOfSMSInTheConcatenatedSMS OPTIONAL, + concatenatedSMSReferenceNumber [198] ConcatenatedSMSReferenceNumber OPTIONAL, + sequenceNumberOfTheCurrentSMS [199] SequenceNumberOfTheCurrentSMS OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + origination [201] CallingNumber OPTIONAL, + callReference [202] CallReference OPTIONAL +} + +HLRIntRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedMSISDN [2] MSISDN OPTIONAL, + recordingEntity [3] RecordingEntity OPTIONAL, + basicService [4] BasicServiceCode OPTIONAL, + routingNumber [5] RoutingNumber OPTIONAL, + interrogationTime [6] TimeStamp OPTIONAL, + numberOfForwarding [7] NumberOfForwarding OPTIONAL, + interrogationResult [8] HLRIntResult OPTIONAL, + recordExtensions [9] ManagementExtensions OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + callReference [169] CallReference OPTIONAL +} + +SSActionRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedIMEI [2] IMEI OPTIONAL, + servedMSISDN [3] MSISDN OPTIONAL, + msClassmark [4] Classmark OPTIONAL, + recordingEntity [5] RecordingEntity OPTIONAL, + location [6] LocationAreaAndCell OPTIONAL, + basicServices [7] BasicServices OPTIONAL, + supplService [8] SS-Code OPTIONAL, + ssAction [9] SSActionType OPTIONAL, + ssActionTime [10] TimeStamp OPTIONAL, + ssParameters [11] SSParameters OPTIONAL, + ssActionResult [12] SSActionResult OPTIONAL, + callReference [13] CallReference OPTIONAL, + recordExtensions [14] ManagementExtensions OPTIONAL, + systemType [15] SystemType OPTIONAL, + ussdCodingScheme [126] UssdCodingScheme OPTIONAL, + ussdString [127] SEQUENCE OF UssdString OPTIONAL, + ussdNotifyCounter [128] UssdNotifyCounter OPTIONAL, + ussdRequestCounter [129] UssdRequestCounter OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + classmark3 [140] Classmark3 OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL +} + +CommonEquipRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + equipmentType [1] EquipmentType OPTIONAL, + equipmentId [2] EquipmentId OPTIONAL, + servedIMSI [3] IMSI OPTIONAL, + servedMSISDN [4] MSISDN OPTIONAL, + recordingEntity [5] RecordingEntity OPTIONAL, + basicService [6] BasicServiceCode OPTIONAL, + changeOfService [7] SEQUENCE OF ChangeOfService OPTIONAL, + supplServicesUsed [8] SEQUENCE OF SuppServiceUsed OPTIONAL, + seizureTime [9] TimeStamp OPTIONAL, + releaseTime [10] TimeStamp OPTIONAL, + callDuration [11] CallDuration OPTIONAL, + callReference [12] CallReference OPTIONAL, + sequenceNumber [13] SequenceNumber OPTIONAL, + recordExtensions [14] ManagementExtensions OPTIONAL, + systemType [15] SystemType OPTIONAL, + rateIndication [16] RateIndication OPTIONAL, + fnur [17] Fnur OPTIONAL, + partialRecordType [18] PartialRecordType OPTIONAL, + causeForTerm [100] CauseForTerm OPTIONAL, + diagnostics [101] Diagnostics OPTIONAL, + servedIMEI [102] IMEI OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL +} + +------------------------------------------------------------------------------ +-- +-- OBSERVED IMEI TICKETS +-- +------------------------------------------------------------------------------ + +ObservedIMEITicket ::= SET +{ + servedIMEI [0] IMEI, + imeiStatus [1] IMEIStatus, + servedIMSI [2] IMSI, + servedMSISDN [3] MSISDN OPTIONAL, + recordingEntity [4] RecordingEntity, + eventTime [5] TimeStamp, + location [6] LocationAreaAndCell, + imeiCheckEvent [7] IMEICheckEvent OPTIONAL, + callReference [8] CallReference OPTIONAL, + recordExtensions [9] ManagementExtensions OPTIONAL, + orgMSCId [168] MSCId OPTIONAL +} + + + +------------------------------------------------------------------------------ +-- +-- LOCATION SERICE TICKETS +-- +------------------------------------------------------------------------------ + +MTLCSRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + recordingEntity [1] RecordingEntity OPTIONAL, + lcsClientType [2] LCSClientType OPTIONAL, + lcsClientIdentity [3] LCSClientIdentity OPTIONAL, + servedIMSI [4] IMSI OPTIONAL, + servedMSISDN [5] MSISDN OPTIONAL, + locationType [6] LocationType OPTIONAL, + lcsQos [7] LCSQoSInfo OPTIONAL, + lcsPriority [8] LCS-Priority OPTIONAL, + mlc-Number [9] ISDN-AddressString OPTIONAL, + eventTimeStamp [10] TimeStamp OPTIONAL, + measureDuration [11] CallDuration OPTIONAL, + notificationToMSUser [12] NotificationToMSUser OPTIONAL, + privacyOverride [13] NULL OPTIONAL, + location [14] LocationAreaAndCell OPTIONAL, + locationEstimate [15] Ext-GeographicalInformation OPTIONAL, + positioningData [16] PositioningData OPTIONAL, + lcsCause [17] LCSCause OPTIONAL, + diagnostics [18] Diagnostics OPTIONAL, + systemType [19] SystemType OPTIONAL, + recordExtensions [20] ManagementExtensions OPTIONAL, + causeForTerm [21] CauseForTerm OPTIONAL, + lcsReferenceNumber [101] CallReferenceNumber OPTIONAL, + servedIMEI [102] IMEI OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + callReference [201] CallReference OPTIONAL +} + +MOLCSRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + recordingEntity [1] RecordingEntity OPTIONAL, + lcsClientType [2] LCSClientType OPTIONAL, + lcsClientIdentity [3] LCSClientIdentity OPTIONAL, + servedIMSI [4] IMSI OPTIONAL, + servedMSISDN [5] MSISDN OPTIONAL, + molr-Type [6] MOLR-Type OPTIONAL, + lcsQos [7] LCSQoSInfo OPTIONAL, + lcsPriority [8] LCS-Priority OPTIONAL, + mlc-Number [9] ISDN-AddressString OPTIONAL, + eventTimeStamp [10] TimeStamp OPTIONAL, + measureDuration [11] CallDuration OPTIONAL, + location [12] LocationAreaAndCell OPTIONAL, + locationEstimate [13] Ext-GeographicalInformation OPTIONAL, + positioningData [14] PositioningData OPTIONAL, + lcsCause [15] LCSCause OPTIONAL, + diagnostics [16] Diagnostics OPTIONAL, + systemType [17] SystemType OPTIONAL, + recordExtensions [18] ManagementExtensions OPTIONAL, + causeForTerm [19] CauseForTerm OPTIONAL, + lcsReferenceNumber [101] CallReferenceNumber OPTIONAL, + servedIMEI [102] IMEI OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + callReference [201] CallReference OPTIONAL +} + +NILCSRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + recordingEntity [1] RecordingEntity OPTIONAL, + lcsClientType [2] LCSClientType OPTIONAL, + lcsClientIdentity [3] LCSClientIdentity OPTIONAL, + servedIMSI [4] IMSI OPTIONAL, + servedMSISDN [5] MSISDN OPTIONAL, + servedIMEI [6] IMEI OPTIONAL, + emsDigits [7] ISDN-AddressString OPTIONAL, + emsKey [8] ISDN-AddressString OPTIONAL, + lcsQos [9] LCSQoSInfo OPTIONAL, + lcsPriority [10] LCS-Priority OPTIONAL, + mlc-Number [11] ISDN-AddressString OPTIONAL, + eventTimeStamp [12] TimeStamp OPTIONAL, + measureDuration [13] CallDuration OPTIONAL, + location [14] LocationAreaAndCell OPTIONAL, + locationEstimate [15] Ext-GeographicalInformation OPTIONAL, + positioningData [16] PositioningData OPTIONAL, + lcsCause [17] LCSCause OPTIONAL, + diagnostics [18] Diagnostics OPTIONAL, + systemType [19] SystemType OPTIONAL, + recordExtensions [20] ManagementExtensions OPTIONAL, + causeForTerm [21] CauseForTerm OPTIONAL, + lcsReferenceNumber [101] CallReferenceNumber OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + callReference [201] CallReference OPTIONAL +} + + +------------------------------------------------------------------------------ +-- +-- FTAM / FTP / TFTP FILE CONTENTS +-- +------------------------------------------------------------------------------ + +CallEventDataFile ::= SEQUENCE +{ + headerRecord [0] HeaderRecord, + callEventRecords [1] SEQUENCE OF CallEventRecord, + trailerRecord [2] TrailerRecord, + extensions [3] ManagementExtensions +} + +ObservedIMEITicketFile ::= SEQUENCE +{ + productionDateTime [0] TimeStamp, + observedIMEITickets [1] SEQUENCE OF ObservedIMEITicket, + noOfRecords [2] INTEGER, + extensions [3] ManagementExtensions +} + +HeaderRecord ::= SEQUENCE +{ + productionDateTime [0] TimeStamp, + recordingEntity [1] RecordingEntity, + extensions [2] ManagementExtensions +} + +TrailerRecord ::= SEQUENCE +{ + productionDateTime [0] TimeStamp, + recordingEntity [1] RecordingEntity, + firstCallDateTime [2] TimeStamp, + lastCallDateTime [3] TimeStamp, + noOfRecords [4] INTEGER, + extensions [5] ManagementExtensions +} + + +------------------------------------------------------------------------------ +-- +-- COMMON DATA TYPES +-- +------------------------------------------------------------------------------ + +AdditionalChgInfo ::= SEQUENCE +{ + chargeIndicator [0] ChargeIndicator OPTIONAL, + chargeParameters [1] OCTET STRING OPTIONAL +} + +AddressString ::= OCTET STRING -- (SIZE (1..maxAddressLength)) + -- This type is used to represent a number for addressing + -- purposes. It is composed of + -- a) one octet for nature of address, and numbering plan + -- indicator. + -- b) digits of an address encoded as TBCD-String. + + -- a) The first octet includes a one bit extension indicator, a + -- 3 bits nature of address indicator and a 4 bits numbering + -- plan indicator, encoded as follows: + + -- bit 8: 1 (no extension) + + -- bits 765: nature of address indicator + -- 000 unknown + -- 001 international number + -- 010 national significant number + -- 011 network specific number + -- 100 subscriber number + -- 101 reserved + -- 110 abbreviated number + -- 111 reserved for extension + + -- bits 4321: numbering plan indicator + -- 0000 unknown + -- 0001 ISDN/Telephony Numbering Plan (Rec CCITT E.164) + -- 0010 spare + -- 0011 data numbering plan (CCITT Rec X.121) + -- 0100 telex numbering plan (CCITT Rec F.69) + -- 0101 spare + -- 0110 land mobile numbering plan (CCITT Rec E.212) + -- 0111 spare + -- 1000 national numbering plan + -- 1001 private numbering plan + -- 1111 reserved for extension + + -- all other values are reserved. + + -- b) The following octets representing digits of an address + -- encoded as a TBCD-STRING. + +-- maxAddressLength INTEGER ::= 20 + +AiurRequested ::= ENUMERATED +{ + -- + -- See Bearer Capability TS 24.008 + -- (note that value "4" is intentionally missing + -- because it is not used in TS 24.008) + -- + + aiur09600BitsPerSecond (1), + aiur14400BitsPerSecond (2), + aiur19200BitsPerSecond (3), + aiur28800BitsPerSecond (5), + aiur38400BitsPerSecond (6), + aiur43200BitsPerSecond (7), + aiur57600BitsPerSecond (8), + aiur38400BitsPerSecond1 (9), + aiur38400BitsPerSecond2 (10), + aiur38400BitsPerSecond3 (11), + aiur38400BitsPerSecond4 (12) +} + +AOCParameters ::= SEQUENCE +{ + -- + -- See TS 22.024. + -- + e1 [1] EParameter OPTIONAL, + e2 [2] EParameter OPTIONAL, + e3 [3] EParameter OPTIONAL, + e4 [4] EParameter OPTIONAL, + e5 [5] EParameter OPTIONAL, + e6 [6] EParameter OPTIONAL, + e7 [7] EParameter OPTIONAL +} + +AOCParmChange ::= SEQUENCE +{ + changeTime [0] TimeStamp, + newParameters [1] AOCParameters +} + +BasicService ::= OCTET STRING -- (SIZE(1)) + +--This parameter identifies the ISDN Basic service as defined in ETSI specification ETS 300 196. +-- allServices '00'h +-- speech '01'h +-- unrestricteDigtalInfo '02'h +-- audio3k1HZ '03'h +-- unrestricteDigtalInfowithtoneandannoucement '04'h +-- telephony3k1HZ '20'h +-- teletext '21'h +-- telefaxGroup4Class1 '22'h +-- videotextSyntaxBased '23'h +-- videotelephony '24'h +-- telefaxGroup2-3 '25'h +-- telephony7kHZ '26'h + + + +BasicServices ::= SET OF BasicServiceCode + +BasicServiceCode ::= CHOICE +{ + bearerService [2] BearerServiceCode, + teleservice [3] TeleserviceCode +} + + +TeleserviceCode ::= OCTET STRING -- (SIZE (1)) + -- This type is used to represent the code identifying a single + -- teleservice, a group of teleservices, or all teleservices. The + -- services are defined in TS GSM 02.03. + -- The internal structure is defined as follows: + + -- bits 87654321: group (bits 8765) and specific service + -- (bits 4321) + +-- allTeleservices (0x00), +-- allSpeechTransmissionServices (0x10), +-- telephony (0x11), +-- emergencyCalls (0x12), +-- +-- allShortMessageServices (0x20), +-- shortMessageMT-PP (0x21), +-- shortMessageMO-PP (0x22), +-- +-- allFacsimileTransmissionServices (0x60), +-- facsimileGroup3AndAlterSpeech (0x61), +-- automaticFacsimileGroup3 (0x62), +-- facsimileGroup4 (0x63), +-- +-- The following non-hierarchical Compound Teleservice Groups +-- are defined in TS GSM 02.30: +-- allDataTeleservices (0x70), +-- covers Teleservice Groups 'allFacsimileTransmissionServices' +-- and 'allShortMessageServices' +-- allTeleservices-ExeptSMS (0x80), +-- covers Teleservice Groups 'allSpeechTransmissionServices' and +-- 'allFacsimileTransmissionServices' +-- +-- Compound Teleservice Group Codes are only used in call +-- independent supplementary service operations, i.e. they +-- are not used in InsertSubscriberData or in +-- DeleteSubscriberData messages. +-- +-- allVoiceGroupCallServices (0x90), +-- voiceGroupCall (0x91), +-- voiceBroadcastCall (0x92), +-- +-- allPLMN-specificTS (0xd0), +-- plmn-specificTS-1 (0xd1), +-- plmn-specificTS-2 (0xd2), +-- plmn-specificTS-3 (0xd3), +-- plmn-specificTS-4 (0xd4), +-- plmn-specificTS-5 (0xd5), +-- plmn-specificTS-6 (0xd6), +-- plmn-specificTS-7 (0xd7), +-- plmn-specificTS-8 (0xd8), +-- plmn-specificTS-9 (0xd9), +-- plmn-specificTS-A (0xda), +-- plmn-specificTS-B (0xdb), +-- plmn-specificTS-C (0xdc), +-- plmn-specificTS-D (0xdd), +-- plmn-specificTS-E (0xde), +-- plmn-specificTS-F (0xdf) + + +BearerServiceCode ::= OCTET STRING -- (SIZE (1)) + -- This type is used to represent the code identifying a single + -- bearer service, a group of bearer services, or all bearer + -- services. The services are defined in TS 3GPP TS 22.002 [3]. + -- The internal structure is defined as follows: + -- + -- plmn-specific bearer services: + -- bits 87654321: defined by the HPLMN operator + + -- rest of bearer services: + -- bit 8: 0 (unused) + -- bits 7654321: group (bits 7654), and rate, if applicable + -- (bits 321) + +-- allBearerServices (0x00), +-- allDataCDA-Services (0x10), +-- dataCDA-300bps (0x11), +-- dataCDA-1200bps (0x12), +-- dataCDA-1200-75bps (0x13), +-- dataCDA-2400bps (0x14), +-- dataCDA-4800bps (0x15), +-- dataCDA-9600bps (0x16), +-- general-dataCDA (0x17), +-- +-- allDataCDS-Services (0x18), +-- dataCDS-1200bps (0x1a), +-- dataCDS-2400bps (0x1c), +-- dataCDS-4800bps (0x1d), +-- dataCDS-9600bps (0x1e), +-- general-dataCDS (0x1f), +-- +-- allPadAccessCA-Services (0x20), +-- padAccessCA-300bps (0x21), +-- padAccessCA-1200bps (0x22), +-- padAccessCA-1200-75bps (0x23), +-- padAccessCA-2400bps (0x24), +-- padAccessCA-4800bps (0x25), +-- padAccessCA-9600bps (0x26), +-- general-padAccessCA (0x27), +-- +-- allDataPDS-Services (0x28), +-- dataPDS-2400bps (0x2c), +-- dataPDS-4800bps (0x2d), +-- dataPDS-9600bps (0x2e), +-- general-dataPDS (0x2f), +-- +-- allAlternateSpeech-DataCDA (0x30), +-- +-- allAlternateSpeech-DataCDS (0x38), +-- +-- allSpeechFollowedByDataCDA (0x40), +-- +-- allSpeechFollowedByDataCDS (0x48), +-- +-- The following non-hierarchical Compound Bearer Service +-- Groups are defined in TS GSM 02.30: +-- allDataCircuitAsynchronous (0x50), +-- covers "allDataCDA-Services", "allAlternateSpeech-DataCDA" and +-- "allSpeechFollowedByDataCDA" +-- allDataCircuitSynchronous (0x58), +-- covers "allDataCDS-Services", "allAlternateSpeech-DataCDS" and +-- "allSpeechFollowedByDataCDS" +-- allAsynchronousServices (0x60), +-- covers "allDataCDA-Services", "allAlternateSpeech-DataCDA", +-- "allSpeechFollowedByDataCDA" and "allPadAccessCDA-Services" +-- allSynchronousServices (0x68), +-- covers "allDataCDS-Services", "allAlternateSpeech-DataCDS", +-- "allSpeechFollowedByDataCDS" and "allDataPDS-Services" +-- +-- Compound Bearer Service Group Codes are only used in call +-- independent supplementary service operations, i.e. they +-- are not used in InsertSubscriberData or in +-- DeleteSubscriberData messages. +-- +-- allPLMN-specificBS (0xd0), +-- plmn-specificBS-1 (0xd1), +-- plmn-specificBS-2 (0xd2), +-- plmn-specificBS-3 (0xd3), +-- plmn-specificBS-4 (0xd4), +-- plmn-specificBS-5 (0xd5), +-- plmn-specificBS-6 (0xd6), +-- plmn-specificBS-7 (0xd7), +-- plmn-specificBS-8 (0xd8), +-- plmn-specificBS-9 (0xd9), +-- plmn-specificBS-A (0xda), +-- plmn-specificBS-B (0xdb), +-- plmn-specificBS-C (0xdc), +-- plmn-specificBS-D (0xdd), +-- plmn-specificBS-E (0xde), +-- plmn-specificBS-F (0xdf) + + +BCDDirectoryNumber ::= OCTET STRING + -- This type contains the binary coded decimal representation of + -- a directory number e.g. calling/called/connected/translated number. + -- The encoding of the octet string is in accordance with the + -- the elements "Calling party BCD number", "Called party BCD number" + -- and "Connected number" defined in TS 24.008. + -- This encoding includes type of number and number plan information + -- together with a BCD encoded digit string. + -- It may also contain both a presentation and screening indicator + -- (octet 3a). + -- For the avoidance of doubt, this field does not include + -- octets 1 and 2, the element name and length, as this would be + -- redundant. + +CallDuration ::= INTEGER + -- + -- The call duration in seconds. + -- For successful calls this is the chargeable duration. + -- For call attempts this is the call holding time. + -- + +CallEventRecordType ::= ENUMERATED -- INTEGER +{ + moCallRecord (0), + mtCallRecord (1), + roamingRecord (2), + incGatewayRecord (3), + outGatewayRecord (4), + transitCallRecord (5), + moSMSRecord (6), + mtSMSRecord (7), + ssActionRecord (10), + hlrIntRecord (11), + commonEquipRecord (14), + moTraceRecord (15), + mtTraceRecord (16), + termCAMELRecord (17), + mtLCSRecord (23), + moLCSRecord (24), + niLCSRecord (25), + forwardCallRecord (100) +} + +CalledNumber ::= BCDDirectoryNumber + +CallingNumber ::= BCDDirectoryNumber + +CallingPartyCategory ::= Category + +CallReference ::= OCTET STRING -- (SIZE (1..8)) + +CallReferenceNumber ::= OCTET STRING -- (SIZE (1..8)) + +CAMELDestinationNumber ::= DestinationRoutingAddress + +CAMELInformation ::= SET +{ + cAMELDestinationNumber [1] CAMELDestinationNumber OPTIONAL, + connectedNumber [2] ConnectedNumber OPTIONAL, + roamingNumber [3] RoamingNumber OPTIONAL, + mscOutgoingROUTE [4] ROUTE OPTIONAL, + seizureTime [5] TimeStamp OPTIONAL, + answerTime [6] TimeStamp OPTIONAL, + releaseTime [7] TimeStamp OPTIONAL, + callDuration [8] CallDuration OPTIONAL, + dataVolume [9] DataVolume OPTIONAL, + cAMELInitCFIndicator [10] CAMELInitCFIndicator OPTIONAL, + causeForTerm [11] CauseForTerm OPTIONAL, + cAMELModification [12] ChangedParameters OPTIONAL, + freeFormatData [13] FreeFormatData OPTIONAL, + diagnostics [14] Diagnostics OPTIONAL, + freeFormatDataAppend [15] BOOLEAN OPTIONAL, + freeFormatData-2 [16] FreeFormatData OPTIONAL, + freeFormatDataAppend-2 [17] BOOLEAN OPTIONAL +} + +CAMELSMSInformation ::= SET +{ + gsm-SCFAddress [1] Gsm-SCFAddress OPTIONAL, + serviceKey [2] ServiceKey OPTIONAL, + defaultSMSHandling [3] DefaultSMS-Handling OPTIONAL, + freeFormatData [4] FreeFormatData OPTIONAL, + callingPartyNumber [5] CallingNumber OPTIONAL, + destinationSubscriberNumber [6] CalledNumber OPTIONAL, + cAMELSMSCAddress [7] AddressString OPTIONAL, + smsReferenceNumber [8] CallReferenceNumber OPTIONAL +} + +CAMELInitCFIndicator ::= ENUMERATED +{ + noCAMELCallForwarding (0), + cAMELCallForwarding (1) +} + +CAMELModificationParameters ::= SET + -- + -- The list contains only parameters changed due to CAMEL call + -- handling. + -- +{ + callingPartyNumber [0] CallingNumber OPTIONAL, + callingPartyCategory [1] CallingPartyCategory OPTIONAL, + originalCalledPartyNumber [2] OriginalCalledNumber OPTIONAL, + genericNumbers [3] GenericNumbers OPTIONAL, + redirectingPartyNumber [4] RedirectingNumber OPTIONAL, + redirectionCounter [5] NumberOfForwarding OPTIONAL +} + + +Category ::= OCTET STRING -- (SIZE(1)) + -- + -- The internal structure is defined in ITU-T Rec Q.763. + --see subscribe category + +CauseForTerm ::= ENUMERATED -- INTEGER + -- + -- Cause codes from 16 up to 31 are defined in TS 32.015 as 'CauseForRecClosing' + -- (cause for record closing). + -- There is no direct correlation between these two types. + -- LCS related causes belong to the MAP error causes acc. TS 29.002. + -- +{ + normalRelease (0), + partialRecord (1), + partialRecordCallReestablishment (2), + unsuccessfulCallAttempt (3), + stableCallAbnormalTermination (4), + cAMELInitCallRelease (5), + unauthorizedRequestingNetwork (52), + unauthorizedLCSClient (53), + positionMethodFailure (54), + unknownOrUnreachableLCSClient (58) +} + +CellId ::= OCTET STRING -- (SIZE(2)) + -- + -- Coded according to TS 24.008 + -- + +ChangedParameters ::= SET +{ + changeFlags [0] ChangeFlags, + changeList [1] CAMELModificationParameters OPTIONAL +} + +ChangeFlags ::= BIT STRING +-- { +-- callingPartyNumberModified (0), +-- callingPartyCategoryModified (1), +-- originalCalledPartyNumberModified (2), +-- genericNumbersModified (3), +-- redirectingPartyNumberModified (4), +-- redirectionCounterModified (5) +-- } + +ChangeOfClassmark ::= SEQUENCE +{ + classmark [0] Classmark, + changeTime [1] TimeStamp +} + +ChangeOfRadioChannel ::= SEQUENCE +{ + radioChannel [0] TrafficChannel, + changeTime [1] TimeStamp, + speechVersionUsed [2] SpeechVersionIdentifier OPTIONAL +} + +ChangeOfService ::= SEQUENCE +{ + basicService [0] BasicServiceCode, + transparencyInd [1] TransparencyInd OPTIONAL, + changeTime [2] TimeStamp, + rateIndication [3] RateIndication OPTIONAL, + fnur [4] Fnur OPTIONAL +} + +ChannelCoding ::= ENUMERATED +{ + tchF4800 (1), + tchF9600 (2), + tchF14400 (3) +} + +ChargeIndicator ::= ENUMERATED -- INTEGER +{ + noIndication (0), + noCharge (1), + charge (2) +} + +Classmark ::= OCTET STRING + -- + -- See Mobile station classmark 2 or 3 TS 24.008 + -- + +ConnectedNumber ::= BCDDirectoryNumber + +DataVolume ::= INTEGER + -- + -- The volume of data transferred in segments of 64 octets. + -- + +Day ::= INTEGER -- (1..31) + +--DayClass ::= ObjectInstance + +--DayClasses ::= SET OF DayClass + +--DayDefinition ::= SEQUENCE +--{ +-- day [0] DayOfTheWeek, +-- dayClass [1] ObjectInstance +--} + +--DayDefinitions ::= SET OF DayDefinition + +--DateDefinition ::= SEQUENCE +--{ +-- month [0] Month, +-- day [1] Day, +-- dayClass [2] ObjectInstance +--} + +--DateDefinitions ::= SET OF DateDefinition + +--DayOfTheWeek ::= ENUMERATED +--{ +-- allDays (0), +-- sunday (1), +-- monday (2), +-- tuesday (3), +-- wednesday (4), +-- thursday (5), +-- friday (6), +-- saturday (7) +--} + +DestinationRoutingAddress ::= BCDDirectoryNumber + +DefaultCallHandling ::= ENUMERATED +{ + continueCall (0), + releaseCall (1) +} + -- exception handling: + -- reception of values in range 2-31 shall be treated as "continueCall" + -- reception of values greater than 31 shall be treated as "releaseCall" + +DeferredLocationEventType ::= BIT STRING +-- { +-- msAvailable (0) +-- } (SIZE (1..16)) + + -- exception handling + -- a ProvideSubscriberLocation-Arg containing other values than listed above in + -- DeferredLocationEventType shall be rejected by the receiver with a return error cause of + -- unexpected data value. + +Diagnostics ::= CHOICE +{ + gsm0408Cause [0] INTEGER, + -- See TS 24.008 + gsm0902MapErrorValue [1] INTEGER, + -- Note: The value to be stored here corresponds to + -- the local values defined in the MAP-Errors and + -- MAP-DialogueInformation modules, for full details + -- see TS 29.002. + ccittQ767Cause [2] INTEGER, + -- See ITU-T Q.767 + networkSpecificCause [3] ManagementExtension, + -- To be defined by network operator + manufacturerSpecificCause [4] ManagementExtension + -- To be defined by manufacturer +} + +DefaultSMS-Handling ::= ENUMERATED +{ + continueTransaction (0) , + releaseTransaction (1) +} +-- exception handling: +-- reception of values in range 2-31 shall be treated as "continueTransaction" +-- reception of values greater than 31 shall be treated as "releaseTransaction" + +--Destinations ::= SET OF AE-title + +EmergencyCallIndEnable ::= BOOLEAN + +EmergencyCallIndication ::= SEQUENCE +{ + cellId [0] CellId, + callerId [1] IMSIorIMEI +} + +EParameter ::= INTEGER -- (0..1023) + -- + -- Coded according to TS 22.024 and TS 24.080 + -- + +EquipmentId ::= INTEGER + +Ext-GeographicalInformation ::= OCTET STRING -- (SIZE (1..maxExt-GeographicalInformation)) + -- Refers to geographical Information defined in 3G TS 23.032. + -- This is composed of 1 or more octets with an internal structure according to + -- 3G TS 23.032 + -- Octet 1: Type of shape, only the following shapes in 3G TS 23.032 are allowed: + -- (a) Ellipsoid point with uncertainty circle + -- (b) Ellipsoid point with uncertainty ellipse + -- (c) Ellipsoid point with altitude and uncertainty ellipsoid + -- (d) Ellipsoid Arc + -- (e) Ellipsoid Point + -- Any other value in octet 1 shall be treated as invalid + -- Octets 2 to 8 for case (a) - Ellipsoid point with uncertainty circle + -- Degrees of Latitude 3 octets + -- Degrees of Longitude 3 octets + -- Uncertainty code 1 octet + -- Octets 2 to 11 for case (b) - Ellipsoid point with uncertainty ellipse: + -- Degrees of Latitude 3 octets + -- Degrees of Longitude 3 octets + -- Uncertainty semi-major axis 1 octet + -- Uncertainty semi-minor axis 1 octet + -- Angle of major axis 1 octet + -- Confidence 1 octet + -- Octets 2 to 14 for case (c) - Ellipsoid point with altitude and uncertainty ellipsoid + -- Degrees of Latitude 3 octets + -- Degrees of Longitude 3 octets + -- Altitude 2 octets + -- Uncertainty semi-major axis 1 octet + -- Uncertainty semi-minor axis 1 octet + -- Angle of major axis 1 octet + -- Uncertainty altitude 1 octet + -- Confidence 1 octet + -- Octets 2 to 13 for case (d) - Ellipsoid Arc + -- Degrees of Latitude 3 octets + -- Degrees of Longitude 3 octets + -- Inner radius 2 octets + -- Uncertainty radius 1 octet + -- Offset angle 1 octet + -- Included angle 1 octet + -- Confidence 1 octet + -- Octets 2 to 7 for case (e) - Ellipsoid Point + -- Degrees of Latitude 3 octets + -- Degrees of Longitude 3 octets + -- + -- An Ext-GeographicalInformation parameter comprising more than one octet and + -- containing any other shape or an incorrect number of octets or coding according + -- to 3G TS 23.032 shall be treated as invalid data by a receiver. + -- + -- An Ext-GeographicalInformation parameter comprising one octet shall be discarded + -- by the receiver if an Add-GeographicalInformation parameter is received + -- in the same message. + -- + -- An Ext-GeographicalInformation parameter comprising one octet shall be treated as + -- invalid data by the receiver if an Add-GeographicalInformation parameter is not + -- received in the same message. + +-- maxExt-GeographicalInformation INTEGER ::= 20 + -- the maximum length allows for further shapes in 3G TS 23.032 to be included in later + -- versions of 3G TS 29.002 + +EquipmentType ::= ENUMERATED -- INTEGER +{ + conferenceBridge (0) +} + +FileType ::= ENUMERATED -- INTEGER +{ + callRecords (1), + traceRecords (9), + observedIMEITicket (14) +} + +Fnur ::= ENUMERATED +{ + -- + -- See Bearer Capability TS 24.008 + -- + fnurNotApplicable (0), + fnur9600-BitsPerSecond (1), + fnur14400BitsPerSecond (2), + fnur19200BitsPerSecond (3), + fnur28800BitsPerSecond (4), + fnur38400BitsPerSecond (5), + fnur48000BitsPerSecond (6), + fnur56000BitsPerSecond (7), + fnur64000BitsPerSecond (8), + fnur33600BitsPerSecond (9), + fnur32000BitsPerSecond (10), + fnur31200BitsPerSecond (11) +} + +ForwardToNumber ::= AddressString + +FreeFormatData ::= OCTET STRING -- (SIZE(1..160)) + -- + -- Free formated data as sent in the FCI message + -- See TS 29.078 + -- + +GenericNumber ::= BCDDirectoryNumber + +GenericNumbers ::= SET OF GenericNumber + +Gsm-SCFAddress ::= ISDNAddressString + -- + -- See TS 29.002 + -- + +HLRIntResult ::= Diagnostics + +Horizontal-Accuracy ::= OCTET STRING -- (SIZE (1)) + -- bit 8 = 0 + -- bits 7-1 = 7 bit Uncertainty Code defined in 3G TS 23.032. The horizontal location + -- error should be less than the error indicated by the uncertainty code with 67% + -- confidence. + +HotBillingTag ::= ENUMERATED --INTEGER +{ + noHotBilling (0), + hotBilling (1) +} + +HSCSDParmsChange ::= SEQUENCE +{ + changeTime [0] TimeStamp, + hSCSDChanAllocated [1] NumOfHSCSDChanAllocated, + initiatingParty [2] InitiatingParty OPTIONAL, + aiurRequested [3] AiurRequested OPTIONAL, + chanCodingUsed [4] ChannelCoding, + hSCSDChanRequested [5] NumOfHSCSDChanRequested OPTIONAL +} + + +IMEI ::= TBCD-STRING -- (SIZE (8)) + -- Refers to International Mobile Station Equipment Identity + -- and Software Version Number (SVN) defined in TS GSM 03.03. + -- If the SVN is not present the last octet shall contain the + -- digit 0 and a filler. + -- If present the SVN shall be included in the last octet. + +IMSI ::= TBCD-STRING -- (SIZE (3..8)) + -- digits of MCC, MNC, MSIN are concatenated in this order. + +IMEICheckEvent ::= ENUMERATED -- INTEGER +{ + mobileOriginatedCall (0), + mobileTerminatedCall (1), + smsMobileOriginating (2), + smsMobileTerminating (3), + ssAction (4), + locationUpdate (5) +} + +IMEIStatus ::= ENUMERATED +{ + greyListedMobileEquipment (0), + blackListedMobileEquipment (1), + nonWhiteListedMobileEquipment (2) +} + +IMSIorIMEI ::= CHOICE +{ + imsi [0] IMSI, + imei [1] IMEI +} + +InitiatingParty ::= ENUMERATED +{ + network (0), + subscriber (1) +} + +ISDN-AddressString ::= AddressString -- (SIZE (1..maxISDN-AddressLength)) + -- This type is used to represent ISDN numbers. + +-- maxISDN-AddressLength INTEGER ::= 9 + +LCSCause ::= OCTET STRING -- (SIZE(1)) + -- + -- See LCS Cause Value, 3GPP TS 49.031 + -- + +LCS-Priority ::= OCTET STRING -- (SIZE (1)) + -- 0 = highest priority + -- 1 = normal priority + -- all other values treated as 1 + +LCSClientIdentity ::= SEQUENCE +{ + lcsClientExternalID [0] LCSClientExternalID OPTIONAL, + lcsClientDialedByMS [1] AddressString OPTIONAL, + lcsClientInternalID [2] LCSClientInternalID OPTIONAL +} + +LCSClientExternalID ::= SEQUENCE +{ + externalAddress [0] AddressString OPTIONAL +-- extensionContainer [1] ExtensionContainer OPTIONAL +} + +LCSClientInternalID ::= ENUMERATED +{ + broadcastService (0), + o-andM-HPLMN (1), + o-andM-VPLMN (2), + anonymousLocation (3), + targetMSsubscribedService (4) +} + -- for a CAMEL phase 3 PLMN operator client, the value targetMSsubscribedService shall be used + +LCSClientType ::= ENUMERATED +{ + emergencyServices (0), + valueAddedServices (1), + plmnOperatorServices (2), + lawfulInterceptServices (3) +} + -- exception handling: + -- unrecognized values may be ignored if the LCS client uses the privacy override + -- otherwise, an unrecognized value shall be treated as unexpected data by a receiver + -- a return error shall then be returned if received in a MAP invoke + +LCSQoSInfo ::= SEQUENCE +{ + horizontal-accuracy [0] Horizontal-Accuracy OPTIONAL, + verticalCoordinateRequest [1] NULL OPTIONAL, + vertical-accuracy [2] Vertical-Accuracy OPTIONAL, + responseTime [3] ResponseTime OPTIONAL +} + +LevelOfCAMELService ::= BIT STRING +-- { +-- basic (0), +-- callDurationSupervision (1), +-- onlineCharging (2) +-- } + +LocationAreaAndCell ::= SEQUENCE +{ + locationAreaCode [0] LocationAreaCode, + cellIdentifier [1] CellId +-- +-- For 2G the content of the Cell Identifier is defined by the Cell Id +-- refer TS 24.008 and for 3G by the Service Area Code refer TS 25.413. +-- + +} + +LocationAreaCode ::= OCTET STRING -- (SIZE(2)) + -- + -- See TS 24.008 + -- + +LocationChange ::= SEQUENCE +{ + location [0] LocationAreaAndCell, + changeTime [1] TimeStamp +} + +Location-info ::= SEQUENCE +{ + mscNumber [1] MscNo OPTIONAL, + location-area [2] LocationAreaCode, + cell-identification [3] CellId OPTIONAL +} + +LocationType ::= SEQUENCE +{ +locationEstimateType [0] LocationEstimateType, + deferredLocationEventType [1] DeferredLocationEventType OPTIONAL +} + +LocationEstimateType ::= ENUMERATED +{ + currentLocation (0), + currentOrLastKnownLocation (1), + initialLocation (2), + activateDeferredLocation (3), + cancelDeferredLocation (4) +} + -- exception handling: + -- a ProvideSubscriberLocation-Arg containing an unrecognized LocationEstimateType + -- shall be rejected by the receiver with a return error cause of unexpected data value + +LocUpdResult ::= Diagnostics + +ManagementExtensions ::= SET OF ManagementExtension + +ManagementExtension ::= SEQUENCE +{ + identifier OBJECT IDENTIFIER, + significance [1] BOOLEAN , -- DEFAULT FALSE, + information [2] OCTET STRING +} + + +MCCMNC ::= OCTET STRING -- (SIZE(3)) + -- + -- This type contains the mobile country code (MCC) and the mobile + -- network code (MNC) of a PLMN. + -- + +RateIndication ::= OCTET STRING -- (SIZE(1)) + +--0 no rate adaption +--1 V.110, I.460/X.30 +--2 ITU-T X.31 flag stuffing +--3 V.120 +--7 H.223 & H.245 +--11 PIAFS + + +MessageReference ::= OCTET STRING + +Month ::= INTEGER -- (1..12) + +MOLR-Type ::= INTEGER +--0 locationEstimate +--1 assistanceData +--2 deCipheringKeys + +MSCAddress ::= AddressString + +MscNo ::= ISDN-AddressString + -- + -- See TS 23.003 + -- + +MSISDN ::= ISDN-AddressString + -- + -- See TS 23.003 + -- + +MSPowerClasses ::= SET OF RFPowerCapability + +NetworkCallReference ::= CallReferenceNumber + -- See TS 29.002 + -- + +NetworkSpecificCode ::= INTEGER + -- + -- To be defined by network operator + -- + +NetworkSpecificServices ::= SET OF NetworkSpecificCode + +NotificationToMSUser ::= ENUMERATED +{ + notifyLocationAllowed (0), + notifyAndVerify-LocationAllowedIfNoResponse (1), + notifyAndVerify-LocationNotAllowedIfNoResponse (2), + locationNotAllowed (3) +} + -- exception handling: + -- At reception of any other value than the ones listed the receiver shall ignore + -- NotificationToMSUser. + +NumberOfForwarding ::= INTEGER -- (1..5) + +NumOfHSCSDChanRequested ::= INTEGER + +NumOfHSCSDChanAllocated ::= INTEGER + +ObservedIMEITicketEnable ::= BOOLEAN + +OriginalCalledNumber ::= BCDDirectoryNumber + +OriginDestCombinations ::= SET OF OriginDestCombination + +OriginDestCombination ::= SEQUENCE +{ + origin [0] INTEGER OPTIONAL, + destination [1] INTEGER OPTIONAL + -- + -- Note that these values correspond to the contents + -- of the attributes originId and destinationId + -- respectively. At least one of the two must be present. + -- +} + +PartialRecordTimer ::= INTEGER + +PartialRecordType ::= ENUMERATED +{ + timeLimit (0), + serviceChange (1), + locationChange (2), + classmarkChange (3), + aocParmChange (4), + radioChannelChange (5), + hSCSDParmChange (6), + changeOfCAMELDestination (7), + firstHotBill (20), + severalSSOperationBill (21) +} + +PartialRecordTypes ::= SET OF PartialRecordType + +PositioningData ::= OCTET STRING -- (SIZE(1..33)) + -- + -- See Positioning Data IE (octet 3..n), 3GPP TS 49.031 + -- + +RadioChannelsRequested ::= SET OF RadioChanRequested + +RadioChanRequested ::= ENUMERATED +{ + -- + -- See Bearer Capability TS 24.008 + -- + halfRateChannel (0), + fullRateChannel (1), + dualHalfRatePreferred (2), + dualFullRatePreferred (3) +} + +--RecordClassDestination ::= CHOICE +--{ +-- osApplication [0] AE-title, +-- fileType [1] FileType +--} + +--RecordClassDestinations ::= SET OF RecordClassDestination + +RecordingEntity ::= AddressString + +RecordingMethod ::= ENUMERATED +{ + inCallRecord (0), + inSSRecord (1) +} + +RedirectingNumber ::= BCDDirectoryNumber + +RedirectingCounter ::= INTEGER + +ResponseTime ::= SEQUENCE +{ + responseTimeCategory ResponseTimeCategory +} + -- note: an expandable SEQUENCE simplifies later addition of a numeric response time. + +ResponseTimeCategory ::= ENUMERATED +{ + lowdelay (0), + delaytolerant (1) +} + -- exception handling: + -- an unrecognized value shall be treated the same as value 1 (delaytolerant) + +RFPowerCapability ::= INTEGER + -- + -- This field contains the RF power capability of the Mobile station + -- classmark 1 and 2 of TS 24.008 expressed as an integer. + -- + +RoamingNumber ::= ISDN-AddressString + -- + -- See TS 23.003 + -- + +RoutingNumber ::= CHOICE +{ + roaming [1] RoamingNumber, + forwarded [2] ForwardToNumber +} + +Service ::= CHOICE +{ + teleservice [1] TeleserviceCode, + bearerService [2] BearerServiceCode, + supplementaryService [3] SS-Code, + networkSpecificService [4] NetworkSpecificCode +} + +ServiceDistanceDependencies ::= SET OF ServiceDistanceDependency + +ServiceDistanceDependency ::= SEQUENCE +{ + aocService [0] INTEGER, + chargingZone [1] INTEGER OPTIONAL + -- + -- Note that these values correspond to the contents + -- of the attributes aocServiceId and zoneId + -- respectively. + -- +} + +ServiceKey ::= INTEGER -- (0..2147483647) + +SimpleIntegerName ::= INTEGER + +SimpleStringName ::= GraphicString + +SMSResult ::= Diagnostics + +SmsTpDestinationNumber ::= OCTET STRING + -- + -- This type contains the binary coded decimal representation of + -- the SMS address field the encoding of the octet string is in + -- accordance with the definition of address fields in TS 23.040. + -- This encoding includes type of number and numbering plan indication + -- together with the address value range. + -- + +SpeechVersionIdentifier ::= OCTET STRING -- (SIZE(1)) +-- see GSM 08.08 + +-- 000 0001 GSM speech full rate version 1 +-- 001 0001 GSM speech full rate version 2 used for enhanced full rate +-- 010 0001 GSM speech full rate version 3 for future use +-- 000 0101 GSM speech half rate version 1 +-- 001 0101 GSM speech half rate version 2 for future use +-- 010 0101 GSM speech half rate version 3 for future use + +SSActionResult ::= Diagnostics + +SSActionType ::= ENUMERATED +{ + registration (0), + erasure (1), + activation (2), + deactivation (3), + interrogation (4), + invocation (5), + passwordRegistration (6), + ussdInvocation (7) +} + +-- ussdInvocation (7) include ussd phase 1,phase 2 + +--SS Request = SSActionType + +SS-Code ::= OCTET STRING -- (SIZE (1)) + -- This type is used to represent the code identifying a single + -- supplementary service, a group of supplementary services, or + -- all supplementary services. The services and abbreviations + -- used are defined in TS 3GPP TS 22.004 [5]. The internal structure is + -- defined as follows: + -- + -- bits 87654321: group (bits 8765), and specific service + -- (bits 4321) ussd = ff + +-- allSS (0x00), +-- reserved for possible future use +-- all SS +-- +-- allLineIdentificationSS (0x10), +-- reserved for possible future use +-- all line identification SS +-- +-- calling-line-identification-presentation (0x11), +-- calling line identification presentation +-- calling-line-identification-restriction (0x12), +-- calling line identification restriction +-- connected-line-identification-presentation (0x13), +-- connected line identification presentation +-- connected-line-identification-restriction (0x14), +-- connected line identification restriction +-- malicious-call-identification (0x15), +-- reserved for possible future use +-- malicious call identification +-- +-- allNameIdentificationSS (0x18), +-- all name identification SS +-- calling-name-presentation (0x19), +-- calling name presentation +-- +-- SS-Codes '00011010'B, to '00011111'B, are reserved for future +-- NameIdentification Supplementary Service use. +-- +-- allForwardingSS (0x20), +-- all forwarding SS +-- call-forwarding-unconditional (0x21), +-- call forwarding unconditional +-- call-deflection (0x24), +-- call deflection +-- allCondForwardingSS (0x28), +-- all conditional forwarding SS +-- call-forwarding-on-mobile-subscriber-busy (0x29), +-- call forwarding on mobile subscriber busy +-- call-forwarding-on-no-reply (0x2a), +-- call forwarding on no reply +-- call-forwarding-on-mobile-subscriber-not-reachable (0x2b), +-- call forwarding on mobile subscriber not reachable +-- +-- allCallOfferingSS (0x30), +-- reserved for possible future use +-- all call offering SS includes also all forwarding SS +-- +-- explicit-call-transfer (0x31), +-- explicit call transfer +-- mobile-access-hunting (0x32), +-- reserved for possible future use +-- mobile access hunting +-- +-- allCallCompletionSS (0x40), +-- reserved for possible future use +-- all Call completion SS +-- +-- call-waiting (0x41), +-- call waiting +-- call-hold (0x42), +-- call hold +-- completion-of-call-to-busy-subscribers-originating-side (0x43), +-- completion of call to busy subscribers, originating side +-- completion-of-call-to-busy-subscribers-destination-side (0x44), +-- completion of call to busy subscribers, destination side +-- this SS-Code is used only in InsertSubscriberData and DeleteSubscriberData +-- +-- multicall (0x45), +-- multicall +-- +-- allMultiPartySS (0x50), +-- reserved for possible future use +-- all multiparty SS +-- +-- multiPTY (0x51), +-- multiparty +-- +-- allCommunityOfInterest-SS (0x60), +-- reserved for possible future use +-- all community of interest SS +-- closed-user-group (0x61), +-- closed user group +-- +-- allChargingSS (0x70), +-- reserved for possible future use +-- all charging SS +-- advice-of-charge-information (0x71), +-- advice of charge information +-- advice-of-charge-charging (0x72), +-- advice of charge charging +-- +-- allAdditionalInfoTransferSS (0x80), +-- reserved for possible future use +-- all additional information transfer SS +-- uUS1-user-to-user-signalling (0x81), +-- UUS1 user-to-user signalling +-- uUS2-user-to-user-signalling (0x82), +-- UUS2 user-to-user signalling +-- uUS3-user-to-user-signalling (0x83), +-- UUS3 user-to-user signalling +-- +-- allBarringSS (0x90), +-- all barring SS +-- barringOfOutgoingCalls (0x91), +-- barring of outgoing calls +-- barring-of-all-outgoing-calls (0x92), +-- barring of all outgoing calls +-- barring-of-outgoing-international-calls (0x93), +-- barring of outgoing international calls +-- boicExHC (0x94), +-- barring of outgoing international calls except those directed +-- to the home PLMN +-- barringOfIncomingCalls (0x99), +-- barring of incoming calls +-- barring-of-all-incoming-calls (0x9a), +-- barring of all incoming calls +-- barring-of-incoming-calls-when-roaming-outside-home-PLMN-Country (0x9b), +-- barring of incoming calls when roaming outside home PLMN +-- Country +-- +-- allCallPrioritySS (0xa0), +-- reserved for possible future use +-- all call priority SS +-- enhanced-Multilevel-Precedence-Pre-emption-EMLPP-service (0xa1), +-- enhanced Multilevel Precedence Pre-emption 'EMLPP) service +-- +-- allLCSPrivacyException (0xb0), +-- all LCS Privacy Exception Classes +-- universal (0xb1), +-- allow location by any LCS client +-- callrelated (0xb2), +-- allow location by any value added LCS client to which a call +-- is established from the target MS +-- callunrelated (0xb3), +-- allow location by designated external value added LCS clients +-- plmnoperator (0xb4), +-- allow location by designated PLMN operator LCS clients +-- +-- allMOLR-SS (0xc0), +-- all Mobile Originating Location Request Classes +-- basicSelfLocation (0xc1), +-- allow an MS to request its own location +-- autonomousSelfLocation (0xc2), +-- allow an MS to perform self location without interaction +-- with the PLMN for a predetermined period of time +-- transferToThirdParty (0xc3), +-- allow an MS to request transfer of its location to another LCS client +-- +-- allPLMN-specificSS (0xf0), +-- plmn-specificSS-1 (0xf1), +-- plmn-specificSS-2 (0xf2), +-- plmn-specificSS-3 (0xf3), +-- plmn-specificSS-4 (0xf4), +-- plmn-specificSS-5 (0xf5), +-- plmn-specificSS-6 (0xf6), +-- plmn-specificSS-7 (0xf7), +-- plmn-specificSS-8 (0xf8), +-- plmn-specificSS-9 (0xf9), +-- plmn-specificSS-A (0xfa), +-- plmn-specificSS-B (0xfb), +-- plmn-specificSS-C (0xfc), +-- plmn-specificSS-D (0xfd), +-- plmn-specificSS-E (0xfe), +-- ussd (0xff) + + +SSParameters ::= CHOICE +{ + forwardedToNumber [0] ForwardToNumber, + unstructuredData [1] OCTET STRING +} + +SupplServices ::= SET OF SS-Code + +SuppServiceUsed ::= SEQUENCE +{ + ssCode [0] SS-Code OPTIONAL, + ssTime [1] TimeStamp OPTIONAL +} + +SwitchoverTime ::= SEQUENCE +{ + hour INTEGER , -- (0..23), + minute INTEGER , -- (0..59), + second INTEGER -- (0..59) +} + +SystemType ::= ENUMERATED + -- "unknown" is not to be used in PS domain. +{ + unknown (0), + iuUTRAN (1), + gERAN (2) +} + +TBCD-STRING ::= OCTET STRING + -- This type (Telephony Binary Coded Decimal String) is used to + -- represent several digits from 0 through 9, *, #, a, b, c, two + -- digits per octet, each digit encoded 0000 to 1001 (0 to 9), + -- 1010 (*), 1011 (#), 1100 (a), 1101 (b) or 1110 (c); 1111 used + -- as filler when there is an odd number of digits. + + -- bits 8765 of octet n encoding digit 2n + -- bits 4321 of octet n encoding digit 2(n-1) +1 + +TariffId ::= INTEGER + +TariffPeriod ::= SEQUENCE +{ + switchoverTime [0] SwitchoverTime, + tariffId [1] INTEGER + -- Note that the value of tariffId corresponds + -- to the attribute tariffId. +} + +TariffPeriods ::= SET OF TariffPeriod + +TariffSystemStatus ::= ENUMERATED +{ + available (0), -- available for modification + checked (1), -- "frozen" and checked + standby (2), -- "frozen" awaiting activation + active (3) -- "frozen" and active +} + + +TimeStamp ::= OCTET STRING -- (SIZE(9)) + -- + -- The contents of this field are a compact form of the UTCTime format + -- containing local time plus an offset to universal time. Binary coded + -- decimal encoding is employed for the digits to reduce the storage and + -- transmission overhead + -- e.g. YYMMDDhhmmssShhmm + -- where + -- YY = Year 00 to 99 BCD encoded + -- MM = Month 01 to 12 BCD encoded + -- DD = Day 01 to 31 BCD encoded + -- hh = hour 00 to 23 BCD encoded + -- mm = minute 00 to 59 BCD encoded + -- ss = second 00 to 59 BCD encoded + -- S = Sign 0 = "+", "-" ASCII encoded + -- hh = hour 00 to 23 BCD encoded + -- mm = minute 00 to 59 BCD encoded + -- + +TrafficChannel ::= ENUMERATED +{ + fullRate (0), + halfRate (1) +} + +TranslatedNumber ::= BCDDirectoryNumber + +TransparencyInd ::= ENUMERATED +{ + transparent (0), + nonTransparent (1) +} + +ROUTE ::= CHOICE +{ + rOUTENumber [0] INTEGER, + rOUTEName [1] GraphicString +} + +--rOUTEName 1 10 octet + +TSChangeover ::= SEQUENCE +{ + newActiveTS [0] INTEGER, + newStandbyTS [1] INTEGER, +-- changeoverTime [2] GeneralizedTime OPTIONAL, + authkey [3] OCTET STRING OPTIONAL, + checksum [4] OCTET STRING OPTIONAL, + versionNumber [5] OCTET STRING OPTIONAL + -- Note that if the changeover time is not + -- specified then the change is immediate. +} + +TSCheckError ::= SEQUENCE +{ + errorId [0] TSCheckErrorId + --fail [1] ANY DEFINED BY errorId OPTIONAL +} + +TSCheckErrorId ::= CHOICE +{ + globalForm [0] OBJECT IDENTIFIER, + localForm [1] INTEGER +} + +TSCheckResult ::= CHOICE +{ + success [0] NULL, + fail [1] SET OF TSCheckError +} + +TSCopyTariffSystem ::= SEQUENCE +{ + oldTS [0] INTEGER, + newTS [1] INTEGER +} + +TSNextChange ::= CHOICE +{ + noChangeover [0] NULL, + tsChangeover [1] TSChangeover +} + +TypeOfSubscribers ::= ENUMERATED +{ + home (0), -- HPLMN subscribers + visiting (1), -- roaming subscribers + all (2) +} + +TypeOfTransaction ::= ENUMERATED +{ + successful (0), + unsuccessful (1), + all (2) +} + +Vertical-Accuracy ::= OCTET STRING -- (SIZE (1)) + -- bit 8 = 0 + -- bits 7-1 = 7 bit Vertical Uncertainty Code defined in 3G TS 23.032. + -- The vertical location error should be less than the error indicated + -- by the uncertainty code with 67% confidence. + +ISDNAddressString ::= AddressString + +EmlppPriority ::= OCTET STRING -- (SIZE (1)) + +--priorityLevelA EMLPP-Priority ::= 6 +--priorityLevelB EMLPP-Priority ::= 5 +--priorityLevel0 EMLPP-Priority ::= 0 +--priorityLevel1 EMLPP-Priority ::= 1 +--priorityLevel2 EMLPP-Priority ::= 2 +--priorityLevel3 EMLPP-Priority ::= 3 +--priorityLevel4 EMLPP-Priority ::= 4 +--See 29.002 + + +EASubscriberInfo ::= OCTET STRING -- (SIZE (3)) + -- The internal structure is defined by the Carrier Identification + -- parameter in ANSI T1.113.3. Carrier codes between "000" and "999" may + -- be encoded as 3 digits using "000" to "999" or as 4 digits using + -- "0000" to "0999". Carrier codes between "1000" and "9999" are encoded + -- using 4 digits. + +SelectedCIC ::= OCTET STRING -- (SIZE (3)) + +PortedFlag ::= ENUMERATED +{ + numberNotPorted (0), + numberPorted (1) +} + +SubscriberCategory ::= OCTET STRING -- (SIZE (1)) +-- unknownuser = 0x00, +-- frenchuser = 0x01, +-- englishuser = 0x02, +-- germanuser = 0x03, +-- russianuser = 0x04, +-- spanishuser = 0x05, +-- specialuser = 0x06, +-- reserveuser = 0x09, +-- commonuser = 0x0a, +-- superioruser = 0x0b, +-- datacalluser = 0x0c, +-- testcalluser = 0x0d, +-- spareuser = 0x0e, +-- payphoneuser = 0x0f, +-- coinuser = 0x20, +-- isup224 = 0xe0 + + +CUGOutgoingAccessIndicator ::= ENUMERATED +{ + notCUGCall (0), + cUGCall (1) +} + +CUGInterlockCode ::= OCTET STRING -- (SIZE (4)) + +-- + +CUGOutgoingAccessUsed ::= ENUMERATED +{ + callInTheSameCUGGroup (0), + callNotInTheSameCUGGroup (1) +} + +SMSTEXT ::= OCTET STRING + +MSCCIC ::= INTEGER -- (0..65535) + +RNCorBSCId ::= OCTET STRING -- (SIZE (3)) +--octet order is the same as RANAP/BSSAP signaling +--if spc is coded as 14bit, then OCTET STRING1 will filled with 00 ,for example rnc id = 123 will be coded as 00 01 23 +--OCTET STRING1 +--OCTET STRING2 +--OCTET STRING3 + +MSCId ::= OCTET STRING -- (SIZE (3)) +--National network format , octet order is the same as ISUP signaling +--if spc is coded as 14bit, then OCTET STRING1 will filled with 00,,for example rnc id = 123 will be coded as 00 01 23 +--OCTET STRING1 +--OCTET STRING2 +--OCTET STRING3 + +EmergencyCallFlag ::= ENUMERATED +{ + notEmergencyCall (0), + emergencyCall (1) +} + +CUGIncomingAccessUsed ::= ENUMERATED +{ + callInTheSameCUGGroup (0), + callNotInTheSameCUGGroup (1) +} + +SmsUserDataType ::= OCTET STRING -- (SIZE (1)) +-- +--00 concatenated-short-messages-8-bit-reference-number +--01 special-sms-message-indication +--02 reserved +--03 Value not used to avoid misinterpretation as +--04 characterapplication-port-addressing-scheme-8-bit-address +--05 application-port-addressing-scheme-16-bit-address +--06 smsc-control-parameters +--07 udh-source-indicator +--08 concatenated-short-message-16-bit-reference-number +--09 wireless-control-message-protocol +--0A text-formatting +--0B predefined-sound +--0C user-defined-sound-imelody-max-128-bytes +--0D predefined-animation +--0E large-animation-16-16-times-4-32-4-128-bytes +--0F small-animation-8-8-times-4-8-4-32-bytes +--10 large-picture-32-32-128-bytes +--11 small-picture-16-16-32-bytes +--12 variable-picture +--13 User prompt indicator +--14 Extended Object +--15 Reused Extended Object +--16 Compression Control +--17 Object Distribution Indicator +--18 Standard WVG object +--19 Character Size WVG object +--1A Extended Object Data Request Command +--1B-1F Reserved for future EMS features (see subclause 3.10) +--20 RFC 822 E-Mail Header +--21 Hyperlink format element +--22 Reply Address Element +--23 - 6F Reserved for future use +--70 - 7F (U)SIM Toolkit Security Headers +--80 - 9F SME to SME specific use +--A0 - BF Reserved for future use +--C0 - DF SC specific use +--E0 - FE Reserved for future use +--FF normal SMS + +ConcatenatedSMSReferenceNumber ::= INTEGER -- (0..65535) + +MaximumNumberOfSMSInTheConcatenatedSMS ::= INTEGER -- (0..255) + +SequenceNumberOfTheCurrentSMS ::= INTEGER -- (0..255) + +SequenceNumber ::= INTEGER + +--(1... ) +-- + +DisconnectParty ::= ENUMERATED +{ + callingPartyRelease (0), + calledPartyRelease (1), + networkRelease (2) +} + +ChargedParty ::= ENUMERATED +{ + callingParty (0), + calledParty (1) +} + +ChargeAreaCode ::= OCTET STRING -- (SIZE (1..3)) + +CUGIndex ::= OCTET STRING -- (SIZE (2)) + +GuaranteedBitRate ::= ENUMERATED +{ + gBR14400BitsPerSecond (1), -- BS20 non-transparent + gBR28800BitsPerSecond (2), -- BS20 non-transparent and transparent, + -- BS30 transparent and multimedia + gBR32000BitsPerSecond (3), -- BS30 multimedia + gBR33600BitsPerSecond (4), -- BS30 multimedia + gBR56000BitsPerSecond (5), -- BS30 transparent and multimedia + gBR57600BitsPerSecond (6), -- BS20 non-transparent + gBR64000BitsPerSecond (7), -- BS30 transparent and multimedia + + gBR12200BitsPerSecond (106), -- AMR speech + gBR10200BitsPerSecond (107), -- AMR speech + gBR7950BitsPerSecond (108), -- AMR speech + gBR7400BitsPerSecond (109), -- AMR speech + gBR6700BitsPerSecond (110), -- AMR speech + gBR5900BitsPerSecond (111), -- AMR speech + gBR5150BitsPerSecond (112), -- AMR speech + gBR4750BitsPerSecond (113) -- AMR speech +} + +MaximumBitRate ::= ENUMERATED +{ + mBR14400BitsPerSecond (1), -- BS20 non-transparent + mBR28800BitsPerSecond (2), -- BS20 non-transparent and transparent, + -- BS30 transparent and multimedia + mBR32000BitsPerSecond (3), -- BS30 multimedia + mBR33600BitsPerSecond (4), -- BS30 multimedia + mBR56000BitsPerSecond (5), -- BS30 transparent and multimedia + mBR57600BitsPerSecond (6), -- BS20 non-transparent + mBR64000BitsPerSecond (7), -- BS30 transparent and multimedia + + mBR12200BitsPerSecond (106), -- AMR speech + mBR10200BitsPerSecond (107), -- AMR speech + mBR7950BitsPerSecond (108), -- AMR speech + mBR7400BitsPerSecond (109), -- AMR speech + mBR6700BitsPerSecond (110), -- AMR speech + mBR5900BitsPerSecond (111), -- AMR speech + mBR5150BitsPerSecond (112), -- AMR speech + mBR4750BitsPerSecond (113) -- AMR speech +} + + +HLC ::= OCTET STRING + +-- this parameter is a 1:1 copy of the contents (i.e. starting with octet 3) of the "high layer compatibility" parameter of ITU-T Q.931 [35]. + +LLC ::= OCTET STRING + +-- this parameter is a 1:1 copy of the contents (i.e. starting with octet 3) of the "low layer compatibility" parameter of ITU-T Q.931 [35]. + + +ISDN-BC ::= OCTET STRING + +-- this parameter is a 1:1 copy of the contents (i.e. starting with octet 3) of the "bearer capability" parameter of ITU-T Q.931 [35]. + +ModemType ::= ENUMERATED +{ + none-modem (0), + modem-v21 (1), + modem-v22 (2), + modem-v22-bis (3), + modem-v23 (4), + modem-v26-ter (5), + modem-v32 (6), + modem-undef-interface (7), + modem-autobauding1 (8), + no-other-modem-type (31), + modem-v34 (33) +} + +UssdCodingScheme ::= OCTET STRING + +UssdString ::= OCTET STRING + +UssdNotifyCounter ::= INTEGER -- (0..255) + +UssdRequestCounter ::= INTEGER -- (0..255) + +Classmark3 ::= OCTET STRING -- (SIZE(2)) + +OptimalRoutingDestAddress ::= BCDDirectoryNumber + +GAI ::= OCTET STRING -- (SIZE(7)) +--such as 64 F0 00 00 ABCD 1234 + +ChangeOfglobalAreaID ::= SEQUENCE +{ + location [0] GAI, + changeTime [1] TimeStamp +} + +InteractionWithIP ::= NULL + +RouteAttribute ::= ENUMERATED +{ + cas (0), + tup (1), + isup (2), + pra (3), + bicc (4), + sip (5), + others (255) +} + +VoiceIndicator ::= ENUMERATED +{ + sendToneByLocalMsc (0) , + sendToneByOtherMsc (1), + voiceNoIndication (3) +} + +BCategory ::= ENUMERATED +{ + subscriberFree (0), + subscriberBusy (1), + subscriberNoIndication (3) +} + +CallType ::= ENUMERATED +{ + unknown (0), + internal (1), + incoming (2), + outgoing (3), + tandem (4) +} + +-- END +END +} + +1; + diff --git a/FS/FS/cdr/taqua62.pm b/FS/FS/cdr/taqua62.pm index 862018e9c..aa9463008 100644 --- a/FS/FS/cdr/taqua62.pm +++ b/FS/FS/cdr/taqua62.pm @@ -20,7 +20,9 @@ use FS::cdr qw(_cdr_date_parser_maker); my($cdr, $field, $conf, $hashref) = @_; $hashref->{skiprow} = 1 unless ($field == 0 && $cdr->disposition == 100 ) #regular CDR - || ($field == 1 && $cdr->lastapp eq 'acctcode'); #accountcode + || ($field == 1 && $cdr->lastapp eq 'acctcode') #accountcode + || ($field == 1 && $cdr->lastapp eq 'CallerId') #CID blocking + ; $cdr->cdrtypenum($field); }, diff --git a/FS/FS/cdr/telstra.pm b/FS/FS/cdr/telstra.pm index 9e644dbc8..603d5c40b 100644 --- a/FS/FS/cdr/telstra.pm +++ b/FS/FS/cdr/telstra.pm @@ -19,7 +19,7 @@ my %cdr_type_of = ( %info = ( 'name' => 'Telstra LinxOnline', - 'weight' => 20, + 'weight' => 215, 'header' => 1, 'type' => 'fixedlength', # Wholesale Usage Information Record format diff --git a/FS/FS/cdr/u4.pm b/FS/FS/cdr/u4.pm new file mode 100644 index 000000000..1b7a660e7 --- /dev/null +++ b/FS/FS/cdr/u4.pm @@ -0,0 +1,104 @@ +package FS::cdr::u4; + +use strict; +use vars qw(@ISA %info); +use FS::cdr qw(_cdr_date_parser_maker); + +@ISA = qw(FS::cdr); + +%info = ( + 'name' => 'U4', + 'weight' => 490, + 'type' => 'fixedlength', + 'fixedlength_format' => [qw( + CDRType:3:1:3 + MasterAccountID:12:4:15 + SubAccountID:12:16:27 + BillToNumber:18:28:45 + AccountCode:12:46:57 + CallDateStartTime:14:58:71 + TimeOfDay:1:72:72 + CalculatedSeconds:12:73:84 + City:30:85:114 + State:2:115:116 + Country:40:117:156 + Charges:21:157:177 + CallDirection:1:178:178 + CallIndicator:1:179:179 + ReportIndicator:1:180:180 + ANI:10:181:190 + DNIS:10:191:200 + PIN:16:201:216 + OrigNumber:10:217:226 + TermNumber:10:227:236 + DialedNumber:18:237:254 + DisplayNumber:18:255:272 + RecordSource:1:273:273 + LECInfoDigits:2:274:275 + OrigNPA:4:276:279 + OrigNXX:5:280:284 + OrigLATA:3:285:287 + OrigZone:1:288:288 + OrigCircuit:12:289:300 + OrigTrunkGroupCLLI:12:301:312 + TermNPA:4:313:316 + TermNXX:5:317:321 + TermLATA:3:322:324 + TermZone:1:325:325 + TermCircuit:12:326:337 + TermTrunkGroupCLLI:12:338:349 + TermOCN:5:350:354 + )], + # at least that's how they're defined in the spec we have. + # the real CDRs have several differences. + 'import_fields' => [ + '', #CDRType (for now always 'V') + '', #MasterAccountID + '', #SubAccountID + 'charged_party', #BillToNumber + 'accountcode', #AccountCode + _cdr_date_parser_maker('startdate'), + #CallDateTime + '', #TimeOfDay (always 'S') + sub { #CalculatedSeconds + my($cdr, $sec) = @_; + $cdr->duration($sec); + $cdr->billsec($sec); + }, + '', #City + '', #State + '', #Country + 'upstream_price', #Charges + sub { #CallDirection + my ($cdr, $dir) = @_; + $cdr->set('direction', $dir); + if ( $dir eq 'O' ) { + $cdr->set('src', $cdr->charged_party); + } elsif ( $dir eq 'I' ) { + $cdr->set('dst', $cdr->charged_party); + } + }, + '', #CallIndicator #calltype? + '', #ReportIndicator + sub { #ANI + # it appears that it's the "other" number, not necessarily ANI. + my ($cdr, $number) = @_; + if ( $cdr->direction eq 'O' ) { + $cdr->set('dst', $number); + } elsif ( $cdr->direction eq 'I' ) { + $cdr->set('src', $number); + } + }, + '', #DNIS + '', #PIN + '', #OrigNumber + '', #TermNumber + '', #DialedNumber + '', #DisplayNumber + '', #RecordSource + '', #LECInfoDigits + ('') x 13, + ], +); + +1; diff --git a/FS/FS/cdr_cust_pkg_usage.pm b/FS/FS/cdr_cust_pkg_usage.pm new file mode 100644 index 000000000..6ef7f2dea --- /dev/null +++ b/FS/FS/cdr_cust_pkg_usage.pm @@ -0,0 +1,124 @@ +package FS::cdr_cust_pkg_usage; + +use strict; +use base qw( FS::Record ); +use FS::Record qw( qsearch qsearchs ); + +=head1 NAME + +FS::cdr_cust_pkg_usage - Object methods for cdr_cust_pkg_usage records + +=head1 SYNOPSIS + + use FS::cdr_cust_pkg_usage; + + $record = new FS::cdr_cust_pkg_usage \%hash; + $record = new FS::cdr_cust_pkg_usage { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cdr_cust_pkg_usage object represents an allocation of included +usage minutes to a call. FS::cdr_cust_pkg_usage inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item cdrusagenum - primary key + +=item acctid - foreign key to cdr.acctid + +=item pkgusagenum - foreign key to cust_pkg_usage.pkgusagenum + +=item minutes - the number of minutes allocated + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new example. To add the example 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 { 'cdr_cust_pkg_usage'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=item delete + +Delete this record from the database. + +=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. + +=item check + +Checks all fields to make sure this is a valid example. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('cdrusagenum') + || $self->ut_foreign_key('acctid', 'cdr', 'acctid') + || $self->ut_foreign_key('pkgusagenum', 'cust_pkg_usage', 'pkgusagenum') + || $self->ut_number('minutes') + ; + return $error if $error; + + $self->SUPER::check; +} + +=item cust_pkg_usage + +Returns the L object that this usage allocation came from. + +=item cdr + +Returns the L object that the usage was applied to. + +=cut + +sub cust_pkg_usage { + FS::cust_pkg_usage->by_key($_[0]->pkgusagenum); +} + +sub cdr { + FS::cdr->by_key($_[0]->acctid); +} + +=back + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm index f84af425b..8fcd724a0 100644 --- a/FS/FS/contact.pm +++ b/FS/FS/contact.pm @@ -326,8 +326,8 @@ sub check { || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum') || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum') || $self->ut_foreign_keyn('classnum', 'contact_class', 'classnum') - || $self->ut_textn('last') - || $self->ut_textn('first') + || $self->ut_namen('last') + || $self->ut_namen('first') || $self->ut_textn('title') || $self->ut_textn('comment') || $self->ut_enum('disabled', [ '', 'Y' ]) diff --git a/FS/FS/contact_Mixin.pm b/FS/FS/contact_Mixin.pm new file mode 100644 index 000000000..6e8f315b9 --- /dev/null +++ b/FS/FS/contact_Mixin.pm @@ -0,0 +1,19 @@ +package FS::contact_Mixin; + +use strict; +use FS::Record qw( qsearchs ); +use FS::contact; + +=item contact_obj + +Returns the contact object, if any (see L). + +=cut + +sub contact_obj { + my $self = shift; + return '' unless $self->contactnum; + qsearchs( 'contact', { 'contactnum' => $self->contactnum } ); +} + +1; diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index e7622d712..8b156c642 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1330,6 +1330,8 @@ invoice and all older invoices is greater than the specified amount. I, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required) +I, if specified, is passed to + =cut sub queueable_send { @@ -1354,6 +1356,7 @@ sub send { my( $template, $invoice_from, $notice_name ); my $agentnums = ''; my $balance_over = 0; + my $lpr = ''; if ( ref($_[0]) ) { my $opt = shift; @@ -1364,6 +1367,7 @@ sub send { $invoice_from = $opt->{'invoice_from'}; $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'}; $notice_name = $opt->{'notice_name'}; + $lpr = $opt->{'lpr'} } else { $template = scalar(@_) ? shift : ''; if ( scalar(@_) && $_[0] ) { @@ -1397,10 +1401,12 @@ sub send { if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list ) && ! $self->invoice_noemail; + $opt{'lpr'} = $lpr; #$self->print_invoice(\%opt) $self->print(\%opt) if grep { $_ eq 'POST' } @invoicing_list; #postal + #this has never been used post-$ORIGINAL_ISP afaik $self->fax_invoice(\%opt) if grep { $_ eq 'FAX' } @invoicing_list; #fax @@ -1564,14 +1570,16 @@ sub print { return if $self->hide; my $conf = $self->conf; - my( $template, $notice_name ); + my( $template, $notice_name, $lpr ); if ( ref($_[0]) ) { my $opt = shift; $template = $opt->{'template'} || ''; $notice_name = $opt->{'notice_name'} || 'Invoice'; + $lpr = $opt->{'lpr'} } else { $template = scalar(@_) ? shift : ''; $notice_name = 'Invoice'; + $lpr = ''; } my %opt = ( @@ -1584,7 +1592,11 @@ sub print { $self->batch_invoice(\%opt); } else { - do_print $self->lpr_data(\%opt); + do_print( + $self->lpr_data(\%opt), + 'agentnum' => $self->cust_main->agentnum, + 'lpr' => $lpr, + ); } } @@ -2118,10 +2130,13 @@ sub print_csv { $previous_balance = sprintf('%.2f', $previous_balance); my $totaldue = sprintf('%.2f', $self->owed + $previous_balance); my @items = map { - ($_->{pkgnum} || ''), - $_->{description}, - $_->{amount} - } $self->_items_pkg; + $_->{pkgnum}, + $_->{description}, + $_->{amount} + } + $self->_items_pkg, #_items_nontax? no sections or anything + # with this format + $self->_items_tax; $csv->combine( $cust_main->agentnum, @@ -3122,11 +3137,16 @@ sub _items_payments { #something more elaborate if $_->amount ne ->cust_pay->paid ? + my $desc = $self->mt('Payment received').' '. + time2str($date_format,$_->cust_pay->_date ); + $desc .= $self->mt(' via ' . $_->cust_pay->payby_payinfo_pretty) + if ( $self->conf->exists('invoice_payment_details') ); + push @b, { - 'description' => $self->mt('Payment received').' '. - time2str($date_format,$_->cust_pay->_date ), + 'description' => $desc, 'amount' => sprintf("%.2f", $_->amount ) }; + } @b; diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 716c0983e..d8cbf5915 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -1104,8 +1104,7 @@ sub upgrade_tax_location { delete @hash{qw(censustract censusyear latitude longitude coord_auto)}; $hash{custnum} = $h_cust_main->custnum; - my $tax_loc = qsearchs('cust_location', \%hash) # unlikely - || FS::cust_location->new({ %hash }); + my $tax_loc = FS::cust_location->new_or_existing(\%hash); if ( !$tax_loc->locationnum ) { $tax_loc->disabled('Y'); my $error = $tax_loc->insert; diff --git a/FS/FS/cust_bill_pkg_tax_location.pm b/FS/FS/cust_bill_pkg_tax_location.pm index 723d6e0a3..140982e53 100644 --- a/FS/FS/cust_bill_pkg_tax_location.pm +++ b/FS/FS/cust_bill_pkg_tax_location.pm @@ -215,10 +215,8 @@ sub cust_credit_bill_pkg { sub cust_main_county { my $self = shift; - my $result; - if ( $self->taxtype eq 'FS::cust_main_county' ) { - $result = qsearchs( 'cust_main_county', { 'taxnum' => $self->taxnum } ); - } + return '' unless $self->taxtype eq 'FS::cust_main_county'; + qsearchs( 'cust_main_county', { 'taxnum' => $self->taxnum } ); } sub _upgrade_data { diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 05d961c3f..ba279a26c 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -717,7 +717,7 @@ sub credit_lineitems { my %cust_bill_pkg = (); my %cust_credit_bill_pkg = (); my %taxlisthash = (); - my %unapplied_payments; #invoice numbers, and then billpaynums + my %unapplied_payments = (); #invoice numbers, and then billpaynums foreach my $billpkgnum ( @{$arg{billpkgnums}} ) { my $setuprecur = shift @{$arg{setuprecurs}}; my $amount = shift @{$arg{amounts}}; diff --git a/FS/FS/cust_credit_bill_pkg.pm b/FS/FS/cust_credit_bill_pkg.pm index 7427d09ab..3cb44a092 100644 --- a/FS/FS/cust_credit_bill_pkg.pm +++ b/FS/FS/cust_credit_bill_pkg.pm @@ -348,13 +348,13 @@ sub cust_bill_pkg { sub cust_bill_pkg_tax_Xlocation { my $self = shift; - if ($self->billpkg_tax_locationnum) { + if ($self->billpkgtaxlocationnum) { return qsearchs( 'cust_bill_pkg_tax_location', { 'billpkgtaxlocationnum' => $self->billpkgtaxlocationnum }, ); - } elsif ($self->billpkg_tax_rate_locationnum) { + } elsif ($self->billpkgtaxratelocationnum) { return qsearchs( 'cust_bill_pkg_tax_rate_location', { 'billpkgtaxratelocationnum' => $self->billpkgtaxratelocationnum }, diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm index b86529b3d..b12a161db 100644 --- a/FS/FS/cust_location.pm +++ b/FS/FS/cust_location.pm @@ -5,7 +5,7 @@ use strict; use vars qw( $import ); use Locale::Country; use FS::UID qw( dbh driver_name ); -use FS::Record qw( qsearch ); #qsearchs ); +use FS::Record qw( qsearch qsearchs ); use FS::Conf; use FS::prospect_main; use FS::cust_main; @@ -104,6 +104,35 @@ points to. You can ask the object for a copy with the I method. sub table { 'cust_location'; } +=item new_or_existing HASHREF + +Returns an existing location matching the customer and address fields in +HASHREF, if one exists; otherwise returns a new location containing those +fields. The following fields must match: address1, address2, city, county, +state, zip, country, geocode, disabled. Other fields are only required +to match if they're specified in HASHREF. + +The new location will not be inserted; the calling code must call C +(or a method such as C) to insert it, and check for errors at that +point. + +=cut + +sub new_or_existing { + my $class = shift; + my %hash = ref($_[0]) ? %{$_[0]} : @_; + # if coords are empty, then it doesn't matter if they're auto or not + if ( !$hash{'latitude'} and !$hash{'longitude'} ) { + delete $hash{'coord_auto'}; + } + foreach ( qw(address1 address2 city county state zip country geocode + disabled ) ) { + # empty fields match only empty fields + $hash{$_} = '' if !defined($hash{$_}); + } + return qsearchs('cust_location', \%hash) || $class->new(\%hash); +} + =item insert Adds this record to the database. If there is an error, returns the error, @@ -479,6 +508,20 @@ sub location_label { $prefix . $self->SUPER::location_label(%opt); } +=item county_state_county + +Returns a string consisting of just the county, state and country. + +=cut + +sub county_state_country { + my $self = shift; + my $label = $self->country; + $label = $self->state.", $label" if $self->state; + $label = $self->county." County, $label" if $self->county; + $label; +} + =back =head1 CLASS METHODS diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 45d57cd79..2a4602e19 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -2,7 +2,6 @@ package FS::cust_main; require 5.006; use strict; - #FS::cust_main:_Marketgear when they're ready to move to 2.1 use base qw( FS::cust_main::Packages FS::cust_main::Status FS::cust_main::NationalID FS::cust_main::Billing FS::cust_main::Billing_Realtime @@ -551,14 +550,6 @@ sub insert { } } - if ( $self->can('start_copy_skel') ) { - my $error = $self->start_copy_skel; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - } - warn " ordering packages\n" if $DEBUG > 1; @@ -1787,12 +1778,19 @@ sub check { || $self->ut_floatn('credit_limit') || $self->ut_numbern('billday') || $self->ut_numbern('prorate_day') - || $self->ut_enum('edit_subject', [ '', 'Y' ] ) - || $self->ut_enum('calling_list_exempt', [ '', 'Y' ] ) - || $self->ut_enum('invoice_noemail', [ '', 'Y' ] ) + || $self->ut_flag('edit_subject') + || $self->ut_flag('calling_list_exempt') + || $self->ut_flag('invoice_noemail') + || $self->ut_flag('message_noemail') || $self->ut_enum('locale', [ '', FS::Locales->locales ]) ; + my $company = $self->company; + $company =~ s/^\s+//; + $company =~ s/\s+$//; + $company =~ s/\s+/ /g; + $self->company($company); + #barf. need message catalogs. i18n. etc. $error .= "Please select an advertising source." if $error =~ /^Illegal or empty \(numeric\) refnum: /; @@ -4086,15 +4084,34 @@ sub ship_contact_firstlast { $contact->get('first') . ' '. $contact->get('last'); } -=item country_full +#XXX this doesn't work in 3.x+ +#=item country_full +# +#Returns this customer's full country name +# +#=cut +# +#sub country_full { +# my $self = shift; +# code2country($self->country); +#} + +=item county_state_county [ PREFIX ] -Returns this customer's full country name +Returns a string consisting of just the county, state and country. =cut -sub country_full { +sub county_state_country { my $self = shift; - code2country($self->country); + my $locationnum; + if ( @_ && $_[0] && $self->has_ship_address ) { + $locationnum = $self->ship_locationnum; + } else { + $locationnum = $self->bill_locationnum; + } + my $cust_location = qsearchs('cust_location', { locationnum=>$locationnum }); + $cust_location->county_state_country; } =item geocode DATA_VENDOR @@ -4917,7 +4934,10 @@ sub queueable_print { sub print { my ($self, $template) = (shift, shift); - do_print [ $self->print_ps($template) ]; + do_print( + [ $self->print_ps($template) ], + 'agentnum' => $self->agentnum, + ); } #these three subs should just go away once agent stuff is all config overrides @@ -5059,12 +5079,12 @@ sub process_censustract_update { } #starting to take quite a while for big dbs +# (JRNL: journaled so it only happens once per database) # - seq scan of h_cust_main (yuck), but not going to index paycvv, so -# - seq scan of cust_main on signupdate... index signupdate? will that help? -# - seq scan of cust_main on paydate... index on substrings? maybe set an -# upgrade journal flag now that we have that, yyyy-m-dd paydates are ancient -# - seq scan of cust_main on payinfo.. certainly not going toi ndex that... -# upgrade journal again? this is also an ancient problem +# JRNL seq scan of cust_main on signupdate... index signupdate? will that help? +# JRNL seq scan of cust_main on paydate... index on substrings? maybe set an +# JRNL seq scan of cust_main on payinfo.. certainly not going toi ndex that... +# JRNL leading/trailing spaces in first, last, company # - otaker upgrade? journal and call it good? (double check to make sure # we're not still setting otaker here) # @@ -5119,10 +5139,30 @@ sub _upgrade_data { #class method local($ignore_banned_card) = 1; local($skip_fuzzyfiles) = 1; local($import) = 1; #prevent automatic geocoding (need its own variable?) - $class->_upgrade_otaker(%opts); FS::cust_main::Location->_upgrade_data(%opts); + unless ( FS::upgrade_journal->is_done('cust_main__trimspaces') ) { + + foreach my $cust_main ( qsearch({ + 'table' => 'cust_main', + 'hashref' => {}, + 'extra_sql' => 'WHERE '. + join(' OR ', + map "$_ LIKE ' %' OR $_ LIKE '% ' OR $_ LIKE '% %'", + qw( first last company ) + ), + }) ) { + my $error = $cust_main->replace; + die $error if $error; + } + + FS::upgrade_journal->set_done('cust_main__trimspaces'); + + } + + $class->_upgrade_otaker(%opts); + } =back diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index cd46c7332..939a625c7 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -116,8 +116,13 @@ sub bill_and_collect { $options{'actual_time'} ||= time; my $job = $options{'job'}; + my $actual_time = ( $conf->exists('next-bill-ignore-time') + ? day_end( $options{actual_time} ) + : $options{actual_time} + ); + $job->update_statustext('0,cleaning expired packages') if $job; - $error = $self->cancel_expired_pkgs( day_end( $options{actual_time} ) ); + $error = $self->cancel_expired_pkgs( $actual_time ); if ( $error ) { $error = "Error expiring custnum ". $self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } @@ -125,7 +130,7 @@ sub bill_and_collect { else { warn $error; } } - $error = $self->suspend_adjourned_pkgs( day_end( $options{actual_time} ) ); + $error = $self->suspend_adjourned_pkgs( $actual_time ); if ( $error ) { $error = "Error adjourning custnum ". $self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } @@ -133,7 +138,7 @@ sub bill_and_collect { else { warn $error; } } - $error = $self->unsuspend_resumed_pkgs( day_end( $options{actual_time} ) ); + $error = $self->unsuspend_resumed_pkgs( $actual_time ); if ( $error ) { $error = "Error resuming custnum ".$self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } @@ -410,6 +415,7 @@ sub bill { my @precommit_hooks = (); $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ]; #param checks? + foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) { next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart}; @@ -914,6 +920,11 @@ sub _make_lines { $cust_pkg->pkgpart($part_pkg->pkgpart); + my $cmp_time = ( $conf->exists('next-bill-ignore-time') + ? day_end( $time ) + : $time + ); + ### # bill setup ### @@ -927,7 +938,7 @@ sub _make_lines { and ( $options{'resetup'} || ( ! $cust_pkg->setup && ( ! $cust_pkg->start_date - || $cust_pkg->start_date <= day_end($time) + || $cust_pkg->start_date <= $cmp_time ) && ( ! $conf->exists('disable_setup_suspended_pkgs') || ( $conf->exists('disable_setup_suspended_pkgs') && @@ -975,7 +986,7 @@ sub _make_lines { && ! $cust_pkg->option('no_suspend_bill',1) ) and - ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= day_end($time) ) + ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time ) || ( $part_pkg->plan eq 'voip_cdr' && $part_pkg->option('bill_every_call') ) @@ -999,7 +1010,7 @@ sub _make_lines { #over two params! lets at least switch to a hashref for the rest... my $increment_next_bill = ( $part_pkg->freq ne '0' - && ( $cust_pkg->getfield('bill') || 0 ) <= day_end($time) + && ( $cust_pkg->getfield('bill') || 0 ) <= $cmp_time && !$options{cancel} ); my %param = ( %setup_param, @@ -1027,13 +1038,35 @@ sub _make_lines { if ( $@ ); #base_cancel??? - $unitrecur = $cust_pkg->part_pkg->base_recur || $recur; #XXX uuh + $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better if ( $increment_next_bill ) { - my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0); + my $next_bill; + + if ( my $main_pkg = $cust_pkg->main_pkg ) { + # supplemental package + # to keep in sync with the main package, simulate billing at + # its frequency + my $main_pkg_freq = $main_pkg->part_pkg->freq; + my $supp_pkg_freq = $part_pkg->freq; + my $ratio = $supp_pkg_freq / $main_pkg_freq; + if ( $ratio != int($ratio) ) { + # the UI should prevent setting up packages like this, but just + # in case + return "supplemental package period is not an integer multiple of main package period"; + } + $next_bill = $sdate; + for (1..$ratio) { + $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq ); + } + + } else { + # the normal case + $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0); return "unparsable frequency: ". $part_pkg->freq if $next_bill == -1; + } #pro-rating magic - if $recur_prog fiddled $sdate, want to use that # only for figuring next bill date, nothing else, so, reset $sdate again @@ -1796,8 +1829,9 @@ sub due_cust_event { #??? #my $DEBUG = $opt{'debug'} + $opt{'debug'} ||= 0; # silence some warnings local($DEBUG) = $opt{'debug'} - if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG; + if $opt{'debug'} > $DEBUG; $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG; warn "$me due_cust_event called with options ". diff --git a/FS/FS/cust_main/Location.pm b/FS/FS/cust_main/Location.pm index ba3513b2f..bd0af5348 100644 --- a/FS/FS/cust_main/Location.pm +++ b/FS/FS/cust_main/Location.pm @@ -167,15 +167,29 @@ sub _upgrade_data { $cust_main->set(bill_locationnum => $bill_location->locationnum); if ( $cust_main->get('ship_address1') ) { - my $ship_location = FS::cust_location->new( - { - custnum => $custnum, - map { $_ => $cust_main->get("ship_$_") } location_fields() + # detect duplicates + my $same = 1; + my $ship_location; + foreach (location_fields()) { + if ( length($cust_main->get("ship_$_")) and + $cust_main->get($_) ne $cust_main->get("ship_$_") ) { + $same = 0; } - ); - $error = $ship_location->insert; - die "error migrating service address for customer $custnum: $error" - if $error; + } + + if ( $same ) { + $ship_location = $bill_location; + } else { + $ship_location = FS::cust_location->new( + { + custnum => $custnum, + map { $_ => $cust_main->get("ship_$_") } location_fields() + } + ); + $error = $ship_location->insert; + die "error migrating service address for customer $custnum: $error" + if $error; + } $cust_main->set(ship_locationnum => $ship_location->locationnum); diff --git a/FS/FS/cust_main/Packages.pm b/FS/FS/cust_main/Packages.pm index 395cce7e0..f83bce915 100644 --- a/FS/FS/cust_main/Packages.pm +++ b/FS/FS/cust_main/Packages.pm @@ -29,6 +29,9 @@ These methods are available on FS::cust_main objects; Orders a single package. +Note that if the package definition has supplemental packages, those will +be ordered as well. + Options may be passed as a list of key/value pairs or as a hash reference. Options are: @@ -84,7 +87,7 @@ sub order_pkg { if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'}; my %insert_params = map { $opt->{$_} ? ( $_ => $opt->{$_} ) : () } - qw( ticket_subject ticket_queue ); + qw( ticket_subject ticket_queue allow_pkgpart ); local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -97,17 +100,48 @@ sub order_pkg { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - if ( $opt->{'cust_location'} && - ( ! $cust_pkg->locationnum || $cust_pkg->locationnum == -1 ) ) { - my $error = $opt->{'cust_location'}->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "inserting cust_location (transaction rolled back): $error"; + if ( $opt->{'contactnum'} and $opt->{'contactnum'} != -1 ) { + + $cust_pkg->contactnum($opt->{'contactnum'}); + + } elsif ( $opt->{'contact'} ) { + + if ( ! $opt->{'contact'}->contactnum ) { + # not inserted yet + my $error = $opt->{'contact'}->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "inserting contact (transaction rolled back): $error"; + } } - $cust_pkg->locationnum($opt->{'cust_location'}->locationnum); + $cust_pkg->contactnum($opt->{'contact'}->contactnum); + + #} else { + # + # $cust_pkg->contactnum(); + } - else { + + if ( $opt->{'locationnum'} and $opt->{'locationnum'} != -1 ) { + + $cust_pkg->locationnum($opt->{'locationnum'}); + + } elsif ( $opt->{'cust_location'} ) { + + if ( ! $opt->{'cust_location'}->locationnum ) { + # not inserted yet + my $error = $opt->{'cust_location'}->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "inserting cust_location (transaction rolled back): $error"; + } + } + $cust_pkg->locationnum($opt->{'cust_location'}->locationnum); + + } else { + $cust_pkg->locationnum($self->ship_locationnum); + } $cust_pkg->custnum( $self->custnum ); @@ -141,6 +175,35 @@ sub order_pkg { } } + # add supplemental packages, if any are needed + my $part_pkg = FS::part_pkg->by_key($cust_pkg->pkgpart); + foreach my $link ($part_pkg->supp_part_pkg_link) { + #warn "inserting supplemental package ".$link->dst_pkgpart; + my $pkg = FS::cust_pkg->new({ + 'pkgpart' => $link->dst_pkgpart, + 'pkglinknum' => $link->pkglinknum, + 'custnum' => $self->custnum, + 'main_pkgnum' => $cust_pkg->pkgnum, + 'locationnum' => $cust_pkg->locationnum, + # try to prevent as many surprises as possible + 'pkgbatch' => $cust_pkg->pkgbatch, + 'start_date' => $cust_pkg->start_date, + 'order_date' => $cust_pkg->order_date, + 'expire' => $cust_pkg->expire, + 'adjourn' => $cust_pkg->adjourn, + 'contract_end' => $cust_pkg->contract_end, + 'refnum' => $cust_pkg->refnum, + 'discountnum' => $cust_pkg->discountnum, + 'waive_setup' => $cust_pkg->waive_setup, + 'allow_pkgpart' => $opt->{'allow_pkgpart'}, + }); + $error = $self->order_pkg('cust_pkg' => $pkg); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "inserting supplemental package: $error"; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no error diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index 1047890c3..7dbb7a859 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -18,7 +18,8 @@ use FS::svc_acct; $DEBUG = 0; $me = '[FS::cust_main::Search]'; -@fuzzyfields = ( 'first', 'last', 'company', 'address1' ); +@fuzzyfields = ( 'cust_main.first', 'cust_main.last', 'cust_main.company', + 'cust_location.address1' ); install_callback FS::UID sub { $conf = new FS::Conf; @@ -339,7 +340,7 @@ sub smart_search { my %fuzopts = ( 'hashref' => \%options, 'select' => '', - 'extra_sql' => " AND $agentnums_sql", #agent virtualization + 'extra_sql' => "WHERE $agentnums_sql", #agent virtualization ); if ( $first && $last ) { @@ -355,7 +356,8 @@ sub smart_search { } if ( $conf->exists('address1-search') ) { push @cust_main, - FS::cust_main::Search->fuzzy_search( { 'address1' => $value }, %fuzopts ); + FS::cust_main::Search->fuzzy_search( + { 'cust_location.address1' => $value }, %fuzopts ); } } @@ -644,6 +646,16 @@ sub search { if $params->{'with_email'}; ## + # "with postal mail invoices" checkbox + ## + + push @where, + "EXISTS ( SELECT 1 FROM cust_main_invoice + WHERE cust_main_invoice.custnum = cust_main.custnum + AND dest = 'POST' )" + if $params->{'POST'}; + + ## # "without postal mail invoices" checkbox ## @@ -792,11 +804,19 @@ sub search { @tagnums = grep /^(\d+)$/, @tagnums; if ( @tagnums ) { + if ( $params->{'all_tags'} ) { + foreach ( @tagnums ) { + push @where, 'exists(select 1 from cust_tag where '. + 'cust_tag.custnum = cust_main.custnum and tagnum = '. + $_ . ')'; + } + } else { # matching any tag, not all my $tags_where = "0 < (select count(1) from cust_tag where " . " cust_tag.custnum = cust_main.custnum and tagnum in (" . join(',', @tagnums) . "))"; push @where, $tags_where; + } } } @@ -814,6 +834,12 @@ sub search { my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : ''; my $addl_from = ''; + # always make address fields available in results + for my $pre ('bill_', 'ship_') { + $addl_from .= + 'LEFT JOIN cust_location AS '.$pre.'location '. + 'ON (cust_main.'.$pre.'locationnum = '.$pre.'location.locationnum) '; + } my $count_query = "SELECT COUNT(*) FROM cust_main $extra_sql"; @@ -831,7 +857,8 @@ sub search { if ($params->{'flattened_pkgs'}) { #my $pkg_join = ''; - $addl_from .= ' LEFT JOIN cust_pkg USING ( custnum ) '; + $addl_from .= + ' LEFT JOIN cust_pkg ON ( cust_main.custnum = cust_pkg.custnum ) '; if ($dbh->{Driver}->{Name} eq 'Pg') { @@ -914,7 +941,8 @@ Additional options are the same as FS::Record::qsearch =cut sub fuzzy_search { - my( $self, $fuzzy ) = @_; + my $self = shift; + my $fuzzy = shift; # sensible defaults, then merge in any passed options my %fuzopts = ( 'table' => 'cust_main', @@ -926,6 +954,11 @@ sub fuzzy_search { my @cust_main = (); + my @fuzzy_mod = 'i'; + my $conf = new FS::Conf; + my $fuzziness = $conf->config('fuzzy-fuzziness'); + push @fuzzy_mod, $fuzziness if $fuzziness; + check_and_rebuild_fuzzyfiles(); foreach my $field ( keys %$fuzzy ) { @@ -933,32 +966,31 @@ sub fuzzy_search { next unless scalar(@$all); my %match = (); - $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) ); - - my @fcust = (); - foreach ( keys %match ) { - if ( $field eq 'address1' ) { - #because it lives outside the table - my $addl_from = $fuzopts{addl_from} . - 'JOIN cust_location USING (custnum)'; - my $extra_sql = $fuzopts{extra_sql} . - " AND cust_location.address1 = ".dbh->quote($_); - push @fcust, qsearch({ - %fuzopts, - 'addl_from' => $addl_from, - 'extra_sql' => $extra_sql, - }); - } else { - my $hash = $fuzopts{hashref}; - $hash->{$field} = $_; - push @fcust, qsearch({ - %fuzopts, - 'hashref' => $hash - }); - } + $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, \@fuzzy_mod, @$all ) ); + next if !keys(%match); + + my $in_matches = 'IN (' . + join(',', map { dbh->quote($_) } keys %match) . + ')'; + + my $extra_sql = $fuzopts{extra_sql}; + if ($extra_sql =~ /^\s*where /i or keys %{ $fuzopts{hashref} }) { + $extra_sql .= ' AND '; + } else { + $extra_sql .= 'WHERE '; + } + $extra_sql .= "$field $in_matches"; + + my $addl_from = $fuzopts{addl_from}; + if ( $field =~ /^cust_location/ ) { + $addl_from .= ' JOIN cust_location USING (custnum)'; } - my %fsaw = (); - push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust; + + push @cust_main, qsearch({ + %fuzopts, + 'addl_from' => $addl_from, + 'extra_sql' => $extra_sql, + }); } # we want the components of $fuzzy ANDed, not ORed, but still don't want dupes @@ -997,28 +1029,29 @@ sub rebuild_fuzzyfiles { foreach my $fuzzy ( @fuzzyfields ) { - open(LOCK,">>$dir/cust_main.$fuzzy") - or die "can't open $dir/cust_main.$fuzzy: $!"; - flock(LOCK,LOCK_EX) - or die "can't lock $dir/cust_main.$fuzzy: $!"; + my ($field, $table) = reverse split('\.', $fuzzy); + $table ||= 'cust_main'; - open (CACHE, '>:encoding(UTF-8)', "$dir/cust_main.$fuzzy.tmp") - or die "can't open $dir/cust_main.$fuzzy.tmp: $!"; + open(LOCK,">>$dir/$table.$field") + or die "can't open $dir/$table.$field: $!"; + flock(LOCK,LOCK_EX) + or die "can't lock $dir/$table.$field: $!"; - foreach my $field ( $fuzzy, "ship_$fuzzy" ) { - my $sth = dbh->prepare("SELECT $field FROM cust_main". - " WHERE $field != '' AND $field IS NOT NULL"); - $sth->execute or die $sth->errstr; + open (CACHE, '>:encoding(UTF-8)', "$dir/$table.$field.tmp") + or die "can't open $dir/$table.$field.tmp: $!"; - while ( my $row = $sth->fetchrow_arrayref ) { - print CACHE $row->[0]. "\n"; - } + my $sth = dbh->prepare( + "SELECT $field FROM $table WHERE $field IS NOT NULL AND $field != ''" + ); + $sth->execute or die $sth->errstr; - } + while ( my $row = $sth->fetchrow_arrayref ) { + print CACHE $row->[0]. "\n"; + } - close CACHE or die "can't close $dir/cust_main.$fuzzy.tmp: $!"; + close CACHE or die "can't close $dir/$table.$field.tmp: $!"; - rename "$dir/cust_main.$fuzzy.tmp", "$dir/cust_main.$fuzzy"; + rename "$dir/$table.$field.tmp", "$dir/$table.$field"; close LOCK; } @@ -1037,20 +1070,24 @@ sub append_fuzzyfiles { my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; - foreach my $field (@fuzzyfields) { + foreach my $fuzzy (@fuzzyfields) { + + my ($field, $table) = reverse split('\.', $fuzzy); + $table ||= 'cust_main'; + my $value = shift; if ( $value ) { - open(CACHE, '>>:encoding(UTF-8)', "$dir/cust_main.$field" ) - or die "can't open $dir/cust_main.$field: $!"; + open(CACHE, '>>:encoding(UTF-8)', "$dir/$table.$field" ) + or die "can't open $dir/$table.$field: $!"; flock(CACHE,LOCK_EX) - or die "can't lock $dir/cust_main.$field: $!"; + or die "can't lock $dir/$table.$field: $!"; print CACHE "$value\n"; flock(CACHE,LOCK_UN) - or die "can't unlock $dir/cust_main.$field: $!"; + or die "can't unlock $dir/$table.$field: $!"; close CACHE; } @@ -1064,10 +1101,13 @@ sub append_fuzzyfiles { =cut sub all_X { - my( $self, $field ) = @_; + my( $self, $fuzzy ) = @_; + my ($field, $table) = reverse split('\.', $fuzzy); + $table ||= 'cust_main'; + my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; - open(CACHE, '<:encoding(UTF-8)', "$dir/cust_main.$field") - or die "can't open $dir/cust_main.$field: $!"; + open(CACHE, '<:encoding(UTF-8)', "$dir/$table.$field") + or die "can't open $dir/$table.$field: $!"; my @array = map { chomp; $_; } ; close CACHE; \@array; diff --git a/FS/FS/cust_main/_Marketgear.pm b/FS/FS/cust_main/_Marketgear.pm deleted file mode 100644 index 2d3c9270e..000000000 --- a/FS/FS/cust_main/_Marketgear.pm +++ /dev/null @@ -1,146 +0,0 @@ -package FS::cust_main::_Marketgear; - -use strict; -use vars qw( $DEBUG $me $conf ); - -$DEBUG = 0; -$me = '[FS::cust_main::_Marketgear]'; - -install_callback FS::UID sub { - $conf = new FS::Conf; -}; - -sub start_copy_skel { - my $self = shift; - - return '' unless $conf->config('cust_main-skeleton_tables') - && $conf->config('cust_main-skeleton_custnum'); - - warn " inserting skeleton records\n" - if $DEBUG > 1 || $cust_main::DEBUG > 1; - - #'mg_user_preference' => {}, - #'mg_user_indicator_profile.user_indicator_profile_id' => { 'mg_profile_indicator.profile_indicator_id' => { 'mg_profile_details.profile_detail_id' }, }, - #'mg_watchlist_header.watchlist_header_id' => { 'mg_watchlist_details.watchlist_details_id' }, - #'mg_user_grid_header.grid_header_id' => { 'mg_user_grid_details.user_grid_details_id' }, - #'mg_portfolio_header.portfolio_header_id' => { 'mg_portfolio_trades.portfolio_trades_id' => { 'mg_portfolio_trades_positions.portfolio_trades_positions_id' } }, - my @tables = eval(join('\n',$conf->config('cust_main-skeleton_tables'))); - die $@ if $@; - - _copy_skel( 'cust_main', #tablename - $conf->config('cust_main-skeleton_custnum'), #sourceid - $self->custnum, #destid - @tables, #child tables - ); -} - -#recursive subroutine, not a method -sub _copy_skel { - my( $table, $sourceid, $destid, %child_tables ) = @_; - - my $primary_key; - if ( $table =~ /^(\w+)\.(\w+)$/ ) { - ( $table, $primary_key ) = ( $1, $2 ); - } else { - my $dbdef_table = dbdef->table($table); - $primary_key = $dbdef_table->primary_key - or return "$table has no primary key". - " (or do you need to run dbdef-create?)"; - } - - warn " _copy_skel: $table.$primary_key $sourceid to $destid for ". - join (', ', keys %child_tables). "\n" - if $DEBUG > 2; - - foreach my $child_table_def ( keys %child_tables ) { - - my $child_table; - my $child_pkey = ''; - if ( $child_table_def =~ /^(\w+)\.(\w+)$/ ) { - ( $child_table, $child_pkey ) = ( $1, $2 ); - } else { - $child_table = $child_table_def; - - $child_pkey = dbdef->table($child_table)->primary_key; - # or return "$table has no primary key". - # " (or do you need to run dbdef-create?)\n"; - } - - my $sequence = ''; - if ( keys %{ $child_tables{$child_table_def} } ) { - - return "$child_table has no primary key". - " (run dbdef-create or try specifying it?)\n" - unless $child_pkey; - - #false laziness w/Record::insert and only works on Pg - #refactor the proper last-inserted-id stuff out of Record::insert if this - # ever gets use for anything besides a quick kludge for one customer - my $default = dbdef->table($child_table)->column($child_pkey)->default; - $default =~ /^nextval\(\(?'"?([\w\.]+)"?'/i - or return "can't parse $child_table.$child_pkey default value ". - " for sequence name: $default"; - $sequence = $1; - - } - - my @sel_columns = grep { $_ ne $primary_key } - dbdef->table($child_table)->columns; - my $sel_columns = join(', ', @sel_columns ); - - my @ins_columns = grep { $_ ne $child_pkey } @sel_columns; - my $ins_columns = ' ( '. join(', ', $primary_key, @ins_columns ). ' ) '; - my $placeholders = ' ( ?, '. join(', ', map '?', @ins_columns ). ' ) '; - - my $sel_st = "SELECT $sel_columns FROM $child_table". - " WHERE $primary_key = $sourceid"; - warn " $sel_st\n" - if $DEBUG > 2; - my $sel_sth = dbh->prepare( $sel_st ) - or return dbh->errstr; - - $sel_sth->execute or return $sel_sth->errstr; - - while ( my $row = $sel_sth->fetchrow_hashref ) { - - warn " selected row: ". - join(', ', map { "$_=".$row->{$_} } keys %$row ). "\n" - if $DEBUG > 2; - - my $statement = - "INSERT INTO $child_table $ins_columns VALUES $placeholders"; - my $ins_sth =dbh->prepare($statement) - or return dbh->errstr; - my @param = ( $destid, map $row->{$_}, @ins_columns ); - warn " $statement: [ ". join(', ', @param). " ]\n" - if $DEBUG > 2; - $ins_sth->execute( @param ) - or return $ins_sth->errstr; - - #next unless keys %{ $child_tables{$child_table} }; - next unless $sequence; - - #another section of that laziness - my $seq_sql = "SELECT currval('$sequence')"; - my $seq_sth = dbh->prepare($seq_sql) or return dbh->errstr; - $seq_sth->execute or return $seq_sth->errstr; - my $insertid = $seq_sth->fetchrow_arrayref->[0]; - - # don't drink soap! recurse! recurse! okay! - my $error = - _copy_skel( $child_table_def, - $row->{$child_pkey}, #sourceid - $insertid, #destid - %{ $child_tables{$child_table_def} }, - ); - return $error if $error; - - } - - } - - return ''; - -} - -1; diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm index 573359571..a61d67e11 100644 --- a/FS/FS/cust_main_county.pm +++ b/FS/FS/cust_main_county.pm @@ -137,33 +137,6 @@ sub check { } -sub taxname { - my $self = shift; - if ( $self->dbdef_table->column('taxname') ) { - return $self->setfield('taxname', $_[0]) if @_; - return $self->getfield('taxname'); - } - return ''; -} - -sub setuptax { - my $self = shift; - if ( $self->dbdef_table->column('setuptax') ) { - return $self->setfield('setuptax', $_[0]) if @_; - return $self->getfield('setuptax'); - } - return ''; -} - -sub recurtax { - my $self = shift; - if ( $self->dbdef_table->column('recurtax') ) { - return $self->setfield('recurtax', $_[0]) if @_; - return $self->getfield('recurtax'); - } - return ''; -} - =item label OPTIONS Returns a label looking like "Anytown, Alameda County, CA, US". @@ -174,13 +147,10 @@ If the taxname field is set, it will look like If the taxclass is set, then it will be "Anytown, Alameda County, CA, US (International)". -Currently it will not contain the district, even if the city+county+state -is not unique. - -OPTIONS may contain "no_taxclass" (hides taxclass) and/or "no_city" -(hides city). It may also contain "out", in which case, if this -region (district+city+county+state+country) contains no non-zero -taxes, the label will read "Out of taxable region(s)". +OPTIONS may contain "with_taxclass", "with_city", and "with_district" to show +those fields. It may also contain "out", in which case, if this region +(district+city+county+state+country) contains no non-zero taxes, the label +will read "Out of taxable region(s)". =cut @@ -202,12 +172,15 @@ sub label { my $label = $self->country; $label = $self->state.", $label" if $self->state; $label = $self->county." County, $label" if $self->county; - if (!$opt{no_city}) { + if ($opt{with_city}) { $label = $self->city.", $label" if $self->city; + if ($opt{with_district} and $self->district) { + $label = $self->district . ", $label"; + } } # ugly labels when taxclass and taxname are both non-null... # but this is how the tax report does it - if (!$opt{no_taxclass}) { + if ($opt{with_taxclass}) { $label = "$label (".$self->taxclass.')' if $self->taxclass; } $label = $self->taxname." ($label)" if $self->taxname; @@ -512,8 +485,10 @@ sub taxline { # now round and distribute my $extra_cents = sprintf('%.2f', $taxable_cents * $self->tax / 100) * 100 - $tax_cents; + # make sure we have an integer + $extra_cents = sprintf('%.0f', $extra_cents); if ( $extra_cents < 0 ) { - die "nonsense extra_cents value $extra_cents"; # because seriously, wtf + die "nonsense extra_cents value $extra_cents"; } $tax_cents += $extra_cents; my $i = 0; diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index 4535aadb2..0e9e8a716 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -1032,18 +1032,48 @@ sub _upgrade_data { #class method ### # not only cust_pay, but also voided and refunded payments - if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch')) { + if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) { + local $FS::Record::nowarn_classload=1; # really inefficient, but again, only has to run once foreach my $table (qw(cust_pay cust_pay_void cust_refund)) { + my $and_batchnum_is_null = + ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' ); foreach my $object ( qsearch({ table => $table, extra_sql => "WHERE payby IN('CARD','CHEK') ". - "AND paybatch IS NOT NULL", + "AND (paybatch IS NOT NULL ". + "OR (paybatch IS NULL AND auth IS NULL + $and_batchnum_is_null ) )", }) ) { + if ( $object->paybatch eq '' ) { + # repair for a previous upgrade that didn't save 'auth' + my $pkey = $object->primary_key; + # find the last history record that had a paybatch value + my $h = qsearchs({ + table => "h_$table", + hashref => { + $pkey => $object->$pkey, + paybatch => { op=>'!=', value=>''}, + history_action => 'replace_old', + }, + order_by => 'ORDER BY history_date DESC LIMIT 1', + }); + if (!$h) { + warn "couldn't find paybatch history record for $table ".$object->$pkey."\n"; + next; + } + # if the paybatch didn't have an auth string, then it's fine + $h->paybatch =~ /:(\w+):/ or next; + # set paybatch to what it was in that record + $object->set('paybatch', $h->paybatch) + # and then upgrade it like the old records + } + my $parsed = $object->_parse_paybatch; if (keys %$parsed) { $object->set($_ => $parsed->{$_}) foreach keys %$parsed; + $object->set('auth' => $parsed->{authorization}); $object->set('paybatch', ''); my $error = $object->replace; warn "error parsing CARD/CHEK paybatch fields on $object #". @@ -1052,7 +1082,7 @@ sub _upgrade_data { #class method } } #$object } #$table - FS::upgrade_journal->set_done('cust_pay__parse_paybatch'); + FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1'); } } diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm index 9f2e9ddfc..e1e32d3d4 100644 --- a/FS/FS/cust_pay_batch.pm +++ b/FS/FS/cust_pay_batch.pm @@ -9,7 +9,7 @@ use FS::payinfo_Mixin; use FS::cust_main; use FS::cust_bill; -@ISA = qw( FS::payinfo_Mixin FS::Record ); +@ISA = qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record ); # 1 is mostly method/subroutine entry and options # 2 traces progress of some operations @@ -80,7 +80,9 @@ following fields are currently supported: =item country -=item status +=item status - 'Approved' or 'Declined' + +=item error_message - the error returned by the gateway if any =back @@ -289,19 +291,21 @@ sub retriable { ''; } -=item approve PAYBATCH +=item approve OPTIONS Approve this payment. This will replace the existing record with the same paybatchnum, set its status to 'Approved', and generate a payment record (L). This should only be called from the batch import process. +OPTIONS may contain "gatewaynum", "processor", "auth", and "order_number". + =cut sub approve { # to break up the Big Wall of Code that is import_results my $new = shift; - my $paybatch = shift; + my %opt = @_; my $paybatchnum = $new->paybatchnum; my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum }) or return "paybatchnum $paybatchnum not found"; @@ -317,13 +321,17 @@ sub approve { my $cust_pay = new FS::cust_pay ( { 'custnum' => $new->custnum, 'payby' => $new->payby, - 'paybatch' => $paybatch, 'payinfo' => $new->payinfo || $old->payinfo, 'paid' => $new->paid, '_date' => $new->_date, 'usernum' => $new->usernum, 'batchnum' => $new->batchnum, + 'gatewaynum' => $opt{'gatewaynum'}, + 'processor' => $opt{'processor'}, + 'auth' => $opt{'auth'}, + 'order_number' => $opt{'order_number'} } ); + $error = $cust_pay->insert; if ( $error ) { return "error inserting payment for paybatchnum $paybatchnum: $error\n"; @@ -361,6 +369,12 @@ sub decline { # Void the payment my $cust_pay = qsearchs('cust_pay', { custnum => $new->custnum, + batchnum => $new->batchnum + }); + # these should all be migrated over, but if it's not found, look for + # batchnum in the 'paybatch' field also + $cust_pay ||= qsearchs('cust_pay', { + custnum => $new->custnum, paybatch => $new->batchnum }); if ( !$cust_pay ) { @@ -375,6 +389,7 @@ sub decline { } } # !$old->status $new->status('Declined'); + $new->error_message($reason); my $error = $new->replace($old); if ( $error ) { return "error updating status of paybatchnum $paybatchnum: $error\n"; diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 22a7b2c03..741d440fa 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -1,12 +1,13 @@ package FS::cust_pkg; use strict; -use base qw( FS::otaker_Mixin FS::cust_main_Mixin FS::location_Mixin +use base qw( FS::otaker_Mixin FS::cust_main_Mixin + FS::contact_Mixin FS::location_Mixin FS::m2m_Common FS::option_Common ); use vars qw($disable_agentcheck $DEBUG $me); use Carp qw(cluck); use Scalar::Util qw( blessed ); -use List::Util qw(max); +use List::Util qw(min max); use Tie::IxHash; use Time::Local qw( timelocal timelocal_nocheck ); use MIME::Entity; @@ -17,10 +18,13 @@ use FS::CurrentUser; use FS::cust_svc; use FS::part_pkg; use FS::cust_main; +use FS::contact; use FS::cust_location; use FS::pkg_svc; use FS::cust_bill_pkg; use FS::cust_pkg_detail; +use FS::cust_pkg_usage; +use FS::cdr_cust_pkg_usage; use FS::cust_event; use FS::h_cust_svc; use FS::reg_code; @@ -197,6 +201,15 @@ Previous locationnum =item waive_setup +=item main_pkgnum + +The pkgnum of the package that this package is supplemental to, if any. + +=item pkglinknum + +The package link (L) that defines this supplemental +package, if it is one. + =back Note: setup, last_bill, bill, adjourn, susp, expire, cancel and change_date @@ -214,7 +227,7 @@ Create a new billing item. To add the item to the database, see L<"insert">. =cut sub table { 'cust_pkg'; } -sub cust_linked { $_[0]->cust_main_custnum; } +sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum } sub cust_unlinked_msg { my $self = shift; "WARNING: can't find cust_main.custnum ". $self->custnum. @@ -256,6 +269,12 @@ a ticket will be added to this customer with this subject an optional queue name for ticket additions +=item allow_pkgpart + +Don't check the legality of the package definition. This should be used +when performing a package change that doesn't change the pkgpart (i.e. +a location change). + =back =cut @@ -263,7 +282,8 @@ an optional queue name for ticket additions sub insert { my( $self, %options ) = @_; - my $error = $self->check_pkgpart; + my $error; + $error = $self->check_pkgpart unless $options{'allow_pkgpart'}; return $error if $error; my $part_pkg = $self->part_pkg; @@ -594,13 +614,15 @@ replace methods. sub check { my $self = shift; - $self->locationnum('') if !$self->locationnum || $self->locationnum == -1; + if ( !$self->locationnum or $self->locationnum == -1 ) { + $self->set('locationnum', $self->cust_main->ship_locationnum); + } my $error = $self->ut_numbern('pkgnum') || $self->ut_foreign_key('custnum', 'cust_main', 'custnum') || $self->ut_numbern('pkgpart') - || $self->check_pkgpart + || $self->ut_foreign_keyn('contactnum', 'contact', 'contactnum' ) || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum') || $self->ut_numbern('start_date') || $self->ut_numbern('setup') @@ -616,6 +638,8 @@ sub check { || $self->ut_numbern('agent_pkgid') || $self->ut_enum('recur_show_zero', [ '', 'Y', 'N', ]) || $self->ut_enum('setup_show_zero', [ '', 'Y', 'N', ]) + || $self->ut_foreign_keyn('main_pkgnum', 'cust_pkg', 'pkgnum') + || $self->ut_foreign_keyn('pkglinknum', 'part_pkg_link', 'pkglinknum') ; return $error if $error; @@ -639,14 +663,19 @@ sub check { =item check_pkgpart +Check the pkgpart to make sure it's allowed with the reg_code and/or +promo_code of the package (if present) and with the customer's agent. +Called from C, unless we are doing a package change that doesn't +affect pkgpart. + =cut sub check_pkgpart { my $self = shift; - my $error = $self->ut_numbern('pkgpart'); - return $error if $error; + # my $error = $self->ut_numbern('pkgpart'); # already done + my $error; if ( $self->reg_code ) { unless ( grep { $self->pkgpart == $_->pkgpart } @@ -730,6 +759,11 @@ sub cancel { my( $self, %options ) = @_; my $error; + # pass all suspend/cancel actions to the main package + if ( $self->main_pkgnum and !$options{'from_main'} ) { + return $self->main_pkg->cancel(%options); + } + my $conf = new FS::Conf; warn "cust_pkg::cancel called with options". @@ -835,6 +869,22 @@ sub cancel { return $error; } + foreach my $supp_pkg ( $self->supplemental_pkgs ) { + $error = $supp_pkg->cancel(%options, 'from_main' => 1); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "canceling supplemental pkg#".$supp_pkg->pkgnum.": $error"; + } + } + + foreach my $usage ( $self->cust_pkg_usage ) { + $error = $usage->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "deleting usage pools: $error"; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; return '' if $date; #no errors @@ -894,6 +944,9 @@ svc_fatal: service provisioning errors are fatal svc_errors: pass an array reference, will be filled in with any provisioning errors +main_pkgnum: link the package as a supplemental package of this one. For +internal use only. + =cut sub uncancel { @@ -902,6 +955,10 @@ sub uncancel { #in case you try do do $uncancel-date = $cust_pkg->uncacel return '' unless $self->get('cancel'); + if ( $self->main_pkgnum and !$options{'main_pkgnum'} ) { + return $self->main_pkg->uncancel(%options); + } + ## # Transaction-alize ## @@ -926,6 +983,7 @@ sub uncancel { bill => ( $options{'bill'} || $self->get('bill') ), uncancel => time, uncancel_pkgnum => $self->pkgnum, + main_pkgnum => ($options{'main_pkgnum'} || ''), map { $_ => $self->get($_) } qw( custnum pkgpart locationnum setup @@ -937,6 +995,7 @@ sub uncancel { my $error = $cust_pkg->insert( 'change' => 1, #supresses any referral credit to a referring customer + 'allow_pkgpart' => 1, # allow this even if the package def is disabled ); if ($error) { $dbh->rollback if $oldAutoCommit; @@ -1023,6 +1082,20 @@ sub uncancel { } ## + # Uncancel any supplemental packages, and make them supplemental to the + # new one. + ## + + foreach my $supp_pkg ( $self->supplemental_pkgs ) { + my $new_pkg; + $error = $supp_pkg->uncancel(%options, 'main_pkgnum' => $cust_pkg->pkgnum); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "canceling supplemental pkg#".$supp_pkg->pkgnum.": $error"; + } + } + + ## # Finish ## @@ -1111,6 +1184,9 @@ of final invoices or unused-time credits unsuspended. This may be more convenient than calling C separately. +=item from_main - allows a supplemental package to be suspended, rather +than redirecting the method call to its main package. For internal use. + =back If there is an error, returns the error, otherwise returns false. @@ -1121,6 +1197,11 @@ sub suspend { my( $self, %options ) = @_; my $error; + # pass all suspend/cancel actions to the main package + if ( $self->main_pkgnum and !$options{'from_main'} ) { + return $self->main_pkg->suspend(%options); + } + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -1271,6 +1352,14 @@ sub suspend { } + foreach my $supp_pkg ( $self->supplemental_pkgs ) { + $error = $supp_pkg->suspend(%options, 'from_main' => 1); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "suspending supplemental pkg#".$supp_pkg->pkgnum.": $error"; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no errors @@ -1353,6 +1442,11 @@ sub unsuspend { my( $self, %opt ) = @_; my $error; + # pass all suspend/cancel actions to the main package + if ( $self->main_pkgnum and !$opt{'from_main'} ) { + return $self->main_pkg->unsuspend(%opt); + } + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -1511,6 +1605,14 @@ sub unsuspend { } + foreach my $supp_pkg ( $self->supplemental_pkgs ) { + $error = $supp_pkg->unsuspend(%opt, 'from_main' => 1); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "unsuspending supplemental pkg#".$supp_pkg->pkgnum.": $error"; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no errors @@ -1662,12 +1764,23 @@ sub change { if ( $opt->{'cust_location'} && ( ! $opt->{'locationnum'} || $opt->{'locationnum'} == -1 ) ) { - $error = $opt->{'cust_location'}->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "inserting cust_location (transaction rolled back): $error"; + + if ( ! $opt->{'cust_location'}->locationnum ) { + # not inserted yet + $error = $opt->{'cust_location'}->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "inserting cust_location (transaction rolled back): $error"; + } } $opt->{'locationnum'} = $opt->{'cust_location'}->locationnum; + + } + + # whether to override pkgpart checking on the new package + my $same_pkgpart = 1; + if ( $opt->{'pkgpart'} and ( $opt->{'pkgpart'} != $self->pkgpart ) ) { + $same_pkgpart = 0; } my $unused_credit = 0; @@ -1676,9 +1789,11 @@ sub change { # going to be credited for remaining time, don't keep setup, bill, # or last_bill dates, and DO pass the flag to cancel() to credit # the customer. - if ( $opt->{'pkgpart'} and $opt->{'pkgpart'} != $self->pkgpart ) { + if ( $opt->{'pkgpart'} + and $opt->{'pkgpart'} != $self->pkgpart + and $self->part_pkg->option('unused_credit_change', 1) ) { + $unused_credit = 1; $keep_dates = 0; - $unused_credit = 1 if $self->part_pkg->option('unused_credit_change', 1); $hash{$_} = '' foreach qw(setup bill last_bill); } @@ -1692,6 +1807,12 @@ sub change { # (i.e. customer default location) $opt->{'locationnum'} = $self->locationnum if !exists($opt->{'locationnum'}); + # usually this doesn't matter. the two cases where it does are: + # 1. unused_credit_change + pkgpart change + setup fee on the new package + # and + # 2. (more importantly) changing a package before it's billed + $hash{'waive_setup'} = $self->waive_setup; + # Create the new package. my $cust_pkg = new FS::cust_pkg { custnum => $self->custnum, @@ -1700,8 +1821,8 @@ sub change { locationnum => ( $opt->{'locationnum'} ), %hash, }; - - $error = $cust_pkg->insert( 'change' => 1 ); + $error = $cust_pkg->insert( 'change' => 1, + 'allow_pkgpart' => $same_pkgpart ); if ($error) { $dbh->rollback if $oldAutoCommit; return $error; @@ -1747,6 +1868,84 @@ sub change { $dbh->rollback if $oldAutoCommit; return "Error setting usage values: $error"; } + } else { + # if NOT changing pkgpart, transfer any usage pools over + foreach my $usage ($self->cust_pkg_usage) { + $usage->set('pkgnum', $cust_pkg->pkgnum); + $error = $usage->replace; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error transferring usage pools: $error"; + } + } + } + + # transfer discounts, if we're not changing pkgpart + if ( $same_pkgpart ) { + foreach my $old_discount ($self->cust_pkg_discount_active) { + # don't remove the old discount, we may still need to bill that package. + my $new_discount = new FS::cust_pkg_discount { + 'pkgnum' => $cust_pkg->pkgnum, + 'discountnum' => $old_discount->discountnum, + 'months_used' => $old_discount->months_used, + }; + $error = $new_discount->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error transferring discounts: $error"; + } + } + } + + # Order any supplemental packages. + my $part_pkg = $cust_pkg->part_pkg; + my @old_supp_pkgs = $self->supplemental_pkgs; + my @new_supp_pkgs; + foreach my $link ($part_pkg->supp_part_pkg_link) { + my $old; + foreach (@old_supp_pkgs) { + if ($_->pkgpart == $link->dst_pkgpart) { + $old = $_; + $_->pkgpart(0); # so that it can't match more than once + } + last if $old; + } + # false laziness with FS::cust_main::Packages::order_pkg + my $new = FS::cust_pkg->new({ + pkgpart => $link->dst_pkgpart, + pkglinknum => $link->pkglinknum, + custnum => $self->custnum, + main_pkgnum => $cust_pkg->pkgnum, + locationnum => $cust_pkg->locationnum, + start_date => $cust_pkg->start_date, + order_date => $cust_pkg->order_date, + expire => $cust_pkg->expire, + adjourn => $cust_pkg->adjourn, + contract_end => $cust_pkg->contract_end, + refnum => $cust_pkg->refnum, + discountnum => $cust_pkg->discountnum, + waive_setup => $cust_pkg->waive_setup, + }); + if ( $old and $opt->{'keep_dates'} ) { + foreach (qw(setup bill last_bill)) { + $new->set($_, $old->get($_)); + } + } + $error = $new->insert( allow_pkgpart => $same_pkgpart ); + # transfer services + if ( $old ) { + $error ||= $old->transfer($new); + } + if ( $error and $error > 0 ) { + # no reason why this should ever fail, but still... + $error = "Unable to transfer all services from supplemental package ". + $old->pkgnum; + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + push @new_supp_pkgs, $new; } #Good to go, cancel old package. Notify 'cancel' of whether to credit @@ -1754,6 +1953,7 @@ sub change { #Don't allow billing the package (preceding period packages and/or #outstanding usage) if we are keeping dates (i.e. location changing), #because the new package will be billed for the same date range. + #Supplemental packages are also canceled here. $error = $self->cancel( quiet => 1, unused_credit => $unused_credit, @@ -1766,7 +1966,9 @@ sub change { if ( $conf->exists('cust_pkg-change_pkgpart-bill_now') ) { #$self->cust_main - my $error = $cust_pkg->cust_main->bill( 'pkg_list' => [ $cust_pkg ] ); + my $error = $cust_pkg->cust_main->bill( + 'pkg_list' => [ $cust_pkg, @new_supp_pkgs ] + ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -1779,6 +1981,24 @@ sub change { } +=item set_quantity QUANTITY + +Change the package's quantity field. This is the one package property +that can safely be changed without canceling and reordering the package +(because it doesn't affect tax eligibility). Returns an error or an +empty string. + +=cut + +sub set_quantity { + my $self = shift; + $self = $self->replace_old; # just to make sure + my $qty = shift; + ($qty =~ /^\d+$/ and $qty > 0) or return "bad package quantity $qty"; + $self->set('quantity' => $qty); + $self->replace; +} + use Storable 'thaw'; use MIME::Base64; sub process_bulk_cust_pkg { @@ -2469,7 +2689,7 @@ sub statuscolor { =item pkg_label Returns a label for this package. (Currently "pkgnum: pkg - comment" or -"pkg-comment" depending on user preference). +"pkg - comment" depending on user preference). =cut @@ -2496,6 +2716,17 @@ sub pkg_label_long { $label; } +=item pkg_locale + +Returns a customer-localized label for this package. + +=cut + +sub pkg_locale { + my $self = shift; + $self->part_pkg->pkg_locale( $self->cust_main->locale ); +} + =item primary_cust_svc Returns a primary service (as FS::cust_svc object) if one can be identified. @@ -3137,6 +3368,207 @@ sub cust_pkg_discount_active { grep { $_->status eq 'active' } $self->cust_pkg_discount; } +=item cust_pkg_usage + +Returns a list of all voice usage counters attached to this package. + +=cut + +sub cust_pkg_usage { + my $self = shift; + qsearch('cust_pkg_usage', { pkgnum => $self->pkgnum }); +} + +=item apply_usage OPTIONS + +Takes the following options: +- cdr: a call detail record (L) +- rate_detail: the rate determined for this call (L) +- minutes: the maximum number of minutes to be charged + +Finds available usage minutes for a call of this class, and subtracts +up to that many minutes from the usage pool. If the usage pool is empty, +and the C global config option is set, minutes may +be taken from other calls as well. Either way, an allocation record will +be created (L) and this method will return the +number of minutes of usage applied to the call. + +=cut + +sub apply_usage { + my ($self, %opt) = @_; + my $cdr = $opt{cdr}; + my $rate_detail = $opt{rate_detail}; + my $minutes = $opt{minutes}; + my $classnum = $rate_detail->classnum; + my $pkgnum = $self->pkgnum; + my $custnum = $self->custnum; + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + my $order = FS::Conf->new->config('cdr-minutes_priority'); + + my $is_classnum; + if ( $classnum ) { + $is_classnum = ' part_pkg_usage_class.classnum = '.$classnum; + } else { + $is_classnum = ' part_pkg_usage_class.classnum IS NULL'; + } + my @usage_recs = qsearch({ + 'table' => 'cust_pkg_usage', + 'addl_from' => ' JOIN part_pkg_usage USING (pkgusagepart)'. + ' JOIN cust_pkg USING (pkgnum)'. + ' JOIN part_pkg_usage_class USING (pkgusagepart)', + 'select' => 'cust_pkg_usage.*', + 'extra_sql' => " WHERE ( cust_pkg.pkgnum = $pkgnum OR ". + " ( cust_pkg.custnum = $custnum AND ". + " part_pkg_usage.shared IS NOT NULL ) ) AND ". + $is_classnum . ' AND '. + " cust_pkg_usage.minutes > 0", + 'order_by' => " ORDER BY priority ASC", + }); + + my $orig_minutes = $minutes; + my $error; + while (!$error and $minutes > 0 and @usage_recs) { + my $cust_pkg_usage = shift @usage_recs; + $cust_pkg_usage->select_for_update; + my $cdr_cust_pkg_usage = FS::cdr_cust_pkg_usage->new({ + pkgusagenum => $cust_pkg_usage->pkgusagenum, + acctid => $cdr->acctid, + minutes => min($cust_pkg_usage->minutes, $minutes), + }); + $cust_pkg_usage->set('minutes', + sprintf('%.0f', $cust_pkg_usage->minutes - $cdr_cust_pkg_usage->minutes) + ); + $error = $cust_pkg_usage->replace || $cdr_cust_pkg_usage->insert; + $minutes -= $cdr_cust_pkg_usage->minutes; + } + if ( $order and $minutes > 0 and !$error ) { + # then try to steal minutes from another call + my %search = ( + 'table' => 'cdr_cust_pkg_usage', + 'addl_from' => ' JOIN cust_pkg_usage USING (pkgusagenum)'. + ' JOIN part_pkg_usage USING (pkgusagepart)'. + ' JOIN cust_pkg USING (pkgnum)'. + ' JOIN part_pkg_usage_class USING (pkgusagepart)'. + ' JOIN cdr USING (acctid)', + 'select' => 'cdr_cust_pkg_usage.*', + 'extra_sql' => " WHERE cdr.freesidestatus = 'rated' AND ". + " ( cust_pkg.pkgnum = $pkgnum OR ". + " ( cust_pkg.custnum = $custnum AND ". + " part_pkg_usage.shared IS NOT NULL ) ) AND ". + " part_pkg_usage_class.classnum = $classnum", + 'order_by' => ' ORDER BY part_pkg_usage.priority ASC', + ); + if ( $order eq 'time' ) { + # find CDRs that are using minutes, but have a later startdate + # than this call + my $startdate = $cdr->startdate; + if ($startdate !~ /^\d+$/) { + die "bad cdr startdate '$startdate'"; + } + $search{'extra_sql'} .= " AND cdr.startdate > $startdate"; + # minimize needless reshuffling + $search{'order_by'} .= ', cdr.startdate DESC'; + } else { + # XXX may not work correctly with rate_time schedules. Could + # fix this by storing ratedetailnum in cdr_cust_pkg_usage, I + # think... + $search{'addl_from'} .= + ' JOIN rate_detail'. + ' ON (cdr.rated_ratedetailnum = rate_detail.ratedetailnum)'; + if ( $order eq 'rate_high' ) { + $search{'extra_sql'} .= ' AND rate_detail.min_charge < '. + $rate_detail->min_charge; + $search{'order_by'} .= ', rate_detail.min_charge ASC'; + } elsif ( $order eq 'rate_low' ) { + $search{'extra_sql'} .= ' AND rate_detail.min_charge > '. + $rate_detail->min_charge; + $search{'order_by'} .= ', rate_detail.min_charge DESC'; + } else { + # this should really never happen + die "invalid cdr-minutes_priority value '$order'\n"; + } + } + my @cdr_usage_recs = qsearch(\%search); + my %reproc_cdrs; + while (!$error and @cdr_usage_recs and $minutes > 0) { + my $cdr_cust_pkg_usage = shift @cdr_usage_recs; + my $cust_pkg_usage = $cdr_cust_pkg_usage->cust_pkg_usage; + my $old_cdr = $cdr_cust_pkg_usage->cdr; + $reproc_cdrs{$old_cdr->acctid} = $old_cdr; + $cdr_cust_pkg_usage->select_for_update; + $old_cdr->select_for_update; + $cust_pkg_usage->select_for_update; + # in case someone else stole the usage from this CDR + # while waiting for the lock... + next if $old_cdr->acctid != $cdr_cust_pkg_usage->acctid; + # steal the usage allocation and flag the old CDR for reprocessing + $cdr_cust_pkg_usage->set('acctid', $cdr->acctid); + # if the allocation is more minutes than we need, adjust it... + my $delta = $cdr_cust_pkg_usage->minutes - $minutes; + if ( $delta > 0 ) { + $cdr_cust_pkg_usage->set('minutes', $minutes); + $cust_pkg_usage->set('minutes', $cust_pkg_usage->minutes + $delta); + $error = $cust_pkg_usage->replace; + } + #warn 'CDR '.$cdr->acctid . ' stealing allocation '.$cdr_cust_pkg_usage->cdrusagenum.' from CDR '.$old_cdr->acctid."\n"; + $error ||= $cdr_cust_pkg_usage->replace; + # deduct the stolen minutes + $minutes -= $cdr_cust_pkg_usage->minutes; + } + # after all minute-stealing is done, reset the affected CDRs + foreach (values %reproc_cdrs) { + $error ||= $_->set_status(''); + # XXX or should we just call $cdr->rate right here? + # it's not like we can create a loop this way, since the min_charge + # or call time has to go monotonically in one direction. + # we COULD get some very deep recursions going, though... + } + } # if $order and $minutes + if ( $error ) { + $dbh->rollback; + die "error applying included minutes\npkgnum ".$self->pkgnum.", class $classnum, acctid ".$cdr->acctid."\n$error\n" + } else { + $dbh->commit if $oldAutoCommit; + return $orig_minutes - $minutes; + } +} + +=item supplemental_pkgs + +Returns a list of all packages supplemental to this one. + +=cut + +sub supplemental_pkgs { + my $self = shift; + qsearch('cust_pkg', { 'main_pkgnum' => $self->pkgnum }); +} + +=item main_pkg + +Returns the package that this one is supplemental to, if any. + +=cut + +sub main_pkg { + my $self = shift; + if ( $self->main_pkgnum ) { + return FS::cust_pkg->by_key($self->main_pkgnum); + } + return; +} + =back =head1 CLASS METHODS @@ -3664,10 +4096,10 @@ sub search { my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : ''; - my $addl_from = 'LEFT JOIN cust_main USING ( custnum ) '. - 'LEFT JOIN part_pkg USING ( pkgpart ) '. + my $addl_from = 'LEFT JOIN part_pkg USING ( pkgpart ) '. 'LEFT JOIN pkg_class ON ( part_pkg.classnum = pkg_class.classnum ) '. - 'LEFT JOIN cust_location USING ( locationnum ) '; + 'LEFT JOIN cust_location USING ( locationnum ) '. + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); my $select; my $count_query; @@ -3951,11 +4383,25 @@ sub order { %hash, }; $error = $cust_pkg->insert( 'change' => $change ); + push @$return_cust_pkg, $cust_pkg; + + foreach my $link ($cust_pkg->part_pkg->supp_part_pkg_link) { + my $supp_pkg = FS::cust_pkg->new({ + custnum => $custnum, + pkgpart => $link->dst_pkgpart, + refnum => $refnum, + main_pkgnum => $cust_pkg->pkgnum, + %hash, + }); + $error ||= $supp_pkg->insert( 'change' => $change ); + push @$return_cust_pkg, $supp_pkg; + } + if ($error) { $dbh->rollback if $oldAutoCommit; return $error; } - push @$return_cust_pkg, $cust_pkg; + } # $return_cust_pkg now contains refs to all of the newly # created packages. diff --git a/FS/FS/cust_pkg_discount.pm b/FS/FS/cust_pkg_discount.pm index 5f4d0dccf..d82d94990 100644 --- a/FS/FS/cust_pkg_discount.pm +++ b/FS/FS/cust_pkg_discount.pm @@ -164,7 +164,7 @@ sub check { $self->ut_numbern('pkgdiscountnum') || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum') || $self->ut_foreign_key('discountnum', 'discount', 'discountnum' ) - || $self->ut_float('months_used') #actually decimal, but this will do + || $self->ut_sfloat('months_used') #actually decimal, but this will do || $self->ut_numbern('end_date') || $self->ut_alphan('otaker') || $self->ut_numbern('usernum') @@ -202,7 +202,7 @@ sub discount { qsearchs('discount', { 'discountnum' => $self->discountnum } ); } -=item increment_months_used +=item increment_months_used MONTHS Increments months_used by the given parameter @@ -216,6 +216,31 @@ sub increment_months_used { $self->replace(); } +=item decrement_months_used MONTHS + +Decrement months_used by the given parameter + +(Note: as in, extending the length of the discount. Typically only used to +stack/extend a discount when the customer package has one active already.) + +=cut + +sub decrement_months_used { + my( $self, $recharged ) = @_; + #UPDATE cust_pkg_discount SET months_used = months_used - ? + #leaves no history, and billing is mutexed per-customer + + #we're run from part_event/Action/referral_pkg_discount on behalf of a + # different customer, so we need to grab this customer's mutex. + # incidentally, that's some inelegant encapsulation breaking shit, and a + # great argument in favor of native-DB trigger history so we can trust + # in normal ACID like the SQL above instead of this + $self->cust_pkg->cust_main->select_for_update; + + $self->months_used( $self->months_used - $recharged ); + $self->replace(); +} + =item status =cut diff --git a/FS/FS/cust_pkg_usage.pm b/FS/FS/cust_pkg_usage.pm new file mode 100644 index 000000000..0eefd7480 --- /dev/null +++ b/FS/FS/cust_pkg_usage.pm @@ -0,0 +1,163 @@ +package FS::cust_pkg_usage; + +use strict; +use base qw( FS::Record ); +use FS::cust_pkg; +use FS::part_pkg_usage; +use FS::Record qw( qsearch qsearchs ); + +=head1 NAME + +FS::cust_pkg_usage - Object methods for cust_pkg_usage records + +=head1 SYNOPSIS + + use FS::cust_pkg_usage; + + $record = new FS::cust_pkg_usage \%hash; + $record = new FS::cust_pkg_usage { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_pkg_usage object represents a counter of remaining included +minutes on a voice-call package. FS::cust_pkg_usage inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item pkgusagenum - primary key + +=item pkgnum - the package (L) containing the usage + +=item pkgusagepart - the usage stock definition (L). +This record in turn links to the call usage classes that are eligible to +use these minutes. + +=item minutes - the remaining minutes + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +# the new method can be inherited from FS::Record, if a table method is defined + +=cut + +sub table { 'cust_pkg_usage'; } + +=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 + +sub delete { + my $self = shift; + my $error = $self->reset || $self->SUPER::delete; +} + +=item reset + +Remove all allocations of this usage to CDRs. + +=cut + +sub reset { + my $self = shift; + my $error = ''; + foreach (qsearch('cdr_cust_pkg_usage', { pkgusagenum => $self->pkgusagenum })) + { + $error ||= $_->delete; + } + $error; +} + +=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 check + +Checks all fields to make sure this is a valid example. 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('pkgusagenum') + || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum') + || $self->ut_numbern('minutes') + || $self->ut_foreign_key('pkgusagepart', 'part_pkg_usage', 'pkgusagepart') + ; + return $error if $error; + + if ( $self->minutes eq '' ) { + $self->set(minutes => $self->part_pkg_usage->minutes); + } + + $self->SUPER::check; +} + +=item cust_pkg + +Return the L linked to this record. + +=item part_pkg_usage + +Return the L linked to this record. + +=cut + +sub cust_pkg { + my $self = shift; + FS::cust_pkg->by_key($self->pkgnum); +} + +sub part_pkg_usage { + my $self = shift; + FS::part_pkg_usage->by_key($self->pkgusagepart); +} + +=back + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index b608b2349..165384048 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -13,6 +13,7 @@ use FS::pkg_svc; use FS::domain_record; use FS::part_export; use FS::cdr; +use FS::UI::Web; #most FS::svc_ classes are autoloaded in svc_x emthod use FS::svc_acct; #this one is used in the cache stuff @@ -883,7 +884,7 @@ sub smart_search_param { my $extra_sql = ' WHERE '.join(' AND ', @extra_sql); #for agentnum my $addl_from = ' LEFT JOIN cust_pkg USING ( pkgnum )'. - ' LEFT JOIN cust_main USING ( custnum )'. + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'). ' LEFT JOIN part_svc USING ( svcpart )'; ( @@ -894,6 +895,48 @@ sub smart_search_param { ); } +sub _upgrade_data { + my $class = shift; + + # fix missing (deleted by mistake) svc_x records + warn "searching for missing svc_x records...\n"; + my %search = ( + 'table' => 'cust_svc', + 'select' => 'cust_svc.*', + 'addl_from' => ' LEFT JOIN ( ' . + join(' UNION ', + map { "SELECT svcnum FROM $_" } + FS::part_svc->svc_tables + ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum', + 'extra_sql' => ' WHERE svc_all.svcnum IS NULL', + ); + my @svcs = qsearch(\%search); + warn "found ".scalar(@svcs)."\n"; + + local $FS::Record::nowarn_classload = 1; # for h_svc_ + local $FS::svc_Common::noexport_hack = 1; # because we're inserting services + + my %h_search = ( + 'hashref' => { history_action => 'delete' }, + 'order_by' => ' ORDER BY history_date DESC LIMIT 1', + ); + foreach my $cust_svc (@svcs) { + my $svcnum = $cust_svc->svcnum; + my $svcdb = $cust_svc->part_svc->svcdb; + $h_search{'hashref'}{'svcnum'} = $svcnum; + $h_search{'table'} = "h_$svcdb"; + my $h_svc_x = qsearchs(\%h_search) + or next; + my $class = "FS::$svcdb"; + my $new_svc_x = $class->new({ $h_svc_x->hash }); + my $error = $new_svc_x->insert; + warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n" + if $error; + } + + ''; +} + =back =head1 BUGS diff --git a/FS/FS/export_svc.pm b/FS/FS/export_svc.pm index 0370f5f0b..b08f8f7c3 100644 --- a/FS/FS/export_svc.pm +++ b/FS/FS/export_svc.pm @@ -5,6 +5,7 @@ use vars qw( @ISA ); use FS::Record qw( qsearch qsearchs dbh ); use FS::part_export; use FS::part_svc; +use FS::svc_export_machine; @ISA = qw(FS::Record); @@ -209,6 +210,19 @@ sub insert { } #end of duplicate check, whew $error = $self->SUPER::insert; + + my $part_export = $self->part_export; + if ( !$error and $part_export->default_machine ) { + foreach my $cust_svc ( $self->part_svc->cust_svc ) { + my $svc_export_machine = FS::svc_export_machine->new({ + 'exportnum' => $self->exportnum, + 'svcnum' => $cust_svc->svcnum, + 'machinenum' => $part_export->default_machine, + }); + $error ||= $svc_export_machine->insert; + } + } + if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -251,7 +265,23 @@ Delete this record from the database. =cut -# the delete method can be inherited from FS::Record +sub delete { + my $self = shift; + my $dbh = dbh; + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + + my $error = $self->SUPER::delete; + foreach ($self->svc_export_machine) { + $error ||= $_->delete; + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; +} + =item replace OLD_RECORD @@ -307,6 +337,24 @@ sub part_svc { qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } ); } +=item svc_export_machine + +Returns all export hostname records (L) for this +combination of svcpart and exportnum. + +=cut + +sub svc_export_machine { + my $self = shift; + qsearch({ + 'table' => 'svc_export_machine', + 'select' => 'svc_export_machine.*', + 'addl_from' => 'JOIN cust_svc USING (svcnum)', + 'hashref' => { 'exportnum' => $self->exportnum }, + 'extra_sql' => ' AND cust_svc.svcpart = '.$self->svcpart, + }); +} + =back =head1 BUGS diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index e38346a66..2f5e4762a 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -3,7 +3,7 @@ package FS::msg_template; use strict; use base qw( FS::Record ); use Text::Template; -use FS::Misc qw( generate_email send_email ); +use FS::Misc qw( generate_email send_email do_print ); use FS::Conf; use FS::Record qw( qsearch qsearchs ); use FS::UID qw( dbh ); @@ -457,24 +457,13 @@ sub render { my %hash = $self->prepare(%opt); my $html = $hash{'html_body'}; - my $tmp = 'msg'.$self->msgnum.'-'.time2str('%Y%m%d', time).'-XXXXXXXX'; - my $dir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc"; - # Graphics/stylesheets should probably go in /var/www on the Freeside # machine. my $kit = PDF::WebKit->new(\$html); #%options # hack to use our wrapper script $kit->configure(sub { shift->wkhtmltopdf('freeside-wkhtmltopdf') }); - my $fh = File::Temp->new( - TEMPLATE => $tmp, - DIR => $dir, - UNLINK => 0, - SUFFIX => '.pdf' - ); - print $fh $kit->to_pdf; - close $fh; - return $fh->filename; + $kit->to_pdf; } =item print OPTIONS @@ -484,13 +473,10 @@ Render a PDF and send it to the printer. OPTIONS are as for 'render'. =cut sub print { - my $file = render(@_); - my @lpr = $conf->config('lpr'); - run ([@lpr, '-r'], '<', $file) - or die "lpr error:\n$?\n"; + my( $self, %opt ) = @_; + do_print( [ $self->render(%opt) ], agentnum=>$opt{cust_main}->agentnum ); } - # helper sub for package dates my $ymd = sub { $_[0] ? time2str('%Y-%m-%d', $_[0]) : '' }; diff --git a/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm b/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm index 73d32e0a7..33aeadd35 100644 --- a/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm +++ b/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm @@ -2,6 +2,7 @@ package FS::part_event::Action::Mixin::credit_agent_pkg_class; use base qw( FS::part_event::Action::Mixin::credit_pkg ); use strict; +use FS::Record qw(qsearchs); sub option_fields { my $class = shift; diff --git a/FS/FS/part_event/Action/Mixin/credit_pkg.pm b/FS/FS/part_event/Action/Mixin/credit_pkg.pm index 9dcd701a9..a3c1d6efb 100644 --- a/FS/FS/part_event/Action/Mixin/credit_pkg.pm +++ b/FS/FS/part_event/Action/Mixin/credit_pkg.pm @@ -16,18 +16,24 @@ sub option_fields { 'type' => 'input-percentage', 'default' => '100', }, - 'what' => { 'label' => 'Of', - 'type' => 'select', - #add additional ways to specify in the package def - 'options' => [ qw( base_recur_permonth unit_setup recur_cost_permonth setup_cost ) ], - 'labels' => { 'base_recur_permonth' => 'Base monthly fee', - 'unit_setup' => 'Setup fee', - 'recur_cost_permonth' => 'Monthly cost', - 'setup_cost' => 'Setup cost', - }, - }, + 'what' => { + 'label' => 'Of', + 'type' => 'select', + #add additional ways to specify in the package def + 'options' => [qw( + base_recur_permonth cust_bill_pkg_recur recur_cost_permonth + unit_setup setup_cost + )], + 'labels' => { + 'base_recur_permonth' => 'Base monthly fee', + 'cust_bill_pkg_recur' => 'Actual invoiced amount of most recent'. + ' recurring charge', + 'recur_cost_permonth' => 'Monthly cost', + 'unit_setup' => 'Setup fee', + 'setup_cost' => 'Setup cost', + }, + }, ); - } #my %no_cust_pkg = ( 'setup_cost' => 1 ); diff --git a/FS/FS/part_event/Action/cust_bill_send_reminder.pm b/FS/FS/part_event/Action/cust_bill_send_reminder.pm index 2ba8136dd..073bb8fd3 100644 --- a/FS/FS/part_event/Action/cust_bill_send_reminder.pm +++ b/FS/FS/part_event/Action/cust_bill_send_reminder.pm @@ -11,9 +11,10 @@ sub eventtable_hashref { sub option_fields { ( - 'notice_name' => 'Reminder name', - #'notes' => { 'label' => 'Reminder notes' }, + 'notice_name' => 'Reminder name', + #'notes' => { 'label' => 'Reminder notes' }, #include standard notes? no/prepend/append + 'lpr' => 'Optional alternate print command', ); } @@ -25,7 +26,10 @@ sub do_action { #my $cust_main = $self->cust_main($cust_bill); #my $cust_main = $cust_bill->cust_main; - $cust_bill->send({ 'notice_name' => $self->option('notice_name') }); + $cust_bill->send({ + 'notice_name' => $self->option('notice_name'), + 'lpr' => $self->option('lpr'), + }); } 1; diff --git a/FS/FS/part_event/Action/referral_pkg_billdate.pm b/FS/FS/part_event/Action/referral_pkg_billdate.pm new file mode 100644 index 000000000..6b485e59b --- /dev/null +++ b/FS/FS/part_event/Action/referral_pkg_billdate.pm @@ -0,0 +1,59 @@ +package FS::part_event::Action::referral_pkg_billdate; + +use strict; +use base qw( FS::part_event::Action ); + +sub description { "Increment the referring customer's package's next bill date"; } + +#sub eventtable_hashref { +#} + +sub option_fields { + ( + 'if_pkgpart' => { 'label' => 'Only packages', + 'type' => 'select-part_pkg', + 'multiple' => 1, + }, + 'increment' => { 'label' => 'Increment by', + 'type' => 'freq', + 'value' => '1m', + }, + ); +} + +#false laziness w/referral_pkg_discount, probably should make +# Mixin/referral_pkg.pm if we need changes or anything else in this vein +sub do_action { + my( $self, $cust_object, $cust_event ) = @_; + + my $cust_main = $self->cust_main($cust_object); + + return 'No referring customer' unless $cust_main->referral_custnum; + + my $referring_cust_main = $cust_main->referring_cust_main; + #return 'Referring customer is cancelled' + # if $referring_cust_main->status eq 'cancelled'; + + my %if_pkgpart = map { $_=>1 } split(/\s*,\s*/, $self->option('if_pkgpart') ); + my @cust_pkg = grep $if_pkgpart{ $_->pkgpart }, + $referring_cust_main->billing_pkgs; + return 'No qualifying billing package definition' unless @cust_pkg; + + my $cust_pkg = $cust_pkg[0]; #only one + + #end of false laziness + + my $bill = $cust_pkg->bill || $cust_pkg->setup || time; + + $cust_pkg->bill( + $cust_pkg->part_pkg->add_freq( $bill, $self->option('increment') ) + ); + + my $error = $cust_pkg->replace; + die "Error incrementing next bill date: $error" if $error; + + ''; + +} + +1; diff --git a/FS/FS/part_event/Action/referral_pkg_discount.pm b/FS/FS/part_event/Action/referral_pkg_discount.pm new file mode 100644 index 000000000..2ff1b35fb --- /dev/null +++ b/FS/FS/part_event/Action/referral_pkg_discount.pm @@ -0,0 +1,101 @@ +package FS::part_event::Action::referral_pkg_discount; + +use strict; +use base qw( FS::part_event::Action ); + +sub description { "Discount the referring customer's package"; } + +#sub eventtable_hashref { +#} + +sub option_fields { + ( + 'if_pkgpart' => { 'label' => 'Only packages', + 'type' => 'select-part_pkg', + 'multiple' => 1, + }, + 'discountnum' => { 'label' => 'Discount', + 'type' => 'select-table', #we don't handle the select-discount create a discount case + 'table' => 'discount', + 'name_col' => 'description', #well, method + 'order_by' => 'ORDER BY discountnum', #requied because name_col is a method + 'hashref' => { 'disabled' => '', + 'months' => { op=>'!=', value=>'0' }, + }, + 'disable_empty' => 1, + }, + ); +} + +#false laziness w/referral_pkg_billdate, probably should make +# Mixin/referral_pkg.pm if we need changes or anything else in this vein +sub do_action { + my( $self, $cust_object, $cust_event ) = @_; + + my $cust_main = $self->cust_main($cust_object); + + return 'No referring customer' unless $cust_main->referral_custnum; + + my $referring_cust_main = $cust_main->referring_cust_main; + #return 'Referring customer is cancelled' + # if $referring_cust_main->status eq 'cancelled'; + + my %if_pkgpart = map { $_=>1 } split(/\s*,\s*/, $self->option('if_pkgpart') ); + my @cust_pkg = grep $if_pkgpart{ $_->pkgpart }, + $referring_cust_main->billing_pkgs; + return 'No qualifying billing package definition' unless @cust_pkg; + + my $cust_pkg = $cust_pkg[0]; #only one + + #end of false laziness + + my @cust_pkg_discount = $cust_pkg->cust_pkg_discount_active; + my @my_cust_pkg_discount = + grep { $_->discountnum == $self->option('discountnum') } @cust_pkg_discount; + + if ( @my_cust_pkg_discount ) { #increment the existing one instead + + die "guru meditation #and: multiple discounts" + if scalar(@my_cust_pkg_discount) > 1; + + my $cust_pkg_discount = $my_cust_pkg_discount[0]; + my $discount = $cust_pkg_discount->discount; + die "guru meditation #goob: can't extended non-expiring discount" + if $discount->months == 0; + + my $error = $cust_pkg_discount->decrement_months_used( $discount->months ); + die "Error extending discount: $error\n" if $error; + + } elsif ( @cust_pkg_discount ) { + + #"stacked" discount case not possible from UI, not handled, so prevent + # against creating one here. i guess we could try to find a different + # @cust_pkg above if this case needed to be handled better? + die "Can't discount an already discounted package"; + + } else { #normal case, create a new one + + my $cust_pkg_discount = new FS::cust_pkg_discount { + 'pkgnum' => $cust_pkg->pkgnum, + 'discountnum' => $self->option('discountnum'), + 'months_used' => 0, + #'end_date' => '', + #we dont handle the create a new discount case + #'_type' => scalar($cgi->param('discountnum__type')), + #'amount' => scalar($cgi->param('discountnum_amount')), + #'percent' => scalar($cgi->param('discountnum_percent')), + #'months' => scalar($cgi->param('discountnum_months')), + #'setup' => scalar($cgi->param('discountnum_setup')), + ##'linked' => scalar($cgi->param('discountnum_linked')), + ##'disabled' => $self->discountnum_disabled, + }; + my $error = $cust_pkg_discount->insert; + die "Error discounting package: $error\n" if $error; + + } + + ''; + +} + +1; diff --git a/FS/FS/part_event/Condition/cust_bill_owed_percent.pm b/FS/FS/part_event/Condition/cust_bill_owed_percent.pm new file mode 100644 index 000000000..e06b511ef --- /dev/null +++ b/FS/FS/part_event/Condition/cust_bill_owed_percent.pm @@ -0,0 +1,50 @@ +package FS::part_event::Condition::cust_bill_owed_percent; + +use strict; +use FS::cust_bill; + +use base qw( FS::part_event::Condition ); + +sub description { + 'Percentage owed on specific invoice'; +} + +sub eventtable_hashref { + { 'cust_main' => 0, + 'cust_bill' => 1, + 'cust_pkg' => 0, + }; +} + +sub option_fields { + ( + 'owed' => { 'label' => 'Percentage of invoice owed over', + 'type' => 'percentage', + 'value' => '0', #default + }, + ); +} + +sub condition { + #my($self, $cust_bill, %opt) = @_; + my($self, $cust_bill) = @_; + + my $percent = $self->option('owed') || 0; + my $over = sprintf('%.2f', + $cust_bill->charged * $percent / 100); + + $cust_bill->owed > $over; +} + +sub condition_sql { + my( $class, $table ) = @_; + + # forces the option to be an integer--do we care? + my $percent = $class->condition_sql_option_integer('owed'); + + my $owed_sql = FS::cust_bill->owed_sql; + + "$owed_sql > CAST( cust_bill.charged * $percent / 100 AS DECIMAL(10,2) )"; +} + +1; diff --git a/FS/FS/part_event/Condition/has_pkgpart.pm b/FS/FS/part_event/Condition/has_pkgpart.pm index c54b7e256..d85e1bd43 100644 --- a/FS/FS/part_event/Condition/has_pkgpart.pm +++ b/FS/FS/part_event/Condition/has_pkgpart.pm @@ -4,7 +4,7 @@ use strict; use base qw( FS::part_event::Condition ); -sub description { 'Customer has uncancelled package of specified definitions'; } +sub description { 'Customer has uncancelled specific package(s)'; } sub eventtable_hashref { { 'cust_main' => 1, @@ -27,7 +27,6 @@ sub condition { my $cust_main = $self->cust_main($object); - #XXX test my $if_pkgpart = $self->option('if_pkgpart') || {}; grep $if_pkgpart->{ $_->pkgpart }, $cust_main->ncancelled_pkgs; diff --git a/FS/FS/part_event/Condition/has_referral_custnum.pm b/FS/FS/part_event/Condition/has_referral_custnum.pm index dee240fec..c50579411 100644 --- a/FS/FS/part_event/Condition/has_referral_custnum.pm +++ b/FS/FS/part_event/Condition/has_referral_custnum.pm @@ -13,7 +13,7 @@ sub option_fields { 'type' => 'checkbox', 'value' => 'Y', }, - 'check_bal' => { 'label' => 'Check referring custoemr balance', + 'check_bal' => { 'label' => 'Check referring customer balance', 'type' => 'checkbox', 'value' => 'Y', }, diff --git a/FS/FS/part_event/Condition/message_email.pm b/FS/FS/part_event/Condition/message_email.pm new file mode 100644 index 000000000..7cceba697 --- /dev/null +++ b/FS/FS/part_event/Condition/message_email.pm @@ -0,0 +1,22 @@ +package FS::part_event::Condition::message_email; +use base qw( FS::part_event::Condition ); +use strict; + +sub description { + 'Customer allows email notices' +} + +sub condition { + my( $self, $object ) = @_; + my $cust_main = $self->cust_main($object); + + $cust_main->message_noemail ? 0 : 1; +} + +sub condition_sql { + my( $self, $table ) = @_; + + "cust_main.message_noemail IS NULL" +} + +1; diff --git a/FS/FS/part_event/Condition/once_percust.pm b/FS/FS/part_event/Condition/once_percust.pm index b8a8fbfb6..67767f91b 100644 --- a/FS/FS/part_event/Condition/once_percust.pm +++ b/FS/FS/part_event/Condition/once_percust.pm @@ -45,7 +45,6 @@ sub condition { } -#XXX test? sub condition_sql { my( $self, $table ) = @_; diff --git a/FS/FS/part_event/Condition/once_perinv.pm b/FS/FS/part_event/Condition/once_perinv.pm index f85a05665..1ee53b812 100644 --- a/FS/FS/part_event/Condition/once_perinv.pm +++ b/FS/FS/part_event/Condition/once_perinv.pm @@ -12,6 +12,15 @@ sub description { "Run only once for each time the package has been billed"; } # Run the event, at most, a number of times equal to the number of # distinct invoices that contain line items from this package. +sub option_fields { + ( + 'paid' => { 'label' => 'Only count paid bills', + 'type' => 'checkbox', + 'value' => 'Y', + }, + ) +} + sub eventtable_hashref { { 'cust_main' => 0, 'cust_bill' => 0, @@ -22,9 +31,15 @@ sub eventtable_hashref { sub condition { my($self, $cust_pkg, %opt) = @_; - my %invnum; - $invnum{$_->invnum} = 1 - foreach ( qsearch('cust_bill_pkg', { 'pkgnum' => $cust_pkg->pkgnum }) ); + my @cust_bill_pkg = qsearch('cust_bill_pkg', { pkgnum=>$cust_pkg->pkgnum }); + + @cust_bill_pkg = grep { ($_->owed_setup + $_->owed_recur) == 0 } + @cust_bill_pkg + if $self->option('paid'); + + my %invnum = (); + $invnum{$_->invnum} = 1 foreach @cust_bill_pkg; + my @events = qsearch( { 'table' => 'cust_event', 'hashref' => { 'eventpart' => $self->eventpart, @@ -40,6 +55,9 @@ sub condition { sub condition_sql { my( $self, $table ) = @_; + #paid flag not yet implemented here, but that's okay, a partial optimization + # is better than none + "( ( SELECT COUNT(distinct(invnum)) FROM cust_bill_pkg diff --git a/FS/FS/part_event/Condition/times_percust.pm b/FS/FS/part_event/Condition/times_percust.pm new file mode 100644 index 000000000..fc7064b7e --- /dev/null +++ b/FS/FS/part_event/Condition/times_percust.pm @@ -0,0 +1,76 @@ +package FS::part_event::Condition::times_percust; + +use strict; +use FS::Record qw( qsearch ); +use FS::part_event; +use FS::cust_event; + +use base qw( FS::part_event::Condition ); + +sub description { "Run this event the specified number of times per customer"; } + +sub option_fields { + ( + 'run_times' => { label=>'Number of times', type=>'text', value=>'1', }, + ); +} + +sub eventtable_hashref { + { 'cust_main' => 0, + 'cust_bill' => 1, + 'cust_pkg' => 1, + }; +} + +sub condition { + my($self, $object, %opt) = @_; + + my $obj_pkey = $object->primary_key; + my $obj_table = $object->table; + my $custnum = $object->custnum; + + my @where = ( + "tablenum IN ( SELECT $obj_pkey FROM $obj_table WHERE custnum = $custnum )" + ); + if ( $opt{'cust_event'}->eventnum =~ /^(\d+)$/ ) { + push @where, " eventnum != $1 "; + } + my $extra_sql = ' AND '. join(' AND ', @where); + + my @existing = qsearch( { + 'table' => 'cust_event', + 'hashref' => { + 'eventpart' => $self->eventpart, + #'tablenum' => $tablenum, + 'status' => { op=>'!=', value=>'failed' }, + }, + 'extra_sql' => $extra_sql, + } ); + + scalar(@existing) < $self->option('run_times'); + +} + +sub condition_sql { + my( $class, $table, %opt ) = @_; + + my %pkey = %{ FS::part_event->eventtable_pkey }; + + my $run_times = + $class->condition_sql_option_integer('run_times', $opt{'driver_name'}); + + my $pkey = $pkey{$table}; + + my $existing = "( SELECT COUNT(*) FROM cust_event + WHERE cust_event.eventpart = part_event.eventpart + AND cust_event.tablenum IN ( + SELECT $pkey FROM $table AS times_percust + WHERE times_percust.custnum = cust_main.custnum ) + AND status != 'failed' + )"; + + "$existing < $run_times"; + +} + +1; diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm index 5d650626e..28cb1419d 100644 --- a/FS/FS/part_export.pm +++ b/FS/FS/part_export.pm @@ -125,31 +125,14 @@ sub insert { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $error = $self->SUPER::insert(@_); + my $error = $self->SUPER::insert(@_) + || $self->replace; + # use replace to do all the part_export_machine and default_machine stuff if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; } - #kinda false laziness with process_m2name - my @machines = map { $_ =~ s/^\s+//; $_ =~ s/\s+$//; $_ } - grep /\S/, - split /[\n\r]{1,2}/, - $self->part_export_machine_textarea; - - foreach my $machine ( @machines ) { - - my $part_export_machine = new FS::part_export_machine { - 'exportnum' => $self->exportnum, - 'machine' => $machine, - }; - $error = $part_export_machine->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - } - $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } @@ -217,6 +200,7 @@ or modified. sub replace { my $self = shift; + my $old = $self->replace_old; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -228,12 +212,7 @@ sub replace { my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; - - my $error = $self->SUPER::replace(@_); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } + my $error; if ( $self->part_export_machine_textarea ) { @@ -258,6 +237,10 @@ sub replace { } } + if ( $self->default_machine_name eq $machine ) { + $self->default_machine( $part_export_machine{$machine}->machinenum ); + } + delete $part_export_machine{$machine}; #so we don't disable it below } else { @@ -272,11 +255,13 @@ sub replace { return $error; } + if ( $self->default_machine_name eq $machine ) { + $self->default_machine( $part_export_machine->machinenum ); + } } } - foreach my $part_export_machine ( values %part_export_machine ) { $part_export_machine->disabled('Y'); $error = $part_export_machine->replace; @@ -286,6 +271,48 @@ sub replace { } } + if ( $old->machine ne '_SVC_MACHINE' ) { + # then set up the default for any already-attached export_svcs + foreach my $export_svc ( $self->export_svc ) { + my @svcs = qsearch('cust_svc', { 'svcpart' => $export_svc->svcpart }); + foreach my $cust_svc ( @svcs ) { + my $svc_export_machine = FS::svc_export_machine->new({ + 'exportnum' => $self->exportnum, + 'svcnum' => $cust_svc->svcnum, + 'machinenum' => $self->default_machine, + }); + $error ||= $svc_export_machine->insert; + } + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } # if switching to selectable hosts + + } elsif ( $old->machine eq '_SVC_MACHINE' ) { + # then we're switching from selectable to non-selectable + foreach my $svc_export_machine ( + qsearch('svc_export_machine', { 'exportnum' => $self->exportnum }) + ) { + $error ||= $svc_export_machine->delete; + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + } + + $error = $self->SUPER::replace(@_); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + if ( $self->machine eq '_SVC_MACHINE' and ! $self->default_machine ) { + $dbh->rollback if $oldAutoCommit; + return "no default export host selected"; } $dbh->commit or die $dbh->errstr if $oldAutoCommit; @@ -308,6 +335,13 @@ sub check { || $self->ut_domainn('machine') || $self->ut_alpha('exporttype') ; + + if ( $self->machine eq '_SVC_MACHINE' ) { + $error ||= $self->ut_numbern('default_machine') + } else { + $self->set('default_machine', ''); + } + return $error if $error; $self->nodomain =~ /^(Y?)$/ or return "Illegal nodomain: ". $self->nodomain; @@ -471,7 +505,9 @@ sub _rebless { $self; } -=item svc_machine +=item svc_machine SVC_X + +Return the export hostname for SVC_X. =cut @@ -483,14 +519,33 @@ sub svc_machine { my $svc_export_machine = qsearchs('svc_export_machine', { 'svcnum' => $svc_x->svcnum, 'exportnum' => $self->exportnum, - }) - #would only happen if you add this export to existing services without a - #machine set then try to run exports without setting it... right? - or die "No hostname selected for ".($self->exportname || $self->exporttype); + }); + + if (!$svc_export_machine) { + warn "No hostname selected for ".($self->exportname || $self->exporttype); + return $self->default_export_machine->machine; + } return $svc_export_machine->part_export_machine->machine; } +=item default_export_machine + +Return the default export hostname for this export. + +=cut + +sub default_export_machine { + my $self = shift; + my $machinenum = $self->default_machine; + if ( $machinenum ) { + my $default_machine = FS::part_export_machine->by_key($machinenum); + return $default_machine->machine if $default_machine; + } + # this should not happen + die "no default export hostname for export ".$self->exportnum; +} + #these should probably all go away, just let the subclasses define em =item export_insert SVC_OBJECT @@ -601,6 +656,17 @@ DEFAULTSREF is a hashref with the same keys where true values indicate the setting is a default (and thus can be displayed in the UI with less emphasis, or hidden by default). +=item actions + +Adds one or more "action" links to the export's display in +browse/part_export.cgi. Should return pairs of values. The first is +the link label; the second is the Mason path to a document to load. +The document will show in a popup. + +=cut + +sub actions { } + =cut =item weight @@ -630,6 +696,10 @@ sub info { #default fallbacks... FS::part_export::DID_Common ? sub get_dids_can_tollfree { 0; } +sub get_dids_can_manual { 0; } +sub get_dids_can_edit { 0; } #don't use without can_manual, otherwise the + # DID selector provisions a new number from + # inventory each edit sub get_dids_npa_select { 1; } =back @@ -688,6 +758,55 @@ sub _upgrade_data { #class method $error = $opt->replace; die $error if $error; } + # for exports that have selectable hostnames, make sure all services + # have a hostname selected + foreach my $part_export ( + qsearch('part_export', { 'machine' => '_SVC_MACHINE' }) + ) { + + my $exportnum = $part_export->exportnum; + my $machinenum = $part_export->default_machine; + if (!$machinenum) { + my ($first) = $part_export->part_export_machine; + if (!$first) { + # user intervention really is required. + die "Export $exportnum has no hostname options defined.\n". + "You must correct this before upgrading.\n"; + } + # warn about this, because we might not choose the right one + warn "Export $exportnum (". $part_export->exporttype. + ") has no default hostname. Setting to ".$first->machine."\n"; + $machinenum = $first->machinenum; + $part_export->set('default_machine', $machinenum); + my $error = $part_export->replace; + die $error if $error; + } + + # the service belongs to a service def that uses this export + # and there is not a hostname selected for this export for that service + my $join = ' JOIN export_svc USING ( svcpart )'. + ' LEFT JOIN svc_export_machine'. + ' ON ( cust_svc.svcnum = svc_export_machine.svcnum'. + ' AND export_svc.exportnum = svc_export_machine.exportnum )'; + + my @svcs = qsearch( { + 'select' => 'cust_svc.*', + 'table' => 'cust_svc', + 'addl_from' => $join, + 'extra_sql' => ' WHERE svcexportmachinenum IS NULL'. + ' AND export_svc.exportnum = '.$part_export->exportnum, + } ); + foreach my $cust_svc (@svcs) { + my $svc_export_machine = FS::svc_export_machine->new({ + 'exportnum' => $exportnum, + 'machinenum' => $machinenum, + 'svcnum' => $cust_svc->svcnum, + }); + my $error = $svc_export_machine->insert; + die $error if $error; + } + } + # pass downstream my %exports_in_use; $exports_in_use{ref $_} = 1 foreach qsearch('part_export', {}); diff --git a/FS/FS/part_export/acct_xmlrpc.pm b/FS/FS/part_export/acct_xmlrpc.pm index a493f5206..acd7ffe5d 100644 --- a/FS/FS/part_export/acct_xmlrpc.pm +++ b/FS/FS/part_export/acct_xmlrpc.pm @@ -131,10 +131,10 @@ sub _export_command { sub _export_replace { my( $self, $new, $old ) = (shift, shift, shift); - my $method = $self->option($action.'_method'); + my $method = $self->option('replace_method'); return '' if $method =~ /^\s*$/; - my @params = split("\n", $self->option($action.'_params') ); + my @params = split("\n", $self->option('replace_params') ); my( @x_param ) = (); my( %x_struct ) = (); diff --git a/FS/FS/part_export/dma_radiusmanager.pm b/FS/FS/part_export/dma_radiusmanager.pm deleted file mode 100644 index d46a996ca..000000000 --- a/FS/FS/part_export/dma_radiusmanager.pm +++ /dev/null @@ -1,355 +0,0 @@ -package FS::part_export::dma_radiusmanager; - -use strict; -use vars qw($DEBUG %info %options); -use base 'FS::part_export'; -use FS::part_svc; -use FS::svc_acct; -use FS::radius_group; -use Tie::IxHash; -use Digest::MD5 'md5_hex'; - -use Locale::Country qw(code2country); -use Locale::SubCountry; -use Date::Format 'time2str'; - -tie %options, 'Tie::IxHash', - 'dbname' => { label=>'Database name', default=>'radius' }, - 'username' => { label=>'Database username' }, - 'password' => { label=>'Database password' }, - 'manager' => { label=>'Manager name' }, - 'template_name' => { label=>'Template service name' }, - 'service_prefix' => { label=>'Service name prefix' }, - 'debug' => { label=>'Enable debugging', type=>'checkbox' }, -; - -%info = ( - 'svc' => 'svc_acct', - 'desc' => 'Export to DMA Radius Manager', - 'options' => \%options, - 'nodomain' => 'Y', - 'notes' => '', #XXX -); - -$DEBUG = 0; - -sub connect { - my $self = shift; - my $datasrc = 'dbi:mysql:host='.$self->machine. - ':database='.$self->option('dbname'); - DBI->connect( - $datasrc, - $self->option('username'), - $self->option('password'), - { AutoCommit => 0 } - ) or die $DBI::errstr; -} - -sub export_insert { my $self = shift; $self->dma_rm_queue('insert', @_) } -sub export_delete { my $self = shift; $self->dma_rm_queue('delete', @_) } -sub export_replace { my $self = shift; $self->dma_rm_queue('replace', @_) } -sub export_suspend { my $self = shift; $self->dma_rm_queue('suspend', @_) } -sub export_unsuspend { my $self = shift; $self->dma_rm_queue('unsuspend', @_) } - -sub dma_rm_queue { - my ($self, $action, $svc_acct, $old) = @_; - - my $svcnum = $svc_acct->svcnum; - - my $cust_pkg = $svc_acct->cust_svc->cust_pkg; - my $cust_main = $cust_pkg->cust_main; - my $location = $cust_pkg->cust_location; - - my $address = $location->address1; - $address .= ' '.$location->address2 if $location->address2; - my $country = code2country($location->country); - my $lsc = Locale::SubCountry->new($location->country); - my $state = $lsc->full_name($location->state) if defined($lsc); - - my %params = ( - # for the remote side - username => $svc_acct->username, - password => md5_hex($svc_acct->_password), - groupid => $self->option('groupid'), - enableuser => 1, - firstname => $cust_main->first, - lastname => $cust_main->last, - company => $cust_main->company, - phone => ($cust_main->daytime || $cust_main->night), - mobile => $cust_main->mobile, - address => $location->address1, # address2? - city => $location->city, - state => $state, #full name - zip => $location->zip, - country => $country, #full name - gpslat => $location->latitude, - gpslong => $location->longitude, - comment => 'svcnum'.$svcnum, - createdby => $self->option('manager'), - owner => $self->option('manager'), - email => $cust_main->invoicing_list_emailonly_scalar, - - # used internally by the export - exportnum => $self->exportnum, - svcnum => $svcnum, - action => $action, - svcpart => $svc_acct->cust_svc->svcpart, - _password => $svc_acct->_password, - ); - if ( $action eq 'replace' ) { - $params{'old_username'} = $old->username; - $params{'old_password'} = $old->_password; - } - my $queue = FS::queue->new({ - 'svcnum' => $svcnum, - 'job' => "FS::part_export::dma_radiusmanager::dma_rm_action", - }); - $queue->insert(%params); -} - -sub dma_rm_action { - my %params = @_; - my $svcnum = delete $params{svcnum}; - my $action = delete $params{action}; - my $svcpart = delete $params{svcpart}; - my $exportnum = delete $params{exportnum}; - - my $username = $params{username}; - my $password = delete $params{_password}; - - my $self = FS::part_export->by_key($exportnum); - my $dbh = $self->connect; - local $DEBUG = 1 if $self->option('debug'); - - # export the part_svc if needed, and get its srvid - my $part_svc = FS::part_svc->by_key($svcpart); - my $srvid = $self->export_part_svc($part_svc, $dbh); # dies on error - $params{srvid} = $srvid; - - if ( $action eq 'insert' ) { - $params{'createdon'} = time2str('%Y-%m-%d', time); - $params{'expiration'} = time2str('%Y-%m-%d', time); - warn "rm_users: inserting svcnum$svcnum\n" if $DEBUG; - my $sth = $dbh->prepare( 'INSERT INTO rm_users ( '. - join(', ', keys(%params)). - ') VALUES ('. - join(', ', ('?') x keys(%params)). - ')' - ); - $sth->execute(values(%params)) or die $dbh->errstr; - - # minor false laziness w/ sqlradius_insert - warn "radcheck: inserting $username\n" if $DEBUG; - $sth = $dbh->prepare( 'INSERT INTO radcheck ( - username, attribute, op, value - ) VALUES (?, ?, ?, ?)' ); - $sth->execute( - $username, - 'Cleartext-Password', - ':=', # :=( - $password, - ) or die $dbh->errstr; - - $sth->execute( - $username, - 'Simultaneous-Use', - ':=', - 1, # should this be an option? - ) or die $dbh->errstr; - # also, we don't support exporting any other radius attrs... - # those should go in 'custattr' if we need them - } elsif ( $action eq 'replace' ) { - - my $old_username = delete $params{old_username}; - my $old_password = delete $params{old_password}; - # svcnum is invariant and on the remote side, so we don't need any - # of the old fields to do this - warn "rm_users: updating svcnum$svcnum\n" if $DEBUG; - my $sth = $dbh->prepare( 'UPDATE rm_users SET '. - join(', ', map { "$_ = ?" } keys(%params)). - ' WHERE comment = ?' - ); - $sth->execute(values(%params), $params{comment}) or die $dbh->errstr; - # except for username/password changes - if ( $old_password ne $password ) { - warn "radcheck: changing password for $old_username\n" if $DEBUG; - $sth = $dbh->prepare( 'UPDATE radcheck SET value = ? '. - 'WHERE username = ? and attribute = \'Cleartext-Password\'' - ); - $sth->execute($password, $old_username) or die $dbh->errstr; - } - if ( $old_username ne $username ) { - warn "radcheck: changing username $old_username to $username\n" - if $DEBUG; - $sth = $dbh->prepare( 'UPDATE radcheck SET username = ? '. - 'WHERE username = ?' - ); - $sth->execute($username, $old_username) or die $dbh->errstr; - } - - } elsif ( $action eq 'suspend' ) { - - # this is sufficient - warn "rm_users: disabling svcnum#$svcnum\n" if $DEBUG; - my $sth = $dbh->prepare( 'UPDATE rm_users SET enableuser = 0 '. - 'WHERE comment = ?' - ); - $sth->execute($params{comment}) or die $dbh->errstr; - - } elsif ( $action eq 'unsuspend' ) { - - warn "rm_users: enabling svcnum#$svcnum\n" if $DEBUG; - my $sth = $dbh->prepare( 'UPDATE rm_users SET enableuser = 1 '. - 'WHERE comment = ?' - ); - $sth->execute($params{comment}) or die $dbh->errstr; - - } elsif ( $action eq 'delete' ) { - - warn "rm_users: deleting svcnum#$svcnum\n" if $DEBUG; - my $sth = $dbh->prepare( 'DELETE FROM rm_users WHERE comment = ?' ); - $sth->execute($params{comment}) or die $dbh->errstr; - - warn "radcheck: deleting $username\n" if $DEBUG; - $sth = $dbh->prepare( 'DELETE FROM radcheck WHERE username = ?' ); - $sth->execute($username) or die $dbh->errstr; - - # if this were smarter it would also delete the rm_services record - # if it was no longer in use, but that's not really necessary - } - - $dbh->commit; - ''; -} - -=item export_part_svc PART_SVC DBH - -Query Radius Manager for a service definition matching the name of -PART_SVC (optionally with a prefix defined in the export options). -If there is one, update it to match the attributes of PART_SVC; if -not, create one. Then return its srvid. - -=cut - -sub export_part_svc { - my ($self, $part_svc, $dbh) = @_; - - # if $dbh exists, use the existing transaction - # otherwise create our own and commit when finished - my $commit = 0; - if (!$dbh) { - $dbh = $self->connect; - $commit = 1; - } - - my $name = $self->option('service_prefix').$part_svc->svc; - - my %params = ( - 'srvname' => $name, - 'enableservice' => 1, - 'nextsrvid' => -1, - 'dailynextsrvid' => -1, - # force price-related fields to zero - 'unitprice' => 0, - 'unitpriceadd' => 0, - 'unitpricetax' => 0, - 'unitpriceaddtax' => 0, - ); - my @fixed_groups; - # use speed settings from fixed usergroups configured on this part_svc - if ( my $psc = $part_svc->part_svc_column('usergroup') ) { - # each part_svc really should only have one fixed group with non-null - # speed settings, but go by priority order for consistency - @fixed_groups = - sort { $a->priority <=> $b->priority } - grep { $_ } - map { FS::radius_group->by_key($_) } - split(/\s*,\s*/, $psc->columnvalue); - } # otherwise there are no fixed groups, so leave speed empty - - foreach (qw(down up)) { - my $speed = "speed_$_"; - foreach my $group (@fixed_groups) { - if ( ($group->$speed || 0) > 0 ) { - $params{$_.'rate'} = $group->$speed; - last; - } - } - } - # anything else we need here? poolname, maybe? - - warn "rm_services: looking for '$name'\n" if $DEBUG; - my $sth = $dbh->prepare( - 'SELECT srvid FROM rm_services WHERE srvname = ? AND enableservice = 1' - ); - $sth->execute($name) or die $dbh->errstr; - if ( $sth->rows > 1 ) { - die "Multiple services with name '$name' found in Radius Manager.\n"; - - } elsif ( $sth->rows == 0 ) { - # leave this blank to disable creating new service defs - my $template_name = $self->option('template_name'); - - die "Can't create a new service profile--no template service specified.\n" - unless $template_name; - - warn "rm_services: fetching template '$template_name'\n" if $DEBUG; - $sth = $dbh->prepare('SELECT * FROM rm_services WHERE srvname = ? LIMIT 1'); - $sth->execute($template_name); - die "Can't create a new service profile--template service ". - "'$template_name' not found.\n" unless $sth->rows == 1; - my $template = $sth->fetchrow_hashref; - %params = (%$template, %params); - - # get the next available srvid - $sth = $dbh->prepare('SELECT MAX(srvid) FROM rm_services'); - $sth->execute or die $dbh->errstr; - my $srvid; - if ( $sth->rows ) { - $srvid = $sth->fetchrow_arrayref->[0] + 1; - } - $params{'srvid'} = $srvid; - - # create a new one based on the template - warn "rm_services: inserting '$name' as srvid#$srvid\n" if $DEBUG; - $sth = $dbh->prepare( - 'INSERT INTO rm_services ('.join(', ', keys %params). - ') VALUES ('.join(', ', map {'?'} keys %params).')' - ); - $sth->execute(values(%params)) or die $dbh->errstr; - # also link it to all the managers allowed on the template service - warn "rm_services: linking to manager\n" if $DEBUG; - $sth = $dbh->prepare( - 'INSERT INTO rm_allowedmanagers (srvid, managername) '. - 'SELECT ?, managername FROM rm_allowedmanagers WHERE srvid = ?' - ); - $sth->execute($srvid, $template->{srvid}) or die $dbh->errstr; - # and the same for NASes - warn "rm_services: linking to nas\n" if $DEBUG; - $sth = $dbh->prepare( - 'INSERT INTO rm_allowednases (srvid, nasid) '. - 'SELECT ?, nasid FROM rm_allowednases WHERE srvid = ?' - ); - $sth->execute($srvid, $template->{srvid}) or die $dbh->errstr; - - $dbh->commit if $commit; - return $srvid; - - } else { # $sth->rows == 1, it already exists - - my $row = $sth->fetchrow_arrayref; - my $srvid = $row->[0]; - warn "rm_services: updating srvid#$srvid\n" if $DEBUG; - $sth = $dbh->prepare( - 'UPDATE rm_services SET '.join(', ', map {"$_ = ?"} keys %params) . - ' WHERE srvid = ?' - ); - $sth->execute(values(%params), $srvid) or die $dbh->errstr; - - $dbh->commit if $commit; - return $srvid; - - } -} - -1; diff --git a/FS/FS/part_export/fibernetics_did.pm b/FS/FS/part_export/fibernetics_did.pm index fb0378550..a51457a03 100644 --- a/FS/FS/part_export/fibernetics_did.pm +++ b/FS/FS/part_export/fibernetics_did.pm @@ -28,6 +28,8 @@ tie my %options, 'Tie::IxHash', sub rebless { shift; } sub get_dids_can_tollfree { 0; }; +sub get_dids_can_manual { 1; }; +sub get_dids_can_edit { 1; }; sub get_dids_npa_select { 0; }; # i guess we could get em from the API, but since its returning states without diff --git a/FS/FS/part_export/http_status.pm b/FS/FS/part_export/http_status.pm index 6fbd3fbe6..80139e776 100644 --- a/FS/FS/part_export/http_status.pm +++ b/FS/FS/part_export/http_status.pm @@ -3,28 +3,53 @@ use base qw( FS::part_export ); use strict; use warnings; -use vars qw( %info ); +use vars qw( %info $DEBUG ); +use URI::Escape; use LWP::UserAgent; use HTTP::Request::Common; +use Email::Valid; tie my %options, 'Tie::IxHash', 'url' => { label => 'URL', }, + 'blacklist_add_url' => { label => 'Optional blacklist add URL', }, + 'blacklist_del_url' => { label => 'Optional blacklist delete URL', }, + 'whitelist_add_url' => { label => 'Optional whitelist add URL', }, + 'whitelist_del_url' => { label => 'Optional whitelist delete URL', }, + 'vacation_add_url' => { label => 'Optional vacation message add URL', }, + 'vacation_del_url' => { label => 'Optional vacation message delete URL', }, + #'user' => { label => 'Username', default=>'' }, #'password' => { label => 'Password', default => '' }, ; %info = ( - 'svc' => 'svc_dsl', + 'svc' => [ 'svc_acct', 'svc_dsl', ], 'desc' => 'Retrieve status information via HTTP or HTTPS', 'options' => \%options, 'no_machine' => 1, 'notes' => <<'END' Fields from the service can be substituted in the URL as $field. + +Optionally, spam black/whitelist addresees and a vacation message may be +modified via HTTP or HTTPS as well. END ); +$DEBUG = 1; + sub rebless { shift; } +our %addl_fields = ( + 'svc_acct' => [qw( email ) ], + 'svc_dsl' => [qw( gateway_access_or_phonenum ) ], +); + +#some NOPs for required subroutines, to avoid throwing the exceptions in the +# part_export.pm fallbacks +sub _export_insert { '' }; +sub _export_replace { '' }; +sub _export_delete { '' }; + sub export_getstatus { my( $self, $svc_x, $htmlref, $hashref ) = @_; @@ -34,10 +59,97 @@ sub export_getstatus { { no strict 'refs'; ${$_} = $svc_x->getfield($_) foreach $svc_x->fields; - if ( $svc_x->table eq 'svc_dsl' ) { - ${$_} = $svc_x->$_() foreach (qw( gateway_access_or_phonenum )); + ${$_} = $svc_x->$_() foreach @{ $addl_fields{ $svc_x->table } }; + $url = eval(qq("$urlopt")); + } + + my $req = HTTP::Request::Common::GET( $url ); + my $ua = LWP::UserAgent->new; + my $response = $ua->request($req); + + if ( $svc_x->table eq 'svc_dsl' ) { + + $$htmlref = $response->is_error ? $response->error_as_HTML + : $response->content; + + #hash data not yet implemented for svc_dsl + + } elsif ( $svc_x->table eq 'svc_acct' ) { + + #this whole section is rather specific to fibernetics and should be an + # option or callback or something + + # to,from,wb_value + + use Text::CSV_XS; + my $csv = Text::CSV_XS->new; + + my @lines = split("\n", $response->content); + pop @lines if $lines[-1] eq ''; + my $header = shift @lines; + $csv->parse($header) or return; + my @header = $csv->fields; + + while ( my $line = shift @lines ) { + $csv->parse($line) or next; + my @fields = $csv->fields; + my %hash = map { $_ => shift(@fields) } @header; + + if ( defined $hash{'wb_value'} ) { + if ( $hash{'wb_value'} =~ /^[WA]/i ) { #Whitelist/Allow + push @{ $hashref->{'whitelist'} }, $hash{'from'}; + } else { # if ( $hash{'wb_value'} =~ /^[BD]/i ) { #Blacklist/Deny + push @{ $hashref->{'blacklist'} }, $hash{'from'}; + } + } + + for (qw( created enddate )) { + $hash{$_} = '' if $hash{$_} =~ /^0000-/; + $hash{$_} = (split(' ', $hash{$_}))[0]; + } + + next unless $hash{'active'}; + $hashref->{"vacation_$_"} = $hash{$_} || '' + foreach qw( active subject body created enddate ); + } + } #else { die 'guru meditation #295'; } + +} + +sub export_setstatus_listadd { + my( $self, $svc_x, $hr ) = @_; + $self->export_setstatus_listX( $svc_x, 'add', $hr->{list}, $hr->{address} ); +} + +sub export_setstatus_listdel { + my( $self, $svc_x, $hr ) = @_; + $self->export_setstatus_listX( $svc_x, 'del', $hr->{list}, $hr->{address} ); +} + +sub export_setstatus_listX { + my( $self, $svc_x, $action, $list, $address ) = @_; + + my $option; + if ( $list =~ /^[WA]/i ) { #Whitelist/Allow + $option = 'whitelist_'; + } else { # if ( $hash{'wb_value'} =~ /^[BD]/i ) { #Blacklist/Deny + $option = 'blacklist_'; + } + $option .= $action. '_url'; + + $address = Email::Valid->address($address) + or die "address failed $Email::Valid::Details check.\n"; + + #some false laziness w/export_getstatus above + my $url; + my $urlopt = $self->option($option) or return; #DIFF + no strict 'vars'; + { + no strict 'refs'; + ${$_} = $svc_x->getfield($_) foreach $svc_x->fields; + ${$_} = $svc_x->$_() foreach @{ $addl_fields{ $svc_x->table } }; $url = eval(qq("$urlopt")); } @@ -45,11 +157,56 @@ sub export_getstatus { my $ua = LWP::UserAgent->new; my $response = $ua->request($req); - $$htmlref = $response->is_error ? $response->error_as_HTML - : $response->content; + die $response->code. ' '. $response->message if $response->is_error; - #hash data note yet implemented for this status export +} +sub export_setstatus_vacationadd { + my( $self, $svc_x, $hr ) = @_; + $self->export_setstatus_vacationX( $svc_x, 'add', $hr ); } +sub export_setstatus_vacationdel { + my( $self, $svc_x, $hr ) = @_; + $self->export_setstatus_vacationX( $svc_x, 'del', $hr ); +} + +sub export_setstatus_vacationX { + my( $self, $svc_x, $action, $hr ) = @_; + + my $option = 'vacation_'. $action. '_url'; + + my $subject = uri_escape($hr->{subject}); + my $body = uri_escape($hr->{body}); + for (qw( created enddate )) { + if ( $hr->{$_} =~ /^(\d{4}-\d{2}-\d{2})$/ ) { + $hr->{$_} = $1; + } else { + $hr->{$_} = ''; + } + } + my $created = $hr->{created}; + my $enddate = $hr->{enddate}; + + #some false laziness w/export_getstatus above + my $url; + my $urlopt = $self->option($option) or return; #DIFF + no strict 'vars'; + { + no strict 'refs'; + ${$_} = $svc_x->getfield($_) foreach $svc_x->fields; + ${$_} = $svc_x->$_() foreach @{ $addl_fields{ $svc_x->table } }; + $url = eval(qq("$urlopt")); + } + + my $req = HTTP::Request::Common::GET( $url ); + my $ua = LWP::UserAgent->new; + my $response = $ua->request($req); + + die $response->code. ' '. $response->message if $response->is_error; + +} + +1; + 1; diff --git a/FS/FS/part_export/huawei_hlr.pm b/FS/FS/part_export/huawei_hlr.pm new file mode 100644 index 000000000..007981880 --- /dev/null +++ b/FS/FS/part_export/huawei_hlr.pm @@ -0,0 +1,340 @@ +package FS::part_export::huawei_hlr; + +use vars qw(@ISA %info $DEBUG $CACHE); +use Tie::IxHash; +use FS::Record qw(qsearch qsearchs dbh); +use FS::part_export; +use FS::svc_phone; +use FS::inventory_class; +use FS::inventory_item; +use IO::Socket::INET; +use Data::Dumper; +use MIME::Base64 qw(decode_base64); +use Storable qw(thaw); + +use strict; + +$DEBUG = 0; +@ISA = qw(FS::part_export); + +tie my %options, 'Tie::IxHash', + 'opname' => { label=>'Operator login' }, + 'pwd' => { label=>'Operator password' }, + 'tplid' => { label=>'Template number' }, + 'hlrsn' => { label=>'HLR serial number' }, + 'k4sno' => { label=>'K4 serial number' }, + 'cardtype' => { label => 'Card type', + type => 'select', + options=> ['SIM', 'USIM'] + }, + 'alg' => { label => 'Authentication algorithm', + type => 'select', + options=> ['COMP128_1', + 'COMP128_2', + 'COMP128_3', + 'MILENAGE' ], + }, + 'opcvalue' => { label=>'OPC value (for MILENAGE only)' }, + 'opsno' => { label=>'OP serial number (for MILENAGE only)' }, + 'timeout' => { label=>'Timeout (seconds)', default => 120 }, + 'debug' => { label=>'Enable debugging', type=>'checkbox' }, +; + +%info = ( + 'svc' => 'svc_phone', + 'desc' => 'Provision mobile phone service to Huawei HLR9820', + 'options' => \%options, + 'notes' => <<'END' +Connects to a Huawei Subscriber Management Unit via TCP and configures mobile +phone services according to a template. The sim_imsi field must be +set on the service, and the template must exist. +END +); + +sub actions { + 'Import SIMs' => 'misc/part_export/huawei_hlr-import_sim.html' +} + +sub _export_insert { + my( $self, $svc_phone ) = (shift, shift); + # svc_phone::check should ensure phonenum and sim_imsi are numeric + my @command = ( + IMSI => '"'.$svc_phone->sim_imsi.'"', + ISDN => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"', + TPLID => $self->option('tplid'), + ); + unshift @command, 'HLRSN', $self->option('hlrsn') + if $self->option('hlrsn'); + unshift @command, 'ADD TPLSUB'; + my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command); + ref($err_or_queue) ? '' : $err_or_queue; +} + +sub _export_replace { + my( $self, $new, $old ) = @_; + my $depend_jobnum; + if ( $new->sim_imsi ne $old->sim_imsi ) { + my @command = ( + 'MOD IMSI', + ISDN => '"'.$old->countrycode.$old->phonenum.'"', + IMSI => '"'.$old->sim_imsi.'"', + NEWIMSI => '"'.$new->sim_imsi.'"', + ); + my $err_or_queue = $self->queue_command($new->svcnum, @command); + return $err_or_queue unless ref $err_or_queue; + $depend_jobnum = $err_or_queue->jobnum; + } + if ( $new->countrycode ne $old->countrycode or + $new->phonenum ne $old->phonenum ) { + my @command = ( + 'MOD ISDN', + ISDN => '"'.$old->countrycode.$old->phonenum.'"', + NEWISDN => '"'.$new->countrycode.$new->phonenum.'"', + ); + my $err_or_queue = $self->queue_command($new->svcnum, @command); + return $err_or_queue unless ref $err_or_queue; + if ( $depend_jobnum ) { + my $error = $err_or_queue->depend_insert($depend_jobnum); + return $error if $error; + } + } + # no other svc_phone changes need to be exported + ''; +} + +sub _export_suspend { + my( $self, $svc_phone ) = (shift, shift); + $self->_export_lock($svc_phone, 'TRUE'); +} + +sub _export_unsuspend { + my( $self, $svc_phone ) = (shift, shift); + $self->_export_lock($svc_phone, 'FALSE'); +} + +sub _export_lock { + my ($self, $svc_phone, $lockstate) = @_; + # XXX I'm not sure this actually suspends. Need to test it. + my @command = ( + 'MOD LCK', + IMSI => '"'.$svc_phone->sim_imsi.'"', + ISDN => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"', + IC => $lockstate, + OC => $lockstate, + GPRSLOCK=> $lockstate, + ); + my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command); + ref($err_or_queue) ? '' : $err_or_queue; +} + +sub _export_delete { + my( $self, $svc_phone ) = (shift, shift); + my @command = ( + 'RMV SUB', + #IMSI => '"'.$svc_phone->sim_imsi.'"', + ISDN => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"', + ); + my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command); + ref($err_or_queue) ? '' : $err_or_queue; +} + +sub queue_command { + my ($self, $svcnum, @command) = @_; + my $queue = FS::queue->new({ + svcnum => $svcnum, + job => 'FS::part_export::huawei_hlr::run_command', + }); + $queue->insert($self->exportnum, @command) || $queue; +} + +sub run_command { + my ($exportnum, @command) = @_; + my $self = FS::part_export->by_key($exportnum); + my $socket = $self->login; + my $result = $self->command($socket, @command); + $self->logout($socket); + $socket->close; + die $result->{error} if $result->{error}; + ''; +} + +sub login { + my $self = shift; + local $DEBUG = $self->option('debug') || 0; + # Send a command to the SMU. + # The caller is responsible for quoting string parameters. + my %socket_param = ( + PeerAddr => $self->machine, + PeerPort => 7777, + Proto => 'tcp', + Timeout => ($self->option('timeout') || 30), + ); + warn "Connecting to ".$self->machine."...\n" if $DEBUG; + warn Dumper(\%socket_param) if $DEBUG; + my $socket = IO::Socket::INET->new(%socket_param) + or die "Failed to connect: $!\n"; + + warn 'Logging in as "'.$self->option('opname').".\"\n" if $DEBUG; + my @login_param = ( + OPNAME => '"'.$self->option('opname').'"', + PWD => '"'.$self->option('pwd').'"', + ); + if ($self->option('HLRSN')) { + unshift @login_param, 'HLRSN', $self->option('HLRSN'); + } + my $login_result = $self->command($socket, 'LGI', @login_param); + die $login_result->{error} if $login_result->{error}; + return $socket; +} + +sub logout { + warn "Logging out.\n" if $DEBUG; + my $self = shift; + my ($socket) = @_; + $self->command($socket, 'LGO'); + $socket->close; +} + +sub command { + my $self = shift; + my ($socket, $command, @param) = @_; + my $string = $command . ':'; + while (@param) { + $string .= shift(@param) . '=' . shift(@param); + $string .= ',' if @param; + } + $string .= "\n;"; + my @result; + eval { # timeout + local $SIG{ALRM} = sub { die "timeout\n" }; + alarm ($self->option('timeout') || 120); + warn "Sending to server:\n$string\n\n" if $DEBUG; + $socket->print($string); + warn "Received:\n"; + my $line; + local $/ = "\r\n"; + do { + $line = $socket->getline(); + warn $line if $DEBUG; + chomp $line; + push @result, $line if length($line); + } until ( $line =~ /^---\s*END$/ or $socket->eof ); + alarm 0; + }; + my %return; + if ( $@ eq "timeout\n" ) { + return { error => 'request timed out' }; + } elsif ( $@ ) { + return { error => $@ }; + } else { + #+++ HLR9820