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
+
This export either always succeeds, always fails (returning an error),
+or always dies, according to the "Result" option. It does nothing else; the
+purpose is purely to simulate success or failure within an export module.
+
The checkbox options can be used to turn the export off for certain
+actions, if this is needed.
+END
+);
+
+sub export_insert {
+ my $self = shift;
+ $self->run(@_) if $self->option('insert');
+}
+
+sub export_delete {
+ my $self = shift;
+ $self->run(@_) if $self->option('delete');
+}
+
+sub export_replace {
+ my $self = shift;
+ $self->run(@_) if $self->option('replace');
+}
+
+sub export_suspend {
+ my $self = shift;
+ $self->run(@_) if $self->option('suspend');
+}
+
+sub export_unsuspend {
+ my $self = shift;
+ $self->run(@_) if $self->option('unsuspend');
+}
+
+sub run {
+ my $self = shift;
+ my $svc_x = shift;
+ my $result = $self->option('result');
+ if ( $result eq 'failure' ) {
+ return $self->option('errormsg');
+ } elsif ( $result eq 'exception' ) {
+ die $self->option('errormsg');
+ } else {
+ return '';
+ }
+}
+
+1;
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 6e7f8f87e..e788269f7 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -1,7 +1,8 @@
package FS::part_pkg;
+use base qw( FS::m2m_Common FS::o2m_Common FS::option_Common );
use strict;
-use vars qw( @ISA %plans $DEBUG $setup_hack $skip_pkg_svc_hack );
+use vars qw( %plans $DEBUG $setup_hack $skip_pkg_svc_hack );
use Carp qw(carp cluck confess);
use Scalar::Util qw( blessed );
use Time::Local qw( timelocal_nocheck );
@@ -16,14 +17,15 @@ use FS::type_pkgs;
use FS::part_pkg_option;
use FS::pkg_class;
use FS::agent;
+use FS::part_pkg_msgcat;
use FS::part_pkg_taxrate;
use FS::part_pkg_taxoverride;
use FS::part_pkg_taxproduct;
use FS::part_pkg_link;
use FS::part_pkg_discount;
+use FS::part_pkg_usage;
use FS::part_pkg_vendor;
-@ISA = qw( FS::m2m_Common FS::option_Common );
$DEBUG = 0;
$setup_hack = 0;
$skip_pkg_svc_hack = 0;
@@ -364,7 +366,7 @@ sub replace {
? shift
: { @_ };
- $options->{options} = {} unless defined($options->{options});
+ $options->{options} = { $old->options } unless defined($options->{options});
warn "FS::part_pkg::replace called on $new to replace $old with options".
join(', ', map "$_ => ". $options->{$_}, keys %$options)
@@ -446,53 +448,55 @@ sub replace {
}
warn " replacing pkg_svc records" if $DEBUG;
- my $pkg_svc = $options->{'pkg_svc'} || {};
+ my $pkg_svc = $options->{'pkg_svc'};
my $hidden_svc = $options->{'hidden_svc'} || {};
- foreach my $part_svc ( qsearch('part_svc', {} ) ) {
- my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
- my $hidden = $hidden_svc->{$part_svc->svcpart} || '';
- my $primary_svc =
- ( defined($options->{'primary_svc'}) && $options->{'primary_svc'}
- && $options->{'primary_svc'} == $part_svc->svcpart
- )
- ? 'Y'
- : '';
+ if ( $pkg_svc ) { # if it wasn't passed, don't change existing pkg_svcs
+ foreach my $part_svc ( qsearch('part_svc', {} ) ) {
+ my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
+ my $hidden = $hidden_svc->{$part_svc->svcpart} || '';
+ my $primary_svc =
+ ( defined($options->{'primary_svc'}) && $options->{'primary_svc'}
+ && $options->{'primary_svc'} == $part_svc->svcpart
+ )
+ ? 'Y'
+ : '';
- my $old_pkg_svc = qsearchs('pkg_svc', {
- 'pkgpart' => $old->pkgpart,
- 'svcpart' => $part_svc->svcpart,
+ my $old_pkg_svc = qsearchs('pkg_svc', {
+ 'pkgpart' => $old->pkgpart,
+ 'svcpart' => $part_svc->svcpart,
+ }
+ );
+ my $old_quantity = 0;
+ my $old_primary_svc = '';
+ my $old_hidden = '';
+ if ( $old_pkg_svc ) {
+ $old_quantity = $old_pkg_svc->quantity;
+ $old_primary_svc = $old_pkg_svc->primary_svc
+ if $old_pkg_svc->dbdef_table->column('primary_svc'); # is this needed?
+ $old_hidden = $old_pkg_svc->hidden;
}
- );
- my $old_quantity = 0;
- my $old_primary_svc = '';
- my $old_hidden = '';
- if ( $old_pkg_svc ) {
- $old_quantity = $old_pkg_svc->quantity;
- $old_primary_svc = $old_pkg_svc->primary_svc
- if $old_pkg_svc->dbdef_table->column('primary_svc'); # is this needed?
- $old_hidden = $old_pkg_svc->hidden;
- }
-
- next unless $old_quantity != $quantity ||
- $old_primary_svc ne $primary_svc ||
- $old_hidden ne $hidden;
-
- my $new_pkg_svc = new FS::pkg_svc( {
- 'pkgsvcnum' => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ),
- 'pkgpart' => $new->pkgpart,
- 'svcpart' => $part_svc->svcpart,
- 'quantity' => $quantity,
- 'primary_svc' => $primary_svc,
- 'hidden' => $hidden,
- } );
- my $error = $old_pkg_svc
- ? $new_pkg_svc->replace($old_pkg_svc)
- : $new_pkg_svc->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
- }
+
+ next unless $old_quantity != $quantity ||
+ $old_primary_svc ne $primary_svc ||
+ $old_hidden ne $hidden;
+
+ my $new_pkg_svc = new FS::pkg_svc( {
+ 'pkgsvcnum' => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ),
+ 'pkgpart' => $new->pkgpart,
+ 'svcpart' => $part_svc->svcpart,
+ 'quantity' => $quantity,
+ 'primary_svc' => $primary_svc,
+ 'hidden' => $hidden,
+ } );
+ my $error = $old_pkg_svc
+ ? $new_pkg_svc->replace($old_pkg_svc)
+ : $new_pkg_svc->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ } #foreach $part_svc
+ } #if $options->{pkg_svc}
my @part_pkg_vendor = $old->part_pkg_vendor;
my @current_exportnum = ();
@@ -712,6 +716,35 @@ sub propagate {
join("\n", @error);
}
+=item pkg_locale LOCALE
+
+Returns a customer-viewable string representing this package for the given
+locale, from the part_pkg_msgcat table. If the given locale is empty or no
+localized string is found, returns the base pkg field.
+
+=cut
+
+sub pkg_locale {
+ my( $self, $locale ) = @_;
+ return $self->pkg unless $locale;
+ my $part_pkg_msgcat = $self->part_pkg_msgcat($locale) or return $self->pkg;
+ $part_pkg_msgcat->pkg;
+}
+
+=item part_pkg_msgcat LOCALE
+
+Like pkg_locale, but returns the FS::part_pkg_msgcat object itself.
+
+=cut
+
+sub part_pkg_msgcat {
+ my( $self, $locale ) = @_;
+ qsearchs( 'part_pkg_msgcat', {
+ pkgpart => $self->pkgpart,
+ locale => $locale,
+ });
+}
+
=item pkg_comment [ OPTION => VALUE... ]
Returns an (internal) string representing this package. Currently,
@@ -1175,6 +1208,17 @@ sub svc_part_pkg_link {
shift->_part_pkg_link('svc', @_);
}
+=item supp_part_pkg_link
+
+Returns the associated part_pkg_link records of type 'supp' (supplemental
+packages).
+
+=cut
+
+sub supp_part_pkg_link {
+ shift->_part_pkg_link('supp', @_);
+}
+
sub _part_pkg_link {
my( $self, $type ) = @_;
qsearch({ table => 'part_pkg_link',
@@ -1384,6 +1428,18 @@ sub part_pkg_discount {
qsearch('part_pkg_discount', { 'pkgpart' => $self->pkgpart });
}
+=item part_pkg_usage
+
+Returns the voice usage pools (see L) defined for
+this package.
+
+=cut
+
+sub part_pkg_usage {
+ my $self = shift;
+ qsearch('part_pkg_usage', { 'pkgpart' => $self->pkgpart });
+}
+
=item _rebless
Reblesses the object into the FS::part_pkg::PLAN class (if available), where
@@ -1439,6 +1495,29 @@ sub recur_cost_permonth {
sprintf('%.2f', $self->recur_cost / $self->freq );
}
+=item cust_bill_pkg_recur CUST_PKG
+
+Actual recurring charge for the specified customer package from customer's most
+recent invoice
+
+=cut
+
+sub cust_bill_pkg_recur {
+ my($self, $cust_pkg) = @_;
+ my $cust_bill_pkg = qsearchs({
+ 'table' => 'cust_bill_pkg',
+ 'addl_from' => 'LEFT JOIN cust_bill USING ( invnum )',
+ 'hashref' => { 'pkgnum' => $cust_pkg->pkgnum,
+ 'recur' => { op=>'>', value=>'0' },
+ },
+ 'order_by' => 'ORDER BY cust_bill._date DESC,
+ cust_bill_pkg.sdate DESC
+ LIMIT 1
+ ',
+ }) or return 0; #die "use cust_bill_pkg_recur credits with once_perinv condition";
+ $cust_bill_pkg->recur;
+}
+
=item format OPTION DATA
Returns data formatted according to the function 'format' described
diff --git a/FS/FS/part_pkg/base_delayed.pm b/FS/FS/part_pkg/base_delayed.pm
deleted file mode 100644
index c6864a692..000000000
--- a/FS/FS/part_pkg/base_delayed.pm
+++ /dev/null
@@ -1,42 +0,0 @@
-package FS::part_pkg::base_delayed;
-
-use strict;
-use vars qw(@ISA %info);
-#use FS::Record qw(qsearch qsearchs);
-use FS::part_pkg::base_rate;
-
-@ISA = qw(FS::part_pkg::base_rate);
-
-%info = (
- 'name' => 'Free (or setup fee) for X days, then base rate'.
- ' (anniversary billing)',
- 'shortname' => 'Bulk (manual from "units" option), w/intro period',
- 'inherit_fields' => [ 'global_Mixin' ],
- 'fields' => {
- 'free_days' => { 'name' => 'Initial free days',
- 'default' => 0,
- },
- 'recur_notify' => { 'name' => 'Number of days before recurring billing'.
- ' commences to notify customer. (0 means'.
- ' no warning)',
- 'default' => 0,
- },
- },
- 'fieldorder' => [ 'free_days', 'recur_notify',
- ],
- #'setup' => '\'my $d = $cust_pkg->bill || $time; $d += 86400 * \' + what.free_days.value + \'; $cust_pkg->bill($d); $cust_pkg_mod_flag=1; \' + what.setup_fee.value',
- #'recur' => 'what.recur_fee.value',
- 'weight' => 54, #&g!
-);
-
-sub calc_setup {
- my($self, $cust_pkg, $time ) = @_;
-
- my $d = $cust_pkg->bill || $time;
- $d += 86400 * $self->option('free_days');
- $cust_pkg->bill($d);
-
- $self->option('setup_fee');
-}
-
-1;
diff --git a/FS/FS/part_pkg/base_rate.pm b/FS/FS/part_pkg/base_rate.pm
deleted file mode 100644
index 43a050610..000000000
--- a/FS/FS/part_pkg/base_rate.pm
+++ /dev/null
@@ -1,97 +0,0 @@
-package FS::part_pkg::base_rate;
-
-use strict;
-use vars qw(@ISA %info);
-#use FS::Record qw(qsearch);
-use FS::part_pkg;
-
-@ISA = qw(FS::part_pkg);
-
-%info = (
- 'name' => 'Base rate (anniversary billing, Times units ordered)',
- # XXX it multiplies recurring fee by cust_pkg option "units", how to
- # express that
- 'shortname' => 'Bulk (manual from "units" option)',
- 'inherit_fields' => [ 'global_Mixin' ],
- 'fields' => {
- 'externalid' => { 'name' => 'Optional External ID',
- 'default' => '',
- },
- },
- 'fieldorder' => [ qw( externalid ) ],
- 'weight' => 52,
-);
-
-sub price_info {
- my $self = shift;
- my $conf = new FS::Conf;
- my $money_char = $conf->config('money_char') || '$';
- my $setup = $self->option('setup_fee') || 0;
- my $recur = $self->option('recur_fee', 1) || 0;
- my $str = '';
- $str = $money_char . $setup . ' one-time' if $setup;
- $str .= ', ' if ($setup && $recur);
- $str .= $money_char . $recur . ' recurring per unit ' if $recur;
- $str;
-}
-
-
-sub calc_setup {
- my($self, $cust_pkg, $sdate, $details ) = @_;
-
- my $i = 0;
- my $count = $self->option( 'additional_count', 'quiet' ) || 0;
- while ($i < $count) {
- push @$details, $self->option( 'additional_info' . $i++ );
- }
-
- $self->option('setup_fee');
-}
-
-sub calc_recur {
- my($self, $cust_pkg) = @_;
- $self->base_recur($cust_pkg);
-}
-
-sub base_recur {
- my($self, $cust_pkg) = @_;
- my $units = $cust_pkg->option('units') ? $cust_pkg->option('units') : 1 ;
- # default to 1 if not found
- sprintf("%.2f",
- ($self->option('recur_fee') * $units )
- );
-}
-
-sub calc_remain {
- my ($self, $cust_pkg, %options) = @_;
- my $time = $options{'time'} || time;
- my $next_bill = $cust_pkg->getfield('bill') || 0;
- return 0 if ! $self->base_recur($cust_pkg)
- || ! $next_bill
- || $next_bill < $time;
-
- my %sec = (
- 'h' => 3600, # 60 * 60
- 'd' => 86400, # 60 * 60 * 24
- 'w' => 604800, # 60 * 60 * 24 * 7
- 'm' => 2629744, # 60 * 60 * 24 * 365.2422 / 12
- );
-
- $self->freq =~ /^(\d+)([hdwm]?)$/
- or die 'unparsable frequency: '. $self->freq;
- my $freq_sec = $1 * $sec{$2||'m'};
- return 0 unless $freq_sec;
-
- sprintf("%.2f", $self->base_recur($cust_pkg) * ( $next_bill - $time ) / $freq_sec );
-
-}
-
-sub is_free_options {
- qw( setup_fee recur_fee );
-}
-
-sub is_prepaid {
- 0; #no, we're postpaid
-}
-
-1;
diff --git a/FS/FS/part_pkg/delayed_Mixin.pm b/FS/FS/part_pkg/delayed_Mixin.pm
index 83e543a4f..ab53bda06 100644
--- a/FS/FS/part_pkg/delayed_Mixin.pm
+++ b/FS/FS/part_pkg/delayed_Mixin.pm
@@ -23,7 +23,8 @@ use NEXT;
);
sub calc_setup {
- my($self, $cust_pkg, $time ) = @_;
+ my $self = shift;
+ my( $cust_pkg, $time ) = @_;
unless ( $self->option('delay_setup', 1) ) {
my $d = $cust_pkg->bill || $time;
@@ -31,7 +32,7 @@ sub calc_setup {
$cust_pkg->bill($d);
}
- $self->option('setup_fee');
+ $self->NEXT::calc_setup(@_);
}
sub calc_remain {
diff --git a/FS/FS/part_pkg/flat_introrate.pm b/FS/FS/part_pkg/flat_introrate.pm
index 10c205609..733760276 100644
--- a/FS/FS/part_pkg/flat_introrate.pm
+++ b/FS/FS/part_pkg/flat_introrate.pm
@@ -1,12 +1,8 @@
package FS::part_pkg::flat_introrate;
+use base qw( FS::part_pkg::flat );
use strict;
-use vars qw(@ISA %info $DEBUG $me);
-use FS::part_pkg::flat;
-
-@ISA = qw(FS::part_pkg::flat);
-$me = '[' . __PACKAGE__ . ']';
-$DEBUG = 0;
+use vars qw( %info );
%info = (
'name' => 'Introductory price for X months, then flat rate,'.
diff --git a/FS/FS/part_pkg/prorate_Mixin.pm b/FS/FS/part_pkg/prorate_Mixin.pm
index d148c963d..9efc7e89d 100644
--- a/FS/FS/part_pkg/prorate_Mixin.pm
+++ b/FS/FS/part_pkg/prorate_Mixin.pm
@@ -67,11 +67,11 @@ the base price per billing cycle.
Options:
- add_full_period: Bill for the time up to the prorate day plus one full
-billing period after that.
+ billing period after that.
- prorate_round_day: Round the current time to the nearest full day,
-instead of using the exact time.
+ instead of using the exact time.
- prorate_defer_bill: Don't bill the prorate interval until the prorate
-day arrives.
+ day arrives.
- prorate_verbose: Generate details to explain the prorate calculations.
=cut
@@ -104,7 +104,7 @@ sub calc_prorate {
$add_period = 1;
}
- # if the customer alreqady has a billing day-of-month established,
+ # if the customer already has a billing day-of-month established,
# and it's a valid cutoff day, try to respect it
my $next_bill_day;
if ( my $next_bill = $cust_pkg->cust_main->next_bill_date ) {
@@ -123,38 +123,56 @@ sub calc_prorate {
my $permonth = $charge / $self->freq;
my $months = ( ( $self->freq - 1 ) + ($mend-$mnow) / ($mend-$mstart) );
-
- if ( $self->option('prorate_verbose',1)
- and $months > 0 and $months < $self->freq ) {
- push @$details,
- 'Prorated (' . time2str('%b %d', $mnow) .
- ' - ' . time2str('%b %d', $mend) . '): ' . $money_char .
- sprintf('%.2f', $permonth * $months + 0.00000001 );
- }
+ # after this, $self->freq - 1 < $months <= $self->freq
# add a full period if currently billing for a partial period
# or periods up to freq_override if billing for an override interval
if ( ($param->{'freq_override'} || 0) > 1 ) {
$months += $param->{'freq_override'} - 1;
- }
- elsif ( $add_period && $months < $self->freq) {
+ # freq_override - 1 correct here?
+ # (probably only if freq == 1, yes?)
+ } elsif ( $add_period && $months < $self->freq ) {
- if ( $self->option('prorate_verbose',1) ) {
- # calculate the prorated and add'l period charges
+ # 'add_period' is a misnomer.
+ # we add enough to make the total at least a full period
+ $months++;
+ $$sdate = $self->add_freq($mstart, 1);
+ # now $self->freq <= $months <= $self->freq + 1
+ # (note that this only happens if $months < $self->freq to begin with)
+
+ }
+
+ if ( $self->option('prorate_verbose',1) and $months > 0 ) {
+ if ( $months < $self->freq ) {
+ # we are billing a fractional period only
+ # # (though maybe not a fractional month)
+ my $period_end = $self->add_freq($mstart);
+ push @$details,
+ 'Prorated (' . time2str('%b %d', $mnow) .
+ ' - ' . time2str('%b %d', $period_end) . '): ' . $money_char .
+ sprintf('%.2f', $permonth * $months + 0.00000001 );
+
+ } elsif ( $months > $self->freq ) {
+ # we are billing MORE than a full period
push @$details,
- 'First full month: ' . $money_char .
- sprintf('%.2f', $permonth);
- }
- $months += $self->freq;
- $$sdate = $self->add_freq($mstart);
+ 'Prorated (' . time2str('%b %d', $mnow) .
+ ' - ' . time2str('%b %d', $mend) . '): ' . $money_char .
+ sprintf('%.2f', $permonth * ($months - $self->freq + 0.0000001)),
+
+ 'First full period: ' . $money_char .
+ sprintf('%.2f', $permonth * $self->freq);
+ } # else $months == $self->freq, and no prorating has happened
}
$param->{'months'} = $months;
#so 1.005 rounds to 1.01
$charge = sprintf('%.2f', $permonth * $months + 0.00000001 );
- return $charge;
+ my $quantity = $cust_pkg->quantity || 1;
+ $charge *= $quantity;
+
+ return sprintf('%.2f', $charge);
}
=item prorate_setup CUST_PKG SDATE
diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm
index aae51e96c..21c6a8a2c 100644
--- a/FS/FS/part_pkg/voip_cdr.pm
+++ b/FS/FS/part_pkg/voip_cdr.pm
@@ -51,6 +51,11 @@ tie my %unrateable_opts, 'Tie::IxHash',
2 => 'Flag for later review',
;
+tie my %detail_formats, 'Tie::IxHash',
+ '' => '',
+ FS::cdr::invoice_formats()
+;
+
%info = (
'name' => 'VoIP rating by plan of CDR records in an internal (or external) SQL table',
'shortname' => 'VoIP/telco CDR rating (standard)',
@@ -152,10 +157,16 @@ tie my %unrateable_opts, 'Tie::IxHash',
'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: ',
},
- 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to: ',
+ 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to this cdrtypenum: ',
+ },
+
+ 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to this cdrtypenum: ',
+ },
+
+ 'use_calltypenum' => { 'name' => 'Only charge for CDRs where the CDR Call Type is set to this calltypenum: ',
},
- 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to: ',
+ 'ignore_calltypenum' => { 'name' => 'Do not charge for CDRs where the CDR Call Type is set to this calltypenum: ',
},
'ignore_disposition' => { 'name' => 'Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: ',
@@ -203,6 +214,11 @@ tie my %unrateable_opts, 'Tie::IxHash',
'skip_max_callers' => { 'name' => 'Do not charge for CDRs where max_callers is less than or equal to this value: ',
},
+ 'skip_same_customer' => {
+ 'name' => 'Do not charge for calls between numbers belonging to the same customer',
+ 'type' => 'checkbox',
+ },
+
'use_duration' => { 'name' => 'Calculate usage based on the duration field instead of the billsec field',
'type' => 'checkbox',
},
@@ -211,12 +227,25 @@ tie my %unrateable_opts, 'Tie::IxHash',
},
#false laziness w/cdr_termination.pm
- 'output_format' => { 'name' => 'CDR invoice display format',
+ 'output_format' => { 'name' => 'CDR display format for invoices',
'type' => 'select',
- 'select_options' => { FS::cdr::invoice_formats() },
+ 'select_options' => \%detail_formats,
'default' => 'default', #XXX test
},
+ 'selfservice_format' =>
+ { 'name' => 'CDR display format for selfservice',
+ 'type' => 'select',
+ 'select_options' => \%detail_formats,
+ 'default' => 'default'
+ },
+ 'selfservice_inbound_format' =>
+ { 'name' => 'Inbound CDR display format for selfservice',
+ 'type' => 'select',
+ 'select_options' => \%detail_formats,
+ 'default' => ''
+ },
+
'usage_section' => { 'name' => 'Section in which to place usage charges (whether separated or not): ',
},
@@ -286,6 +315,7 @@ tie my %unrateable_opts, 'Tie::IxHash',
use_amaflags
use_carrierid
use_cdrtypenum ignore_cdrtypenum
+ use_calltypenum ignore_calltypenum
ignore_disposition disposition_in
skip_dcontext skip_dst_prefix
skip_dstchannel_prefix skip_src_length_more
@@ -295,9 +325,12 @@ tie my %unrateable_opts, 'Tie::IxHash',
noskip_dst_length_accountcode_tollfree
skip_lastapp
skip_max_callers
+ skip_same_customer
use_duration
411_rewrite
- output_format usage_mandate summarize_usage usage_section
+ output_format
+ selfservice_format selfservice_inbound_format
+ usage_mandate summarize_usage usage_section
bill_every_call bill_inactive_svcs
count_available_phones suspend_bill
)
@@ -394,6 +427,7 @@ sub calc_usage {
'disable_src' => $self->option('disable_src'),
'default_prefix' => $self->option('default_prefix'),
'cdrtypenum' => $self->option('use_cdrtypenum'),
+ 'calltypenum' => $self->option('use_calltypenum'),
'status' => '',
'for_update' => 1,
); # $last_bill, $$sdate )
@@ -414,6 +448,7 @@ sub calc_usage {
my $error = $cdr->rate(
'part_pkg' => $self,
+ 'cust_pkg' => $cust_pkg,
'svcnum' => $svc_x->svcnum,
'single_price_included_min' => \$included_min,
'region_group_included_min' => \$region_group_included_min,
@@ -460,6 +495,7 @@ sub calc_usage {
}
#returns a reason why not to rate this CDR, or false if the CDR is chargeable
+# lots of false laziness w/voip_inbound
sub check_chargable {
my( $self, $cdr, %flags ) = @_;
@@ -493,6 +529,15 @@ sub check_chargable {
if length($self->option_cacheable('ignore_cdrtypenum'))
&& $cdr->cdrtypenum eq $self->option_cacheable('ignore_cdrtypenum'); #eq otherwise 0 matches ''
+ # unlike everything else, use_calltypenum is applied in FS::svc_x::get_cdrs.
+ return "calltypenum != ". $self->option_cacheable('use_calltypenum')
+ if length($self->option_cacheable('use_calltypenum'))
+ && $cdr->calltypenum ne $self->option_cacheable('use_calltypenum'); #ne otherwise 0 matches ''
+
+ return "calltypenum == ". $self->option_cacheable('ignore_calltypenum')
+ if length($self->option_cacheable('ignore_calltypenum'))
+ && $cdr->calltypenum eq $self->option_cacheable('ignore_calltypenum'); #eq otherwise 0 matches ''
+
return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )"
if $self->option_cacheable('skip_dcontext') =~ /\S/
&& grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext'));
@@ -561,6 +606,41 @@ sub calc_units {
$count;
}
+sub reset_usage {
+ my ($self, $cust_pkg, %opt) = @_;
+ my @part_pkg_usage = $self->part_pkg_usage or return '';
+ warn " resetting usage minutes\n" if $opt{debug};
+ my %cust_pkg_usage = map { $_->pkgusagepart, $_ } $cust_pkg->cust_pkg_usage;
+ foreach my $part_pkg_usage (@part_pkg_usage) {
+ my $part = $part_pkg_usage->pkgusagepart;
+ my $usage = $cust_pkg_usage{$part} ||
+ FS::cust_pkg_usage->new({
+ 'pkgnum' => $cust_pkg->pkgnum,
+ 'pkgusagepart' => $part,
+ 'minutes' => $part_pkg_usage->minutes,
+ });
+ foreach my $cdr_usage (
+ qsearch('cdr_cust_pkg_usage', {'cdrusagenum' => $usage->cdrusagenum})
+ ) {
+ my $error = $cdr_usage->delete;
+ warn " error resetting CDR usage: $error\n";
+ }
+
+ if ( $usage->pkgusagenum ) {
+ if ( $part_pkg_usage->rollover ) {
+ $usage->set('minutes', $part_pkg_usage->minutes + $usage->minutes);
+ } else {
+ $usage->set('minutes', $part_pkg_usage->minutes);
+ }
+ my $error = $usage->replace;
+ warn " error resetting usage minutes: $error\n" if $error;
+ } else {
+ my $error = $usage->insert;
+ warn " error resetting usage minutes: $error\n" if $error;
+ }
+ } #foreach $part_pkg_usage
+}
+
# tells whether cust_bill_pkg_detail should return a single line for
# each phonenum
sub sum_usage {
diff --git a/FS/FS/part_pkg/voip_inbound.pm b/FS/FS/part_pkg/voip_inbound.pm
index 9054f7b99..525db804d 100644
--- a/FS/FS/part_pkg/voip_inbound.pm
+++ b/FS/FS/part_pkg/voip_inbound.pm
@@ -60,15 +60,21 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities();
'type' => 'checkbox',
},
- 'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to: ',
+ 'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: ',
},
- 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to: ',
+ 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to this cdrtypenum: ',
},
- 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to: ',
+ 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to this cdrtypenum: ',
},
+ 'use_calltypenum' => { 'name' => 'Only charge for CDRs where the CDR Call Type is set to this cdrtypenum: ',
+ },
+
+ 'ignore_calltypenum' => { 'name' => 'Do not charge for CDRs where the CDR Call Type is set to this cdrtypenum: ',
+ },
+
'ignore_disposition' => { 'name' => 'Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: ',
},
@@ -147,6 +153,7 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities();
use_amaflags
use_carrierid
use_cdrtypenum ignore_cdrtypenum
+ use_calltypenum ignore_calltypenum
ignore_disposition disposition_in
skip_dcontext skip_dstchannel_prefix
skip_dst_length_less skip_lastapp
@@ -329,67 +336,58 @@ sub calc_usage {
}
#returns a reason why not to rate this CDR, or false if the CDR is chargeable
+# lots of false laziness w/voip_cdr...
sub check_chargable {
my( $self, $cdr, %flags ) = @_;
- #should have some better way of checking these options from a hash
- #or something
-
- my @opt = qw(
- use_amaflags
- use_carrierid
- use_cdrtypenum
- ignore_cdrtypenum
- disposition_in
- ignore_disposition
- skip_dcontext
- skip_dstchannel_prefix
- skip_dst_length_less
- skip_lastapp
- );
- foreach my $opt (grep !exists($flags{option_cache}->{$_}), @opt ) {
- $flags{option_cache}->{$opt} = $self->option($opt, 1);
- }
- my %opt = %{ $flags{option_cache} };
-
return 'amaflags != 2'
- if $opt{'use_amaflags'} && $cdr->amaflags != 2;
-
- return "disposition NOT IN ( $opt{'disposition_in'} )"
- if $opt{'disposition_in'} =~ /\S/
- && !grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $opt{'disposition_in'});
-
- return "disposition IN ( $opt{'ignore_disposition'} )"
- if $opt{'ignore_disposition'} =~ /\S/
- && grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $opt{'ignore_disposition'});
-
- return "carrierid != $opt{'use_carrierid'}"
- if length($opt{'use_carrierid'})
- && $cdr->carrierid ne $opt{'use_carrierid'}; #ne otherwise 0 matches ''
+ if $self->option_cacheable('use_amaflags') && $cdr->amaflags != 2;
- return "cdrtypenum != $opt{'use_cdrtypenum'}"
- if length($opt{'use_cdrtypenum'})
- && $cdr->cdrtypenum ne $opt{'use_cdrtypenum'}; #ne otherwise 0 matches ''
-
- return "cdrtypenum == $opt{'ignore_cdrtypenum'}"
- if length($opt{'ignore_cdrtypenum'})
- && $cdr->cdrtypenum eq $opt{'ignore_cdrtypenum'}; #eq otherwise 0 matches ''
+ return "disposition NOT IN ( ". $self->option_cacheable('disposition_in')." )"
+ if $self->option_cacheable('disposition_in') =~ /\S/
+ && !grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('disposition_in'));
+
+ return "disposition IN ( ". $self->option_cacheable('ignore_disposition')." )"
+ if $self->option_cacheable('ignore_disposition') =~ /\S/
+ && grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('ignore_disposition'));
+
+ return "carrierid NOT IN ( ". $self->option_cacheable('use_carrierid'). " )"
+ if $self->option_cacheable('use_carrierid') =~ /\S/
+ && !grep { $cdr->carrierid eq $_ } split(/\s*,\s*/, $self->option_cacheable('use_carrierid')); #eq otherwise 0 matches ''
+
+ # unlike everything else, use_cdrtypenum is applied in FS::svc_x::get_cdrs.
+ return "cdrtypenum != ". $self->option_cacheable('use_cdrtypenum')
+ if length($self->option_cacheable('use_cdrtypenum'))
+ && $cdr->cdrtypenum ne $self->option_cacheable('use_cdrtypenum'); #ne otherwise 0 matches ''
+
+ return "cdrtypenum == ". $self->option_cacheable('ignore_cdrtypenum')
+ if length($self->option_cacheable('ignore_cdrtypenum'))
+ && $cdr->cdrtypenum eq $self->option_cacheable('ignore_cdrtypenum'); #eq otherwise 0 matches ''
+
+ # unlike everything else, use_calltypenum is applied in FS::svc_x::get_cdrs.
+ return "calltypenum != ". $self->option_cacheable('use_calltypenum')
+ if length($self->option_cacheable('use_calltypenum'))
+ && $cdr->calltypenum ne $self->option_cacheable('use_calltypenum'); #ne otherwise 0 matches ''
+
+ return "calltypenum == ". $self->option_cacheable('ignore_calltypenum')
+ if length($self->option_cacheable('ignore_calltypenum'))
+ && $cdr->calltypenum eq $self->option_cacheable('ignore_calltypenum'); #eq otherwise 0 matches ''
- return "dcontext IN ( $opt{'skip_dcontext'} )"
- if $opt{'skip_dcontext'} =~ /\S/
- && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $opt{'skip_dcontext'});
+ return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )"
+ if $self->option_cacheable('skip_dcontext') =~ /\S/
+ && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext'));
- my $len_prefix = length($opt{'skip_dstchannel_prefix'});
- return "dstchannel starts with $opt{'skip_dstchannel_prefix'}"
+ my $len_prefix = length($self->option_cacheable('skip_dstchannel_prefix'));
+ return "dstchannel starts with ". $self->option_cacheable('skip_dstchannel_prefix')
if $len_prefix
- && substr($cdr->dstchannel,0,$len_prefix) eq $opt{'skip_dstchannel_prefix'};
+ && substr($cdr->dstchannel,0,$len_prefix) eq $self->option_cacheable('skip_dstchannel_prefix');
- my $dst_length = $opt{'skip_dst_length_less'};
+ my $dst_length = $self->option_cacheable('skip_dst_length_less');
return "destination less than $dst_length digits"
if $dst_length && length($cdr->dst) < $dst_length;
- return "lastapp is $opt{'skip_lastapp'}"
- if length($opt{'skip_lastapp'}) && $cdr->lastapp eq $opt{'skip_lastapp'};
+ return "lastapp is ". $self->option_cacheable('skip_lastapp')
+ if length($self->option_cacheable('skip_lastapp')) && $cdr->lastapp eq $self->option_cacheable('skip_lastapp');
#all right then, rate it
'';
diff --git a/FS/FS/part_pkg_link.pm b/FS/FS/part_pkg_link.pm
index fb7a8d387..9ce8e6a76 100644
--- a/FS/FS/part_pkg_link.pm
+++ b/FS/FS/part_pkg_link.pm
@@ -49,12 +49,13 @@ Destination package (see L)
=item link_type
Link type - currently, "bill" (source package bills a line item from target
-package), or "svc" (source package includes services from target package).
+package), or "svc" (source package includes services from target package),
+or "supp" (ordering source package creates a target package).
=item hidden
Flag indicating that this subpackage should be felt, but not seen as an invoice
-line item when set to 'Y'
+line item when set to 'Y'. Not allowed for "supp" links.
=back
@@ -119,11 +120,26 @@ sub check {
$self->ut_numbern('pkglinknum')
|| $self->ut_foreign_key('src_pkgpart', 'part_pkg', 'pkgpart')
|| $self->ut_foreign_key('dst_pkgpart', 'part_pkg', 'pkgpart')
- || $self->ut_enum('link_type', [ 'bill', 'svc' ] )
+ || $self->ut_enum('link_type', [ 'bill', 'svc', 'supp' ] )
|| $self->ut_enum('hidden', [ '', 'Y' ] )
;
return $error if $error;
+ if ( $self->link_type eq 'supp' ) {
+ # some sanity checking
+ my $src_pkg = $self->src_pkg;
+ my $dst_pkg = $self->dst_pkg;
+ if ( $src_pkg->freq eq '0' and $dst_pkg->freq ne '0' ) {
+ return "One-time charges can't have supplemental packages."
+ } elsif ( $dst_pkg->freq ne '0' ) {
+ my $ratio = $dst_pkg->freq / $src_pkg->freq;
+ if ($ratio != int($ratio)) {
+ return "Supplemental package period (pkgpart ".$dst_pkg->pkgpart.
+ ") must be an integer multiple of main package period.";
+ }
+ }
+ }
+
$self->SUPER::check;
}
diff --git a/FS/FS/part_pkg_msgcat.pm b/FS/FS/part_pkg_msgcat.pm
new file mode 100644
index 000000000..7c00c26ac
--- /dev/null
+++ b/FS/FS/part_pkg_msgcat.pm
@@ -0,0 +1,138 @@
+package FS::part_pkg_msgcat;
+
+use strict;
+use base qw( FS::Record );
+use FS::Locales;
+#use FS::Record qw( qsearch qsearchs );
+use FS::part_pkg;
+
+=head1 NAME
+
+FS::part_pkg_msgcat - Object methods for part_pkg_msgcat records
+
+=head1 SYNOPSIS
+
+ use FS::part_pkg_msgcat;
+
+ $record = new FS::part_pkg_msgcat \%hash;
+ $record = new FS::part_pkg_msgcat { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_msgcat object represents localized labels of a package
+definition. FS::part_pkg_msgcat inherits from FS::Record. The following
+fields are currently supported:
+
+=over 4
+
+=item pkgpartmsgnum
+
+primary key
+
+=item pkgpart
+
+Package definition
+
+=item locale
+
+locale
+
+=item pkg
+
+Localized package name (customer-viewable)
+
+=item comment
+
+Localized package comment (non-customer-viewable), optional
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record 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 { 'part_pkg_msgcat'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record. 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('pkgpartmsgnum')
+ || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart')
+ || $self->ut_enum('locale', [ FS::Locales->locales ] )
+ || $self->ut_text('pkg')
+ || $self->ut_textn('comment')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg_usage.pm b/FS/FS/part_pkg_usage.pm
new file mode 100644
index 000000000..99014d398
--- /dev/null
+++ b/FS/FS/part_pkg_usage.pm
@@ -0,0 +1,159 @@
+package FS::part_pkg_usage;
+
+use strict;
+use base qw( FS::m2m_Common FS::Record );
+use FS::Record qw( qsearch qsearchs );
+use Scalar::Util qw(blessed);
+
+=head1 NAME
+
+FS::part_pkg_usage - Object methods for part_pkg_usage records
+
+=head1 SYNOPSIS
+
+ use FS::part_pkg_usage;
+
+ $record = new FS::part_pkg_usage \%hash;
+ $record = new FS::part_pkg_usage { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_usage object represents a stock of usage minutes (generally
+for voice services) included in a package definition. FS::part_pkg_usage
+inherits from FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item pkgusagepart - primary key
+
+=item pkgpart - the package definition (L)
+
+=item minutes - the number of minutes included per billing cycle
+
+=item priority - the relative order in which to use this stock of minutes.
+
+=item shared - 'Y' to allow these minutes to be shared with other packages
+belonging to the same customer. Otherwise, only usage allocated to this
+package will use this stock of minutes.
+
+=item rollover - 'Y' to allow unused minutes to carry over between billing
+cycles. Otherwise, the available minutes will reset to the value of the
+"minutes" field upon billing.
+
+=item description - a text description of this stock of minutes
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+=item insert CLASSES
+
+=item replace CLASSES
+
+CLASSES can be an array or hash of usage classnums (see L)
+to link to this record.
+
+=item delete
+
+=cut
+
+sub table { 'part_pkg_usage'; }
+
+sub insert {
+ my $self = shift;
+ my $opt = ref($_[0]) eq 'HASH' ? shift : { @_ };
+
+ $self->SUPER::insert
+ || $self->process_m2m( 'link_table' => 'part_pkg_usage_class',
+ 'target_table' => 'usage_class',
+ 'params' => $opt,
+ );
+}
+
+sub replace {
+ my $self = shift;
+ my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+ ? shift
+ : $self->replace_old;
+ my $opt = ref($_[0]) eq 'HASH' ? $_[0] : { @_ };
+ $self->SUPER::replace($old)
+ || $self->process_m2m( 'link_table' => 'part_pkg_usage_class',
+ 'target_table' => 'usage_class',
+ 'params' => $opt,
+ );
+}
+
+sub delete {
+ my $self = shift;
+ $self->process_m2m( 'link_table' => 'part_pkg_usage_class',
+ 'target_table' => 'usage_class',
+ 'params' => {},
+ ) || $self->SUPER::delete;
+}
+
+=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('pkgusagepart')
+ || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart')
+ || $self->ut_number('minutes')
+ || $self->ut_numbern('priority')
+ || $self->ut_flag('shared')
+ || $self->ut_flag('rollover')
+ || $self->ut_textn('description')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item classnums
+
+Returns the usage class numbers that are allowed to use minutes from this
+pool.
+
+=cut
+
+sub classnums {
+ my $self = shift;
+ if (!$self->get('classnums')) {
+ my $classnums = [
+ map { $_->classnum }
+ qsearch('part_pkg_usage_class', { 'pkgusagepart' => $self->pkgusagepart })
+ ];
+ $self->set('classnums', $classnums);
+ }
+ @{ $self->get('classnums') };
+}
+
+=back
+
+=head1 SEE ALSO
+
+L, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg_usage_class.pm b/FS/FS/part_pkg_usage_class.pm
new file mode 100644
index 000000000..9a99783af
--- /dev/null
+++ b/FS/FS/part_pkg_usage_class.pm
@@ -0,0 +1,125 @@
+package FS::part_pkg_usage_class;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::part_pkg_usage_class - Object methods for part_pkg_usage_class records
+
+=head1 SYNOPSIS
+
+ use FS::part_pkg_usage_class;
+
+ $record = new FS::part_pkg_usage_class \%hash;
+ $record = new FS::part_pkg_usage_class { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_usage_class object is a link between a package usage stock
+(L) and a voice usage class (L.
+FS::part_pkg_usage_class inherits from FS::Record. The following fields
+are currently supported:
+
+=over 4
+
+=item num - primary key
+
+=item pkgusagepart - L key
+
+=item classnum - L key. Set to null to allow this stock
+to be used for calls that have no usage class. To avoid confusion, you
+should only do this if you don't use usage classes on your system.
+
+=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 { 'part_pkg_usage_class'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record. 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('num')
+ || $self->ut_foreign_key('pkgusagepart', 'part_pkg_usage', 'pkgusagepart')
+ || $self->ut_foreign_keyn('classnum', 'usage_class', 'classnum')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+The author forgot to customize this manpage.
+
+=head1 SEE ALSO
+
+L, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm
index c47177171..da794dd4c 100644
--- a/FS/FS/part_svc.pm
+++ b/FS/FS/part_svc.pm
@@ -58,6 +58,13 @@ L, and L, among others.
=item preserve - Preserve after cancellation, empty or 'Y'
+=item selfservice_access - Access allowed to the service via self-service:
+empty for full access, "readonly" for read-only, "hidden" to hide it entirely
+
+=item restrict_edit_password - Require the "Provision customer service" access
+right to change the password field, rather than just "Edit password". Only
+relevant to svc_acct for now.
+
=back
=head1 METHODS
@@ -391,7 +398,8 @@ sub check {
|| $self->ut_enum('preserve', [ '', 'Y' ] )
|| $self->ut_enum('selfservice_access', [ '', 'hidden', 'readonly' ] )
|| $self->ut_foreign_keyn('classnum', 'part_svc_class', 'classnum' )
- ;
+ || $self->ut_enum('restrict_edit_password', [ '', 'Y' ] )
+;
return $error if $error;
my @fields = eval { fields( $self->svcdb ) }; #might die
@@ -749,11 +757,9 @@ sub process {
if ( $flag =~ /^[MAH]$/ ) {
$param->{ $f } = delete( $param->{ $f.'_classnum' } );
}
- if ( $flag =~ /^S$/
- or $_ eq 'usergroup' ) {
- $param->{ $f } = ref($param->{ $f })
- ? join(',', @{$param->{ $f }} )
- : $param->{ $f };
+ if ( ( $flag =~ /^[MAHS]$/ or $_ eq 'usergroup' )
+ and ref($param->{ $f }) ) {
+ $param->{ $f } = join(',', @{ $param->{ $f } });
}
( $f, $f.'_flag', $f.'_label' );
}
diff --git a/FS/FS/part_svc_column.pm b/FS/FS/part_svc_column.pm
index d467516ed..38ce1fa80 100644
--- a/FS/FS/part_svc_column.pm
+++ b/FS/FS/part_svc_column.pm
@@ -99,8 +99,14 @@ sub check {
$self->columnflag(uc($1));
if ( $self->columnflag =~ /^[MA]$/ ) {
- $error =
- $self->ut_foreign_key( 'columnvalue', 'inventory_class', 'classnum' );
+ # split, check all values independently, and normalize
+ my @classnums = split(/\s*,\s*/, $self->columnvalue);
+ foreach (@classnums) {
+ $self->set('columnvalue', $_);
+ $error = $self->ut_foreign_key( 'columnvalue', 'inventory_class', 'classnum' );
+ return $error if $error;
+ }
+ $self->set('columnvalue', join(',', @classnums));
}
if ( $self->columnflag eq 'H' ) {
$error =
diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm
index b8da9b49b..2a048a115 100644
--- a/FS/FS/pay_batch.pm
+++ b/FS/FS/pay_batch.pm
@@ -201,7 +201,7 @@ foreach my $INC (@INC) {
\\%FS::pay_batch::$mod\::export_info,
\$FS::pay_batch::$mod\::name)";
$name ||= $mod; # in case it's not defined
- if( $@) {
+ if ($@) {
# in FS::cdr this is a die, not a warn. That's probably a bug.
warn "error using FS::pay_batch::$mod (skipping): $@\n";
next;
@@ -401,12 +401,12 @@ sub import_results {
foreach ('paid', '_date', 'payinfo') {
$new_cust_pay_batch->$_($hash{$_}) if $hash{$_};
}
- $error = $new_cust_pay_batch->approve($hash{'paybatch'} || $self->batchnum);
+ $error = $new_cust_pay_batch->approve(%hash);
$total += $hash{'paid'};
} elsif ( &{$declined_condition}(\%hash) ) {
- $error = $new_cust_pay_batch->decline;
+ $error = $new_cust_pay_batch->decline($hash{'error_message'});;
}
@@ -572,8 +572,6 @@ sub import_from_gateway {
my $payby; # CARD or CHEK
my $error;
- # follow realtime gateway practice here
- # though eventually this stuff should go into separate fields...
my $paybatch = $gateway->gatewaynum . '-' . $gateway->gateway_module .
':' . $item->authorization . ':' . $item->order_number;
@@ -644,8 +642,11 @@ sub import_from_gateway {
payby => $payby,
invnum => $item->invoice_number,
batchnum => $pay_batch->batchnum,
- paybatch => $paybatch,
payinfo => $payinfo,
+ gatewaynum => $gateway->gatewaynum,
+ processor => $gateway->gateway_module,
+ auth => $item->authorization,
+ order_number => $item->order_number,
}
);
$error ||= $cust_pay->insert;
@@ -725,7 +726,12 @@ sub import_from_gateway {
# approval status
if ( $item->approved ) {
# follow Billing_Realtime format for paybatch
- $error = $cust_pay_batch->approve($paybatch);
+ $error = $cust_pay_batch->approve(
+ 'gatewaynum' => $gateway->gatewaynum,
+ 'processor' => $gateway->gateway_module,
+ 'auth' => $item->authorization,
+ 'order_number' => $item->order_number,
+ );
$total += $cust_pay_batch->paid;
}
else {
@@ -829,6 +835,9 @@ sub try_to_resolve {
}
return $error if $error;
}
+ } elsif ( @unresolved ) {
+ # auto resolve is not enabled, and we're not ready to resolve
+ return;
}
$self->set_status('R');
@@ -1028,7 +1037,6 @@ sub manual_approve {
my $self = shift;
my $date = time;
my %opt = @_;
- my $paybatch = $opt{'paybatch'} || $self->batchnum;
my $usernum = $opt{'usernum'} || die "manual approval requires a usernum";
my $conf = FS::Conf->new;
return 'manual batch approval disabled'
@@ -1058,7 +1066,9 @@ sub manual_approve {
'_date' => $date,
'usernum' => $usernum,
};
- my $error = $new_cust_pay_batch->approve($paybatch);
+ my $error = $new_cust_pay_batch->approve();
+ # there are no approval options here (authorization, order_number, etc.)
+ # because the transaction wasn't really approved
if ( $error ) {
$dbh->rollback;
return 'paybatchnum '.$cust_pay_batch->paybatchnum.": $error";
diff --git a/FS/FS/pay_batch/BoM.pm b/FS/FS/pay_batch/BoM.pm
index 719b504e5..a3708d477 100644
--- a/FS/FS/pay_batch/BoM.pm
+++ b/FS/FS/pay_batch/BoM.pm
@@ -31,13 +31,13 @@ $name = 'BoM';
},
header => sub {
my $pay_batch = shift;
- sprintf( "A%10s%04u%06u%05u%54s\n", #80
+ sprintf( "A%10s%04u%06u%05u%53s\n", #80
$origid,
$pay_batch->batchnum,
jdate($pay_batch->download),
$datacenter,
"") .
- sprintf( "XD%03u%06u%-15s%-30s%09u%-12s \n", #80
+ sprintf( "XD%03u%06u%-15s%-30s%09u%-12s ", #80
$typecode,
jdate($pay_batch->download),
$shortname,
@@ -48,7 +48,7 @@ $name = 'BoM';
row => sub {
my ($cust_pay_batch, $pay_batch) = @_;
my ($account, $aba) = split('@', $cust_pay_batch->payinfo);
- sprintf( "D%010.0f%09u%-12s%-29s%-19s\n", #80
+ sprintf( "D%010.0f%09u%-12s%-29s%-18s ", #80
$cust_pay_batch->amount * 100,
$aba,
$account,
@@ -58,8 +58,8 @@ $name = 'BoM';
},
footer => sub {
my ($pay_batch, $batchcount, $batchtotal) = @_;
- sprintf( "YD%08u%014.0f%56s\n", $batchcount, $batchtotal*100, ""). #80
- sprintf( "Z%014u%04u%014u%05u%42s\n", #80 now
+ sprintf( "YD%08u%014.0f%55s\n", $batchcount, $batchtotal*100, ""). #80
+ sprintf( "Z%014u%05u%014u%05u%40s", #80 now
$batchtotal*100, $batchcount, "0", "0", "");
},
);
diff --git a/FS/FS/pay_batch/eft_canada.pm b/FS/FS/pay_batch/eft_canada.pm
index 220fecb3d..b24c9c3a4 100644
--- a/FS/FS/pay_batch/eft_canada.pm
+++ b/FS/FS/pay_batch/eft_canada.pm
@@ -25,12 +25,6 @@ my %holiday_yearly = (
12 => { map {$_=>1} 26 }, #boxing day
);
my %holiday = (
- 2012 => {
- 7 => { map {$_=>1} 2 }, #canada day
- 8 => { map {$_=>1} 6 }, #First Monday of August Civic Holiday
- 9 => { map {$_=>1} 3 }, #labour day
- 10 => { map {$_=>1} 8 }, #thanksgiving
- },
2013 => { 2 => { map {$_=>1} 18 }, #family day
3 => { map {$_=>1} 29 }, #good friday
4 => { map {$_=>1} 1 }, #easter monday
diff --git a/FS/FS/pay_batch/nacha.pm b/FS/FS/pay_batch/nacha.pm
new file mode 100644
index 000000000..c069082c7
--- /dev/null
+++ b/FS/FS/pay_batch/nacha.pm
@@ -0,0 +1,208 @@
+package FS::pay_batch::nacha;
+
+use strict;
+use vars qw( %import_info %export_info $name $conf $entry_hash $DEBUG );
+use Date::Format;
+#use Time::Local 'timelocal';
+#use FS::Conf;
+
+$name = 'NACHA';
+
+$DEBUG = 0;
+
+%import_info = (
+ #XXX stub finish me
+ 'filetype' => 'CSV',
+ 'fields' => [
+ ],
+ 'hook' => sub {
+ my $hash = shift;
+ },
+ 'approved' => sub { 1 },
+ 'declined' => sub { 0 },
+);
+
+%export_info = (
+
+ #optional
+ init => sub {
+ $conf = shift;
+ },
+
+ delimiter => '',
+
+
+ header => sub {
+ my( $pay_batch, $cust_pay_batch_arrayref ) = @_;
+
+ $conf->config('batchconfig-nacha-destination') =~ /^\s*(\d{9})\s*$/
+ or die 'illegal NACHA Destination';
+ my $dest = $1;
+
+ my $dest_name = $conf->config('batchconfig-nacha-destination_name');
+ $dest_name = substr( $dest_name. (' 'x23), 0, 23);
+
+ $conf->config('batchconfig-nacha-origin') =~ /^\s*(\d{10})\s*$/
+ or die 'illegal NACHA Origin';
+ my $origin = $1;
+
+ my $company = $conf->config('company_name', $pay_batch->agentnum);
+ $company = substr(uc($company). (' 'x23), 0, 23);
+
+ my $now = time;
+
+ #haha don't want to break after a quarter million years of a batch a day
+ #or 54 years for 5000 agent-virtualized hosted companies batching daily
+ my $refcode = substr( (' 'x8). $pay_batch->batchnum, -8);
+
+ #or only 25,000 years or 5.4 for 5000 companies :)
+ #though they would probably want them numbered per company
+ my $batchnum = substr( ('0'x7). $pay_batch->batchnum, -7);
+
+ $entry_hash = 0;
+
+ warn "building File & Batch Header Records\n" if $DEBUG;
+
+ ##
+ # File Header Record
+ ##
+
+ '1'. #Record Type Code
+ '01'. #Priority Code
+ ' '. $dest. #Immediate Destination / 9-digit transit routing #
+ $origin. #Immediate Origin / 10 digit company number
+ time2str('%y%m%d', $now). #File Creation Date
+ time2str('%H%M', $now). #File Creation Time
+ 'A'. #XXX file ID modifier, mult. files in transit? [A-Z0-9]
+ '094'. #94 character records
+ '10'. #Blocking Factor
+ '1'. #Format code
+ $dest_name. #Immediate Destination Name / 23 char bank name
+ $company. #Immediate Origin Name / 23 char company name
+ $refcode. #Reference Code (internal/optional)
+
+ ###
+ # Batch Header Record
+ ###
+
+ '5'. #Record Type Code
+ '225'. #Service Class Code (220 credits only,
+ # 200 mixed debits & credits)
+ substr($company, 0, 16). #on cust. statements
+ (' 'x20 ). #20 char "company internal use if desired"
+ $origin. #Company Identification (Immediate Origin)
+ 'PPD'. #others?
+ #PPD "Prearranged Payments and Deposit entries" for consumer items
+ #CCD (Cash Concentration and Disbursement)
+ #CTX (Corporate Trade Exchange)
+ #TEL (Telephone initiated entires)
+ #WEB (Authorization received via the Internet)
+ 'InterntSvc'. #XXX from conf 10 char txn desc, printed on cust. statements
+
+ #6 char "Descriptive date" printed on customer statements
+ #XXX now? or use a separate post date?
+ time2str('%y%m%d', $now).
+
+ #6 char date transactions are to be posted
+ #XXX now? or do we need a future banking day date like eft_canada trainwreck
+ time2str('%y%m%d', $now).
+
+ (' 'x3). #Settlement Date / Reserved
+ '1'. #Originator Status Code
+ substr($dest, 0, 8). #Originating Financial Institution
+ $batchnum #Batch Number ("number batches sequentially")
+
+ },
+
+ 'row' => sub {
+ my( $cust_pay_batch, $pay_batch, $batchcount, $batchtotal ) = @_;
+
+ my ($account, $aba) = split('@', $cust_pay_batch->payinfo);
+
+ # "Total of all positions 4-11 on each 6 record"
+ $entry_hash += substr($aba,0,8);
+
+ my $cust_main = $cust_pay_batch->cust_main;
+ my $cust_identifier = substr($cust_main->display_custnum. (' 'x15), 0, 15);
+
+ #XXX paytype should actually be in the batch, but this will do for now
+ #27 checking debit, 37 savings debit
+ my $transaction_code = ( $cust_main->paytype =~ /savings/i ? '37' : '27' );
+
+ my $cust_name = substr($cust_main->name. (' 'x22), 0, 22);
+
+ #non-PPD transactions? future
+
+ warn "building PPD Record\n" if $DEBUG;
+
+ ###
+ # PPD Entry Detail Record
+ ###
+
+ '6'. #Record Type Code
+ $transaction_code. #Transaction Code
+ $aba. #Receiving DFI Identification, check digit
+ substr($account.(' 'x17), 0, 17). #DFI Account number (Left justify)
+ sprintf('%010d', $cust_pay_batch->amount * 100). #Amount
+ $cust_identifier. #Individual Identification Number, 15 char
+ $cust_name. #Individual name (22-char)
+ ' '. #2 char "company internal use if desired"
+ '0'. #Addenda Record Indicator
+ (' 'x15) #15 digit "bank will assign trace number"
+ # (00000?)
+ },
+
+ 'footer' => sub {
+ my( $pay_batch, $batchcount, $batchtotal ) = @_;
+
+ #Only use the final 10 positions in the entry
+ $entry_hash = substr( '00'.$entry_hash, -10);
+
+ $conf->config('batchconfig-nacha-destination') =~ /^\s*(\d{9})\s*$/
+ or die 'illegal NACHA Destination';
+ my $dest = $1;
+
+ $conf->config('batchconfig-nacha-origin') =~ /^\s*(\d{10})\s*$/
+ or die 'illegal NACHA Origin';
+ my $origin = $1;
+
+ my $batchnum = substr( ('0'x7). $pay_batch->batchnum, -7);
+
+ warn "building Batch & File Control Records\n" if $DEBUG;
+
+ ###
+ # Batch Control Record
+ ###
+
+ '8'. #Record Type Code
+ '225'. #Service Class Code (220 credits only,
+ # 200 mixed debits&credits)
+ sprintf('%06d', $batchcount). #Entry / Addenda Count
+ $entry_hash.
+ sprintf('%012d', $batchtotal * 100). #Debit total
+ '000000000000'. #Credit total
+ $origin. #Company Identification (Immediate Origin)
+ (' 'x19). #Message Authentication Code (19 char blank)
+ (' 'x6). #Federal Reserve Use (6 char blank)
+ substr($dest, 0, 8). #Originating Financial Institution
+ $batchnum. #Batch Number ("number batches sequentially")
+
+ ###
+ # File Control Record
+ ###
+
+ '9'. #Record Type Code
+ '000001'. #Batch Counter (# of batch header recs)
+ sprintf('%06d', $batchcount + 4). #num of physical blocks on the file..?
+ sprintf('%08d', $batchcount). #total # of entry detail and addenda
+ $entry_hash.
+ sprintf('%012d', $batchtotal * 100). #Debit total
+ '000000000000'. #Credit total
+ ( ' 'x39 ) #Reserved / blank
+
+ },
+
+);
+
+1;
+
diff --git a/FS/FS/pay_batch/paymentech.pm b/FS/FS/pay_batch/paymentech.pm
index c687cc8e2..1ecf35afd 100644
--- a/FS/FS/pay_batch/paymentech.pm
+++ b/FS/FS/pay_batch/paymentech.pm
@@ -23,7 +23,10 @@ my $gateway;
'_date',
'approvalStatus',
'order_number',
- 'authorization',
+ 'auth',
+ 'procStatus',
+ 'procStatusMessage',
+ 'respCodeMessage',
],
xmlkeys => [
'orderID',
@@ -31,6 +34,9 @@ my $gateway;
'approvalStatus',
'txRefNum',
'authorizationCode',
+ 'procStatus',
+ 'procStatusMessage',
+ 'respCodeMessage',
],
'hook' => sub {
if ( !$gateway ) {
@@ -38,7 +44,7 @@ my $gateway;
# as the batch config, if there is one. If not, leave
# gateway out entirely.
my $merchant = (FS::Conf->new->config('batchconfig-paymentech'))[2];
- my $g = qsearchs({
+ $gateway = qsearchs({
'table' => 'payment_gateway',
'addl_from' => ' JOIN payment_gateway_option USING (gatewaynum) ',
'hashref' => { disabled => '',
@@ -46,18 +52,19 @@ my $gateway;
optionvalue => $merchant,
},
});
- $gateway = ($g ? $g->gatewaynum . '-' : '') . 'PaymenTech';
}
my ($hash, $oldhash) = @_;
+ $hash->{'gatewaynum'} = $gateway->gatewaynum if $gateway;
+ $hash->{'processor'} = 'PaymenTech';
my ($mon, $day, $year, $hour, $min, $sec) =
$hash->{'_date'} =~ /^(..)(..)(....)(..)(..)(..)$/;
$hash->{'_date'} = timelocal($sec, $min, $hour, $day, $mon-1, $year);
$hash->{'paid'} = $oldhash->{'amount'};
- $hash->{'paybatch'} = join(':',
- $gateway,
- $hash->{'authorization'},
- $hash->{'order_number'},
- );
+ if ( $hash->{'procStatus'} == 0 ) {
+ $hash->{'error_message'} = $hash->{'respCodeMessage'};
+ } else {
+ $hash->{'error_message'} = $hash->{'procStatusMessage'};
+ }
},
'approved' => sub { my $hash = shift;
$hash->{'approvalStatus'}
@@ -103,32 +110,32 @@ my %paymentech_countries = map { $_ => 1 } qw( US CA GB UK );
$xml->startTag('newOrder', BatchRequestNo => $count++);
my $status = $_->cust_main->status;
tie my %order, 'Tie::IxHash', (
- industryType => 'EC',
- transType => 'AC',
- bin => $bin,
- merchantID => $merchantID,
- terminalID => $terminalID,
+ industryType => 'EC',
+ transType => 'AC',
+ bin => $bin,
+ merchantID => $merchantID,
+ terminalID => $terminalID,
($_->payby eq 'CARD') ? (
- ccAccountNum => $_->payinfo,
- ccExp => $_->expmmyy,
+ ccAccountNum => $_->payinfo,
+ ccExp => $_->expmmyy,
) : (
ecpCheckRT => ($_->payinfo =~ /@(\d+)/),
ecpCheckDDA => ($_->payinfo =~ /(\d+)@/),
ecpBankAcctType => $paytype{lc($_->cust_main->paytype)},
ecpDelvMethod => 'A',
),
- avsZip => substr($_->zip, 0, 10),
+ avsZip => substr($_->zip, 0, 10),
avsAddress1 => substr($_->address1, 0, 30),
avsAddress2 => substr($_->address2, 0, 30),
- avsCity => substr($_->city, 0, 20),
- avsState => $_->state,
- avsName => substr($_->first . ' ' . $_->last, 0, 30),
- avsCountryCode => ( $paymentech_countries{ $_->country }
- ? $_->country
- : ''
- ),
- orderID => $_->paybatchnum,
- amount => $_->amount * 100,
+ avsCity => substr($_->city, 0, 20),
+ avsState => substr($_->state, 0, 2),
+ avsName => substr($_->first. ' '. $_->last, 0, 30),
+ ( $paymentech_countries{ $_->country }
+ ? ( avsCountryCode => $_->country )
+ : ()
+ ),
+ orderID => $_->paybatchnum,
+ amount => $_->amount * 100,
);
# only do this if recurringInd is enabled in config,
# and the customer has at least one non-canceled recurring package
diff --git a/FS/FS/payinfo_transaction_Mixin.pm b/FS/FS/payinfo_transaction_Mixin.pm
index 093891e93..50659ac1e 100644
--- a/FS/FS/payinfo_transaction_Mixin.pm
+++ b/FS/FS/payinfo_transaction_Mixin.pm
@@ -73,10 +73,7 @@ sub _parse_paybatch {
my $payment_gateway =
qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
- die "payment gateway $gatewaynum not found" #?
- unless $payment_gateway;
-
- $processor = $payment_gateway->gateway_module;
+ $processor = $payment_gateway->gateway_module if $payment_gateway;
}
diff --git a/FS/FS/prospect_main.pm b/FS/FS/prospect_main.pm
index b5d51d333..a18c8ff67 100644
--- a/FS/FS/prospect_main.pm
+++ b/FS/FS/prospect_main.pm
@@ -2,7 +2,7 @@ package FS::prospect_main;
use strict;
use base qw( FS::Quotable_Mixin FS::o2m_Common FS::Record );
-use vars qw( $DEBUG );
+use vars qw( $DEBUG @location_fields );
use Scalar::Util qw( blessed );
use FS::Record qw( dbh qsearch qsearchs );
use FS::agent;
@@ -12,6 +12,43 @@ use FS::qual;
$DEBUG = 0;
+#started as false laziness w/cust_main/Location.pm
+
+use Carp qw(carp);
+
+my $init = 0;
+BEGIN {
+ # set up accessors for location fields
+ if (!$init) {
+ no strict 'refs';
+ @location_fields =
+ qw( address1 address2 city county state zip country district
+ latitude longitude coord_auto censustract censusyear geocode
+ addr_clean );
+
+ foreach my $f (@location_fields) {
+ *{"FS::prospect_main::$f"} = sub {
+ carp "WARNING: tried to set cust_main.$f with accessor" if (@_ > 1);
+ my @cust_location = shift->cust_location or return '';
+ #arbitrarily picking the first because the UI only lets you add one
+ $cust_location[0]->$f
+ };
+ }
+ $init++;
+ }
+}
+
+#debugging shim--probably a performance hit, so remove this at some point
+sub get {
+ my $self = shift;
+ my $field = shift;
+ if ( $DEBUG and grep { $_ eq $field } @location_fields ) {
+ carp "WARNING: tried to get() location field $field";
+ $self->$field;
+ }
+ $self->FS::Record::get($field);
+}
+
=head1 NAME
FS::prospect_main - Object methods for prospect_main records
@@ -208,6 +245,12 @@ sub check {
;
return $error if $error;
+ my $company = $self->company;
+ $company =~ s/^\s+//;
+ $company =~ s/\s+$//;
+ $company =~ s/\s+/ /g;
+ $self->company($company);
+
$self->SUPER::check;
}
diff --git a/FS/FS/quotation.pm b/FS/FS/quotation.pm
index bf2711b0a..47f13e6dc 100644
--- a/FS/FS/quotation.pm
+++ b/FS/FS/quotation.pm
@@ -176,6 +176,36 @@ sub _total {
}
+#prevent things from falsely showing up as taxes, at least until we support
+# quoting tax amounts..
+sub _items_tax {
+ return ();
+}
+sub _items_nontax {
+ shift->cust_bill_pkg;
+}
+
+sub _items_total {
+ my( $self, $total_items ) = @_;
+
+ if ( $self->total_setup > 0 ) {
+ push @$total_items, {
+ 'total_item' => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
+ 'total_amount' => $self->total_setup,
+ };
+ }
+
+ #could/should add up the different recurring frequencies on lines of their own
+ # but this will cover the 95% cases for now
+ if ( $self->total_recur > 0 ) {
+ push @$total_items, {
+ 'total_item' => $self->mt('Total Recurring'),
+ 'total_amount' => $self->total_recur,
+ };
+ }
+
+}
+
=item enable_previous
=cut
diff --git a/FS/FS/quotation_pkg.pm b/FS/FS/quotation_pkg.pm
index 3d40bb03a..efff9683f 100644
--- a/FS/FS/quotation_pkg.pm
+++ b/FS/FS/quotation_pkg.pm
@@ -1,10 +1,12 @@
package FS::quotation_pkg;
use strict;
-use base qw( FS::Record );
+use base qw( FS::TemplateItem_Mixin FS::Record );
use FS::Record qw( qsearchs ); #qsearch
use FS::part_pkg;
use FS::cust_location;
+use FS::quotation;
+use FS::quotation_pkg_discount; #so its loaded when TemplateItem_Mixin needs it
=head1 NAME
@@ -80,6 +82,14 @@ points to. You can ask the object for a copy with the I method.
sub table { 'quotation_pkg'; }
+sub display_table { 'quotation_pkg'; }
+
+#forget it, just overriding cust_bill_pkg_display entirely
+#sub display_table_orderby { 'quotationpkgnum'; } # something else?
+# # (for invoice display order)
+
+sub discount_table { 'quotation_pkg_discount'; }
+
=item insert
Adds this record to the database. If there is an error, returns the error,
@@ -107,8 +117,9 @@ sub check {
my $error =
$self->ut_numbern('quotationpkgnum')
- || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart' )
- || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum' )
+ || $self->ut_foreign_key( 'quotationnum', 'quotation', 'quotationnum' )
+ || $self->ut_foreign_key( 'pkgpart', 'part_pkg', 'pkgpart' )
+ || $self->ut_foreign_keyn( 'locationnum', 'cust_location', 'locationnum' )
|| $self->ut_numbern('start_date')
|| $self->ut_numbern('contract_end')
|| $self->ut_numbern('quantity')
@@ -131,7 +142,7 @@ sub desc {
sub setup {
my $self = shift;
- return '0.00' if $self->waive_setup eq 'Y';
+ return '0.00' if $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'};
my $part_pkg = $self->part_pkg;
#my $setup = $part_pkg->can('base_setup') ? $part_pkg->base_setup
# : $part_pkg->option('setup_fee');
@@ -144,6 +155,7 @@ sub setup {
sub recur {
my $self = shift;
+ return '0.00' if $self->{'_NO_RECUR_KLUDGE'};
my $part_pkg = $self->part_pkg;
my $recur = $part_pkg->can('base_recur') ? $part_pkg->base_recur
: $part_pkg->option('recur_fee');
@@ -152,6 +164,43 @@ sub recur {
sprintf('%.2f', $recur);
}
+=item cust_bill_pkg_display [ type => TYPE ]
+
+=cut
+
+sub cust_bill_pkg_display {
+ my ( $self, %opt ) = @_;
+
+ my $type = $opt{type} if exists $opt{type};
+ return () if $type eq 'U'; #quotations don't have usage
+
+ if ( $self->get('display') ) {
+ return ( grep { defined($type) ? ($type eq $_->type) : 1 }
+ @{ $self->get('display') }
+ );
+ } else {
+
+ #??
+ my $setup = $self->new($self->hashref);
+ $setup->{'_NO_RECUR_KLUDGE'} = 1;
+ $setup->{'type'} = 'S';
+ my $recur = $self->new($self->hashref);
+ $recur->{'_NO_SETUP_KLUDGE'} = 1;
+ $recur->{'type'} = 'R';
+
+ if ( $type eq 'S' ) {
+ return ($setup);
+ } elsif ( $type eq 'R' ) {
+ return ($recur);
+ } else {
+ #return ($setup, $recur);
+ return ($self);
+ }
+
+ }
+
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/rate.pm b/FS/FS/rate.pm
index a2511cf99..49ac938fd 100644
--- a/FS/FS/rate.pm
+++ b/FS/FS/rate.pm
@@ -308,17 +308,28 @@ sub dest_detail {
#find a rate prefix, first look at most specific, then fewer digits,
# finally trying the country code only
my $rate_prefix = '';
- for my $len ( reverse(1..10) ) {
- $rate_prefix = qsearchs('rate_prefix', {
+ $rate_prefix = qsearchs({
+ 'table' => 'rate_prefix',
+ 'addl_from' => ' JOIN rate_region USING (regionnum)',
+ 'hashref' => {
+ 'countrycode' => $countrycode,
+ 'npa' => $phonenum,
+ },
+ 'extra_sql' => ' AND exact_match = \'Y\''
+ });
+ if (!$rate_prefix) {
+ for my $len ( reverse(1..10) ) {
+ $rate_prefix = qsearchs('rate_prefix', {
+ 'countrycode' => $countrycode,
+ #'npa' => { op=> 'LIKE', value=> substr($number, 0, $len) }
+ 'npa' => substr($phonenum, 0, $len),
+ } ) and last;
+ }
+ $rate_prefix ||= qsearchs('rate_prefix', {
'countrycode' => $countrycode,
- #'npa' => { op=> 'LIKE', value=> substr($number, 0, $len) }
- 'npa' => substr($phonenum, 0, $len),
- } ) and last;
+ 'npa' => '',
+ });
}
- $rate_prefix ||= qsearchs('rate_prefix', {
- 'countrycode' => $countrycode,
- 'npa' => '',
- });
return '' unless $rate_prefix;
diff --git a/FS/FS/rate_region.pm b/FS/FS/rate_region.pm
index f4a0ab196..d42fdb41e 100644
--- a/FS/FS/rate_region.pm
+++ b/FS/FS/rate_region.pm
@@ -36,7 +36,10 @@ inherits from FS::Record. The following fields are currently supported:
=item regionnum - primary key
-=item regionname
+=item regionname - name of the region
+
+=item exact_match - 'Y' if "prefixes" in this region really represent
+complete phone numbers. Null if they represent prefixes (the usual case).
=back
@@ -233,6 +236,7 @@ sub check {
my $error =
$self->ut_numbern('regionnum')
|| $self->ut_text('regionname')
+ || $self->ut_flag('exact_match')
;
return $error if $error;
diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm
index 7aede54a6..0aea4559b 100644
--- a/FS/FS/svc_Common.pm
+++ b/FS/FS/svc_Common.pm
@@ -43,27 +43,6 @@ inherit from, i.e. FS::svc_acct. FS::svc_Common inherits from FS::Record.
=over 4
-=item search_sql_field FIELD STRING
-
-Class method which returns an SQL fragment to search for STRING in FIELD.
-
-It is now case-insensitive by default.
-
-=cut
-
-sub search_sql_field {
- my( $class, $field, $string ) = @_;
- my $table = $class->table;
- my $q_string = dbh->quote($string);
- "LOWER($table.$field) = LOWER($q_string)";
-}
-
-#fallback for services that don't provide a search...
-sub search_sql {
- #my( $class, $string ) = @_;
- '1 = 0'; #false
-}
-
=item new
=cut
@@ -863,13 +842,20 @@ sub set_auto_inventory {
next if $columnflag eq 'A' && $self->$field() ne '';
my $classnum = $part_svc_column->columnvalue;
- my %hash = ( 'classnum' => $classnum );
+ my %hash;
if ( $columnflag eq 'A' && $self->$field() eq '' ) {
$hash{'svcnum'} = '';
} elsif ( $columnflag eq 'M' ) {
return "Select inventory item for $field" unless $self->getfield($field);
$hash{'item'} = $self->getfield($field);
+ my $chosen_classnum = $self->getfield($field.'_classnum');
+ if ( grep {$_ == $chosen_classnum} split(',', $classnum) ) {
+ $classnum = $chosen_classnum;
+ }
+ # otherwise the chosen classnum is either (all), or somehow not on
+ # the list, so ignore it and choose the first item that's in any
+ # class on the list
}
my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql(
@@ -880,18 +866,30 @@ sub set_auto_inventory {
my $inventory_item = qsearchs({
'table' => 'inventory_item',
'hashref' => \%hash,
- 'extra_sql' => "AND $agentnums_sql",
+ 'extra_sql' => "AND classnum IN ($classnum) AND $agentnums_sql",
'order_by' => 'ORDER BY ( agentnum IS NULL ) '. #agent inventory first
' LIMIT 1 FOR UPDATE',
});
unless ( $inventory_item ) {
+ # should really only be shown if columnflag eq 'A'...
$dbh->rollback if $oldAutoCommit;
- my $inventory_class =
- qsearchs('inventory_class', { 'classnum' => $classnum } );
- return "Can't find inventory_class.classnum $classnum"
- unless $inventory_class;
- return "Out of ". PL_N($inventory_class->classname);
+ my $message = 'Out of ';
+ my @classnums = split(',', $classnum);
+ foreach ( @classnums ) {
+ my $class = FS::inventory_class->by_key($_)
+ or return "Can't find inventory_class.classnum $_";
+ $message .= PL_N($class->classname);
+ if ( scalar(@classnums) > 2 ) { # english is hard
+ if ( $_ != $classnums[-1] ) {
+ $message .= ', ';
+ }
+ }
+ if ( scalar(@classnums) > 1 and $_ == $classnums[-2] ) {
+ $message .= 'and ';
+ }
+ }
+ return $message;
}
next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum;
@@ -899,13 +897,14 @@ sub set_auto_inventory {
$self->setfield( $field, $inventory_item->item );
#if $columnflag eq 'A' && $self->$field() eq '';
+ # release the old inventory item, if there was one
if ( $old && $old->$field() && $old->$field() ne $self->$field() ) {
my $old_inv = qsearchs({
'table' => 'inventory_item',
- 'hashref' => { 'classnum' => $classnum,
+ 'hashref' => {
'svcnum' => $old->svcnum,
},
- 'extra_sql' => ' AND '.
+ 'extra_sql' => "AND classnum IN ($classnum) AND ".
'( ( svc_field IS NOT NULL AND svc_field = '.$dbh->quote($field).' )'.
' OR ( svc_field IS NULL AND item = '. dbh->quote($old->$field).' )'.
')',
@@ -941,6 +940,9 @@ sub set_auto_inventory {
=item return_inventory
+Release all inventory items attached to this service's fields. Call
+when unprovisioning the service.
+
=cut
sub return_inventory {
@@ -1082,17 +1084,22 @@ otherwise returns false.
=cut
-sub export_setstatus {
- my( $self, @args ) = @_;
- my $error = $self->export('setstatus', @args);
+sub export_setstatus { shift->_export_setstatus_X('setstatus', @_) }
+sub export_setstatus_listadd { shift->_export_setstatus_X('setstatus_listadd', @_) }
+sub export_setstatus_listdel { shift->_export_setstatus_X('setstatus_listdel', @_) }
+sub export_setstatus_vacationadd { shift->_export_setstatus_X('setstatus_vacationadd', @_) }
+sub export_setstatus_vacationdel { shift->_export_setstatus_X('setstatus_vacationdel', @_) }
+
+sub _export_setstatus_X {
+ my( $self, $method, @args ) = @_;
+ my $error = $self->export($method, @args);
if ( $error ) {
- warn "error running export_setstatus: $error";
+ warn "error running export_$method: $error";
return $error;
}
'';
}
-
=item export HOOK [ EXPORT_ARGS ]
Runs the provided export hook (i.e. "suspend", "unsuspend") for this service.
@@ -1277,6 +1284,221 @@ sub nms_ip_delete {
#XXX not yet implemented
}
+=item search_sql_field FIELD STRING
+
+Class method which returns an SQL fragment to search for STRING in FIELD.
+
+It is now case-insensitive by default.
+
+=cut
+
+sub search_sql_field {
+ my( $class, $field, $string ) = @_;
+ my $table = $class->table;
+ my $q_string = dbh->quote($string);
+ "LOWER($table.$field) = LOWER($q_string)";
+}
+
+#fallback for services that don't provide a search...
+sub search_sql {
+ #my( $class, $string ) = @_;
+ '1 = 0'; #false
+}
+
+=item search HASHREF
+
+Class method which returns a qsearch hash expression to search for parameters
+specified in HASHREF.
+
+Parameters:
+
+=over 4
+
+=item unlinked - set to search for all unlinked services. Overrides all other options.
+
+=item agentnum
+
+=item custnum
+
+=item svcpart
+
+=item ip_addr
+
+=item pkgpart - arrayref
+
+=item routernum - arrayref
+
+=item sectornum - arrayref
+
+=item towernum - arrayref
+
+=item order_by
+
+=back
+
+=cut
+
+# svc_broadband::search should eventually use this instead
+sub search {
+ my ($class, $params) = @_;
+
+ my @from = (
+ 'LEFT JOIN cust_svc USING ( svcnum )',
+ 'LEFT JOIN part_svc USING ( svcpart )',
+ 'LEFT JOIN cust_pkg USING ( pkgnum )',
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'),
+ );
+
+ my @where = ();
+
+ $class->_search_svc($params, \@from, \@where) if $class->can('_search_svc');
+
+# # domain
+# if ( $params->{'domain'} ) {
+# my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
+# #preserve previous behavior & bubble up an error if $svc_domain not found?
+# push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
+# }
+#
+# # domsvc
+# if ( $params->{'domsvc'} =~ /^(\d+)$/ ) {
+# push @where, "domsvc = $1";
+# }
+
+ #unlinked
+ push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
+
+ #agentnum
+ if ( $params->{'agentnum'} =~ /^(\d+)$/ && $1 ) {
+ push @where, "cust_main.agentnum = $1";
+ }
+
+ #custnum
+ if ( $params->{'custnum'} =~ /^(\d+)$/ && $1 ) {
+ push @where, "custnum = $1";
+ }
+
+ #customer status
+ if ( $params->{'cust_status'} =~ /^([a-z]+)$/ ) {
+ push @where, FS::cust_main->cust_status_sql . " = '$1'";
+ }
+
+ #customer balance
+ if ( $params->{'balance'} =~ /^\s*(\-?\d*(\.\d{1,2})?)\s*$/ && length($1) ) {
+ my $balance = $1;
+
+ my $age = '';
+ if ( $params->{'balance_days'} =~ /^\s*(\d*(\.\d{1,3})?)\s*$/ && length($1) ) {
+ $age = time - 86400 * $1;
+ }
+ push @where, FS::cust_main->balance_date_sql($age) . " > $balance";
+ }
+
+ #payby
+ if ( $params->{'payby'} && scalar(@{ $params->{'payby'} }) ) {
+ my @payby = map "'$_'", grep /^(\w+)$/, @{ $params->{'payby'} };
+ push @where, 'payby IN ('. join(',', @payby ). ')';
+ }
+
+ #pkgpart
+ ##pkgpart, now properly untainted, can be arrayref
+ #for my $pkgpart ( $params->{'pkgpart'} ) {
+ # if ( ref $pkgpart ) {
+ # my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
+ # push @where, "cust_pkg.pkgpart IN ($where)" if $where;
+ # }
+ # elsif ( $pkgpart =~ /^(\d+)$/ ) {
+ # push @where, "cust_pkg.pkgpart = $1";
+ # }
+ #}
+ if ( $params->{'pkgpart'} ) {
+ my @pkgpart = ref( $params->{'pkgpart'} )
+ ? @{ $params->{'pkgpart'} }
+ : $params->{'pkgpart'}
+ ? ( $params->{'pkgpart'} )
+ : ();
+ @pkgpart = grep /^(\d+)$/, @pkgpart;
+ push @where, 'cust_pkg.pkgpart IN ('. join(',', @pkgpart ). ')' if @pkgpart;
+ }
+
+ #svcnum
+ if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
+ push @where, "svcnum = $1";
+ }
+
+ # svcpart
+ if ( $params->{'svcpart'} ) {
+ my @svcpart = ref( $params->{'svcpart'} )
+ ? @{ $params->{'svcpart'} }
+ : $params->{'svcpart'}
+ ? ( $params->{'svcpart'} )
+ : ();
+ @svcpart = grep /^(\d+)$/, @svcpart;
+ push @where, 'svcpart IN ('. join(',', @svcpart ). ')' if @svcpart;
+ }
+
+ if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
+ push @from, ' LEFT JOIN export_svc USING ( svcpart )';
+ push @where, "exportnum = $1";
+ }
+
+# # sector and tower
+# my @where_sector = $class->tower_sector_sql($params);
+# if ( @where_sector ) {
+# push @where, @where_sector;
+# push @from, ' LEFT JOIN tower_sector USING ( sectornum )';
+# }
+
+ # here is the agent virtualization
+ #if ($params->{CurrentUser}) {
+ # my $access_user =
+ # qsearchs('access_user', { username => $params->{CurrentUser} });
+ #
+ # if ($access_user) {
+ # push @where, $access_user->agentnums_sql('table'=>'cust_main');
+ # }else{
+ # push @where, "1=0";
+ # }
+ #} else {
+ push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
+ 'table' => 'cust_main',
+ 'null_right' => 'View/link unlinked services',
+ );
+ #}
+
+ push @where, @{ $params->{'where'} } if $params->{'where'};
+
+ my $addl_from = join(' ', @from);
+ my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
+
+ my $table = $class->table;
+
+ my $count_query = "SELECT COUNT(*) FROM $table $addl_from $extra_sql";
+ #if ( keys %svc_X ) {
+ # $count_query .= ' WHERE '.
+ # join(' AND ', map "$_ = ". dbh->quote($svc_X{$_}),
+ # keys %svc_X
+ # );
+ #}
+
+ {
+ 'table' => $table,
+ 'hashref' => {},
+ 'select' => join(', ',
+ "$table.*",
+ 'part_svc.svc',
+ 'cust_main.custnum',
+ @{ $params->{'addl_select'} || [] },
+ FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
+ ),
+ 'addl_from' => $addl_from,
+ 'extra_sql' => $extra_sql,
+ 'order_by' => $params->{'order_by'},
+ 'count_query' => $count_query,
+ };
+
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/svc_Tower_Mixin.pm b/FS/FS/svc_Tower_Mixin.pm
index 6adbc6f5e..3da07c1cd 100644
--- a/FS/FS/svc_Tower_Mixin.pm
+++ b/FS/FS/svc_Tower_Mixin.pm
@@ -27,12 +27,10 @@ towernum or sectornum can also contain 'none' to allow null values.
=cut
sub tower_sector_sql {
- my $class = shift;
- my $params = shift;
- return '' unless keys %$params;
- my $where = '';
+ my( $class, $params ) = @_;
+ return () unless keys %$params;
- my @where;
+ my @where = ();
for my $field (qw(towernum sectornum)) {
my $value = $params->{$field} or next;
if ( ref $value and grep { $_ } @$value ) {
diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm
index 8e71d829d..26d6e5b72 100644
--- a/FS/FS/svc_acct.pm
+++ b/FS/FS/svc_acct.pm
@@ -15,6 +15,7 @@ use vars qw( $DEBUG $me $conf $skip_fuzzyfiles
$username_noperiod $username_nounderscore $username_nodash
$username_uppercase $username_percent $username_colon
$username_slash $username_equals $username_pound
+ $username_exclamation
$password_noampersand $password_noexclamation
$warning_template $warning_from $warning_subject $warning_mimetype
$warning_cc
@@ -85,6 +86,7 @@ FS::UID->install_callback( sub {
$username_slash = $conf->exists('username-slash');
$username_equals = $conf->exists('username-equals');
$username_pound = $conf->exists('username-pound');
+ $username_exclamation = $conf->exists('username-exclamation');
$password_noampersand = $conf->exists('password-noexclamation');
$password_noexclamation = $conf->exists('password-noexclamation');
$dirhash = $conf->config('dirhash') || 0;
@@ -1193,7 +1195,7 @@ sub check {
my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
- $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:\/\=\#]{$usernamemin,$ulen})$/i
+ $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:\/\=\#\!]{$usernamemin,$ulen})$/i
or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
$recref->{username} = $1;
@@ -1234,6 +1236,9 @@ sub check {
unless ( $username_pound ) {
$recref->{username} =~ /\#/ and return $uerror;
}
+ unless ( $username_exclamation ) {
+ $recref->{username} =~ /\!/ and return $uerror;
+ }
$recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
@@ -1890,12 +1895,14 @@ sub email {
$self->username. '@'. $self->domain(@_);
}
+
=item acct_snarf
Returns an array of FS::acct_snarf records associated with the account.
=cut
+# unused as originally intended, but now by Communigate Pro "RPOP"
sub acct_snarf {
my $self = shift;
qsearch({
@@ -2817,116 +2824,39 @@ Arrayref of additional WHERE clauses, will be ANDed together.
=cut
-sub search {
- my ($class, $params) = @_;
-
- my @from = (
- ' LEFT JOIN cust_svc USING ( svcnum ) ',
- ' LEFT JOIN part_svc USING ( svcpart ) ',
- ' LEFT JOIN cust_pkg USING ( pkgnum ) ',
- ' LEFT JOIN cust_main USING ( custnum ) ',
- );
+sub _search_svc {
+ my( $class, $params, $from, $where ) = @_;
- my @where = ();
+ #these two should probably move to svc_Domain_Mixin ?
# domain
if ( $params->{'domain'} ) {
my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
#preserve previous behavior & bubble up an error if $svc_domain not found?
- push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
+ push @$where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
}
# domsvc
if ( $params->{'domsvc'} =~ /^(\d+)$/ ) {
- push @where, "domsvc = $1";
+ push @$where, "domsvc = $1";
}
- #unlinked
- push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
-
- #agentnum
- if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
- push @where, "cust_main.agentnum = $1";
- }
-
- #custnum
- if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
- push @where, "custnum = $1";
- }
-
- #pkgpart
- if ( $params->{'pkgpart'} && scalar(@{ $params->{'pkgpart'} }) ) {
- #XXX untaint or sql quote
- push @where,
- 'cust_pkg.pkgpart IN ('. join(',', @{ $params->{'pkgpart'} } ). ')';
- }
# popnum
if ( $params->{'popnum'} =~ /^(\d+)$/ ) {
- push @where, "popnum = $1";
+ push @$where, "popnum = $1";
}
- # svcpart
- if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
- push @where, "svcpart = $1";
- }
- if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
- push @from, ' LEFT JOIN export_svc USING ( svcpart )';
- push @where, "exportnum = $1";
- }
+ #and these in svc_Tower_Mixin, or maybe we never should have done svc_acct
+ # towers (or, as mark thought, never should have done svc_broadband)
# sector and tower
my @where_sector = $class->tower_sector_sql($params);
if ( @where_sector ) {
- push @where, @where_sector;
- push @from, ' LEFT JOIN tower_sector USING ( sectornum )';
- }
-
- # here is the agent virtualization
- #if ($params->{CurrentUser}) {
- # my $access_user =
- # qsearchs('access_user', { username => $params->{CurrentUser} });
- #
- # if ($access_user) {
- # push @where, $access_user->agentnums_sql('table'=>'cust_main');
- # }else{
- # push @where, "1=0";
- # }
- #} else {
- push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
- 'table' => 'cust_main',
- 'null_right' => 'View/link unlinked services',
- );
- #}
-
- push @where, @{ $params->{'where'} } if $params->{'where'};
-
- my $addl_from = join(' ', @from);
- my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
-
- my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql";
- #if ( keys %svc_acct ) {
- # $count_query .= ' WHERE '.
- # join(' AND ', map "$_ = ". dbh->quote($svc_acct{$_}),
- # keys %svc_acct
- # );
- #}
-
- my $sql_query = {
- 'table' => 'svc_acct',
- 'hashref' => {}, # \%svc_acct,
- 'select' => join(', ',
- 'svc_acct.*',
- 'part_svc.svc',
- 'cust_main.custnum',
- FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
- ),
- 'addl_from' => $addl_from,
- 'extra_sql' => $extra_sql,
- 'order_by' => $params->{'order_by'},
- 'count_query' => $count_query,
- };
+ push @$where, @where_sector;
+ push @$from, ' LEFT JOIN tower_sector USING ( sectornum )';
+ }
}
diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm
index af8135304..002aa55ce 100755
--- a/FS/FS/svc_broadband.pm
+++ b/FS/FS/svc_broadband.pm
@@ -103,10 +103,10 @@ sub table_info {
'ip_field' => 'ip_addr',
'fields' => {
'svcnum' => 'Service',
- 'description' => 'Descriptive label for this particular device',
- 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
- 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
- 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
+ 'description' => 'Descriptive label',
+ 'speed_down' => 'Download speed (Kbps)',
+ 'speed_up' => 'Upload speed (Kbps)',
+ 'ip_addr' => 'IP address',
'blocknum' =>
{ 'label' => 'Address block',
'type' => 'select',
@@ -134,6 +134,15 @@ sub table_info {
disable_inventory => 1,
multiple => 1,
},
+ 'radio_serialnum' => 'Radio Serial Number',
+ 'radio_location' => 'Radio Location',
+ 'poe_location' => 'POE Location',
+ 'rssi' => 'RSSI',
+ 'suid' => 'SUID',
+ 'shared_svcnum' => { label => 'Shared Service',
+ type => 'search-svc_broadband',
+ disable_inventory => 1,
+ },
},
};
}
@@ -175,115 +184,44 @@ Parameters:
=cut
-sub search {
- my ($class, $params) = @_;
- my @where = ();
- my @from = (
- 'LEFT JOIN cust_svc USING ( svcnum )',
- 'LEFT JOIN part_svc USING ( svcpart )',
- 'LEFT JOIN cust_pkg USING ( pkgnum )',
- 'LEFT JOIN cust_main USING ( custnum )',
- );
-
- # based on FS::svc_acct::search, probably the most mature of the bunch
- #unlinked
- push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
-
- #agentnum
- if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
- push @where, "cust_main.agentnum = $1";
- }
- push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
- 'null_right' => 'View/link unlinked services',
- 'table' => 'cust_main'
- );
-
- #custnum
- if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
- push @where, "custnum = $1";
- }
-
- #pkgpart, now properly untainted, can be arrayref
- for my $pkgpart ( $params->{'pkgpart'} ) {
- if ( ref $pkgpart ) {
- my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
- push @where, "cust_pkg.pkgpart IN ($where)" if $where;
- }
- elsif ( $pkgpart =~ /^(\d+)$/ ) {
- push @where, "cust_pkg.pkgpart = $1";
- }
- }
+sub _search_svc {
+ my( $class, $params, $from, $where ) = @_;
#routernum, can be arrayref
for my $routernum ( $params->{'routernum'} ) {
# this no longer uses addr_block
if ( ref $routernum and grep { $_ } @$routernum ) {
my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
- my @orwhere;
+ my @orwhere = ();
push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
push @orwhere, "svc_broadband.routernum IS NULL"
if grep /^none$/, @$routernum;
- push @where, '( '.join(' OR ', @orwhere).' )';
+ push @$where, '( '.join(' OR ', @orwhere).' )';
}
elsif ( $routernum =~ /^(\d+)$/ ) {
- push @where, "svc_broadband.routernum = $1";
+ push @$where, "svc_broadband.routernum = $1";
}
elsif ( $routernum eq 'none' ) {
- push @where, "svc_broadband.routernum IS NULL";
+ push @$where, "svc_broadband.routernum IS NULL";
}
}
+ #this should probably move to svc_Tower_Mixin, or maybe we never should have
+ # done svc_acct # towers (or, as mark thought, never should have done
+ # svc_broadband)
+
#sector and tower, as above
my @where_sector = $class->tower_sector_sql($params);
if ( @where_sector ) {
- push @where, @where_sector;
- push @from, 'LEFT JOIN tower_sector USING ( sectornum )';
+ push @$where, @where_sector;
+ push @$from, 'LEFT JOIN tower_sector USING ( sectornum )';
}
- #svcnum
- if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
- push @where, "svcnum = $1";
- }
-
- #svcpart
- if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
- push @where, "svcpart = $1";
- }
-
- #exportnum
- if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
- push @from, 'LEFT JOIN export_svc USING ( svcpart )';
- push @where, "exportnum = $1";
- }
-
#ip_addr
if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
- push @where, "ip_addr = '$1'";
+ push @$where, "ip_addr = '$1'";
}
- #custnum
- if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
- push @where, "custnum = $1";
- }
-
- my $addl_from = join(' ', @from);
- my $extra_sql = '';
- $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
- my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
- return( {
- 'table' => 'svc_broadband',
- 'hashref' => {},
- 'select' => join(', ',
- 'svc_broadband.*',
- 'part_svc.svc',
- 'cust_main.custnum',
- FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
- ),
- 'extra_sql' => $extra_sql,
- 'addl_from' => $addl_from,
- 'order_by' => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
- 'count_query' => $count_query,
- } );
}
=item search_sql STRING
@@ -296,15 +234,31 @@ sub search_sql {
my( $class, $string ) = @_;
if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
$class->search_sql_field('ip_addr', $string );
- }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
+ } elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
$class->search_sql_field('mac_addr', uc($string));
- }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
+ } elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
$class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
+ } elsif ( $string =~ /^(\d+)$/ ) {
+ my $table = $class->table;
+ "$table.svcnum = $1";
} else {
'1 = 0'; #false
}
}
+=item smart_search STRING
+
+=cut
+
+sub smart_search {
+ my( $class, $string ) = @_;
+ qsearch({
+ 'table' => $class->table, #'svc_broadband',
+ 'hashref' => {},
+ 'extra_sql' => 'WHERE '. $class->search_sql($string),
+ });
+}
+
=item label
Returns the IP address.
@@ -313,7 +267,12 @@ Returns the IP address.
sub label {
my $self = shift;
- $self->ip_addr;
+ my $label = 'IP:'. ($self->ip_addr || 'Unknown');
+ $label .= ', MAC:'. $self->mac_addr
+ if $self->mac_addr;
+ $label .= ' ('. $self->description. ')'
+ if $self->description;
+ return $label;
}
=item insert [ , OPTION => VALUE ... ]
@@ -377,7 +336,7 @@ sub check {
# remove delimiters
my $mac_addr = uc($self->get('mac_addr'));
- $mac_addr =~ s/[-: ]//g;
+ $mac_addr =~ s/[\W_]//g;
$self->set('mac_addr', $mac_addr);
my $error =
@@ -396,6 +355,12 @@ sub check {
|| $self->ut_sfloatn('altitude')
|| $self->ut_textn('vlan_profile')
|| $self->ut_textn('plan_id')
+ || $self->ut_alphan('radio_serialnum')
+ || $self->ut_textn('radio_location')
+ || $self->ut_textn('poe_location')
+ || $self->ut_snumbern('rssi')
+ || $self->ut_numbern('suid')
+ || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
;
return $error if $error;
diff --git a/FS/FS/svc_export_machine.pm b/FS/FS/svc_export_machine.pm
index 10f7b6821..7ca20ccb6 100644
--- a/FS/FS/svc_export_machine.pm
+++ b/FS/FS/svc_export_machine.pm
@@ -40,6 +40,10 @@ fields are currently supported:
primary key
+=item exportnum
+
+Export definition, see L
+
=item svcnum
Customer service, see L
diff --git a/FS/FS/svc_hardware.pm b/FS/FS/svc_hardware.pm
index af6865f12..b28cc9ef5 100644
--- a/FS/FS/svc_hardware.pm
+++ b/FS/FS/svc_hardware.pm
@@ -105,9 +105,13 @@ sub search_sql {
my ($class, $string) = @_;
my @where = ();
- my $ip = NetAddr::IP->new($string);
- if ( $ip ) {
- push @where, $class->search_sql_field('ip_addr', $ip->addr);
+ if ( $string =~ /^[\d\.:]+$/ ) {
+ # if the string isn't an IP address, this will waste several seconds
+ # attempting a DNS lookup. so try to filter those out.
+ my $ip = NetAddr::IP->new($string);
+ if ( $ip ) {
+ push @where, $class->search_sql_field('ip_addr', $ip->addr);
+ }
}
if ( $string =~ /^(\w+)$/ ) {
@@ -164,7 +168,7 @@ sub check {
return $x unless ref $x;
my $hw_addr = $self->getfield('hw_addr');
- $hw_addr = join('', split(/\W/, $hw_addr));
+ $hw_addr = join('', split(/[_\W]/, $hw_addr));
if ( $conf->exists('svc_hardware-check_mac_addr') ) {
$hw_addr = uc($hw_addr);
$hw_addr =~ /^[0-9A-F]{12}$/
diff --git a/FS/FS/svc_pbx.pm b/FS/FS/svc_pbx.pm
index 4182a1315..66e51da71 100644
--- a/FS/FS/svc_pbx.pm
+++ b/FS/FS/svc_pbx.pm
@@ -292,7 +292,9 @@ to allow title to indicate a range of IP addresses.
=item begin, end: Start and end of date range, as unix timestamp.
-=item cdrtypenum: Only return CDRs with this type number.
+=item cdrtypenum: Only return CDRs with this type.
+
+=item calltypenum: Only return CDRs with this call type.
=back
@@ -310,6 +312,9 @@ sub psearch_cdrs {
if ($options{'cdrtypenum'}) {
$hash{'cdrtypenum'} = $options{'cdrtypenum'};
}
+ if ($options{'calltypenum'}) {
+ $hash{'calltypenum'} = $options{'calltypenum'};
+ }
my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
diff --git a/FS/FS/svc_phone.pm b/FS/FS/svc_phone.pm
index 1296c1e85..3cc1adc66 100644
--- a/FS/FS/svc_phone.pm
+++ b/FS/FS/svc_phone.pm
@@ -23,10 +23,11 @@ $DEBUG = 0;
@pw_set = ( 'a'..'k', 'm','n', 'p-z', 'A'..'N', 'P'..'Z' , '2'..'9' );
#ask FS::UID to run this stuff for us later
-$FS::UID::callback{'FS::svc_acct'} = sub {
+FS::UID->install_callback( sub {
$conf = new FS::Conf;
$phone_name_max = $conf->config('svc_phone-phone_name-max_length');
-};
+}
+);
=head1 NAME
@@ -68,6 +69,10 @@ primary key
=item phonenum
+=item sim_imsi
+
+SIM IMSI (http://en.wikipedia.org/wiki/International_mobile_subscriber_identity)
+
=item sip_password
=item pin
@@ -147,6 +152,7 @@ sub table_info {
disable_select => 1,
},
'phonenum' => 'Phone number',
+ 'sim_imsi' => 'IMSI', #http://en.wikipedia.org/wiki/International_mobile_subscriber_identity
'pin' => { label => 'Voicemail PIN', #'Personal Identification Number',
type => 'text',
disable_inventory => 1,
@@ -466,6 +472,7 @@ sub check {
$self->ut_numbern('svcnum')
|| $self->ut_numbern('countrycode')
|| $self->$phonenum_check_method('phonenum')
+ || $self->ut_numbern('sim_imsi')
|| $self->ut_anything('sip_password')
|| $self->ut_numbern('pin')
|| $self->ut_textn('phone_name')
@@ -486,6 +493,10 @@ sub check {
;
return $error if $error;
+ return 'Illegal IMSI (not 14-15 digits)' #shorter?
+ if length($self->sim_imsi)
+ && ( length($self->sim_imsi) < 14 || length($self->sim_imsi) > 15 );
+
# LNP data validation
return 'Cannot set LNP fields: no LNP in progress'
if ( ($self->lnp_desired_due_date || $self->lnp_due_date
@@ -673,10 +684,14 @@ with the chosen prefix.
=item begin, end: Start and end of a date range, as unix timestamp.
-=item cdrtypenum: Only return CDRs with this type number.
+=item cdrtypenum: Only return CDRs with this type.
+
+=item calltypenum: Only return CDRs with this call type.
=item disable_src => 1: Only match on "charged_party", not "src".
+=item nonzero: Only return CDRs where duration > 0.
+
=item by_svcnum: not supported for svc_phone
=item billsec_sum: Instead of returning all of the CDRs, return a single
@@ -722,6 +737,9 @@ sub psearch_cdrs {
if ($options{'cdrtypenum'}) {
$hash{'cdrtypenum'} = $options{'cdrtypenum'};
}
+ if ($options{'calltypenum'}) {
+ $hash{'calltypenum'} = $options{'calltypenum'};
+ }
my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
@@ -744,6 +762,9 @@ sub psearch_cdrs {
if ( $options{'end'} ) {
push @where, 'startdate < '. $options{'end'};
}
+ if ( $options{'nonzero'} ) {
+ push @where, 'duration > 0';
+ }
my $extra_sql = ( keys(%hash) ? ' AND ' : ' WHERE ' ). join(' AND ', @where );
@@ -770,6 +791,30 @@ sub get_cdrs {
qsearch ( $psearch->{query} )
}
+=item sum_cdrs
+
+Takes the same options as psearch_cdrs, but returns a single row containing
+"count" (the number of CDRs) and the sums of the following fields: duration,
+billsec, rated_price, rated_seconds, rated_minutes.
+
+Note that if any calls are not rated, their rated_* fields will be null.
+If you want to use those fields, pass the 'status' option to limit to
+calls that have been rated. This is intentional; please don't "fix" it.
+
+=cut
+
+sub sum_cdrs {
+ my $self = shift;
+ my $psearch = $self->psearch_cdrs(@_);
+ $psearch->{query}->{'select'} = join(',',
+ 'COUNT(*) AS count',
+ map { "SUM($_) AS $_" }
+ qw(duration billsec rated_price rated_seconds rated_minutes)
+ );
+ # hack
+ $psearch->{query}->{'extra_sql'} =~ s/ ORDER BY.*$//;
+ qsearchs ( $psearch->{query} );
+}
=back
diff --git a/FS/MANIFEST b/FS/MANIFEST
index f954fe8dd..94232905e 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -13,6 +13,7 @@ bin/freeside-deloutsource
bin/freeside-deloutsourceuser
bin/freeside-deluser
bin/freeside-email
+bin/freeside-phonenum_list
bin/freeside-queued
bin/freeside-radgroup
bin/freeside-reexport
@@ -33,7 +34,6 @@ FS/ClientAPI_SessionCache.pm
FS/ClientAPI_XMLRPC.pm
FS/ClientAPI/passwd.pm
FS/ClientAPI/Agent.pm
-FS/ClientAPI/Bulk.pm
FS/ClientAPI/MasonComponent.pm
FS/ClientAPI/MyAccount.pm
FS/ClientAPI/PrepaidPhone.pm
@@ -74,7 +74,6 @@ FS/cust_main/Billing_Realtime.pm
FS/cust_main/Import.pm
FS/cust_main/Packages.pm
FS/cust_main/Search.pm
-FS/cust_main/_Marketgear.pm
FS/cust_main_Mixin.pm
FS/cust_main_county.pm
FS/cust_main_invoice.pm
@@ -149,8 +148,6 @@ FS/part_pkg/sqlradacct_hour.pm
FS/part_pkg/subscription.pm
FS/part_pkg/voip_sqlradacct.pm
FS/part_pkg/voip_cdr.pm
-FS/part_pkg/base_rate.pm
-FS/part_pkg/base_delayed.pm
FS/part_pop_local.pm
FS/part_referral.pm
FS/part_svc.pm
@@ -493,6 +490,8 @@ FS/phone_type.pm
t/phone_type.t
FS/contact_email.pm
t/contact_email.t
+FS/contact_Mixin.pm
+t/contact_Mixin.t
FS/prospect_main.pm
t/prospect_main.t
FS/o2m_Common.pm
@@ -679,3 +678,15 @@ FS/log.pm
t/log.t
FS/log_context.pm
t/log_context.t
+FS/part_pkg_usage_class.pm
+t/part_pkg_usage_class.t
+FS/cust_pkg_usage.pm
+t/cust_pkg_usage.t
+FS/part_pkg_usage_class.pm
+t/part_pkg_usage_class.t
+FS/part_pkg_usage.pm
+t/part_pkg_usage.t
+FS/cdr_cust_pkg_usage.pm
+t/cdr_cust_pkg_usage.t
+FS/part_pkg_msgcat.pm
+t/part_pkg_msgcat.t
diff --git a/FS/bin/freeside-cdr-sftp_and_import b/FS/bin/freeside-cdr-sftp_and_import
index 7f2693fcb..c37ff11d6 100755
--- a/FS/bin/freeside-cdr-sftp_and_import
+++ b/FS/bin/freeside-cdr-sftp_and_import
@@ -218,6 +218,8 @@ or FTP and then import them into the database.
-c: cdrtypenum to set, defaults to none
+-g: File is gzipped
+
user: freeside username
format: CDR format name
diff --git a/FS/bin/freeside-cdrrated b/FS/bin/freeside-cdrrated
index 131b56a7e..99ea67594 100644
--- a/FS/bin/freeside-cdrrated
+++ b/FS/bin/freeside-cdrrated
@@ -33,9 +33,11 @@ if ( @cdrtypenums ) {
$extra_sql .= ' AND cdrtypenum IN ('. join(',', @cdrtypenums ). ')';
}
-our %svcnum = ();
-our %pkgpart = ();
-our %part_pkg = ();
+our %svcnum = (); # phonenum => svcnum
+our %pkgnum = (); # phonenum => pkgnum
+our %cust_pkg = (); # pkgnum => cust_pkg (NOT phonenum => cust_pkg!)
+our %pkgpart = (); # phonenum => pkgpart
+our %part_pkg = (); # phonenum => part_pkg
#some false laziness w/freeside-cdrrewrited
@@ -91,6 +93,9 @@ while (1) {
next;
}
+ $pkgnum{$number} = $cust_pkg->pkgnum;
+ $cust_pkg{$cust_pkg->pkgnum} ||= $cust_pkg;
+
#get the package, search through the part_pkg and linked for a voip_cdr def w/matching cdrtypenum (or no use_cdrtypenum)
my @part_pkg =
grep { $_->plan eq 'voip_cdr'
@@ -126,10 +131,11 @@ while (1) {
#}
#XXX if $part_pkg->option('min_included') then we can't prerate this CDR
-
+
my $error = $cdr->rate(
'part_pkg' => $part_pkg{ $pkgpart{$number} },
- 'svcnum' => $svcnum{ $number },
+ 'cust_pkg' => $cust_pkg{ $pkgnum{$number} },
+ 'svcnum' => $svcnum{$number},
);
if ( $error ) {
#XXX ???
diff --git a/FS/bin/freeside-cdrrewrited b/FS/bin/freeside-cdrrewrited
index f2c3926fb..16f931fbf 100644
--- a/FS/bin/freeside-cdrrewrited
+++ b/FS/bin/freeside-cdrrewrited
@@ -30,9 +30,9 @@ die "not running; cdr-asterisk_forward_rewrite, cdr-charged_party_rewrite ".
#--
-my %accountcode_unmatch = ();
-my $accountcode_retry = 4 * 60 * 60; # 4 hours
-my $accountcode_giveup = 4 * 24 * 60 * 60; # 4 days
+my %sessionnum_unmatch = ();
+my $sessionnum_retry = 4 * 60 * 60; # 4 hours
+my $sessionnum_giveup = 4 * 24 * 60 * 60; # 4 days
my %cdr_type = map { lc($_->cdrtypename) => $_->cdrtypenum }
qsearch('cdr_type',{});
@@ -45,8 +45,8 @@ while (1) {
# instead of just doing this search like normal CDRs
#hmm :/
- my @recent = grep { ($accountcode_unmatch{$_} + $accountcode_retry) > time }
- keys %accountcode_unmatch;
+ my @recent = grep { ($sessionnum_unmatch{$_} + $sessionnum_retry) > time }
+ keys %sessionnum_unmatch;
my $extra_sql = scalar(@recent)
? ' AND acctid NOT IN ('. join(',', @recent). ') '
: '';
@@ -136,45 +136,62 @@ while (1) {
}
- if ( $conf->exists('cdr-taqua-accountcode_rewrite')
- && $cdr->lastapp eq 'acctcode' && $cdr->cdrtypenum == 1
+ if ( $cdr->cdrtypenum == 1
+ and $cdr->lastapp
+ and (
+ $conf->exists('cdr-taqua-accountcode_rewrite') or
+ $conf->exists('cdr-taqua-callerid_rewrite') )
)
{
#find the matching CDR
- my $primary = qsearchs('cdr', {
- 'sessionnum' => $cdr->sessionnum,
- 'src' => $cdr->subscriber,
- #'accountcode' => '',
- });
+ my %search = ( 'sessionnum' => $cdr->sessionnum );
+ if ( $cdr->lastapp eq 'acctcode' ) {
+ $search{'src'} = $cdr->subscriber;
+ } elsif ( $cdr->lastapp eq 'CallerId' ) {
+ $search{'dst'} = $cdr->subscriber;
+ }
+ my $primary = qsearchs('cdr', \%search);
unless ( $primary ) {
my $cantfind = "can't find primary CDR with session ". $cdr->sessionnum.
", src ". $cdr->subscriber;
- if ( $cdr->calldate_unix + $accountcode_giveup < time ) {
+ if ( $cdr->calldate_unix + $sessionnum_giveup < time ) {
warn "ERROR: $cantfind; giving up\n";
- push @status, 'taqua-accountcode-NOTFOUND';
+ push @status, 'taqua-sessionnum-NOTFOUND';
$cdr->status('done'); #so it doesn't try to rate
- delete $accountcode_unmatch{$cdr->acctid}; #so it doesn't suck mem
+ delete $sessionnum_unmatch{$cdr->acctid}; #so it doesn't suck mem
} else {
warn "WARNING: $cantfind; will keep trying\n";
- $accountcode_unmatch{$cdr->acctid} = time;
+ $sessionnum_unmatch{$cdr->acctid} = time;
next;
}
} else {
- $primary->accountcode( $cdr->lastdata );
+ if ( $cdr->lastapp eq 'acctcode' ) {
+ # lastdata contains the dialed account code
+ $primary->accountcode( $cdr->lastdata );
+ push @status, 'taqua-accountcode';
+ } elsif ( $cdr->lastapp eq 'CallerId' ) {
+ # lastdata contains "allowed" or "restricted"
+ # or case variants thereof
+ if ( lc($cdr->lastdata) eq 'restricted' ) {
+ $primary->clid( 'PRIVATE' );
+ }
+ push @status, 'taqua-callerid';
+ } else {
+ warn "unknown Taqua service name: ".$cdr->lastapp."\n";
+ }
#$primary->freesiderewritestatus( 'taqua-accountcode-primary' );
- my $error = $primary->replace;
+ my $error = $primary->replace if $primary->modified;
if ( $error ) {
warn "WARNING: error rewriting primary CDR (will retry): $error\n";
next;
}
$skip{$primary->acctid} = 1;
- push @status, 'taqua-accountcode';
$cdr->status('done'); #so it doesn't try to rate
}
@@ -214,7 +231,10 @@ sub _shouldrun {
$conf->exists('cdr-asterisk_forward_rewrite')
|| $conf->exists('cdr-asterisk_australia_rewrite')
|| $conf->exists('cdr-charged_party_rewrite')
- || $conf->exists('cdr-taqua-accountcode_rewrite');
+ || $conf->exists('cdr-taqua-accountcode_rewrite')
+ || $conf->exists('cdr-taqua-callerid_rewrite')
+ || 0
+ ;
}
sub usage {
diff --git a/FS/bin/freeside-ipifony-download b/FS/bin/freeside-ipifony-download
index 837cc3329..9df4db08a 100644
--- a/FS/bin/freeside-ipifony-download
+++ b/FS/bin/freeside-ipifony-download
@@ -9,6 +9,7 @@ use FS::UID qw(adminsuidsetup);
use FS::Record qw(qsearch qsearchs);
use FS::cust_main;
use FS::Conf;
+use File::Copy qw(copy);
use Text::CSV;
my %opt;
@@ -116,7 +117,7 @@ die "failed to connect to '$sftpuser\@$host'\n(".$sftp->error.")\n"
$sftp->setcwd($path) if $path;
-my $files = $sftp->ls('.', wanted => qr/\.csv$/, names_only => 1);
+my $files = $sftp->ls('ready', wanted => qr/\.csv$/, names_only => 1);
if (!@$files) {
print STDERR "No charge files found.\n" if $opt{v};
exit(-1);
@@ -129,23 +130,23 @@ my %is_e911 = map {$_ => 1} @E911_CODES;
FILE: foreach my $filename (@$files) {
print STDERR "Retrieving $filename\n" if $opt{v};
- $sftp->get("$filename", "$tmpdir/$filename");
+ $sftp->get("ready/$filename", "$tmpdir/$filename");
if($sftp->error) {
warn "failed to download $filename\n";
next FILE;
}
# make sure server archive dir exists
- if ( !$sftp->stat('Archive') ) {
- print STDERR "Creating $path/Archive\n" if $opt{v};
- $sftp->mkdir('Archive');
+ if ( !$sftp->stat('done') ) {
+ print STDERR "Creating $path/done\n" if $opt{v};
+ $sftp->mkdir('done');
if($sftp->error) {
# something is seriously wrong
die "failed to create archive directory on server:\n".$sftp->error."\n";
}
}
#move to server archive dir
- $sftp->rename("$filename", "Archive/$filename");
+ $sftp->rename("ready/$filename", "done/$filename");
if($sftp->error) {
warn "failed to archive $filename on server:\n".$sftp->error."\n";
} # process it anyway, I guess/
@@ -159,11 +160,6 @@ FILE: foreach my $filename (@$files) {
}
open my $fh, "<$tmpdir/$filename";
- my $header = <$fh>;
- if ($header !~ /^"cust_id"/) {
- warn "warning: $filename has incorrect header row:\n$header\n";
- # but try anyway
- }
my $csv = Text::CSV->new; # orthodox CSV
my %hash;
while (my $line = <$fh>) {
@@ -172,6 +168,11 @@ FILE: foreach my $filename (@$files) {
next FILE;
};
@hash{@fields} = $csv->fields();
+ if ( $hash{custnum} =~ /^cust/ ) {
+ # there appears to be a header row
+ print STDERR "skipping header row\n" if $opt{v};
+ next;
+ }
my $cust_main =
$cust_main{$hash{custnum}} ||= FS::cust_main->by_key($hash{custnum});
if (!$cust_main) {
@@ -184,10 +185,11 @@ FILE: foreach my $filename (@$files) {
my $amount = sprintf('%.2f',$hash{quantity} * $hash{unit_price});
# construct arguments for $cust_main->charge
my %charge_opt = (
- amount => $amount,
+ amount => $hash{unit_price},
quantity => $hash{quantity},
start_date => $cust_main->next_bill_date,
- pkg => $hash{date_desc},
+ pkg => $hash{date_desc} .
+ ' (' . $hash{quantity} . ' @ $' . $hash{unit_price} . ' ea)',
taxclass => $TAXCLASSES{ $hash{taxclass} },
);
if (my $classname = $hash{classname}) {
@@ -221,7 +223,7 @@ FILE: foreach my $filename (@$files) {
$num_errors++;
} else {
$num_charges++;
- $sum_charges += $hash{amount};
+ $sum_charges += $amount;
}
if ( $opt{e} and $is_e911{$hash{classname}} ) {
diff --git a/FS/bin/freeside-phonenum_list b/FS/bin/freeside-phonenum_list
new file mode 100755
index 000000000..19b564dee
--- /dev/null
+++ b/FS/bin/freeside-phonenum_list
@@ -0,0 +1,86 @@
+#!/usr/bin/perl
+
+use strict;
+use vars qw( $opt_c $opt_o $opt_l $opt_p $opt_b $opt_d $opt_s $opt_t );
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup);
+use FS::Conf;
+use FS::Record qw(qsearch);
+use FS::svc_phone;
+
+getopts('colp:b:d:s:t:');
+
+my $user = shift or &usage;
+adminsuidsetup $user;
+
+my $conf = new FS::Conf;
+my $default_locale = $conf->config('locale') || 'en_US';
+
+my %search = ();
+
+$search{payby} = [ split(/\s*,\s*/, $opt_p) ] if $opt_p;
+$search{balance} = $opt_b if $opt_b;
+$search{balance_days} = $opt_d if $opt_d;
+$search{svcpart} = [ split(/\s*,\s*/, $opt_s) ] if $opt_s;
+$search{cust_status} = lc($opt_t) if $opt_t;
+
+my @svc_phone = qsearch( FS::svc_phone->search(\%search) );
+
+foreach my $svc_phone (@svc_phone) {
+ print $svc_phone->countrycode if $opt_c;
+ print $svc_phone->phonenum;
+ print '@'. $svc_phone->domain if $opt_o;
+ if ( $opt_l ) {
+ my $cust_pkg = $svc_phone->cust_svc->cust_pkg;
+ print ','. ($cust_pkg && $cust_pkg->cust_main->locale || $default_locale);
+ }
+ print "\n";
+}
+
+sub usage {
+ die "usage: freeside-phonenum_list [ -c ] [ -o ] [ -l ] [ -p payby,payby... ] [ -b balance [ -d balance_days ] ] [ -s svcpart,svcpart... ] username \n";
+}
+
+=head1 NAME
+
+freeside-phonenum_list
+
+=head1 SYNOPSIS
+ freeside-phonenum_list [ -c ] [ -o ] [ -l ] [ -p payby,payby... ] [ -b balance [ -d balance_days ] ] [ -s svcpart,svcpart... ] username
+
+=head1 DESCRIPTION
+
+Command-line tool to list phone numbers.
+
+Display options:
+
+-c: Include country code
+
+-o: Include domain
+
+-l: Include customer locale
+
+Selection options:
+
+-p: Customer payby (CARD, BILL, etc.). Separate multiple values with commas.
+
+-b: Customer balance over (or equal to) this amount
+
+-d: Customer balance age over this many days
+
+-s: Service definition (svcpart). Separate multiple values with commas.
+
+-t: Customer status: prospect, active, ordered, inactive, suspended or cancelled
+
+username: Employee username
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L, L
+
+=cut
+
+1;
+
diff --git a/FS/bin/freeside-queued b/FS/bin/freeside-queued
index 2fd80255e..dcc6ac4ba 100644
--- a/FS/bin/freeside-queued
+++ b/FS/bin/freeside-queued
@@ -212,8 +212,10 @@ while (1) {
# don't put @args in the log, may expose passwords
$log->info('starting job ('.$ljob->job.')');
warn 'running "&'. $ljob->job. '('. join(', ', @args). ")\n" if $DEBUG;
+ local $FS::UID::AutoCommit = 0; # so that we can clean up failures
eval $eval; #throw away return value? suppose so
if ( $@ ) {
+ dbh->rollback;
my %hash = $ljob->hash;
$hash{'statustext'} = $@;
if ( $hash{'statustext'} =~ /\/misc\/queued_report/ ) { #use return?
@@ -225,8 +227,10 @@ while (1) {
my $fjob = new FS::queue( \%hash );
my $error = $fjob->replace($ljob);
die $error if $error;
+ dbh->commit; # for the status change only
} else {
$ljob->delete;
+ dbh->commit; # for the job itself
}
if ( UNIVERSAL::can(dbh, 'sprintProfile') ) {
diff --git a/FS/bin/freeside-selfservice-server b/FS/bin/freeside-selfservice-server
index c10623c96..9df313fec 100644
--- a/FS/bin/freeside-selfservice-server
+++ b/FS/bin/freeside-selfservice-server
@@ -108,31 +108,7 @@ while (1) {
if ( $keepalives && $keepalive_count++ > 10 ) {
$keepalive_count = 0;
lock_write;
-
nstore_fd( { _token => '_keepalive' }, $writer );
-
-#commenting izoom stuff out until we can move it to a branch (or just remove)
-# foreach my $agent ( qsearch( 'agent', { disabled => '' } ) ) {
-# my $config = qsearchs( 'conf', { name => 'selfservice-bulk_ftp_dir',
-# agentnum => $agent->agentnum,
-# } )
-# or next;
-#
-# my $session =
-# FS::ClientAPI->dispatch( 'Agent/agent_login',
-# { username => $agent->username,
-# password => $agent->_password,
-# }
-# );
-#
-# nstore_fd( { _token => '_ftp_scan',
-# dir => $config->value,
-# session_id => $session->{session_id},
-# },
-# $writer
-# );
-# }
-
unlock_write;
}
next;
diff --git a/FS/bin/freeside-upgrade b/FS/bin/freeside-upgrade
index b08a8401f..3d1c2e072 100755
--- a/FS/bin/freeside-upgrade
+++ b/FS/bin/freeside-upgrade
@@ -123,6 +123,8 @@ my $cf;
while ( $cf = $cfsth->fetchrow_hashref ) {
my $tbl = $cf->{'dbtable'};
my $name = $cf->{'name'};
+ $name = lc($name) unless driver_name =~ /^mysql/i;
+
@statements = grep { $_ !~ /^\s*ALTER\s+TABLE\s+(h_|)$tbl\s+DROP\s+COLUMN\s+cf_$name\s*$/i }
@statements;
push @statements,
diff --git a/FS/bin/freeside-username_list b/FS/bin/freeside-username_list
new file mode 100755
index 000000000..5352f02eb
--- /dev/null
+++ b/FS/bin/freeside-username_list
@@ -0,0 +1,84 @@
+#!/usr/bin/perl
+
+use strict;
+use vars qw( $opt_o $opt_l $opt_p $opt_b $opt_d $opt_s $opt_t );
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup);
+use FS::Conf;
+use FS::Record qw(qsearch);
+use FS::svc_acct;
+
+getopts('olp:b:d:s:t:');
+
+my $user = shift or &usage;
+adminsuidsetup $user;
+
+my $conf = new FS::Conf;
+my $default_locale = $conf->config('locale') || 'en_US';
+
+my %search = ();
+
+$search{payby} = [ split(/\s*,\s*/, $opt_p) ] if $opt_p;
+$search{balance} = $opt_b if $opt_b;
+$search{balance_days} = $opt_d if $opt_d;
+$search{svcpart} = [ split(/\s*,\s*/, $opt_s) ] if $opt_s;
+$search{cust_status} = lc($opt_t) if $opt_t;
+
+my @svc_acct = qsearch( FS::svc_acct->search(\%search) );
+
+foreach my $svc_acct (@svc_acct) {
+ print $svc_acct->username;
+ print '@'. $svc_acct->domain if $opt_o;
+ if ( $opt_l ) {
+ my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+ print ','. ($cust_pkg && $cust_pkg->cust_main->locale || $default_locale);
+ }
+ print "\n";
+}
+
+sub usage {
+ die "usage: freeside-username_list [ -c ] [ -l ] [ -p payby,payby... ] [ -b balance [ -d balance_days ] ] [ -s svcpart,svcpart... ] username \n";
+}
+
+=head1 NAME
+
+freeside-username_list
+
+=head1 SYNOPSIS
+
+ freeside-username_list [ -c ] [ -l ] [ -p payby,payby... ] [ -b balance [ -d balance_days ] ] [ -s svcpart,svcpart... ] username
+
+=head1 DESCRIPTION
+
+Command-line tool to list usernames.
+
+Display options:
+
+-o: Include domain
+
+-l: Include customer locale
+
+Selection options:
+
+-p: Customer payby (CARD, BILL, etc.). Separate multiple values with commas.
+
+-b: Customer balance over (or equal to) this amount
+
+-d: Customer balance age over this many days
+
+-s: Service definition (svcpart). Separate multiple values with commas.
+
+-t: Customer status: prospect, active, ordered, inactive, suspended or cancelled
+
+username: Employee username
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L, L
+
+=cut
+
+1;
+
diff --git a/FS/bin/freeside-wkhtmltopdf b/FS/bin/freeside-wkhtmltopdf
index c6c5531a5..f0c53e6da 100755
--- a/FS/bin/freeside-wkhtmltopdf
+++ b/FS/bin/freeside-wkhtmltopdf
@@ -1,7 +1,7 @@
#!/bin/sh
-if [ $DISPLAY ] ; then
- wkhtmltopdf $@
-else
+#if [ $DISPLAY ] ; then
+# wkhtmltopdf $@
+#else
xvfb-run -- wkhtmltopdf $@
-fi
+#fi
diff --git a/FS/t/cdr_cust_pkg_usage.t b/FS/t/cdr_cust_pkg_usage.t
new file mode 100644
index 000000000..1e2060e96
--- /dev/null
+++ b/FS/t/cdr_cust_pkg_usage.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cdr_cust_pkg_usage;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/contact_Mixin.t b/FS/t/contact_Mixin.t
new file mode 100644
index 000000000..89dcc37c5
--- /dev/null
+++ b/FS/t/contact_Mixin.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::contact_Mixin;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_pkg_usage.t b/FS/t/cust_pkg_usage.t
new file mode 100644
index 000000000..23a7b299e
--- /dev/null
+++ b/FS/t/cust_pkg_usage.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_pkg_usage;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_pkg_msgcat.t b/FS/t/part_pkg_msgcat.t
new file mode 100644
index 000000000..541c16799
--- /dev/null
+++ b/FS/t/part_pkg_msgcat.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg_msgcat;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_pkg_usage.t b/FS/t/part_pkg_usage.t
new file mode 100644
index 000000000..ba5ccb6c8
--- /dev/null
+++ b/FS/t/part_pkg_usage.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg_usage;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_pkg_usage_class.t b/FS/t/part_pkg_usage_class.t
new file mode 100644
index 000000000..e46ff0648
--- /dev/null
+++ b/FS/t/part_pkg_usage_class.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg_usage_class;
+$loaded=1;
+print "ok 1\n";
diff --git a/INSTALL b/INSTALL
deleted file mode 100644
index 4ea167893..000000000
--- a/INSTALL
+++ /dev/null
@@ -1,3 +0,0 @@
-See:
-
-http://www.freeside.biz/mediawiki/index.php/Freeside:1.7:Documentation#Installation_and_upgrades
diff --git a/Makefile b/Makefile
index 010678f14..dd7adb0bd 100644
--- a/Makefile
+++ b/Makefile
@@ -164,11 +164,15 @@ wikiman:
chmod a+rx ./bin/pod2x
./bin/pod2x
-install-docs: check-conflicts docs
- [ -e ${FREESIDE_DOCUMENT_ROOT} ] && mv ${FREESIDE_DOCUMENT_ROOT} ${FREESIDE_DOCUMENT_ROOT}.`date +%Y%m%d%H%M%S` || true
- cp -r masondocs ${FREESIDE_DOCUMENT_ROOT}
+install-docs: docs
+ #ancient attempt to avoid overwriting customer modifications directly to production web files that's overlived its usefulness
+ #[ -e ${FREESIDE_DOCUMENT_ROOT} ] && mv ${FREESIDE_DOCUMENT_ROOT} ${FREESIDE_DOCUMENT_ROOT}.`date +%Y%m%d%H%M%S` || true
+ #cp -r masondocs ${FREESIDE_DOCUMENT_ROOT}
+ [ -h ${FREESIDE_DOCUMENT_ROOT} ] && rm ${FREESIDE_DOCUMENT_ROOT} || true
+ mkdir -p ${FREESIDE_DOCUMENT_ROOT}
+ cp -r masondocs/* masondocs/.htaccess ${FREESIDE_DOCUMENT_ROOT}
chown -R freeside:freeside ${FREESIDE_DOCUMENT_ROOT}
- cp htetc/handler.pl ${MASON_HANDLER}
+ install -D htetc/handler.pl ${MASON_HANDLER}
perl -p -i -e "\
s|%%%FREESIDE_EXPORT%%%|${FREESIDE_EXPORT}|g;\
s'%%%RT_ENABLED%%%'${RT_ENABLED}'g; \
@@ -225,7 +229,7 @@ perl-modules:
s|%%%DIST_CONF%%%|${DIST_CONF}|g;\
" blib/script/*
-install-perl-modules: check-conflicts perl-modules install-rt-initialdata
+install-perl-modules: perl-modules install-rt-initialdata
[ -L ${PERL_INC_DEV_KLUDGE}/FS ] \
&& rm ${PERL_INC_DEV_KLUDGE}/FS \
&& mv ${PERL_INC_DEV_KLUDGE}/FS.old ${PERL_INC_DEV_KLUDGE}/FS \
@@ -372,7 +376,7 @@ create-rt: configure-rt
--datafile ${RT_PATH}/etc/initialdata \
|| true
-install-rt: check-conflicts
+install-rt:
if [ ${RT_ENABLED} -eq 1 ]; then ( cd rt; make install ); fi
if [ ${RT_ENABLED} -eq 1 ]; then perl -p -i -e "\
s'%%%RT_DOMAIN%%%'${RT_DOMAIN}'g;\
@@ -412,9 +416,6 @@ clean:
-cd fs_selfservice/FS-SelfService; \
make clean
-check-conflicts:
- ! grep -r --exclude='*config.log*' '--exclude=*config.status*' --exclude=gnupg_details_on_output_formats '--exclude=*mason_handler*' '^=======$$' .
-
#these are probably only useful if you're me...
#release: upload-docs
diff --git a/bin/23diff b/bin/23diff
index d38c84834..1dc1659d2 100755
--- a/bin/23diff
+++ b/bin/23diff
@@ -7,7 +7,7 @@ $dir =~ s/freeside(\/?)/freeside2.3$1/;
warn $dir;
#$cmd = "diff -u $file $dir/$file";
-$cmd = "diff -u $dir/$file $file";
+$cmd = "diff -ubBw $dir/$file $file";
print "$cmd\n";
system($cmd);
diff --git a/bin/cdr-netsapiens.import b/bin/cdr-netsapiens.import
index 8aa4ac0b7..1cce461e2 100755
--- a/bin/cdr-netsapiens.import
+++ b/bin/cdr-netsapiens.import
@@ -37,6 +37,7 @@ do {
my $ns = $part_export->ns_command( 'GET', '/cdr/',
'time_release' => "$time_release,",
'_sort' => '+time_release',
+ '_limit' => '500',
);
#loop over them, double check duplicates, insert the rest
diff --git a/bin/cust_main-bulk_change b/bin/cust_main-bulk_change
index fdf53d999..32a6d7bd6 100755
--- a/bin/cust_main-bulk_change
+++ b/bin/cust_main-bulk_change
@@ -1,13 +1,15 @@
#!/usr/bin/perl
use strict;
-use vars qw( $opt_p );
+use vars qw( $opt_a $opt_p $opt_t $opt_k );
use Getopt::Std;
use FS::UID qw(adminsuidsetup);
-use FS::Record qw(qsearchs);
+use FS::Record qw(qsearch qsearchs);
use FS::cust_main;
+use FS::cust_tag;
+use FS::cust_pkg;
-getopts('p:');
+getopts('a:p:t:k:');
my $user = shift or &usage;
adminsuidsetup $user;
@@ -31,17 +33,41 @@ while () {
next;
}
- if ( $opt_p ) {
- $cust_main->payby($opt_p);
+ my %cust_tag = ( custnum=>$custnum, tagnum=>$opt_t );
+ if ( $opt_t && ! qsearchs('cust_tag', \%cust_tag) ) {
+ my $cust_tag = new FS::cust_tag \%cust_tag;
+ my $error = $cust_tag->insert;
+ die "$error\n" if $error;
}
- my $error = $cust_main->replace;
- die "$error\n" if $error;
+ if ( $opt_p || $opt_a ) {
+ $cust_main->agentnum($opt_a) if $opt_a;
+ $cust_main->payby($opt_p) if $opt_p;
+
+ my $error = $cust_main->replace;
+ die "$error\n" if $error;
+ }
+
+ if ( $opt_k ) {
+ foreach my $k (split(/\s*,\s*/, $opt_k)) {
+ my($old, $new) = split(/\s*:\s*/, $k);
+ foreach my $cust_pkg ( qsearch('cust_pkg', {
+ 'custnum' => $cust_main->custnum,
+ 'pkgpart' => $old,
+ })
+ )
+ {
+ $cust_pkg->pkgpart($new);
+ my $error = $cust_pkg->replace;
+ die "$error\n" if $error;
+ }
+ }
+ }
}
sub usage {
- die "usage: cust_main-bulk_change -p NEW_PAYBY employee_username or I
+
+-t: tagnum to add if not present
--p: new payby, for example, I or I.
+-k: old_pkgpart:new_pkgpart, for example, I<5:4>. Multiple entries can be comma-separated.
user: Employee username
diff --git a/bin/fs-migrate-supplemental b/bin/fs-migrate-supplemental
new file mode 100755
index 000000000..dbef95fc1
--- /dev/null
+++ b/bin/fs-migrate-supplemental
@@ -0,0 +1,151 @@
+#!/usr/bin/perl
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch qsearchs);
+use FS::cust_pkg;
+use FS::part_pkg;
+
+my $user = shift or die &usage;
+my @pkgparts = @ARGV or die &usage;
+my $dbh = adminsuidsetup $user;
+
+$FS::UID::AutoCommit = 0;
+
+my %stats = (
+ mainpkgs => 0,
+ created => 0,
+ linked => 0,
+ errors => 0,
+);
+
+my %pkg_freq; # cache
+foreach my $pkgpart (@pkgparts) {
+ my $part_pkg = FS::part_pkg->by_key($pkgpart)
+ or die "pkgpart $pkgpart not found.\n";
+ $pkg_freq{$pkgpart} = $part_pkg->freq;
+ my @links = $part_pkg->supp_part_pkg_link
+ or die "pkgpart $pkgpart has no supplemental packages.\n";
+ CUST_PKG: foreach my $cust_pkg (
+ qsearch('cust_pkg', {
+ 'pkgpart' => $pkgpart,
+ 'cancel' => '',
+ })
+ ) {
+ my $cust_main = $cust_pkg->cust_main;
+ my @existing = $cust_pkg->supplemental_pkgs;
+ my @active = grep { !$_->main_pkgnum } $cust_main->ncancelled_pkgs;
+ LINK: foreach my $link (@links) {
+ # yeah, it's expensive
+ # see if there's an existing package with this link identity
+ foreach (@existing) {
+ if ($_->pkglinknum == $link->pkglinknum) {
+ next LINK;
+ }
+ }
+ # no? then is there one with this pkgpart?
+ my $i = 0;
+ foreach (@active) {
+ if ( $_->pkgpart == $link->dst_pkgpart ) {
+ set_link($cust_pkg, $link, $_);
+ splice(@active, $i, 1); # delete it so we don't reuse it
+ next LINK;
+ }
+ }
+ # no? then create one
+ create_linked($cust_pkg, $link);
+ } #foreach $link
+ $stats{mainpkgs}++;
+ } #foreach $cust_pkg
+} #foreach $pkgpart
+
+print "
+Main packages: $stats{mainpkgs}
+Supplemental packages linked: $stats{linked}
+Supplemental packages ordered: $stats{created}
+Errors: $stats{errors}
+";
+
+$dbh->commit or die $dbh->errstr;
+
+sub set_link {
+ my ($main_pkg, $part_pkg_link, $supp_pkg) = @_;
+ my $task = "linking package ".$supp_pkg->pkgnum.
+ " to package ".$main_pkg->pkgnum;
+ $supp_pkg->set('main_pkgnum', $main_pkg->pkgnum);
+ $supp_pkg->set('pkglinknum', $part_pkg_link->pkglinknum);
+ # Set the next bill date of the supplemental package to the nearest one in
+ # the future that lines up with the main package. If the main package
+ # hasn't started billing yet, use its future start date.
+ my $new_bill = $main_pkg->get('bill') || $main_pkg->get('start_date');
+ if ( $new_bill ) {
+ my $old_bill = $supp_pkg->get('bill');
+ my $diff = $new_bill - $old_bill;
+ my $main_freq = $pkg_freq{$main_pkg->pkgpart};
+ my $prev_bill = 0;
+ while ($diff < 0) {
+ # this will exit once $new_bill has overtaken the existing bill date.
+ # if there is no existing bill date, then this will exit right away
+ # and set bill to the bill date of the main package, which is correct.
+ $prev_bill = $new_bill;
+ $new_bill = FS::part_pkg->add_freq($new_bill, $main_freq);
+ $diff = $new_bill - $old_bill;
+ }
+ # then, of $new_bill and $prev_bill, pick the one that's closer to $old_bill
+ if ( $prev_bill > 0 and
+ $new_bill - $old_bill > $old_bill - $prev_bill ) {
+ $supp_pkg->set('bill', $prev_bill);
+ } else {
+ $supp_pkg->set('bill', $new_bill);
+ }
+ } else {
+ # otherwise the main package hasn't been billed yet and has no
+ # start date, so we can't sync the supplemental to it yet.
+ # but we can still link them.
+ warn "$task: main package has no next bill date.\n";
+ }
+ my $error = $supp_pkg->replace;
+ if ( $error ) {
+ warn "$task:\n $error\n";
+ $stats{errors}++;
+ } else {
+ $stats{linked}++;
+ }
+ return;
+}
+
+sub create_linked {
+ my ($main_pkg, $part_pkg_link) = @_;
+ my $task = "creating pkgpart ".$part_pkg_link->dst_pkgpart.
+ " supplemental to package ".$main_pkg->pkgnum;
+ my $supp_pkg = FS::cust_pkg->new({
+ 'pkgpart' => $part_pkg_link->dst_pkgpart,
+ 'pkglinknum' => $part_pkg_link->pkglinknum,
+ 'custnum' => $main_pkg->custnum,
+ 'main_pkgnum' => $main_pkg->pkgnum,
+ 'locationnum' => $main_pkg->locationnum,
+ 'start_date' => $main_pkg->start_date,
+ 'order_date' => $main_pkg->order_date,
+ 'expire' => $main_pkg->expire,
+ 'adjourn' => $main_pkg->adjourn,
+ 'contract_end' => $main_pkg->contract_end,
+ 'susp' => $main_pkg->susp,
+ 'bill' => $main_pkg->bill,
+ 'refnum' => $main_pkg->refnum,
+ 'discountnum' => $main_pkg->discountnum,
+ 'waive_setup' => $main_pkg->waive_setup,
+ });
+ my $error = $supp_pkg->insert;
+ if ( $error ) {
+ warn "$task:\n $error\n";
+ $stats{errors}++;
+ } else {
+ $stats{created}++;
+ }
+ return;
+}
+
+sub usage {
+ die "Usage:\n fs-migrate-supplemental user main_pkgpart\n";
+}
+
diff --git a/bin/megapop.pl b/bin/megapop.pl
new file mode 100755
index 000000000..e2930fb55
--- /dev/null
+++ b/bin/megapop.pl
@@ -0,0 +1,114 @@
+#!/usr/bin/perl -Tw
+#
+# this will break when megapop changes the URL or format of their listing page.
+# that's stupid. perhaps they can provide a machine-readable listing?
+
+use strict;
+use LWP::UserAgent;
+use FS::UID qw(adminsuidsetup);
+use FS::svc_acct_pop;
+
+my $url = "http://www.megapop.com/location.htm";
+
+my $user = shift or die &usage;
+adminsuidsetup($user);
+
+my %state2usps = &state2usps;
+$state2usps{'WASHINGTON STATE'} = 'WA'; #megapop's on crack
+$state2usps{'CANADA'} = 'CANADA'; #freeside's on crack
+
+my $ua = new LWP::UserAgent;
+my $request = new HTTP::Request('GET', $url);
+my $response = $ua->request($request);
+die $response->error_as_HTML unless $response->is_success;
+my $line;
+my $usps = '';
+foreach $line ( split("\n", $response->content) ) {
+ if ( $line =~ /\W(\w[\w\s]*\w)\s+LOCATIONS/i ) {
+ $usps = $state2usps{uc($1)}
+ or warn "warning: unknown state $1\n";
+ } elsif ( $line =~ /(\d{3})\-(\d{3})\-(\d{4})\s+(\w[\w\s]*\w)/ ) {
+ print "$1 $2 $3 $4 $usps\n";
+ my $svc_acct_pop = new FS::svc_acct_pop ( {
+ 'city' => $4,
+ 'state' => $usps,
+ 'ac' => $1,
+ 'exch' => $2,
+ } );
+ my $error = $svc_acct_pop->insert;
+ die $error if $error;
+ }
+}
+
+sub usage {
+ die "Usage:\n $0 user\n";
+}
+
+sub state2usps{ (
+ 'ALABAMA' => 'AL',
+ 'ALASKA' => 'AK',
+ 'AMERICAN SAMOA' => 'AS',
+ 'ARIZONA' => 'AZ',
+ 'ARKANSAS' => 'AR',
+ 'CALIFORNIA' => 'CA',
+ 'COLORADO' => 'CO',
+ 'CONNECTICUT' => 'CT',
+ 'DELAWARE' => 'DE',
+ 'DISTRICT OF COLUMBIA' => 'DC',
+ 'FEDERATED STATES OF MICRONESIA' => 'FM',
+ 'FLORIDA' => 'FL',
+ 'GEORGIA' => 'GA',
+ 'GUAM' => 'GU',
+ 'HAWAII' => 'HI',
+ 'IDAHO' => 'ID',
+ 'ILLINOIS' => 'IL',
+ 'INDIANA' => 'IN',
+ 'IOWA' => 'IA',
+ 'KANSAS' => 'KS',
+ 'KENTUCKY' => 'KY',
+ 'LOUISIANA' => 'LA',
+ 'MAINE' => 'ME',
+ 'MARSHALL ISLANDS' => 'MH',
+ 'MARYLAND' => 'MD',
+ 'MASSACHUSETTS' => 'MA',
+ 'MICHIGAN' => 'MI',
+ 'MINNESOTA' => 'MN',
+ 'MISSISSIPPI' => 'MS',
+ 'MISSOURI' => 'MO',
+ 'MONTANA' => 'MT',
+ 'NEBRASKA' => 'NE',
+ 'NEVADA' => 'NV',
+ 'NEW HAMPSHIRE' => 'NH',
+ 'NEW JERSEY' => 'NJ',
+ 'NEW MEXICO' => 'NM',
+ 'NEW YORK' => 'NY',
+ 'NORTH CAROLINA' => 'NC',
+ 'NORTH DAKOTA' => 'ND',
+ 'NORTHERN MARIANA ISLANDS' => 'MP',
+ 'OHIO' => 'OH',
+ 'OKLAHOMA' => 'OK',
+ 'OREGON' => 'OR',
+ 'PALAU' => 'PW',
+ 'PENNSYLVANIA' => 'PA',
+ 'PUERTO RICO' => 'PR',
+ 'RHODE ISLAND' => 'RI',
+ 'SOUTH CAROLINA' => 'SC',
+ 'SOUTH DAKOTA' => 'SD',
+ 'TENNESSEE' => 'TN',
+ 'TEXAS' => 'TX',
+ 'UTAH' => 'UT',
+ 'VERMONT' => 'VT',
+ 'VIRGIN ISLANDS' => 'VI',
+ 'VIRGINIA' => 'VA',
+ 'WASHINGTON' => 'WA',
+ 'WEST VIRGINIA' => 'WV',
+ 'WISCONSIN' => 'WI',
+ 'WYOMING' => 'WY',
+ 'ARMED FORCES AFRICA' => 'AE',
+ 'ARMED FORCES AMERICAS' => 'AA',
+ 'ARMED FORCES CANADA' => 'AE',
+ 'ARMED FORCES EUROPE' => 'AE',
+ 'ARMED FORCES MIDDLE EAST' => 'AE',
+ 'ARMED FORCES PACIFIC' => 'AP',
+) }
+
diff --git a/conf/invoice_html b/conf/invoice_html
index 567385b06..cd348274f 100644
--- a/conf/invoice_html
+++ b/conf/invoice_html
@@ -132,8 +132,8 @@
$OUT .= '