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::'$a'>' ../FS.pm >/dev/null || echo "missing $a" ; done
L<FS::part_pkg> - Package definition class
+L<FS::part_pkg_msgcat> - Package definition localization class
+
L<FS::part_pkg_link> - Package definition link class
L<FS::part_pkg_taxclass> - Tax class class
'Recharge customer service', #NEW
'Unprovision customer service',
'Change customer service', #NEWNEW
+ 'Edit password',
'Edit usage', #NEW
'Edit home dir', #NEW
'Edit www config', #NEW
'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
###
'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.
'Services: Hardware',
'Services: Hardware: Advanced search',
'Services: Phone numbers',
+ 'Services: Phone numbers: Advanced search',
'Services: PBXs',
'Services: Ports',
'Services: Mailing lists',
'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 },
],
'Edit package definitions',
{ rightname=>'Edit global package definitions', global=>1 },
+ 'Bulk edit package definitions',
+
'Edit billing events',
{ rightname=>'Edit global billing events', global=>1 },
+++ /dev/null
-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;
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
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 )
),
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 ),
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 ],
$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
}
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'
'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'),
);
} 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,
}
@cust_svc
],
+ 'usage_pools' => [
+ map { $usage_pools{$_} }
+ sort { $a cmp $b }
+ keys %usage_pools
+ ],
};
}
}
-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';
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;
# 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 {
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) ) ];
}
'svcnum' => $p->{svcnum},
'beginning' => $p->{beginning},
'ending' => $p->{ending},
+ 'inbound' => $p->{inbound},
'previous' => ($previous > $start) ? $previous : $start,
'next' => ($next < $end) ? $next : $end,
'header' => $header,
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
)
),
} );
+ $bill_hash = { $template_cust->bill_location->location_hash };
+ $ship_hash = { $template_cust->ship_location->location_hash };
+
} else {
$cust_main = new FS::cust_main ( {
# " 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',
'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',
},
);
+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;
},
{
+ '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.',
'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',
'section' => 'required',
'description' => 'Print command for paper invoices, for example `lpr -h\'',
'type' => 'text',
+ 'per_agent' => 1,
},
{
'key' => 'locale',
'section' => 'UI',
'description' => 'Default locale',
- 'type' => 'select',
+ 'type' => 'select-sub',
'options_sub' => sub {
map { $_ => FS::Locales->description($_) } FS::Locales->locales;
},
'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'
]
'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'
]
},
},
{
- '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.',
},
{
+ '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.',
},
{
- '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',
'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.',
},
{
+ '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.',
'type' => 'select',
'multiple' => 1,
'select_hash' => [
- 'address1' => 'Billing address',
+ #'address1' => 'Billing address',
],
},
{
'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 ) ],
},
},
{
+ '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',
},
{
- '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.",
},
{
+ '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.',
},
{
+ '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.',
{
'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',
},
{
'key' => 'pkg-balances',
'section' => 'billing',
- 'description' => 'Enable experimental package balances. Not recommended for general use.',
+ 'description' => 'Enable per-package balances.',
'type' => 'checkbox',
},
},
{
+ '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?
'type' => 'checkbox',
},
+ {
+ 'key' => 'username-exclamation',
+ 'section' => 'username',
+ 'description' => 'Allow the exclamation character (!) in usernames.',
+ 'type' => 'checkbox',
+ },
+
{
'key' => 'ie-compatibility_mode',
'section' => 'UI',
$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',
},
{
+ '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.',
'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 => "<b>DEPRECATED</b>", type => "text" },
{ key => "apachemachine", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
{ key => "apachemachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
# 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";
}
-=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
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;
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;
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;
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 ) {
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 = '';
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 (<PDF>) {
}
-=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;
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;
#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);
=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
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,
# 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 = '';
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,
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'}
? $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'} }
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;
$count++;
$row = $header || 0;
+
} elsif ( $type eq 'xml' ) {
+
# FS::pay_batch
eval "use XML::Simple;";
die $@ if $@;
$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";
}
while (1) {
my @columns = ();
+ my %hash = %$params;
if ( $type eq 'csv' ) {
last unless scalar(@buffer);
#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 ) {
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);
+ }
+
'';
}
# 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.
=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 = (
'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, '', '',
'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, '', '',
'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', '', '', '',
],
'quotation_pkg' => {
'columns' => [
'quotationpkgnum', 'serial', '', '', '', '',
+ 'quotationnum', 'int', 'NULL', '', '', '', #shouldn't be null,
+ # but history...
'pkgpart', 'int', '', '', '', '',
'locationnum', 'int', 'NULL', '', '', '',
'start_date', @date_type, '', '',
'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' => [],
'custnum', 'int', '', '', '', '',
'pkgpart', 'int', '', '', '', '',
'pkgbatch', 'varchar', 'NULL', $char_d, '', '',
+ 'contactnum', 'int', 'NULL', '', '', '',
'locationnum', 'int', 'NULL', '', '', '',
'otaker', 'varchar', 'NULL', 32, '', '',
'usernum', 'int', 'NULL', '', '', '',
'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', '', '', '',
'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', '', '', '', '',
],
},
+ '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', '', '', '', '',
'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' ] ],
'cgp_sendmdnmode', 'varchar', 'NULL', $char_d, '', '',#SendMDNMode
#mail
#XXX RPOP settings
+ #
],
'primary_key' => 'svcnum',
#'unique' => [ [ 'username', 'domsvc' ] ],
'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' => [],
'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' ] ],
'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', '', '', '', '',
'columns' => [
'regionnum', 'serial', '', '', '', '',
'regionname', 'varchar', '', $char_d, '', '',
+ 'exact_match', 'char', 'NULL', 1, '', '',
],
'primary_key' => 'regionnum',
'unique' => [],
'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
'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, '', '',
'rated_classnum', 'int', 'NULL', '', '', '',
'rated_ratename', 'varchar', 'NULL', $char_d, '', '',
- 'carrierid', 'int', 'NULL', '', '', '',
+ 'carrierid', 'bigint', 'NULL', '', '', '',
# service it was matched to
'svcnum', 'int', 'NULL', '', '', '',
'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, '', '',
=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/;
} 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",
});
}
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)
my $date_format = $date_formats{$format};
- my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
- },
- 'html' => sub { return '<b>'. shift(). '</b>'
- },
- 'template' => sub { shift },
- );
- my $embolden_function = $embolden_functions{$format};
-
my %newline_tokens = ( 'latex' => '\\\\',
'html' => '<br>',
'template' => "\n",
#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
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;
$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 = [];
$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'},
$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 '<b>'. shift(). '</b>' },
+ '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')
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')
=cut
+sub _items_nontax {
+ my $self = shift;
+ grep { $_->pkgnum } $self->cust_bill_pkg;
+}
+
sub _items_pkg {
my $self = shift;
my %options = @_;
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;
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 );
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;
|| $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 ) {
unit_amount => $cust_bill_pkg->unitsetup,
quantity => $cust_bill_pkg->quantity,
ext_description => \@d,
+ svc_label => ($svc_label || ''),
};
};
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' ) {
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
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'
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;
quantity => $cust_bill_pkg->quantity,
%item_dates,
ext_description => \@d,
+ svc_label => ($svc_label || ''),
};
$r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
}
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?
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 );
'(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?
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 = ();
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<cust_sql_fields> 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<cust_header()> 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 ]
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;
}
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;
@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;
}
#insert default tower_sector if not present
'tower' => [],
+ #repair improperly deleted services
+ 'cust_svc' => [],
+
#routernum/blocknum
'svc_broadband' => [],
'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',
'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 ) {
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 );
$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?
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 ... ] ]
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
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
);
}
+ 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)
$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;
$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);
}
#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
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
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
'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,
'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,
$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',
];
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] ];
#$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 =
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
},
--- /dev/null
+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;
--- /dev/null
+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;
--- /dev/null
+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 <LF>
+--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;
+
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);
},
%info = (
'name' => 'Telstra LinxOnline',
- 'weight' => 20,
+ 'weight' => 215,
'header' => 1,
'type' => 'fixedlength',
# Wholesale Usage Information Record format
--- /dev/null
+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;
--- /dev/null
+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<hash> 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<FS::cust_pkg_usage> object that this usage allocation came from.
+
+=item cdr
+
+Returns the L<FS::cdr> 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<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
|| $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' ])
--- /dev/null
+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<FS::contact>).
+
+=cut
+
+sub contact_obj {
+ my $self = shift;
+ return '' unless $self->contactnum;
+ qsearchs( 'contact', { 'contactnum' => $self->contactnum } );
+}
+
+1;
I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+I<lpr>, if specified, is passed to
+
=cut
sub queueable_send {
my( $template, $invoice_from, $notice_name );
my $agentnums = '';
my $balance_over = 0;
+ my $lpr = '';
if ( ref($_[0]) ) {
my $opt = shift;
$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] ) {
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
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 = (
$self->batch_invoice(\%opt);
}
else {
- do_print $self->lpr_data(\%opt);
+ do_print(
+ $self->lpr_data(\%opt),
+ 'agentnum' => $self->cust_main->agentnum,
+ 'lpr' => $lpr,
+ );
}
}
$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,
#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;
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;
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 {
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}};
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 },
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;
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<insert>
+(or a method such as C<move_to>) 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,
$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
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
}
}
- 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;
|| $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: /;
$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
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
}
#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)
#
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
$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; }
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; }
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; }
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};
$cust_pkg->pkgpart($part_pkg->pkgpart);
+ my $cmp_time = ( $conf->exists('next-bill-ignore-time')
+ ? day_end( $time )
+ : $time
+ );
+
###
# bill setup
###
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') &&
&& ! $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')
)
#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,
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
#???
#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 ".
$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);
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:
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';
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 );
}
}
+ # 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
$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;
my %fuzopts = (
'hashref' => \%options,
'select' => '',
- 'extra_sql' => " AND $agentnums_sql", #agent virtualization
+ 'extra_sql' => "WHERE $agentnums_sql", #agent virtualization
);
if ( $first && $last ) {
}
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 );
}
}
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
##
@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;
+ }
}
}
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";
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') {
=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',
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 ) {
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
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;
}
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;
}
=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; $_; } <CACHE>;
close CACHE;
\@array;
+++ /dev/null
-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;
}
-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".
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
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;
# 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;
###
# 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 #".
}
} #$object
} #$table
- FS::upgrade_journal->set_done('cust_pay__parse_paybatch');
+ FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
}
}
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
=item country
-=item status
+=item status - 'Approved' or 'Declined'
+
+=item error_message - the error returned by the gateway if any
=back
'';
}
-=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<FS::cust_pay>). 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";
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";
# 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 ) {
}
} # !$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";
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;
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;
=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<FS::part_pkg_link>) that defines this supplemental
+package, if it is one.
+
=back
Note: setup, last_bill, bill, adjourn, susp, expire, cancel and change_date
=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.
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
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;
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')
|| $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;
=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<insert>, 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 }
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".
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
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 {
#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
##
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
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;
}
##
+ # 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
##
unsuspended. This may be more convenient than calling C<unsuspend()>
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.
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';
}
+ 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
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';
}
+ 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
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;
# 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);
}
# (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,
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;
$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
#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,
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;
}
+=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 {
=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
$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.
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<FS::cdr>)
+- rate_detail: the rate determined for this call (L<FS::rate_detail>)
+- 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<cdr-minutes_priority> global config option is set, minutes may
+be taken from other calls as well. Either way, an allocation record will
+be created (L<FS::cdr_cust_pkg_usage>) 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
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;
%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.
$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')
qsearchs('discount', { 'discountnum' => $self->discountnum } );
}
-=item increment_months_used
+=item increment_months_used MONTHS
Increments months_used by the given parameter
$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
--- /dev/null
+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<FS::cust_pkg>) containing the usage
+
+=item pkgusagepart - the usage stock definition (L<FS::part_pkg_usage>).
+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<FS::cust_pkg> linked to this record.
+
+=item part_pkg_usage
+
+Return the L<FS::part_pkg_usage> 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<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
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
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 )';
(
);
}
+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
use FS::Record qw( qsearch qsearchs dbh );
use FS::part_export;
use FS::part_svc;
+use FS::svc_export_machine;
@ISA = qw(FS::Record);
} #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;
=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
qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
}
+=item svc_export_machine
+
+Returns all export hostname records (L<FS::svc_export_machine>) 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
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 );
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
=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]) : '' };
use base qw( FS::part_event::Action::Mixin::credit_pkg );
use strict;
+use FS::Record qw(qsearchs);
sub option_fields {
my $class = shift;
'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 );
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',
);
}
#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;
--- /dev/null
+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;
--- /dev/null
+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;
--- /dev/null
+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;
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,
my $cust_main = $self->cust_main($object);
- #XXX test
my $if_pkgpart = $self->option('if_pkgpart') || {};
grep $if_pkgpart->{ $_->pkgpart }, $cust_main->ncancelled_pkgs;
'type' => 'checkbox',
'value' => 'Y',
},
- 'check_bal' => { 'label' => 'Check referring custoemr balance',
+ 'check_bal' => { 'label' => 'Check referring customer balance',
'type' => 'checkbox',
'value' => 'Y',
},
--- /dev/null
+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;
}
-#XXX test?
sub condition_sql {
my( $self, $table ) = @_;
# 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,
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,
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
--- /dev/null
+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;
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;
'';
}
sub replace {
my $self = shift;
+ my $old = $self->replace_old;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
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 ) {
}
}
+ 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 {
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;
}
}
+ 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;
|| $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;
$self;
}
-=item svc_machine
+=item svc_machine SVC_X
+
+Return the export hostname for SVC_X.
=cut
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
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
#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
$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', {});
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 ) = ();
+++ /dev/null
-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;
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
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 ) = @_;
{
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"));
}
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;
--- /dev/null
+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 <i>sim_imsi</i> 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 <date> <time>\n
+ my $header = shift(@result);
+ $header =~ /(\+\+\+.*)/
+ or return { error => 'malformed response: '.$header };
+ $return{header} = $1;
+ #SMU #<serial number>\n
+ $return{smu} = shift(@result);
+ #%%<command string>%%\n
+ $return{echo} = shift(@result); # should match the input
+ #<message code>: <message description>\n
+ my $message = shift(@result);
+ if ($message =~ /^SUCCESS/) {
+ $return{success} = $message;
+ } else { #/^ERR/
+ $return{error} = $message;
+ }
+ $return{trailer} = pop(@result);
+ $return{details} = join("\n",@result,'');
+ }
+ \%return;
+}
+
+sub process_import_sim {
+ my $job = shift;
+ my $param = thaw(decode_base64(shift));
+ $param->{'job'} = $job;
+ my $exportnum = delete $param->{'exportnum'};
+ my $export = __PACKAGE__->by_key($exportnum);
+ my $file = delete $param->{'uploaded_files'};
+ $file =~ s/^file://;
+ my $dir = $FS::UID::cache_dir .'/cache.'. $FS::UID::datasrc;
+ open( $param->{'filehandle'}, '<', "$dir/$file" )
+ or die "unable to open '$file'.\n";
+ my $error = $export->import_sim($param);
+}
+
+sub import_sim {
+ # import a SIM list
+ local $FS::UID::AutoCommit = 1; # yes, 1
+ my $self = shift;
+ my $param = shift;
+ my $job = $param->{'job'};
+ my $fh = $param->{'filehandle'};
+ my @lines = $fh->getlines;
+
+ my @command = 'ADD KI';
+ push @command, ('HLRSN', $self->option('hlrsn')) if $self->option('hlrsn');
+
+ my @args = ('OPERTYPE', 'ADD');
+ push @args, ('K4SNO', $self->option('k4sno')) if $self->option('k4sno');
+ push @args, ('CARDTYPE', $self->option('cardtype'),
+ 'ALG', $self->option('alg'));
+ push @args, ('OPCVALUE', $self->option('opcvalue'),
+ 'OPSNO', $self->option('opsno'))
+ if $self->option('alg') eq 'MILENAGE';
+
+ my $agentnum = $param->{'agentnum'};
+ my $classnum = $param->{'classnum'};
+ my $class = FS::inventory_class->by_key($classnum)
+ or die "bad inventory class $classnum\n";
+ my %existing = map { $_->item, 1 }
+ qsearch('inventory_item', { 'classnum' => $classnum });
+
+ my $socket = $self->login;
+ my $num=0;
+ my $total = scalar(@lines);
+ foreach my $line (@lines) {
+ $num++;
+ $job->update_statustext(int(100*$num/$total).',Provisioning IMSIs...')
+ if $job;
+
+ chomp $line;
+ my ($imsi, $iccid, $pin1, $puk1, $pin2, $puk2, $acc, $ki) =
+ split(' ', $line);
+ # the only fields we really care about are the IMSI and KI.
+ if ($imsi !~ /^\d{15}$/ or $ki !~ /^[0-9A-Z]{32}$/) {
+ warn "misspelled line in SIM file: $line\n";
+ next;
+ }
+ if ($existing{$imsi}) {
+ warn "IMSI $imsi already in inventory, skipped\n";
+ next;
+ }
+
+ # push IMSI/KI to the HLR
+ my $return = $self->command($socket,
+ @command,
+ 'IMSI', $imsi,
+ 'KIVALUE', $ki,
+ @args
+ );
+ if ( $return->{success} ) {
+ # add to inventory
+ my $item = FS::inventory_item->new({
+ 'classnum' => $classnum,
+ 'agentnum' => $agentnum,
+ 'item' => $imsi,
+ });
+ my $error = $item->insert;
+ if ( $error ) {
+ die "IMSI $imsi added to HLR, but not to inventory:\n$error\n";
+ }
+ } else {
+ die "IMSI $imsi could not be added to HLR:\n".$return->{error}."\n";
+ }
+ } #foreach $line
+ $self->logout($socket);
+ return;
+}
+
+1;
;
%info = (
- 'svc' => [ 'svc_phone', ], # 'part_device',
+ 'svc' => [qw( svc_phone part_device )],
'desc' => 'Provision phone numbers to NetSapiens',
'options' => \%options,
'no_machine' => 1,
#- suspension/unsuspension
tie my %options, 'Tie::IxHash',
- 'user' => { label=>'Remote username', default=>'root', },
- 'useradd' => { label=>'Insert command', },
- 'userdel' => { label=>'Delete command', },
- 'usermod' => { label=>'Modify command', },
- 'suspend' => { label=>'Suspension command', },
- 'unsuspend' => { label=>'Unsuspension command', },
+ 'user' => { label=>'Remote username', default=>'root', },
+ 'useradd' => { label=>'Insert command', },
+ 'userdel' => { label=>'Delete command', },
+ 'usermod' => { label=>'Modify command', },
+ 'suspend' => { label=>'Suspension command', },
+ 'unsuspend' => { label=>'Unsuspension command', },
+ 'mac_insert' => { label=>'Device MAC address insert command', },
+ 'mac_delete' => { label=>'Device MAC address delete command', },
;
%info = (
- 'svc' => 'svc_phone',
+ 'svc' => [qw( svc_phone part_device )],
'desc' => 'Run remote commands via SSH, for phone numbers',
'options' => \%options,
'notes' => <<'END'
<LI><code>$pin</code> - Personal identification number
<LI><code>$cust_name</code> - Customer name (quoted for the shell)
<LI><code>$pkgnum</code> - Internal package number
+ <LI><code>$mac_addr</code> - MAC address (Device MAC address insert and delete commands only)
</UL>
END
);
sub rebless { shift; }
sub _export_insert {
- my($self) = shift;
+ my $self = shift;
$self->_export_command('useradd', @_);
}
sub _export_delete {
- my($self) = shift;
+ my $self = shift;
$self->_export_command('userdel', @_);
}
sub _export_suspend {
- my($self) = shift;
+ my $self = shift;
$self->_export_command('suspend', @_);
}
sub _export_unsuspend {
- my($self) = shift;
+ my $self = shift;
$self->_export_command('unsuspend', @_);
}
+sub export_device_insert {
+ my( $self, $svc_phone, $phone_device ) = @_;
+ $self->_export_command('mac_insert', $svc_phone,
+ 'mac_addr'=>$phone_device->mac_addr
+ );
+}
+
+sub export_device_delete {
+ my( $self, $svc_phone, $phone_device ) = @_;
+ $self->_export_command('mac_delete', $svc_phone,
+ 'mac_addr'=>$phone_device->mac_addr
+ );
+}
+
sub _export_command {
- my ( $self, $action, $svc_phone) = (shift, shift, shift);
+ my ( $self, $action, $svc_phone, %addl_vars) = @_;
my $command = $self->option($action);
return '' if $command =~ /^\s*$/;
{
no strict 'refs';
${$_} = $svc_phone->getfield($_) foreach $svc_phone->fields;
+ ${$_} = $addl_vars{$_} foreach keys %addl_vars;
}
my $cust_pkg = $svc_phone->cust_svc->cust_pkg;
my $pkgnum = $cust_pkg ? $cust_pkg->pkgnum : '';
${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
# snarfs are unused at this point?
- my $count = 1;
- foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
- ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
- foreach qw( machine username _password );
- $count++;
- }
+ # my $count = 1;
+ # foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
+ # ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
+ # foreach qw( machine username _password );
+ # $count++;
+ # }
}
my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
=item stoptime_end - Upper bound for AcctStopTime, as a UNIX timestamp
-=item open_sessions - Only show records with no AcctStopTime (typically used without stoptime_* options and with starttime_* options instead)
+=item session_status - 'closed' to only show records with AcctStopTime,
+'open' to only show records I<without> AcctStopTime, empty to show both.
=item starttime_start - Lower bound for AcctStartTime, as a UNIX timestamp
push @where, " CalledStationID LIKE 'sip:$prefix\%'";
}
- if ( $start ) {
- push @where, "$str2time AcctStopTime ) >= ?";
- push @param, $start;
- }
- if ( $end ) {
- push @where, "$str2time AcctStopTime ) <= ?";
- push @param, $end;
+ my $acctstoptime = '';
+ if ( $opt->{session_status} ne 'open' ) {
+ if ( $start ) {
+ $acctstoptime .= "$str2time AcctStopTime ) >= ?";
+ push @param, $start;
+ $acctstoptime .= ' AND ' if $end;
+ }
+ if ( $end ) {
+ $acctstoptime .= "$str2time AcctStopTime ) <= ?";
+ push @param, $end;
+ }
}
- if ( $opt->{open_sessions} ) {
- push @where, 'AcctStopTime IS NULL';
+ if ( $opt->{session_status} ne 'closed' ) {
+ if ( $acctstoptime ) {
+ $acctstoptime = "( ( $acctstoptime ) OR AcctStopTime IS NULL )";
+ } else {
+ $acctstoptime = 'AcctStopTime IS NULL';
+ }
}
+ push @where, $acctstoptime;
+
if ( $opt->{starttime_start} ) {
push @where, "$str2time AcctStartTime ) >= ?";
push @param, $opt->{starttime_start};
my $orderby = 'ORDER BY AcctStartTime DESC';
$orderby = '' if $summarize;
- my $sth = $dbh->prepare('SELECT '. join(', ', @fields).
- " FROM radacct $where $groupby $orderby
- ") or die $dbh->errstr;
- $sth->execute(@param) or die $sth->errstr;
+ my $sql = 'SELECT '. join(', ', @fields).
+ " FROM radacct $where $groupby $orderby";
+ if ( $DEBUG ) {
+ warn $sql;
+ warn join(',', @param);
+ }
+ my $sth = $dbh->prepare($sql) or die $dbh->errstr;
+ $sth->execute(@param) or die $sth->errstr;
[ map { { %$_ } } @{ $sth->fetchall_arrayref({}) } ];
sub export_setstatus {
my($self, $svc_acct, $hashref) = @_;
+ for (qw( spam_tag2_level spam_kill_level )) {
+ $hashref->{$_} =~ /^\d+(\.\d+)?$/ or return "illegal $_";
+ }
+
my @shellargs = (
$svc_acct->svcnum,
user => $self->option('user') || 'root',
--- /dev/null
+package FS::part_export::test;
+
+use strict;
+use vars qw(%options %info);
+use Tie::IxHash;
+use base qw(FS::part_export);
+
+tie %options, 'Tie::IxHash',
+ 'result' => { label => 'Result',
+ type => 'select',
+ options => [ 'success', 'failure', 'exception' ],
+ default => 'success',
+ },
+ 'errormsg'=> { label => 'Error message',
+ default => 'Test export' },
+ 'insert' => { label => 'Insert', type => 'checkbox', default => 1, },
+ 'delete' => { label => 'Delete', type => 'checkbox', default => 1, },
+ 'replace' => { label => 'Replace',type => 'checkbox', default => 1, },
+ 'suspend' => { label => 'Suspend',type => 'checkbox', default => 1, },
+ 'unsuspend'=>{ label => 'Unsuspend', type => 'checkbox', default => 1, },
+;
+
+%info = (
+ 'svc' => [ qw(svc_acct svc_broadband svc_phone svc_domain) ],
+ 'desc' => 'Test export for development',
+ 'options' => \%options,
+ 'notes' => <<END,
+<P>Test export. Do not use this in production systems.</P>
+<P>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.</P>
+<P>The checkbox options can be used to turn the export off for certain
+actions, if this is needed.</P>
+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;
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 );
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;
? 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)
}
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 = ();
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,
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',
qsearch('part_pkg_discount', { 'pkgpart' => $self->pkgpart });
}
+=item part_pkg_usage
+
+Returns the voice usage pools (see L<FS::part_pkg_usage>) 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
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
+++ /dev/null
-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;
+++ /dev/null
-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;
);
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;
$cust_pkg->bill($d);
}
- $self->option('setup_fee');
+ $self->NEXT::calc_setup(@_);
}
sub calc_remain {
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,'.
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
$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 ) {
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
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)',
'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: ',
'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',
},
},
#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): ',
},
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
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
)
'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 )
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,
}
#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 ) = @_;
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'));
$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 {
'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: ',
},
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
}
#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
'';
=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
$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;
}
--- /dev/null
+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<hash> 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<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
--- /dev/null
+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<FS::part_pkg>)
+
+=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<FS::usage_class>)
+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<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
--- /dev/null
+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<FS::part_pkg_usage>) and a voice usage class (L<FS::usage_class)>.
+FS::part_pkg_usage_class inherits from FS::Record. The following fields
+are currently supported:
+
+=over 4
+
+=item num - primary key
+
+=item pkgusagepart - L<FS::part_pkg_usage> key
+
+=item classnum - L<FS::usage_class> 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<hash> 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<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
=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
|| $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
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' );
}
$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 =
\\%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;
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'});;
}
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;
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;
# 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 {
}
return $error if $error;
}
+ } elsif ( @unresolved ) {
+ # auto resolve is not enabled, and we're not ready to resolve
+ return;
}
$self->set_status('R');
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'
'_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";
},
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,
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,
},
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", "");
},
);
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
--- /dev/null
+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;
+
'_date',
'approvalStatus',
'order_number',
- 'authorization',
+ 'auth',
+ 'procStatus',
+ 'procStatusMessage',
+ 'respCodeMessage',
],
xmlkeys => [
'orderID',
'approvalStatus',
'txRefNum',
'authorizationCode',
+ 'procStatus',
+ 'procStatusMessage',
+ 'respCodeMessage',
],
'hook' => sub {
if ( !$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 => '',
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'}
$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
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;
}
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;
$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
;
return $error if $error;
+ my $company = $self->company;
+ $company =~ s/^\s+//;
+ $company =~ s/\s+$//;
+ $company =~ s/\s+/ /g;
+ $self->company($company);
+
$self->SUPER::check;
}
}
+#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
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
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,
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')
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');
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');
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
#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;
=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
my $error =
$self->ut_numbern('regionnum')
|| $self->ut_text('regionname')
+ || $self->ut_flag('exact_match')
;
return $error if $error;
=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
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(
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;
$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).' )'.
')',
=item return_inventory
+Release all inventory items attached to this service's fields. Call
+when unprovisioning the service.
+
=cut
sub return_inventory {
=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.
#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
=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 ) {
$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
$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;
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;
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};
$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({
=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 )';
+ }
}
'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',
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,
+ },
},
};
}
=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
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.
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 ... ]
# 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 =
|| $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;
primary key
+=item exportnum
+
+Export definition, see L<FS::part_export>
+
=item svcnum
Customer service, see L<FS::cust_svc>
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+)$/ ) {
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}$/
=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
if ($options{'cdrtypenum'}) {
$hash{'cdrtypenum'} = $options{'cdrtypenum'};
}
+ if ($options{'calltypenum'}) {
+ $hash{'calltypenum'} = $options{'calltypenum'};
+ }
my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
@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
=item phonenum
+=item sim_imsi
+
+SIM IMSI (http://en.wikipedia.org/wiki/International_mobile_subscriber_identity)
+
=item sip_password
=item pin
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,
$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')
;
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
=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
if ($options{'cdrtypenum'}) {
$hash{'cdrtypenum'} = $options{'cdrtypenum'};
}
+ if ($options{'calltypenum'}) {
+ $hash{'calltypenum'} = $options{'calltypenum'};
+ }
my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
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 );
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
bin/freeside-deloutsourceuser
bin/freeside-deluser
bin/freeside-email
+bin/freeside-phonenum_list
bin/freeside-queued
bin/freeside-radgroup
bin/freeside-reexport
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
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
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
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
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
-c: cdrtypenum to set, defaults to none
+-g: File is gzipped
+
user: freeside username
format: CDR format name
$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
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'
#}
#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 ???
#--
-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',{});
# 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). ') '
: '';
}
- 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
}
$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 {
use FS::Record qw(qsearch qsearchs);
use FS::cust_main;
use FS::Conf;
+use File::Copy qw(copy);
use Text::CSV;
my %opt;
$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);
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/
}
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>) {
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) {
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}) {
$num_errors++;
} else {
$num_charges++;
- $sum_charges += $hash{amount};
+ $sum_charges += $amount;
}
if ( $opt{e} and $is_e911{$hash{classname}} ) {
--- /dev/null
+#!/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<FS::svc_phone>, L<FS::cust_main>
+
+=cut
+
+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?
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') ) {
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;
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,
--- /dev/null
+#!/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<FS::svc_acct>, L<FS::cust_main>
+
+=cut
+
+1;
+
#!/bin/sh
-if [ $DISPLAY ] ; then
- wkhtmltopdf $@
-else
+#if [ $DISPLAY ] ; then
+# wkhtmltopdf $@
+#else
xvfb-run -- wkhtmltopdf $@
-fi
+#fi
--- /dev/null
+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";
--- /dev/null
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::contact_Mixin;
+$loaded=1;
+print "ok 1\n";
--- /dev/null
+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";
--- /dev/null
+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";
--- /dev/null
+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";
--- /dev/null
+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";
+++ /dev/null
-See:
-
-http://www.freeside.biz/mediawiki/index.php/Freeside:1.7:Documentation#Installation_and_upgrades
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; \
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 \
--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;\
-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
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);
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
#!/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;
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 <custnums.txt\n";
+ die "usage: cust_main-bulk_change [ -a agentnum ] [ -p NEW_PAYBY ] [ -t tagnum ] [ -k old_pkgpart:new_pkgpart,... ] employee_username <custnums.txt\n";
}
=head1 NAME
=head1 SYNOPSIS
- cust_main-bulk_change -p NEW_PAYBY username <custnums.txt
+ cust_main-bulk_change [ -a agentnum ] [ -p NEW_PAYBY ] [ -t tagnum ] [ -k old_pkgpart:new_pkgpart,... ] username <custnums.txt
=head1 DESCRIPTION
-Command-line tool to change the payby field for a group of customers.
+Command-line tool to make bulk changes to a group of customers.
+
+-a: new agentnum
+
+-p: new payby, for example, I<CARD> or I<DCRD>
+
+-t: tagnum to add if not present
--p: new payby, for example, I<CARD> or I<DCRD>.
+-k: old_pkgpart:new_pkgpart, for example, I<5:4>. Multiple entries can be comma-separated.
user: Employee username
--- /dev/null
+#!/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";
+}
+
--- /dev/null
+#!/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',
+) }
+
$OUT .= '<th align="center">' . emt('Ref') . '</th>'.
'<th align="left">' . emt('Description') . '</th>'.
( $unitprices
- ? '<th align="left">' . emt('Unit Price') . '</th>'.
- '<th align="left">' . emt('Quantity') . '</th>'
+ ? '<th align="right">' . emt('Unit Price') . '</th>'.
+ '<th align="right">' . emt('Quantity') . '</th>'
: '' ).
'<th align="right">' . emt('Amount') . '</th>';
}
( $line->{'ref'} ne $lastref ? $line->{'ref'} : '' ). '</td>'.
'<td align="left">'. $line->{'description'}. '</td>'.
( $unitprices
- ? '<td align="left">'. $line->{'unit_amount'}. '</td>'.
- '<td align="left">'. $line->{'quantity'}. '</td>'
+ ? '<td align="right">'. $line->{'unit_amount'}. '</td>'.
+ '<td align="right">'. $line->{'quantity'}. '</td>'
: ''
).
<%=
my ($last) = grep { $_->{tax_section} || !$_->{summarized} and !($finance_section && $_->{'description'} eq $finance_section) and $_->{'description'} !~ /^\d+ $/ } reverse @sections;
- foreach my $section ( grep { $_->{tax_section} || !$_->{summarized} and !($finance_section && $_->{'description'} eq $finance_section) and $_->{'description'} !~ /^\d+ $/ } @sections ) {
+ #false laziness w/invoice_latexsummary
+ foreach my $section (
+ grep {
+ $_->{tax_section} || !$_->{summarized}
+ and ! $_->{adjust_section}
+ and !($finance_section && $_->{'description'} eq $finance_section)
+ and $_->{'description'} !~ /^\d+ $/
+ }
+ @sections
+ ) {
$OUT .= '<tr><td><b>'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '</b></td>';
my $celltype = ($last == $section) ? 'th' : 'td';
$OUT .= qq(<$celltype align="right"><b>). $section->{'subtotal'}. "</b></$celltype></tr>";
<td><b>New Charges</b></td>
<th align="right"><b><%= $dollar.$current_less_finance %></b></th>
</tr>
+
+ <%=
+
+ #false laziness w/invoice_latexsummary and above
+ foreach my $section ( grep $_->{adjust_section}, @sections) {
+ $OUT .= '<tr><td><b>'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '</b></td>';
+ $OUT .= qq(<th align="right"><b>). $section->{'subtotal'}. "</b></th></tr>";
+ }
+ %>
+
<tr>
<td><b>Total Amount Due</b></td>
- <td align="right"><b><%= $dollar.sprintf('%.2f', $true_previous_balance + $current_charges - $balance_adjustments) %></b></td>
+ <td align="right"><b><%= $dollar.sprintf('%.2f', $balance) %></b></td>
</tr>
<tr><th colspan=2><br></th></tr>
</table>
\newcommand{\FSdescriptionlength} { [@-- $unitprices ? '8.2cm' : '12.8cm' --@] }\r
\newcommand{\FSdescriptioncolumncount} { [@-- $unitprices ? '4' : '6' --@] }\r
\newcommand{\FSunitcolumns}{ [@-- \r
- $unitprices \r
- ? '\makebox[2.5cm][l]{\textbf{~~'.emt('Unit Price').'}}&\makebox[1.4cm]{\textbf{~'.emt('Quantity').'}}&' \r
+ $unitprices\r
+ ? '\makebox[2.5cm][r]{\textbf{~~' . emt('Unit Price') . '}} &' .\r
+ '\makebox[1.4cm]{\textbf{~' . emt('Quantity') . '}} & ' \r
: '' --@] }\r
\r
\newcommand{\FShead}{\r
\newcommand{\FSdesc}[5]{\r
\multicolumn{1}{c}{\rule{0pt}{2.5ex}\textbf{#1}} &\r
\multicolumn{[@-- $unitprices ? '4' : '6' --@]}{l}{\textbf{#2}} &\r
-[@-- $unitprices ? ' \multicolumn{1}{l}{\textbf{#3}} &'."\n".\r
+[@-- $unitprices ? ' \multicolumn{1}{r}{\textbf{\dollar #3}} &'."\n".\r
' \multicolumn{1}{r}{\textbf{#4}} &'."\n"\r
: ''\r
--@]\r
\textbf{\underline{Summary of New Charges}} & \\
&\\
[@--
- foreach my $section ( grep { $_->{tax_section} || !$_->{summarized} and !($finance_section && $_->{'description'} eq $finance_section) and $_->{'description'} !~ /^\d+ $/ } @sections ) {
+ #false laziness w/invoice_htmlsummary
+ foreach my $section (
+ grep {
+ $_->{tax_section} || !$_->{summarized}
+ and ! $_->{adjust_section}
+ and !($finance_section && $_->{'description'} eq $finance_section)
+ and $_->{'description'} !~ /^\d+ $/
+ }
+ @sections
+ ) {
$OUT .= '\textbf{'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '}';
$OUT .= '&\textbf{'. $section->{'subtotal'}. '}\\\\';
}
\textbf{Previous Past Due Charges}&\textbf{\dollar[@-- sprintf('%.2f', $true_previous_balance - $balance_adjustments) --@]}\\
\textbf{Finance charges on overdue amount}&\textbf{\dollar[@-- $finance_amount --@]}\\
\textbf{New Charges}&\textbf{\dollar[@-- $current_less_finance --@]}\\
+
+[@--
+ #false laziness w/invoice_htmlsummary and above
+ foreach my $section ( grep $_->{adjust_section}, @sections ) {
+ $OUT .= '\textbf{'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '}';
+ $OUT .= '&\textbf{'. $section->{'subtotal'}. '}\\\\';
+ }
+--@]
+
\cline{2-2}
-\textbf{Total Amount Due}&\textbf{\dollar[@-- sprintf('%.2f', $true_previous_balance + $current_charges - $balance_adjustments) --@]}\\
+\textbf{Total Amount Due}&\textbf{\dollar[@-- sprintf('%.2f', $balance) --@]}\\
&\\
\hline
\end{tabular}
--- /dev/null
+#!/bin/sh
+# config script for freeside
+
+set -e
+
+# source debconf stuff
+. /usr/share/debconf/confmodule
+
+# source dbconfig-common shell library, and call the hook function
+if [ -f /usr/share/dbconfig-common/dpkg/config ]; then
+ # we support mysql and pgsql
+ dbc_dbtypes="pgsql, mysql"
+
+ # source dbconfig-common stuff
+ . /usr/share/dbconfig-common/dpkg/config
+ dbc_go freeside $@
+fi
+
+# ... rest of your code ...
--- /dev/null
+#
+# Regular cron jobs for the freeside package
+#
+0 0 * * * freeside /usr/bin/freeside-daily fs_daily
--- /dev/null
+#!/bin/sh
+
+. /etc/dbconfig-common/freeside.conf
+
+DB_USER=$dbc_dbuser
+DB_PASSWORD=$dbc_dbpass
+
+# -- can't find a better place to hook this in. dammit.
+
+[ "$dbc_dbtype" = "pgsql" ] && DB_TYPE=Pg
+[ "$dbc_dbtype" = "mysql" ] && DB_TYPE=mysql
+#XXX ask dbc about a remote database etc.
+DATASOURCE=DBI:${DB_TYPE}:dbname=${dbc_dbname}
+
+#debian/rules
+FREESIDE_CONF=/etc/freeside
+FREESIDE_CACHE=/var/cache/freeside
+#XXX huh?
+FREESIDE_EXPORT=/var/spool/freeside
+DEFAULT_CONF=/usr/share/freeside/default_conf
+
+#XXX this rather seriously needs proper debian-style config file handling.
+
+#shamelessly lifted from Makefile create-config target
+[ -e ${FREESIDE_CONF} ] || install -d -o freeside ${FREESIDE_CONF}
+
+touch ${FREESIDE_CONF}/secrets
+chown freeside ${FREESIDE_CONF}/secrets
+chmod 600 ${FREESIDE_CONF}/secrets
+
+[ -s ${FREESIDE_CONF}/secrets ] || echo -e "${DATASOURCE}\n${DB_USER}\n${DB_PASSWORD}" >${FREESIDE_CONF}/secrets
+chmod 600 ${FREESIDE_CONF}/secrets
+chown freeside ${FREESIDE_CONF}/secrets
+
+#XXX yuck! this too!
+[ -e /var/opt/freeside/rt/etc/RT_Config.pm.dbc ] || cp /var/opt/freeside/rt/etc/RT_Config.pm.dbc.generic /var/opt/freeside/rt/etc/RT_Config.pm.dbc
+perl -pi.generic -e "s/^\\s*Set\\s*\\(\s*\\\$DatabaseType.*\$/Set(\\\$DatabaseType, '$DB_TYPE');/" /var/opt/freeside/rt/etc/RT_Config.pm.dbc
+mv /var/opt/freeside/rt/etc/RT_Config.pm.dbc /var/opt/freeside/rt/etc/RT_Config.pm
+perl -pi -e "\
+ s'_DBC_DBUSER_'${dbc_dbuser}'g;\
+ s'_DBC_DBPASS_'${dbc_dbpass}'g;\
+ s'_DBC_DBNAME_'${dbc_dbname}'g;\
+" /var/opt/freeside/rt/etc/RT_Config.pm
+
+#dunno how to hook this in where i need it...
+#dbc_generate_include="template:/var/opt/freeside/rt/etc/RT_Config.pm"
+#dbc_generate_include_args="-o template_infile=/var/opt/freeside/rt/etc/RT_Config.pm.dbc"
+
+install -o freeside -d "${FREESIDE_CACHE}/counters.${DATASOURCE}"
+install -o freeside -d "${FREESIDE_CACHE}/cache.${DATASOURCE}"
+install -o freeside -d "${FREESIDE_EXPORT}/export.${DATASOURCE}"
+
+if [ ! -d "${FREESIDE_CONF}/conf.${DATASOURCE}" ] ; then #don't clobber conf
+install -o freeside -d "${FREESIDE_CONF}/conf.${DATASOURCE}"
+#cp conf/[a-z]* "${FREESIDE_CONF}/conf.${DATASOURCE}"
+cp -i `ls -d ${DEFAULT_CONF}/[a-z]* | grep -v CVS` "${FREESIDE_CONF}/conf.${DATASOURCE}" #-i just in case
+chown -R freeside "${FREESIDE_CONF}/conf.${DATASOURCE}"
+fi
+
+# -- back to your regularly schedule program... go ahead, create the db
+
+DOMAIN=`dnsdomainname`
+if [ "$DOMAIN" = "localdomain" ]; then #freeside needs a valid domain
+ DOMAIN='example.com'
+fi
+
+# XXX this should probably be handled by the _install_...
+# dpkg-statoverride or something
+chown freeside /etc/freeside
+
+su freeside -c "/usr/bin/freeside-setup -d $DOMAIN"
+su freeside -c '/usr/bin/freeside-adduser -g 1 fs_queue'
+su freeside -c '/usr/bin/freeside-adduser -g 1 fs_daily'
+su freeside -c '/usr/bin/freeside-adduser -g 1 fs_selfservice'
+su freeside -c '/usr/bin/freeside-adduser -g 1 fs_upgrade'
+
+#RT paths are bunk for deb proper
+
+chown freeside /var/opt/freeside/rt/etc/RT_Config.pm
+
+su freeside -c "/var/opt/freeside/rt/sbin/rt-setup-database --dba '$DB_USER' --dba-password '$DB_PASSWORD' --action schema"
+
+su freeside -c '/var/opt/freeside/rt/sbin/rt-setup-database --action insert_initial'
+
+su freeside -c '/var/opt/freeside/rt/sbin/rt-setup-database --action insert --datafile /var/opt/freeside/rt/etc/initialdata'
+
+#XXX this totally doesn't belong here, but what the hey
+chown -R freeside /var/cache/freeside/masondata
+
+exit 0
--- /dev/null
+#!/bin/sh
+su freeside -c '/usr/bin/freeside-upgrade fs_upgrade'
+#RT upgrade
--- /dev/null
+Alias /freeside/ /usr/share/freeside/www/
--- /dev/null
+#!/bin/sh
+# postinst script for freeside
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# source debconf stuff
+. /usr/share/debconf/confmodule
+
+# source dbconfig-common stuff
+. /usr/share/dbconfig-common/dpkg/postinst
+
+dbc_pgsql_createdb_encoding='sql_ascii'
+
+#echo "i should create the db here"
+dbc_go freeside $@
+#echo "db should be craeted now"
+
+# summary of how this script can be called:
+# * <postinst> `configure' <most-recently-configured-version>
+# * <old-postinst> `abort-upgrade' <new version>
+# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
+# <new-version>
+# * <postinst> `abort-remove'
+# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
+# <failed-install-package> <version> `removing'
+# <conflicting-package> <version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+case "$1" in
+ configure)
+
+ a2enmod perl
+
+ ;;
+
+ abort-upgrade|abort-remove|abort-deconfigure)
+ ;;
+
+ *)
+ echo "postinst called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
+
--- /dev/null
+#!/bin/sh
+# postrm script for freeside
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# source debconf stuff
+. /usr/share/debconf/confmodule
+
+# source dbconfig-common stuff
+if [ -f /usr/share/dbconfig-common/dpkg/postrm ]; then
+ . /usr/share/dbconfig-common/dpkg/postrm
+ dbc_go freeside $@
+fi
+
+# summary of how this script can be called:
+# * <postrm> `remove'
+# * <postrm> `purge'
+# * <old-postrm> `upgrade' <new-version>
+# * <new-postrm> `failed-upgrade' <old-version>
+# * <new-postrm> `abort-install'
+# * <new-postrm> `abort-install' <old-version>
+# * <new-postrm> `abort-upgrade' <old-version>
+# * <disappearer's-postrm> `disappear' <overwriter>
+# <overwriter-version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+ purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
+ ;;
+
+ *)
+ echo "postrm called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
+
+
--- /dev/null
+#!/bin/sh
+# prerm script for freeside
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# source debconf stuff
+. /usr/share/debconf/confmodule
+# source dbconfig-common stuff
+. /usr/share/dbconfig-common/dpkg/prerm
+dbc_go freeside $@
+
+# summary of how this script can be called:
+# * <prerm> `remove'
+# * <old-prerm> `upgrade' <new-version>
+# * <new-prerm> `failed-upgrade' <old-version>
+# * <conflictor's-prerm> `remove' `in-favour' <package> <new-version>
+# * <deconfigured's-prerm> `deconfigure' `in-favour'
+# <package-being-installed> <version> `removing'
+# <conflicting-package> <version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+ remove|upgrade|deconfigure)
+ ;;
+
+ failed-upgrade)
+ ;;
+
+ *)
+ echo "prerm called with unknown argument \`$1'" >&2
+ exit 1
+ ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
+
+
+--- High ---
+
+web stuff going to /var/www/freeside/masondocs oops
+
+apache configs going to
+./etc/freeside/apache2/freeside-rt.conf
+?? oh there's links etc. check
+
+file
+./and..?/
+in freeside-lib? oops wtf
+also
+./default_conf/
+and
+/#for/
+
+test actually installing!
+- FS files
+- /var/www/ files
+- what else should package install?
+ - init script
+ - apache config
+ - /usr/local/etc/freeside/default_conf for new installs
+
+test RT was missing, but we're cheating more now by ignoring a huge remap
+to deb policy-comliant paths. get it working
+
+init.d/freeside-init
+htetc/handler.pl
+
+#copied to /usr/local/etc/freeside by make install-docs
+htetc/htpasswd.logout
+
+init.d/insserv-override-apache2
+
+etc/longtable.sty
+
+--- Medium ---
test) freeside-webui /etc/apache/conf.d/freeside.conf
AuthUserFile is wrong (just fucked)
-test its working) somes sort of Alias /freeside /usr/share/freeside/www is needed
-
test in postinst) freeside package var/cache/freeside/cache.<datasrc is missing>
-test RT is missing. doh. get it working.
-
-test actually installing!
+--- Low ---
---- rc2... right? ---
+bin/* ? Anything here needed in a live customer install should be moved to FS/bin so it installs as part of the packaging.
freeside-selfservice-client doesn't install at all
-start freeside-sqlradius-radacctd from /etc/default/freeside too
+--- Debian ---
+
+redo & test its working) somes sort of Alias /freeside /usr/share/freeside/www is needed
+/var/www/freeside -> /usr/lib/freeside and Alias in apache
Added to README.Debian... do something else?
Ensure apache is set to run as User freeside.
finish
-RT install locations (or for now: disable for unstable, enable for
-experiemental. but try to get it finished off in time for lenny)
+RT install locations (? maybe our RT libraries shouldn't conflict with
+upstream ones?)
debian/copyright administrivia
-AGPL drama
-
upload
-
-AGPL drama or silent waiting for days or years
-
-profit! err
+freeside (3.0~20130205-1) UNRELEASED; urgency=low
+
+ * Another stab at packaging.
+
+ -- Ivan Kohler <ivan-debian@420.am> Tue, 05 Feb 2013 17:00:36 -0800
+
freeside (2.1.1-1) UNRELEASED; urgency=low
* New upstream release
+++ /dev/null
-#!/bin/sh
-# config script for freeside
-
-set -e
-
-# source debconf stuff
-. /usr/share/debconf/confmodule
-
-# source dbconfig-common shell library, and call the hook function
-if [ -f /usr/share/dbconfig-common/dpkg/config ]; then
- # we support mysql and pgsql
- dbc_dbtypes="pgsql, mysql"
-
- # source dbconfig-common stuff
- . /usr/share/dbconfig-common/dpkg/config
- dbc_go freeside $@
-fi
-
-# ... rest of your code ...
Build-Depends: debhelper (>= 5), perl (>= 5.8)
Standards-Version: 3.7.2
Homepage: http://www.freeside.biz/freeside
-Vcs-Browser: http://www.freeside.biz/cgi-bin/viewvc.cgi/freeside/
-Vcs-Cvs: :pserver:anonymous:anonymous@cvs.420.am:/home/cvs/cvsroot freeside
+#Vcs-Browser: http://www.freeside.biz/cgi-bin/viewvc.cgi/freeside/
+#Vcs-Cvs: :pserver:anonymous:anonymous@cvs.420.am:/home/cvs/cvsroot freeside
Package: freeside
Architecture: all
-Pre-Depends: freeside-lib, dbconfig-common
+Pre-Depends: freeside-lib
+# dbconfig-common
Depends: ${perl:Depends}, ${shlibs:Depends}, ${misc:Depends}, freeside-webui, debconf, adduser (>= 3.11)
Recommends: cron
Suggests: gnupg
Description: Billing and trouble ticketing for service providers
- Freeside is a web-based billing and trouble ticketing application. It
- includes features for ISPs, hosting providers, and VoIP providers, but can
- also be used as a generic customer database, invoicing and membership
- application. If you like buzzwords, call it an "BSS/OSS and CRM solution".
+ Freeside is a web-based billing, trouble ticketing and network monitoring
+ application. It includes features for ISPs and WISPs, hosting providers and
+ VoIP providers, but can also be used as a generic customer database, invoicing
+ and membership application. If you like buzzwords, you can call it a
+ "BSS/OSS and CRM solution".
Package: freeside-lib
Architecture: all
Description: Libraries for Freeside billing and trouble ticketing
Freeside is a web-based billing and trouble ticketing application.
.
- This package provides the perl libraries and command line utilities.
+ This package provides the perl libraries and command line utilities. Also,
+ the init script and daemons used by the system are currently provided by this
+ package.
#Package: freeside-bin
#Architecture: all
Copyright:
-Copyright (C) 2005-2008 Freeside Internet Services, Inc.
+Copyright (C) 2005-2013 Freeside Internet Services, Inc.
Copyright (C) 2000-2005 Ivan Kohler
Copyright (C) 1999 Silicon Interactive Software Design
All rights reserved
+++ /dev/null
-#
-# Regular cron jobs for the freeside package
-#
-0 0 * * * freeside /usr/bin/freeside-daily fs_daily
+++ /dev/null
-#!/bin/sh
-
-. /etc/dbconfig-common/freeside.conf
-
-DB_USER=$dbc_dbuser
-DB_PASSWORD=$dbc_dbpass
-
-# -- can't find a better place to hook this in. dammit.
-
-[ "$dbc_dbtype" = "pgsql" ] && DB_TYPE=Pg
-[ "$dbc_dbtype" = "mysql" ] && DB_TYPE=mysql
-#XXX ask dbc about a remote database etc.
-DATASOURCE=DBI:${DB_TYPE}:dbname=${dbc_dbname}
-
-#debian/rules
-FREESIDE_CONF=/etc/freeside
-FREESIDE_CACHE=/var/cache/freeside
-#XXX huh?
-FREESIDE_EXPORT=/var/spool/freeside
-DEFAULT_CONF=/usr/share/freeside/default_conf
-
-#XXX this rather seriously needs proper debian-style config file handling.
-
-#shamelessly lifted from Makefile create-config target
-[ -e ${FREESIDE_CONF} ] || install -d -o freeside ${FREESIDE_CONF}
-
-touch ${FREESIDE_CONF}/secrets
-chown freeside ${FREESIDE_CONF}/secrets
-chmod 600 ${FREESIDE_CONF}/secrets
-
-[ -s ${FREESIDE_CONF}/secrets ] || echo -e "${DATASOURCE}\n${DB_USER}\n${DB_PASSWORD}" >${FREESIDE_CONF}/secrets
-chmod 600 ${FREESIDE_CONF}/secrets
-chown freeside ${FREESIDE_CONF}/secrets
-
-#XXX yuck! this too!
-[ -e /var/opt/freeside/rt/etc/RT_Config.pm.dbc ] || cp /var/opt/freeside/rt/etc/RT_Config.pm.dbc.generic /var/opt/freeside/rt/etc/RT_Config.pm.dbc
-perl -pi.generic -e "s/^\\s*Set\\s*\\(\s*\\\$DatabaseType.*\$/Set(\\\$DatabaseType, '$DB_TYPE');/" /var/opt/freeside/rt/etc/RT_Config.pm.dbc
-mv /var/opt/freeside/rt/etc/RT_Config.pm.dbc /var/opt/freeside/rt/etc/RT_Config.pm
-perl -pi -e "\
- s'_DBC_DBUSER_'${dbc_dbuser}'g;\
- s'_DBC_DBPASS_'${dbc_dbpass}'g;\
- s'_DBC_DBNAME_'${dbc_dbname}'g;\
-" /var/opt/freeside/rt/etc/RT_Config.pm
-
-#dunno how to hook this in where i need it...
-#dbc_generate_include="template:/var/opt/freeside/rt/etc/RT_Config.pm"
-#dbc_generate_include_args="-o template_infile=/var/opt/freeside/rt/etc/RT_Config.pm.dbc"
-
-install -o freeside -d "${FREESIDE_CACHE}/counters.${DATASOURCE}"
-install -o freeside -d "${FREESIDE_CACHE}/cache.${DATASOURCE}"
-install -o freeside -d "${FREESIDE_EXPORT}/export.${DATASOURCE}"
-
-if [ ! -d "${FREESIDE_CONF}/conf.${DATASOURCE}" ] ; then #don't clobber conf
-install -o freeside -d "${FREESIDE_CONF}/conf.${DATASOURCE}"
-#cp conf/[a-z]* "${FREESIDE_CONF}/conf.${DATASOURCE}"
-cp -i `ls -d ${DEFAULT_CONF}/[a-z]* | grep -v CVS` "${FREESIDE_CONF}/conf.${DATASOURCE}" #-i just in case
-chown -R freeside "${FREESIDE_CONF}/conf.${DATASOURCE}"
-fi
-
-# -- back to your regularly schedule program... go ahead, create the db
-
-DOMAIN=`dnsdomainname`
-if [ "$DOMAIN" = "localdomain" ]; then #freeside needs a valid domain
- DOMAIN='example.com'
-fi
-
-# XXX this should probably be handled by the _install_...
-# dpkg-statoverride or something
-chown freeside /etc/freeside
-
-su freeside -c "/usr/bin/freeside-setup -d $DOMAIN"
-su freeside -c '/usr/bin/freeside-adduser -g 1 fs_queue'
-su freeside -c '/usr/bin/freeside-adduser -g 1 fs_daily'
-su freeside -c '/usr/bin/freeside-adduser -g 1 fs_selfservice'
-su freeside -c '/usr/bin/freeside-adduser -g 1 fs_upgrade'
-
-#RT paths are bunk for deb proper
-
-chown freeside /var/opt/freeside/rt/etc/RT_Config.pm
-
-su freeside -c "/var/opt/freeside/rt/sbin/rt-setup-database --dba '$DB_USER' --dba-password '$DB_PASSWORD' --action schema"
-
-su freeside -c '/var/opt/freeside/rt/sbin/rt-setup-database --action insert_initial'
-
-su freeside -c '/var/opt/freeside/rt/sbin/rt-setup-database --action insert --datafile /var/opt/freeside/rt/etc/initialdata'
-
-#XXX this totally doesn't belong here, but what the hey
-chown -R freeside /var/cache/freeside/masondata
-
-exit 0
+++ /dev/null
-#!/bin/sh
-su freeside -c '/usr/bin/freeside-upgrade fs_upgrade'
-#RT upgrade
+++ /dev/null
-Alias /freeside/ /usr/share/freeside/www/
+++ /dev/null
-#! /bin/sh
-#
-# skeleton example file to build /etc/init.d/ scripts.
-# This file should be used to construct scripts for /etc/init.d.
-#
-# Written by Miquel van Smoorenburg <miquels@cistron.nl>.
-# Modified for Debian
-# by Ian Murdock <imurdock@gnu.ai.mit.edu>.
-# Further changes by Javier Fernandez-Sanguino <jfs@debian.org>
-#
-# Version: @(#)skeleton 1.9 26-Feb-2001 miquels@cistron.nl
-#
-
-PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
-DAEMON=/usr/sbin/freeside
-NAME=freeside
-DESC=freeside
-
-test -x $DAEMON || exit 0
-
-LOGDIR=/var/log/freeside
-PIDFILE=/var/run/$NAME.pid
-DODTIME=1 # Time to wait for the server to die, in seconds
- # If this value is set too low you might not
- # let some servers to die gracefully and
- # 'restart' will not work
-
-# Include freeside defaults if available
-if [ -f /etc/default/freeside ] ; then
- . /etc/default/freeside
-fi
-
-set -e
-
-running_pid()
-{
- # Check if a given process pid's cmdline matches a given name
- pid=$1
- name=$2
- [ -z "$pid" ] && return 1
- [ ! -d /proc/$pid ] && return 1
- cmd=`cat /proc/$pid/cmdline | tr "\000" "\n"|head -n 1 |cut -d : -f 1`
- # Is this the expected child?
- [ "$cmd" != "$name" ] && return 1
- return 0
-}
-
-running()
-{
-# Check if the process is running looking at /proc
-# (works for all users)
-
- # No pidfile, probably no daemon present
- [ ! -f "$PIDFILE" ] && return 1
- # Obtain the pid and check it against the binary name
- pid=`cat $PIDFILE`
- running_pid $pid $NAME || return 1
- return 0
-}
-
-force_stop() {
-# Forcefully kill the process
- [ ! -f "$PIDFILE" ] && return
- if running ; then
- kill -15 $pid
- # Is it really dead?
- [ -n "$DODTIME" ] && sleep "$DODTIME"s
- if running ; then
- kill -9 $pid
- [ -n "$DODTIME" ] && sleep "$DODTIME"s
- if running ; then
- echo "Cannot kill $LABEL (pid=$pid)!"
- exit 1
- fi
- fi
- fi
- rm -f $PIDFILE
- return 0
-}
-
-case "$1" in
- start)
- echo -n "Starting $DESC: "
- start-stop-daemon --start --quiet --pidfile $PIDFILE \
- --exec $DAEMON -- $DAEMON_OPTS
- if running then
- echo "$NAME."
- else
- echo " ERROR."
- fi
- ;;
- stop)
- echo -n "Stopping $DESC: "
- start-stop-daemon --stop --quiet --pidfile $PIDFILE \
- --exec $DAEMON
- echo "$NAME."
- ;;
- force-stop)
- echo -n "Forcefully stopping $DESC: "
- force_stop
- if ! running then
- echo "$NAME."
- else
- echo " ERROR."
- fi
- ;;
- #reload)
- #
- # If the daemon can reload its config files on the fly
- # for example by sending it SIGHUP, do it here.
- #
- # If the daemon responds to changes in its config file
- # directly anyway, make this a do-nothing entry.
- #
- # echo "Reloading $DESC configuration files."
- # start-stop-daemon --stop --signal 1 --quiet --pidfile \
- # /var/run/$NAME.pid --exec $DAEMON
- #;;
- force-reload)
- #
- # If the "reload" option is implemented, move the "force-reload"
- # option to the "reload" entry above. If not, "force-reload" is
- # just the same as "restart" except that it does nothing if the
- # daemon isn't already running.
- # check wether $DAEMON is running. If so, restart
- start-stop-daemon --stop --test --quiet --pidfile \
- /var/run/$NAME.pid --exec $DAEMON \
- && $0 restart \
- || exit 0
- ;;
- restart)
- echo -n "Restarting $DESC: "
- start-stop-daemon --stop --quiet --pidfile \
- /var/run/$NAME.pid --exec $DAEMON
- [ -n "$DODTIME" ] && sleep $DODTIME
- start-stop-daemon --start --quiet --pidfile \
- /var/run/$NAME.pid --exec $DAEMON -- $DAEMON_OPTS
- echo "$NAME."
- ;;
- status)
- echo -n "$LABEL is "
- if running ; then
- echo "running"
- else
- echo " not running."
- exit 1
- fi
- ;;
- *)
- N=/etc/init.d/$NAME
- # echo "Usage: $N {start|stop|restart|reload|force-reload}" >&2
- echo "Usage: $N {start|stop|restart|force-reload|status|force-stop}" >&2
- exit 1
- ;;
-esac
-
-exit 0
+++ /dev/null
-#!/bin/sh
-#
-# Example init.d script with LSB support.
-#
-# Please read this init.d carefully and modify the sections to
-# adjust it to the program you want to run.
-#
-# Copyright (c) 2007 Javier Fernandez-Sanguino <jfs@debian.org>
-#
-# This is free software; you may redistribute it and/or modify
-# it under the terms of the GNU General Public License as
-# published by the Free Software Foundation; either version 2,
-# or (at your option) any later version.
-#
-# This is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License with
-# the Debian operating system, in /usr/share/common-licenses/GPL; if
-# not, write to the Free Software Foundation, Inc., 59 Temple Place,
-# Suite 330, Boston, MA 02111-1307 USA
-#
-### BEGIN INIT INFO
-# Provides: freeside
-# Required-Start: $network $local_fs
-# Required-Stop:
-# Should-Start: $named
-# Should-Stop:
-# Default-Start: 2 3 4 5
-# Default-Stop: 0 1 6
-# Short-Description: <Enter a short description of the sortware>
-# Description: <Enter a long description of the software>
-# <...>
-# <...>
-### END INIT INFO
-
-PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
-
-DAEMON=/usr/sbin/freeside # Introduce the server's location here
-NAME=#PACKAGE # Introduce the short server's name here
-DESC=#PACKAGE # Introduce a short description here
-LOGDIR=/var/log/freeside # Log directory to use
-
-PIDFILE=/var/run/$NAME.pid
-
-test -x $DAEMON || exit 0
-test -x $DAEMON_WRAPPER || exit 0
-
-. /lib/lsb/init-functions
-
-# Default options, these can be overriden by the information
-# at /etc/default/$NAME
-DAEMON_OPTS="" # Additional options given to the server
-
-DODTIME=10 # Time to wait for the server to die, in seconds
- # If this value is set too low you might not
- # let some servers to die gracefully and
- # 'restart' will not work
-
-LOGFILE=$LOGDIR/$NAME.log # Server logfile
-#DAEMONUSER=freeside # Users to run the daemons as. If this value
- # is set start-stop-daemon will chuid the server
-
-# Include defaults if available
-if [ -f /etc/default/$NAME ] ; then
- . /etc/default/$NAME
-fi
-
-# Use this if you want the user to explicitly set 'RUN' in
-# /etc/default/
-#if [ "x$RUN" != "xyes" ] ; then
-# log_failure_msg "$NAME disabled, please adjust the configuration to your needs "
-# log_failure_msg "and then set RUN to 'yes' in /etc/default/$NAME to enable it."
-# exit 1
-#fi
-
-# Check that the user exists (if we set a user)
-# Does the user exist?
-if [ -n "$DAEMONUSER" ] ; then
- if getent passwd | grep -q "^$DAEMONUSER:"; then
- # Obtain the uid and gid
- DAEMONUID=`getent passwd |grep "^$DAEMONUSER:" | awk -F : '{print $3}'`
- DAEMONGID=`getent passwd |grep "^$DAEMONUSER:" | awk -F : '{print $4}'`
- else
- log_failure_msg "The user $DAEMONUSER, required to run $NAME does not exist."
- exit 1
- fi
-fi
-
-
-set -e
-
-running_pid() {
-# Check if a given process pid's cmdline matches a given name
- pid=$1
- name=$2
- [ -z "$pid" ] && return 1
- [ ! -d /proc/$pid ] && return 1
- cmd=`cat /proc/$pid/cmdline | tr "\000" "\n"|head -n 1 |cut -d : -f 1`
- # Is this the expected server
- [ "$cmd" != "$name" ] && return 1
- return 0
-}
-
-running() {
-# Check if the process is running looking at /proc
-# (works for all users)
-
- # No pidfile, probably no daemon present
- [ ! -f "$PIDFILE" ] && return 1
- pid=`cat $PIDFILE`
- running_pid $pid $DAEMON_WRAPPER || return 1
- return 0
-}
-
-start_server() {
-# Start the process using the wrapper
- if [ -z "$DAEMONUSER" ] ; then
- start-stop-daemon --start --quiet --pidfile $PIDFILE \
- --exec $DAEMON -- $DAEMON_OPTS
- errcode=$?
- else
-# if we are using a daemonuser then change the user id
- start-stop-daemon --start --quiet --pidfile $PIDFILE \
- --chuid $DAEMONUSER \
- --exec $DAEMON -- $DAEMON_OPTS
- errcode=$?
- fi
- return $errcode
-}
-
-stop_server() {
-# Stop the process using the wrapper
- if [ -z "$DAEMONUSER" ] ; then
- start-stop-daemon --stop --quiet --pidfile $PIDFILE \
- --exec $DAEMON
- errcode=$
- else
-# if we are using a daemonuser then look for process that match
- start-stop-daemon --stop --quiet --pidfile $PIDFILE \
- --user $DAEMONUSER \
- --exec $DAEMON
- errcode=$
- fi
-
- return $errcode
-}
-
-reload_server() {
- [ ! -f "$PIDFILE" ] && return 1
- pid=`cat $PIDFILE` # This is the daemon's pid
- # Send a SIGHUP
- kill -1 $pid
- return $?
-}
-
-force_stop() {
-# Force the process to die killing it manually
- [ ! -e "$PIDFILE" ] && return
- if running ; then
- kill -15 $pid
- # Is it really dead?
- sleep "$DIETIME"s
- if running ; then
- kill -9 $pid
- sleep "$DIETIME"s
- if running ; then
- echo "Cannot kill $NAME (pid=$pid)!"
- exit 1
- fi
- fi
- fi
- rm -f $PIDFILE
-}
-
-
-case "$1" in
- start)
- log_daemon_msg "Starting $DESC " "$NAME"
- # Check if it's running first
- if running ; then
- log_progress_msg "apparently already running"
- log_end_msg 0
- exit 0
- fi
- if start_server && running ; then
- # It's ok, the server started and is running
- log_end_msg 0
- else
- # Either we could not start it or it is not running
- # after we did
- # NOTE: Some servers might die some time after they start,
- # this code does not try to detect this and might give
- # a false positive (use 'status' for that)
- log_end_msg 1
- fi
- ;;
- stop)
- log_daemon_msg "Stopping $DESC" "$NAME"
- if running ; then
- # Only stop the server if we see it running
- stop_server
- log_end_msg $?
- else
- # If it's not running don't do anything
- log_progress_msg "apparently not running"
- log_end_msg 0
- exit 0
- fi
- ;;
- force-stop)
- # First try to stop gracefully the program
- $0 stop
- if running; then
- # If it's still running try to kill it more forcefully
- log_daemon_msg "Stopping (force) $DESC" "$NAME"
- force_stop
- log_end_msg $?
- fi
- ;;
- restart|force-reload)
- log_daemon_msg "Restarting $DESC" "$NAME"
- stop_server
- # Wait some sensible amount, some server need this
- [ -n "$DIETIME" ] && sleep $DIETIME
- start_server
- running
- log_end_msg $?
- ;;
- status)
-
- log_daemon_msg "Checking status of $DESC" "$NAME"
- if running ; then
- log_progress_msg "running"
- log_end_msg 0
- else
- log_progress_msg "apparently not running"
- log_end_msg 1
- exit 1
- fi
- ;;
- # Use this if the daemon cannot reload
- reload)
- log_warning_msg "Reloading $NAME daemon: not implemented, as the daemon"
- log_warning_msg "cannot re-read the config file (use restart)."
- ;;
- # And this if it cann
- #reload)
- #
- # If the daemon can reload its config files on the fly
- # for example by sending it SIGHUP, do it here.
- #
- # If the daemon responds to changes in its config file
- # directly anyway, make this a do-nothing entry.
- #
- # log_daemon_msg "Reloading $DESC configuration files" "$NAME"
- # if running ; then
- # reload_server
- # if ! running ; then
- # Process died after we tried to reload
- # log_progress_msg "died on reload"
- # log_end_msg 1
- # exit 1
- # fi
- # else
- # log_progress_msg "server is not running"
- # log_end_msg 1
- # exit 1
- # fi
- #;;
-
- *)
- N=/etc/init.d/$NAME
- echo "Usage: $N {start|stop|force-stop|restart|force-reload|status}" >&2
- exit 1
- ;;
-esac
-
-exit 0
+++ /dev/null
-#!/bin/sh
-# postinst script for freeside
-#
-# see: dh_installdeb(1)
-
-set -e
-
-# source debconf stuff
-. /usr/share/debconf/confmodule
-
-# source dbconfig-common stuff
-. /usr/share/dbconfig-common/dpkg/postinst
-
-dbc_pgsql_createdb_encoding='sql_ascii'
-
-#echo "i should create the db here"
-dbc_go freeside $@
-#echo "db should be craeted now"
-
-# summary of how this script can be called:
-# * <postinst> `configure' <most-recently-configured-version>
-# * <old-postinst> `abort-upgrade' <new version>
-# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
-# <new-version>
-# * <postinst> `abort-remove'
-# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
-# <failed-install-package> <version> `removing'
-# <conflicting-package> <version>
-# for details, see http://www.debian.org/doc/debian-policy/ or
-# the debian-policy package
-
-case "$1" in
- configure)
-
- a2enmod perl
-
- ;;
-
- abort-upgrade|abort-remove|abort-deconfigure)
- ;;
-
- *)
- echo "postinst called with unknown argument \`$1'" >&2
- exit 1
- ;;
-esac
-
-# dh_installdeb will replace this with shell code automatically
-# generated by other debhelper scripts.
-
-#DEBHELPER#
-
-exit 0
-
+++ /dev/null
-#!/bin/sh
-# postrm script for freeside
-#
-# see: dh_installdeb(1)
-
-set -e
-
-# source debconf stuff
-. /usr/share/debconf/confmodule
-
-# source dbconfig-common stuff
-if [ -f /usr/share/dbconfig-common/dpkg/postrm ]; then
- . /usr/share/dbconfig-common/dpkg/postrm
- dbc_go freeside $@
-fi
-
-# summary of how this script can be called:
-# * <postrm> `remove'
-# * <postrm> `purge'
-# * <old-postrm> `upgrade' <new-version>
-# * <new-postrm> `failed-upgrade' <old-version>
-# * <new-postrm> `abort-install'
-# * <new-postrm> `abort-install' <old-version>
-# * <new-postrm> `abort-upgrade' <old-version>
-# * <disappearer's-postrm> `disappear' <overwriter>
-# <overwriter-version>
-# for details, see http://www.debian.org/doc/debian-policy/ or
-# the debian-policy package
-
-
-case "$1" in
- purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
- ;;
-
- *)
- echo "postrm called with unknown argument \`$1'" >&2
- exit 1
- ;;
-esac
-
-# dh_installdeb will replace this with shell code automatically
-# generated by other debhelper scripts.
-
-#DEBHELPER#
-
-exit 0
-
-
+++ /dev/null
-#!/bin/sh
-# prerm script for freeside
-#
-# see: dh_installdeb(1)
-
-set -e
-
-# source debconf stuff
-. /usr/share/debconf/confmodule
-# source dbconfig-common stuff
-. /usr/share/dbconfig-common/dpkg/prerm
-dbc_go freeside $@
-
-# summary of how this script can be called:
-# * <prerm> `remove'
-# * <old-prerm> `upgrade' <new-version>
-# * <new-prerm> `failed-upgrade' <old-version>
-# * <conflictor's-prerm> `remove' `in-favour' <package> <new-version>
-# * <deconfigured's-prerm> `deconfigure' `in-favour'
-# <package-being-installed> <version> `removing'
-# <conflicting-package> <version>
-# for details, see http://www.debian.org/doc/debian-policy/ or
-# the debian-policy package
-
-
-case "$1" in
- remove|upgrade|deconfigure)
- ;;
-
- failed-upgrade)
- ;;
-
- *)
- echo "prerm called with unknown argument \`$1'" >&2
- exit 1
- ;;
-esac
-
-# dh_installdeb will replace this with shell code automatically
-# generated by other debhelper scripts.
-
-#DEBHELPER#
-
-exit 0
-
-
#PACKAGE = $(shell dh_listpackages)
PACKAGE = freeside
TMP = $(CURDIR)/debian/$(PACKAGE)
-DBC_SCRIPTS = $(TMP)/usr/share/dbconfig-common/scripts/freeside
+#DBC_SCRIPTS = $(TMP)/usr/share/dbconfig-common/scripts/freeside
-#this is gotten from dbconfig-common
-DB_TYPE = db_type_is_configured_during_pkg_install_by_dbconfig-common_not_at_build_time
+##this is gotten from dbconfig-common
+#DB_TYPE = db_type_is_configured_during_pkg_install_by_dbconfig-common_not_at_build_time
#no chance, it doesn't get backslash-interpolted now...
-#DEBVERSION = `head -1 debian/changelog | cut -d')' -f1 | cut -c11-`
-DEBVERSION = 1.7.3~rc2-1
-export VERSION = $(DEBVERSION) (Debian)
+##DEBVERSION = `head -1 debian/changelog | cut -d')' -f1 | cut -c11-`
+#DEBVERSION = 1.7.3~rc2-1
+#export VERSION = $(DEBVERSION) (Debian)
-export FREESIDE_CONF = /etc/freeside
-export FREESIDE_LOG = /var/log/freeside
-export FREESIDE_LOCK = /var/lock/freeside
-export FREESIDE_CACHE = $(TMP)/var/cache/freeside
-FREESIDE_CACHE = $(TMP)/var/cache/freeside
+#export FREESIDE_CONF = /etc/freeside
+#export FREESIDE_LOG = /var/log/freeside
+#export FREESIDE_LOCK = /var/lock/freeside
+#export FREESIDE_CACHE = $(TMP)/var/cache/freeside
+#FREESIDE_CACHE = $(TMP)/var/cache/freeside
#XXX huh?
-export FREESIDE_EXPORT = /var/spool/freeside
+#export FREESIDE_EXPORT = /var/spool/freeside
+
+export FREESIDE_CONF = $(TMP)/usr/local/etc/freeside
+export FREESIDE_LOG = $(TMP)/usr/local/etc/freeside
+export FREESIDE_LOCK = $(TMP)/usr/local/etc/freeside
+export FREESIDE_CACHE = $(TMP)/usr/local/etc/freeside
+export FREESIDE_EXPORT = $(TMP)/usr/local/etc/freeside
#XXX own subdir?
-export MASON_HANDLER = $(TMP)-webui/usr/share/freeside/handler.pl
+#export MASON_HANDLER = $(TMP)-webui/usr/share/freeside/handler.pl
+export MASON_HANDLER=$(TMP)-webui/usr/local/etc/freeside/handler.pl
-export APACHE_VERSION = 2
-export FREESIDE_DOCUMENT_ROOT = $(TMP)-webui/usr/share/freeside/www
+#export FREESIDE_DOCUMENT_ROOT = $(TMP)-webui/usr/share/freeside/www
+export FREESIDE_DOCUMENT_ROOT = $(TMP)-webui/var/www/freeside
export INIT_FILE = $(TMP).init
export INIT_INSTALL = /bin/true
export HTTPD_RESTART = /bin/true
export SELFSERVICE_MACHINES =
#prompt ? XXX these are runtime, not buildtime :/
-export RT_DOMAIN = `dnsdomainname`
-export RT_TIMEZONE = `cat /etc/timezone`
+#export RT_DOMAIN = `dnsdomainname`
+#export RT_TIMEZONE = `cat /etc/timezone`
-export HOSTNAME = `hostname -f`
-export FREESIDE_URL = http://$(HOSTNAME)/freeside/
+#export HOSTNAME = `hostname -f`
+#export FREESIDE_URL = http://$(HOSTNAME)/freeside/
#specific to deb pkg, for purposes of saving off a permanent copy of default
#config for postinst and that sort of thing
-export DIST_CONF = $(TMP)/usr/share/freeside/default_conf
+#export DIST_CONF = $(TMP)/usr/share/freeside/default_conf
#XXX yuck. proper RT layout is entirely necessary
#this seems to infect way to much of RT with the build location, requiring
# a kludge to hack it out afterwords. look into using fakeroot (didn't
# realize it would need to be explicit argh)
# (but leaving it for now, otherwise can't get RT to put files where we need em)
-export RT_PATH = $(TMP)/var/opt/freeside/rt
+#export RT_PATH = $(TMP)/var/opt/freeside/rt
# This has to be exported to make some magic below work.
export DH_OPTIONS
#false laziness w/install-perl-modules now
#install this for postinst later (no create-config)
- install -d $(DIST_CONF)
+ ##install -d $(DIST_CONF)
#install conf/[a-z]* $(DEFAULT_CONF)
#CVS is not [a-z]
- install `ls -d conf/[a-z]* | grep -v CVS` $(DIST_CONF)
+ ##install `ls -d conf/[a-z]* | grep -v CVS` $(DIST_CONF)
install -d $(FREESIDE_DOCUMENT_ROOT)
install -d $(FREESIDE_CACHE)/masondata #MASONDATA
- $(MAKE) -e install-docs
+ $(MAKE) -e DESTDIR=$(TMP)-webui install-docs
#hack the build dir out of Freeside too. oh yeah, sucky.
perl -p -i -e "\
${TMP}/usr/share/perl5/FS/*/* \
${TMP}/usr/bin/*
- rm -r $(FREESIDE_DOCUMENT_ROOT).*
+ #rm -r $(FREESIDE_DOCUMENT_ROOT).*
install -d $(APACHE_CONF)
- install debian/freeside.apache-alias.conf $(APACHE_CONF)/freeside-alias.conf
- FREESIDE_DOCUMENT_ROOT=/usr/share/freeside/www MASON_HANDLER=/usr/share/freeside/handler.pl FREESIDE_CONF=/etc/freeside $(MAKE) -e install-apache
+ #install debian/freeside.apache-alias.conf $(APACHE_CONF)/freeside-alias.conf
+ #FREESIDE_DOCUMENT_ROOT=/usr/share/freeside/www MASON_HANDLER=/usr/share/freeside/handler.pl FREESIDE_CONF=/etc/freeside $(MAKE) -e install-apache
+ $(MAKE) -e install-apache
$(MAKE) -e install-init
#RT
#(configure-rt)
-
- # XXX need to adjust db-type, db-database, db-rt-user, db-rt-pass
- # based on info from dbc
- ( cd rt; \
- cp config.layout.in config.layout; \
- perl -p -i -e "\
- s'%%%FREESIDE_DOCUMENT_ROOT%%%'${FREESIDE_DOCUMENT_ROOT}'g;\
- s'%%%MASONDATA%%%'${FREESIDE_CACHE}/masondata'g;\
- " config.layout; \
- ./configure --prefix=${RT_PATH} \
- --enable-layout=Freeside \
- --with-db-type=Pg \
- --with-db-dba=freeside \
- --with-db-database=_DBC_DBNAME_ \
- --with-db-rt-user=_DBC_DBUSER_ \
- --with-db-rt-pass=_DBC_DBPASS_ \
- --with-web-user=freeside \
- --with-web-group=freeside \
- --with-rt-group=freeside \
- )
-
- #(create-rt)
- install -d $(RT_PATH)
- ( cd rt; make install )
- #hack the build dir out of RT. yeah, sucky.
- perl -p -i -e "\
- s'${TMP}''g;\
- " ${RT_PATH}/etc/RT_Config.pm \
- ${RT_PATH}/lib/RT.pm \
- ${RT_PATH}/bin/mason_handler.fcgi \
- ${RT_PATH}/bin/mason_handler.scgi \
- ${RT_PATH}/bin/standalone_httpd \
- ${RT_PATH}/bin/webmux.pl \
- ${RT_PATH}/bin/rt-crontool \
- ${RT_PATH}/sbin/rt-dump-database \
- ${RT_PATH}/sbin/rt-setup-database
-
- #hack @INC dir out of RT (well, handler.pl) too.
- perl -p -i -e "\
- s'/opt/rt3/'/var/opt/freeside/rt/'g;\
- " ${TMP}-webui/usr/share/freeside/handler.pl
-
- mv ${RT_PATH}/etc/RT_Config.pm ${RT_PATH}/etc/RT_Config.pm.dbc
-
- perl -p -i -e "\
- s'%%%RT_DOMAIN%%%'${RT_DOMAIN}'g;\
- s'%%%RT_TIMEZONE%%%'${RT_TIMEZONE}'g;\
- s'%%%FREESIDE_URL%%%'${FREESIDE_URL}'g;\
- " ${RT_PATH}/etc/RT_SiteConfig.pm
-
- install -D debian/dbconfig-common.install $(DBC_SCRIPTS)/install/pgsql
- install -D debian/dbconfig-common.install $(DBC_SCRIPTS)/install/mysql
+ $(MAKE) -e configure-rt
+
+ ## XXX need to adjust db-type, db-database, db-rt-user, db-rt-pass
+ ## based on info from dbc
+ #( cd rt; \
+ # cp config.layout.in config.layout; \
+ # perl -p -i -e "\
+ # s'%%%FREESIDE_DOCUMENT_ROOT%%%'${FREESIDE_DOCUMENT_ROOT}'g;\
+ # s'%%%MASONDATA%%%'${FREESIDE_CACHE}/masondata'g;\
+ # " config.layout; \
+ # ./configure --prefix=${RT_PATH} \
+ # --enable-layout=Freeside \
+ # --with-db-type=Pg \
+ # --with-db-dba=freeside \
+ # --with-db-database=_DBC_DBNAME_ \
+ # --with-db-rt-user=_DBC_DBUSER_ \
+ # --with-db-rt-pass=_DBC_DBPASS_ \
+ # --with-web-user=freeside \
+ # --with-web-group=freeside \
+ # --with-rt-group=freeside \
+ #)
+
+ ##(create-rt)
+ #$(MAKE) -e create-rt
+
+ #install -d $(RT_PATH)
+ #( cd rt; make install )
+ ##hack the build dir out of RT. yeah, sucky.
+ #perl -p -i -e "\
+ # s'${TMP}''g;\
+ #" ${RT_PATH}/etc/RT_Config.pm \
+ # ${RT_PATH}/lib/RT.pm \
+ # ${RT_PATH}/bin/mason_handler.fcgi \
+ # ${RT_PATH}/bin/mason_handler.scgi \
+ # ${RT_PATH}/bin/standalone_httpd \
+ # ${RT_PATH}/bin/webmux.pl \
+ # ${RT_PATH}/bin/rt-crontool \
+ # ${RT_PATH}/sbin/rt-dump-database \
+ # ${RT_PATH}/sbin/rt-setup-database
+ #
+ ##hack @INC dir out of RT (well, handler.pl) too.
+ #perl -p -i -e "\
+ # s'/opt/rt3/'/var/opt/freeside/rt/'g;\
+ #" ${TMP}-webui/usr/share/freeside/handler.pl
+
+ #mv ${RT_PATH}/etc/RT_Config.pm ${RT_PATH}/etc/RT_Config.pm.dbc
+
+ #perl -p -i -e "\
+ # s'%%%RT_DOMAIN%%%'${RT_DOMAIN}'g;\
+ # s'%%%RT_TIMEZONE%%%'${RT_TIMEZONE}'g;\
+ # s'%%%FREESIDE_URL%%%'${FREESIDE_URL}'g;\
+ #" ${RT_PATH}/etc/RT_SiteConfig.pm
+
+ #install -D debian/dbconfig-common.install $(DBC_SCRIPTS)/install/pgsql
+ #install -D debian/dbconfig-common.install $(DBC_SCRIPTS)/install/mysql
- install -D debian/dbconfig-common.upgrade $(DBC_SCRIPTS)/upgrade/pgsql/$(DEBVERSION)
- install -D debian/dbconfig-common.upgrade $(DBC_SCRIPTS)/upgrade/mysql/$(DEBVERSION)
+ #install -D debian/dbconfig-common.upgrade $(DBC_SCRIPTS)/upgrade/pgsql/$(DEBVERSION)
+ #install -D debian/dbconfig-common.upgrade $(DBC_SCRIPTS)/upgrade/mysql/$(DEBVERSION)
dh_install
binary-indep: build install
dh_testdir
dh_testroot
- dh_installchangelogs ChangeLog
dh_installdocs #freeside.docs README AGPL
dh_installexamples eg/*
# dh_installmenu
+++ /dev/null
-#!/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',
-) }
-
'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',
{ title=>'Logout', url=>'logout', size=>'+1', },
;
+my %menu_disable = map { $_=>1 } @menu_disable;
foreach my $item ( @menu ) {
- next if $menu_skipblanks && $item->{'title'} =~ /^\s*$/;
- next if $menu_skipheadings && ! $item->{'url'};
-
+ next if ( $menu_skipblanks && $item->{'title'} =~ /^\s*$/ )
+ || ( $menu_skipheadings && ! $item->{'url'} )
+ || $menu_disable{$item->{'title'}};
+
$OUT .= '<TR><TD';
if ( $menu_body_image ) {
if ( exists $item->{'url'} && $action eq $item->{'url'} ) {
'svcnum' => $cgi->param('svcnum'),
'beginning' => $cgi->param('beginning') || '',
'ending' => $cgi->param('ending') || '',
+ 'inbound' => $cgi->param('inbound') || 0,
);
}
' Signup form</FONT><BR><BR>';
%>
-<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+<FONT SIZE="+1" COLOR="#ff0000"><%= encode_entities($error) %></FONT>
<FORM NAME="OneTrueForm" ACTION="<%= $self_url %>" METHOD=POST onSubmit="document.OneTrueForm.signup.disabled=true">
-<INPUT TYPE="hidden" NAME="prepaid_shortform" VALUE="<%= $prepaid_shortform %>">
+<INPUT TYPE="hidden" NAME="prepaid_shortform" VALUE="<%= encode_entities($prepaid_shortform) %>">
<INPUT TYPE="hidden" NAME="session" VALUE="<%= $session_id %>">
<INPUT TYPE="hidden" NAME="action" VALUE="process_signup">
<INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>">
else {
@payby = ('PREPAY');
}
+'';
%>
<BR>Billing information<TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=0 WIDTH="100%">
my( $account, $aba ) = split('@', $payinfo);
my %paybychecked = (
- 'CARD' => '<TABLE BGCOLOR="'. ( $box_bgcolor || '#c0c0c0' ). qq!" BORDER=0 CELLSPACING=0 WIDTH="100%"><TR><TD ALIGN="right"><font color="#ff0000">*</font> Card type</TD><TD>$cardselect</TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Card number</TD><TD><INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19></TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Expration</TD><TD>!. expselect("CARD", $paydate). qq!</TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Name on card</TD><TD><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname"></TD></TR>!,
+ 'CARD' => '<TABLE BGCOLOR="'. ( $box_bgcolor || '#c0c0c0' ). qq!" BORDER=0 CELLSPACING=0 WIDTH="100%"><TR><TD ALIGN="right"><font color="#ff0000">*</font> Card type</TD><TD>$cardselect</TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Card number</TD><TD><INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19></TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Expiration</TD><TD>!. expselect("CARD", $paydate). qq!</TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Name on card</TD><TD><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname"></TD></TR>!,
'DCRD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("DCRD", $paydate). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="$payname">!,
'CHEK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE="$account" MAXLENGTH=10> Type <SELECT NAME="CHEK_paytype">!. join('', map {qq!<OPTION VALUE="$_"!.($paytype eq $_ ? ' SELECTED' : '').">$_</OPTION>"} @paytypes). qq!</SELECT><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="$payname">!,
'DCHK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE="$account" MAXLENGTH=10> Type <SELECT NAME="DCHK_paytype">!. join('', map {qq!<OPTION VALUE="$_"!.($paytype eq $_ ? ' SELECTED' : '').">$_</OPTION>"} @paytypes). qq!</SELECT><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="">!,
? '<I><FONT SIZE="-1">Billing Address</FONT></I><BR>'
: ''
%>
- <%= $first %> <%= $last %><BR>
- <%= $company ? $company.'<BR>' : '' %>
- <%= $address1 %><BR>
- <%= $address2 ? $address2.'<BR>' : '' %>
+ <%= encode_entities($first) %> <%= encode_entities($last) %><BR>
+ <%= $company ? encode_entities($company).'<BR>' : '' %>
+ <%= encode_entities($address1) %><BR>
+ <%= $address2 ? encode_entities($address2).'<BR>' : '' %>
<%= $city %>, <%= $state %> <%= $zip %><BR>
<%= $country && $country ne ($countrydefault||'US')
? $country.'<BR>'
<%= $url = "$selfurl?session=$session_id;action="; ''; %>
-<%= include('header', 'Call usage for '.
+<%= include('header', ($inbound ? 'Received calls' : 'Dialed calls' ) .
+ ' for '.
Date::Format::time2str('%b %o %Y', $beginning).
' - '.
Date::Format::time2str('%b %o %Y', $ending)
<%= $url = "$selfurl?session=$session_id;action=";
- @svc_acct = grep { $_->{svcdb} eq 'svc_acct' } @svcs;
- @svc_phone = grep { $_->{svcdb} eq 'svc_phone' } @svcs;
- @svc_port = grep { $_->{svcdb} eq 'svc_port' } @svcs;
+ %by_pkg_label = (); # not used yet, but I'm sure it will be...
+ @svc_acct = ();
+ @svc_phone = ();
+ @svc_port = ();
+
+ foreach (@svcs) {
+ $by_pkg_label{ $_->{pkg_label} } ||= [];
+ push @{ $by_pkg_label{ $_->{pkg_label} } }, $_;
+ if ( $_->{svcdb} eq 'svc_acct' ) {
+ push @svc_acct, $_;
+ } elsif ( $_->{svcdb} eq 'svc_phone' ) {
+ push @svc_phone, $_;
+ } elsif ( $_->{svcdb} eq 'svc_port' ) {
+ push @svc_port, $_;
+ }
+ }
'';
%>
<%= include('header', 'Account usage') %>
<%= scalar(@svc_acct) ? '</TABLE><BR><BR>' : '' %>
<%= if ( @svc_phone ) {
+ %any = ();
+ for my $dir (qw(outbound inbound)) {
+ $any{$dir} = grep { $_->{$dir} } @svc_phone;
+ }
$OUT.= '<FONT SIZE="4">Call usage</FONT><BR><BR>
- <TABLE BGCOLOR="#cccccc">
+ <TABLE BGCOLOR="#cccccc" STYLE="display:inline-block">
<TR>
- <TH ALIGN="left">Number</TH>'; #"Account" ?
- #what else?
+ <TH ALIGN="left">Number</TH>';
+ if ( $any{outbound} ) {
+ $OUT .= '
+ <TH>Dialed</TH>';
+ }
+ if ( $any{inbound} ) {
+ $OUT .= '
+ <TH>Received</TH>';
+ }
$OUT .= '</TR>';
} else {
$OUT .= '';
<%= foreach my $svc_phone ( @svc_phone ) {
my $link = "${url}view_cdr_details;".
"svcnum=$svc_phone->{'svcnum'};beginning=0;ending=0";
- $OUT .= '<TR><TD>';
- $OUT .= qq!<A HREF="$link">!. $svc_phone->{'label'}. ': '. $svc_phone->{'value'}.'</A>';
- $OUT .= '</TD></TR>';
+ $OUT .= '<TR><TD>'. $svc_phone->{'label'}. ': '. $svc_phone->{'value'};
+ $OUT .= '</TD>';
+ # usage summary w/ links
+ for my $dir (qw(outbound inbound)) {
+ if ( $dir eq 'inbound' ) {
+ $link .= ';inbound=1';
+ }
+ if ( $svc_phone->{$dir} ) {
+ $OUT .= '<TD ALIGN="right">'.qq!<A HREF="$link">! .
+ sprintf('%d calls (%.0f minutes)',
+ $svc_phone->{$dir}->{'count'},
+ $svc_phone->{$dir}->{'duration'} / 60
+ ) .
+ '</A></TD>';
+ } elsif ( $any{$dir} ) {
+ $OUT .= '<TD></TD>';
+ }
}
+ $OUT .= '</TR>';
+}
+'';
%>
-<%= scalar(@svc_phone) ? '</TABLE><BR><BR>' : '' %>
+<%= if ( @usage_pools ) {
+ $OUT .= '</TABLE>
+ <TABLE BGCOLOR="#cccccc" STYLE="display: inline-block">
+ <TR><TH COLSPAN=4>Remaining minutes</TH></TR>
+ ';
+ my $any_shared = 0;
+ foreach my $usage (@usage_pools) {
+ # false laziness with the back office side
+ my ($description, $remain, $total, $shared) = @$usage;
+ if ( $shared ) {
+ $any_shared = 1;
+ $description .= '*';
+ }
+ my $ratio = 255 * ($remain/$total);
+ $ratio = 255 if $color > 255;
+ my $color =
+ sprintf('STYLE="font-weight: bold; color: #%02x%02x00"',
+ 255 - $ratio, $ratio);
+ $OUT .=
+ qq!<TR>
+ <TD ALIGN="right">$description</TD>
+ <TD $color ALIGN="right">$remain</TD>
+ <TD $color> / </TD>
+ <TD $color>$total</TD>
+ </TR>!;
+ }
+ if ( $any_shared ) {
+ $OUT .= '<TR STYLE="font-size: 80%; font-style: italic">'.
+ '<TD COLSPAN=4>* shared among all your phone plans</TD></TR>';
+ }
+}
+if ( scalar(@svc_phone) or scalar(@usage_pools) ) {
+ $OUT .= '</TABLE><BR><BR>';
+}
+'';
+%>
<%= if ( @svc_port ) {
$OUT.= '<FONT SIZE="4">Bandwidth Graphs</FONT><BR><BR>
SetHandler perl-script
PerlHandler HTML::Mason
</DirectoryMatch>
+
+<DirectoryMatch "^%%%FREESIDE_DOCUMENT_ROOT%%%/rt/RTx/Statistics/.*/>
+ <FilesMatch Results.tsv>
+ SetHandler perl-script
+ PerlHandler HTML::Mason
+ </FilesMatch>
+</DirectoryMatch>
<& elements/browse.html,
- title => mt('Message catalog'),
+ title => mt('Translation strings'),
name_singular => 'string', #mt? no, we need to do it through the quant/PL stuff
query => { 'table' => 'msgcat',
'hashref' => { 'locale' => $locale, },
<TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
<% $part_export->label_html %>
(<A HREF="<% $p %>edit/part_export.cgi?<% $part_export->exportnum %>">edit</A> | <A HREF="javascript:part_export_areyousure('<% $p %>misc/delete-part_export.cgi?<% $part_export->exportnum %>')">delete</A>)
+% if ( my @actions = $part_export->actions ) {
+ <P STYLE="position: absolute">
+ Management:
+% while (@actions) {
+% my $label = shift @actions;
+% my $path = shift @actions;
+ <& /elements/popup_link.html,
+ 'label' => $label,
+ 'action' => $fsurl.$path.'?'.$part_export->exportnum,
+ 'actionlabel' => $label,
+ &><% @actions ? ' | ' : '' %>
+% }
+ </P>
+% } #if @actions
+
</TD>
<TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
<% include( 'elements/browse.html',
'title' => 'Package Definitions',
+ 'menubar' => \@menubar,
'html_init' => $html_init,
+ 'html_form' => $html_form,
'html_posttotal' => $html_posttotal,
'name' => 'package definitions',
'disableable' => 1,
'fields' => \@fields,
'links' => \@links,
'align' => $align,
+ 'link_field' => 'pkgpart',
+ 'html_init' => $html_init,
+ 'html_foot' => $html_foot,
)
%>
<%init>
my $acl_config = $curuser->access_right('Configuration'); #to edit services
#and agent types
#and bulk change
+my $acl_edit_bulk = $curuser->access_right('Bulk edit package definitions');
die "access denied"
unless $acl_edit || $acl_edit_global;
";
-my $html_init;
-#unless ( $cgi->param('active') ) {
- $html_init = qq!
+my $html_init = qq!
One or more service definitions are grouped together into a package
definition and given pricing information. Customers purchase packages
rather than purchase services directly.<BR><BR>
</FORM>
<BR><BR>
!;
-#}
$cgi->param('dummy', 1);
: ()
),
],
+ ( map { my $dst_pkg = $_->dst_pkg;
+ [
+ { data => 'Supplemental: '.
+ '<A HREF="#'. $dst_pkg->pkgpart . '">' .
+ $dst_pkg->pkg . '</A>',
+ align=> 'center',
+ colspan => 2,
+ }
+ ]
+ }
+ $part_pkg->supp_part_pkg_link
+ ),
( map {
my $dst_pkg = $_->dst_pkg;
[
$align .= 'l';
}
+# make a table of report class optionnames => the actual
+my %report_optionname_name = map { 'report_option_'.$_->num, $_->name }
+ qsearch('part_pkg_report_option', { disabled => '' });
+
push @header, 'Plan options',
'Services';
#'Service', 'Quan', 'Primary';
if ( $part_pkg->plan ) {
my %options = $part_pkg->options;
-
- [ map {
+ # gather any options that are really report options,
+ # convert them to their user-friendly names,
+ # and sort them (I think?)
+ my @report_options =
+ sort { $a cmp $b }
+ map { $report_optionname_name{$_} }
+ grep { $options{$_}
+ and exists($report_optionname_name{$_}) }
+ keys %options;
+
+ my @rows = (
+ map {
[
{ 'data' => "$_: ",
'align' => 'right',
];
}
grep { $options{$_} =~ /\S/ }
- grep { $_ !~ /^(setup|recur)_fee$/ }
+ grep { $_ !~ /^(setup|recur)_fee$/
+ and $_ !~ /^report_option_\d+$/ }
keys %options
- ];
+ );
+ if ( @report_options ) {
+ push @rows,
+ [ { 'data' => 'Report classes',
+ 'align' => 'center',
+ 'style' => 'font-weight: bold',
+ 'colspan' => 2
+ } ];
+ foreach (@report_options) {
+ push @rows, [
+ { 'data' => $_,
+ 'align' => 'center',
+ 'colspan' => 2
+ }
+ ];
+ } # foreach @report_options
+ } # if @report_options
+
+ return \@rows;
- } else {
+ } else { # should never happen...
[ map { [
{ 'data' => uc($_),
sub {
my $part_pkg = shift;
+ my @part_pkg_usage = sort { $a->priority <=> $b->priority }
+ $part_pkg->part_pkg_usage;
[
(map {
]
}
$part_pkg->svc_part_pkg_link
- )
+ ),
+ ( scalar(@part_pkg_usage) ?
+ [ { data => 'Usage minutes',
+ align => 'center',
+ colspan => 2,
+ data_style => 'b',
+ link => $p.'browse/part_pkg_usage.html#pkgpart'.
+ $part_pkg->pkgpart
+ } ]
+ : ()
+ ),
+ ( map {
+ [ { data => $_->minutes,
+ align => 'right'
+ },
+ { data => $_->description,
+ align => 'left'
+ },
+ ]
+ } @part_pkg_usage
+ ),
];
};
if $extra_count;
my $count_query = "SELECT COUNT(*) FROM part_pkg $count_extra_sql $extra_count";
+my $html_form = '';
+my $html_foot = '';
+if ( $acl_edit_bulk ) {
+ # insert a checkbox column
+ push @header, '';
+ push @fields, sub {
+ '<INPUT TYPE="checkbox" NAME="pkgpart" VALUE=' . $_[0]->pkgpart .'>';
+ };
+ push @links, '';
+ $align .= 'c';
+ $html_form = qq!<FORM ACTION="${p}edit/bulk-part_pkg.html" METHOD="POST">!;
+ $html_foot = include('/search/elements/checkbox-foot.html',
+ submit => 'edit report classes', # for now it's only report classes
+ ) . '</FORM>';
+}
+
+my @menubar;
+# show this if there are any voip_cdr packages defined
+if ( FS::part_pkg->count("plan = 'voip_cdr'") ) {
+ push @menubar, 'Per-package usage minutes' => $p.'browse/part_pkg_usage.html';
+}
</%init>
--- /dev/null
+<& /elements/header.html, 'Package usage minutes' &>
+<& /elements/menubar.html, 'Package definitions', $p.'browse/part_pkg.cgi' &>
+<STYLE TYPE="text/css">
+.pkg_head {
+ background-color: #dddddd;
+ font-style: italic;
+}
+.pkg_head > td {
+ border-style: solid;
+ border-radius: 3px;
+ border-color: #555555;
+ border-width: 1px;
+}
+.usage > td {
+ text-align: center;
+}
+.error {
+ color: #ff0000;
+}
+</STYLE>
+<FORM METHOD="POST" ACTION="<%$fsurl%>edit/process/part_pkg_usage.html">
+ <TABLE STYLE="margin-top: 1em">
+ <TR>
+ <TH>Minutes</TH>
+ <TH>Shared</TH>
+ <TH>Rollover</TH>
+ <TH>Description</TH>
+ <TH>Priority</TH>
+% foreach my $class (@usage_class) {
+ <TH><% $class->classname %></TH>
+% }
+ </TR>
+
+% my $error = $cgi->param('error');
+% foreach my $part_pkg (@part_pkg) {
+% my $pkgpart = $part_pkg->pkgpart;
+% my @part_pkg_usage;
+% if ( $error ) {
+% @part_pkg_usage = @{ $error->{$pkgpart} };
+% } else {
+% @part_pkg_usage = $part_pkg->part_pkg_usage;
+% foreach my $usage (@part_pkg_usage) {
+% foreach ($usage->classnums) {
+% $usage->set("class$_".'_', 'Y');
+% }
+% }
+% }
+ <TR CLASS="pkg_head" ID="pkgpart<%$pkgpart%>">
+ <TD COLSPAN=<%$n_cols%>><% $part_pkg->pkg_comment %></TD>
+% # make it easy to enumerate the pkgparts later
+ <INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $pkgpart %>">
+ </TR>
+% # template row
+ <TR id="pkgpart<%$pkgpart%>_template" CLASS="usage">
+ <TD>
+ <INPUT TYPE="hidden" NAME="pkgusagepart">
+ <INPUT TYPE="text" NAME="minutes" ID="minutes" SIZE=7>
+ </TD>
+% foreach (qw(shared rollover)) {
+ <TD>
+ <INPUT TYPE="checkbox" NAME="<% $_ %>" ID="<% $_ %>" VALUE="Y">
+ </TD>
+% }
+ <TD>
+ <INPUT TYPE="text" NAME="description" ID="description" SIZE=20>
+ </TD>
+ <TD>
+ <INPUT TYPE="text" NAME="priority" ID="priority" SIZE=3>
+ </TD>
+% foreach (@usage_class) {
+% my $classnum = 'class' . $_->classnum . '_';
+ <TD>
+ <INPUT TYPE="checkbox" NAME="<% $classnum %>" ID="<% $classnum %>" VALUE="Y">
+ </TD>
+% }
+ </TR>
+ <& /elements/auto-table.html,
+ table => "pkgpart$pkgpart",
+ template_row => "pkgpart$pkgpart".'_template',
+ data => \@part_pkg_usage,
+ &>
+% }
+ </TABLE>
+ <BR>
+ <INPUT TYPE="submit">
+</FORM>
+<& /elements/footer.html &>
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied"
+ unless $curuser->access_right(
+ ['Edit package definitions', 'Edit global package definitions']
+ );
+
+my @where = ("(plan = 'voip_cdr' OR plan = 'voip_inbound')",
+ "freq != '0'",
+ "disabled IS NULL");
+push @where, FS::part_pkg->curuser_pkgs_sql
+ unless $curuser->access_right('Edit global package definitions');
+my $extra_sql = ' WHERE '.join(' AND ', @where);
+my @part_pkg = qsearch({
+ 'table' => 'part_pkg',
+ 'extra_sql' => $extra_sql,
+ 'order_by' => ' ORDER BY pkgpart',
+});
+
+my @usage_class = sort { $a->weight <=> $b->weight }
+ qsearch('usage_class', { disabled => '' });
+
+my $n_usage_classes = scalar(@usage_class);
+my $n_cols = $n_usage_classes + 5; # minutes, shared, rollover, desc, prio
+</%init>
% }
% @dfields ;
% my $rowspan = scalar(@fields) || 1;
+% $rowspan++ if $part_svc->restrict_edit_password;
% my $url = "${p}edit/part_svc.cgi?". $part_svc->svcpart;
%
% if ( $bgcolor eq $bgcolor1 ) {
% my $value = &$formatter($part_svc->part_svc_column($field)->columnvalue);
% if ( $flag =~ /^[MAH]$/ ) {
% my $select_table = ($flag eq 'H') ? 'hardware_class' : 'inventory_class';
-% $select_class{$value} ||=
-% qsearchs($select_table, { 'classnum' => $value } );
+% foreach my $classnum ( split(',', $value) ) {
+% $select_class{$classnum} =
+% qsearchs($select_table, { 'classnum' => $classnum } );
%
- <% $select_class{$value}
- ? $select_class{$value}->classname
- : "WARNING: $select_table.classnum $value not found" %>
+ <% $select_class{$classnum}
+ ? $select_class{$classnum}->classname
+ : "WARNING: $select_table.classnum $classnum not found" %><BR>
+% }
% } else {
<% $value %>
-% }
+% }
</TD>
% $n1="</TR><TR>";
-% }
-%
+% } #foreach $field
+% if ( $part_svc->restrict_edit_password ) {
+ <TR>
+ <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" COLSPAN=4 ALIGN="left">
+ <B><% emt('Password editing restricted.') %></B>
+ </TD>
+ </TR>
+% }
</TR>
-% }
+% } #foreach $part_svc
</TABLE>
</BODY>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+my $sub_prefixes = sub {
+ my $region = shift;
+ $region->prefixes .
+ ($region->exact_match ? ' <I>(exact match only)</I>' : '');
+};
+
my @header = ( '#', 'Region', 'Country code', 'Prefixes' );
-my @fields = ( 'regionnum', 'regionname', 'ccode', 'prefixes' );
+my @fields = ( 'regionnum', 'regionname', 'ccode', $sub_prefixes );
my @links = ( ($link) x 4 );
my @align = ( 'right', 'left', 'right', 'left' );
my @xls_format = ( ({ locked=>1, bg_color=>22 }) x 4 );
<P>
-Copyright © 2005-2012 Freeside Internet Services, Inc.<BR>
+Copyright © 2005-2013 Freeside Internet Services, Inc.<BR>
Copyright © 2000-2005 Ivan Kohler<BR>
Copyright © 1999 Silicon Interactive Software Design<BR>
All rights reserved<BR>
<SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
<SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
+<SCRIPT TYPE="text/javascript">
+var submit_fields = [];
+function confirm_changes() {
+ var i;
+ var querystring = 'pkgnum=<%$pkgnum%>';
+ var f = document.forms.formname;
+ for(i = 0; i < submit_fields.length; i++) {
+ querystring += ';'
+ + submit_fields[i]
+ + '='
+ + encodeURIComponent(f.elements[submit_fields[i] + '_text'].value);
+ }
+ overlib(
+ OLiframeContent(
+ '<%$p%>/misc/confirm-cust_pkg-edit_dates.html?' + querystring,
+ 576, 576, 'confirm_popup'
+ ),
+ CAPTION, 'Package date changes', STICKY, AUTOSTATUSCAP, CLOSETEXT, '',
+ MIDX, 0, MIDY, 0, DRAGGABLE, BGCOLOR, '#333399', CGCOLOR, '#333399',
+ TEXTSIZE, 3
+ );
+}
+</SCRIPT>
<FORM NAME="formname" ACTION="process/REAL_cust_pkg.cgi" METHOD="POST">
<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
<TD BGCOLOR="#ffffff"><% $part_pkg->pkg %></TD>
</TR>
+% if ( $cust_pkg->main_pkgnum ) {
+% my $main_pkg = $cust_pkg->main_pkg;
+ <TR>
+ <TD ALIGN="right">Supplemental to</TD>
+ <TD BGCOLOR="#ffffff">Package #<% $cust_pkg->main_pkgnum%>: \
+ <% $main_pkg->part_pkg->pkg %></TD>
+ </TR>
+
+% }
<TR>
<TD ALIGN="right">Custom</TD>
<TD BGCOLOR="#ffffff"><% $part_pkg->custom %></TD>
<TR>
<TD ALIGN="right">Comment</TD>
- <TD BGCOLOR="#ffffff"><% $part_pkg->comment %></TD>
+ <TD BGCOLOR="#ffffff"><% $part_pkg->comment |h %></TD>
</TR>
<TR>
% if ( $cust_pkg->setup && ! $cust_pkg->start_date ) {
<& .row_display, cust_pkg=>$cust_pkg, column=>'start_date', label=>'Start' &>
% } else {
- <& .row_edit, cust_pkg=>$cust_pkg, column=>'start_date', label=>'Start' &>
+ <& .row_edit, cust_pkg=>$cust_pkg, column=>'start_date', label=>'Start', if_primary=>1 &>
% }
- <& .row_edit, cust_pkg=>$cust_pkg, column=>'setup', label=>'Setup' &>
+ <& .row_edit, cust_pkg=>$cust_pkg, column=>'setup', label=>'Setup', if_primary=>1 &>
<& .row_edit, cust_pkg=>$cust_pkg, column=>'last_bill', label=>$last_bill_or_renewed &>
<& .row_edit, cust_pkg=>$cust_pkg, column=>'bill', label=>$next_bill_or_prepaid_until &>
%#if ( $cust_pkg->contract_end or $part_pkg->option('contract_end_months',1) ) {
- <& .row_edit, cust_pkg=>$cust_pkg, column=>'contract_end',label=>'Contract end' &>
+ <& .row_edit, cust_pkg=>$cust_pkg, column=>'contract_end',label=>'Contract end', if_primary=>1 &>
%#}
<& .row_display, cust_pkg=>$cust_pkg, column=>'adjourn', label=>'Adjournment', note=>'(will <b>suspend</b> this package when the date is reached)' &>
<& .row_display, cust_pkg=>$cust_pkg, column=>'susp', label=>'Suspension' &>
$column
$label
$note => ''
+ $if_primary => 0
</%args>
% my $value = $cust_pkg->get($column);
% $value = $value ? time2str($format, $value) : "";
-
+%
+% # if_primary for the dates that can't be edited on supplemental packages
+% if ($if_primary and $cust_pkg->main_pkgnum) {
+ <INPUT TYPE="hidden" ID="<%$column%>_text" VALUE="<% $cust_pkg->get($column) %>">
+ <SCRIPT>submit_fields.push('<%$column%>');</SCRIPT>
+ <& .row_display, %ARGS &>
+% } else {
<TR>
<TD ALIGN="right"><% $label %> date</TD>
<TD>
button: "<% $column %>_button",
align: "BR"
});
- </SCRIPT>
+ submit_fields.push('<%$column%>');
+
+ </SCRIPT>
+% }
</%def>
<%def .row_display>
$column
$label
$note => ''
+ $is_primary => 0 #ignored
</%args>
% if ( $cust_pkg->get($column) ) {
<TR>
</TABLE>
<BR>
-<INPUT TYPE="submit" VALUE="<% mt('Apply changes') |h %>">
+<INPUT TYPE="button" VALUE="<% mt('Apply changes') |h %>" onclick="confirm_changes()">
</FORM>
<% include('/elements/footer.html') %>
my @errors = ();
my %errors = map { $_=>1 } split(',', $cgi->param('error'));
$cgi->param('error', '');
-
- if ( $errors{'_bill_areyousure'} ) {
- if ( $cgi->param('bill') =~ /^([\s\d\/\:\-\(\w\)]*)$/ ) {
- my $bill = $1;
- push @errors,
- "You are attempting to set the next bill date to $bill, which is
- in the past. This will charge the customer for the interval
- from $bill until now. Are you sure you want to do this? ".
- '<INPUT TYPE="checkbox" NAME="bill_areyousure" VALUE="1">';
- }
- }
-
- if ( $errors{'_setup_areyousure'} ) {
- push @errors,
- "You are attempting to remove the setup date. This will re-charge the
- customer for the setup fee. Are you sure you want to do this? ".
- '<INPUT TYPE="checkbox" NAME="setup_areyousure" VALUE="1">';
- }
-
- if ( $errors{'_setupadd_areyousure'} ) {
- push @errors,
- "You are attempting to add a setup date. This will prevent charging the
- customer for the setup fee. Are you sure you want to do this? ".
- '<INPUT TYPE="checkbox" NAME="setupadd_areyousure" VALUE="1">';
- }
-
- if ( $errors{'_start'} ) {
- push @errors,
- "You are attempting to add a start date to a package that has already
- started billing.";
- }
-
$error = join('<BR><BR>', @errors );
}
--- /dev/null
+<& /elements/header.html, 'Edit package report classes' &>
+%# change that title if we add any other editing controls
+
+%# this should be centralized somewhere
+<STYLE TYPE="text/css">
+.row0 { background-color: #eeeeee; }
+.row1 { background-color: #ffffff; }
+</STYLE>
+
+<FORM ACTION="process/bulk-part_pkg.html" METHOD="POST">
+<DIV>
+The following packages will be changed:<BR>
+% foreach my $pkgpart (sort keys(%part_pkg)) {
+<INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $pkgpart %>">
+<% $part_pkg{$pkgpart}->pkg_comment |h %><BR>
+% }
+</DIV>
+<BR>
+<& /elements/table-grid.html &>\
+<& /elements/tr-justtitle.html, value => mt('Report classes') &>
+% my $row = 0;
+% foreach my $num (sort keys %report_class) {
+ <TR CLASS="row<%$row % 2%>">
+ <TD>
+% if ( defined $initial_state{$num} ) {
+ <& /elements/checkbox.html,
+ field => 'report_option_'.$num,
+ value => 1,
+ curr_value => $initial_state{$num}
+ &>
+% } else {
+% # needs to be a tristate so that you can say "don't change it"
+ <& /elements/checkbox-tristate.html, field => 'report_option_'.$num &>
+% }
+ </TD>
+ <TD><% $report_class{$num}->name %></TD>
+ </TR>
+% $row++;
+% }
+</TABLE>
+<BR>
+<INPUT TYPE="submit">
+</FORM>
+<& /elements/footer.html &>
+<%init>
+die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Bulk edit package definitions');
+my @pkgparts = $cgi->param('pkgpart')
+ or die "no package definitions selected";
+
+my %part_pkg = map { $_ => FS::part_pkg->by_key($_) } @pkgparts;
+my %part_pkg_option = map { $_ => { $part_pkg{$_}->options } } @pkgparts;
+my %report_class = map { $_->num => $_ }
+ qsearch('part_pkg_report_option', { disabled => '' });
+
+my %initial_state;
+foreach my $num (keys %report_class) {
+ my $yes = 0;
+ my $no = 0;
+ foreach my $option (values %part_pkg_option) {
+ if ( $option->{"report_option_$num"} ) {
+ $yes = 1;
+ } else {
+ $no = 1;
+ }
+ }
+ if ( $yes and $no ) {
+ $initial_state{$num} = undef;
+ } elsif ( $yes ) {
+ $initial_state{$num} = 1;
+ } elsif ( $no ) {
+ $initial_state{$num} = 0;
+ } # else, uh, you didn't provide any pkgparts
+}
+</%init>
<TH ALIGN="right" COLSPAN=2>Total credit amount: </TD>
<TH ALIGN="right" ID="total_td"><% $money_char %><% sprintf('%.2f', 0) %></TD>
</TR>
-<INPUT TYPE="hidden" NAME="amount" ID="total_el" VALUE="0.00">
</table>
+<INPUT TYPE="hidden" NAME="amount" ID="total_el" VALUE="0.00">
+
<table>
<& /elements/tr-select-reason.html,
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
-die "access denied" unless $curuser->access_right('Post credit');
+die "access denied" unless $curuser->access_right('Credit line items');
#a tiny bit of false laziness w/search/cust_bill_pkg.cgi, but we're pretty
# specialized and a piece of UI, not a report
<INPUT TYPE="hidden" NAME="locationnum" VALUE="<% $locationnum %>">
<% ntable('#cccccc') %>
-<% include('/elements/location.html',
- 'object' => $cust_location,
- 'no_asterisks' => 1,
- ) %>
+<& /elements/location.html,
+ 'object' => $cust_location,
+ 'no_asterisks' => 1,
+ # these are service locations, so they need all this stuff
+ 'enable_coords' => 1,
+ 'enable_district' => 1,
+ 'enable_censustract' => 1,
+&>
+<& /elements/standardize_locations.html,
+ 'form' => 'EditLocationForm',
+ 'callback' => 'document.EditLocationForm.submit();',
+&>
</TABLE>
<BR>
<SCRIPT TYPE="text/javascript">
-function areyousure() {
- return confirm('Modify this service location?');
+function go() {
+% if ( FS::Conf->new->config('address_standardize_method') ) {
+ standardize_locations();
+% } else {
+ confirm('Modify this service location?') &&
+ document.EditLocationForm.submit();
+% }
}
</SCRIPT>
-<INPUT TYPE="submit" VALUE="Submit" onclick="return areyousure()">
-
+<INPUT TYPE="button" NAME="submitButton" VALUE="Submit" onclick="go()">
</FORM>
</BODY>
</HTML>
<TD STYLE="width:650px">
%#; padding-right:2px; vertical-align:top">
<FONT CLASS="fsinnerbox-title"><% mt('Billing address') |h %></FONT>
- <TABLE CLASS="fsinnerbox">
+ <TABLE CLASS="fsinnerbox" WIDTH="100%">
<& cust_main/before_bill_location.html, $cust_main &>
<& /elements/location.html,
object => $cust_main->bill_location,
<TR><TD STYLE="height:40px"></TD></TR>
<TR>
<TD STYLE="width:650px">
-%#; padding-left:2px; vertical-align:top">
<FONT CLASS="fsinnerbox-title"><% mt('Service address') |h %></FONT>
<INPUT TYPE="checkbox"
NAME="same"
VALUE="Y"
<% $has_ship_address ? '' : 'CHECKED' %>
><% mt('same as billing address') |h %>
- <TABLE CLASS="fsinnerbox" ID="table_ship_location">
- <& /elements/location.html,
- object => $cust_main->ship_location,
- prefix => 'ship_',
- enable_censustract => 1,
- enable_district => 1,
- enable_coords => 1,
- &>
- </TABLE>
- <TABLE CLASS="fsinnerbox" ID="table_ship_location_blank"
- STYLE="display:none">
- <TR><TD></TD></TR>
- </TABLE>
+ <DIV CLASS="fsinnerbox">
+ <TABLE ID="table_ship_location" WIDTH="100%">
+ <& /elements/location.html,
+ object => $cust_main->ship_location,
+ prefix => 'ship_',
+ enable_censustract => 1,
+ enable_district => 1,
+ enable_coords => 1,
+ &>
+ </TABLE>
+ </DIV>
</TD>
</TR></TABLE>
%# document.getElementById('table_ship_location').style.visibility =
%# what.checked ? 'hidden' : 'visible';
var t1 = document.getElementById('table_ship_location');
- var t2 = document.getElementById('table_ship_location_blank');
if ( what.checked ) {
- t2.style.width = t1.clientWidth + 'px';
- t2.style.height = t1.clientHeight + 'px';
- t1.style.display = 'none';
- t2.style.display = '';
+ t1.style.visibility = 'hidden';
}
else {
- t2.style.display = 'none';
- t1.style.display = '';
+ t1.style.visibility = 'visible'
}
}
-samechanged(document.getElementById('same'));
+//samechanged(document.getElementById('same'));
</SCRIPT>
<BR>
my( $query ) = $cgi->keywords;
$query =~ /^(\d+)$/;
$custnum=$1;
- $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+ $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+ or die "custnum $custnum not found";
if ( $cust_main->dbdef_table->column('paycvv')
&& length($cust_main->paycvv) ) {
my $paycvv = $cust_main->paycvv;
<TR><TD> </TD></TR>
+% my $curuser = $FS::CurrentUser::CurrentUser;
% my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups');
-
% if ( $conf->exists('cust_class-tax_exempt')
% || $conf->exists('tax-cust_exempt-groups-require_individual_nums')
+% || ! $curuser->access_right('Edit customer tax exemptions')
% )
% {
% }
-% foreach my $exempt_group ( @exempt_groups ) {
-% my $cust_main_exemption = $cust_main->tax_exemption($exempt_group);
-% #escape $exempt_group for NAME etc.
-% my $checked = ($cust_main_exemption || $cgi->param("tax_$exempt_group"));
- <TR>
- <TD> <INPUT TYPE="checkbox" NAME="tax_<% $exempt_group %>" ID="tax_<% $exempt_group %>" VALUE="Y" <% $checked ? 'CHECKED' : '' %> onChange="tax_changed(this)"> Tax Exempt (<% $exempt_group %> taxes)</TD>
- <TD> - Exemption number <INPUT TYPE="text" NAME="tax_<% $exempt_group %>_num" ID="tax_<% $exempt_group %>_num" VALUE="<% $cgi->param("tax_$exempt_group".'_num') || ( $cust_main_exemption ? $cust_main_exemption->exempt_number : '' ) |h %>" <% $checked ? '' : 'DISABLED' %>></TD>
- </TR>
+% if ( $curuser->access_right('Edit customer tax exemptions') ) {
+% foreach my $exempt_group ( @exempt_groups ) {
+% my $cust_main_exemption = $cust_main->tax_exemption($exempt_group);
+% #escape $exempt_group for NAME etc.
+% my $checked = ($cust_main_exemption || $cgi->param("tax_$exempt_group"));
+ <TR>
+ <TD> <INPUT TYPE="checkbox" NAME="tax_<% $exempt_group %>" ID="tax_<% $exempt_group %>" VALUE="Y" <% $checked ? 'CHECKED' : '' %> onChange="tax_changed(this)"> Tax Exempt (<% $exempt_group %> taxes)</TD>
+ <TD> - Exemption number <INPUT TYPE="text" NAME="tax_<% $exempt_group %>_num" ID="tax_<% $exempt_group %>_num" VALUE="<% $cgi->param("tax_$exempt_group".'_num') || ( $cust_main_exemption ? $cust_main_exemption->exempt_number : '' ) |h %>" <% $checked ? '' : 'DISABLED' %>></TD>
+ </TR>
+% }
% }
% unless ( $conf->exists('emailinvoiceonly') ) {
<% $conf->exists('cust_main-require_invoicing_list_email', $agentnum)
? $r : '' %>Email address(es)
</TD>
- <TD WIDTH="408"><INPUT TYPE="text" NAME="invoicing_list" VALUE="<% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) %>"></TD>
+ <TD WIDTH="408"><INPUT TYPE="text" NAME="invoicing_list" VALUE="<% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) %>">
+ <INPUT TYPE="checkbox" NAME="message_noemail" VALUE="Y" <%
+ ( $cust_main->message_noemail eq 'Y' )
+ ? 'CHECKED'
+ : ''
+ %>> <% emt('Do not send notices') %>
+ </TD>
</TR>
% }
<& /elements/standardize_locations.js,
'callback' => 'submit_continue();',
- 'main_prefix' => 'bill_',
- 'no_company' => 1,
+ 'billship' => 1,
+ 'with_census' => 1, # no with_firm, apparently
&>
function copyelement(from, to) {
+++ /dev/null
-<FORM NAME="choosegeocodeform">
-<CENTER><BR><B>Choose tax location</B><BR><BR>
-<P>the geocode is:<% $header %></P>
-<P STYLE="<% $style %>"><% $header %></P>
-
-<SELECT NAME='geocodes' ID='geocodes' STYLE="<% $style %>">
-% foreach my $location (@cust_tax_location) {
-% my %value = ( zip => $zip5,
-% map { $_ => $location->$_ }
-% qw ( city state geocode )
-% );
-% map { $value{$_} = $location{$_} } qw ( city state )
-% if $location{country} eq 'CA';
-%
-% my $value = encode_entities(objToJson({ %value })
-% );
-% my $content = '';
-% $content .= $location->$_. ' ' x ( $max{$_} - length($location->$_) )
-% foreach qw( city county state );
-% $content .= $location->cityflag eq 'I' ? 'Y' : 'N' ;
-% my $selected = '' ;
-% if ($geocode && $location->geocode eq $geocode) {
-% $selected = 'SELECTED';
-% }
- <OPTION VALUE="<% $value %>" STYLE="<% $style %>" <% $selected %>><% $content %>
-% }
-</SELECT><BR><BR>
-
-<TABLE><TR>
- <TD> <BUTTON TYPE="button" onClick="set_geocode(document.getElementById('geocodes'));"><IMG SRC="<%$p%>images/tick.png" ALT=""> Set location </BUTTON></TD>
- <TD><BUTTON TYPE="button" onClick="document.CustomerForm.submitButton.disabled=false; parent.cClick();"><IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel submission </BUTTON></TD>
-</TR>
-</TABLE>
-
-</CENTER>
-</FORM>
-<%init>
-
-my $conf = new FS::Conf;
-
-my %location = ();
-
-($location{data_vendor}) = $cgi->param('data_vendor') =~ /^([-\w]+)$/;
-($location{city}) = $cgi->param('city') =~ /^([\w ]+)$/;
-($location{state}) = $cgi->param('state') =~ /^(\w+)$/;
-($location{zip}) = $cgi->param('zip') =~ /^([-\w ]+)$/;
-($location{country}) = $cgi->param('country') =~ /^([\w ]+)$/;
-
-my($geocode) = $cgi->param('geocode') =~ /^([\w]+)$/;
-
-my($zip5, $zip4) = split('-', $location{zip});
-
-#only support US & CA
-my $hashref = { 'data_vendor' => $location{data_vendor} };
-$hashref->{zip} = $location{country} eq 'CA' ? substr($zip5,0,1) : $zip5,
-
-my @keys = keys(%$hashref);
-my @cust_tax_location = ();
-until ( @cust_tax_location ) {
- @cust_tax_location = qsearch({ table => 'cust_tax_location',
- hashref => $hashref,
- order_by => 'LIMIT 50',
- });
- last unless scalar(@keys);
- delete $hashref->{ shift @keys };
-}
-
-my %max = ( city => 4, county => 6, state => 5);
-foreach my $location (@cust_tax_location) {
- foreach ( qw( city county state ) ) {
- my $length = length($location->$_);
- $max{$_} = ($length > $max{$_}) ? $length : $max{$_};
- }
-}
-foreach ( qw( city county state ) ) {
- $max{$_} = $location{$_} if $location{$_} > $max{$_};
- $max{$_}++;
-}
-
-my $header = ' ';
-$header .= $_. ' ' x ( $max{lc($_)} - length($_) )
- foreach qw( City County State );
-$header .= "In city?";
-
-my $style = "font-family:monospace;";
-
-</%init>
document.getElementById('contacts_div').style.display = 'none';
}
}
+
+ var ship_locked_agents = <% encode_json(\%ship_locked_agents) %>;
+ var ship_fields = ['address1', 'city', 'state', 'zip', 'country',
+ 'latitude', 'longitude', 'district'];
+ function agent_changed(what) {
+ var agentnum = what.value;
+ var f = what.form;
+ if ( ship_locked_agents[agentnum] ) {
+% # For this agent, the service location (except address2)
+% # should be locked to the agent's location.
+% # Set the ship_ fields to those values (just for display) and
+% # then disable them.
+ for(var x in ship_locked_agents[agentnum]) {
+ f['ship_'+x].value = ship_locked_agents[agentnum][x];
+ f['ship_'+x].disabled = true;
+ }
+ f['same'].checked = false;
+ f['same'].disabled = true;
+ } else {
+% # Unlock the ship_ location fields. If they were previously
+% # disabled, then they contain some agent's address, which is
+% # no longer meaningful. So set them back to the customer's
+% # current location.
+ for(var i=0; i<ship_fields.length; i++) {
+ x = ship_fields[i];
+ if ( f['ship_'+x].disabled ) {
+ f['ship_'+x].value = f['old_ship_'+x].value;
+ }
+ f['ship_'+x].disabled = false;
+ }
+ f['same'].disabled = false;
+ }
+ samechanged(f['same']);
+ }
+ window.onload = function() {
+ agent_changed(document.getElementById('agentnum'));
+ }
+
</SCRIPT>
% foreach my $field ($cust_main->virtual_fields) {
% $cust_main->agentnum($agentnum);
<INPUT TYPE="hidden" NAME="lock_agentnum" VALUE="<% $agentnum %>">
- <INPUT TYPE="hidden" NAME="agentnum" VALUE="<% $agentnum %>">
+ <INPUT TYPE="hidden" NAME="agentnum" ID="agentnum"
+ VALUE="<% $agentnum %>">
<TR>
<TD ALIGN="right"><% mt('Agent') |h %></TD>
<TD CLASS="fsdisabled"><% $cust_main->agent->agent |h %></TD>
</TR>
-
+
% } else {
<& /elements/tr-select-agent.html,
'empty_label' => emt('Select agent'),
'disable_empty' => ( $cust_main->agentnum ? 1 : 0 ),
'viewall_right' => emt('None'),
+ 'onchange' => 'agent_changed(this)',
&>
% }
my $r = qq!<font color="#ff0000">*</font> !;
+# which agents lock the service address, if any
+my %ship_locked_agents;
+foreach (qsearch('agent',{})) {
+ my $agentnum = $_->agentnum;
+ next unless $conf->exists('agent-ship_address', $_->agentnum);
+ my $cust_main = $_->agent_cust_main or next;
+ my $agent_ship_location = $cust_main->ship_location;
+ $ship_locked_agents{$agentnum} = +{
+ map { $_ => $agent_ship_location->$_ }
+ qw(address1 city state zip country latitude longitude district)
+ };
+}
+
</%init>
<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
%#current packages
-%my @cust_pkg = qsearch('cust_pkg', { 'custnum' => $custnum, 'cancel' => '' } );
%if (@cust_pkg) {
Current packages - select to remove (services are moved to a new package below)
</TR>
<BR><BR>
%
-%
-% foreach ( sort { $all_pkg{ $a->getfield('pkgpart') }
-% cmp $all_pkg{ $b->getfield('pkgpart') }
-% }
-% @cust_pkg
-% )
-% {
+% foreach ( @main_pkgs ) {
% my($pkgnum,$pkgpart)=( $_->getfield('pkgnum'), $_->getfield('pkgpart') );
% my $checked = $remove_pkg{$pkgnum} ? ' CHECKED' : '';
%
<TD ALIGN="right"><% $pkgnum %>:</TD>
<TD><% $all_pkg{$pkgpart} %> - <% $all_comment{$pkgpart} %></TD>
</TR>
+% foreach my $supp_pkg ( @{ $supp_pkgs_of{$pkgnum} } ) {
+ <TR>
+ <TD></TD>
+ <TD></TD>
+ <TD>+ <% $all_pkg{$supp_pkg->pkgpart} %> - <% $all_comment{$supp_pkg->pkgpart} %></TD>
+ </TR>
+% }
% }
my $p1 = popurl(1);
+my @cust_pkg = qsearch('cust_pkg', { 'custnum' => $custnum, 'cancel' => '' } );
+my @main_pkgs;
+my %supp_pkgs_of; # main pkgnum => arrayref of cust_pkgs
+
+
+foreach my $cust_pkg
+ ( sort { $all_pkg{ $a->pkgpart } cmp $all_pkg{ $b->getfield('pkgpart') } }
+ @cust_pkg
+ )
+ # XXX does not properly handle recursive supplemental links
+{
+ if ( my $main_pkgnum = $cust_pkg->main_pkgnum ) {
+ $supp_pkgs_of{$main_pkgnum} ||= [];
+ push @{ $supp_pkgs_of{$main_pkgnum} }, $cust_pkg;
+ } else {
+ push @main_pkgs, $cust_pkg;
+ $supp_pkgs_of{$cust_pkg->pkgnum} ||= [];
+ }
+}
+
</%init>
<TR>
<TD ALIGN="right">Comment</TD>
- <TD BGCOLOR="#ffffff"><% $part_pkg->comment %></TD>
+ <TD BGCOLOR="#ffffff"><% $part_pkg->comment |h %></TD>
</TR>
<TR>
--- /dev/null
+<& /elements/header-popup.html, "Change Quantity" &>
+<& /elements/error.html &>
+
+<FORM ACTION="<% $p %>edit/process/cust_pkg_quantity.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
+<& /elements/table-grid.html, 'bgcolor' => '#cccccc', 'cellpadding' => 2 &>
+
+ <TR>
+ <TH ALIGN="right">Current package </TH>
+ <TD CLASS="grid">
+ <% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B> - <% $part_pkg->comment |h %>
+ </TD>
+ </TR>
+
+<& /elements/tr-input-text.html,
+ 'field' => 'quantity',
+ 'curr_value' => $cust_pkg->quantity,
+ 'label' => emt('Quantity')
+&>
+
+</TABLE>
+
+<BR>
+<INPUT NAME="submit" TYPE="submit" VALUE="Change">
+
+</FORM>
+</BODY>
+</HTML>
+
+<%init>
+
+#some false laziness w/misc/change_pkg.cgi
+
+my $conf = new FS::Conf;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('Change customer package');
+
+my $pkgnum = scalar($cgi->param('pkgnum'));
+$pkgnum =~ /^(\d+)$/ or die "illegal pkgnum $pkgnum";
+$pkgnum = $1;
+
+my $cust_pkg = FS::cust_pkg->by_key($pkgnum) or die "unknown pkgnum $pkgnum";
+
+my $part_pkg = $cust_pkg->part_pkg;
+
+</%init>
</TD>
</TR>
% }
-
+% if ( $cust_pay->processor ) {
<TR>
<TD ALIGN="right">Processor</TD>
<TD BGCOLOR="#ffffff"><% $cust_pay->processor %></TD>
</TR>
-% if ( length($auth) ) {
+% if ( length($cust_pay->auth) ) {
<TR>
<TD ALIGN="right">Authorization</TD>
<TD BGCOLOR="#ffffff"><% $cust_pay->order_number %></TD>
</TR>
% }
-% } #if $cust_pay
+% } # if ($cust_pay->processor)
</TABLE>
-% }
+% } #if $cust_pay
<BR>Refund
% qw( country ), #select-country
% qw( width height ), #htmlarea
% qw( alt_format ), #select-cust_location
+% qw( classnum ), # select-inventory_item
% ;
%
% #select-table
--- /dev/null
+<%doc>
+To be called from part_svc.cgi.
+<& elements/part_svc_column.html,
+ 'svc_acct',
+ # options...
+ 'part_svc' => $part_svc, # the existing part_svc to edit
+ 'clone' => 0, # or a svcpart to clone from
+&>
+
+</%doc>
+<%once>
+# the semantics of this could be better
+
+# all of these conditions are when NOT to allow that flag choice
+# don't allow the 'inventory' flags (M, A) to be chosen for
+# fields that aren't free-text
+my $inv_sub = sub { $_[0]->{disable_inventory} || $_[0]->{type} ne 'text' };
+tie my %flag, 'Tie::IxHash',
+ '' => { 'desc' => 'No default', 'condition' => sub { 0 } },
+ 'D' => { 'desc' => 'Default',
+ 'condition' =>
+ sub { $_[0]->{disable_default } }
+ },
+ 'F' => { 'desc' => 'Fixed (unchangeable)',
+ 'condition' =>
+ sub { $_[0]->{disable_fixed} },
+ },
+ 'S' => { 'desc' => 'Selectable Choice',
+ 'condition' =>
+ sub { $_[0]->{disable_select} },
+ },
+ 'M' => { 'desc' => 'Manual selection from inventory',
+ 'condition' => $inv_sub,
+ },
+ 'A' => { 'desc' => 'Automatically fill in from inventory',
+ 'condition' => $inv_sub,
+ },
+ 'H' => { 'desc' => 'Select from hardware class',
+ 'condition' => sub { $_[0]->{type} ne 'select-hardware' },
+ },
+ 'X' => { 'desc' => 'Excluded',
+ 'condition' => sub { 1 }, # obsolete
+ },
+;
+
+# the semantics of this could be much better
+sub flag_condition {
+ my $f = shift;
+ not &{ $flag{$f}->{'condition'} }(@_);
+}
+
+my %communigate_fields = (
+ 'svc_acct' => { map { $_=>1 }
+ qw( file_quota file_maxnum file_maxsize
+ password_selfchange password_recover
+ ),
+ grep /^cgp_/, fields('svc_acct')
+ },
+ 'svc_domain' => { map { $_=>1 }
+ qw( max_accounts trailer parent_svcnum ),
+ grep /^(cgp|acct_def)_/, fields('svc_domain')
+ },
+);
+</%once>
+<INPUT TYPE="hidden" NAME="svcdb" VALUE="<% $svcdb %>">
+<BR><BR>
+<& /elements/table.html &>
+ <TR><TH COLSPAN=<% $columns %>>Exports</TH></TR>
+ <TR>
+% # exports
+% foreach my $part_export (@part_export) {
+ <TD>
+ <INPUT TYPE="checkbox" \
+ NAME="exportnum<% $part_export->exportnum %>" \
+ VALUE=1 \
+ <% $has_export_svc{$part_export->exportnum} ? 'CHECKED' : '' %>>
+ <% $part_export->label_html %>
+ </TD>
+% $count++;
+% if ( $count % $columns == 0 ) {
+ </TR>
+ <TR>
+% }
+% }
+ </TR>
+</TABLE><BR><BR>
+For the selected table, you can give fields default or fixed (unchangeable)
+values, or select an inventory class to manually or automatically fill in
+that field.
+<& /elements/table-grid.html, cellpadding => 4 &>
+ <TR>
+ <TH BGCOLOR="#cccccc">Field</TH>
+ <TH BGCOLOR="#cccccc">Label</TH>
+ <TH BGCOLOR="#cccccc" COLSPAN=2>Modifier</TH>
+ </TR>
+% $part_svc->set('svcpart' => $opt{'clone'}) if $opt{'clone'}; # for now
+% my $i = 0;
+% foreach my $field (@fields) {
+% my $def = shift @defs;
+% my $part_svc_column = $part_svc->part_svc_column($field);
+% my $flag = $part_svc_column->columnflag;
+% my $formatter = $def->{'format'} || sub { shift };
+% my $value = &{$formatter}($part_svc_column->columnvalue);
+ <TR CLASS="row<%$i%>">
+ <TD ROWSPAN=2 CLASS="grid" ALIGN="right">
+ <% $def->{'label'} || $field %>
+ </TD>
+ <TD ROWSPAN=2 CLASS="grid">
+ <INPUT NAME="<% $svcdb %>__<% $field %>_label"
+ STYLE="text-align: right"
+ VALUE="<% $part_svc_column->columnlabel || $def->{'label'} |h %>">
+ </TD>
+
+ <TD ROWSPAN=1 CLASS="grid">
+% # flag selection
+% if ( $def->{'type'} eq 'disabled' ) {
+% $flag = '';
+ No default
+% } else {
+% my $name = $svcdb.'__'.$field.'_flag';
+ <SELECT NAME="<%$name%>"
+ ID="<%$name%>"
+ STYLE="width:100%"
+ onchange="flag_changed(this)">
+% foreach my $f (keys %flag) {
+% if ( flag_condition($f, $def, $svcdb, $field) ) {
+ <OPTION VALUE="<%$f%>"<% $flag eq $f ? ' SELECTED' : ''%>>
+ <% $flag{$f}->{desc} %>
+ </OPTION>
+% }
+% }
+ </SELECT>
+% } # if $def->{'type'} eq 'disabled'
+ </TD>
+ <TD CLASS="grid">
+% # value entry/selection
+% my $name = $svcdb.'__'.$field;
+% # These are all MANDATORY SELECT types. Regardless of the flag value,
+% # there will never be a text input (either in svc_* or in part_svc) for
+% # these fields.
+% if ( $def->{'type'} eq 'checkbox' ) {
+ <& /elements/checkbox.html,
+ 'field' => $name,
+ 'curr_value' => $value,
+ 'value' => 'Y' &>
+%
+% } elsif ( $def->{'type'} eq 'select' ) {
+%
+% if ( $def->{'select_table'} ) {
+ <& /elements/select-table.html,
+ 'field' => $name,
+ 'id' => $name.'_select',
+ 'table' => $def->{'select_table'},
+ 'name_col' => $def->{'select_label'},
+ 'value_col' => $def->{'select_key'},
+ 'order_by' => dbdef->table($def->{'select_table'})->primary_key,
+ 'multiple' => $def->{'multiple'},
+ 'disable_empty' => 1,
+ 'curr_value' => $value,
+ &>
+% } else {
+% my (@options, %labels);
+% if ( $def->{'select_list'} ) {
+% @options = @{ $def->{'select_list'} };
+% @labels{@options} = @options;
+% } elsif ( $def->{'select_hash'} ) {
+% if ( ref($def->{'select_hash'}) eq 'ARRAY' ) {
+% tie my %hash, 'Tie::IxHash', @{ $def->{'select_hash'} };
+% $def->{'select_hash'} = \%hash;
+% }
+% @options = keys( %{ $def->{'select_hash'} } );
+% %labels = %{ $def->{'select_hash'} };
+% }
+ <& /elements/select.html,
+ 'field' => $name,
+ 'id' => $name.'_select',
+ 'options' => \@options,
+ 'labels' => \%labels,
+ 'multiple' => $def->{'multiple'},
+ 'curr_value' => $value,
+ &>
+% }
+% } elsif ( $def->{'type'} =~ /select-(.*?).html/ ) {
+ <& '/elements/'.$def->{'type'},
+ 'field' => $name,
+ 'id' => $name.'_select',
+ 'multiple' => $def->{'multiple'},
+ 'curr_value' => $value,
+ &>
+% } elsif ( $def->{'type'} eq 'communigate_pro-accessmodes' ) {
+ <& /elements/communigate_pro-accessmodes.html,
+ 'element_name_prefix' => $name.'_',
+ 'curr_value' => $value,
+ &>
+% } elsif ( $def->{'type'} eq 'textarea' ) {
+% # special cases
+ <TEXTAREA NAME="<%$name%>"><% $value |h %></TEXTAREA>
+% } elsif ( $def->{'type'} eq 'disabled' ) {
+ <INPUT TYPE="hidden" NAME="<%$name%>" VALUE="">
+% } else {
+% # the normal case: a text input, and a _select which is an inventory
+% # or hardware class
+ <INPUT TYPE="text"
+ NAME="<%$name%>"
+ ID="<%$name%>"
+ VALUE="<%$value%>">
+% # inventory class selection
+ <& /elements/select-table.html,
+ 'field' => $name.'_classnum',
+ 'id' => $name.'_select',
+ 'table' => 'inventory_class',
+ 'name_col' => 'classname',
+ 'curr_value' => $value,
+ 'empty_label' => 'Select inventory class',
+ 'multiple' => 1,
+ &>
+% }
+ </TD>
+ </TR>
+ <TR CLASS="row<%$i%>">
+ <TD COLSPAN=2 CLASS="def_info">
+% if ( $def->{def_info} ) {
+ (<% $def->{def_info} %>)
+ </TD>
+ </TR>
+% }
+% $i = 1-$i;
+% } # foreach my $field
+%
+% # special case: svc_acct password edit ACL
+% if ( $svcdb eq 'svc_acct' ) {
+% push @fields, 'restrict_edit_password';
+ <TR>
+ <TD COLSPAN=3 ALIGN="right">
+ <% emt('Require "Provision" access right to edit password') %>
+ </TD>
+ <TD>
+ <INPUT TYPE="checkbox" NAME="restrict_edit_password" VALUE="Y" \
+ <% $part_svc->restrict_edit_password ? 'CHECKED' : '' %>>
+ </TD>
+ </TR>
+% }
+</TABLE>
+<& /elements/progress-init.html,
+ $svcdb, #form name
+ [ # form fields to send
+ qw(svc svcpart classnum selfservice_access disabled preserve exportnum),
+ @fields
+ ],
+ 'process/part_svc.cgi', # target
+ $p.'browse/part_svc.cgi', # redirect landing
+ $svcdb, #key
+&>
+% $svcpart = '' if $opt{clone};
+<BR>
+<INPUT NAME="submit"
+ TYPE="button"
+ VALUE="<% emt($svcpart ? 'Apply changes' : 'Add service') %>"
+ onclick="fixup_submit('<%$svcdb%>')"
+>
+<%init>
+my $svcdb = shift;
+my %opt = @_;
+my $columns = 3;
+my $count = 0;
+my $communigate = 0;
+my $conf = FS::Conf->new;
+
+my $part_svc = $opt{'part_svc'} || FS::part_svc->new;
+
+my @part_export;
+my $export_info = FS::part_export::export_info($svcdb);
+foreach (keys %{ $export_info }) {
+ push @part_export, qsearch('part_export', { exporttype => $_ });
+}
+$communigate = scalar(grep {$_->exporttype =~ /^communigate/} @part_export);
+
+my $svcpart = $opt{'clone'} || $part_svc->svcpart;
+my %has_export_svc;
+if ( $svcpart ) {
+ foreach (qsearch('export_svc', { svcpart => $svcpart })) {
+ $has_export_svc{$_->exportnum} = 1;
+ }
+}
+
+my @fields;
+if ( defined( dbdef->table($svcdb) ) ) { # when is it ever not defined?
+ @fields = grep {
+ $_ ne 'svcnum'
+ and ( $communigate || ! $communigate_fields{$svcdb}->{$_} )
+ and ( !FS::part_svc->svc_table_fields($svcdb)->{$_}->{disable_part_svc_column}
+ || $part_svc->part_svc_column($_)->columnflag )
+ } fields($svcdb);
+}
+if ( $svcdb eq 'svc_acct'
+ or ( $svcdb eq 'svc_broadband' and $conf->exists('svc_broadband-radius') )
+ )
+{
+ push @fields, 'usergroup';
+}
+
+my @defs = map { FS::part_svc->svc_table_fields($svcdb)->{$_} } @fields;
+</%init>
} elsif ( $flag eq 'A' ) {
$f->{'type'} = 'hidden';
} elsif ( $flag eq 'M' ) {
+ $f->{'type'} = 'select-inventory_item';
$f->{'empty_label'} = 'Select inventory item';
- $f->{'type'} = 'select-table';
- $f->{'table'} = 'inventory_item';
- $f->{'name_col'} = 'item';
- $f->{'value_col'} = 'item';
- $f->{'agent_virt'} = 1;
- $f->{'agent_null'} = 1;
- $f->{'hashref'} = {
- 'classnum'=>$columndef->columnvalue,
- #'svcnum' => '',
- };
- $f->{'extra_sql'} = 'AND ( svcnum IS NULL ';
- $f->{'extra_sql'} .= ' OR svcnum = '. $object->svcnum
- if $object->svcnum;
- $f->{'extra_sql'} .= ' ) ';
+ $f->{'extra_sql'} = 'WHERE ( svcnum IS NULL ' .
+ ($object->svcnum && ' OR svcnum = '.$object->svcnum) .
+ ')';
+ $f->{'classnum'} = $columndef->columnvalue;
$f->{'disable_empty'} = $object->svcnum ? 1 : 0;
- if ( $f->{'field'} eq 'mac_addr' ) {
- $f->{'compare_sub'} = sub {
- my($a, $b) = @_;
- $a =~ s/[-: ]//g;
- $b =~ s/[-: ]//g;
- lc($a) eq lc($b);
- };
- }
} elsif ( $flag eq 'H' ) {
$f->{'type'} = 'select-hardware_type';
$f->{'hashref'} = {
<% include('/elements/error.html') %>
+<SCRIPT TYPE="text/javascript">
+ function svc_machine_changed (what, layer) {
+ if ( what.checked ) {
+ var machine = document.getElementById(layer + "_machine");
+ var part_export_machine =
+ document.getElementById(layer + "_part_export_machine");
+ if ( what.value == 'Y' ) {
+ machine.disabled = true;
+ part_export_machine.disabled = false;
+ } else if ( what.value == 'N' ) {
+ machine.disabled = false;
+ part_export_machine.disabled = true;
+ }
+ }
+ }
+
+ function part_export_machine_changed (what, layer) {
+ var select_default = document.getElementById(layer + '_default_machine');
+ var selected = select_default.value;
+ select_default.options.length = 0;
+ var choices = what.value.split("\n");
+ for (var i = 0; i < choices.length; i++) {
+ select_default.options[i] = new Option(choices[i]);
+ }
+ select_default.value = selected;
+ }
+
+</SCRIPT>
<FORM NAME="dummy">
<INPUT TYPE="hidden" NAME="exportnum" VALUE="<% $part_export->exportnum %>">
'form_name' => 'dummy',
'form_action' => 'process/part_export.cgi',
'form_text' => [qw( exportnum exportname )],
-# 'form_checkbox' => [qw()],
'html_between' => "</TD></TR></TABLE>\n",
'layer_callback' => sub {
my $layer = shift;
if ( $exports->{$layer}{svc_machine} ) {
my( $N_CHK, $Y_CHK) = ( 'CHECKED', '' );
my( $machine_DISABLED, $pem_DISABLED) = ( '', 'DISABLED' );
- my $part_export_machine = '';
+ my @part_export_machine;
+ my $default_machine = '';
if ( $cgi->param('svc_machine') eq 'Y'
|| $machine eq '_SVC_MACHINE'
)
$machine_DISABLED = 'DISABLED';
$pem_DISABLED = '';
$machine = '';
- $part_export_machine =
- $cgi->param('part_export_machine')
- || join "\n",
+ @part_export_machine = $cgi->param('part_export_machine');
+ if (!@part_export_machine) {
+ @part_export_machine =
map $_->machine,
grep ! $_->disabled,
$part_export->part_export_machine;
+ }
+ $default_machine =
+ $cgi->param('default_machine_name')
+ || $part_export->default_export_machine;
}
- my $oc = qq(onChange="${layer}_svc_machine_changed(this)");
+ my $oc = qq(onChange="svc_machine_changed(this, '$layer')");
$html .= qq[
<INPUT TYPE="radio" NAME="svc_machine" VALUE="N" $N_CHK $oc>
<INPUT TYPE="text" NAME="machine" ID="${layer}_machine" VALUE="$machine" $machine_DISABLED>
<BR>
<INPUT TYPE="radio" NAME="svc_machine" VALUE="Y" $Y_CHK $oc>
- Selected in each customer service from these choices
- <TEXTAREA NAME="part_export_machine" ID="${layer}_part_export_machine" $pem_DISABLED>$part_export_machine</TEXTAREA>
-
- <SCRIPT TYPE="text/javascript">
- function ${layer}_svc_machine_changed (what) {
- if ( what.checked ) {
- var machine = document.getElementById("${layer}_machine");
- var part_export_machine = document.getElementById("${layer}_part_export_machine");
- if ( what.value == 'Y' ) {
- machine.disabled = true;
- part_export_machine.disabled = false;
- } else if ( what.value == 'N' ) {
- machine.disabled = false;
- part_export_machine.disabled = true;
- }
- }
- }
- </SCRIPT>
+ <DIV STYLE="display:inline-block; vertical-align: top; text-align: right">
+ Selected in each customer service from these choices:
+ <TEXTAREA STYLE="vertical-align: top" NAME="part_export_machine"
+ ID="${layer}_part_export_machine"
+ onchange="part_export_machine_changed(this, '$layer')"
+ $pem_DISABLED>] .
+
+ join("\n", @part_export_machine) .
+
+ qq[</TEXTAREA>
+ <BR>
+ Default:
+ <SELECT NAME="default_machine_name" ID="${layer}_default_machine">
];
+ foreach (@part_export_machine) {
+ $_ = encode_entities($_); # oh noes, XSS
+ my $sel = ($default_machine eq $_) ? ' SELECTED' : '';
+ $html .= qq!<OPTION VALUE="$_"$sel>$_</OPTION>\n!;
+ }
+ $html .= '</DIV></SELECT>'
} else {
$html .= qq(<INPUT TYPE="text" NAME="machine" VALUE="$machine">).
'<INPUT TYPE="hidden" NAME="svc_machine" VALUE=N">';
'labels' => {
'pkgpart' => 'Package Definition',
- 'pkg' => 'Package (customer-visible)',
+ 'pkg' => 'Package',
+ %locale_field_labels,
'comment' => 'Comment (customer-hidden)',
'classnum' => 'Package class',
'addon_classnum' => 'Restrict additional orders to package class',
'discountnum' => 'Offer discounts for longer terms',
'bill_dst_pkgpart' => 'Include line item(s) from package',
'svc_dst_pkgpart' => 'Include services of package',
+ 'supp_dst_pkgpart' => 'Include complete package',
'report_option' => 'Report classes',
'fcc_ds0s' => 'Voice-grade equivalents',
'fcc_voip_class' => 'Category',
size => 40, #32
maxlength => 50,
},
+ #@locale_fields,
{field=>'comment', type=>'text', size=>40 }, #32
{ field => 'agentnum',
type => 'select-agent',
},
{ 'type' => 'tablebreak-tr-title',
+ 'value' => 'Supplemental packages',
+ 'colspan' => '4',
+ },
+ { 'field' => 'supp_dst_pkgpart',
+ 'type' => 'select-part_pkg',
+ 'm2_label' => 'Include complete package',
+ 'm2m_method' => 'supp_part_pkg_link',
+ 'm2m_dstcol' => 'dst_pkgpart',
+ 'm2_error_callback' =>
+ &{$m2_error_callback_maker}('supp'),
+ },
+
+ { 'type' => 'tablebreak-tr-title',
'value' => 'Pricing add-ons',
'colspan' => 4,
},
my $conf = new FS::Conf;
my $taxproducts = $conf->exists('enable_taxproducts');
+my @locales = grep { ! /^en_/i } $conf->config('available-locales'); #should filter from the default locale lang instead of en_
+my %locale_labels = map {
+ ( $_ => 'Package -- '. FS::Locales->description($_) )
+} @locales;
+@locales =
+ sort { $locale_labels{$a} cmp $locale_labels{$b} }
+ @locales;
+
+my $n = 0;
+my %locale_field_labels = (
+ map {
+ ( 'pkgpartmsgnum'. $n++. '_pkg' => $locale_labels{$_} );
+ }
+ @locales
+);
+
my $sth = dbh->prepare("SELECT COUNT(*) FROM part_pkg_report_option".
" WHERE disabled IS NULL OR disabled = '' ")
or die dbh->errstr;
my $pkgpart = '';
+my $splice_locale_fields = sub {
+ my( $fields, $pkey_value_callback, $pkg_value_callback ) = @_;
+
+ my $n = 0;
+ my @locale_fields = (
+ map {
+ my $pkey_value= $pkey_value_callback ? &$pkey_value_callback($_) : '';
+ my $pkg_value = $pkg_value_callback
+ ? $pkg_value_callback eq 'cgiparam'
+ ? $cgi->param('pkgpartmsgnum'. $n. '_pkg')
+ : &$pkg_value_callback($_)
+ : '';
+ (
+ { field => 'pkgpartmsgnum'. $n,
+ type => 'hidden',
+ value => $pkey_value,
+ },
+ { field => 'pkgpartmsgnum'. $n. '_locale',
+ type => 'hidden',
+ value => $_,
+ },
+ { field => 'pkgpartmsgnum'. $n++. '_pkg',
+ type => 'text',
+ size => 40,
+ #maxlength => 50,
+ value => $pkg_value,
+ },
+ );
+
+ }
+ @locales
+ );
+ splice(@$fields, 7, 0, @locale_fields); #XXX 7 is arbitrary above
+
+};
+
my $error_callback = sub {
my($cgi, $object, $fields, $opt ) = @_;
$pkgpart = $object->pkgpart;
+ &$splice_locale_fields(
+ $fields,
+ sub {
+ my $locale = shift;
+ my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
+ $part_pkg_msgcat ? $part_pkg_msgcat->pkgpartmsgnum : '';
+ },
+ 'cgiparam'
+ );
+
};
my $new_hashref_callback = sub { { 'plan' => 'flat' }; };
$pkgpart = $object->pkgpart;
+ &$splice_locale_fields(
+ $fields,
+ sub {
+ my $locale = shift;
+ my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
+ $part_pkg_msgcat ? $part_pkg_msgcat->pkgpartmsgnum : '';
+ },
+ sub {
+ my $locale = shift;
+ my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
+ $part_pkg_msgcat ? $part_pkg_msgcat->pkg : '';
+ }
+ );
+
};
my $new_callback = sub {
$options{'suspend_bill'}=1 if $conf->exists('part_pkg-default_suspend_bill');
+ &$splice_locale_fields($fields, '', '');
+
};
my $clone_callback = sub {
foreach (qw( setup_fee recur_fee disable_line_item_date_ranges ));
$recur_disabled = $object->freq ? 0 : 1;
+
+ &$splice_locale_fields(
+ $fields,
+ '',
+ sub {
+ my $locale = shift;
+ my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
+ $part_pkg_msgcat ? $part_pkg_msgcat->pkg : '';
+ }
+ );
};
my $discount_error_callback = sub {
-<& /elements/header.html, "$action Service Definition",
- menubar('View all service definitions' => "${p}browse/part_svc.cgi"),
+<& /elements/header.html, "$action Service Definition" &>
+<& /elements/menubar.html,
+ 'View all service definitions' => "${p}browse/part_svc.cgi"
#" onLoad=\"visualize()\""
&>
<& /elements/init_overlib.html &>
-<BR>
+<BR><BR>
+
+<STYLE TYPE="text/css">
+.disabled {
+ background-color: #dddddd;
+}
+.hidden {
+ display: none;
+}
+.enabled {
+ background-color: #ffffff;
+}
+.row0 TD {
+ background-color: #eeeeee;
+}
+.row1 TD {
+ background-color: #ffffff;
+}
+.def_info {
+ text-align: center;
+ padding: 0px;
+ border-top: none;
+ font-size: smaller;
+ font-style: italic;
+}
+</STYLE>
+<SCRIPT TYPE="text/javascript">
+function fixup_submit(layer) {
+ document.forms[layer].submit.disabled = true;
+ fixup(document.forms[layer]);
+ window[layer+'process'].call();
+}
+
+function flag_changed(obj) {
+ var newflag = obj.value;
+ var a = obj.name.match(/(.*)__(.*)_flag/);
+ var layer = a[1];
+ var field = a[2];
+ var input = document.getElementById(layer + '__' + field);
+ // for fields that have both 'input' and 'select', 'select' is 'select from
+ // inventory class'.
+ var select = document.getElementById(layer + '__' + field + '_select');
+ if (newflag == "" || newflag == "X") { // disable
+ if ( input ) {
+ input.disabled = true;
+ input.className = 'disabled';
+ }
+ if ( select ) {
+ select.disabled = true;
+ select.className = 'hidden';
+ }
+ } else if ( newflag == 'D' || newflag == 'F' || newflag == 'S' ) {
+ if ( input ) {
+ // enable text box, disable inventory select
+ input.disabled = false;
+ input.className = 'enabled';
+ if ( select ) {
+ select.disabled = false;
+ select.className = 'hidden';
+ }
+ } else if ( select ) {
+ // enable select
+ select.disabled = false;
+ select.className = 'enabled';
+ if ( newflag == 'S' || select.getAttribute('should_be_multiple') ) {
+ select.multiple = true;
+ } else {
+ select.multiple = false;
+ }
+ }
+ } else if ( newflag == 'M' || newflag == 'A' || newflag == 'H' ) {
+ // these all require a class selection
+ if ( select ) {
+ select.disabled = false;
+ select.className = 'enabled';
+ if ( input ) {
+ input.disabled = false;
+ input.className = 'hidden';
+ }
+ }
+ }
+}
+
+window.onload = function() {
+ var selects = document.getElementsByTagName('SELECT');
+ for(i = 0; i < selects.length; i++) {
+ var obj = selects[i];
+ if ( obj.multiple ) {
+ obj.setAttribute('should_be_multiple', true);
+ }
+ }
+ for(i = 0; i < selects.length; i++) {
+ var obj = selects[i];
+ if ( obj.name.match(/_flag$/) ) {
+ flag_changed(obj);
+ }
+ }
+};
+
+</SCRIPT>
<FORM NAME="dummy">
<BR>
-% my %vfields;
-% #code duplication w/ edit/part_svc.cgi, should move this hash to part_svc.pm
-% # and generalize the subs
-% # condition sub is tested to see whether to disable display of this choice
-% # params: ( $def, $layer, $field ) (see SUB below)
-% my $inv_sub = sub {
-% $_[0]->{disable_inventory}
-% || $_[0]->{'type'} ne 'text'
-% };
-% tie my %flag, 'Tie::IxHash',
-% '' => { 'desc' => 'No default', },
-% 'D' => { 'desc' => 'Default',
-% 'condition' =>
-% sub { $_[0]->{disable_default} },
-% },
-% 'F' => { 'desc' => 'Fixed (unchangeable)',
-% 'condition' =>
-% sub { $_[0]->{disable_fixed} },
-% },
-% 'S' => { 'desc' => 'Selectable Choice',
-% 'condition' =>
-% sub { !ref($_[0]) || $_[0]->{disable_select} },
-% },
-% 'M' => { 'desc' => 'Manual selection from inventory',
-% 'condition' => $inv_sub,
-% },
-% 'A' => { 'desc' => 'Automatically fill in from inventory',
-% 'condition' => $inv_sub,
-% },
-% 'H' => { 'desc' => 'Select from hardware class',
-% 'condition' => sub { $_[0]->{type} ne 'select-hardware' },
-% },
-% 'X' => { 'desc' => 'Excluded',
-% 'condition' =>
-% sub { ! $vfields{$_[1]}->{$_[2]} },
-%
-% },
-% ;
-%
-% my @dbs = $hashref->{svcdb}
-% ? ( $hashref->{svcdb} )
-% : FS::part_svc->svc_tables();
-%
-% my $help = '';
-% unless ( $hashref->{svcpart} ) {
-% $help = ' '.
-% include('/elements/popup_link.html',
-% 'action' => $p.'docs/part_svc-table.html',
-% 'label' => 'help',
-% 'actionlabel' => 'Service table help',
-% 'width' => 763,
-% #'height' => 400,
-% );
-% }
-%
-% tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } grep dbdef->table($_), @dbs;
-% my $widget = new HTML::Widgets::SelectLayers(
-% #'selected_layer' => $p_svcdb,
-% 'selected_layer' => $hashref->{svcdb} || 'svc_acct',
-% 'options' => \%svcdb,
-% 'form_name' => 'dummy',
-% #'form_action' => 'process/part_svc.cgi',
-% 'form_action' => 'part_svc.cgi', #self
-% 'form_elements' => [qw( svc svcpart classnum selfservice_access
-% disabled preserve
-% )],
-% 'html_between' => $help,
-% 'layer_callback' => sub {
-% my $layer = shift;
-%
-% my $html = qq!<INPUT TYPE="hidden" NAME="svcdb" VALUE="$layer">!;
-%
-% #$html .= $svcdb_info;
-%
-% my $columns = 3;
-% my $count = 0;
-% my $communigate = 0;
-% my @part_export =
-% map { qsearch( 'part_export', {exporttype => $_ } ) }
-% keys %{FS::part_export::export_info($layer)};
-% $html .= '<BR><BR>'. include('/elements/table.html') .
-% "<TR><TH COLSPAN=$columns>Exports</TH></TR><TR>";
-% foreach my $part_export ( @part_export ) {
-% $communigate++ if $part_export->exporttype =~ /^communigate/;
-% $html .= '<TD><INPUT TYPE="checkbox"'.
-% ' NAME="exportnum'. $part_export->exportnum. '" VALUE="1" ';
-% $html .= 'CHECKED'
-% if ( $clone || $part_svc->svcpart ) #null svcpart search causing error
-% && qsearchs( 'export_svc', {
-% exportnum => $part_export->exportnum,
-% svcpart => $clone || $part_svc->svcpart });
-% $html .= '>'. $part_export->label_html. '</TD>';
-% $count++;
-% $html .= '</TR><TR>' unless $count % $columns;
-% }
-% $html .= '</TR></TABLE><BR><BR>'. $mod_info;
-%
-% $html .= include('/elements/table-grid.html', 'cellpadding' => 4 ).
-% '<TR>'.
-% '<TH CLASS="grid" BGCOLOR="#cccccc">Field</TH>'.
-% '<TH CLASS="grid" BGCOLOR="#cccccc">Label</TH>'.
-% '<TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=2>Modifier</TH>'.
-% '</TR>';
-%
-% my $bgcolor1 = '#eeeeee';
-% my $bgcolor2 = '#ffffff';
-% my $bgcolor;
-%
-% #yucky kludge
-% my @fields = ();
-% if ( defined( dbdef->table($layer) ) ) {
-% @fields = grep {
-% $_ ne 'svcnum'
-% && ( $communigate || !$communigate_fields{$layer}->{$_} )
-% && ( !FS::part_svc->svc_table_fields($layer)
-% ->{$_}->{disable_part_svc_column}
-% || $part_svc->part_svc_column($_)->columnflag
-% )
-% } fields($layer);
-% }
-% push @fields, 'usergroup'
-% if $layer eq 'svc_acct'
-% or ( $layer eq 'svc_broadband' and
-% $conf->exists('svc_broadband-radius') ); # double kludge
-% # (but we do want to check the config, right?)
-% $part_svc->svcpart($clone) if $clone; #haha, undone below
-%
-%
-% foreach my $field (@fields) {
-%
-% #a few lines of false laziness w/browse/part_svc.cgi
-% my $def = FS::part_svc->svc_table_fields($layer)->{$field};
-% my $def_info = $def->{'def_info'};
-% my $formatter = $def->{'format'} || sub { shift };
-%
-% my $part_svc_column = $part_svc->part_svc_column($field);
-% my $label = $part_svc_column->columnlabel || $def->{'label'};
-% my $value = &$formatter($part_svc_column->columnvalue);
-% my $flag = $part_svc_column->columnflag;
-%
-% if ( $bgcolor eq $bgcolor1 ) {
-% $bgcolor = $bgcolor2;
-% } else {
-% $bgcolor = $bgcolor1;
-% }
-%
-% $html .= qq!<TR><TD ROWSPAN=2 CLASS="grid" BGCOLOR="$bgcolor" ALIGN="right">!.
-% ( $def->{'label'} || $field ).
-% "</TD>";
-%
-% $html .= qq!<TD ROWSPAN=2 CLASS="grid" BGCOLOR="$bgcolor"><INPUT NAME="${layer}__${field}_label" VALUE="!. encode_entities($label). '" STYLE="text-align:right"></TD>';
-%
-% $flag = '' if $def->{type} eq 'disabled';
-%
-% $html .= qq!<TD CLASS="grid" BGCOLOR="$bgcolor">!;
-%
-% if ( $def->{type} eq 'disabled' ) {
-%
-% $html .= 'No default';
-%
-% } else {
-%
-% $html .= qq!<SELECT NAME="${layer}__${field}_flag"!.
-% qq! onChange="${layer}__${field}_flag_changed(this)">!;
-%
-% foreach my $f ( keys %flag ) {
-%
-% # need to template-ize more httemplate/edit/svc_* first
-% next if $f eq 'M' and $layer !~ /^svc_(broadband|external|phone|dish)$/;
-%
-% #here is where the SUB from above is called, to skip some choices
-% next if $flag{$f}->{condition}
-% && &{ $flag{$f}->{condition} }( $def, $layer, $field );
-%
-% $html .= qq!<OPTION VALUE="$f"!.
-% ' SELECTED'x($flag eq $f ).
-% '>'. $flag{$f}->{desc};
-%
-% }
-%
-% $html .= '</SELECT>';
-%
-% $html .= join("\n",
-% '<SCRIPT>',
-% " function ${layer}__${field}_flag_changed(what) {",
-% ' var f = what.options[what.selectedIndex].value;',
-% ' if ( f == "" || f == "X" ) { //disable',
-% " what.form.${layer}__${field}.disabled = true;".
-% " what.form.${layer}__${field}.style.backgroundColor = '#dddddd';".
-% " if ( what.form.${layer}__${field}_classnum ) {".
-% " what.form.${layer}__${field}_classnum.disabled = true;".
-% " what.form.${layer}__${field}_classnum.style.backgroundColor = '#dddddd';".
-% " }".
-% ' } else if ( f == "D" || f == "F" || f =="S" ) { //enable, text box',
-% " what.form.${layer}__${field}.disabled = false;".
-% " what.form.${layer}__${field}.style.backgroundColor = '#ffffff';".
-% " if ( f == 'S' || '${field}' == 'usergroup' ) {". # kludge
-% " what.form.${layer}__${field}.multiple = true;".
-% " } else {".
-% " what.form.${layer}__${field}.multiple = false;".
-% " }".
-% " what.form.${layer}__${field}.style.display = '';".
-% " if ( what.form.${layer}__${field}_classnum ) {".
-% " what.form.${layer}__${field}_classnum.disabled = false;".
-% " what.form.${layer}__${field}_classnum.style.backgroundColor = '#ffffff';".
-% " what.form.${layer}__${field}_classnum.style.display = 'none';".
-% " }".
-% ' } else if ( f == "M" || f == "A" || f == "H" ) { '.
-% '//enable, inventory',
-% " what.form.${layer}__${field}.disabled = false;".
-% " what.form.${layer}__${field}.style.backgroundColor = '#ffffff';".
-% " what.form.${layer}__${field}.style.display = 'none';".
-% " if ( what.form.${layer}__${field}_classnum ) {".
-% " what.form.${layer}__${field}_classnum.disabled = false;".
-% " what.form.${layer}__${field}_classnum.style.backgroundColor = '#ffffff';".
-% " what.form.${layer}__${field}_classnum.style.display = '';".
-% " }".
-% ' }',
-% ' }',
-% '</SCRIPT>',
-% );
-%
-% }
-%
-% $html .= qq!</TD><TD CLASS="grid" BGCOLOR="$bgcolor">!;
-%
-% my $disabled = $flag ? ''
-% : 'DISABLED STYLE="background-color: #dddddd"';
-% my $nodisplay = ' STYLE="display:none"';
-%
-% if ( !$def->{type} || $def->{type} eq 'text' ) {
-%
-% my $is_inv = ( $flag =~ /^[MA]$/ );
-%
-% $html .=
-% qq!<INPUT TYPE="text" NAME="${layer}__${field}" VALUE="$value" !.
-% $disabled.
-% ( $is_inv ? $nodisplay : $disabled ).
-% '>';
-%
-% $html .= include('/elements/select-table.html',
-% 'element_name' => "${layer}__${field}_classnum",
-% 'id' => "${layer}__${field}_classnum",
-% 'element_etc' => ( $is_inv
-% ? $disabled
-% : $nodisplay
-% ),
-% 'table' => 'inventory_class',
-% 'name_col' => 'classname',
-% 'value' => $value,
-% 'empty_label' => 'Select inventory class',
-% );
-%
-% } elsif ( $def->{type} eq 'checkbox' ) {
-%
-% $html .= include('/elements/checkbox.html',
-% 'field' => $layer.'__'.$field,
-% 'curr_value' => $value,
-% 'value' => 'Y',
-% );
-%
-% } elsif ( $def->{type} eq 'select' ) {
-%
-% $html .= qq!<SELECT NAME="${layer}__${field}" $disabled!;
-% $html .= ' MULTIPLE' if $flag eq 'S';
-% $html .= '>';
-% $html .= '<OPTION> </OPTION>' unless $value;
-% if ( $def->{select_table} ) {
-% foreach my $record ( qsearch( $def->{select_table}, {} ) ) {
-% my $rvalue = $record->getfield($def->{select_key});
-% my $select_label = $def->{select_label};
-% $html .= qq!<OPTION VALUE="$rvalue"!.
-% (grep(/^$rvalue$/, split(',',$value)) ? ' SELECTED>' : '>' ).
-% $record->$select_label(). '</OPTION>';
-% } #next $record
-% } elsif ( $def->{select_list} ) {
-% foreach my $item ( @{$def->{select_list}} ) {
-% $html .= qq!<OPTION VALUE="$item"!.
-% (grep(/^$item$/, split(',',$value)) ? ' SELECTED>' : '>' ).
-% $item. '</OPTION>';
-% } #next $item
-% } elsif ( $def->{select_hash} ) {
-% if ( ref($def->{select_hash}) eq 'ARRAY' ) {
-% tie my %hash, 'Tie::IxHash', @{ $def->{select_hash} };
-% $def->{select_hash} = \%hash;
-% }
-% foreach my $key ( keys %{$def->{select_hash}} ) {
-% $html .= qq!<OPTION VALUE="$key"!.
-% (grep(/^$key$/, split(',',$value)) ? ' SELECTED>' : '>' ).
-% $def->{select_hash}{$key}. '</OPTION>';
-% } #next $key
-% } #endif
-% $html .= '</SELECT>';
-%
-% } elsif ( $def->{type} eq 'textarea' ) {
-%
-% $html .=
-% qq!<TEXTAREA NAME="${layer}__${field}">!. encode_entities($value).
-% '</TEXTAREA>';
-%
-% } elsif ( $def->{type} =~ /select-(.*?).html/ ) {
-%
-% $html .= include("/elements/".$def->{type},
-% 'curr_value' => $value,
-% 'element_name' => "${layer}__${field}",
-% 'element_etc' => $disabled,
-% 'multiple' => ($def->{multiple} ||
-% $flag eq 'S'),
-% # allow the table def to force 'multiple'
-% );
-%
-% } elsif ( $def->{type} eq 'communigate_pro-accessmodes' ) {
-%
-% $html .= include('/elements/communigate_pro-accessmodes.html',
-% 'element_name_prefix' => "${layer}__${field}_",
-% 'curr_value' => $value,
-% #doesn't work#'element_etc' => $disabled,
-% );
-%
-% } elsif ( $def->{type} eq 'select-hardware' ) {
-%
-% $html .= qq!<INPUT TYPE="text" NAME="${layer}__${field}" $disabled>!;
-% $html .= include('/elements/select-hardware_class.html',
-% 'curr_value' => $value,
-% 'element_name' => "${layer}__${field}_classnum",
-% 'id' => "${layer}__${field}_classnum",
-% 'element_etc' => $flag ne 'H' && $nodisplay,
-% 'empty_label' => 'Select hardware class',
-% );
-%
-% } elsif ( $def->{type} eq 'disabled' ) {
-%
-% $html .=
-% qq!<INPUT TYPE="hidden" NAME="${layer}__${field}" VALUE="">!;
-%
-% } else {
-%
-% $html .= '<font color="#ff0000">unknown type '. $def->{type};
-%
-% }
-%
-% $html .= "</TD></TR>\n";
-
-% $def_info = "($def_info)" if $def_info;
-% $html .=
-% qq!<TR>!.
-% qq! <TD COLSPAN=2 BGCOLOR="$bgcolor" ALIGN="center" !.
-% qq! STYLE="padding:0; border-top: none">!.
-% qq! <FONT SIZE="-1"><I>$def_info</I></FONT>!.
-% qq! </TD>!.
-% qq!</TR>\n!;
-%
-% } #foreach my $field (@fields) {
-%
-% $part_svc->svcpart('') if $clone; #undone
-% $html .= "</TABLE>";
-%
-% $html .= include('/elements/progress-init.html',
-% $layer, #form name
-% [ qw(svc svcpart classnum selfservice_access
-% disabled preserve
-% exportnum),
-% @fields ],
-% 'process/part_svc.cgi',
-% $p.'browse/part_svc.cgi',
-% $layer,
-% );
-% $html .= '<BR><INPUT NAME="submit" TYPE="button" VALUE="'.
-% ($hashref->{svcpart} ? 'Apply changes' : 'Add service'). '" '.
-% ' onClick="document.'. "$layer.submit.disabled=true; ".
-% "fixup(document.$layer); $layer". 'process();">';
-%
-% #$html .= '<BR><INPUT TYPE="submit" VALUE="'.
-% # ($hashref->{svcpart} ? 'Apply changes' : 'Add service'). '">';
-%
-% $html;
-%
-% },
-% );
-
<BR>
Table <% $widget->html %>
my $hashref = $part_svc->hashref;
# my $p_svcdb = $part_svc->svcdb || 'svc_acct';
-my %communigate_fields = (
- 'svc_acct' => { map { $_=>1 }
- qw( file_quota file_maxnum file_maxsize
- password_selfchange password_recover
- ),
- grep /^cgp_/, fields('svc_acct')
- },
- 'svc_domain' => { map { $_=>1 }
- qw( max_accounts trailer parent_svcnum ),
- grep /^(cgp|acct_def)_/, fields('svc_domain')
- },
- #'svc_forward' => { map { $_=>1 } qw( ) },
- #'svc_mailinglist' => { map { $_=>1 } qw( ) },
- #'svc_cert' => { map { $_=>1 } qw( ) },
-);
-my $mod_info = '
-For the selected table, you can give fields default or fixed (unchangable)
-values, or select an inventory class to manually or automatically fill in
-that field.
-';
+my @dbs = $hashref->{svcdb}
+ ? ( $hashref->{svcdb} )
+ : FS::part_svc->svc_tables();
+
+my $help = '';
+unless ( $hashref->{svcpart} ) {
+ $help = ' '.
+ include('/elements/popup_link.html',
+ 'action' => $p.'docs/part_svc-table.html',
+ 'label' => 'help',
+ 'actionlabel' => 'Service table help',
+ 'width' => 763,
+ #'height' => 400,
+ );
+}
+tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } grep dbdef->table($_), @dbs;
+my $widget = new HTML::Widgets::SelectLayers(
+ #'selected_layer' => $p_svcdb,
+ 'selected_layer' => $hashref->{svcdb} || 'svc_acct',
+ 'options' => \%svcdb,
+ 'form_name' => 'dummy',
+ #'form_action' => 'process/part_svc.cgi',
+ 'form_action' => 'part_svc.cgi', #self
+ 'form_elements' => [qw( svc svcpart classnum selfservice_access
+ disabled preserve
+ )],
+ 'html_between' => $help,
+ 'layer_callback' => sub {
+ include('elements/part_svc_column.html',
+ shift,
+ 'part_svc' => $part_svc,
+ 'clone' => $clone
+ )
+ }
+);
</%init>
{ field=>'by_default', type=>'checkbox', value=>'Y' },
$tagcolor,
],
- 'labels' => { 'tagnum' => 'Tag #',
+ 'labels' => { 'tagnum' => 'Tag',
'tagname' => 'Tag',
'tagdesc' => 'Message',
'tagcolor' => 'Highlight Color',
<SCRIPT TYPE="text/javascript">
- var modulesForNamespace = <% to_json(\%modules_for_namespace, {canonical=>1}) %>;
+ var modulesForNamespace = <% encode_json(\%modules_for_namespace, {canonical=>1}) %>;
function changeNamespace(what) {
var ns = what.value;
var select_module = document.getElementById('gateway_module');
%>
<%init>
-my @deviceparts_with_inventory;
-my @part_device = qsearch('part_device', {} );
-foreach my $part_device ( @part_device ) {
- push @deviceparts_with_inventory, $part_device->devicepart
- if $part_device->inventory_classnum;
-}
+my @deviceparts_with_inventory =
+ map $_->devicepart,
+ qsearch({ 'table' => 'part_device',
+ 'extra_sql' => 'WHERE inventory_classnum IS NOT NULL',
+ });
my $html_foot = sub {
my $js = "
var devicepart = what.options[what.selectedIndex].value;
- var deviceparts_with_inventory = new Array(\"";
-$js .= join("\",\"",@deviceparts_with_inventory);
-$js .= "\");
+ var deviceparts_with_inventory = new Array(";
+$js .= join(',', map qq("$_"), @deviceparts_with_inventory);
+$js .= ");
var hasInventory = false;
for ( i = 0; i < deviceparts_with_inventory.length; i++ ) {
my $pkgnum = $cgi->param('pkgnum') or die;
my $old = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
my %hash = $old->hash;
-$hash{$_}= $cgi->param($_) ? parse_datetime($cgi->param($_)) : ''
- foreach qw( start_date setup bill last_bill contract_end );
+foreach ( qw( start_date setup bill last_bill contract_end ) ) {
+ if ( $cgi->param($_) =~ /^(\d+)$/ ) {
+ $hash{$_} = $1;
+ } else {
+ $hash{$_} = '';
+ }
# adjourn, expire, resume not editable this way
-
-my @errors = ();
-
-push @errors, '_bill_areyousure'
- if $hash{'bill'} != $old->bill # if the next bill date was changed
- && $hash{'bill'} < time # to a date in the past
- && ! $cgi->param('bill_areyousure'); # and it wasn't confirmed
-
-push @errors, '_setup_areyousure'
- if ! $hash{'setup'} && $old->setup # if the setup date was removed
- && ! $cgi->param('setup_areyousure'); # and it wasn't confirmed
-
-push @errors, '_setupadd_areyousure'
- if $hash{'setup'} && ! $old->setup # if the setup date was added
- && ! $cgi->param('setupadd_areyousure'); # and it wasn't confirmed
-
-push @errors, '_start'
- if $hash{'start_date'} && !$old->start_date # if a start date was added
- && $hash{'setup'}; # but there's a setup date
+}
my $new;
my $error;
-if ( @errors ) {
- $error = join(',', @errors);
-} else {
- $new = new FS::cust_pkg \%hash;
- $error = $new->replace($old);
+$new = new FS::cust_pkg \%hash;
+$error = $new->replace($old);
+
+if (!$error) {
+ my @supp_pkgs = $old->supplemental_pkgs;
+ foreach $new (@supp_pkgs) {
+ foreach ( qw( start_date setup contract_end ) ) {
+ # propagate these to supplementals
+ $new->set($_, $hash{$_});
+ }
+ if ( $hash{'bill'} ne $old->get('bill') ) {
+ if ( $hash{'bill'} and $old->get('bill') ) {
+ # adjust by the same interval
+ my $diff = $hash{'bill'} - $old->get('bill');
+ $new->set('bill', $new->get('bill') + $diff);
+ } else {
+ # absolute date
+ $new->set('bill', $hash{'bill'});
+ }
+ }
+ $error = $new->replace;
+ $error .= ' (supplemental package '.$new->pkgnum.')' if $error;
+ last if $error;
+ }
}
</%init>
--- /dev/null
+% if ( $error ) {
+% $cgi->param('error', $error);
+<% $cgi->redirect(popurl(3).'/edit/bulk-part_pkg.cgi?', $cgi->query_string) %>
+% } else {
+<% $cgi->redirect(popurl(3).'/browse/part_pkg.cgi') %>
+% }
+<%init>
+die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Bulk edit package definitions');
+
+my @pkgparts = $cgi->param('pkgpart')
+ or die "no package definitions selected";
+
+my %changes;
+foreach my $param (grep { /^report_option_\d+$/ } $cgi->param) {
+ if ( length($cgi->param($param)) ) {
+ if ( $cgi->param($param) == 1 ) {
+ $changes{$param} = 1;
+ } else {
+ $changes{$param} = '';
+ }
+ }
+}
+
+my $error;
+foreach my $pkgpart (@pkgparts) {
+ my $part_pkg = FS::part_pkg->by_key($pkgpart);
+ my %options = ( $part_pkg->options, %changes );
+ $error ||= $part_pkg->replace( options => \%options );
+}
+</%init>
$change{'keep_dates'} = 1;
if ( $cgi->param('locationnum') == -1 ) {
- my $cust_location = new FS::cust_location {
+ my $cust_location = FS::cust_location->new_or_existing({
'custnum' => $cust_pkg->custnum,
map { $_ => scalar($cgi->param($_)) }
qw( address1 address2 city county state zip country )
- };
+ });
$change{'cust_location'} = $cust_location;
}
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Post credit');
+ unless $FS::CurrentUser::CurrentUser->access_right('Credit line items');
my @billpkgnum_setuprecurs =
map { $_ =~ /^billpkgnum(\d+\-\w*)$/ or die 'gm#23'; $1; }
});
die "unknown locationnum $locationnum" unless $cust_location;
-my $new = FS::cust_location->new({
+my $new = FS::cust_location->new_or_existing({
custnum => $cust_location->custnum,
prospectnum => $cust_location->prospectnum,
- map { $_ => scalar($cgi->param($_)) }
- qw( address1 address2 city county state zip country )
+ map { $_ => scalar($cgi->param($_)) } FS::cust_main->location_fields
});
my $error = $cust_location->move_to($new);
</%once>
<%init>
-die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Edit customer');
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied" unless $curuser->access_right('Edit customer');
my $conf = new FS::Conf;
$cgi->param('duplicate_of_custnum') =~ /^(\d+)$/;
my $duplicate_of = $1;
+# if this is enabled, enforce it
+if ( $conf->exists('agent-ship_address', $cgi->param('agentnum')) ) {
+ my $agent = FS::agent->by_key($cgi->param('agentnum'));
+ my $agent_cust_main = $agent->agent_cust_main;
+ if ( $agent_cust_main ) {
+ my $agent_location = $agent_cust_main->ship_location;
+ foreach (qw(address1 city state zip country latitude longitude district)) {
+ $cgi->param("ship_$_", $agent_location->get($_));
+ }
+ }
+}
+
my %locations;
for my $pre (qw(bill ship)) {
}
$hash{'custnum'} = $cgi->param('custnum');
warn Dumper \%hash if $DEBUG;
- # if we can qsearchs it, then it's unchanged, so use that
- $locations{$pre} = qsearchs('cust_location', \%hash)
- || FS::cust_location->new( \%hash );
-
+ $locations{$pre} = FS::cust_location->new_or_existing(\%hash);
}
if ( ($cgi->param('same') || '') eq 'Y' ) {
$new->setfield('paid', $cgi->param('paid') )
if $cgi->param('paid');
-my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups');
-my @tax_exempt = grep { $cgi->param("tax_$_") eq 'Y' } @exempt_groups;
-my %tax_exempt = map { $_ => scalar($cgi->param("tax_$_".'_num')) } @tax_exempt;
+my %options = ();
+if ( $curuser->access_right('Edit customer tax exemptions') ) {
+ my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups');
+ my @tax_exempt = grep { $cgi->param("tax_$_") eq 'Y' } @exempt_groups;
+ $options{'tax_exemption'} = {
+ map { $_ => scalar($cgi->param("tax_$_".'_num')) } @tax_exempt
+ };
+}
#perhaps this stuff should go to cust_main.pm
if ( $new->custnum eq '' or $duplicate_of ) {
else {
# create the customer
$error ||= $new->insert( \%hash, \@invoicing_list,
- 'tax_exemption'=> \%tax_exempt,
- 'prospectnum' => scalar($cgi->param('prospectnum')),
+ %options,
+ prospectnum => scalar($cgi->param('prospectnum')),
);
my $conf = new FS::Conf;
warn Dumper({ new => $new, old => $old }) if $DEBUG;
$error ||= $new->replace( $old, \@invoicing_list,
- 'tax_exemption' => \%tax_exempt,
+ %options,
);
warn "$me returned from replace" if $DEBUG;
--- /dev/null
+% if ($error) {
+% $cgi->param('error', $error);
+% $cgi->redirect(popurl(3). 'edit/cust_pkg_quantity.html?'. $cgi->query_string );
+% } else {
+
+ <& /elements/header-popup.html, "Quantity changed" &>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+ </BODY>
+ </HTML>
+
+% }
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('Change customer package');
+
+my $cust_pkg = qsearchs({
+ 'table' => 'cust_pkg',
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => { 'pkgnum' => scalar($cgi->param('pkgnum')), },
+ 'extra_sql' => ' AND '. $curuser->agentnums_sql,
+});
+die 'unknown pkgnum' unless $cust_pkg;
+
+$cgi->param('quantity') =~ /^(\d+)$/;
+my $quantity = $1;
+my $error = $cust_pkg->set_quantity($1);
+
+</%init>
%}
<%init>
-die 'access deined'
+die 'access denied'
unless $FS::CurrentUser::CurrentUser->access_right('Change customer service');
my $svcnum = $cgi->param('svcnum');
if ( !$error ) {
if ( $old_pkey ) {
+
+ &{ $opt{'edit_callback'} }( $new, $old ) if $opt{'edit_callback'};
+
$error = $new->replace($old, @args);
} else {
$error = $new->insert(@args);
my $table = $opt{'table'};
$opt{'fields'} ||= [ fields($table) ];
push @{ $opt{'fields'} }, qw( pkgnum svcpart );
+foreach (fields($table)) {
+ if ( $cgi->param($_.'_classnum') ) {
+ push @{ $opt{'fields'} }, $_.'_classnum';
+ }
+}
</%init>
if ( $cgi->param('svc_machine') eq 'Y' ) {
$new->machine('_SVC_MACHINE');
$new->part_export_machine_textarea( $cgi->param('part_export_machine') );
+ $new->default_machine_name( $cgi->param('default_machine_name') );
}
my $error;
'precheck_callback' => $precheck_callback,
'args_callback' => $args_callback,
'process_m2m' => \@process_m2m,
+ 'process_o2m' => \@process_o2m,
)
%>
<%init>
grep /^svc_dst_pkgpart/, $cgi->param
],
},
+ { 'link_table' => 'part_pkg_link',
+ 'target_table' => 'part_pkg',
+ 'base_field' => 'src_pkgpart',
+ 'target_field' => 'dst_pkgpart',
+ 'hashref' => { 'link_type' => 'supp', 'hidden' => '' },
+ 'params' => [ map $cgi->param($_),
+ grep /^supp_dst_pkgpart/, $cgi->param
+ ],
+ },
map {
my $hidden = $_;
{ 'link_table' => 'part_pkg_link',
};
}
+my @process_o2m = (
+ {
+ 'table' => 'part_pkg_msgcat',
+ 'fields' => [qw( locale pkg )],
+ },
+);
+
</%init>
--- /dev/null
+% if ( $is_error ) {
+% $cgi->param('error' => \%part_pkg_usage);
+% # internal redirect, because it's a lot of state to pass through
+<& /browse/part_pkg_usage.html &>
+% } else {
+% # uh, not quite sure...
+<% $cgi->redirect($fsurl.'browse/part_pkg.cgi') %>
+% }
+<%init>
+my %vars = $cgi->Vars;
+my %part_pkg_usage;
+my $is_error;
+foreach my $pkgpart ($cgi->param('pkgpart')) {
+ next unless $pkgpart =~ /^\d+$/;
+ my $part_pkg = FS::part_pkg->by_key($pkgpart)
+ or die "unknown pkgpart $pkgpart";
+ my %old = map { $_->pkgusagepart => $_ } $part_pkg->part_pkg_usage;
+ $part_pkg_usage{$pkgpart} ||= [];
+ my @rows;
+ foreach (grep /^pkgpart$pkgpart/, keys %vars) {
+ /^pkgpart\d+_(\w+\D)(\d+)$/ or die "misspelled field name '$_'";
+ my $value = delete $vars{$_};
+ my $field = $1;
+ my $row = $2;
+ $rows[$row] ||= {};
+ $rows[$row]->{$field} = $value;
+ }
+
+ foreach my $row (@rows) {
+ next if !defined($row);
+ my $error;
+ my %classes;
+ foreach my $class (grep /^class/, keys %$row) {
+ $class =~ /^class(\d+)_$/;
+ my $classnum = $1;
+ $classes{$classnum} = delete $row->{$class};
+ }
+ my $usage = FS::part_pkg_usage->new($row);
+ $usage->set('pkgpart', $pkgpart);
+ if ( $usage->pkgusagepart and $row->{minutes} > 0 ) {
+ $error = $usage->replace(\%classes);
+ # and don't delete the existing one
+ delete($old{$usage->pkgusagepart});
+ } elsif ( $row->{minutes} > 0 ) {
+ $error = $usage->insert(\%classes);
+ } else {
+ next;
+ }
+ if ( $error ) {
+ $usage->set('error', $error);
+ $is_error = 1;
+ }
+ push @{ $part_pkg_usage{$pkgpart} }, $usage;
+ }
+
+ foreach my $usage (values %old) {
+ # all of these were not sent back by the client, so delete them
+ my $error = $usage->delete;
+ if ( $error ) {
+ $usage->set('error', $error);
+ $is_error = 1;
+ unshift @{ $part_pkg_usage{$pkgpart} }, $usage;
+ }
+ }
+
+}
+</%init>
$cgi->param('refnum') =~ /^(\d*)$/
or die 'illegal refnum '. $cgi->param('refnum');
my $refnum = $1;
+$cgi->param('contactnum') =~ /^(\-?\d*)$/
+ or die 'illegal contactnum '. $cgi->param('contactnum');
+my $contactnum = $1;
$cgi->param('locationnum') =~ /^(\-?\d*)$/
or die 'illegal locationnum '. $cgi->param('locationnum');
my $locationnum = $1;
: ''
),
'refnum' => $refnum,
+ 'contactnum' => $contactnum,
'locationnum' => $locationnum,
'discountnum' => $discountnum,
#for the create a new discount case
my %opt = ( 'cust_pkg' => $cust_pkg );
+ if ( $contactnum == -1 ) {
+ my $contact = FS::contact->new({
+ 'custnum' => scalar($cgi->param('custnum')),
+ map { $_ => scalar($cgi->param("contactnum_$_")) } qw( first last )
+ });
+ $opt{'contact'} = $contact;
+ }
+
if ( $locationnum == -1 ) {
- my $cust_location = new FS::cust_location {
+ my $cust_location = FS::cust_location->new_or_existing({
map { $_ => scalar($cgi->param($_)) }
- qw( custnum address1 address2 city county state zip country geocode )
- };
+ ('custnum', FS::cust_main->location_fields)
+ });
$opt{'cust_location'} = $cust_location;
}
'table' => 'svc_phone',
'args_callback' => $args_callback,
'value_callback' => $value_callback,
+ 'edit_callback' => $edit_callback,
%opt,
&>
<%init>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right($right);
+$cgi->param('phonenum', $cgi->param('phonenum_manual') )
+ if $cgi->param('phonenum_which') eq 'phonenum_manual';
+
my $tollfreephonenum = $cgi->param('tollfreephonenum');
$cgi->param('phonenum',$tollfreephonenum) if $tollfreephonenum =~ /^\d+$/;
my %opt = ();
if ( $cgi->param('locationnum') == -1 ) {
- my $cust_location = new FS::cust_location {
+ my $cust_location = FS::cust_location->new_or_existing({
map { $_ => scalar($cgi->param($_)) }
qw( custnum address1 address2 city county state zip country )
- };
+ });
$opt{'cust_location'} = $cust_location;
}
};
my $value_callback = sub {
- my ($field, $value) = @_;
- ($field =~ /_date$/) ? parse_datetime($value) : $value;
+ my ($field, $value) = @_;
+ ($field =~ /_date$/) ? parse_datetime($value) : $value;
+};
+
+my $edit_callback = sub {
+ my( $new, $old ) = @_;
+ $new->sip_password( $old->sip_password ) if $new->sip_password eq '*HIDDEN*';
};
</%init>
<% mt('with terms') |h %>
<& /elements/select-terms.html,
'curr_value' => scalar($cgi->param('invoice_terms')),
- 'empty_value' => $default_terms,
'disabled' => ( $cgi->param('bill_now') ? 0 : 1 ),
&>
</TD>
</TD>
</TR>
+ <& /elements/tr-checkbox.html,
+ label => 'Exact match',
+ field => 'exact_match',
+ cell_style => 'font-weight: bold',
+ value => 'Y',
+ curr_value => $rate_region->exact_match
+ &>
+
</TABLE>
<BR>
<BR>
% }
-<SCRIPT TYPE="text/javascript">
-function randomPass() {
- var i=0;
- var pw_set='<% join('', 'a'..'z', 'A'..'Z', '0'..'9' ) %>';
- var pass='';
- while(i < 8) {
- i++;
- pass += pw_set.charAt(Math.floor(Math.random() * pw_set.length));
- }
- document.OneTrueForm.clear_password.value = pass;
-}
-</SCRIPT>
-
<FORM NAME="OneTrueForm" ACTION="<% $p1 %>process/svc_acct.cgi" METHOD=POST>
<INPUT TYPE="hidden" NAME="svcnum" VALUE="<% $svcnum %>">
<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
%if ( $part_svc->part_svc_column('_password')->columnflag ne 'F' ) {
<TR>
+% #XXX eventually should require "Edit Password" ACL
<TD ALIGN="right"><% mt('Password') |h %></TD>
<TD>
- <INPUT TYPE="text" NAME="clear_password" VALUE="<% $password %>" SIZE=<% $pmax2 %> MAXLENGTH=<% $pmax %>>
- <INPUT TYPE="button" VALUE="<% mt('Generate') |h %>" onclick="randomPass();">
+ <INPUT TYPE="text" ID="clear_password" NAME="clear_password" VALUE="<% $password %>" SIZE=<% $pmax2 %> MAXLENGTH=<% $pmax %>>
+ <& /elements/random_pass.html, 'clear_password' &>
</TD>
</TR>
%}else{
{ field=>'sectornum', type=>'select-tower_sector', },
{ field=>'routernum', type=>'select-router_block_ip' },
{ field=>'mac_addr' , type=>'input-mac_addr' },
- qw( latitude longitude altitude vlan_profile
- performance_profile authkey plan_id )
+ qw(
+ latitude longitude altitude
+ radio_serialnum radio_location poe_location rssi suid
+ ),
+ { field=>'shared_svcnum', type=>'search-svc_broadband', },
+ qw( vlan_profile performance_profile authkey plan_id ),
);
if ( $conf->exists('svc_broadband-radius') ) {
my( $cgi, $svc_x, $part_svc, $cust_pkg, $fields, $opt ) = @_;
$svc_x->locationnum($cust_pkg->locationnum) if $cust_pkg;
},
+ 'svc_edit_callback' => sub {
+ my( $cgi, $svc_x, $part_svc, $cust_pkg, $fields, $opt) = @_;
+ my $conf = new FS::Conf;
+ $svc_x->sip_password('*HIDDEN*') unless $conf->exists('showpasswords');
+ },
&>
<%init>
type => 'select-did',
label => 'Phone number',
multiple => $bulk,
+ },
+ { field => 'sim_imsi',
+ type => 'text',
+ size => 15,
+ maxlength => 15,
};
push @$fields, { field => 'domsvc',
var <%$pre%>set_rownum;
var <%$pre%>addRow;
var <%$pre%>deleteRow;
-var <%$pre%>fieldorder = <% to_json($fieldorder) %>;
+var <%$pre%>fieldorder = <% encode_json($fieldorder) %>;
function <%$pre%>possiblyAddRow_factory(obj) {
var callback = obj.onchange;
if ( obj.id ) {
obj.id = obj.id + rownum;
}
- if ( obj.name ) {
- obj.name = obj.name + rownum;
+ if ( obj.getAttribute('name') ) {
+ obj.setAttribute('name', obj.getAttribute('name') + rownum);
// also, in this case it's a form field that will be part of the record
// so set up an onchange handler
obj.onchange = <%$pre%>possiblyAddRow_factory(obj);
<%$pre%>set_rownum(row, this_rownum);
if(data instanceof Array) {
for (i = 0; i < data.length && i < <%$pre%>fieldorder.length; i++) {
- var el = document.getElementsByName(<%$pre%>fieldorder[i] + this_rownum)[0];
+ var el = document.getElementsByName(<%$pre |js_string%> +
+ <%$pre%>fieldorder[i] +
+ this_rownum)[0];
if (el) {
- el.value = data[i];
+ if ( el.tagName.toLowerCase() == 'span' ) {
+ el.innerHTML = data[i];
+ } else if ( el.type == 'checkbox' ) {
+ el.checked = (el.value == data[i]);
+ } else {
+ el.value = data[i];
+ }
}
}
} else if (data instanceof Object) {
for (var field in data) {
- var el = document.getElementsByName(field + this_rownum)[0];
+ var el = document.getElementsByName(<%$pre |js_string%> +
+ field +
+ this_rownum)[0];
if (el) {
- el.value = data[field];
-% # doesn't work for checkbox
+ if ( el.tagName.toLowerCase() == 'span' ) {
+ el.innerHTML = data[field];
+ } else if ( el.type == 'checkbox' ) {
+ el.checked = (el.value == data[field]);
+ } else {
+ el.value = data[field];
+ }
}
}
} // else nothing
<%$pre%>tbody.removeChild(r);
}
+function <%$pre%>set_prefix(obj) {
+ if ( obj.id ) {
+ obj.id = <%$pre |js_string%> + obj.id;
+ }
+ if ( obj.getAttribute('name') ) {
+ obj.setAttribute('name', <%$pre |js_string%> + obj.getAttribute('name'));
+ }
+ for (var i = 0; i < obj.children.length; i++) {
+ if ( obj.children[i] instanceof Node ) {
+ <%$pre%>set_prefix(obj.children[i]);
+ }
+ }
+}
+
function <%$pre%>init() {
<%$pre%>template = document.getElementById(<% $template_row |js_string%>);
<%$pre%>tbody = document.getElementById('<%$pre%>autotable');
var table = <%$pre%>template.parentNode;
table.removeChild(<%$pre%>template);
// give it an id
- <%$pre%>template.id = <%$pre |js_string%> + 'row';
- // and a magic identifier so we know it's been submitted
+ <%$pre%>template.id = 'row';
+ // prefix the ids and names of the TR object and all its descendants
+ <%$pre%>set_prefix(<%$pre%>template);
+ // add a magic identifier so we know it's been submitted
var magic = document.createElement('INPUT');
magic.setAttribute('type', 'hidden');
magic.setAttribute('name', '<%$pre%>magic');
// and a delete button
%# should this be enclosed in an actual <button> for aesthetics?
var delete_button = document.createElement('IMG');
- delete_button.id = 'delete_button';
+ delete_button.id = '<%$pre%>delete_button';
delete_button.src = '<%$fsurl%>images/cross.png';
delete_button.alt = 'X';
// use an inline string for this so that it will be cloned properly
delete_button.setAttribute('onclick', "<%$pre%>deleteRow(this.rownum);");
+ // and an error display
+ var error_span = document.createElement('SPAN');
+ error_span.className = 'error';
+ error_span.style.color = '#FF0000';
+ error_span.setAttribute('name', '<%$pre%>error');
+ error_span.style.padding = '5px';
var delete_cell = document.createElement('TD');
+ delete_cell.style.textAlign = 'left';
delete_cell.appendChild(delete_button);
delete_cell.appendChild(magic); // it has to go somewhere
+ delete_cell.appendChild(error_span);
<%$pre%>template.appendChild(delete_cell);
// preload rows
- var rows = <% to_json(\@rows) %>;
+ var rows = <% encode_json(\@rows) %>;
for (var i = 0; i < rows.length; i++) {
<%$pre%>addRow(rows[i]);
}
<TH CLASS="grid" BGCOLOR="#cccccc">Description</TH>
</TR>
-% foreach my $item ( sort { $a->history_date <=> $b->history_date
-% #|| table order
-% || $a->historynum <=> $b->historynum
-% }
-% @history
-% )
-% {
+% foreach my $item ( @history ) {
% my $history_other = '';
% my $act = $item->history_action;
% if ( $act =~ /^replace/ ) {
if $conf->exists('cust_pkg-display_times')
|| $curuser->option('cust_pkg-display_times');
+@history = sort { $a->history_date <=> $b->history_date
+ || $a->historynum <=> $b->historynum } @history;
+
+if ( $curuser->option('history_order') eq 'newest' ) {
+ @history = reverse @history;
+}
+
</%init>
--- /dev/null
+<STYLE>
+.passwordbox {
+ border: 1px solid #7e0079;
+ padding: 2px;
+ position: absolute;
+ font-size: 80%;
+ background-color: #ffffff;
+ display: none;
+}
+</STYLE>
+<A ID="<%$pre%>link" HREF="#" onclick="<%$pre%>toggle(true)">(<% mt('change') %>)</A>
+<DIV ID="<%$pre%>form" CLASS="passwordbox">
+ <FORM METHOD="POST" ACTION="<%$fsurl%>misc/process/change-password.html">
+ <INPUT TYPE="hidden" NAME="svcnum" VALUE="<% $svc_acct->svcnum |h%>">
+ <INPUT TYPE="text" ID="<%$pre%>password" NAME="password" VALUE="<% $curr_value |h%>">
+ <& /elements/random_pass.html, $pre.'password', 'randomize' &>
+ <INPUT TYPE="submit" VALUE="change">
+ <INPUT TYPE="button" VALUE="cancel" onclick="<%$pre%>toggle(false)">
+% if ( $error ) {
+ <BR><SPAN STYLE="color: #ff0000"><% $error |h %></SPAN>
+% }
+ </FORM>
+</DIV>
+<SCRIPT TYPE="text/javascript">
+function <%$pre%>toggle(val) {
+ document.getElementById('<%$pre%>form').style.display =
+ val ? 'inline-block' : 'none';
+ document.getElementById('<%$pre%>link').style.display =
+ val ? 'none' : 'inline';
+}
+% if ( $error ) {
+<%$pre%>toggle(true);
+% }
+</SCRIPT>
+<%init>
+my %opt = @_;
+my $svc_acct = $opt{'svc_acct'};
+my $curr_value = $opt{'curr_value'} || '';
+my $pre = 'changepw'.$svc_acct->svcnum.'_';
+my $error = $cgi->param($pre.'error');
+</%init>
--- /dev/null
+<%doc>
+A tristate checkbox (with three values: true, false, and null).
+Internally, this creates a checkbox, coupled via javascript to a hidden
+field that actually contains the value. For now, the only values these
+can have are 1, 0, and empty. Clicking the checkbox cycles between them.
+</%doc>
+<%shared>
+my $init = 0;
+</%shared>
+% if ( !$init ) {
+% $init = 1;
+<SCRIPT TYPE="text/javascript">
+function tristate_onclick() {
+ var checkbox = this;
+ var input = checkbox.input;
+ if ( input.value == "" ) {
+ input.value = "0";
+ checkbox.checked = false;
+ checkbox.indeterminate = false;
+ } else if ( input.value == "0" ) {
+ input.value = "1";
+ checkbox.checked = true;
+ checkbox.indeterminate = false;
+ } else if ( input.value == "1" ) {
+ input.value = "";
+ checkbox.checked = true;
+ checkbox.indeterminate = true
+ }
+}
+
+var tristates = [];
+var tristate_boxes = [];
+window.onload = function() { // don't do this until all of the checkboxes exist
+%# tristates = document.getElementsByClassName('tristate'); # curse you, IE8
+ var all_inputs = document.getElementsByTagName('input');
+ for (var i=0; i < all_inputs.length; i++) {
+ if ( all_inputs[i].className == 'tristate' ) {
+ tristates.push(all_inputs[i]);
+ }
+ }
+ for (var i=0; i < tristates.length; i++) {
+ tristate_boxes[i] =
+ document.getElementById('checkbox_' + tristates[i].name);
+ // make sure they can find each other
+ tristate_boxes[i].input = tristates[i];
+ tristates[i].checkbox = tristate_boxes[i];
+ // set event handler
+ tristate_boxes[i].onclick = tristate_onclick;
+ // set initial value
+ if ( tristates[i].value == "" ) {
+ tristate_boxes[i].indeterminate = true
+ }
+ if ( tristates[i].value != "0" ) {
+ tristate_boxes[i].checked = true;
+ }
+ }
+};
+</SCRIPT>
+% } # end of $init
+<INPUT TYPE="hidden" NAME="<% $opt{field} %>"
+ ID="<% $opt{id} %>"
+ VALUE="<% $curr_value %>"
+ CLASS="tristate">
+<INPUT TYPE="checkbox" ID="checkbox_<%$opt{field}%>" CLASS="partial">
+<%init>
+
+my %opt = @_;
+
+# might be useful but I'm not implementing it yet
+#my $onchange = $opt{'onchange'}
+# ? 'onChange="'. $opt{'onchange'}. '(this)"'
+# : '';
+
+$opt{'id'} ||= 'hidden_'.$opt{'field'};
+my $curr_value = $opt{curr_value};
+$curr_value = undef
+ unless $curr_value eq '0' or $curr_value eq '1';
+</%init>
<INPUT TYPE="hidden" NAME="<%$name%>" ID="<%$id%>" VALUE="<% $curr_value %>">
- <TABLE>
+ <TABLE STYLE="display:inline">
<TR>
-% if ( @contact_class ) {
+% if ( @contact_class && ! $opt{name_only} ) {
<TD>
<SELECT NAME="<%$name%>_classnum" <% $onchange %>>
<OPTION VALUE="">
$label{'comment'} = 'Comment';
-my @fields = keys %label;
+my @fields = $opt{'name_only'} ? qw( first last ) : keys %label;
</%init>
ObjectCustomFieldValues.ObjectId = cust_tickets.Id
)
GROUP BY cust_tickets.custnum, ObjectCustomFieldValues.Content";
- #warn $sql."\n";
} else { # no custom_priority_field
$sql =
"SELECT cust_tickets.custnum,
my $sth = dbh->prepare($sql) or die dbh->errstr;
$sth->execute or die $sth->errstr;
while ( my $row = $sth->fetchrow_hashref ) {
- #warn to_json($row)."\n";
$num_tickets_by_priority{ $row->{priority} }->{ $row->{custnum} } =
$row->{num_tickets};
}
}
-#warn Dumper \%num_tickets_by_priority;
</%init>
// Internet Explorer 5.5+\r
if ( /*@cc_on!@*/false && sAgent.indexOf("mac") == -1 )\r
{\r
- var sBrowserVersion = navigator.appVersion.match(/MSIE (.\..)/)[1] ;\r
+ var sBrowserVersion = navigator.appVersion.match(/MSIE ([\d.]+)/)[1] ;\r
return ( sBrowserVersion >= 5.5 ) ;\r
}\r
\r
</TR>
% } else {
% foreach (qw(latitude longitude)) {
-<INPUT TYPE="hidden" NAME="<% $_ %>" VALUE="<% $object->get($_) |h%>">
+<INPUT TYPE="hidden" NAME="<% $_ %>" ID="<% $_ %>" VALUE="<% $object->get($_) |h%>">
% }
% }
<INPUT TYPE="hidden" NAME="<%$pre%>coord_auto" VALUE="<% $object->coord_auto %>">
<TD COLSPAN=8>
<INPUT TYPE="text" SIZE=15
NAME="<%$pre%>district"
+ ID="<%$pre%>district"
VALUE="<% $object->district |h %>">
<% '(automatic)' %>
</TD>
</TR>
% } else {
- <INPUT TYPE="hidden" NAME="<%$pre%>district" VALUE="<% $object->district %>">
+ <INPUT TYPE="hidden" ID="<%$pre%>" NAME="<%$pre%>district" VALUE="<% $object->district %>">
% }
% }
%# keep a clean copy of the address so we know if we need
%# to re-standardize
% foreach (qw(address1 city state country zip latitude
-% longitude censustract addr_clean) ) {
+% longitude censustract district addr_clean) ) {
<INPUT TYPE="hidden" NAME="old_<%$pre.$_%>" ID="old_<%$pre.$_%>" VALUE="<% $object->get($_) |h%>">
% }
%# Placeholders
} elsif ( $svcdb eq 'svc_phone' ) {
$report_svc{"${name}' total usage by time period"} =
- [ $fsurl. 'search/report_svc_phone.html',
+ [ $fsurl. 'search/report_svc_phone_usage.html',
'Total usage (minutes, and amount billed) for the specified time period, per phone number.',
];
$report_svc{"Advanced $lcsname reports"} =
[ $fsurl."search/report_$svcdb.html", '' ]
- if $svcdb =~ /^svc_(acct|broadband|hardware)$/
+ if $svcdb =~ /^svc_(acct|broadband|hardware|phone)$/
&& $curuser->access_right("Services: $name: Advanced search");
if ( $svcdb eq 'svc_phone' ) {
$report_packages{'Package definitions (by # active)'} = [ $fsurl.'browse/part_pkg.cgi?active=1', 'Package definitions by number of active packages' ]
if $curuser->access_right('Edit package definitions')
|| $curuser->access_right('Edit global package definitions');
-$report_packages{'Package Costs Report'} = [ $fsurl.'graph/report_cust_pkg_cost.html', 'Package setup and recurring costs graph' ]
+$report_packages{'Package costs'} = [ $fsurl.'graph/report_cust_pkg_cost.html', 'Package setup and recurring costs graph' ]
if $curuser->access_right('Financial reports');
$report_packages{'separator'} = ''
if keys %report_packages;
'Advanced ticket reports' => [ $fsurl.'rt/Search/Build.html?NewQuery=1', 'List tickets by any criteria' ],
;
-tie my %report_employees, 'Tie::IxHash',
- 'Employee Commission Report' => [ $fsurl.'search/report_employee_commission.html', '' ],
- 'Employee Audit Report' => [ $fsurl.'search/report_employee_audit.html', 'Employee audit report' ],
+tie my %report_employees, 'Tie::IxHash';
+$report_employees{'Employee Commission Report'} = [ $fsurl.'search/report_employee_commission.html', '' ]
+ if $curuser->access_right('Employees: Commission Report');
+$report_employees{'Employee Audit Report'} = [ $fsurl.'search/report_employee_audit.html', 'Employee audit report' ]
+ if $curuser->access_right('Employees: Audit Report');
;
tie my %report_bill_event, 'Tie::IxHash',
'Daily Sales, Credits and Receipts' => [ $fsurl.'graph/report_money_time_daily.html', 'Sales, credits and receipts (broken down by day) summary graph' ],
'Sales Report' => [ $fsurl.'graph/report_cust_bill_pkg.html', 'Sales report and graph (by agent, package class and/or date range)' ],
'Rated Call Sales Report' => [ $fsurl.'graph/report_cust_bill_pkg_detail.html', 'Sales report and graph (by agent, package class, usage class and/or date range)' ],
- 'Sales With Advertising Source' => [ $fsurl.'search/report_cust_bill_pkg_referral.html' ],
+ 'Sales with Advertising Source' => [ $fsurl.'search/report_cust_bill_pkg_referral.html' ],
+ 'Sales with Agent Commissions' => [ $fsurl.'search/report_agent_commission.html' ],
;
tie my %report_financial, 'Tie::IxHash';
if $conf->config('ticket_system')
;#&& FS::TicketSystem->access_right(\%session, 'Something');
$report_menu{'Employees'} = [ \%report_employees, 'Employee reports' ]
- if $curuser->access_right('Financial reports');
+ if keys %report_employees;
$report_menu{'Billing events'} = [ \%report_bill_event, 'Billing events' ]
if $curuser->access_right('Billing event reports');
$report_menu{'Financial'} = [ \%report_financial, 'Financial reports' ]
if $curuser->access_right('Job queue');
$tools_menu{'Ticketing'} = [ \%tools_ticketing, 'Ticketing tools' ]
if $conf->config('ticket_system');
+$tools_menu{'Customer email settings'} = [ $fsurl.'misc/manage_cust_email.html' ]
+ if $curuser->access_right('Edit customer');
$tools_menu{'Business card scan'} = [ $fsurl.'edit/prospect_main-upload.html' ]
if $curuser->access_right('New prospect');
$tools_menu{'Time Queue'} = [ $fsurl.'search/report_timeworked.html', 'View pending support time' ]
|| $curuser->access_right('Edit global advertising sources');
if ( $curuser->access_right('Configuration') ) {
$config_misc{'Custom fields'} = [ $fsurl.'browse/part_virtual_field.html', 'Locally defined fields', ];
- $config_misc{'Message catalog'} = [ $fsurl.'browse/msgcat.html', 'Change error messages and other customizable labels for each locale' ];
+ $config_misc{'Translation strings'} = [ $fsurl.'browse/msgcat.html', 'Translations and other customizable labels for each locale' ];
}
$config_misc{'Inventory classes and inventory'} = [ $fsurl.'browse/inventory_class.html', 'Setup inventory classes and stock inventory' ]
if $curuser->access_right('Edit inventory')
function submit_abort() {
document.OrderPkgForm.submitButton.disabled = false;
+ nd(1);
}
function <%$key%>myCallback( jobnum ) {
- overlib( OLiframeContent('<%$p%>elements/progress-popup.html?jobnum=' + jobnum + ';<%$url_or_message_link%>;formname=<%$formname%>' , 444, 168, '<% $popup_name %>'), CAPTION, 'Please wait...', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', CLOSECLICK, MIDX, 0, MIDY, 0 );
+ overlib( OLiframeContent('<%$fsurl%>elements/progress-popup.html?jobnum=' + jobnum + ';<%$url_or_message_link%>;formname=<%$formname%>' , 444, 168, '<% $popup_name %>'), CAPTION, 'Please wait...', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', CLOSECLICK, MIDX, 0, MIDY, 0 );
}
--- /dev/null
+<INPUT TYPE="button" VALUE="<% emt($label) %>" onclick="randomPass()">
+<SCRIPT TYPE="text/javascript">
+function randomPass() {
+ var i=0;
+ var pw_set='<% join('', 'a'..'z', 'A'..'Z', '0'..'9' ) %>';
+ var pass='';
+ while(i < 8) {
+ i++;
+ pass += pw_set.charAt(Math.floor(Math.random() * pw_set.length));
+ }
+ document.getElementById('<% $id %>').value = pass;
+}
+</SCRIPT>
+<%init>
+my $id = shift;
+my $label = shift || 'Generate';
+</%init>
--- /dev/null
+<%doc>
+
+Example:
+
+ include( '/elements/search-svc_broadband.html,
+ 'field' => 'svcnum',
+ #slightly deprecated old synonym for field#'field_name'=>'svcnum',
+ 'find_button' => 1, #add a "find" button to the field
+ 'curr_value' => 54, #current value
+ 'value => 32, #deprecated synonym for curr_value
+ );
+
+</%doc>
+<INPUT TYPE="hidden" NAME="<% $field %>" ID="<% $field %>" VALUE="<% $value %>">
+
+<!-- some false laziness w/ misc/batch-cust_pay.html, though not as bad as i'd thought at first... -->
+
+<INPUT TYPE = "text"
+ NAME = "<% $field %>_search"
+ ID = "<% $field %>_search"
+ SIZE = "32"
+ VALUE="<% $svc_broadband ? $svc_broadband->label : '(svcnum, ip or mac)' %>"
+ onFocus="clearhint_<% $field %>_search(this);"
+ onClick="clearhint_<% $field %>_search(this);"
+ onChange="smart_<% $field %>_search(this);"
+>
+
+% if ( $opt{'find_button'} ) {
+ <INPUT TYPE = "button"
+ VALUE = 'Find',
+ NAME = "<% $field %>_findbutton"
+ onClick = "smart_<% $field %>_search(this.form.<% $field %>_search);"
+ >
+% }
+
+<SELECT NAME="<% $field %>_select" ID="<% $field %>_select" STYLE="color:#ff0000; display:none" onChange="select_<% $field %>(this);">
+</SELECT>
+
+<% include('/elements/xmlhttp.html',
+ 'url' => $p. 'misc/xmlhttp-svc_broadband-search.cgi',
+ 'subs' => [ 'smart_search' ],
+ )
+%>
+
+<SCRIPT TYPE="text/javascript">
+
+ function clearhint_<% $field %>_search (what) {
+
+ what.style.color = '#000000';
+
+ if ( what.value == '(svcnum, ip or mac)' )
+ what.value = '';
+
+ if ( what.value.indexOf('Service not found: ') == 0 )
+ what.value = what.value.substr(20);
+
+ }
+
+ var <% $field %>_search_active = false;
+
+ function smart_<% $field %>_search(what) {
+
+ if ( <% $field %>_search_active )
+ return;
+
+ var service = what.value;
+
+ if ( service == 'searching...' || service == ''
+ || service.indexOf('Service not found: ') == 0 )
+ return;
+
+ if ( what.getAttribute('magic') == 'nosearch' ) {
+ what.setAttribute('magic', '');
+ return;
+ }
+
+ //what.value = 'searching...'
+ what.disabled = true;
+ what.style.color= '#000000';
+ what.style.backgroundColor = '#dddddd';
+
+ var service_select = document.getElementById('<% $field %>_select');
+
+ //alert("search for customer " + customer);
+
+ function <% $field %>_search_update(services) {
+
+ //alert('customers returned: ' + customers);
+
+ var serviceArray = eval('(' + services + ')');
+
+ what.disabled = false;
+ what.style.backgroundColor = '#ffffff';
+
+ if ( serviceArray.length == 0 ) {
+
+ what.form.<% $field %>.value = '';
+
+ what.value = 'Service not found: ' + what.value;
+ what.style.color = '#ff0000';
+
+ what.style.display = '';
+ service_select.style.display = 'none';
+
+ } else if ( serviceArray.length == 1 ) {
+
+ //alert('one customer found: ' + customerArray[0]);
+
+ what.form.<% $field %>.value = serviceArray[0][0];
+ what.value = serviceArray[0][1];
+
+ what.style.display = '';
+ service_select.style.display = 'none';
+
+ } else {
+
+ //alert('multiple customers found, have to create select dropdown');
+
+ //blank the current list
+ for ( var i = service_select.length; i >= 0; i-- )
+ service_select.options[i] = null;
+
+ opt(service_select, '', 'Multiple services match "' + service + '" - select one', '#ff0000');
+
+ //add the multiple services
+ for ( var s = 0; s < serviceArray.length; s++ )
+ opt(service_select, serviceArray[s][0], serviceArray[s][1], '#000000');
+
+ opt(service_select, 'cancel', '(Edit search string)', '#000000');
+
+ what.style.display = 'none';
+ service_select.style.display = '';
+
+ }
+
+ <% $field %>_search_active = false;
+
+ }
+
+ <% $field %>_search_active = true;
+
+ smart_search( service, <% $field %>_search_update );
+
+
+ }
+
+ function select_<% $field %> (what) {
+
+ var svcnum = what.options[what.selectedIndex].value;
+ var service = what.options[what.selectedIndex].text;
+
+ var service_obj = document.getElementById('<% $field %>_search');
+
+ if ( svcnum == '' ) {
+ //what.style.color = '#ff0000';
+
+ } else if ( svcnum == 'cancel' ) {
+
+ service_obj.style.color = '#000000';
+
+ what.style.display = 'none';
+ service_obj.style.display = '';
+ service_obj.focus();
+
+ } else {
+
+ what.form.<% $field %>.value = svcnum;
+
+ service_obj.value = service;
+ service_obj.style.color = '#000000';
+
+ what.style.display = 'none';
+ service_obj.style.display = '';
+
+ }
+
+ }
+
+ function opt(what,value,text,color) {
+ var optionName = new Option(text, value, false, false);
+ optionName.style.color = color;
+ var length = what.length;
+ what.options[length] = optionName;
+ }
+
+</SCRIPT>
+<%init>
+
+my( %opt ) = @_;
+
+my $field = $opt{'field'} || $opt{'field_name'} || 'svcnum';
+
+my $value = $opt{'curr_value'} || $opt{'value'};
+
+my $svc_broadband = '';
+if ( $value ) {
+ $svc_broadband = qsearchs({
+ 'table' => 'svc_broadband',
+ 'hashref' => { 'svcnum' => $value },
+ #have to join to cust_main for an agentnum 'extra_sql' => " AND ". $FS::CurrentUser::CurrentUser->agentnums_sql,
+ });
+}
+
+</%init>
what.form.<% $opt{'prefix'} %>areacode.disabled = 'disabled';
what.form.<% $opt{'prefix'} %>areacode.style.display = 'none';
var areacodewait = document.getElementById('<% $opt{'prefix'} %>areacodewait');
- areacodewait.style.display = '';
+ areacodewait.style.display = 'inline';
var areacodeerror = document.getElementById('<% $opt{'prefix'} %>areacodeerror');
areacodeerror.style.display = 'none';
what.form.<% $opt{'prefix'} %>areacode.style.display = '';
} else {
var areacodeerror = document.getElementById('<% $opt{'prefix'} %>areacodeerror');
- areacodeerror.style.display = '';
+ areacodeerror.style.display = 'inline';
}
//run the callback
<TABLE>
<TR>
+% my( $phonenum_checked, $manual_checked ) = ( '', '' );
+% if ( $export->get_dids_can_manual ) {
+% #not 100% perfect UI on error handling, but it'll do
+% if ( $opt{'curr_value'} ) {
+% $phonenum_checked = '';
+% $manual_checked = 'CHECKED';
+% } else {
+% $phonenum_checked = 'CHECKED';
+% $manual_checked = '';
+% }
+
+ <TD VALIGN="top">
+ <INPUT TYPE = "radio"
+ NAME = "phonenum_which"
+ VALUE = "phonenum"
+ onChange = "phonenum_which_changed(this)"
+ onClick = "phonenum_which_changed(this)"
+ <% $phonenum_checked %>
+ > Inventory
+ </TD>
+% }
+
% if ( $export->get_dids_npa_select ) {
<TD VALIGN="top">
'svcpart' => $svcpart,
'disable_empty' => 0,
'empty_label' => 'Select state',
+ 'disabled' => ( $manual_checked ? 1 : 0 ),
)
%>
- <BR><FONT SIZE="-1">State</FONT>
+ <BR><FONT SIZE="-1" ID="phonenum_state_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>State</FONT>
</TD>
<TD VALIGN="top">
'empty' => 'Select area code',
)
%>
- <BR><FONT SIZE="-1">Area code</FONT>
+ <BR><FONT SIZE="-1" ID="areacode_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Area code</FONT>
</TD>
<TD VALIGN="top">
<% include('/elements/select-exchange.html',
- 'svcpart' => $svcpart,
- 'empty' => 'Select exchange',
+ 'svcpart' => $svcpart,
+ 'empty' => 'Select exchange',
)
%>
- <BR><FONT SIZE="-1">City / Exchange</FONT>
+ <BR><FONT SIZE="-1" ID="exchange_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>City / Exchange</FONT>
</TD>
% } else {
+%
+% #this code path currently only being used by fibernetics
+% # should change "Province" label to "State" or make it configurable
+% # if/when other folks need an areacode-less DID selector that goes
+% # directly from state to region
<TD VALIGN="top">
<% include('/elements/select.html',
'options' => [ '', @{ $export->get_dids } ],
'labels' => { '' => 'Select province' },
'onchange' => 'phonenum_state_changed(this);',
+ 'disabled' => ( $manual_checked ? 1 : 0 ),
)
%>
- <BR><FONT SIZE="-1">Province</FONT>
+ <BR><FONT SIZE="-1" ID="phonenum_state_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Province</FONT>
</TD>
<TD VALIGN="top">
'empty' => 'Select region',
)
%>
- <BR><FONT SIZE="-1">Region</FONT>
+ <BR><FONT SIZE="-1" ID="region_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Region</FONT>
</TD>
% }
'region' => ! $export->get_dids_npa_select,
)
%>
- <BR><FONT SIZE="-1">Phone number</FONT>
+ <BR><FONT SIZE="-1" ID="phonenum_phonenum_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Phone number</FONT>
</TD>
</TR>
+
+% if ( $export->get_dids_can_manual ) {
+ <TR>
+
+ <TD VALIGN="top">
+ <INPUT TYPE = "radio"
+ NAME = "phonenum_which"
+ VALUE = "phonenum_manual"
+ onChange = "phonenum_which_changed(this)"
+ onClick = "phonenum_which_changed(this)"
+ <% $manual_checked %>
+ > Manual entry
+ </TD>
+
+ <TD VALIGN="top" COLSPAN=4>
+ <& /elements/input-text.html,
+ %opt,
+ field => 'phonenum_manual',
+ id => 'phonenum_manual',
+ type => 'text',
+ disabled => ( $phonenum_checked ? 1 : 0 ),
+ &>
+ </TD>
+ </TR>
+
+ <SCRIPT TYPE="text/javascript">
+ function phonenum_which_changed(what) {
+
+ if ( what.value == 'phonenum' && what.checked ) {
+
+ what.form.phonenum_manual.disabled = true;
+ what.form.phonenum_manual.style.backgroundColor = '#dddddd';
+
+ what.form.phonenum_state.disabled = false;
+
+ document.getElementById('phonenum_state_label').style.color = '#000000';
+ if ( document.getElementById('areacode_label') ) {
+ document.getElementById('areacode_label').style.color = '#000000';
+ }
+ if ( document.getElementById('exchange_label') ) {
+ document.getElementById('exchange_label').style.color = '#000000';
+ }
+ if ( document.getElementById('region_label') ) {
+ document.getElementById('region_label').style.color = '#000000';
+ }
+ document.getElementById('phonenum_phonenum_label').style.color = '#000000';
+
+ var value = what.form.phonenum_state.options[ what.form.phonenum_state.selectedIndex].value;
+
+ if ( value != '' ) {
+
+ if ( what.form.areacode ) {
+ what.form.areacode.disabled = false;
+
+ var areacode_value = what.form.areacode.options[ what.form.areacode.selectedIndex].value;
+
+ if ( areacode_value != '' ) {
+ what.form.exchange.disabled = false;
+
+ var exchange_value = what.form.exchange.options[ what.form.exchange.selectedIndex].value;
+
+ if ( exchange_value != '' ) {
+ what.form.phonenum.disabled = false;
+ }
+
+ }
+
+ }
+ if ( what.form.region ) {
+ what.form.region.disabled = false;
+
+ var region_value = what.form.region.options[ what.form.region.selectedIndex].value;
+
+ if ( region_value != '' ) {
+ what.form.phonenum.disabled = false;
+ }
+
+ }
+
+ }
+
+ }
+
+ if ( what.value == 'phonenum_manual' && what.checked ) {
+
+ what.form.phonenum_manual.disabled = false;
+ what.form.phonenum_manual.style.backgroundColor = '#ffffff';
+
+ what.form.phonenum_state.disabled = true;
+
+ document.getElementById('phonenum_state_label').style.color = '#999999';
+ if ( document.getElementById('areacode_label') ) {
+ document.getElementById('areacode_label').style.color = '#999999';
+ }
+ if ( document.getElementById('exchange_label') ) {
+ document.getElementById('exchange_label').style.color = '#999999';
+ }
+ if ( document.getElementById('region_label') ) {
+ document.getElementById('region_label').style.color = '#999999';
+ }
+ document.getElementById('phonenum_phonenum_label').style.color = '#999999';
+
+ if ( what.form.areacode ) {
+ what.form.areacode.disabled = true;
+ }
+
+ if ( what.form.exchange ) {
+ what.form.exchange.disabled = true;
+ }
+
+ if ( what.form.region ) {
+ what.form.region.disabled = true;
+ }
+
+ what.form.phonenum.disabled = true;
+ }
+
+ }
+ </SCRIPT>
+
+% }
+
</TABLE>
% }
what.form.<% $opt{'prefix'} %>exchange.disabled = 'disabled';
what.form.<% $opt{'prefix'} %>exchange.style.display = 'none';
var exchangewait = document.getElementById('<% $opt{'prefix'} %>exchangewait');
- exchangewait.style.display = '';
+ exchangewait.style.display = 'inline';
var exchangeerror = document.getElementById('<% $opt{'prefix'} %>exchangeerror');
exchangeerror.style.display = 'none';
what.form.<% $opt{'prefix'} %>exchange.style.display = '';
} else {
var exchangeerror = document.getElementById('<% $opt{'prefix'} %>exchangeerror');
- exchangeerror.style.display = '';
+ exchangeerror.style.display = 'inline';
}
//run the callback
<% include( '/elements/input-text.html', %opt, 'type'=>'text' ) %>
<SELECT ID="<% $opt{'prefix'} %>sel_mac_addr" NAME="<% $opt{'prefix'} %>sel_mac_addr"
- notonChange="<% $opt{'prefix'} %>mac_addr_changed(this); <% $opt{'onchange'} %>"
+%# notonChange="<% $opt{'prefix'} %>mac_addr_changed(this); <% $opt{'onchange'} %>"
<% $opt{'disabled'} %> STYLE="display: none">
<OPTION VALUE="">Select MAC address</OPTION>
</SELECT>
$opt{'records'} = delete $opt{'part_svc'}
if $opt{'part_svc'};
-$opt{'records'} ||= [ qsearch( 'part_svc', {} ) ]; # { disabled=>'' } )
+my %hash = ();
+$hash{'svcdb'} = $opt{'svcdb'} if $opt{'svcdb'};
+
+$opt{'records'} ||= [ qsearch( 'part_svc', \%hash ) ]; # { disabled=>'' } )
</%init>
what.form.<% $opt{'prefix'} %>phonenum.disabled = 'disabled';
what.form.<% $opt{'prefix'} %>phonenum.style.display = 'none';
var phonenumwait = document.getElementById('<% $opt{'prefix'} %>phonenumwait');
- phonenumwait.style.display = '';
+ phonenumwait.style.display = 'inline';
var phonenumerror = document.getElementById('<% $opt{'prefix'} %>phonenumerror');
phonenumerror.style.display = 'none';
what.form.<% $opt{'prefix'} %>phonenum.style.display = '';
} else {
var phonenumerror = document.getElementById('<% $opt{'prefix'} %>phonenumerror');
- phonenumerror.style.display = '';
+ phonenumerror.style.display = 'inline';
}
//run the callback
what.form.<% $opt{'prefix'} %>region.disabled = 'disabled';
what.form.<% $opt{'prefix'} %>region.style.display = 'none';
var regionwait = document.getElementById('<% $opt{'prefix'} %>regionwait');
- regionwait.style.display = '';
+ regionwait.style.display = 'inline';
var regionerror = document.getElementById('<% $opt{'prefix'} %>regionerror');
regionerror.style.display = 'none';
what.form.<% $opt{'prefix'} %>region.style.display = '';
} else {
var regionerror = document.getElementById('<% $opt{'prefix'} %>regionerror');
- regionerror.style.display = '';
+ regionerror.style.display = 'inline';
}
//run the callback
# required
##
'table' => 'table_name',
- 'name_col' => 'name_column',
+ 'name_col' => 'name_column', #or method if you pass an order_by
#strongly recommended (you want your forms to be "sticky" on errors, right?)
'curr_value' => 'current_value',
<% $opt{'label_callback'}
? &{ $opt{'label_callback'} }( $record )
: $record->$name_col()
+ |h
%>
% }
my $pre = $opt{prefix} || '';
my $tiers = $opt{tiers} or die "no tiers defined";
-#my $json = JSON->new()->canonical(); #sort
-# something super weird and broken going on with JSON's auto-loading, just
-# using JSON alone errors out with
-# Can't locate object method "new" via package "null" (perhaps you forgot to
-# load "null"?)
-# yes, "null", not "JSON". so instead, using JSON::XS explicity...
-use JSON::XS;
my $json = JSON::XS->new();
$json->canonical;
$date_noinit = 1;
}
else {
- $include = "input-$include" if $include =~ /^(text|money)$/;
+ $include = "input-$include" if $include =~ /^(text|money|percentage)$/;
$include = "tr-$include" unless $include eq 'hidden';
$html .= include( "/elements/$include.html",
%$lf,
function form_address_info() {
var cf = document.<% $formname %>;
- var returnobj = { onlyship: <% $onlyship ? 1 : 0 %> };
-% if ( !$onlyship ) {
+ var returnobj = { billship: <% $billship %> };
+% if ( $billship ) {
returnobj['same'] = cf.elements['same'].checked;
% }
% if ( $withfirm ) {
cf.elements['<% $pre %>coord_auto'].value = 'Y';
changed = true;
}
-
-% } #foreach $pre
-
// standardize if the old address wasn't clean
- if ( cf.elements['old_ship_addr_clean'].value == '' ||
- cf.elements['old_bill_addr_clean'].value == '' ) {
-
+ if ( cf.elements['<% $pre %>addr_clean'].value == '' ) {
changed = true;
-
}
+% } #foreach $pre
+
// or if it was clean but has been changed
for (var key in address_info) {
var old_el = cf.elements['old_'+key];
% # If address hasn't been changed, auto-confirm the existing value of
% # censustract so that we don't ask the user to confirm it again.
- if ( !changed ) {
+ if ( !changed && <% $withcensus %> ) {
if ( address_info['same'] ) {
cf.elements['bill_censustract'].value =
address_info['bill_censustract'];
% if ( $conf->exists('enable_taxproducts') ) {
+ var cf = document.<% $formname %>;
+
if ( new String(cf.elements['<% $taxpre %>zip'].value).length < 10 )
{
var country_el = cf.elements['<% $taxpre %>country'];
var country = country_el.options[ country_el.selectedIndex ].value;
- var geocode = cf.elements['geocode'].value;
+ var geocode = cf.elements['bill_geocode'].value;
if ( country == 'CA' || country == 'US' ) {
} else {
- cf.elements['geocode'].value = 'DEFAULT';
+ cf.elements['bill_geocode'].value = 'DEFAULT';
<% $post_geocode %>;
}
} else {
- cf.elements['geocode'].value = '';
+ cf.elements['bill_geocode'].value = '';
<% $post_geocode %>;
}
cf.elements['<% $taxpre %>city'].value = argsHash['city'];
setselect(cf.elements['<% $taxpre %>state'], argsHash['state']);
cf.elements['<% $taxpre %>zip'].value = argsHash['zip'];
- cf.elements['geocode'].value = argsHash['geocode'];
+ cf.elements['bill_geocode'].value = argsHash['geocode'];
<% $post_geocode %>;
}
// popup a chooser
- overlib( OLresponseAJAX, CAPTION, 'Select tax location', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH, 576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399', TEXTSIZE, 3 );
+ overlib( OLresponseAJAX, CAPTION, 'Select tax location', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, WIDTH, 576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399', TEXTSIZE, 3 );
}
my %opt = @_;
my $conf = new FS::Conf;
-my $withfirm = 1;
-my $withcensus = 1;
+my $withfirm = $opt{'with_firm'} ? 1 : 0;
+my $withcensus = $opt{'with_census'} ? 1 : 0;
+
+my @prefixes = '';
+my $billship = $opt{'billship'} ? 1 : 0; # whether to have bill_ and ship_ prefixes
+my $taxpre = '';
+if ($billship) {
+ @prefixes = qw(bill_ ship_);
+ $taxpre = $conf->exists('tax-ship_address') ? 'ship_' : 'bill_';
+}
my $formname = $opt{form} || 'CustomerForm';
-my $onlyship = $opt{onlyship} || '';
-#my $main_prefix = $opt{main_prefix} || '';
-#my $ship_prefix = $opt{ship_prefix} || ($onlyship ? '' : 'ship_');
-# The prefixes are now 'ship_' and 'bill_'.
-my $taxpre = 'bill_';
-$taxpre = 'ship_' if ( $conf->exists('tax-ship_address') || $onlyship );
my $post_geocode = $opt{callback} || 'post_geocode();';
-$withfirm = 0 if $opt{no_company};
-$withcensus = 0 if $opt{no_census};
-
-my @prefixes = ('ship_');
-unshift @prefixes, 'bill_' unless $onlyship;
</%init>
my $manage_link = $opt{'manage_link'};
my $manage_target = '';
if ( $part_svc->svcdb eq 'svc_broadband' and $manage_link ) {
- my $ip_addr = $svc_x->ip_addr; #substitution for $manage_link
+ my $ip_addr = $svc_x->ip_addr; #substitution for $manage_link
+ my $mac_addr = $svc_x->mac_addr; # ditto
$manage_link = eval(qq("$manage_link"));
$opt{'manage_link_text'} ||= mt('Manage Device');
$opt{'manage_link_loc'} ||= 'bottom';
my( $size, $maxlength ) = ( 11, 10 );
if ( $opt{'input_time'} ) {
$input_time = ', showsTime: true, timeFormat: "12"'; # http://www.dynarch.com/demos/jscalendar/doc/html/reference.html#node_sec_2.3
- $time_format = ' %k:%M:%S'; # http://www.dynarch.com/demos/jscalendar/doc/html/reference.html#node_sec_5.3.5
+ $time_format = ' %H:%M:%S'; # http://www.dynarch.com/demos/jscalendar/doc/html/reference.html#node_sec_5.3.5
$time_hint = ' h:m:s';
$size = 21;
$maxlength = 27;
--- /dev/null
+<& tr-td-label.html, @_ &>
+
+ <TD <% $colspan %> <% $cell_style %> ID="<% $opt{input_id} || $opt{id}.'_input0' %>"><& search-svc_broadband.html, @_ &></TD>
+
+</TR>
+
+<%init>
+
+my %opt = @_;
+
+my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
+
+my $colspan = $opt{'colspan'} ? 'COLSPAN="'.$opt{'colspan'}.'"' : '';
+
+</%init>
--- /dev/null
+<%doc>
+
+Example:
+
+ include('/elements/tr-select-contact.html',
+ 'cgi' => $cgi,
+
+ 'cust_main' => $cust_main,
+ #or
+ 'prospect_main' => $prospect_main,
+
+ #optional
+ 'empty_label' => '(default contact)',
+ )
+
+</%doc>
+
+<SCRIPT TYPE="text/javascript">
+
+ function contact_disable(what) {
+% for (@contact_fields) {
+ what.form.<%$_%>.disabled = true;
+ var ftype = what.form.<%$_%>.tagName;
+ if( ftype == 'SELECT') changeSelect(what.form.<%$_%>, '');
+ else what.form.<%$_%>.value = '';
+ if( ftype != 'SELECT') what.form.<%$_%>.style.backgroundColor = '#dddddd';
+% }
+ }
+
+ function contact_clear(what) {
+% for (@contact_fields) {
+ var ftype = what.form.<%$_%>.tagName;
+ if( ftype == 'INPUT' ) what.form.<%$_%>.value = '';
+% }
+ }
+
+ function contact_enable(what) {
+% for (@contact_fields) {
+ what.form.<%$_%>.disabled = false;
+ var ftype = what.form.<%$_%>.tagName;
+ if( ftype != 'SELECT') what.form.<%$_%>.style.backgroundColor = '#ffffff';
+% }
+ }
+
+ function contactnum_changed(what) {
+ var contactnum = what.options[what.selectedIndex].value;
+ if ( contactnum == -1 ) { //Add new contact
+ contact_clear(what);
+
+ contact_enable(what);
+ return;
+ }
+
+% if ( $editable ) {
+ if ( contactnum == 0 ) {
+% }
+
+% #sleep/wait until dropdowns are updated?
+ contact_disable(what);
+
+% if ( $editable ) {
+ } else {
+
+% #sleep/wait until dropdowns are updated?
+ contact_enable(what);
+
+ }
+% }
+
+ }
+
+ function changeSelect(what, value) {
+ for ( var i=0; i<what.length; i++) {
+ if ( what.options[i].value == value ) {
+ what.selectedIndex = i;
+ }
+ }
+ }
+
+</SCRIPT>
+
+<TR>
+ <<%$th%> ALIGN="right" VALIGN="top"><% $opt{'label'} || emt('Service contact') %></<%$th%>>
+ <TD VALIGN="top" COLSPAN=7>
+ <SELECT NAME = "contactnum"
+ ID = "contactnum"
+ STYLE = "vertical-align:top;margin:3px"
+ onchange = "contactnum_changed(this);"
+ >
+% if ( $cust_main ) {
+ <OPTION VALUE=""><% $opt{'empty_label'} || '(customer default)' |h %>
+% }
+%
+% foreach my $contact ( @contact ) {
+ <OPTION VALUE="<% $contact->contactnum %>"
+ <% $contactnum == $contact->contactnum ? 'SELECTED' : '' %>
+ ><% $contact->line |h %>
+% }
+% if ( $addnew ) {
+ <OPTION VALUE="-1"
+ <% $contactnum == -1 ? 'SELECTED' : '' %>
+ >New contact
+% }
+ </SELECT>
+
+<% include('/elements/contact.html',
+ 'object' => $contact,
+ #'onchange' ? probably not
+ 'disabled' => $disabled,
+ 'name_only' => 1,
+ )
+%>
+
+ </TD>
+</TR>
+
+<SCRIPT TYPE="text/javascript">
+ contactnum_changed(document.getElementById('contactnum'));
+</SCRIPT>
+<%init>
+
+#based on / kinda false laziness w/tr-select-cust_contact.html
+
+my $conf = new FS::Conf;
+
+my %opt = @_;
+my $cgi = $opt{'cgi'};
+my $cust_pkg = $opt{'cust_pkg'};
+my $cust_main = $opt{'cust_main'};
+my $prospect_main = $opt{'prospect_main'};
+die "cust_main or prospect_main required" unless $cust_main or $prospect_main;
+
+my $contactnum = '';
+if ( $cgi->param('error') ) {
+ $cgi->param('contactnum') =~ /^(\-?\d*)$/ or die "illegal contactnum";
+ $contactnum = $1;
+} else {
+ if ( length($opt{'curr_value'}) ) {
+ $contactnum = $opt{'curr_value'};
+ } elsif ($prospect_main) {
+ my @cust_contact = $prospect_main->cust_contact;
+ $contactnum = $cust_contact[0]->contactnum if scalar(@cust_contact)==1;
+ } else { #$cust_main
+ $cgi->param('contactnum') =~ /^(\-?\d*)$/ or die "illegal contactnum";
+ $contactnum = $1;
+ }
+}
+
+##probably could use explicit controls
+#my $editable = $cust_main ? 0 : 1; #could use explicit control
+my $editable = 0;
+my $addnew = $cust_main ? 1 : ( $contactnum>0 ? 0 : 1 );
+
+my @contact_fields = map "contactnum_$_", qw( first last );
+
+my $contact; #the one that shows by default in the contact edit space
+if ( $contactnum && $contactnum > 0 ) {
+ $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
+ or die "unknown contactnum";
+} else {
+ $contact = new FS::contact;
+ if ( $contactnum == -1 ) {
+ $contact->$_( $cgi->param($_) ) foreach @contact_fields; #XXX
+ } elsif ( $cust_pkg && $cust_pkg->contactnum ) {
+ my $pkg_contact = $cust_pkg->contact_obj;
+ $contact->$_( $pkg_contact->$_ ) foreach @contact_fields; #XXX why are we making a new one gagain??
+ $opt{'empty_label'} ||= 'package contact: '.$pkg_contact->line;
+ } elsif ( $cust_main ) {
+ $contact = new FS::contact; #I think
+ }
+}
+
+my $contact_sort = sub {
+ lc($a->last) cmp lc($b->last)
+ or lc($a->first) cmp lc($b->first)
+};
+
+my @contact;
+push @contact, $cust_main->cust_contact if $cust_main;
+push @contact, $prospect_main->contact if $prospect_main;
+push @contact, $contact
+ if !$cust_main && $contact && $contact->contactnum > 0
+ && ! grep { $_->contactnum == $contact->contactnum } @contact;
+
+@contact = sort $contact_sort grep !$_->disabled, @contact;
+
+$contact = $contact[0]
+ if ( $prospect_main )
+ && !$opt{'is_optional'}
+ && @contact;
+
+my $disabled =
+ ( $contactnum < 0
+ || ( $editable && $contactnum )
+ || ( $prospect_main
+ && !$opt{'is_optional'} && !@contact && $addnew
+ )
+ )
+ ? ''
+ : 'DISABLED';
+
+my $th = $opt{'no_bold'} ? 'TD' : 'TH';
+
+</%init>
}
}
+ var location_fields = <% encode_json(\@location_fields) %>;
function update_location( string ) {
- var hash = eval('('+string+')');
- document.getElementById('address1').value = hash['address1'];
- document.getElementById('city').value = hash['city'];
- document.getElementById('zip').value = hash['zip'];
-
-% if ( $opt{'alt_format'} ) {
- changeSelect( document.getElementById('location_kind'), hash['location_kind']);
- changeSelect( document.getElementById('location_type'), hash['location_type']);
- document.getElementById('location_number').value = hash['location_number'];
-% } else {
- document.getElementById('address2').value = hash['address2'];
-% }
-
- var country_el = document.getElementById('country');
-
- changeSelect( country_el, hash['country'] );
-
- country_changed( country_el,
+ var hash = JSON.parse(string);
+ for(var i = 0; i < location_fields.length; i++) {
+ var f = location_fields[i];
+ if (hash[f] && document.getElementById(f)) {
+ document.getElementById(f).value = hash[f];
+ }
+ }
+ country_changed( document.getElementById('country'),
fix_state_factory( hash['state'],
hash['county']
)
<TD COLSPAN=7>
<SELECT NAME = "locationnum"
ID = "locationnum"
- onChange = "locationnum_changed(this);"
+ onchange = "locationnum_changed(this);"
>
% if ( $cust_main ) {
<OPTION VALUE="<% $cust_main->ship_locationnum %>"><% $opt{'empty_label'} || '(default service address)' |h %>
my $editable = $cust_main ? 0 : 1; #could use explicit control
my $addnew = $cust_main ? 1 : ( $locationnum>0 ? 0 : 1 );
-my @location_fields = qw( address1 address2 city county state zip country
- latitude longitude
- );
+my @location_fields = FS::cust_main->location_fields;
if ( $opt{'alt_format'} ) {
push @location_fields, qw( location_type location_number location_kind );
}
<% include('tr-td-label.html', @_ ) %>
-% if ( $opt{'curr_value'} ne '' && $use_selector ) {
+% if ( $use_selector && $opt{'curr_value'} ne '' && ! $can_edit ) {
<TD BGCOLOR="#dddddd" <% $cell_style %>><% $opt{'formatted_value'} || $opt{'curr_value'} || $opt{'value'} |h %></TD>
my $use_selector = scalar(@exports) ? 1 : 0;
+my $can_edit = scalar(@exports) && $exports[0]->get_dids_can_edit;
+
</%init>
id => 'discount_term',
options => [ '', @discount_term ],
labels => { '' => mt('1 month'),
- map { $_ => mt('[_1] months', $_) } @discount_term },
+ map { $_ => mt('[_1] months', sprintf('%.0f', $_)) }
+ @discount_term
+ },
curr_value => '',
onchange => $amount_id ? 'change_discount_term(this)' : '',
&>
my %hash = (
'show_month_abbr' => 1,
'start_year' => '1999',
- 'end_year' => '2013', #haha, well...
+ 'end_year' => '2014',
@_,
);
</%init>
--- /dev/null
+% if ( scalar(@classnums) == 0 ) {
+<& tr-fixed.html, %opt &>
+% } elsif ( scalar(@classnums) == 1 ) {
+% $opt{'extra_sql'} .= ' AND '.$classnum_sql;
+<& tr-select-table.html,
+ 'table' => 'inventory_item',
+ 'name_col' => 'item',
+ 'value_col' => 'item',
+ %opt
+&>
+% } else {
+<& tr-td-label.html, %opt &>
+<TD>
+<& select-tiered.html,
+ 'prefix' => $opt{'field'}.'_',
+ 'tiers' => [
+ {
+ field => $opt{'field'}.'_classnum',
+ table => 'inventory_class',
+ extra_sql => "WHERE $classnum_sql",
+ name_col => 'classname',
+ empty_label => '(all)',
+ },
+ {
+ field => $opt{'field'},
+ table => 'inventory_item',
+ name_col => 'item',
+ value_col => 'item',
+ link_col => 'classnum',
+ extra_sql => delete($opt{'extra_sql'}),
+ disable_empty => 1,
+ },
+ ],
+ %opt,
+&>
+</TD>
+</TR>
+% }
+<%init>
+my %opt = @_;
+my @classnums;
+if (ref($opt{'classnum'})) {
+ @classnums = @{ $opt{'classnum'} };
+} else {
+ @classnums = split(',', $opt{'classnum'});
+}
+my $classnum_sql = 'classnum IN('.join(',', @classnums).')';
+</%init>
% } else {
<TR>
- <TD ALIGN="right"><% $opt{'label'} || 'Package definition' %></TD>
+ <TD ALIGN="right"><% $opt{'label'} || 'Service definition' %></TD>
<TD>
<% include( '/elements/select-part_svc.html',
'multiple' => 1,
my( %opt ) = @_;
-$opt{'part_svc'} ||= [ qsearch( 'part_svc', {} ) ]; # { disabled=>'' } )
+my %hash = ();
+$hash{'svcdb'} = $opt{'svcdb'} if $opt{'svcdb'};
+
+$opt{'part_svc'} ||= [ qsearch( 'part_svc', \%hash ) ]; # { disabled=>'' } )
</%init>
my $id = $opt{'id'} || $func_suffix;
-my( $add_access_right, $access_right );
+my $add_access_right;
if ($class eq 'C') {
- $access_right = 'Cancel customer';
$add_access_right = 'Add on-the-fly cancel reason';
} elsif ($class eq 'S') {
- $access_right = 'Suspend customer package';
$add_access_right = 'Add on-the-fly suspend reason';
} elsif ($class eq 'R') {
- $access_right = 'Post credit';
$add_access_right = 'Add on-the-fly credit reason';
} else {
die "illegal class: $class";
'' => '',
1 => 'VoIP without Broadband',
2 => 'VoIP with Broadband',
- 3 => 'Wholesale VoIP'
+ 3 => 'Wholesale VoIP',
+ 4 => 'Local Exchange (non-VoIP)',
);
</%init>
-<% objToJson(\@areacodes) %>
+<% encode_json(\@areacodes) %>\
<%init>
my( $state, $svcpart ) = $cgi->param('arg');
function custnum_update_callback(rownum, prefix) {
var custnum = document.getElementById('custnum'+rownum).value;
- document.getElementById('enable_app'+rownum).disabled = (
- custnum == 0 ||
- num_open_invoices[rownum] < 2
- );
+ // if there is a custnum and more than one open invoice, enable
+ // (and check) the box
+ var show_applications = (custnum > 0 && num_open_invoices[rownum] > 1);
+ var enable_app_checkbox = document.getElementById('enable_app'+rownum);
+ enable_app_checkbox.disabled = show_applications;
+
% if ( $use_discounts ) {
select_discount_term(rownum, prefix);
% }
}
+function invnum_update_callback(rownum, prefix) {
+ custnum_update_callback(rownum, prefix);
+}
+
function select_discount_term(row, prefix) {
var custnum_obj = document.getElementById('custnum'+prefix+row);
var select_obj = document.getElementById('discount_term'+prefix+row);
next.call(this, rownum);
}
);
+ } else {
+ var row = document.getElementById('row'+rownum);
+ var table_rows = row.parentNode.rows;
+ for (i = row.sectionRowIndex; i < table_rows.count; i++) {
+ if ( table_rows[i].id.indexof('row'+rownum+'.') > -1 ) {
+ table_rows.removeChild(table_rows[i]);
+ } else {
+ break;
+ }
+ }
+ lock_payment_row(rownum, false);
}
}
&& amount_unapplied(rownum) > 0 ) {
create_application_row(rownum, parseInt(appnum) + 1);
-
}
}
footer_align => \@footer_align,
onchange => \@onchange,
custnum_update_callback => 'custnum_update_callback',
+ invnum_update_callback => 'invnum_update_callback',
add_row_callback => 'add_row_callback',
&>
<& /elements/standardize_locations.html,
'form' => "OrderPkgForm",
- 'onlyship' => 1,
- 'no_company' => 1,
- 'no_census' => 1,
'callback' => 'document.OrderPkgForm.submit();',
&>
--- /dev/null
+<& /elements/header-popup.html, mt("Change Package Contact") &>
+
+<& /elements/error.html &>
+
+<FORM ACTION="<% $p %>misc/process/change_pkg_contact.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
+
+<% ntable('#cccccc') %>
+
+ <TR>
+ <TH ALIGN="right"><% mt('Package') |h %></TH>
+ <TD COLSPAN=7>
+ <% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B> - <% $part_pkg->comment |h %>
+ </TD>
+ </TR>
+
+% if ( $cust_pkg->contactnum ) {
+ <TR>
+ <TH ALIGN="right"><% mt('Current Contact') %></TH>
+ <TD COLSPAN=7>
+ <% $cust_pkg->contact_obj->line |h %>
+ </TD>
+ </TR>
+% }
+
+<& /elements/tr-select-contact.html,
+ 'label' => mt('New Contact'), #XXX test
+ 'cgi' => $cgi,
+ 'cust_main' => $cust_pkg->cust_main,
+&>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE = "submit"
+ VALUE = "<% $cust_pkg->contactnum ? mt("Change contact") : mt("Add contact") |h %>"
+>
+
+</FORM>
+</BODY>
+</HTML>
+
+<%init>
+
+my $conf = new FS::Conf;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('Change customer package');
+
+my $pkgnum = scalar($cgi->param('pkgnum'));
+$pkgnum =~ /^(\d+)$/ or die "illegal pkgnum $pkgnum";
+$pkgnum = $1;
+
+my $cust_pkg =
+ qsearchs({
+ 'table' => 'cust_pkg',
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => { 'pkgnum' => $pkgnum },
+ 'extra_sql' => ' AND '. $curuser->agentnums_sql,
+ }) or die "unknown pkgnum $pkgnum";
+
+my $cust_main = $cust_pkg->cust_main
+ or die "can't get cust_main record for custnum ". $cust_pkg->custnum.
+ " ( pkgnum ". cust_pkg->pkgnum. ")";
+
+my $part_pkg = $cust_pkg->part_pkg;
+
+</%init>
<FORM NAME="choosegeocodeform">
<CENTER><BR><B>Choose tax location</B><BR><BR>
-<P>the geocode is:<% $header %></P>
<P STYLE="<% $style %>"><% $header %></P>
<SELECT NAME='geocodes' ID='geocodes' STYLE="<% $style %>">
% map { $value{$_} = $location{$_} } qw ( city state )
% if $location{country} eq 'CA';
%
-% my $value = encode_entities(objToJson({ %value })
+% my $value = encode_entities(encode_json({ %value })
% );
% my $content = '';
% $content .= $location->$_. ' ' x ( $max{$_} - length($location->$_) )
</B><BR><BR>
<TABLE WIDTH="100%">
-% my @prefixes;
-% if ( $old{onlyship} ) {
-% @prefixes = ('ship_');
-% } elsif ( $old{same} ) {
+% my @prefixes = ('');
+% if ( $old{same} ) {
% @prefixes = ('bill_');
-% } else {
+% } elsif ( $old{billship} ) {
% @prefixes = ('bill_', 'ship_');
% }
% for my $pre (@prefixes) {
-% my $name = $pre eq 'ship_' ? 'service' : 'billing';
+% my $name = $pre eq 'bill_' ? 'billing' : 'service';
% if ( $new{$pre.'addr_clean'} ) {
<TR>
<TH>Entered <%$name%> address</TH>
my %old = %{ $q->{old} };
my %new = %{ $q->{new} };
-my $addresses = $old{onlyship} ? 'address' : 'addresses';
+my $addresses = $old{billship} ? 'addresses' : 'address';
</%init>
--- /dev/null
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('Edit customer package dates');
+
+my %arg = $cgi->Vars;
+
+my $pkgnum = $arg{'pkgnum'};
+$pkgnum =~ /^\d+$/ or die "bad pkgnum '$pkgnum'";
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+my %hash = $cust_pkg->hash;
+foreach (qw( start_date setup bill last_bill contract_end )) {
+ # adjourn, expire, resume not editable this way
+ if( $arg{$_} =~ /^\d+$/ ) {
+ $hash{$_} = $arg{$_};
+ } elsif ( $arg{$_} ) {
+ $hash{$_} = parse_datetime($arg{$_});
+ } else {
+ $hash{$_} = '';
+ }
+}
+
+my (@changes, @confirm, @errors);
+
+my $part_pkg = $cust_pkg->part_pkg;
+my @supp_pkgs = $cust_pkg->supplemental_pkgs;
+my $main_pkg = $cust_pkg->main_pkg;
+
+my $conf = FS::Conf->new;
+my $date_format = $conf->config('date_format') || '%b %o, %Y';
+# Start date
+if ( $hash{'start_date'} != $cust_pkg->get('start_date') and !$hash{'setup'} ) {
+ my $start = '';
+ $start = time2str($date_format, $hash{'start_date'}) if $hash{'start_date'};
+ my $text = 'Set this package';
+ if ( @supp_pkgs ) {
+ $text .= ' and all its supplemental packages';
+ }
+ $text .= ' to start billing';
+ if ( $start ) {
+ $text .= ' on [_1].';
+ push @changes, mt($text, $start);
+ } else {
+ $text .= ' immediately.';
+ push @changes, mt($text);
+ }
+ push @confirm, '';
+}
+
+# Setup date changes
+if ( $hash{'setup'} != $cust_pkg->get('setup') ) {
+ my $setup = time2str($date_format, $hash{'setup'});
+ my $has_setup_fee = grep { $_->part_pkg->option('setup_fee',1) > 0 }
+ $cust_pkg, @supp_pkgs;
+ if ( !$hash{'setup'} ) {
+ my $text = 'Remove the setup date';
+ $text .= ' from this and all its supplemental packages' if @supp_pkgs;
+ $text .= '.';
+ push @changes, mt($text);
+ if ( $has_setup_fee ) {
+ push @confirm, mt('This will re-charge the customer for the setup fee.');
+ } else {
+ push @confirm, '';
+ }
+ } elsif ( $hash{'setup'} and !$cust_pkg->get('setup') ) {
+ my $text = 'Add a setup date of [_1]';
+ $text .= ' to this and all its supplemental packages' if @supp_pkgs;
+ $text .= '.';
+ push @changes, mt($text, $setup);
+ if ( $has_setup_fee ) {
+ push @confirm, mt('This will prevent charging the setup fee.');
+ } else {
+ push @confirm, '';
+ }
+ } else {
+ my $text = 'Set the setup date to [_1]';
+ $text .= ' on this and all its supplemental packages' if @supp_pkgs;
+ $text .= '.';
+ push @changes, mt($text, $setup);
+ push @confirm, '';
+ }
+}
+
+# Check for start date + setup date
+if ( $hash{'start_date'} and $hash{'setup'} ) {
+ if ( $cust_pkg->get('setup') ) {
+ push @errors, mt('Since the package has already started billing, it '.
+ 'cannot have a start date.');
+ } else {
+ push @errors, mt('You cannot set both a start date and a setup date on '.
+ 'the same package.');
+ }
+}
+
+# Last bill date change
+if ( $hash{'last_bill'} != $cust_pkg->get('last_bill') ) {
+ my $last_bill = time2str($date_format, $hash{'last_bill'});
+ my $name = 'last bill date';
+ $name = 'last renewal date' if $part_pkg->is_prepaid;
+ if ( $hash{'last_bill'} ) {
+ push @changes, mt('Set the [_1] to [_2].', $name, $last_bill);
+ } else {
+ push @changes, mt('Remove the [_1].', $name);
+ }
+ push @confirm, '';
+ # I don't think we want to adjust this on supplemental packages.
+}
+
+# Bill date change
+if ( $hash{'bill'} != $cust_pkg->get('bill') ) {
+ my $bill = time2str($date_format, $hash{'bill'});
+ $bill = 'today' if !$hash{'bill'}; # or 'the end of today'?...
+ my $name = 'next bill date';
+ $name = 'end of the prepaid period' if $part_pkg->is_prepaid;
+ push @changes, mt('Set the [_1] to [_2].', $name, $bill);
+
+ if ( $hash{'bill'} < time and $hash{'bill'} ) {
+ push @confirm,
+ mt('The customer will be charged for the interval from [_1] until now.',
+ $bill);
+ } elsif ( !$hash{'bill'} and ($hash{'last_bill'} or $hash{'setup'}) ) {
+ my $last_bill =
+ time2str($date_format, $hash{'last_bill'} || $hash{'setup'});
+ push @confirm,
+ mt('The customer will be charged for the interval from [_1] until now.',
+ $last_bill);
+ } else {
+ push @confirm, '';
+ }
+
+ if ( @supp_pkgs ) {
+ push @changes, '';
+ if ( $cust_pkg->get('bill') and $hash{'bill'} ) {
+ # the package already has a bill date, so adjust the dates
+ # of supplementals by the same interval
+ my $diff = $hash{'bill'} - $cust_pkg->get('bill');
+ my $sign = $diff < 0 ? -1 : 1;
+ $diff = $diff * $sign / 86400;
+ if ( $diff < 1 ) {
+ $diff = mt('[quant,_1,hour]', int($diff * 24));
+ } else {
+ $diff = mt('[quant,_1,day]', int($diff));
+ }
+ push @confirm,
+ mt('[_1] supplemental package will also be billed [_2] [_3].',
+ (@supp_pkgs > 1 ? 'Each' : 'The'),
+ $diff,
+ ($sign > 0 ? 'later' : 'earlier')
+ );
+ } else {
+ # the package hasn't been billed yet, or you've set bill = null
+ push @confirm,
+ mt('[_1] supplemental package will also be billed on [_2].',
+ (@supp_pkgs > 1 ? 'Each' : 'The'),
+ $bill
+ );
+ }
+ } #if @supp_pkgs
+
+ if ( $main_pkg ) {
+ push @changes, '';
+ push @confirm,
+ mt('This package is a supplemental package. The bill date of its '.
+ 'main package will not be adjusted.');
+ }
+}
+
+# Contract end change
+if ( $hash{'contract_end'} != $cust_pkg->get('contract_end') ) {
+ if ( $hash{'contract_end'} ) {
+ my $contract_end = time2str($date_format, $hash{'contract_end'});
+ push @changes,
+ mt('Set this package\'s contract end date to [_1]', $contract_end);
+ } else {
+ push @changes, mt('Remove this package\'s contract end date.');
+ }
+ if ( @supp_pkgs ) {
+ my $text = 'This change will also apply to ' .
+ (@supp_pkgs > 1 ?
+ 'all supplemental packages.':
+ 'the supplemental package.');
+ push @confirm, mt($text);
+ } else {
+ push @confirm, '';
+ }
+}
+
+my $title = '';
+if ( @errors ) {
+ $title = 'Error changing package dates';
+} else {
+ $title = 'Confirm date changes';
+}
+</%init>
+<& /elements/header-popup.html, { title => $title, etc => 'BGCOLOR=""' } &>
+<STYLE TYPE="text/css">
+.error {
+ color: #ff0000;
+ font-weight: bold;
+ text-align: center;
+}
+.confirm { color: #ff0000 }
+.button-container {
+ position: fixed;
+ bottom: 5px;
+ text-align: center;
+ width: 100%
+}
+</STYLE>
+<DIV STYLE="text-align: center; padding:1em">
+<% emt('Package #') %><B><% $pkgnum %></B>: <B><% $cust_pkg->part_pkg->pkg %></B><BR>
+% if ( @changes ) {
+ <% emt('The following changes will be made:') %>
+% } else {
+ <% emt('No changes will be made.') %>
+% }
+</DIV>
+<TABLE WIDTH="100%">
+% if ( @errors ) {
+% foreach my $error ( @errors ) {
+<TR>
+ <TD><IMG SRC="<%$p%>images/cross.png"></TD>
+ <TD CLASS="error"><% $error %></TD>
+</TR>
+% }
+% } else {
+% while (@changes, @confirm) {
+% my $text = shift @changes;
+% if (length $text) {
+<TR>
+ <TD><IMG SRC="<%$p%>images/tick.png"></TD>
+ <TD><% $text %></TD>
+</TR>
+% }
+% $text = shift @confirm;
+% if (length $text) {
+<TR>
+ <TD>
+ <INPUT TYPE="checkbox" NAME="areyousure" VALUE=1 onclick="submit_ready()">
+ </TD>
+ <TD CLASS="confirm"><% $text %></TD>
+</TR>
+% }
+% }
+% }
+</TABLE>
+%# action buttons
+<DIV CLASS="button-container">
+ <BUTTON TYPE="button" STYLE="width:145px" ID="submit_cancel"\
+ onclick="submit_cancel()">
+ <IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel
+ </BUTTON>
+% if (!@errors ) {
+ <BUTTON TYPE="button" STYLE="width:145px" ID="submit_continue"\
+ onclick="submit_continue()">
+ <IMG SRC="<%$p%>images/tick.png" ALT=""> Continue
+ </BUTTON>
+</DIV>
+% }
+<FORM NAME="DateEditForm" STYLE="display:none" TARGET="_parent" ACTION="<%$p%>edit/process/REAL_cust_pkg.cgi" METHOD="POST">
+% foreach (keys %hash) {
+<INPUT TYPE="hidden" NAME="<%$_%>" VALUE="<% $hash{$_} |h%>">
+% }
+</FORM>
+<SCRIPT>
+function submit_ready() {
+ var ready = true;
+ var checkboxes = document.getElementsByName('areyousure');
+ var i;
+ for (i=0; i < checkboxes.length; i++) {
+ if (! checkboxes[i].checked ) {
+ ready = false;
+ }
+ }
+ document.getElementById('submit_continue').disabled = !ready;
+ return ready;
+}
+function submit_cancel() {
+ parent.nd(1);
+}
+function submit_continue() {
+ if ( submit_ready() ) {
+ document.forms.DateEditForm.submit();
+ }
+}
+submit_ready();
+</SCRIPT>
+<& /elements/footer.html &>
-<% objToJson( \@return ) %>
+<% encode_json( \@return ) %>\
<%init>
my( $custnum, $prospectnum, $classnum ) = $cgi->param('arg');
<TR>
<TD ALIGN="right" VALIGN="top" STYLE="padding-top:3px">Message: </TD>
- <TD><& '/elements/htmlarea.html',
- 'field' => 'html_body',
- 'width' => 600 &></TD>
+ <TD><& /elements/htmlarea.html,
+ 'field' => 'html_body',
+ 'width' => 763,
+ &>
+ </TD>
</TR>
</TABLE>
-<% objToJson(\@exchanges) %>
+<% encode_json(\@exchanges) %>\
<%init>
my( $areacode, $svcpart ) = $cgi->param('arg');
-<% objToJson(\%hash) %>
+<% encode_json(\%hash) %>\
<%init>
my $locationnum = $cgi->param('arg');
my %hash = ();
%hash = map { $_ => $cust_location->$_() }
- qw( address1 address2 city county state zip country
- location_kind location_type location_number )
+ ( FS::cust_main->location_fields,
+ qw( location_kind location_type location_number )
+ )
if $cust_location;
</%init>
-<% objToJson(\@macs) %>
+<% encode_json(\@macs) %>\
<%init>
# XXX: this should be agent-virtualized / limited
my $inventory_class = $part_device->inventory_class;
die "devicepart $devicepart has no inventory" unless $inventory_class;
-my @inventory_item =
+my @macs =
+ map $_->item,
qsearch('inventory_item', { 'classnum' => $inventory_class->classnum } );
-my @macs;
-
-foreach my $inventory_item ( @inventory_item ) {
- push @macs, $inventory_item->item;
-}
-
</%init>
-<% objToJson( $return ) %>
+<% encode_json( $return ) %>\
<%init>
my $return;
--- /dev/null
+<& /elements/header.html, 'Manage customer email settings' &>
+<STYLE TYPE="text/css">
+.hidden { display: none }
+</STYLE>
+<& /elements/xmlhttp.html,
+ url => $p.'misc/xmlhttp-cust_main-email_search.html',
+ subs => ['email_search']
+&>
+<SCRIPT TYPE="text/javascript">
+
+function receive_search(result) {
+ var recs = JSON.parse(result);
+ var tbody = document.getElementById('tbody_results');
+ var j = tbody.rows.length;
+ for(var i = 0; i < j; i++) {
+ tbody.deleteRow(tbody.rows[i]);
+ }
+ if (recs.length > 0) {
+ for(var i = 0; i < recs.length; i++) {
+ var rec = recs[i];
+ var row = tbody.insertRow(i);
+ row.style.backgroundColor = (i % 2 ? '#eeeeee' : '#ffffff');
+
+ var cell = row.insertCell(0); // custnum
+ cell.appendChild( document.createTextNode(rec[0]) );
+ cell = row.insertCell(1); // customer name
+ cell.appendChild( document.createTextNode(rec[1]) );
+ cell = row.insertCell(2); // email
+ cell.appendChild( document.createTextNode(rec[2]) );
+
+ cell = row.insertCell(3); // invoice_email
+ var input = document.createElement('INPUT');
+ input.type = 'hidden';
+ input.name = 'custnum';
+ input.value = rec[0];
+ cell.appendChild(input);
+
+ input = document.createElement('INPUT');
+ input.type = 'checkbox';
+ input.name = 'custnum' + rec[0] + '_invoice_email';
+ input.value = 'Y';
+ input.checked = (rec[3] != 'Y');
+ cell.appendChild(input);
+ cell.style.textAlign = 'center';
+
+ cell = row.insertCell(4); // message_email
+ input = document.createElement('INPUT');
+ input.type = 'checkbox';
+ input.name = 'custnum' + rec[0] + '_message_email';
+ input.value = 'Y';
+ input.checked = (rec[4] != 'Y');
+ cell.appendChild(input);
+ cell.style.textAlign = 'center';
+ }
+ document.getElementById('div_found').style.display = '';
+ } else {
+ document.getElementById('div_notfound').style.display = '';
+ }
+}
+
+function start_search() {
+ document.getElementById('div_found').style.display = 'none';
+ document.getElementById('div_notfound').style.display = 'none';
+ var email = document.getElementById('input_email').value;
+ email_search(email, receive_search);
+}
+% if ( $cgi->param('search') ) {
+window.onload = start_search;
+% }
+</SCRIPT>
+<FORM ACTION="<%$p%>misc/process/manage_cust_email.html" METHOD="POST">
+<DIV>
+% if ( $cgi->param('done') ) {
+<P STYLE="font-weight: bold; color: #00ff00">Changes saved.</P>
+% } elsif ( $cgi->param('error') ) {
+<P STYLE="font-weight: bold; color: #ff0000"><% $cgi->param('error') |h %></P>
+% }
+ Email address:
+ <INPUT TYPE="text" ID="input_email" NAME="search"\
+ VALUE="<% $cgi->param('search') |h %>">
+ <INPUT TYPE="button" onclick="start_search()" VALUE="find">
+</DIV>
+<DIV ID="div_notfound" STYLE="display: none; padding: 1em">
+No matching email addresses found.
+</DIV>
+<DIV ID="div_found" STYLE="display: none">
+<TABLE CLASS="grid" STYLE="border-spacing: 0px">
+ <THEAD>
+ <TR STYLE="background-color: #dddddd">
+ <TH>#</TH>
+ <TH>Customer</TH>
+ <TH>Email</TH>
+ <TH>Send invoices</TH>
+ <TH>Send other notices</TH>
+ </TR>
+ </THEAD>
+ <TBODY ID="tbody_results"></TBODY>
+</TABLE>
+<INPUT TYPE="submit" VALUE="Save changes">
+</FORM>
+<& /elements/footer.html &>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Edit customer');
+
+</%init>
&>
% }
+<& /elements/tr-select-contact.html,
+ 'cgi' => $cgi,
+ 'cust_main' => $cust_main,
+ 'prospect_main' => $prospect_main,
+&>
+
% if ( $cgi->param('lock_locationnum') ) {
<INPUT TYPE = "hidden"
<& /elements/standardize_locations.html,
'form' => "OrderPkgForm",
- 'onlyship' => 1,
- 'no_company' => 1,
- 'no_census' => 1,
'callback' => 'document.OrderPkgForm.submit();',
&>
--- /dev/null
+<& /elements/header-popup.html, 'Import SIMs' &>
+Import a file containing SIM card properties.<BR>
+Each row should contain the following fields, separated by spaces:<BR>
+IMSI, ICCID, PIN1, PUK1, PIN2, PUK2, ACC, Ki<BR>
+<BR>
+<& /elements/form-file_upload.html,
+ 'name' => 'ImportForm',
+ 'action' => 'process/huawei_hlr-import_sim.html',
+ 'num_files' => 1,
+ 'fields' => [ 'exportnum', 'classnum', 'agentnum', ],
+ 'message' => 'Inventory import successful',
+ 'onsubmit' => "document.ImportForm.submitButton.disabled=true;",
+&>
+<TABLE CLASS="inv" WIDTH="100%">
+ <INPUT TYPE="hidden" NAME="exportnum" VALUE="<%$exportnum%>">
+ <& /elements/file-upload.html,
+ 'field' => 'file',
+ 'label' => 'Filename',
+ &>
+ <& /elements/tr-select-agent.html,
+ 'disable_empty' => 1,
+ &>
+ <& /elements/tr-select-table.html,
+ 'table' => 'inventory_class',
+ 'name_col' => 'classname',
+ 'label' => 'Inventory class',
+ 'disable_empty' => 1,
+ &>
+
+ <TR>
+ <TD COLSPAN=2 ALIGN="center" STYLE="padding-top:6px">
+ <INPUT TYPE = "submit"
+ NAME = "submitButton"
+ ID = "submitButton"
+ VALUE = "Import file"
+ >
+ </TD>
+ </TR>
+
+</TABLE>
+
+</FORM>
+
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my ($exportnum) = $cgi->keywords;
+$exportnum =~ /^\d+$/ or die "bad exportnum '$exportnum'";
+my $part_export = FS::part_export->by_key($exportnum)
+ or die "export $exportnum not found";
+</%init>
--- /dev/null
+<% $server->process %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $server = new FS::UI::Web::JSRPC
+ 'FS::part_export::huawei_hlr::process_import_sim', $cgi;
+
+</%init>
-<% objToJson(\@output) %>
+<% encode_json(\@output) %>\
<%init>
my $conf = new FS::Conf;
-<% objToJson(\@phonenums) %>
+<% encode_json(\@phonenums) %>\
<%init>
my( $exchangestring, $svcpart ) = $cgi->param('arg');
--- /dev/null
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+$cgi->param('svcnum') =~ /^(\d+)$/ or die "illegal svcnum";
+my $svcnum = $1;
+my $svc_acct = FS::svc_acct->by_key($svcnum)
+ or die "svc_acct $svcnum not found";
+my $part_svc = $svc_acct->part_svc;
+die "access denied" unless (
+ $curuser->access_right('Provision customer service') or
+ ( $curuser->access_right('Edit password') and
+ ! $part_svc->restrict_edit_password )
+ );
+my $error = $svc_acct->set_password($cgi->param('password'))
+ || $svc_acct->replace;
+
+# annoyingly specific to view/svc_acct.cgi, for now...
+$cgi->delete('password');
+</%init>
+% if ( $error ) {
+% $cgi->param('svcnum', $svcnum);
+% $cgi->param("changepw${svcnum}_error", $error);
+% } else {
+% $cgi->query_string($svcnum);
+% }
+<% $cgi->redirect($fsurl.'view/svc_acct.cgi?'.$cgi->query_string) %>
--- /dev/null
+<% header(emt("Package contact $past_method")) %>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+ </BODY>
+</HTML>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Change customer package');
+
+#untaint pkgnum
+my $pkgnum = $cgi->param('pkgnum');
+$pkgnum =~ /^(\d+)$/ or die "Illegal pkgnum";
+$pkgnum = $1;
+
+my $cust_pkg = qsearchs( 'cust_pkg', {'pkgnum'=>$pkgnum} ); #needs agent virt
+
+my $contactnum = $cgi->param('contactnum');
+$contactnum =~ /^(-?\d*)$/ or die "Illegal contactnum";
+$contactnum = $1;
+
+my $past_method = $cust_pkg->contactnum ? 'changed' : 'added';
+
+my $error = '';
+
+if ( $contactnum == -1 ) {
+
+ #little false laziness w/edit/process/quick-cust_pkg.cgi, also the whole
+ # thing should be a single transaction
+ my $contact = new FS::contact {
+ 'custnum' => $cust_pkg->custnum,
+ map { $_ => scalar($cgi->param("contactnum_$_")) } qw( first last )
+ };
+ $error = $contact->insert;
+ $cust_pkg->contactnum( $contact->contactnum );
+
+} else {
+ $cust_pkg->contactnum($contactnum);
+}
+
+$error ||= $cust_pkg->replace;
+
+if ($error) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "change_pkg_contact.html?". $cgi->query_string );
+}
+
+</%init>
--- /dev/null
+<% $cgi->redirect($fsurl.'misc/manage_cust_email.html?' .
+ $cgi->query_string) %>
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Edit customer');
+
+my $error;
+foreach my $custnum ($cgi->param('custnum')) {
+ my $cust = FS::cust_main->by_key($custnum)
+ or die "customer not found: $custnum\n";
+ my $new_invoice_noemail =
+ $cgi->param('custnum'.$custnum.'_invoice_email') ? '' : 'Y';
+ my $new_message_noemail =
+ $cgi->param('custnum'.$custnum.'_message_email') ? '' : 'Y';
+ if ( $new_invoice_noemail ne $cust->invoice_noemail
+ or $new_message_noemail ne $cust->message_noemail ) {
+
+ $cust->set('invoice_noemail', $new_invoice_noemail);
+ $cust->set('message_noemail', $new_message_noemail);
+ $error ||= $cust->replace;
+
+ }
+ $cgi->delete('custnum'.$custnum.'_invoice_email');
+ $cgi->delete('custnum'.$custnum.'_message_email');
+}
+$cgi->delete('custnum');
+if ( $error ) {
+ $cgi->param('error' => $error); # probably unnecessary...
+} else {
+ $cgi->param('done' => 1) unless $error;
+}
+</%init>
$new->set( 'paycvv' => '');
}
- $new->set( $_ => $cgi->param($_) ) foreach @{$payby2fields{$payby}};
+ if ( $payby eq 'CARD' ) {
+ my $bill_location = FS::cust_location->new;
+ $bill_location->set( $_ => $cgi->param($_) )
+ foreach @{$payby2fields{$payby}};
+ $new->set('bill_location' => $bill_location);
+ # will do nothing if the fields are all unchanged
+ } else {
+ $new->set( $_ => $cgi->param($_) ) foreach @{$payby2fields{$payby}};
+ }
my $error = $new->replace($cust_main);
errorpage("payment processed successfully, but error saving info: $error")
-<% objToJson(\@regions) %>
+<% encode_json(\@regions) %>\
<%init>
my( $state, $svcpart ) = $cgi->param('arg');
-<% encode_json($return) %>
+<% encode_json($return) %>\
<%init>
local $SIG{__DIE__}; #disable Mason error trap
my %new;
-my @prefixes;
-if ($old{onlyship}) {
- @prefixes = ('ship_');
-} elsif ( $old{same} ) {
+my @prefixes = ('');
+if ( $old{same} ) {
@prefixes = ('bill_');
-} else {
+} elsif ( $old{billship} ) {
@prefixes = ('bill_', 'ship_');
}
my $all_same = 1;
$all_same = 0 if ( $new{$pre.$_} ne $old{$pre.$_} );
last if !$all_same;
}
+
+ $all_same = 0 if $new{$pre.'error'};
}
my $return = { old => \%old, new => \%new, all_same => $all_same };
-<% objToJson($return) %>
+<% encode_json($return) %>\
<%init>
my $DEBUG = 0;
-<% encode_json(\@return) %>
+<% encode_json(\@return) %>\
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
my @return;
if ( $cgi->param('sub') eq 'custnum_search_open' ) {
my $custnum = $cgi->param('arg');
- #warn "searching invoices for $custnum\n";
- my $cust_main = FS::cust_main->by_key($custnum);
- @return = map {
- +{ $_->hash,
- 'owed' => $_->owed }
- } $cust_main->open_cust_bill
- if $curuser->agentnums_href->{ $cust_main->agentnum };
+ if ( $custnum =~ /^(\d+)$/ ) {
+#warn "searching invoices for $custnum\n";
+ my $cust_main = FS::cust_main->by_key($custnum);
+ @return = map {
+ +{ $_->hash,
+ 'owed' => $_->owed }
+ } $cust_main->open_cust_bill
+ if $curuser->agentnums_href->{ $cust_main->agentnum };
+ }
}
</%init>
-<% to_json($return) %>
+<% encode_json($return) %>\
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
-die "access denied" unless $curuser->access_right('Post credit');
+die "access denied" unless $curuser->access_right('Credit line items');
my $DEBUG = 0;
-<% objToJson($return) %>
+<% encode_json($return) %>\
<%init>
my %arg = $cgi->param('arg');
% }
% }
%
-<% objToJson($return) %>
+<% encode_json($return) %>\
% }
<%init>
--- /dev/null
+<% encode_json(\@result) %>\
+<%init>
+die 'access denied'
+ unless $FS::CurrentUser::CurrentUser->access_right('Edit customer');
+
+my $sub = $cgi->param('sub');
+my $email = $cgi->param('arg');
+my @where = (
+ "cust_main_invoice.dest != 'POST'",
+ "cust_main_invoice.dest LIKE ".dbh->quote('%'.$email.'%'),
+ $FS::CurrentUser::CurrentUser->agentnums_sql(table => 'cust_main'),
+);
+my @cust_main = qsearch({
+ 'table' => 'cust_main',
+ 'select' => 'cust_main.*, cust_main_invoice.dest',
+ 'addl_from' => 'JOIN cust_main_invoice USING (custnum)',
+ 'extra_sql' => 'WHERE '.join(' AND ', @where),
+});
+
+my @result = map {
+ [ $_->custnum,
+ $_->name,
+ $_->dest,
+ $_->invoice_noemail,
+ $_->message_noemail,
+ ]
+} @cust_main;
+
+</%init>
% # cust_main-agent_custid-format') eq 'ww?d+'
% $return = findbycustnum_or_agent_custid($1);
% }
-<% objToJson($return) %>
+<% encode_json($return) %>\
% } elsif ( $sub eq 'smart_search' ) {
%
% my $string = $cgi->param('arg');
% @cust_main
% ];
%
-<% objToJson($return) %>
+<% encode_json($return) %>\
% } elsif ( $sub eq 'invnum_search' ) {
%
% my $string = $cgi->param('arg');
% if ( $string =~ /^(\d+)$/ ) {
% my $inv = qsearchs('cust_bill', { 'invnum' => $1 });
% my $return = $inv ? findbycustnum($inv->custnum) : [];
-<% objToJson($return) %>
+<% encode_json($return) %>\
% } else { #return nothing
[]
% }
% city => $_->city,
% };
% }
-<% objToJson($return) %>
+<% encode_json($return) %>\
% }
<%init>
-<% objToJson($return) %>
+<% encode_json($return) %>\
<%init>
my $conf = new FS::Conf;
--- /dev/null
+% if ( $sub eq 'smart_search' ) {
+%
+% my $string = $cgi->param('arg');
+% my @svc_broadband = FS::svc_broadband->smart_search( $string );
+% my $return = [ map { my $cust_pkg = $_->cust_svc->cust_pkg;
+% [ $_->svcnum,
+% $_->label. ( $cust_pkg
+% ? ' ('. $cust_pkg->cust_main->name. ')'
+% : ''
+% ),
+% ];
+% }
+% @svc_broadband,
+% ];
+%
+<% encode_json($return) %>\
+% }
+<%init>
+
+my $sub = $cgi->param('sub');
+
+</%init>
#XXX autogen
my @paramlist = qw( locale menu_position default_customer_view
+ history_order
spreadsheet_format mobile_menu
enable_fuzzy_on_exact
disable_html_editor disable_enter_submit_onetimecharge
vonage-fromnumber vonage-username vonage-password
cust_pkg-display_times
show_pkgnum show_confitem_counts export_getsettings
- show_db_profile save_db_profile
+ show_db_profile save_db_profile save_tmp_typesetting
height width availHeight availWidth colorDepth
);
</SELECT>
</TD>
</TR>
+
+% my $history_order = $curuser->option('history_order') || 'oldest';
+ <TR>
+ <TH ALIGN="right">Customer history sort order: </TH>
+ <TD COLSPAN=2>
+ <& /elements/select.html,
+ field => 'history_order',
+ curr_value => $history_order,
+ options => [ 'oldest', 'newest' ],
+ labels => { 'oldest' => 'Oldest first',
+ 'newest' => 'Newest first',
+ },
+ &>
+ </TD>
+ </TR>
<TR>
<TH ALIGN="right">Spreadsheet download format: </TH>
</TR>
<TR>
- <TH ALIGN="right" COLSPAN=1>Enable approximate customer searching even when an exact match is found: </TH>
+ <TH ALIGN="right" COLSPAN=1>Enable approximate customer searching <BR>even when an exact match is found: </TH>
<TD ALIGN="left" COLSPAN=2>
<INPUT TYPE="checkbox" NAME="enable_fuzzy_on_exact" VALUE="1" <% $curuser->option('enable_fuzzy_on_exact') ? 'CHECKED' : '' %>>
</TD>
<TH>Save database profiling logs (when available): </TH>
<TD><INPUT TYPE="checkbox" NAME="save_db_profile" VALUE="1" <% $curuser->option('save_db_profile') ? 'CHECKED' : '' %>></TD>
</TR>
+ <TR>
+ <TH>Save temporary invoice typesetting files: </TH>
+ <TD><INPUT TYPE="checkbox" NAME="save_tmp_typesetting" VALUE="1" <% $curuser->option('save_tmp_typesetting') ? 'CHECKED' : '' %>></TD>
+ </TR>
</TABLE>
<BR>
<Form_477_submission xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://specialreports.fcc.gov/wcb/Form477/XMLSchema-instance/form_477_upload_Schema.xsd" >
% } else { #html
<& /elements/header.html, "FCC Form 477 Results - $state" &>
+%# XXX when we stop supporting IE8, add this to freeside.css using :nth-child
+%# selectors, and remove it from everywhere else
+<STYLE TYPE="text/css">
+.grid TH { background-color: #cccccc; padding: 0px 3px 2px; text-align: right }
+.row0 TD { background-color: #eeeeee; padding: 0px 3px 2px; text-align: right }
+.row1 TD { background-color: #ffffff; padding: 0px 3px 2px; text-align: right }
+</STYLE>
+
<TABLE WIDTH="100%">
<TR>
<TD></TD>
% if ( $type eq 'xml' ) {
<<% 'Part_IA_'. chr(65 + $tech) %>>
% }
-<& "477part${part}_summary.html", 'tech_code' => $tech, 'url' => $url &>
-<& "477part${part}_detail.html", 'tech_code' => $tech, 'url' => $url &>
+<& "477part${part}.html",
+ 'tech_code' => $tech,
+ 'url' => $url,
+ 'type' => $type
+&>
% if ( $type eq 'xml' ) {
</<% 'Part_IA_'. chr(65 + $tech) %>>
% }
&FS::Report::FCC_477::save_fcc477map("part2b_row_option_$i",$part2b_row_option[$i]);
}
+my $part5_report_option = $cgi->param('part5_report_option');
+if ( $part5_report_option ) {
+ FS::Report::FCC_477::save_fcc477map('part5_report_option', $part5_report_option);
+}
+
my $url_mangler = sub {
my $part = shift;
my $url = $cgi->url('-path_info' => 1, '-full' => 1);
--- /dev/null
+% if ( $opt{'type'} eq 'xml' ) {
+%# container element <Part_IA_$tech> is in 477.html
+% my $col = 'a';
+% foreach ( @summary_row ) {
+% my $el = $xml_prefix . $col . '1'; # PartIA_Aa1, PartIA_Ab1, etc.
+ <<% $el %>><% $_ %><<% "/$el" %>>
+% $col++;
+% }
+% foreach my $col_data ( @data ) {
+% my $row = 1;
+% foreach my $cell ( @$col_data ) {
+% my $el = $xml_prefix . $col . $row; # PartIA_Af1, PartIA_Af2...
+ <<% $el %>><% $cell->[0] %><<% "/$el" %>>
+% if ( $percentages ) {
+% $el = $xml_percent . $col . $row; # Part_p_IA_Af1, ...
+ <<% $el %>><% $cell->[1] %><<% "/$el" %>>
+% }
+% $row++;
+% } # foreach $cell
+% $col++;
+% } # foreach $col_data
+% } else { # not XML
+
+<H2><% $title %> totals</H2>
+<& /elements/table-grid.html &>
+ <TR>
+% foreach ( 'Total Connections',
+% '% owned loop',
+% '% billed to end users',
+% '% residential',
+% '% residential > 200 kbps') {
+ <TH WIDTH="20%"><% $_ |h %></TH>
+% }
+ </TR>
+ <TR CLASS="row0">
+% foreach ( @summary_row ) {
+ <TD><% $_ %></TD>
+% }
+ </TR>
+</TABLE>
+<H2><% $title %> breakdown by speed</H2>
+<TABLE CLASS="grid" CELLSPACING=0>
+ <TR>
+ <TH WIDTH="12%"></TH>
+% for (my $col = 0; $col < scalar(@download_option); $col++) {
+ <TH WIDTH="11%">
+ <% $FS::Report::FCC_477::download[$col] |h %>
+ </TH>
+% }
+ </TR>
+% for (my $row = 0; $row < scalar(@upload_option); $row++) {
+ <TR CLASS="row<% $row % 2%>">
+ <TD STYLE="text-align: left; font-weight: bold">
+% if ( $asymmetric ) {
+ <% $FS::Report::FCC_477::upload[$row] |h %>
+% }
+ </TD>
+% for (my $col = 0; $col < scalar(@download_option); $col++) {
+ <TD>
+ <% $data[$col][$row][0] %>
+% if ( $percentages ) {
+ <BR><% $data[$col][$row][1] %>
+% }
+ </TD>
+% } # for $col
+ </TR>
+% } # for $row
+</TABLE>
+% }
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('List packages');
+
+my %opt = @_;
+my %search_hash;
+
+for ( qw(agentnum state) ) {
+ $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
+}
+$search_hash{'status'} = 'active';
+$search_hash{'country'} = 'US';
+$search_hash{'classnum'} = [ $cgi->param('classnum') ];
+
+# arrays of report_option_ numbers, running parallel to
+# the download and upload speed arrays
+my @download_option = $cgi->param('part1_column_option');
+my @upload_option = $cgi->param('part1_row_option');
+
+my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi);
+
+my $total_count = 0;
+my $total_residential = 0;
+my $above_200 = 0;
+my $tech_code = $opt{tech_code};
+my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown';
+my $title = "Part IA $technology";
+my $xml_prefix = 'PartIA_'. chr(65 + $tech_code);
+my $xml_percent = 'Part_p_IA_'. chr(65 + $tech_code); # yes, seriously
+
+# whether to show the results as a matrix (upload speeds in rows) or a single
+# row
+my $asymmetric = 1;
+if ( $technology eq 'Symmetric xDSL' or $technology eq 'Other Wireline' ) {
+ $asymmetric = 0;
+ @upload_option = ( undef );
+}
+# whether to show residential percentages in each cell of the matrix
+my $percentages = ($technology eq 'Terrestrial Mobile Wireless');
+
+my $query = FS::cust_pkg->search(\%search_hash);
+my $count_query = $query->{'count_query'};
+
+my $is_residential = " AND COALESCE(cust_main.company, '') = ''";
+my $has_option = sub {
+ my $optionnum = shift;
+ $optionnum =~ /^\d+$/ ?
+ " AND EXISTS(
+ SELECT 1 FROM part_pkg_option
+ WHERE part_pkg_option.pkgpart = part_pkg.pkgpart
+ AND optionname = 'report_option_$optionnum'
+ AND optionvalue = '1'
+ )" : '';
+};
+
+# limit to those that have technology option $tech_code
+$count_query .= $has_option->($technology_option[$tech_code]);
+
+my @data;
+for ( my $row = 0; $row < scalar @upload_option; $row++ ) {
+ for ( my $col = 0; $col < scalar @download_option; $col++ ) {
+
+ my $this_count_query = $count_query .
+ $has_option->($upload_option[$row]) .
+ $has_option->($download_option[$col]);
+
+ my $count = FS::Record->scalar_sql($this_count_query);
+ my $residential = FS::Record->scalar_sql($this_count_query . $is_residential);
+
+ my $percent = sprintf('%.2f', $count ? 100 * $residential / $count : 0);
+ $data[$col][$row] = [ $count, $percent ];
+
+ $total_count += $count;
+ $total_residential += $residential;
+ $above_200 += $residential if $row > 0 or !$asymmetric;
+ }
+}
+
+my $total_percentage =
+ sprintf("%.2f", $total_count ? 100*$total_residential/$total_count : 0);
+
+my $above_200_percentage =
+ sprintf("%.2f", $total_count ? 100*$above_200/$total_count : 0);
+
+my @summary_row = (
+ $total_count,
+ 100.00, # own local loop--consistent with previous practice, but probably wrong
+ 100.00, # billed to end user--also wrong
+ $total_percentage, # residential percentage
+ $above_200_percentage,
+);
+
+</%init>
+++ /dev/null
-<% include( 'elements/search.html',
- 'html_init' => $html_init,
- 'name' => 'lines',
- 'query' => $query,
- 'count_query' => $count_query,
- 'really_disable_download' => 1,
- 'disable_download' => 1,
- 'nohtmlheader' => 1,
- 'disable_total' => 1,
- 'header' => [ '', @column_option_name ],
- 'xml_elements' => [ @xml_elements ],
- 'xml_omit_empty' => 1,
- 'fields' => [ @fields ],
- )
-%>
-<%init>
-
-my $curuser = $FS::CurrentUser::CurrentUser;
-
-die "access denied"
- unless $curuser->access_right('List packages');
-
-my %opt = @_;
-my %search_hash = ();
-
-for ( qw(agentnum magic state) ) {
- $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
-}
-$search_hash{'country'} = 'US';
-
-$search_hash{'classnum'} = [ $cgi->param('classnum') ];
-
-my @column_option = grep { /^\d+/ } $cgi->param('part1_column_option')
- if $cgi->param('part1_column_option');
-
-my @row_option = grep { /^\d+/ } $cgi->param('part1_row_option')
- if $cgi->param('part1_row_option');
-
-my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi);
-
-my @column_option_name = scalar(@column_option)
- ? ( map { my $part_pkg_report_option =
- qsearchs({ 'table' => 'part_pkg_report_option',
- 'hashref' => { num => $_ },
- });
- $part_pkg_report_option ? $part_pkg_report_option->name
- : 'no such report option';
- } @column_option
- )
- : ( 'all packages' );
-
-my $where = join(' OR ', map { "num = $_" } @row_option );
-my %row_option_name = $where ?
- ( map { $_->num => $_->name }
- qsearch({ 'table' => 'part_pkg_report_option',
- 'hashref' => {},
- 'extra_sql' => "WHERE $where",
- })
- ) :
- ();
-
-my $tech_code = $opt{tech_code};
-my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown';
-my $html_init = "<H2>Part IA $technology breakdown by speeds</H2>";
-my $xml_prefix = 'PartIA_'. chr(65 + $tech_code);
-
-if ($cgi->param('_type') eq 'xml') {
- #rotate data pi/2
- my @temp = @column_option;
- @column_option = @row_option;
- @row_option = @temp;
-}
-
-my $query = 'SELECT '. join(' UNION ALL SELECT ',@row_option);
-my $count_query = 'SELECT '. scalar(@row_option);
-
-my $xml_element = 'OOPS, I was never set';
-my $rowchar = 101; # 'e' -- rows are columns! (pi/2)
-
-my $value = sub {
- my ($rowref, $column) = (shift, shift);
- my $row = $rowref->[0];
-
- if ($column eq 'name') {
- return $row_option_name{$row} || 'no such report option';
- } elsif ( $column =~ /^(\d+)$/ ) {
- my @report_option = ( $row || '',
- $column_option[$column] || '',
- $technology_option[$tech_code] || '',
- );
-
- my ( $count, $residential ) = FS::cust_pkg->fcc_477_count(
- { %search_hash, 'report_option' => join(',', @report_option) }
- );
-
- my $percentage = sprintf('%.2f', $count ? 100 * $residential / $count : 0);
- my $return = $count;
-
- if ($cgi->param('_type') eq 'xml') {
- $rowchar++ if $column == 0;
- $xml_element = $xml_prefix. chr($rowchar). ($column+1);
- $return = '' if $count == 0 and $cgi->param('_type') eq 'xml';
- } else {
- $return .= "<BR>$percentage% residential";
- }
-
- return $return;
- } else {
- return '<FONT SIZE="+1" COLOR="#ff0000">Bad call to column_value</FONT>';
- }
-};
-
-my @fields = map { my $ci = $_; sub { &{$value}(shift, $ci); } }
- ( 'name', (0 .. $#column_option) );
-shift @fields if $cgi->param('_type') eq 'xml';
-
-my @xml_elements = ( # -- columns are rows! (pi/2)
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
-);
-
-</%init>
+++ /dev/null
-<% include( 'elements/search.html',
- 'html_init' => $html_init,
- 'name' => 'lines',
- 'query' => 'SELECT 1',
- 'count_query' => 'SELECT 1',
- 'really_disable_download' => 1,
- 'disable_download' => 1,
- 'nohtmlheader' => 1,
- 'disable_total' => 1,
- 'header' => [
- 'Total Connections',
- '% owned loop',
- '% billed to end users',
- '% residential',
- '% residential > 200kbps',
- ],
- 'xml_elements' => [
- $xml_prefix. 'a1',
- $xml_prefix. 'b1',
- $xml_prefix. 'c1',
- $xml_prefix. 'd1',
- $xml_prefix. 'e1',
- ],
- 'fields' => [
- sub { $total_count },
- sub { '100.00' },
- sub { '100.00' },
- sub { $total_percentage },
- sub { $above_200_percentage },
- ],
- )
-%>
-<%init>
-
-my $curuser = $FS::CurrentUser::CurrentUser;
-
-die "access denied"
- unless $curuser->access_right('List packages');
-
-my %opt = @_;
-my %search_hash = ();
-
-for ( qw(agentnum magic state) ) {
- $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
-}
-$search_hash{'country'} = 'US';
-$search_hash{'classnum'} = [ $cgi->param('classnum') ];
-
-my @column_option = grep { /^\d+$/ } $cgi->param('part1_column_option')
- if $cgi->param('part1_column_option');
-
-my @row_option = grep { /^\d+$/ } $cgi->param('part1_row_option')
- if $cgi->param('part1_row_option');
-
-my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi);
-
-my $total_count = 0;
-my $total_residential = 0;
-my $above_200 = 0;
-my $tech_code = $opt{tech_code};
-my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown';
-my $html_init = "<H2>Part IA $technology totals</H2>";
-my $xml_prefix = 'PartIA_'. chr(65 + $tech_code);
-
-my $not_first_row = 0; # ugh;
-foreach my $row ( @row_option ) {
- foreach my $column ( @column_option ) {
-
- my @report_option = ( $row || '-1', $column || '-1', $technology_option[$tech_code] );
-
- my ( $count, $residential ) = FS::cust_pkg->fcc_477_count(
- { %search_hash, 'report_option' => join(',', @report_option) }
- );
-
- $total_count += $count;
- $total_residential += $residential;
- $above_200 += $residential if $not_first_row;
- }
- $not_first_row++;
-}
-
-my $total_percentage =
- sprintf("%.2f", $total_count ? 100*$total_residential/$total_count : 0);
-
-my $above_200_percentage =
- sprintf("%.2f", $total_count ? 100*$above_200/$total_count : 0);
-
-
-</%init>
-<% include( 'elements/search.html',
- 'html_init' => $html_init,
- 'name' => 'lines',
- 'query' => $query,
- 'count_query' => 'SELECT 11',
- 'really_disable_download' => 1,
- 'disable_download' => 1,
- 'nohtmlheader' => 1,
- 'disable_total' => 1,
- 'header' => [ @headers ],
- 'xml_elements' => [ @xml_elements ],
- 'fields' => [ @fields ],
- )
-%>
+% if ( $cgi->param('_type') eq 'xml' ) {
+% my @cols = qw(a b c d);
+% for ( my $row = 0; $row < scalar(@rows); $row++ ) {
+% for my $col (0..3) {
+% if ( exists($data[$col][$row]) and $data[$col][$row] > 0 ) {
+<PartII_<% $row + 1 %><% $cols[$col] %>>\
+<% $data[$col][$row] %>\
+</PartII_<% $row + 1 %><% $cols[$col] %>>
+% }
+% } #for $col
+% } #for $row
+% } else { # HTML mode
+% # fake up the search-html.html header
+<H2>Part IIA</H2>
+<TABLE>
+ <TR><TD VALIGN="bottom"><BR></TD></TR>
+ <TR><TD COLSPAN=2>
+ <TABLE CLASS="grid" CELLSPACING=0>
+ <TR>
+% foreach (@row1_headers) {
+ <TH><% $_ %></TH>
+% }
+ </TR>
+% my $row = 0;
+% foreach my $rowhead (@rows) {
+ <TR CLASS="row<%$row % 2%>">
+ <TD STYLE="text-align: left; font-weight: bold"><% $rowhead %></TD>
+% for my $col (0..3) {
+ <TD>
+% if ( exists($data[$col][$row]) ) {
+ <% $data[$col][$row] %>
+% }
+ </TD>
+% } # for $col
+ </TR>
+% $row++;
+% } #for $rowhead
+ </TABLE>
+ </TD></TR>
+</TABLE>
+% } #XML/HTML
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
die "access denied"
unless $curuser->access_right('List packages');
-my $html_init = '<H2>Part IIA</H2>';
my %search_hash = ();
-
-for ( qw(agentnum magic state) ) {
- $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
-}
-$search_hash{'country'} = 'US';
-$search_hash{'classnum'} = [ $cgi->param('classnum') ];
-
-my @row_option = grep { /^\d+$/ } $cgi->param('part2a_row_option')
- if $cgi->param('part2a_row_option');
-
-# fudge in two rows of LD carrier
-unshift @row_option, $row_option[0];
-
-# fudge in the first pair of rows
-unshift @row_option, '';
-unshift @row_option, '';
-
-my $query = 'SELECT '. join(' UNION SELECT ', 1..11);
-my $total_count = 0;
-my $column_value = sub {
- my $row = shift;
-
- my @report_option = ( $row_option[$row - 1] || '' );
-
- my $sql_query = FS::cust_pkg->search(
- { %search_hash, 'report_option' => join(',', @report_option) }
- );
-
- my $count_sql = delete($sql_query->{'count_query'});
- if ( $row == 2 || $row == 4 ) {
- $count_sql =~ s/COUNT\(\*\) FROM/sum(COALESCE(CASE WHEN cust_main.company IS NULL OR cust_main.company = '' THEN CASE WHEN part_pkg.fcc_ds0s IS NOT NULL AND part_pkg.fcc_ds0s > 0 THEN part_pkg.fcc_ds0s WHEN pkg_class.fcc_ds0s IS NOT NULL AND pkg_class.fcc_ds0s > 0 THEN pkg_class.fcc_ds0s ELSE 0 END ELSE 0 END, 0) ) FROM/
- or die "couldn't parse count_sql";
- } else {
- $count_sql =~ s/COUNT\(\*\) FROM/sum(COALESCE(CASE WHEN part_pkg.fcc_ds0s IS NOT NULL AND part_pkg.fcc_ds0s > 0 THEN part_pkg.fcc_ds0s WHEN pkg_class.fcc_ds0s IS NOT NULL AND pkg_class.fcc_ds0s > 0 THEN pkg_class.fcc_ds0s ELSE 0 END, 0)) FROM/
- or die "couldn't parse count_sql";
- }
-
- my $count_sth = dbh->prepare($count_sql)
- or die "Error preparing $count_sql: ". dbh->errstr;
- $count_sth->execute
- or die "Error executing $count_sql: ". $count_sth->errstr;
- my $count_arrayref = $count_sth->fetchrow_arrayref;
- my $count = $count_arrayref->[0];
+$search_hash{'agentnum'} = $cgi->param('agentnum');
+$search_hash{'state'} = $cgi->param('state');
+$search_hash{'classnum'} = [ $cgi->param('classnum') ];
+$search_hash{'status'} = 'active';
- $total_count = $count if $row == 1;
- $count = sprintf('%.2f', $total_count ? 100*$count/$total_count : 0)
- if $row != 1;
+my @row_option;
+foreach ($cgi->param('part2a_row_option')) {
+ push @row_option, (/^\d+$/ ? $_ : undef);
+}
- return "$count";
+my $is_residential = "AND COALESCE(cust_main.company, '') = ''";
+my $has_report_option = sub {
+ map {
+ defined($row_option[$_]) ?
+ " AND EXISTS(
+ SELECT 1 FROM part_pkg_option
+ WHERE part_pkg_option.pkgpart = part_pkg.pkgpart
+ AND optionname = 'report_option_" . $row_option[$_]."'
+ AND optionvalue = '1'
+ )" : ' AND FALSE'
+ } @_
};
-my @headers = (
- '',
- 'End user lines',
- 'UNE-P replacement',
- 'UNE (unswitched)',
- 'UNE-P',
+# an arrayref for each column
+my @data;
+# get the skeleton of the query
+my $sql_query = FS::cust_pkg->search(\%search_hash);
+my $from_where = $sql_query->{'count_query'};
+$from_where =~ s/^SELECT COUNT\(\*\) //;
+
+# for row 1
+my $query_ds0 = "SELECT SUM(COALESCE(part_pkg.fcc_ds0s, pkg_class.fcc_ds0s, 0))
+ $from_where AND fcc_voip_class = '4'"; # 4 = Local Exchange
+
+my $total_lines = FS::Record->scalar_sql($query_ds0);
+# always return zero for the number of resold lines, until an actual ILEC
+# starts using this report
+
+@data = (
+ [ $total_lines ],
+ [ 0 ],
+ [ 0 ],
+ [ 0 ],
);
-my @xml_elements = (
- sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}a" },
- sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}b" },
- sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}c" },
- sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}d" },
+my @row_conds = (
+ $is_residential,
+ $has_report_option->(0), # LD carrier
+ ($has_report_option->(0))[0] . $is_residential,
+ $has_report_option->(1..7),
);
+if ( $total_lines > 0 ) {
+ foreach (@row_conds) {
+ my $sql = $query_ds0 . $_;
+ my $lines = FS::Record->scalar_sql($sql);
+ my $percent = sprintf('%.2f', 100 * $lines / $total_lines);
+ push @{ $data[0] }, $percent;
+ }
+}
my @rows = (
'lines',
'% residential',
'% LD carrier',
- '% residential and LD carrier',
- '% own loops',
- '% obtained unswitched UNE loops',
+ '% residential and LD',
+ '% owned loops',
+ '% unswitched UNE',
'% UNE-P',
'% UNE-P replacement',
'% FTTP',
'% wireless',
);
-my @fields = (
- sub { my $row = shift; $rows[$row->[0] - 1]; },
- sub { my $row = shift; &{$column_value}($row->[0]); },
- sub { 0; },
- sub { 0; },
- sub { 0; },
+my @row1_headers = (
+ '',
+ 'End user lines',
+ 'UNE-P replacement',
+ 'unswitched UNE',
+ 'UNE-P',
);
-shift @fields if $cgi->param('_type') eq 'xml';
</%init>
% for ( my $row = 0; $row < scalar(@rows); $row++ ) {
% for my $col (0..2) {
% if ( exists($data[$col][$row]) ) {
-<PartII_<% $row %><% $cols[$col] %>>
+<PartII_<% $row + 1 %><% $cols[$col] %>>\
+<% $data[$col][$row] %>\
+</PartII_<% $row + 1 %><% $cols[$col] %>>
% }
-</PartII_<% $row %><% $cols[$col] %>>
% } #for $col
% } #for $row
% } else { # HTML mode
<TABLE>
<TR><TD VALIGN="bottom"><BR></TD></TR>
<TR><TD COLSPAN=2>
- <TABLE CLASS="grid" CELLSPACING=0 STYLE="border: 1px solid #cccccc;" BGCOLOR="#cccccc">
+ <TABLE CLASS="grid" CELLSPACING=0>
<TR>
% foreach (@headers) {
- <TH class="grid"><% $_ %></TH>
+ <TH><% $_ %></TH>
% }
</TR>
-% my @bgcolor = ('eeeeee','ffffff');
% my $row = 0;
% foreach my $rowhead (@rows) {
- <TR>
- <TD CLASS="grid" BGCOLOR="#<% $bgcolor[$row % 2] %>"><% $rowhead %></TD>
+ <TR CLASS="row<% $row % 2 %>">
+ <TD STYLE="text-align: left; font-weight: bold"><% $rowhead %></TD>
% for my $col (0..2) {
- <TD CLASS="grid" BGCOLOR="#<% $bgcolor[$row % 2] %>">
+ <TD>
% if ( exists($data[$col][$row]) ) {
<% $data[$col][$row] %>
% }
-<% include( 'elements/search.html',
+% if ( $cgi->param('_type') =~ /^xml$/ ) {
+<zip_code>
+% }
+<& elements/search.html,
'html_init' => $html_init,
'name' => 'zip code',
'query' => $sql_query,
'url' => $opt{url} || '',
'really_disable_download' => 1,
- )
-%>
+
+&>
+% if ( $cgi->param('_type') =~ /^xml$/ ) {
+</zip_code>
+% }
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
}
$search_hash{'country'} = 'US';
$search_hash{'classnum'} = [ $cgi->param('classnum') ];
-$search_hash{report_option} = $cgi->param('partv_report_option')
- if $cgi->param('partv_report_option');
+$search_hash{report_option} = $cgi->param('part5_report_option')
+ if $cgi->param('part5_report_option');
my $sql_query = FS::cust_pkg->search( { %search_hash,
'fcc_line' => 1,
-<% include( 'elements/search.html',
+<& elements/search.html,
'html_init' => '<H2>Part VI</H2>',
'html_foot' => $html_foot,
'name' => 'regions',
'url' => $opt{url} || '',
'xml_row_element' => 'Datarow',
'really_disable_download' => 1,
- )
-%>
+
+&>
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
--- /dev/null
+%# still not a good way to do rows grouped by some field in a search.html
+%# report
+% if ( $type eq 'xls' ) {
+<% $data %>\
+% } else {
+<& /elements/header.html, $title &>
+<P ALIGN="right" CLASS="noprint">
+Download full results<BR>
+as <A HREF="<% $cgi->self_url %>;_type=xls">Excel spreadsheet</A></P>
+<BR>
+<STYLE TYPE="text/css">
+td.cust_head {
+ border-left: none;
+ border-right: none;
+ padding-top: 0.5em;
+ font-weight: bold;
+ background-color: #ffffff;
+}
+td.money { text-align: right; }
+td.money:before { content: '<% $money_char %>'; }
+.row0 { background-color: #eeeeee; }
+.row1 { background-color: #ffffff; }
+</STYLE>
+<& /elements/table-grid.html &>
+ <TR STYLE="background-color: #cccccc">
+ <TH CLASS="grid">Package</TH>
+ <TH CLASS="grid">Sales</TH>
+ <TH CLASS="grid">Percentage</TH>
+ <TH CLASS="grid">Commission</TH>
+ </TR>
+% my ($custnum, $sales, $commission, $row, $bgcolor) = (0, 0, 0, 0);
+% foreach my $cust_pkg ( @cust_pkg ) {
+% if ( $custnum ne $cust_pkg->custnum ) {
+% # start of a new customer section
+% my $cust_main = $cust_pkg->cust_main;
+% my $label = $cust_main->custnum . ': '. $cust_main->name;
+% $bgcolor = 0;
+ <TR>
+ <TD COLSPAN=4 CLASS="cust_head">
+ <A HREF="<%$p%>view/cust_main.cgi?<%$cust_main->custnum%>"><% $label %></A>
+ </TD>
+ </TR>
+% }
+ <TR CLASS="row<% $bgcolor %>">
+ <TD CLASS="grid"><% $cust_pkg->pkg_label %></TD>
+ <TD CLASS="money"><% sprintf('%.2f', $cust_pkg->sum_charged) %></TD>
+ <TD ALIGN="right"><% $cust_pkg->percent %>%</TD>
+ <TD CLASS="money"><% sprintf('%.2f',
+ $cust_pkg->sum_charged * $cust_pkg->percent / 100) %></TD>
+ </TR>
+% $sales += $cust_pkg->sum_charged;
+% $commission += $cust_pkg->sum_charged * $cust_pkg->percent / 100;
+% $row++;
+% $bgcolor = 1-$bgcolor;
+% $custnum = $cust_pkg->custnum;
+% }
+ <TR STYLE="background-color: #f5f6be">
+ <TD CLASS="grid">
+ <% emt('[quant,_1,package] with commission', $row) %>
+ </TD>
+ <TD CLASS="money"><% sprintf('%.2f', $sales) %></TD>
+ <TD></TD>
+ <TD CLASS="money"><% sprintf('%.2f', $commission) %></TD>
+ </TR>
+</TABLE>
+<& /elements/footer.html &>
+% }
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+my ($begin, $end) = FS::UI::Web::parse_beginning_ending($cgi);
+$cgi->param('agentnum') =~ /^(\d+)$/ or die "bad agentnum";
+my $agentnum = $1;
+my $agent = FS::agent->by_key($agentnum);
+
+my $title = $agent->agent . ' commissions';
+
+my $sum_charged =
+ '(SELECT SUM(setup + recur) FROM cust_bill_pkg JOIN cust_bill USING (invnum)'.
+ 'WHERE cust_bill_pkg.pkgnum = cust_pkg.pkgnum AND '.
+ "cust_bill._date >= $begin AND cust_bill._date < $end)";
+
+my @select = (
+ 'cust_pkg.*',
+ 'agent_pkg_class.commission_percent AS percent',
+ "$sum_charged AS sum_charged",
+);
+
+my $query = {
+ 'table' => 'cust_pkg',
+ 'select' => join(',', @select),
+ 'addl_from' => 'JOIN cust_main USING (custnum) '.
+ 'JOIN part_pkg USING (pkgpart) '.
+ 'JOIN agent_pkg_class ON ( '.
+ 'cust_main.agentnum = agent_pkg_class.agentnum AND '.
+ '( agent_pkg_class.classnum = part_pkg.classnum OR '.
+ '(agent_pkg_class IS NULL AND part_pkg.classnum IS NULL)'.
+ ' ) ) ',
+ 'extra_sql' => "WHERE cust_main.agentnum = $agentnum AND ".
+ 'agent_pkg_class.commission_percent > 0 AND '.
+ "$sum_charged > 0",
+ 'order_by' => 'ORDER BY cust_pkg.custnum ASC',
+};
+
+my @cust_pkg = qsearch($query);
+
+my $money_char = FS::Conf->new->config('money_char') || '$';
+
+my $data = '';
+my $type = $cgi->param('_type');
+if ( $type eq 'xls') {
+ # some false laziness with the above...
+ my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format;
+ my $filename = 'agent_commission' . $format->{extension};
+ http_header('Content-Type' => $format->{mime_type});
+ http_header('Content-Disposition' => qq!attachment;filename="$filename"!);
+ my $XLS = IO::Scalar->new(\$data);
+ my $workbook = $format->{class}->new($XLS);
+ my $worksheet = $workbook->add_worksheet(substr($title, 0, 31));
+
+ my $cust_head_format = $workbook->add_format(
+ bold => 1,
+ underline => 1,
+ text_wrap => 0,
+ bg_color => 'white',
+ );
+
+ my $col_head_format = $workbook->add_format(
+ bold => 1,
+ align => 'center',
+ bg_color => 'silver'
+ );
+
+ my @format;
+ foreach (0, 1) {
+ my %bg = (bg_color => $_ ? 'white' : 'silver');
+ $format[$_] = {
+ 'text' => $workbook->add_format(%bg),
+ 'money' => $workbook->add_format(%bg, num_format => $money_char.'#0.00'),
+ 'percent' => $workbook->add_format(%bg, num_format => '0.00%'),
+ };
+ }
+ my $total_format = $workbook->add_format(
+ bg_color => 'yellow',
+ num_format => $money_char.'#0.00',
+ top => 1
+ );
+
+ my ($r, $c) = (0, 0);
+ foreach (qw(Package Sales Percentage Commission)) {
+ $worksheet->write($r, $c++, $_, $col_head_format);
+ }
+ $r++;
+
+ my ($custnum, $sales, $commission, $row, $bgcolor) = (0, 0, 0, 0);
+ my $label_length = 0;
+ foreach my $cust_pkg ( @cust_pkg ) {
+ if ( $custnum ne $cust_pkg->custnum ) {
+ # start of a new customer section
+ my $cust_main = $cust_pkg->cust_main;
+ my $label = $cust_main->custnum . ': '. $cust_main->name;
+ $bgcolor = 0;
+ $worksheet->set_row($r, 20);
+ $worksheet->merge_range($r, 0, $r, 3, $label, $cust_head_format);
+ $r++;
+ }
+ $c = 0;
+ my $percent = $cust_pkg->percent / 100;
+ $worksheet->write($r, $c++, $cust_pkg->pkg_label, $format[$bgcolor]{text});
+ $worksheet->write($r, $c++, $cust_pkg->sum_charged, $format[$bgcolor]{money});
+ $worksheet->write($r, $c++, $percent, $format[$bgcolor]{percent});
+ $worksheet->write($r, $c++, ($cust_pkg->sum_charged * $percent),
+ $format[$bgcolor]{money});
+
+ $label_length = max($label_length, length($cust_pkg->pkg_label));
+ $sales += $cust_pkg->sum_charged;
+ $commission += $cust_pkg->sum_charged * $cust_pkg->percent / 100;
+ $row++;
+ $bgcolor = 1-$bgcolor;
+ $custnum = $cust_pkg->custnum;
+ $r++;
+ }
+
+ $c = 0;
+ $label_length = max($label_length, 20);
+ $worksheet->set_column($c, $c, $label_length);
+ $worksheet->write($r, $c++, mt('[quant,_1,package] with commission', $row),
+ $total_format);
+ $worksheet->set_column($c, $c + 2, 11);
+ $worksheet->write($r, $c++, $sales, $total_format);
+ $worksheet->write($r, $c++, '', $total_format);
+ $worksheet->write($r, $c++, $commission, $total_format);
+
+ $workbook->close;
+}
+</%init>
-<% include('elements/search.html',
+<& elements/search.html,
'title' => 'Inventory summary per agent',
'name_singular' => 'agent',
'query' => { 'table' => 'agent',
" AND $agentnums_sql",
'header' => \@header,
'fields' => \@fields,
- )
-%>
+&>
<%init>
die "access denied"
% -expires => '-1d',
% );
% $r->headers_out->add( 'Set-Cookie' => $cookie->as_string );
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Invoice Batches',
'name_singular' => 'batch',
'query' => { 'table' => 'bill_batch',
'agent_pos' => 1,
'html_foot' => include('.foot'),
- )
-
-%>
+&>
%}
<%def .foot>
<SCRIPT type="text/javascript">
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => $title,
'name' => 'call detail records',
'query' => $query,
'fields' => \@fields,
'links' => \@links,
'html_form' => qq!<FORM NAME="cdrForm" ACTION="$p/misc/cdr.cgi" METHOD="POST">!,
- #false laziness w/queue.html
- 'html_foot' => sub {
- if ( $areboxes ) {
- '<BR><INPUT TYPE="button" VALUE="select all" onClick="setAll(true)">'.
- '<INPUT TYPE="button" VALUE="unselect all" onClick="setAll(false)">'.
- qq!<BR><INPUT TYPE="submit" NAME="action" VALUE="reprocess selected" onClick="return confirm('Are you sure you want to reprocess the selected CDRs?')">!.
- qq!<INPUT TYPE="submit" NAME="action" VALUE="delete selected" onClick="return confirm('Are you sure you want to delete the selected CDRs?')"><BR>!.
- '<SCRIPT TYPE="text/javascript">'.
- ' function setAll(setTo) { '.
- ' theForm = document.cdrForm;'.
- ' for (i=0,n=theForm.elements.length;i<n;i++)'.
- ' if (theForm.elements[i].name.indexOf("acctid") != -1)'.
- ' theForm.elements[i].checked = setTo;'.
- ' }'.
- '</SCRIPT>';
- } else {
- '';
- }
- },
- )
-%>
+ 'html_foot' => $html_foot,
+&>
<%init>
die "access denied"
my $conf = new FS::Conf;
-my $areboxes = 0;
-
my $title = 'Call Detail Records';
my $hashref = {};
@fields = map { exists($fields{$_}) ? $fields{$_} : $_ } @fields;
unshift @fields, sub {
return '' unless $edit_data;
- $areboxes = 1;
my $cdr = shift;
my $acctid = $cdr->acctid;
qq!<INPUT NAME="acctid$acctid" TYPE="checkbox" VALUE="1">!;
$nototalminutes = 1;
}
+my $html_foot = include('/search/elements/checkbox-foot.html',
+ actions => [
+ { submit => "reprocess selected",
+ name => "action",
+ confirm => "Are you sure you want to reprocess the selected CDRs?" },
+ { submit => "delete selected",
+ name => "action",
+ confirm => "Are you sure you want to delete the selected CDRs?" },
+ ]
+);
</%init>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('List invoices');
-my $join_cust_main = 'LEFT JOIN cust_main USING ( custnum )';
+my $join_cust_main = FS::UI::Web::join_cust_main('cust_bill');
#here is the agent virtualization
my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
$search{'refnum'} = $1;
}
- if ( $cgi->param('cust_classnum') ) {
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
$search{'cust_classnum'} = [ $cgi->param('cust_classnum') ];
}
};
}
-
my $link = [ "${p}view/cust_bill.cgi?", 'invnum', ];
my $clink = sub {
my $cust_bill = shift;
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => $title,
'html_init' => $html_init,
'menubar' => $menubar,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
my $join = 'LEFT JOIN part_bill_event USING ( eventpart ) '.
'LEFT JOIN cust_bill USING ( invnum ) '.
- 'LEFT JOIN cust_main USING ( custnum ) ';
+ FS::UI::Web::join_cust_main('cust_bill');
my $sql_query = {
'table' => 'cust_bill_event',
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => $title,
'name' => 'net payments',
'query' => $sql_query,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
die "access denied"
$title = $part_referral->referral. " $title";
}
-if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
- push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, prepaid_income.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
#
my $count_query = 'SELECT COUNT(*), SUM(amount)
FROM cust_bill_pay
- LEFT JOIN cust_bill USING ( invnum )
- LEFT JOIN cust_main USING ( custnum ) '.
+ LEFT JOIN cust_bill USING ( invnum ) '.
+ FS::UI::Web::join_cust_main('cust_bill') .
$where;
my $sql_query = {
'hashref' => {},
'extra_sql' => $where,
'addl_from' => 'LEFT JOIN cust_bill USING ( invnum )
- LEFT JOIN cust_pay USING ( paynum )
- LEFT JOIN cust_main ON ( cust_bill.custnum = cust_main.custnum )',
+ LEFT JOIN cust_pay USING ( paynum ) '.
+ FS::UI::Web::join_cust_main('cust_bill')
};
my $cust_bill_link = sub {
# valid in both the tax and non-tax cases
my $join_cust =
- " LEFT JOIN cust_bill USING (invnum)
- LEFT JOIN cust_main USING (custnum)
- ";
+ " LEFT JOIN cust_bill USING (invnum)".
+ # use cust_pkg.locationnum if it exists
+ FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg');
#agent virtualization
my $agentnums_sql =
push @where, "cust_main.refnum = $1";
}
-# cust_classnum
-if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
- push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
+
# custnum
if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
push @where, "cust_main.custnum = $1";
LEFT JOIN part_pkg USING (pkgpart)';
my $part_pkg = 'part_pkg';
-if ( $cgi->param('use_override') ) {
+if ( $cgi->param('use_override') ) { #"Separate sub-packages from parents"
# still need the real part_pkg for tax applicability,
# so alias this one
$join_pkg .= " LEFT JOIN part_pkg AS override ON (
- COALESCE(cust_bill_pkg.pkgpart_override, cust_pkg.pkgpart, 0) = part_pkg.pkgpart
+ COALESCE(cust_bill_pkg.pkgpart_override, cust_pkg.pkgpart, 0) = override.pkgpart
)";
$part_pkg = 'override';
}
#total payments
-my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount) AS pay_amount,
- billpkgnum
- FROM cust_bill_pay_pkg
- GROUP BY billpkgnum";
-$join_pkg .= " LEFT JOIN ($pay_sub) AS item_pay USING (billpkgnum)";
-push @select, 'item_pay.pay_amount';
+my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount)
+ FROM cust_bill_pay_pkg
+ WHERE cust_bill_pkg.billpkgnum = cust_bill_pay_pkg.billpkgnum
+ ";
+push @select, "($pay_sub) AS pay_amount";
# credit
#still want a credit total column
- my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount,
- billpkgnum
- FROM cust_credit_bill_pkg
- GROUP BY billpkgnum";
- $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit USING (billpkgnum)";
-
- push @select, 'item_credit.credit_amount';
+ my $credit_sub = "
+ SELECT SUM(cust_credit_bill_pkg.amount)
+ FROM cust_credit_bill_pkg
+ WHERE cust_bill_pkg.billpkgnum = cust_credit_bill_pkg.billpkgnum
+ ";
+ push @select, "($credit_sub) AS credit_amount";
}
my $query = {
'table' => 'cust_bill_pkg',
- 'addl_from' => "$join_cust $join_pkg",
+ 'addl_from' => "$join_pkg $join_cust",
'hashref' => {},
'select' => join(",\n", @select ),
'extra_sql' => $where,
my $count_query =
'SELECT ' . join(',', @total) .
- " FROM cust_bill_pkg $join_cust $join_pkg
+ " FROM cust_bill_pkg $join_pkg $join_cust
$where";
@peritem_desc = map {emt($_)} @peritem_desc;
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Discounts',
'name' => 'discounts',
'query' => $query,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
#a little false laziness below w/cust_bill_pkg.cgi
'LEFT JOIN cust_pkg_discount USING (pkgdiscountnum)';
my $join_cust =
- ' JOIN cust_bill_pkg USING ( billpkgnum )
- JOIN cust_bill USING ( invnum )
- LEFT JOIN cust_main USING ( custnum ) ';
+ ' JOIN cust_bill USING ( invnum ) '.
+ FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg');
my $join_pkg =
- ' LEFT JOIN cust_pkg ON ( cust_bill_pkg.pkgnum = cust_pkg.pkgnum )
+ ' JOIN cust_bill_pkg USING ( billpkgnum )
+ LEFT JOIN cust_pkg ON ( cust_bill_pkg.pkgnum = cust_pkg.pkgnum )
LEFT JOIN part_pkg USING ( pkgpart ) ';
#LEFT JOIN part_pkg AS override
# ON pkgpart_override = override.pkgpart ';
my $where = ' WHERE '. join(' AND ', @where);
$count_query .=
- " FROM cust_bill_pkg_discount $join_cust_pkg_discount $join_cust $join_pkg ".
+ " FROM cust_bill_pkg_discount $join_cust_pkg_discount $join_pkg $join_cust ".
$where;
my @select = (
my $query = {
'table' => 'cust_bill_pkg_discount',
- 'addl_from' => "$join_cust_pkg_discount $join_cust $join_pkg",
+ 'addl_from' => "$join_cust_pkg_discount $join_pkg $join_cust",
'hashref' => {},
'select' => join(', ', @select ),
'extra_sql' => $where,
push @where, 'cust_main.refnum IN ('.join(',', @refnum).')';
}
-my @cust_classnums = grep /^\d+$/, $cgi->param('cust_classnum');
-if ( @cust_classnums ) {
- push @where, 'cust_main.classnum IN ('.join(',', @cust_classnums).')';
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
+ if @classnums;
}
if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
$title = $part_referral->referral. " $title";
}
-if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
- push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
my $count_query = 'SELECT COUNT(*), SUM(amount) ';
$count_query .= ', SUM(' . FS::cust_credit->unapplied_sql . ') ' if $unapplied;
-$count_query .= 'FROM cust_credit LEFT JOIN cust_main USING ( custnum ) '.
+$count_query .= 'FROM cust_credit'. FS::UI::Web::join_cust_main('cust_credit').
$where;
my @count_addl = ( $money_char.'%.2f total credited (gross)' );
'select' => join(', ',@select),
'hashref' => {},
'extra_sql' => $where,
- 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'addl_from' => FS::UI::Web::join_cust_main('cust_credit')
};
</%init>
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => $title,
'name' => 'net credits',
'query' => $sql_query,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
die "access denied"
#
my $count_query = 'SELECT COUNT(*), SUM(amount)
FROM cust_credit_bill
- LEFT JOIN cust_bill USING ( invnum )
- LEFT JOIN cust_main USING ( custnum ) '.
+ LEFT JOIN cust_bill USING ( invnum ) '.
+ FS::UI::Web::join_cust_main('cust_bill') .
$where;
my $sql_query = {
'hashref' => {},
'extra_sql' => $where,
'addl_from' => 'LEFT JOIN cust_bill USING ( invnum )
- LEFT JOIN cust_credit USING ( crednum )
- LEFT JOIN cust_main ON ( cust_bill.custnum = cust_main.custnum )',
+ LEFT JOIN cust_credit USING ( crednum )'.
+ FS::UI::Web::join_cust_main('cust_bill')
};
my $cust_bill_link = sub {
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Credit application detail', #to line item
'name_singular' => 'credit application',
'query' => $query,
# line item
'Description',
+ 'Location',
@post_desc_header,
#invoice
? $_[0]->get('pkg') # possibly use override.pkg
: $_[0]->get('itemdesc') # but i think this correct
},
+ $location_sub,
@post_desc,
'invnum',
sub { time2str('%b %d %Y', shift->_date ) },
'', #'otaker',
'', #reason
'', #line item description
+ '', #location
@post_desc_null,
'invnum',
'_date',
'',
'',
'',
+ '',
@post_desc_null,
$ilink,
$ilink,
FS::UI::Web::cust_header()
),
],
- 'align' => 'rrlll'.
+ 'align' => 'rrllll'.
$post_desc_align.
'rr'.
FS::UI::Web::cust_aligns(),
'',
'',
'',
+ '',
@post_desc_null,
'',
'',
'',
'',
'',
+ '',
@post_desc_null,
'',
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
#LOTS of false laziness below w/cust_bill_pkg.cgi
SUM(cust_credit_bill_pkg.amount)";
my $join_cust =
- ' JOIN cust_bill ON ( cust_bill_pkg.invnum = cust_bill.invnum )
- LEFT JOIN cust_main ON ( cust_bill.custnum = cust_main.custnum ) ';
+ ' JOIN cust_bill ON ( cust_bill_pkg.invnum = cust_bill.invnum )'.
+ FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg');
my $join_pkg;
s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g for @where;
}
-} else {
+} else {
- #die?
- warn "neiether nottax nor istax parameters specified";
+ #warn "neither nottax nor istax parameters specified";
#same as before?
$join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum )
LEFT JOIN part_pkg USING ( pkgpart ) ';
my @post_desc = ();
my @post_desc_null = ();
my $post_desc_align = '';
-if ( $conf->exists('enable_taxclasses') ) {
+if ( $conf->exists('enable_taxclasses') && ! $cgi->param('istax') ) {
push @post_desc_header, 'Tax class';
push @post_desc, 'taxclass';
push @post_desc_null, '';
my $conf = new FS::Conf;
my $money_char = $conf->config('money_char') || '$';
+my $tax_pkg_address = $conf->exists('tax-pkg_address');
+my $tax_ship_address = $conf->exists('tax-ship_address');
+
+my $location_sub = sub {
+ #my $cust_credit_bill_pkg = shift;
+ my $self = shift;
+ my $tax_Xlocation = $self->cust_bill_pkg_tax_Xlocation;
+ if ( defined($tax_Xlocation) && $tax_Xlocation ) {
+
+ if ( ref($tax_Xlocation) eq 'FS::cust_bill_pkg_tax_location' ) {
+
+ if ( $tax_Xlocation->taxtype eq 'FS::cust_main_county' ) {
+ my $cust_main_county = $tax_Xlocation->cust_main_county;
+ if ( $cust_main_county ) {
+ $cust_main_county->label;
+ } else {
+ ''; #cust_main_county record is gone... history? yuck.
+ }
+ } else {
+ '(CCH tax_rate)'; #XXX FS::tax_rate.. vendor taxes not yet handled here
+ }
+
+ } elsif ( ref($tax_Xlocation) eq 'FS::cust_bill_pkg_tax_rate_location' ) {
+ '(CCH)'; #XXX vendor taxes not yet handled here
+ } else {
+ 'unknown tax_Xlocation '. ref($tax_Xlocation);
+ }
+
+ } else {
+
+ my $cust_bill_pkg = $self->cust_bill_pkg;
+ if ( $cust_bill_pkg->pkgnum > 0 ) {
+ my $cust_pkg = $cust_bill_pkg->cust_pkg;
+ if ( $tax_pkg_address && (my $cust_location = $cust_pkg->cust_location) ){
+ $cust_location->county_state_country;
+ } else {
+ my $cust_main = $cust_pkg->cust_main;
+ if ( $tax_ship_address && $cust_main->has_ship_address ) {
+ $cust_main->county_state_country('ship_');
+ } else {
+ $cust_main->county_state_country;
+ }
+ }
+
+ } else {
+ #tax? we shouldn't have wound up here then...
+ ''; #return customer ship or bill address? (depending on tax-ship_address)
+ }
+
+ }
+
+};
+
</%init>
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => $title,
'name' => 'net refunds',
'query' => $sql_query,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
die "access denied"
$title = $part_referral->referral. " $title";
}
-if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
- push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
#
my $count_query = 'SELECT COUNT(*), SUM(cust_credit_refund.amount)
FROM cust_credit_refund
- LEFT JOIN cust_credit USING ( crednum )
- LEFT JOIN cust_main USING ( custnum ) '.
+ LEFT JOIN cust_credit USING ( crednum ) '.
+ FS::UI::Web::join_cust_main('cust_credit') .
$where;
my $sql_query = {
'hashref' => {},
'extra_sql' => $where,
'addl_from' => 'LEFT JOIN cust_credit USING ( crednum )
- LEFT JOIN cust_refund USING ( refundnum )
- LEFT JOIN cust_main ON ( cust_credit.custnum = cust_main.custnum )',
+ LEFT JOIN cust_refund USING ( refundnum )'.
+ FS::UI::Web::join_cust_main('cust_credit')
};
#my $cust_credit_link = sub {
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => $title,
'html_init' => $html_init,
'menubar' => $menubar,
#'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+&>
<%once>
my $status_sub = sub {
my $where = ' WHERE '. FS::cust_event->search_sql_where( \%search );
-my $join = FS::cust_event->join_sql();
+my $join = FS::cust_event->join_sql() .
+ 'LEFT JOIN cust_location bill_location '.
+ 'ON (cust_main.bill_locationnum = bill_location.locationnum) '.
+ 'LEFT JOIN cust_location ship_location '.
+ 'ON (cust_main.ship_locationnum = ship_location.locationnum)';
+ # warning: does not show the true service address for package events.
+ # the query to do that would be painfully slow.
my $sql_query = {
'table' => 'cust_event',
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Zip code Search Results',
'name' => 'zip codes',
'query' => $sql_query,
'header' => [ 'Zip code', 'Customers', ],
'fields' => [ 0, 1 ],
'links' => [ '', $link ],
- )
-%>
+&>
<%init>
die "access denied"
% my $pkg_rowspan = shift @pkg_rowspans;
<% $n1 %><TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN="<% $pkg_rowspan%>">
- <A HREF="<% $pkgview %>"><FONT SIZE=-1><% $pkg_comment %></FONT></A>
+ <A HREF="<% $pkgview %>"><FONT SIZE=-1><% $pkg_comment |h %></FONT></A>
</TD>
% my $n2 = '';
#scalars
my @scalars = qw (
agentnum status address zip paydate_year paydate_month invoice_terms
- no_censustract with_geocode with_email no_POST
+ no_censustract with_geocode with_email POST no_POST
custbatch usernum
cancelled_pkgs
cust_fields flattened_pkgs
+ all_tags
);
for my $param ( @scalars ) {
-<% include('elements/search.html',
+<& elements/search.html,
'title' => 'Batch payment details',
'name' => 'batch details',
'query' => $sql_query,
'disable_download' => 1,
'header' => [ '#',
'Inv #',
- 'Customer',
+ 'Cust #',
'Customer',
'Card Name',
'Card',
'Exp',
'Amount',
'Status',
+ '', # error_message
],
- 'fields' => [ sub {
- shift->[0];
- },
- sub {
- shift->[1];
- },
- sub {
- shift->[2];
- },
- sub {
- my $cpb = shift;
- $cpb->[3] . ', ' . $cpb->[4];
- },
- sub {
- shift->[5];
- },
- sub {
- my $cardnum = shift->[6];
- 'x'x(length($cardnum)-4). substr($cardnum,(length($cardnum)-4));
- },
- sub {
- shift->[7] =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
- my( $mon, $year ) = ( $2, $1 );
- $mon = "0$mon" if length($mon) == 1;
- "$mon/$year";
- },
- sub {
- shift->[8];
- },
- sub {
- shift->[9];
- },
- ],
- 'align' => 'lllllllrl',
- 'links' => [ ['', sub{'#';}],
- ["${p}view/cust_bill.cgi?", sub{shift->[1];},],
- ["${p}view/cust_main.cgi?", sub{shift->[2];},],
- ["${p}view/cust_main.cgi?", sub{shift->[2];},],
+ 'fields' => [ 'paybatchnum',
+ 'invnum',
+ 'custnum',
+ sub { $_[0]->cust_main->name_short },
+ 'payname',
+ 'mask_payinfo',
+ sub {
+ return('') if $_[0]->payby ne 'CARD';
+ $_[0]->get('exp') =~ /^\d\d(\d\d)-(\d\d)/;
+ sprintf('%02d/%02d',$1,$2);
+ },
+ sub {
+ sprintf('%.02f', $_[0]->amount)
+ },
+ 'status',
+ 'error_message',
+ ],
+ 'align' => 'rrrlllcrll',
+ 'links' => [ '',
+ ["${p}view/cust_bill.cgi?", 'invnum'],
+ (["${p}view/cust_main.cgi?", 'custnum']) x 2,
],
- )
-%>
+ 'link_onclicks' => [ ('') x 8,
+ $sub_receipt
+ ],
+&>
<%init>
my $conf = new FS::Conf;
}
if ( not $cgi->param('dcln') ) {
- push @search, "cpb.status IS DISTINCT FROM 'Approved'";
+ push @search, "cust_pay_batch.status IS DISTINCT FROM 'Approved'";
}
my ($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
my $search = ' WHERE ' . join(' AND ', @search);
-$count_query = 'SELECT COUNT(*) FROM cust_pay_batch AS cpb ' .
+$count_query = 'SELECT COUNT(*) FROM cust_pay_batch ' .
'LEFT JOIN cust_main USING ( custnum ) ' .
'LEFT JOIN pay_batch USING ( batchnum )' .
$search;
-#grr
-$sql_query = "SELECT paybatchnum,invnum,custnum,cpb.last,cpb.first," .
- "cpb.payname,cpb.payinfo,cpb.exp,amount,cpb.status " .
- "FROM cust_pay_batch AS cpb " .
- 'LEFT JOIN cust_main USING ( custnum ) ' .
- 'LEFT JOIN pay_batch USING ( batchnum ) ' .
- "$search ORDER BY $orderby";
+$sql_query = {
+ 'table' => 'cust_pay_batch',
+ 'select' => 'cust_pay_batch.*, cust_main.*, cust_pay.paynum',
+ 'hashref' => {},
+ 'addl_from' => 'LEFT JOIN pay_batch USING ( batchnum ) '.
+ 'LEFT JOIN cust_main USING ( custnum ) '.
+
+ 'LEFT JOIN cust_pay USING ( batchnum, custnum ) ',
+ 'extra_sql' => $search,
+ 'order_by' => "ORDER BY $orderby",
+};
+
+my $sub_receipt = sub {
+ my $paynum = shift->paynum or return '';
+ include('/elements/popup_link_onclick.html',
+ 'action' => $p.'view/cust_pay.html?link=popup;paynum='.$paynum,
+ 'actionlabel' => emt('Payment Receipt'),
+ );
+};
my $html_init = '';
if ( $pay_batch ) {
emt('Package'),
emt('Class'),
emt('Status'),
+ emt('Ordered by'),
emt('Setup'),
emt('Base Recur'),
emt('Freq.'),
sub { $_[0]->pkg; },
'classname',
sub { ucfirst(shift->status); },
+ 'otaker',
sub { sprintf( $money_char.'%.2f',
shift->part_pkg->option('setup_fee'),
);
'',
'',
'',
+ '',
FS::UI::Web::cust_colors(),
'',
],
- 'style' => [ '', '', '', '', 'b', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
+ 'style' => [ '', '', '', '', 'b', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
FS::UI::Web::cust_styles() ],
'size' => [ '', '', '', '', '-1' ],
- 'align' => 'rrlccrrlrrrrrrrrrrl'. FS::UI::Web::cust_aligns(). 'r',
+ 'align' => 'rrlcccrrlrrrrrrrrrrl'. FS::UI::Web::cust_aligns(). 'r',
'links' => [
$link,
$link,
'',
'',
'',
+ '',
'', # link to changed-from package?
'',
'',
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Package discounts',
'name' => 'discounts',
'query' => $query,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
die "access denied"
my $join = ' LEFT JOIN discount USING ( discountnum )
LEFT JOIN cust_pkg USING ( pkgnum )
- LEFT JOIN part_pkg USING ( pkgpart )
- LEFT JOIN cust_main USING ( custnum ) ';
+ LEFT JOIN part_pkg USING ( pkgpart ) '.
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
my $where = ' WHERE '. join(' AND ', @where);
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => $part_svc->svc.' services in package #'.$pkgnum,
'name' => 'services',
'html_form' => $html_form,
('')x4,
],
'html_foot' => sub { $areboxes ? $html_foot : '' }
- )
-%>
+
+&>
<%init>
die "access denied"
my $addl_from = ' LEFT JOIN part_svc USING ( svcpart ) '.
' LEFT JOIN cust_pkg USING ( pkgnum ) '.
- ' LEFT JOIN cust_main USING ( custnum ) ';
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
my @extra_sql = ();
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => $title,
'name_singular' => 'tax adjustment',
'query' => $query,
},
],
'links' => [ '', '', '', $ilink ],
- )
-%>
-
+
+&>
<%init>
die "access denied"
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Legacy tax exemptions',
'name' => 'legacy tax exemptions',
'query' => $query,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
-my $join_cust = "
- LEFT JOIN cust_main USING ( custnum )
-";
+my $join_cust = FS::UI::Web::join_cust_main('cust_tax_exempt');
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('View customer tax exemptions');
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Tax exemptions',
'name' => 'tax exemptions',
'query' => $query,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+&>
<%once>
my $join_cust = "
- JOIN cust_bill USING ( invnum )
- LEFT JOIN cust_main USING ( custnum )
-";
+ JOIN cust_bill USING ( invnum )" .
+ FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg');
my $join_pkg = "
LEFT JOIN cust_pkg USING ( pkgnum )
my $join = "
JOIN cust_bill_pkg USING ( billpkgnum )
- $join_cust
$join_pkg
+ $join_cust
";
</%once>
$title .= 'Customer Accounting Summary Report';
-my @cust_classnums = grep /^\d+$/, $cgi->param('cust_classnum');
-
my @items = ('netsales', 'cashflow');
my @params = ( [], [] );
my $setuprecur = '';
}
}
$search_hash{'classnum'} = [ $cgi->param('cust_classnum') ]
- if $cgi->param('cust_classnum');
+ if grep { $_ eq 'cust_classnum' } $cgi->param;
my $query = FS::cust_main::Search->search(\%search_hash);
my @custs = qsearch($query);
--- /dev/null
+<%doc>
+<& /elements/search.html,
+ # options...
+ html_foot => include('elements/checkbox-foot.html',
+ actions => [
+ { label => 'Edit selected packages',
+ action => 'popup_package_edit()',
+ },
+ { submit => 'Delete selected packages',
+ confirm => 'Really delete these packages?'
+ },
+ ],
+ filter => '.name = "pkgpart"', # see below
+ ),
+&>
+
+This creates a footer for a search page containing a column of checkboxes.
+Typically this is used to select several items from the search result and
+apply some change to all of them at once. The footer always provides
+"select all" and "unselect all" buttons.
+
+"actions" is an arrayref of action buttons to show. Each element of the
+array is a hashref of either:
+
+- "submit" and, optionally, "confirm". Creates a submit button. The value
+of "submit" becomes the "value" property of the button (and thus its label).
+If "confirm" is specified, the button will have an onclick handler that
+displays the value of "confirm" in a popup message box and asks the user to
+confirm the choice.
+
+- "onclick" and "label". Creates a non-submit button that executes the
+Javascript code in "onclick". "label" is used as the text of the button.
+
+If you want only a single action, you can forget the arrayref-of-hashrefs
+business and just put "submit" and "confirm" (or "onclick" and "label")
+elements in the argument list.
+
+"filter" is a javascript expression to limit which checkboxes are included in
+the "select/unselect all" actions. By default, any input with type="checkbox"
+will be included. If this option is given, it will be evaluated with the
+HTML node in a variable named "obj". The expression should return true or
+false.
+
+</%doc>
+<DIV ID="checkbox_footer" STYLE="display:block">
+<INPUT TYPE="button" VALUE="<% emt('select all') %>" onclick="setAll(true)">
+<INPUT TYPE="button" VALUE="<% emt('unselect all') %>" onclick="setAll(false)">
+<BR>
+% foreach my $action (@$actions) {
+% if ( $action->{onclick} ) {
+<INPUT TYPE="button" <% $action->{name} %> onclick="<% $opt{onclick} %>"\
+ VALUE="<% $action->{label} |h%>">
+% } elsif ( $action->{submit} ) {
+<INPUT TYPE="submit" <% $action->{name} %> <% $action->{confirm} %>\
+ VALUE="<% $action->{submit} |h%>">
+% } # else do nothing
+% } #foreach
+</DIV>
+<SCRIPT>
+var checkboxes = [];
+var inputs = document.getElementsByTagName('input');
+for (var i = 0; i < inputs.length; i++) {
+ var obj = inputs[i];
+ if ( obj.type == "checkbox" && <% $filter %> ) {
+ checkboxes.push(obj);
+ }
+}
+%# avoid the need for "$areboxes" late-evaluation hackery
+if ( checkboxes.length == 0 ) {
+ document.getElementById('checkbox_footer').style.display = 'none';
+}
+function setAll(setTo) {
+ for (var i = 0; i < checkboxes.length; i++) {
+ checkboxes[i].checked = setTo;
+ }
+}
+</SCRIPT>
+<%init>
+my %opt = @_;
+my $actions = $opt{'actions'} || [ \%opt ];
+foreach (@$actions) {
+ $_->{confirm} &&= qq!onclick="return confirm('! . $_->{confirm} . qq!')"!;
+ $_->{name} &&= qq!NAME="! . $_->{name} . qq!"!;
+}
+my $filter = $opt{filter} || 'true';
+</%init>
push @where, FS::cust_main->$method();
}
+# cust_classnum (false laziness w/prepaid_income.html, elements/cust_pay_or_refund.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
+ if @classnums;
+}
+
#here is the agent virtualization
push @where, $FS::CurrentUser::CurrentUser->agentnums_sql;
my $sql_query = {
'table' => 'cust_main',
+ 'addl_from' => FS::UI::Web::join_cust_main('cust_main'),
'hashref' => {},
'select' => join(',',
#'cust_main.*',
- 'custnum',
+ 'cust_main.custnum',
$range_cols,
$packages_cols,
FS::UI::Web::cust_sql_fields(),
tie my %download_formats, 'Tie::IxHash', (
'' => 'Default batch mode',
+'NACHA' => '94 byte NACHA',
'csv-td_canada_trust-merchant_pc_batch' =>
'CSV file for TD Canada Trust Merchant PC Batch',
'csv-chase_canada-E-xactBatch' =>
'sort_fields' => \@sort_fields,
'align' => $align,
'links' => \@links,
+ 'link_onclicks' => \@link_onclicks,
'color' => \@color,
'style' => \@style,
&>
}
}
-my @header = ();
-my @fields = ();
-my @sort_fields = ();
+my @header;
+my @fields;
+my @sort_fields;
my $align = '';
-my @links = ();
+my @links;
+my @link_onclicks;
if ( $opt{'pre_header'} ) {
push @header, @{ $opt{'pre_header'} };
$align .= 'c' x scalar(@{ $opt{'pre_header'} });
push @sort_fields, @{ $opt{'pre_fields'} };
}
+my $sub_receipt = sub {
+ my $obj = shift;
+ my $objnum = $obj->primary_key . '=' . $obj->get($obj->primary_key);
+
+ include('/elements/popup_link_onclick.html',
+ 'action' => $p.'view/cust_pay.html?link=popup;'.$objnum,
+ 'actionlabel' => emt('Payment Receipt'),
+ );
+};
+
push @header, "\u$name_singular",
'Amount',
;
push @fields, 'payby_payinfo_pretty',
sub { sprintf('$%.2f', shift->$amount_field() ) },
;
+push @link_onclicks, $sub_receipt, '',
push @sort_fields, '', $amount_field;
if ( $unapplied ) {
$title = $part_referral->referral. " $title";
}
- if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
- push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+ # cust_classnum (false laziness w/ elements/cust_main_dayranges.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+ if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
}
if ( $cgi->param('payby') ) {
- $cgi->param('payby') =~
- /^(CARD|CHEK|BILL|PREP|CASH|WEST|MCRD)(-(VisaMC|Amex|Discover|Maestro))?$/
- or die "illegal payby ". $cgi->param('payby');
- push @search, "$table.payby = '$1'";
- if ( $3 ) {
-
- my $cardtype = $3;
-
- my $search;
- if ( $cardtype eq 'VisaMC' ) {
- #avoid posix regexes for portability
- $search =
- " ( ( substring($table.payinfo from 1 for 1) = '4' ".
- " AND substring($table.payinfo from 1 for 4) != '4936' ".
- " AND substring($table.payinfo from 1 for 6) ".
- " NOT SIMILAR TO '49030[2-9]' ".
- " AND substring($table.payinfo from 1 for 6) ".
- " NOT SIMILAR TO '49033[5-9]' ".
- " AND substring($table.payinfo from 1 for 6) ".
- " NOT SIMILAR TO '49110[1-2]' ".
- " AND substring($table.payinfo from 1 for 6) ".
- " NOT SIMILAR TO '49117[4-9]' ".
- " AND substring($table.payinfo from 1 for 6) ".
- " NOT SIMILAR TO '49118[1-2]' ".
- " )".
- " OR substring($table.payinfo from 1 for 2) = '51' ".
- " OR substring($table.payinfo from 1 for 2) = '52' ".
- " OR substring($table.payinfo from 1 for 2) = '53' ".
- " OR substring($table.payinfo from 1 for 2) = '54' ".
- " OR substring($table.payinfo from 1 for 2) = '54' ".
- " OR substring($table.payinfo from 1 for 2) = '55' ".
- " OR substring($table.payinfo from 1 for 2) = '36' ". #Diner's int'l processed as Visa/MC inside US
- " ) ";
- } elsif ( $cardtype eq 'Amex' ) {
- $search =
- " ( substring($table.payinfo from 1 for 2 ) = '34' ".
- " OR substring($table.payinfo from 1 for 2 ) = '37' ".
- " ) ";
- } elsif ( $cardtype eq 'Discover' ) {
- $search =
- " ( substring($table.payinfo from 1 for 4 ) = '6011' ".
- " OR substring($table.payinfo from 1 for 2 ) = '65' ".
- " OR substring($table.payinfo from 1 for 3 ) = '622' ". #China Union Pay processed as Discover outside CN
- " ) ";
- } elsif ( $cardtype eq 'Maestro' ) {
- $search =
- " ( substring($table.payinfo from 1 for 2 ) = '63' ".
- " OR substring($table.payinfo from 1 for 2 ) = '67' ".
- " OR substring($table.payinfo from 1 for 6 ) = '564182' ".
- " OR substring($table.payinfo from 1 for 4 ) = '4936' ".
- " OR substring($table.payinfo from 1 for 6 ) ".
- " SIMILAR TO '49030[2-9]' ".
- " OR substring($table.payinfo from 1 for 6 ) ".
- " SIMILAR TO '49033[5-9]' ".
- " OR substring($table.payinfo from 1 for 6 ) ".
- " SIMILAR TO '49110[1-2]' ".
- " OR substring($table.payinfo from 1 for 6 ) ".
- " SIMILAR TO '49117[4-9]' ".
- " OR substring($table.payinfo from 1 for 6 ) ".
- " SIMILAR TO '49118[1-2]' ".
- " ) ";
- } else {
- die "unknown card type $cardtype";
- }
- my $masksearch = $search;
- $masksearch =~ s/$table\.payinfo/$table.paymask/gi;
+ my @all_payby_search = ();
+ foreach my $payby ( $cgi->param('payby') ) {
+
+ $payby =~
+ /^(CARD|CHEK|BILL|PREP|CASH|WEST|MCRD)(-(VisaMC|Amex|Discover|Maestro))?$/
+ or die "illegal payby $payby";
+
+ my $payby_search = "$table.payby = '$1'";
+
+ if ( $3 ) {
+
+ my $cardtype = $3;
+
+ my $search;
+ if ( $cardtype eq 'VisaMC' ) {
+ #avoid posix regexes for portability
+ $search =
+ " ( ( substring($table.payinfo from 1 for 1) = '4' ".
+ " AND substring($table.payinfo from 1 for 4) != '4936' ".
+ " AND substring($table.payinfo from 1 for 6) ".
+ " NOT SIMILAR TO '49030[2-9]' ".
+ " AND substring($table.payinfo from 1 for 6) ".
+ " NOT SIMILAR TO '49033[5-9]' ".
+ " AND substring($table.payinfo from 1 for 6) ".
+ " NOT SIMILAR TO '49110[1-2]' ".
+ " AND substring($table.payinfo from 1 for 6) ".
+ " NOT SIMILAR TO '49117[4-9]' ".
+ " AND substring($table.payinfo from 1 for 6) ".
+ " NOT SIMILAR TO '49118[1-2]' ".
+ " )".
+ " OR substring($table.payinfo from 1 for 2) = '51' ".
+ " OR substring($table.payinfo from 1 for 2) = '52' ".
+ " OR substring($table.payinfo from 1 for 2) = '53' ".
+ " OR substring($table.payinfo from 1 for 2) = '54' ".
+ " OR substring($table.payinfo from 1 for 2) = '54' ".
+ " OR substring($table.payinfo from 1 for 2) = '55' ".
+# " OR substring($table.payinfo from 1 for 2) = '36' ". #Diner's int'l was processed as Visa/MC inside US, now Discover
+ " ) ";
+ } elsif ( $cardtype eq 'Amex' ) {
+ $search =
+ " ( substring($table.payinfo from 1 for 2 ) = '34' ".
+ " OR substring($table.payinfo from 1 for 2 ) = '37' ".
+ " ) ";
+ } elsif ( $cardtype eq 'Discover' ) {
+
+ my $conf = new FS::Conf;
+ my $country = $conf->config('countrydefault') || 'US';
+
+ $search =
+ " ( substring($table.payinfo from 1 for 4 ) = '6011' ".
+ " OR substring($table.payinfo from 1 for 2 ) = '65' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '300' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '301' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '302' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '303' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '304' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '305' ".
+ " OR substring($table.payinfo from 1 for 4 ) = '3095' ".
+ " OR substring($table.payinfo from 1 for 2 ) = '36' ".
+ " OR substring($table.payinfo from 1 for 2 ) = '38' ".
+ " OR substring($table.payinfo from 1 for 2 ) = '39' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '644' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '645' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '646' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '647' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '648' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '649' ".
+ ( $country =~ /^(US|CA)$/
+ ?" OR substring($table.payinfo from 1 for 4 ) = '3528' ". # JCB cards in the 3528-3589 range identified as Discover inside US/CA
+ " OR substring($table.payinfo from 1 for 4 ) = '3529' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '353' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '354' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '355' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '356' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '357' ".
+ " OR substring($table.payinfo from 1 for 3 ) = '358' "
+ :""
+ ).
+ " OR substring($table.payinfo from 1 for 3 ) = '622' ". #China Union Pay processed as Discover outside CN
+ " ) ";
+ } elsif ( $cardtype eq 'Maestro' ) {
+ $search =
+ " ( substring($table.payinfo from 1 for 2 ) = '63' ".
+ " OR substring($table.payinfo from 1 for 2 ) = '67' ".
+ " OR substring($table.payinfo from 1 for 6 ) = '564182' ".
+ " OR substring($table.payinfo from 1 for 4 ) = '4936' ".
+ " OR substring($table.payinfo from 1 for 6 ) ".
+ " SIMILAR TO '49030[2-9]' ".
+ " OR substring($table.payinfo from 1 for 6 ) ".
+ " SIMILAR TO '49033[5-9]' ".
+ " OR substring($table.payinfo from 1 for 6 ) ".
+ " SIMILAR TO '49110[1-2]' ".
+ " OR substring($table.payinfo from 1 for 6 ) ".
+ " SIMILAR TO '49117[4-9]' ".
+ " OR substring($table.payinfo from 1 for 6 ) ".
+ " SIMILAR TO '49118[1-2]' ".
+ " ) ";
+ } else {
+ die "unknown card type $cardtype";
+ }
+
+ my $masksearch = $search;
+ $masksearch =~ s/$table\.payinfo/$table.paymask/gi;
+
+ $payby_search = "( $payby_search AND ( $search OR ( $table.paymask IS NOT NULL AND $masksearch ) ) )";
- push @search,
- "( $search OR ( $table.paymask IS NOT NULL AND $masksearch ) )";
+ }
+
+ push @all_payby_search, $payby_search;
}
+
+ push @search, ' ( '. join(' OR ', @all_payby_search). ' ) ' if @all_payby_search;
+
}
if ( $cgi->param('payinfo') ) {
}
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+
push @search, "_date >= $beginning ",
"_date <= $ending";
#here is the agent virtualization
push @search, $curuser->agentnums_sql;
- my $addl_from = ' LEFT JOIN cust_main USING ( custnum ) ';
+ my $addl_from = FS::UI::Web::join_cust_main($table);
my $group_by = '';
if ( $cgi->param('tax_names') ) {
<TR>
<TD ALIGN="right"><% ucfirst(PL($name_singular)) %> of type: </TD>
<TD>
- <SELECT NAME="payby" onChange="payby_changed(this)">
- <OPTION VALUE=""><% mt('all') |h %></OPTION>
- <OPTION VALUE="CARD"><% mt('credit card (all)') |h %></OPTION>
- <OPTION VALUE="CARD-VisaMC"><% mt('credit card (Visa/MasterCard)') |h %></OPTION>
- <OPTION VALUE="CARD-Amex"><% mt('credit card (American Express)') |h %></OPTION>
- <OPTION VALUE="CARD-Discover"><% mt('credit card (Discover)') |h %></OPTION>
- <OPTION VALUE="CARD-Maestro"><% mt('credit card (Maestro/Switch/Solo)') |h %></OPTION>
- <OPTION VALUE="CHEK"><% mt('electronic check / ACH') |h %></OPTION>
- <OPTION VALUE="BILL"><% mt('check') |h %></OPTION>
- <OPTION VALUE="PREP"><% mt('prepaid card') |h %></OPTION>
- <OPTION VALUE="CASH"><% mt('cash') |h %></OPTION>
- <OPTION VALUE="WEST"><% mt('Western Union') |h %></OPTION>
- <OPTION VALUE="MCRD"><% mt('manual credit card') |h %></OPTION>
+ <SELECT NAME="payby" SIZE=10 MULTIPLE>
+%# <OPTION VALUE=""><% mt('all') |h %></OPTION>
+%# <OPTION VALUE="CARD"><% mt('credit card (all)') |h %></OPTION>
+ <OPTION VALUE="CARD-VisaMC" SELECTED><% mt('credit card (Visa/MasterCard)') |h %></OPTION>
+ <OPTION VALUE="CARD-Amex" SELECTED><% mt('credit card (American Express)') |h %></OPTION>
+ <OPTION VALUE="CARD-Discover" SELECTED><% mt('credit card (Discover)') |h %></OPTION>
+ <OPTION VALUE="CARD-Maestro" SELECTED><% mt('credit card (Maestro/Switch/Solo)') |h %></OPTION>
+ <OPTION VALUE="CHEK" SELECTED><% mt('electronic check / ACH') |h %></OPTION>
+ <OPTION VALUE="BILL" SELECTED><% mt('check') |h %></OPTION>
+ <OPTION VALUE="PREP" SELECTED><% mt('prepaid card') |h %></OPTION>
+ <OPTION VALUE="CASH" SELECTED><% mt('cash') |h %></OPTION>
+ <OPTION VALUE="WEST" SELECTED><% mt('Western Union') |h %></OPTION>
+ <OPTION VALUE="MCRD" SELECTED><% mt('manual credit card') |h %></OPTION>
</SELECT>
</TD>
</TR>
- <SCRIPT TYPE="text/javascript">
-
- function payby_changed(what) {
- if ( what.value == 'BILL' ) {
- show('payinfo');
- hide('ccpay');
- } else if ( what.value.match(/^CARD|CHEK/) ) {
- hide('payinfo');
- show('ccpay');
- } else {
- hide('payinfo');
- hide('ccpay');
- }
- }
-
- function show(what) {
- document.getElementById(what+'_caption').style.color = '#000000';
- document.getElementById(what).disabled = false;
- document.getElementById(what).style.backgroundColor = '#ffffff';
- }
-
- function hide(what) {
- document.getElementById(what+'_caption').style.color = '#bbbbbb';
- document.getElementById(what).disabled = true;
- document.getElementById(what).style.backgroundColor = '#dddddd';
- }
-
-
-
- </SCRIPT>
-
<TR>
- <TD ALIGN="right"><FONT ID="payinfo_caption" COLOR="#bbbbbb"><% mt('Check #:') |h %> </FONT></TD>
+ <TD ALIGN="right"><% mt('Check #:') |h %> </TD>
<TD>
- <INPUT TYPE="text" ID="payinfo" NAME="payinfo" DISABLED STYLE="background-color: #dddddd">
+ <INPUT TYPE="text" ID="payinfo" NAME="payinfo">
</TD>
</TR>
<TR>
- <TD ALIGN="right">
- <FONT ID="ccpay_caption" COLOR="#bbbbbb">
- <% mt('Transaction #') |h %>
- </FONT>
- </TD>
+ <TD ALIGN="right"><% mt('Transaction #:') |h %> </TD>
<TD>
- <INPUT TYPE="text" ID="ccpay" NAME="ccpay" DISABLED STYLE="background-color: #dddddd">
+ <INPUT TYPE="text" ID="ccpay" NAME="ccpay">
</TD>
</TR>
<TD>
<TABLE>
<& /elements/tr-input-beginning_ending.html,
- layout => 'horiz',
+ layout => 'horiz',
+ input_time => $conf->exists('report-cust_pay-select_time'),
&>
</TABLE>
</TD>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+my $conf = new FS::Conf;
+
my $void = $cgi->param('void') ? 1 : 0;
my $unapplied = $cgi->param('unapplied') ? 1 : 0;
--- /dev/null
+<%doc>
+
+Example:
+
+ <& elements/report_svc_Common.html,
+
+ #required
+ 'table' => 'svc_something',
+ 'title' => 'Page title',
+
+ #optional
+ 'action' => 'svc_tablename.html', #defaults to svc_tablename.html
+
+ &>
+
+</%doc>
+<& /elements/header.html, $title &>
+
+<FORM ACTION="<% $opt{'action'} || $opt{'table'}. '.html' %>" METHOD="GET">
+<INPUT TYPE="hidden" NAME="magic" VALUE="advanced">
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
+
+ <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
+
+ <TR>
+ <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1"><% mt('Search options') |h %></FONT></TH>
+ </TR>
+
+% unless ( $custnum ) {
+
+ <& /elements/tr-select-agent.html,
+ curr_value => scalar( $cgi->param('agentnum') ),
+ disable_empty => 0,
+ &>
+
+ <& /elements/tr-select-cust_main-status.html,
+ label => 'Customer Status',
+ field => 'cust_status',
+ &>
+
+ <& /elements/tr-select-payby.html,
+ label => emt('Payment method:'),
+ payby_type => 'cust',
+ multiple => 1,
+ all_selected => 1,
+ &>
+
+ <& /elements/tr-input-money.html,
+ label => 'Balance over',
+ field => 'balance',
+ &>
+
+ <& /elements/tr-input-text.html,
+ label => 'Balance age (days)',
+ field => 'balance_days',
+ size => 4,
+ &>
+
+% }
+
+% # just this customer's domains?
+%# <& /elements/tr-select-domain.html,
+%# 'element_name' => 'domsvc',
+%# 'curr_value' => scalar( $cgi->param('domsvc') ),
+%# 'disable_empty' => 0,
+%# &>
+
+ <& /elements/tr-selectmultiple-part_pkg.html &>
+
+ <& /elements/tr-select-part_svc.html,
+ 'svcdb' => $svcdb,
+ 'label' => 'Services',
+ &>
+
+ <TR>
+ <TH CLASS="background" COLSPAN=2> </TH>
+ </TR>
+
+ <TR>
+ <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1"><% mt('Display options') |h %></FONT></TH>
+ </TR>
+
+% #"package fields" ala advanced svc_acct search?
+% #move to /elements/tr-select-cust_pkg-fields and use it from there if so...
+
+ <& /elements/tr-select-cust-fields.html &>
+
+ </TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="<% mt('Get Report') |h %>">
+
+</FORM>
+
+<& /elements/footer.html &>
+<%init>
+
+my(%opt) = @_;
+
+my $svcdb = $opt{'table'};
+
+my $name = "FS::$svcdb"->table_info->{'name_plural'}
+ || PL( "FS::$svcdb"->table_info->{'name'} );
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right("Services: $name: Advanced search");
+
+my $title = $opt{'title'};
+
+#false laziness w/report_cust_pkg.html
+my( $custnum, $cust_main) = ('', '');
+if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+ $custnum = $1;
+ my $cust_main = qsearchs({
+ 'table' => 'cust_main',
+ 'hashref' => { 'custnum' => $custnum },
+ 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+ }) or die "unknown custnum $custnum";
+ $title = mt("$title: [_1]", $cust_main->name);
+}
+
+</%init>
% $bgcolor = $bgcolor1;
% }
- <TR>
+% my $trid = '';
+% if ( $opt{'link_field' } ) {
+% my $link_field = $opt{'link_field'};
+% if ( ref($link_field) eq 'CODE' ) {
+% $trid = &{$link_field}($row);
+% } else {
+% $trid = $row->$link_field();
+% }
+% }
+ <TR ID="<%$trid |h%>">
+
% if ( $opt{'fields'} ) {
%
# miscellany
'download_label' => 'Download this report',
# defaults to 'Download full results'
+ 'link_field' => 'pkgpart'
+ # will create internal links for each row,
+ # with the value of this field as the NAME attribute
+ # If this is a coderef, will evaluate it, passing the
+ # row as an argument, and use the result as the NAME.
&>
</%doc>
my $limit = '';
my($confmax, $maxrecords, $offset );
-unless ( $type =~ /^(csv|\w*.xls)$/) {
+unless ( $type =~ /^(csv|xml|\w*.xls)$/) {
# html mode
unless (exists($opt{count_query}) && length($opt{count_query})) {
( $opt{count_query} = $opt{query} ) =~
--- /dev/null
+<& search.html, %opt &>
+<%doc>
+Currently does nothing but insert the classnames for fields chosen from an
+inventory class.
+</%doc>
+<%init>
+my %opt = @_;
+my $query = $opt{query};
+my $svcdb = $query->{'table'};
+
+# to avoid looking up the inventory class of every service in the database,
+# keep as much of the base query as possible.
+my $item_query = { %$query };
+$item_query->{'table'} = 'inventory_item';
+$item_query->{'addl_from'} =
+ " JOIN ( $svcdb ". $query->{'addl_from'} .
+ ") ON inventory_item.svcnum = $svcdb.svcnum ".
+ " JOIN inventory_class ON (inventory_item.classnum = inventory_class.classnum)";
+# avoid conflict with inventory_item.agentnum
+$item_query->{'extra_sql'} =~ s/ agentnum/ cust_main.agentnum/g;
+$item_query->{'select'} = 'inventory_item.svcnum, '.
+ 'inventory_item.svc_field, '.
+ 'inventory_class.classname';
+my @items = qsearch($item_query);
+my %item_fields;
+foreach my $i (@items) {
+ $item_fields{ $i->svc_field } ||= {};
+ $item_fields{ $i->svc_field }{ $i->svcnum } = $i->classname;
+}
+
+$opt{'sort_fields'} ||= [];
+for ( my $i = 0; $i < @{ $opt{'fields'} }; $i++ ) {
+ my $f = $opt{'fields'}[$i];
+ next if ref($f); # it's not a plain table column
+ $opt{'sort_fields'}[$i] ||= $f;
+ my $classnames = $item_fields{$f}; # hashref of svcnum -> classname
+ next if !$classnames; # there are no inventory items in this column
+ $opt{'fields'}[$i] = sub {
+ my $svc = $_[0];
+ if ( exists($classnames->{$svc->svcnum}) ) {
+ return $svc->$f . '<BR><I>('. $classnames->{$svc->svcnum} . ')</I>';
+ } else {
+ return $svc->$f;
+ }
+ }; #sub
+}
+
+</%init>
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+ unless $FS::CurrentUser::CurrentUser->access_right('Employees: Audit Report');
my %tables = (
cust_pay => 'Payments',
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => $title,
'menubar' => [ 'View inventory classes' =>
<INPUT TYPE="hidden" NAME="classnum" VALUE="$classnum">
<INPUT TYPE="hidden" NAME="avail" VALUE="! .$cgi->param('avail') . '">', #'
'html_foot' => $sub_foot,
- )
-%>
+
+&>
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
' LEFT JOIN part_svc USING ( svcpart ) '.
' LEFT JOIN cust_pkg USING ( pkgnum ) '.
- ' LEFT JOIN cust_main USING ( custnum ) ';
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
my $areboxes = 0;
my $sub_checkbox = sub {
-<% include('elements/search.html',
+<& elements/search.html,
'title' => $title,
'name_singular' => 'member',
'query' => $query,
'header' => [ 'Email address' ],
'fields' => [ $email_sub, ], #just this one for now
'html_init' => $html_init,
- )
-%>
+&>
<%init>
#XXX ACL:
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => $title,
'name_singular' => $name,
'header' => \@header,
'links' => \@links,
'align' => $align,
'sort_fields' => [],
- )
-%>
+
+&>
<%init>
#this is about reports about packages definitions (starting w/commission ones)
my $curuser = $FS::CurrentUser::CurrentUser;
die "access denied"
- unless $curuser->access_right('Financial reports');
+ unless $curuser->access_right('Employees: Commission Report'); #that's all this does so far
my $conf = new FS::Conf;
my $money_char = $conf->config('money_char') || '$';
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Payment Batches',
'name_singular' => 'batch',
'query' => { 'table' => 'pay_batch',
],
'html_init' => $html_init,
'html_foot' => include('.upload_incoming'),
- )
-%>
+&>
<%def .upload_incoming>
% if ( FS::payment_gateway->count("gateway_namespace = 'Business::BatchPayment' AND disabled IS NULL") > 0 ) {
<& /elements/form-file_upload.html,
my($begin, $end) = ( '', '' );
my @where;
-if ( $cgi->param('beginning')
- && $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/ ) {
- $begin = parse_datetime($1);
- push @where, "download >= $begin";
-}
-if ( $cgi->param('ending')
- && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) {
- $end = parse_datetime($1) + 86399;
- push @where, "download < $end";
-}
+
+my($beginning,$ending) = FS::UI::Web::parse_beginning_ending($cgi);
+push @where, "( (download >= $beginning AND download <= $ending) ".
+ ' OR download IS NULL )';
my @status;
if ( $cgi->param('open') ) {
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Phone Number (DID) Search Results',
'name_singular' => 'phone number',
'query' => {
FS::UI::Web::cust_styles(),
'',
],
- )
-%>
+
+&>
<%init>
die "access denied"
my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
#' LEFT JOIN part_svc USING ( svcpart ) '.
' LEFT JOIN cust_pkg USING ( pkgnum ) '.
- ' LEFT JOIN cust_main USING ( custnum ) ';
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
my $count_query = "SELECT COUNT(*) FROM phone_avail $search"; #$addl_from?
+# All of these relationships are left joined in the many-to-one direction,
+# so including $addl_from won't affect the count. Logic!
my $hashref = {};
$hashref->{'ordernum'} = $1 if $cgi->param('ordernum') =~ /^(\d+)$/;
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'LATA Search Results',
'name_singular' => 'LATA',
'query' => {
'',
'',
],
- )
-%>
+
+&>
<%init>
die "access denied"
push @where, FS::cust_main->cust_status_sql . " = '$status'";
}
-if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
$link .= ";cust_classnum=$_" foreach @classnums;
- push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
+ push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Unused Prepaid Cards'.
($agent ? ' for '. $agent->agent : ''),
'menubar' => [
$agent ? [ "${p}edit/agent.cgi?", 'agentnum' ] : '';
},
],
- )
-%>
+
+&>
<%init>
die "access denied"
-<% include('elements/search.html',
+<& elements/search.html,
'title' => 'Prospect Search Results',
'name_singular' => 'prospect',
'query' => $query,
'', #link to contact edit???
],
'agent_virt' => 1,
- )
-%>
+&>
<%init>
die "access denied"
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Qualifications',
'name_singular' => 'qualification',
'query' => { 'table' => 'qual',
'',
'',
],
- )
-%>
+
+&>
<%init>
die "access denied"
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Job Queue',
'name' => 'jobs',
'html_form' => qq!<FORM NAME="jobForm" ACTION="$p/misc/queue.cgi" METHOD="POST">!,
'';
}
},
- )
-
-%>
+
+&>
<%init>
die "access denied"
unless $curuser->access_right('List quotations');
my $join_prospect_main = 'LEFT JOIN prospect_main USING ( prospectnum )';
-my $join_cust_main = 'LEFT JOIN cust_main ON ( quotation.custnum = cust_main.custnum )';
+my $join_cust_main = FS::UI::Web::join_cust_main('quotation');
#here is the agent virtualization
my $agentnums_sql = ' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Unused Registration Codes for '.
$agent->agent,
'name' => 'registration codes',
#$plink,
'',
],
- )
-%>
+
+&>
<%init>
die "access denied"
'table' => 'part_pkg_report_option',
'name_col' => 'name',
'hashref' => { 'disabled' => '' },
- 'element_name' => 'partv_report_option',
+ 'element_name' => 'part5_report_option',
+ 'curr_value' =>
+ FS::Report::FCC_477::restore_fcc477map("part5_report_option"),
)
%>
</TD>
--- /dev/null
+<% include('/elements/header.html', 'Agent commission report' ) %>
+
+<FORM ACTION="agent_commission.html">
+
+<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
+
+<% include( '/elements/tr-select-agent.html', disable_empty => 1 ) %>
+
+<% include( '/elements/tr-input-beginning_ending.html', ) %>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Get Report">
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
<INPUT TYPE="hidden" NAME="magic" VALUE="_date">
<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
-<TABLE BGCOLOR="#cccccc" CELLSPACING=0
+<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
% unless ( $custnum ) {
<& /elements/tr-select-agent.html,
</TR>
% }
- <& /elements/tr-select-cust_tag.html,
- 'cgi' => $cgi,
- 'is_report' => 1,
- 'multiple' => 1,
- &>
+ <TR>
+ <TD ALIGN="right">Tags</TD>
+ <TD>
+ <& /elements/select-cust_tag.html,
+ 'cgi' => $cgi,
+ 'is_report' => 1,
+ 'multiple' => 1,
+ &>
+ <DIV STYLE="display:inline-block; vertical-align:baseline">
+ <INPUT TYPE="radio" NAME="all_tags" VALUE="0" CHECKED> Any of these
+ <BR>
+ <INPUT TYPE="radio" NAME="all_tags" VALUE="1"> All of these
+ </DIV>
+ </TD>
+ </TR>
<& /elements/tr-select-payby.html,
'payby_type' => 'cust',
</TR>
<TR>
+ <TD ALIGN="right" VALIGN="center"><% mt('With postal mail invoices') |h %></TD>
+ <TD><INPUT TYPE="checkbox" NAME="POST" ID="POST" onClick="POST_changed();"></TD>
+ </TR>
+
+ <TR>
<TD ALIGN="right" VALIGN="center"><% mt('Without postal mail invoices') |h %></TD>
- <TD><INPUT TYPE="checkbox" NAME="no_POST"></TD>
+ <TD><INPUT TYPE="checkbox" NAME="no_POST" ID="no_POST" onClick="no_POST_changed();"></TD>
</TR>
+ <SCRIPT TYPE="text/javascript">
+ function POST_changed() {
+ if ( document.getElementById('POST').checked == true ) {
+ document.getElementById('no_POST').checked = false;
+ }
+ }
+ function no_POST_changed() {
+ if ( document.getElementById('no_POST').checked == true ) {
+ document.getElementById('POST').checked = false;
+ }
+ }
+ </SCRIPT>
+
<TR>
<TH CLASS="background" COLSPAN=2> </TH>
</TR>
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+ unless $FS::CurrentUser::CurrentUser->access_right('Employees: Audit Report');
my %tables = (
cust_pay => 'Payments',
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+ unless $FS::CurrentUser::CurrentUser->access_right('Employees: Commission Report');
</%init>
<& /elements/tr-select-cust_main-status.html,
'label' => emt('Customer Status'),
&>
-
+
+ <& /elements/tr-select-cust_class.html,
+ 'label' => emt('Customer class'),
+ 'field' => 'cust_classnum',
+ 'multiple' => 1,
+ 'pre_options' => [ '' => emt('(none)') ],
+ 'all_selected' => 1,
+ &>
+
<TR>
<TD ALIGN="right"><% mt('Customers') |h %></TD>
<TD>
'empty_label' => 'all',
&>
-% my @exporttypes = map { "'$_'" } qw(sqlradius broadband_sqlradius);
+%#more future-proof to actually ask all exports if they ->can('usage_sessions')
+% my @exporttypes = qw( sqlradius sqlradius_withdomain broadband_sqlradius
+% phone_sqlradius radiator
+% );
<& /elements/tr-select-table.html,
'label' => 'Export',
'table' => 'part_export',
'name_col' => 'label',
'hashref' => {},
- 'extra_sql' => ' WHERE exporttype IN('.join(',', @exporttypes).')',
+ 'extra_sql' => ' WHERE exporttype IN ( '.
+ join(',', map "'$_'", @exporttypes).
+ ')',
'disable_empty' => 1,
'order_by' => 'ORDER BY exportnum',
&>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Services: Accounts: Advanced search'); #?
-my $title = emt('Account Report');
+my $title = mt('Account Report');
#false laziness w/report_cust_pkg.html
my $custnum = '';
'hashref' => { 'custnum' => $custnum },
'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
}) or die "unknown custnum $custnum";
- $title = emt("Account Report: [_1]", $cust_main->name);
+ $title = mt("Account Report: [_1]", $cust_main->name);
}
</%init>
-<% include('/elements/header.html', 'Phone number total usage' ) %>
+<& elements/report_svc_Common.html,
+ 'table' => 'svc_phone',
+ 'title' => 'Phone number report',
-<FORM ACTION="svc_phone.cgi" METHOD="GET">
-
-<INPUT TYPE="hidden" NAME="magic" VALUE="all">
-<INPUT TYPE="hidden" NAME="usage_total" VALUE="1">
-
-<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
-
-%# <TR>
-%# <TH CLASS="background" COLSPAN=2 ALIGN="left">
-%# <FONT SIZE="+1">Search options</FONT>
-%# </TH>
-%# </TR>
-
- <% include ( '/elements/tr-input-beginning_ending.html', prefix=>'usage' ) %>
-
-</TABLE>
-
-<BR>
-<INPUT TYPE="submit" VALUE="Search phone numbers">
-
-</FORM>
-
-<% include('/elements/footer.html') %>
-<%init>
-
-#? 'List services' ? something new?
-die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('List rating data');
-
-</%init>
+ 'action' => 'svc_phone.cgi',
+&>
--- /dev/null
+<% include('/elements/header.html', 'Phone number total usage' ) %>
+
+<FORM ACTION="svc_phone.cgi" METHOD="GET">
+
+<INPUT TYPE="hidden" NAME="magic" VALUE="all">
+<INPUT TYPE="hidden" NAME="usage_total" VALUE="1">
+
+<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
+
+%# <TR>
+%# <TH CLASS="background" COLSPAN=2 ALIGN="left">
+%# <FONT SIZE="+1">Search options</FONT>
+%# </TH>
+%# </TR>
+
+ <% include ( '/elements/tr-input-beginning_ending.html', prefix=>'usage' ) %>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Search phone numbers">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+#? 'List services' ? something new?
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List rating data');
+
+</%init>
my $out = 'Out of taxable region(s)';
my %label_opt = ( out => 1 ); #enable 'Out of Taxable Region' label
-$label_opt{no_city} = 1 unless $cgi->param('show_cities');
-$label_opt{no_taxclass} = 1 unless $cgi->param('show_taxclasses');
+$label_opt{with_city} = 1 if $cgi->param('show_cities');
+$label_opt{with_district} = 1 if $cgi->param('show_districts');
+
+$label_opt{with_taxclass} = 1 if $cgi->param('show_taxclasses');
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
my $tot_credit = 0;
my @loc_params = qw(country state county);
-push @loc_params, qw(city district) if $cgi->param('show_cities');
+push @loc_params, 'city' if $cgi->param('show_cities');
+push @loc_params, 'district' if $cgi->param('show_districts');
foreach my $r ( qsearch({ 'table' => 'cust_main_county', })) {
my $taxnum = $r->taxnum;
}
if ( $cgi->param('show_taxclasses') ) {
- my $base_label = $r->label(%label_opt, 'no_taxclass' => 1);
+ my $base_label = $r->label(%label_opt, 'with_taxclass' => 0);
$base_regions{$base_label} ||=
{
label => $base_label,
% if ( $city ) {
<TR>
- <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_cities" VALUE="1"></TD>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_cities" VALUE="1" onclick="toggle_show_cities(this)"></TD>
<TD>Show cities</TD>
</TR>
+ <TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_districts" VALUE="1" DISABLED></TD>
+ <TD>Show districts</TD>
+ </TR>
+ <SCRIPT TYPE="text/javascript">
+ function toggle_show_cities() {
+ what = document.getElementsByName('show_cities')[0];
+ what.form.show_districts.disabled = !what.checked;
+ what.form.show_districts.checked = what.checked;
+ }
+ toggle_show_cities();
+ </SCRIPT>
% }
% if ( $conf->exists('enable_taxclasses') ) {
-<% include('elements/search.html',
+<& elements/search.html,
'title' => 'Time worked summary',
'name_singular' => 'ticket',
'query' => $query,
'count_query' => $count_query,
'count_addl' => [ $format_seconds_sub,
- $applied_time ? $format_seconds_sub : () ],
+ $applied ? $format_seconds_sub : () ],
'header' => [ 'Ticket #',
'Ticket',
'Time',
- $applied_time ? 'Applied' : (),
+ $applied ? 'Applied' : (),
],
'fields' => [ 'ticketid',
sub { encode_entities(shift->get('subject')) },
sub { my $seconds = shift->get('ticket_time');
&{ $format_seconds_sub }( $seconds );
},
- ($applied_time ?
+ ($applied ?
sub { my $seconds = shift->get('applied_time');
&{ $format_seconds_sub }( $seconds );
} : () ),
'sort_fields' => [ 'ticketid',
'subject',
'transaction_time',
- $applied_time ? 'applied_time' : (),
+ $applied ? 'applied_time' : (),
],
'links' => [
$link,
'',
'',
],
- )
-%>
+&>
<%once>
my $format_seconds_sub = sub {
);
my @select_total = ( 'COUNT(*)' );
-my ($transaction_time, $applied_time);
my $join = 'JOIN Users ON Transactions.Creator = Users.Id '; #.
my $twhere = "
AND Transactions.ObjectId = Tickets.Id
";
+my $transaction_time;
+my $applied = '';
my $cfname = '';
if ( $cgi->param('cfname') =~ /^\w(\w|\s)*$/ ) {
$twhere .= " AND CustomFields.Name = '$cfname'
AND (ocfv_new.Id IS NOT NULL OR ocfv_old.Id IS NOT NULL OR ocfv_main.Id IS NOT NULL)";
-}
-else {
+} else {
+
$transaction_time = "
CASE transactions.type when 'Set'
THEN (to_number(newvalue,'999999')-to_number(oldvalue, '999999')) * 60
ELSE timetaken*60
END";
- my $applied = '';
if ( $cgi->param('svcnum') =~ /^\s*(\d+)\s*$/ ) {
$twhere .= " AND EXISTS( SELECT 1 FROM acct_rt_transaction WHERE acct_rt_transaction.transaction_id = Transactions.id AND svcnum = $1 )";
$applied = "AND svcnum = $1";
AND ( ( Transactions.Type = 'Set'
AND Transactions.Field = 'TimeWorked'
AND Transactions.NewValue != Transactions.OldValue )
- OR ( ( Transactions.Type='Create' OR Transactions.Type='Comment' OR Transactions.Type='Correspond' OR Transactions.Type='Touch' )
+ OR ( Transactions.Type IN ( 'Create', 'Comment', 'Correspond', 'Touch' )
AND Transactions.TimeTaken > 0
)
)";
- $applied_time = "( SELECT SUM(support) FROM acct_rt_transaction LEFT JOIN Transactions ON ( transaction_id = Id ) $twhere $applied )";
-
}
push @select, "$ticket_time AS ticket_time";
push @select_total, "SUM($ticket_time)";
-if ( $applied_time) {
+if ( $applied ) {
+
+ my $applied_time = "( SELECT SUM(support) FROM acct_rt_transaction LEFT JOIN Transactions ON ( transaction_id = Id ) $twhere $applied )";
+
push @select, "$applied_time AS applied_time";
push @select_total, "SUM($applied_time)";
+
}
my $query = {
-<% include('elements/search.html',
+<& elements/search.html,
'title' => 'Time worked',
'name_singular' => 'transaction',
'query' => $query,
'',
'',
],
- )
-%>
+&>
<%once>
my $format_seconds_sub = sub {
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Query Results',
'name' => 'rows',
'query' => "SELECT $sql",
- )
-%>
+
+&>
<%init>
die "access denied"
% @{ $part_export->usage_sessions( {
% 'stoptime_start' => $beginning,
% 'stoptime_end' => $ending,
-% 'open_sessions' => $open_sessions,
+% 'session_status' => $status,
% 'starttime_start' => $starttime_beginning,
% 'starttime_end' => $starttime_ending,
% 'svc_acct' => $cgi_svc_acct,
$ending = $1;
}
-my $open_sessions = '';
-if ( $cgi->param('open_sessions') =~ /^(\d*)$/ ) {
- $open_sessions = $1;
+my $status = '';
+if ( $cgi->param('session_status') =~ /^(closed|open)$/ ) {
+ $status = $1;
}
my( $starttime_beginning, $starttime_ending ) = ( '', '' );
$pretty;
};
+my $time_format_or_open = sub {
+ my $time = shift;
+ return '<CENTER>OPEN</CENTER>' if $time == 0;
+ &{$time_format}($time);
+};
+
my $duration_format = sub {
my $seconds = shift;
+ return '' if $seconds eq ''; # open session
my $hour = int($seconds/3600);
my $min = int( ($seconds%3600) / 60 );
my $sec = $seconds%60;
'acctstoptime' => {
name => 'End time',
attrib => 'Acct-Stop-Time',
- fmt => $time_format,
+ fmt => $time_format_or_open,
align => 'left',
},
'acctsessiontime' => {
<TR>
<TD>Show:</TD>
<TD>
- <INPUT TYPE="radio" NAME="open_sessions" VALUE="0" onClick="open_changed(this);" CHECKED>Completed sessions<BR>
- <INPUT TYPE="radio" NAME="open_sessions" VALUE="1" onClick="open_changed(this);">Open sessions
+ <INPUT TYPE="radio" NAME="session_status" VALUE="" onClick="enable_stop(true);" CHECKED>All sessions<BR>
+ <INPUT TYPE="radio" NAME="session_status" VALUE="closed" onClick="enable_stop(true);">Completed sessions<BR>
+ <INPUT TYPE="radio" NAME="session_status" VALUE="open" onClick="enable_stop(false);">Open sessions
</TD>
</TR>
<SCRIPT TYPE="text/javascript">
- function open_changed(what) {
-
- var value=get_open_value(what);
- if ( value == '1' ) {
- what.form.stoptime_beginning_text.disabled = true;
- what.form.stoptime_ending_text.disabled = true;
- what.form.stoptime_beginning_text.style.backgroundColor = '#dddddd';
- what.form.stoptime_ending_text.style.backgroundColor = '#dddddd';
- what.form.stoptime_beginning_button.style.display = 'none';
- what.form.stoptime_ending_button.style.display = 'none';
- what.form.stoptime_beginning_disabled.style.display = '';
- what.form.stoptime_ending_disabled.style.display = '';
- } else if ( value == '0' ) {
- what.form.stoptime_beginning_text.disabled = false;
- what.form.stoptime_ending_text.disabled = false;
- what.form.stoptime_beginning_text.style.backgroundColor = '#ffffff';
- what.form.stoptime_ending_text.style.backgroundColor = '#ffffff';
- what.form.stoptime_beginning_button.style.display = '';
- what.form.stoptime_ending_button.style.display = '';
- what.form.stoptime_beginning_disabled.style.display = 'none';
- what.form.stoptime_ending_disabled.style.display = 'none';
+ function enable_stop(value) {
+
+ var f = document.OneTrueForm;
+ if ( value ) {
+ f.stoptime_beginning_text.disabled = false;
+ f.stoptime_ending_text.disabled = false;
+ f.stoptime_beginning_text.style.backgroundColor = '#ffffff';
+ f.stoptime_ending_text.style.backgroundColor = '#ffffff';
+ f.stoptime_beginning_button.style.display = '';
+ f.stoptime_ending_button.style.display = '';
+ f.stoptime_beginning_disabled.style.display = 'none';
+ f.stoptime_ending_disabled.style.display = 'none';
+ } else {
+ f.stoptime_beginning_text.disabled = true;
+ f.stoptime_ending_text.disabled = true;
+ f.stoptime_beginning_text.style.backgroundColor = '#dddddd';
+ f.stoptime_ending_text.style.backgroundColor = '#dddddd';
+ f.stoptime_beginning_button.style.display = 'none';
+ f.stoptime_ending_button.style.display = 'none';
+ f.stoptime_beginning_disabled.style.display = '';
+ f.stoptime_ending_disabled.style.display = '';
}
}
- function get_open_value(what) {
- var rad_val = '';
- for (var i=0; i < what.form.open_sessions.length; i++) {
- if (what.form.open_sessions[i].checked) {
- var rad_val = what.form.open_sessions[i].value;
- }
- }
- return rad_val;
- }
-
</SCRIPT>
<TR>
-<& elements/search.html,
+<& elements/svc_Common.html,
'title' => emt('Account Search Results'),
'name' => emt('accounts'),
'query' => $sql_query,
-<% include( 'elements/search.html',
+<& elements/svc_Common.html,
'title' => 'Broadband Search Results',
'name' => 'broadband services',
'html_init' => $html_init,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
die "access denied" unless
}
if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
- $search_hash{'order_by'} = $1;
+ $search_hash{'order_by'} = "ORDER BY $1";
}
my $sql_query = FS::svc_broadband->search(\%search_hash);
-<% include( 'elements/search.html',
+<& elements/svc_Common.html,
'title' => 'Dish Network Search Results',
'name' => 'services',
'query' => $sql_query,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
die "access denied"
my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
' LEFT JOIN part_svc USING ( svcpart ) '.
' LEFT JOIN cust_pkg USING ( pkgnum ) '.
- ' LEFT JOIN cust_main USING ( custnum ) ';
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
#here is the agent virtualization
push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => "Domain Search Results",
'name' => 'domains',
'query' => $sql_query,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
die "access denied"
my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
' LEFT JOIN part_svc USING ( svcpart ) '.
' LEFT JOIN cust_pkg USING ( pkgnum ) '.
- ' LEFT JOIN cust_main USING ( custnum ) ';
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
#here is the agent virtualization
push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
-<% include( 'elements/search.html',
+<& elements/svc_Common.html,
'title' => 'External service search results',
'name' => 'external services',
'query' => $sql_query,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
-
+
+&>
<%init>
die "access denied"
my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
' LEFT JOIN part_svc USING ( svcpart ) '.
' LEFT JOIN cust_pkg USING ( pkgnum ) '.
- ' LEFT JOIN cust_main USING ( custnum ) ';
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
#here is the agent virtualization
push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => "Mail forward Search Results",
'name' => 'mail forwards',
'query' => $sql_query,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
die "access denied"
my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
' LEFT JOIN part_svc USING ( svcpart ) '.
' LEFT JOIN cust_pkg USING ( pkgnum ) '.
- ' LEFT JOIN cust_main USING ( custnum ) ';
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
#here is the agent virtualization
push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
-<% include('elements/search.html',
+<& elements/svc_Common.html,
'title' => 'Hardware service search results',
'name' => 'installations',
'query' => $sql_query,
FS::UI::Web::cust_colors() ],
'style' => [ $svc_cancel_style, ('') x 7,
FS::UI::Web::cust_styles() ],
- )
-%>
+&>
<%init>
die "access denied"
my $addl_from = '
LEFT JOIN cust_svc USING ( svcnum )
LEFT JOIN part_svc USING ( svcpart )
- LEFT JOIN cust_pkg USING ( pkgnum )
- LEFT JOIN cust_main USING ( custnum )
+ LEFT JOIN cust_pkg USING ( pkgnum )'.
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').'
LEFT JOIN hardware_type USING ( typenum )';
my @extra_sql;
-<% include( 'elements/search.html',
+<& elements/svc_Common.html,
'title' => "Phone number search results",
'name' => 'phone numbers',
'query' => $sql_query,
'Country code',
'Phone number',
@header,
- FS::UI::Web::cust_header(),
+ FS::UI::Web::cust_header($cgi->param('cust_fields')),
],
'fields' => [ 'svcnum',
'svc',
$link,
( map '', @header ),
( map { $_ ne 'Cust. Status' ? $link_cust : '' }
- FS::UI::Web::cust_header()
+ FS::UI::Web::cust_header($cgi->param('cust_fields'))
),
],
'align' => 'rlrr'.
( map '', @header ),
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
die "access denied"
my $conf = new FS::Conf;
my @select = ();
-my %svc_phone = ();
-my @extra_sql = ();
my $orderby = 'ORDER BY svcnum';
my @header = ();
my $link = [ "${p}view/svc_phone.cgi?", 'svcnum' ];
my $redirect = $link;
+my %search_hash = ();
+my @extra_sql = ();
+
if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
- push @extra_sql, 'pkgnum IS NULL'
+ $search_hash{'unlinked'} = 1
if $cgi->param('magic') eq 'unlinked';
if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
}
+} elsif ( $cgi->param('magic') =~ /^advanced$/ ) {
+
+ for (qw( agentnum custnum cust_status balance balance_days cust_fields )) {
+ $search_hash{$_} = $cgi->param($_) if length($cgi->param($_));
+ }
+
+ for (qw( payby pkgpart svcpart )) {
+ $search_hash{$_} = [ $cgi->param($_) ] if $cgi->param($_);
+ }
+
} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
- push @extra_sql, "svcpart = $1";
+ $search_hash{'svcpart'} = [ $1 ];
} else {
$cgi->param('phonenum') =~ /^([\d\- ]+)$/;
- ( $svc_phone{'phonenum'} = $1 ) =~ s/\D//g;
+ my $phonenum = $1;
+ $phonenum =~ s/\D//g;
+ push @extra_sql, "phonenum = '$phonenum'";
}
-my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
- ' LEFT JOIN part_svc USING ( svcpart ) '.
- ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
- ' LEFT JOIN cust_main USING ( custnum ) ';
-
-#here is the agent virtualization
-push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
- 'null_right' => 'View/link unlinked services'
- );
-
-my $extra_sql = '';
-if ( @extra_sql ) {
- $extra_sql = ( keys(%svc_phone) ? ' AND ' : ' WHERE ' ).
- join(' AND ', @extra_sql );
-}
+$search_hash{'addl_select'} = \@select;
+$search_hash{'order_by'} = $orderby;
+$search_hash{'where'} = \@extra_sql;
-my $count_query = "SELECT COUNT(*) FROM svc_phone $addl_from ";
-if ( keys %svc_phone ) {
- $count_query .= ' WHERE '.
- join(' AND ', map "$_ = ". dbh->quote($svc_phone{$_}),
- keys %svc_phone
- );
-}
-$count_query .= $extra_sql;
-
-my $sql_query = {
- 'table' => 'svc_phone',
- 'hashref' => \%svc_phone,
- 'select' => join(', ',
- 'svc_phone.*',
- 'part_svc.svc',
- @select,
- 'cust_main.custnum',
- FS::UI::Web::cust_sql_fields(),
- ),
- 'extra_sql' => $extra_sql,
- 'order_by' => $orderby,
- 'addl_from' => $addl_from,
-};
+my $sql_query = FS::svc_phone->search(\%search_hash);
+my $count_query = delete($sql_query->{'count_query'});
#smaller false laziness w/svc_*.cgi here
my $link_cust = sub {
-<% include( 'elements/search.html',
+<& elements/svc_Common.html,
'title' => 'Virtual Host Search Results',
'name' => 'virtual hosts',
'query' => $sql_query,
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
die "access denied"
my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
' LEFT JOIN part_svc USING ( svcpart ) '.
' LEFT JOIN cust_pkg USING ( pkgnum ) '.
- ' LEFT JOIN cust_main USING ( custnum ) ';
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
#here is the agent virtualization
push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Time Worked',
'name' => 'time',
'html_form' => qq!<FORM NAME="timeForm" ACTION="${p}misc/timeworked.html" METHOD="POST">!,
'',
],
'html_foot' => $html_foot,
- )
-
-%>
+
+&>
<%init>
die "access denied"
push @where, "cust_bill._date >= $beginning",
"cust_bill._date <= $ending";
-if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
- push @where, "cust_main.agentnum = $1";
-}
-
-if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
- push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
#usage always excluded
# always 'nottax', not 'istax'
-$join_cust = ' JOIN cust_bill USING ( invnum )
- LEFT JOIN cust_main USING ( custnum ) ';
+$join_cust = ' JOIN cust_bill USING ( invnum ) '.
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
$join_pkg .= ' LEFT JOIN cust_pkg USING ( pkgnum )
LEFT JOIN part_pkg USING ( pkgpart )
my $count_query = "SELECT COUNT(DISTINCT billpkgnum),
SUM( $unearned_base ), SUM( $unearned_sql )
- FROM cust_bill_pkg $join_cust $join_pkg $where";
+ FROM cust_bill_pkg $join_pkg $join_cust $where";
push @select, 'part_pkg.pkg',
'part_pkg.freq',
my $query = {
'table' => 'cust_bill_pkg',
- 'addl_from' => "$join_cust $join_pkg",
+ 'addl_from' => "$join_pkg $join_cust",
'hashref' => {},
'select' => join(",\n", @select ),
'extra_sql' => $where,
-<% include( 'elements/search.html',
+<& elements/search.html,
'title' => 'Unprovisioned Service Search Results',
'name' => 'packages with unprovisioned services',
'query' => {
'',
FS::UI::Web::cust_styles(),
],
- )
-%>
+
+&>
<%init>
die "access denied"
. " cust_svc.pkgnum = cust_pkg.pkgnum and "
. " cust_svc.svcpart = pkg_svc.svcpart) $svcpart_limit";
-my $addl_from = " join pkg_svc using (pkgpart) join cust_main using (custnum) ";
+my $addl_from = " join pkg_svc using (pkgpart) ".
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
# this was very painful to derive but it appears correct
#select cust_pkg.custnum,cust_pkg.pkgpart,cust_pkg.pkgnum, pkg_svc.svcpart from cust_pkg join
'hashref' => { },
'addl_from' =>
'LEFT JOIN cust_bill USING ( invnum ) '.
- 'LEFT JOIN cust_main USING ( custnum )',
+ FS::UI::Web::join_cust_main('cust_bill'),
'extra_sql' => " WHERE batchnum = $batchnum",
},
'count_query' => "SELECT COUNT(*) FROM cust_bill_batch WHERE batchnum = $batchnum",
<TD ALIGN="right"><% mt('Email address(es)') |h %></TD>
<TD BGCOLOR="#ffffff">
<% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) || $no %>
+% if ( $cust_main->message_noemail ) {
+ <BR>
+ <SPAN STYLE="font-size: small"><% emt('(do not send notices)') %></SPAN>
+% }
</TD>
</TR>
% }
'svc_external' => 'External service',
'svc_phone' => 'Phone',
'phone_device' => 'Phone device',
+ 'cust_pkg_discount' => 'Discount',
#? it gets provisioned anyway 'phone_avail' => 'Phone',
;
-my $svc_join = 'JOIN cust_svc USING ( svcnum ) JOIN cust_pkg USING ( pkgnum )';
+my $pkg_join = "JOIN cust_pkg USING ( pkgnum )";
+my $svc_join = "JOIN cust_svc USING ( svcnum ) $pkg_join";
my %table_join = (
'svc_acct' => $svc_join,
'svc_external' => $svc_join,
'svc_phone' => $svc_join,
'phone_device' => $svc_join,
+ 'cust_pkg_discount'=> $pkg_join,
);
my $curuser = $FS::CurrentUser::CurrentUser;
-die "access deined"
+die "access denied"
unless $curuser->access_right('View customer history');
# find out the beginning of this customer history, if possible
+<STYLE TYPE="text/css">
+td.package {
+ vertical-align: top;
+ border-width: 0;
+ border-style: solid;
+ border-color: #bbbbff;
+}
+table.package {
+ border: none;
+ padding: 0;
+ border-spacing: 0;
+ width: 100%;
+}
+table.usage {
+ border: 1px solid black;
+ margin: auto;
+ width: 60%;
+ border-spacing: 0px;
+}
+.shared > * {
+ background-color: #ffffaa;
+}
+.row0 { background-color: #eeeeee; }
+.row1 { background-color: #ffffff; }
+
+</STYLE>
% my $s = 0;
% if ( $curuser->access_right('Qualify service') ) {
<TR>
<TD COLSPAN=2>
-% if ( $conf->exists('cust_pkg-group_by_location') and $show_location ) {
+% if ( $conf->exists('cust_pkg-group_by_location') ) {
<& locations.html,
'cust_main' => $cust_main,
'packages' => $packages,
<& packages/section.html,
'cust_main' => $cust_main,
'packages' => $packages,
- 'show_location' => $show_location,
&>
</TABLE>
% }
my( $packages, $num_old_packages ) = get_packages($cust_main, $conf);
-
-my $show_location = $conf->exists('cust_pkg-always_show_location')
- || (grep $_->locationnum, @$packages); # ? '1' : '0';
-
my $countrydefault = scalar($conf->config('countrydefault')) || 'US';
#subroutines
}
$num_old_packages -= scalar(@packages);
+
+ # don't include supplemental packages in this list; they'll be found from
+ # their main packages
+ @packages = grep !$_->main_pkgnum, @packages;
( \@packages, $num_old_packages );
}
--- /dev/null
+% if ( $contact ) {
+ <% $contact->line |h %>
+% if ( $show_link ) {
+ <FONT SIZE=-1>
+ ( <%pkg_change_contact_link($cust_pkg)%> )
+ </FONT>
+% }
+% } elsif ( $show_link ) {
+ <FONT SIZE=-1>
+ ( <%pkg_add_contact_link($cust_pkg)%> )
+ </FONT>
+% }
+<%init>
+
+my $conf = new FS::Conf;
+my %opt = @_;
+
+my $cust_pkg = $opt{'cust_pkg'};
+
+my $show_link =
+ ! $cust_pkg->get('cancel')
+ && $FS::CurrentUser::CurrentUser->access_right('Change customer package');
+
+my $contact = $cust_pkg->contact_obj;
+
+sub pkg_change_contact_link {
+ my $cust_pkg = shift;
+ #my $pkgpart = $cust_pkg->pkgpart;
+ include( '/elements/popup_link-cust_pkg.html',
+ 'action' => $p. "misc/change_pkg_contact.html",
+ 'label' => emt('Change'), # contact'),
+ 'actionlabel' => emt('Change'),
+ 'cust_pkg' => $cust_pkg,
+ 'width' => 616,
+ 'height' => 220,
+ );
+}
+
+sub pkg_add_contact_link {
+ my $cust_pkg = shift;
+ #my $pkgpart = $cust_pkg->pkgpart;
+ include( '/elements/popup_link-cust_pkg.html',
+ 'action' => $p. "misc/change_pkg_contact.html",
+ 'label' => emt('Add contact'),
+ 'actionlabel' => emt('Change'),
+ 'cust_pkg' => $cust_pkg,
+ 'width' => 616,
+ 'height' => 192,
+ );
+}
+
+#sub edit_contact_link {
+# my $contactnum = shift;
+# include( '/elements/popup_link.html',
+# 'action' => $p. "edit/cust_contact.cgi?contactnum=$contactnum",
+# 'label' => emt('Edit contact'),
+# 'actionlabel' => emt('Edit'),
+# );
+#}
+
+</%init>
-<TD CLASS="inv" BGCOLOR="<% $bgcolor %>" WIDTH="20%">
-
-% unless ( $cust_pkg->locationnum ) {
- <I><FONT SIZE=-1>(<% mt('default service address') |h %>)</FONT><BR>
+% if ( $default ) {
+ <DIV STYLE="font-style: italic; font-size: small">
% }
<% $loc->location_label( 'join_string' => '<BR>',
</FONT>
% }
-% unless ( $cust_pkg->locationnum ) {
- </I>
+% if ( $default ) {
+ </DIV>
% }
% if ( ! $cust_pkg->get('cancel')
</FONT>
% }
-</TD>
<%init>
my $conf = new FS::Conf;
my %opt = @_;
-my $bgcolor = $opt{'bgcolor'};
my $cust_pkg = $opt{'cust_pkg'};
my $countrydefault = $opt{'countrydefault'} || 'US';
my $statedefault = $opt{'statedefault'}
|| ($countrydefault eq 'US' ? 'CA' : '');
my $loc = $cust_pkg->cust_location_or_main;
+# dubious--they should all have a location now
+my $default = $cust_pkg->locationnum == $opt{'cust_main'}->ship_locationnum;
sub pkg_change_location_link {
my $cust_pkg = shift;
-<TD CLASS="inv" BGCOLOR="<% $bgcolor %>" VALIGN="top">
- <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
+<TD CLASS="inv package" BGCOLOR="<% $bgcolor %>" VALIGN="top"
+ STYLE="border-left-width: <% $supplemental * 30 %>px">
+ <TABLE CLASS="inv package">
<TR>
<TD COLSPAN=2>
<A NAME="cust_pkg<% $cust_pkg->pkgnum %>"
<B><% $cust_pkg->quantity %></B>
</TD>
</TR>
-% }
+% }
<TR>
<TD COLSPAN=2>
<FONT SIZE=-1>
-% unless ( $cust_pkg->get('cancel') ) {
+% unless ( $cust_pkg->get('cancel') ) {
%
-% my $br = 0;
-% if ( $curuser->access_right('Change customer package') ) {
-% $br=1;
- ( <%pkg_change_link($cust_pkg)%> )
-% }
+% if ( $supplemental or $part_pkg->freq eq '0' ) {
+% # Supplemental packages can't be changed independently.
+% # One-time charges don't need to be changed.
+% # For both of those, we only show "Edit dates", "Add comments",
+% # and "Add invoice details".
+% if ( $curuser->access_right('Edit customer package dates') ) {
+ ( <%pkg_dates_link($cust_pkg)%> )
+% }
+% } else {
+% # the usual case: links to change package definition,
+% # discount, and customization
+% my $br = 0;
+% if ( $curuser->access_right('Change customer package') ) {
+% $br=1;
+ ( <%pkg_change_link($cust_pkg)%> )
+% }
%
-% if ( $curuser->access_right('Edit customer package dates') ) {
-% $br=1;
- ( <%pkg_dates_link($cust_pkg)%> )
-% }
+% if ( $curuser->access_right('Edit customer package dates') ) {
+% $br=1;
+ ( <%pkg_dates_link($cust_pkg)%> )
+% }
%
-% if ( $curuser->access_right('Discount customer package')
-% && $part_pkg->can_discount
-% && ! scalar($cust_pkg->cust_pkg_discount_active)
-% && ! scalar($cust_pkg->part_pkg->part_pkg_discount)
-% )
-% {
-% $br=1;
- ( <%pkg_discount_link($cust_pkg)%> )
-% }
+% if ( $curuser->access_right('Discount customer package')
+% && $part_pkg->can_discount
+% && ! scalar($cust_pkg->cust_pkg_discount_active)
+% && ! scalar($cust_pkg->part_pkg->part_pkg_discount)
+% )
+% {
+% $br=1;
+ ( <%pkg_discount_link($cust_pkg)%> )
+% }
%
-% if ( $curuser->access_right('Customize customer package') ) {
-% $br=1;
- ( <%pkg_customize_link($cust_pkg,$part_pkg)%> )
-% }
+% if ( $curuser->access_right('Customize customer package') ) {
+% $br=1;
+ ( <%pkg_customize_link($cust_pkg,$part_pkg)%> )
+% }
%
- <% $br ? '<BR>' : '' %>
-% }
+ <% $br ? '<BR>' : '' %>
+% }
-% if ( $cust_pkg->num_cust_event
-% && ( $curuser->access_right('Billing event reports')
-% || $curuser->access_right('View customer billing events')
-% )
-% ) {
- ( <%pkg_event_link($cust_pkg)%> )
-% }
+% if ( $cust_pkg->num_cust_event
+% && ( $curuser->access_right('Billing event reports')
+% || $curuser->access_right('View customer billing events')
+% )
+% ) {
+ ( <%pkg_event_link($cust_pkg)%> )
+% }
+% } #!$supplemental
</FONT>
</TD>
</TR>
% if ( $curuser->access_right('Change customer package') and
% !$cust_pkg->get('cancel') and
-% !$opt{'show_location'}) {
+% !$supplemental and
+% $part_pkg->freq ne '0' ) {
<TR>
+% if ( FS::Conf->new->exists('invoice-unitprice') ) {
<TD><FONT SIZE="-1">
- ( <% pkg_change_location_link($cust_pkg) %> )
+ ( <% pkg_change_quantity_link($cust_pkg) %> )
</FONT></TD>
+% }
</TR>
% }
% }
</TABLE>
+% if ( @cust_pkg_usage ) {
+ <TABLE CLASS="usage inv">
+ <TR><TH COLSPAN=4><% mt('Included usage') %></TH></TR>
+% foreach my $usage (@cust_pkg_usage) {
+% my $part = $usage->part_pkg_usage;
+% my $ratio = 255 * ($usage->minutes / $part->minutes);
+% $ratio = 255 if $ratio > 255; # because rollover
+% my $color = sprintf('STYLE="font-weight: bold; color: #%02x%02x00"', 255 - $ratio, $ratio);
+% my $trstyle = '';
+% $trstyle = ' CLASS="shared"' if $part->shared;
+ <TR<%$trstyle%>>
+ <TD ALIGN="right"><% $part->description %>: </TD>
+ <TD <%$color%> ALIGN="right"><% $usage->minutes %></TD>
+ <TD <%$color%>> / </TD>
+ <TD <%$color%>><% $part->minutes %></TD>
+% if ( $part->shared ) {
+ <TD><I>(shared)</I></TD>
+% }
+ </TR>
+% }
+ </TABLE>
+% }
</TD>
my $statedefault = $opt{'statedefault'}
|| ($countrydefault eq 'US' ? 'CA' : '');
+my $supplemental = $opt{'supplemental'} || 0;
+
+$cust_pkg->pkgnum =~ /^(\d+)$/;
+my $pkgnum = $1;
+my @cust_pkg_usage = qsearch({
+ 'select' => 'cust_pkg_usage.*',
+ 'table' => 'cust_pkg_usage',
+ 'addl_from' => ' JOIN part_pkg_usage USING (pkgusagepart)',
+ 'extra_sql' => " WHERE pkgnum = $1",
+ 'order_by' => ' ORDER BY priority ASC, description ASC',
+});
+
#subroutines
#false laziness w/status.html
);
}
+sub pkg_change_quantity_link {
+ include( '/elements/popup_link-cust_pkg.html',
+ 'action' => $p. 'edit/cust_pkg_quantity.html?',
+ 'label' => emt('Change quantity'),
+ 'actionlabel' => emt('Change'),
+ 'cust_pkg' => shift,
+ 'width' => 390,
+ 'height' => 220,
+ );
+}
+
sub pkg_dates_link { pkg_link('edit/REAL_cust_pkg', emt('Edit dates'), @_ ); }
sub pkg_discount_link {
% if ( @$packages ) {
-% my $bgcolor1 = '#eeeeee';
-% my $bgcolor2 = '#ffffff';
-% my $bgcolor = '';
-
<TR>
% #my $width = $show_location ? 'WIDTH="25%"' : 'WIDTH="33%"';
<TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Package') |h %></TH>
<TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Status') |h %></TH>
-% if ( $show_location ) {
- <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Location') |h %></TH>
-% }
+ <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Contact/Location') |h %></TH>
<TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Services') |h %></TH>
</TR>
% #$FS::cust_pkg::DEBUG = 2;
% foreach my $cust_pkg (@$packages) {
+ <& .packagerow, $cust_pkg,
+ 'cust_main' => $opt{'cust_main'},
+ 'bgcolor' => $opt{'bgcolor'},
+ %conf_opt
+ &>
+% }
+% } else { # there are no packages
+<BR>
+% }
+<%def .packagerow>
%
-% if ( $bgcolor eq $bgcolor1 ) {
-% $bgcolor = $bgcolor2;
-% } else {
-% $bgcolor = $bgcolor1;
-% }
-%
-% my %iopt = (
-% 'bgcolor' => $bgcolor,
-% 'cust_pkg' => $cust_pkg,
-% 'part_pkg' => $cust_pkg->part_pkg,
-% 'cust_main' => $opt{'cust_main'},
-% %conf_opt,
-% );
-%
-
+% my ($cust_pkg, %iopt) = @_;
+% $iopt{'cust_pkg'} = $cust_pkg;
+% $iopt{'part_pkg'} = $cust_pkg->part_pkg;
<!--pkgnum: <% $cust_pkg->pkgnum %>-->
- <TR>
+ <TR CLASS="row<%$row % 2%>">
<& package.html, %iopt &>
- <& status.html, %iopt &>
-% if ( $show_location ) {
- <& location.html, %iopt &>
-% }
+ <& status.html, %iopt &>
+ <TD CLASS="inv" BGCOLOR="<% $iopt{bgcolor} %>" WIDTH="20%" VALIGN="top">
+ <& contact.html, %iopt &>
+ <& location.html, %iopt &>
+ </TD>
<& services.html, %iopt &>
</TR>
-
-% } #foreach $cust_pkg
-%# </TABLE>
-% } #if @$packages
-% else {
-<BR>
+% $row++;
+% # include supplemental packages if any
+% $iopt{'supplemental'} = ($iopt{'supplemental'} || 0) + 1;
+% foreach my $supp_pkg ($cust_pkg->supplemental_pkgs) {
+ <& .packagerow, $supp_pkg, %iopt &>
% }
-
+</%def>
+<%shared>
+my $row = 0;
+</%shared>
<%init>
my %opt = @_;
my $curuser = $FS::CurrentUser::CurrentUser;
my $packages = $opt{'packages'};
-my $show_location = $opt{'show_location'};
# Sort order is hardcoded for now, can change this if needed.
@$packages = sort {
'manage_link_loc' => scalar($conf->config('svc_broadband-manage_link_loc')),
'manage_link-new_window' => $conf->exists('svc_broadband-manage_link-new_window'),
'maestro-status_test' => $conf->exists('maestro-status_test'),
- 'cust_pkg-large_pkg_size' => $conf->config('cust_pkg-large_pkg_size'),
+ 'cust_pkg-large_pkg_size' => scalar($conf->config('cust_pkg-large_pkg_size')),
- # for packages.html Change location link
- 'show_location' => $show_location,
);
</%init>
-<TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+<TD CLASS="inv" BGCOLOR="<% $bgcolor %>" VALIGN="top">
<TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
%#this should use cust_pkg->status and cust_pkg->statuscolor eventually
-% if ( $cust_pkg->order_date ) {
+% if ( $supplemental ) {
+ <% pkg_status_row_colspan($cust_pkg, emt('Supplemental'), '', 'color' => '7777FF', %opt) %>
+% } elsif ( $cust_pkg->order_date ) {
<% pkg_status_row($cust_pkg, emt('Ordered'), 'order_date', %opt ) %>
% }
<% pkg_status_row($cust_pkg, emt('Cancelled'), 'cancel', 'color'=>'FF0000', %opt ) %>
- <% pkg_status_row_colspan( $cust_pkg,
- ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '',
- 'align'=>'right', 'color'=>'ff0000', 'size'=>'-2', 'colspan'=>$colspan,
- %opt
- )
- %>
+ <% pkg_reason_row($cust_pkg, $cpr, color => 'ff0000', %opt) %>
% unless ( $cust_pkg->get('setup') ) {
- <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', 'colspan'=>$colspan, %opt, ) %>
+ <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', %opt, ) %>
% } else {
<% pkg_status_row( $cust_pkg, emt('Setup'), 'setup', %opt ) %>
- <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_changed( $cust_pkg, %opt ) %>
<% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %>
<% pkg_status_row_if( $cust_pkg, emt('Suspended'), 'susp', %opt, curuser=>$curuser ) %>
% }
%
-% if ( $part_pkg->freq ) { #?
+% if ( $part_pkg->freq and !$supplemental ) { #?
<TR>
- <TD COLSPAN=<%$colspan%>>
+ <TD COLSPAN=<%$opt{colspan}%>>
<FONT SIZE=-1>
% if ( $curuser->access_right('Un-cancel customer package') ) {
( <% pkg_uncancel_link($cust_pkg) %> )
<% pkg_status_row( $cust_pkg, emt('Suspended'), 'susp', 'color'=>'FF9900', %opt ) %>
- <% pkg_status_row_colspan( $cust_pkg,
- ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '',
- 'align'=>'right', 'color'=>'FF9900', 'size'=>'-2', 'colspan'=>$colspan,
- %opt,
- )
- %>
+ <% pkg_reason_row( $cust_pkg, $cpr, 'color' => 'FF9900', %opt ) %>
- <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
- <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_discount( $cust_pkg, %opt ) %>
% unless ( $cust_pkg->get('setup') ) {
- <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', 'colspan'=>$colspan, %opt ) %>
+ <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', %opt ) %>
% } else {
<% pkg_status_row($cust_pkg, emt('Setup'), 'setup', %opt ) %>
% }
<% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
- <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_changed( $cust_pkg, %opt ) %>
<% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %>
% if ( $cust_pkg->option('suspend_bill', 1)
% || ( $part_pkg->option('suspend_bill', 1)
<% pkg_status_row_if( $cust_pkg, emt('Expires'), 'expire', %opt, curuser=>$curuser ) %>
<% pkg_status_row_if( $cust_pkg, emt('Contract ends'), 'contract_end', %opt ) %>
- <TR>
- <TD COLSPAN=<%$colspan%>>
- <FONT SIZE=-1>
-% if ( $curuser->access_right('Unsuspend customer package') ) {
- ( <% pkg_unsuspend_link($cust_pkg) %> )
- ( <% pkg_resume_link($cust_pkg) %> )
-% }
-% if ( $curuser->access_right('Cancel customer package immediately') ) {
- ( <% pkg_cancel_link($cust_pkg) %> )
-% }
- </FONT>
- </TD>
- </TR>
-
+% if ( !$supplemental ) {
+ <TR>
+ <TD COLSPAN=<%$opt{colspan}%>>
+ <FONT SIZE=-1>
+% if ( $curuser->access_right('Unsuspend customer package') ) {
+ ( <% pkg_unsuspend_link($cust_pkg) %> )
+ ( <% pkg_resume_link($cust_pkg) %> )
+% }
+% if ( $curuser->access_right('Cancel customer package immediately') ) {
+ ( <% pkg_cancel_link($cust_pkg) %> )
+% }
+ </FONT>
+ </TD>
+ </TR>
+% }
+%
% } else { #status: active
%
% unless ( $cust_pkg->get('setup') ) { #not setup
%
% unless ( $part_pkg->freq ) {
- <% pkg_status_row_colspan( $cust_pkg, emt('Not yet billed (one-time charge)'), '', 'colspan'=>$colspan, %opt ) %>
+ <% pkg_status_row_colspan( $cust_pkg, emt('Not yet billed (one-time charge)'), '', %opt ) %>
- <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
- <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_discount( $cust_pkg, %opt ) %>
<% pkg_status_row_if(
$cust_pkg,
<% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
+% if (!$supplemental) {
<TR>
- <TD COLSPAN=<%$colspan%>>
+ <TD COLSPAN=<%$opt{colspan}%>>
<FONT SIZE=-1>
% if ( $curuser->access_right('Cancel customer package immediately') ) {
( <% pkg_cancel_link($cust_pkg) %> )
</FONT>
</TD>
</TR>
+% }
% } else {
- <% pkg_status_row_colspan($cust_pkg, emt("Not yet billed ($billed_or_prepaid [_1])", myfreq($part_pkg) ), '', 'colspan'=>$colspan, %opt ) %>
+ <% pkg_status_row_colspan($cust_pkg, emt("Not yet billed ($billed_or_prepaid [_1])", myfreq($part_pkg) ), '', %opt ) %>
- <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
- <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_discount( $cust_pkg, %opt ) %>
<% pkg_status_row_if($cust_pkg, emt('Start billing'), 'start_date', %opt) %>
<% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
%
% unless ( $part_pkg->freq ) {
- <% pkg_status_row_colspan($cust_pkg, emt('One-time charge'), '', 'colspan'=>$colspan, %opt ) %>
+ <% pkg_status_row_colspan($cust_pkg, emt('One-time charge'), '', %opt ) %>
<% pkg_status_row($cust_pkg, emt('Billed'), 'setup', %opt) %>
- <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
- <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_discount( $cust_pkg, %opt ) %>
<% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
<% pkg_status_row_colspan( $cust_pkg,
emt('Overlimit'),
$billed_or_prepaid. ' '. myfreq($part_pkg),
- 'color'=>'FFD000', 'colspan'=>$colspan,
+ 'color'=>'FFD000',
%opt
)
%>
<% pkg_status_row_colspan( $cust_pkg,
emt('Active'),
$billed_or_prepaid. ' '. myfreq($part_pkg),
- 'color'=>'00CC00', 'colspan'=>$colspan,
+ 'color'=>'00CC00',
%opt
)
%>
% }
- <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
- <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_discount( $cust_pkg, %opt ) %>
<% pkg_status_row($cust_pkg, emt('Setup'), 'setup', %opt) %>
% $cust_pkg->set('autosuspend', $autosuspend) if $autosuspend;
% }
- <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+ <% pkg_status_row_changed( $cust_pkg, %opt ) %>
<% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %>
<% pkg_status_row_if( $cust_pkg, $next_bill_or_prepaid_until, 'bill', %opt, curuser=>$curuser ) %>
<% pkg_status_row_if($cust_pkg, emt('Will automatically suspend by'), 'autosuspend', %opt) %>
<% pkg_status_row_if( $cust_pkg, emt('Expires'), 'expire', %opt, curuser=>$curuser ) %>
<% pkg_status_row_if( $cust_pkg, emt('Contract ends'), 'contract_end', %opt ) %>
-% if ( $part_pkg->freq ) {
+% if ( $part_pkg->freq and !$supplemental ) {
<TR>
- <TD COLSPAN=<%$colspan%>>
+ <TD COLSPAN=<%$opt{colspan}%>>
<FONT SIZE=-1>
% if ( $curuser->access_right('Suspend customer package') ) {
( <% pkg_suspend_link($cust_pkg) %> )
my $cust_pkg = $opt{'cust_pkg'};
my $part_pkg = $opt{'part_pkg'};
my $curuser = $FS::CurrentUser::CurrentUser;
-my $colspan = $opt{'cust_pkg-display_times'} ? 8 : 4;
my $width = $opt{'cust_pkg-display_times'} ? '38%' : '56%';
+my $supplemental = $opt{'supplemental'};
+
+$opt{colspan} = $opt{'cust_pkg-display_times'} ? 8 : 4;
#false laziness w/edit/REAL_cust_pkg.cgi
my( $billed_or_prepaid, $last_bill_or_renewed, $next_bill_or_prepaid_until );
sub pkg_status_row {
my( $cust_pkg, $title, $field, %opt ) = @_;
+ if ( $field and $cust_pkg->main_pkgnum ) {
+ # for supplemental packages, we mostly only show these if they're
+ # different from the main package
+ my $main_pkg = $cust_pkg-> main_pkg;
+ if ( $main_pkg->get($field) ne $cust_pkg->get($field)
+ # with some exceptions
+ or $field eq 'bill'
+ or $field eq 'last_bill'
+ or $field eq 'setup'
+ or $field eq 'susp'
+ or $field eq 'cancel'
+ ) {
+ # handle it normally
+ } else {
+ return '';
+ }
+ }
+
my $color = $opt{'color'};
- my $html = qq(<TR><TD WIDTH="<%$width%>" ALIGN="right">);
+ my $html = qq(<TR><TD WIDTH="$width" ALIGN="right">);
$html .= qq(<FONT COLOR="#$color"><B>) if length($color);
$html .= qq($title );
$html .= qq(</B></FONT>) if length($color);
'',
'size' => '-1',
'align' => 'right',
- 'colspan' => $opt{'colspan'},
);
}
return '' unless $cust_main->payby =~ /^(CARD|CHEK)$/;
my $what = lc(FS::payby->shortname($cust_main->payby));
- pkg_status_row_colspan( $cust_pkg, emt("No automatic $what charge"), '',
- 'colspan' => $opt{'colspan'},
- );
+ pkg_status_row_colspan( $cust_pkg, emt("No automatic $what charge"), '');
}
sub pkg_status_row_discount {
$cust_pkg_discount->pkgdiscountnum.
'">'.emt('remove discount').'</A>)</FONT>';
- $html .= pkg_status_row_colspan( $cust_pkg, $label, '',
- 'colspan' => $opt{'colspan'},
- );
+ $html .= pkg_status_row_colspan( $cust_pkg, $label, '', %opt );
}
$html;
}
+sub pkg_reason_row {
+ my ($cust_pkg, $cpr, %opt) = @_;
+ return '' if $cust_pkg->main_pkgnum;
+
+ my $reasontext = '';
+ $reasontext = $cpr->reasontext . ' by ' . $cpr->otaker if $cpr;
+ pkg_status_row_colspan( $cust_pkg, $reasontext, '',
+ 'align'=>'right', 'size'=>'-2', %opt
+ );
+}
+
sub pkg_status_row_colspan {
my($cust_pkg, $title, $addl, %opt) = @_;
<A HREF="<% $p %>edit/cust_pay.cgi?payby=WEST;custnum=<% $custnum %>"><% mt('Enter Western Union payment') |h %></A>
% }
-<BR>
+<% $s ? '<BR>' : '' %>
% $s=0;
% if ( ( $payby{'CARD'} || $payby{'DCRD'} )
<A HREF="<% $p %>edit/cust_pay.cgi?payby=MCRD;custnum=<% $custnum %>"><% mt('Post manual (offline/POS) credit card payment') |h %></A>
% }
-<BR>
+<% $s ? '<BR>' : '' %>
-%# credit link
+%# credit links
+% $s=0;
% if ( $curuser->access_right('Post credit') ) {
+ <% $s++ ? ' | ' : '' %>
<& /elements/popup_link-cust_main.html,
'label' => emt('Enter credit'),
'action' => "${p}edit/cust_credit.cgi",
'actionlabel' => emt('Enter credit'),
'width' => 616, #make room for reasons #540 default
&>
- |
+% }
+% if ( $curuser->access_right('Credit line items') ) {
+ <% $s++ ? ' | ' : '' %>
<& /elements/popup_link-cust_main.html,
'label' => emt('Credit line items'),
#'action' => "${p}search/cust_bill_pkg.cgi?nottax=1;type=select",
'width' => 968, #763,
'height' => 575,
&>
- <BR>
% }
+<% $s ? '<BR>' : '' %>
%# refund links
%#display payment history
-%my $money_char = $conf->config('money_char') || '$';
-%
-%sub balance_forward_row {
-% my( $b, $date, $money_char ) = @_;
-% ( my $balance_forward = $money_char. $b ) =~ s/^\$\-/- \$/;
-
- <TR ID="balance_forward_row">
- <TD CLASS="grid" BGCOLOR="#dddddd">
- <% time2str($date_format, $date) %>
- </TD>
-
- <TD CLASS="grid" BGCOLOR="#dddddd">
- <I><% mt("Starting balance on [_1]", time2str($date_format, $date) ) |h %></I>
- (<A HREF="javascript:void(0);" onClick="show_history();"><% mt('show prior history') |h %></A>)
- </TD>
-
- <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
- <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
- <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
- <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
- <TD CLASS="grid" BGCOLOR="#dddddd" ALIGN="right"><I><% $balance_forward %></I></TD>
-
- </TR>
-%}
-%
-%my $balance = 0;
%my %target = ();
%
-%my $years = $conf->config('payment_history-years') || 2;
-%my $older_than = time - $years * 31556926; #60*60*24*365.2422
%my $hidden = 0;
%my $seen = 0;
%my $old_history = 0;
%my $lastdate = 0;
%
-%foreach my $item ( sort { $a->{'date'} <=> $b->{'date'} } @history ) {
+%foreach my $item ( @history ) {
%
% $lastdate = $item->{'date'};
%
-% my $display;
-% if ( $item->{'date'} < $older_than ) {
+% my $display = '';
+% if ( $item->{'hide'} ) {
% $display = ' STYLE="display:none" ';
-% $hidden = 1;
-% } else {
-%
-% $display = '';
-%
-% if ( $hidden && ! $seen++ ) {
-% balance_forward_row($balance, $item->{'date'}, $money_char);
-% }
-%
% }
%
% if ( $bgcolor eq $bgcolor1 ) {
%
% my $target = exists($item->{'target'}) ? $item->{'target'} : '';
%
-% $balance += $item->{'charge'} if exists $item->{'charge'};
-% $balance -= $item->{'payment'} if exists $item->{'payment'};
-% $balance -= $item->{'credit'} if exists $item->{'credit'};
-% $balance += $item->{'refund'} if exists $item->{'refund'};
-% $balance = sprintf("%.2f", $balance);
-% $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
-% ( my $showbalance = $money_char. $balance ) =~ s/^\$\-/- \$/;
-%
-%
-
+% my $showbalance = $money_char . $item->{'balance'};
+% $showbalance =~ s/^\$\-/- \$/;
<TR <% $display ? $display.' ID="old_history'.$old_history++.'"' : ''%>>
<TD VALIGN="top" CLASS="grid" BGCOLOR="<% $bgcolor %>">
<% $showbalance %>
</TD>
</TR>
-% }
-%if ( scalar(@history) && $hidden && ! $seen++ ) {
-% balance_forward_row($balance, $lastdate, $money_char);
-%}
+% if ( $item->{'balance_forward'} ) {
+<& .balance_forward_row, $item->{'balance'}, $item->{'date'} &>
+% }
+%} # foreach $item
</TABLE>
</TD>
}
</SCRIPT>
+<%def .balance_forward_row>
+% my( $b, $date ) = @_;
+% ( my $balance_forward = $money_char. $b ) =~ s/^\$\-/- \$/;
-<%init>
+ <TR ID="balance_forward_row">
+ <TD CLASS="grid" BGCOLOR="#dddddd">
+ <% time2str($date_format, $date) %>
+ </TD>
-my( $cust_main ) = @_;
-my $custnum = $cust_main->custnum;
+ <TD CLASS="grid" BGCOLOR="#dddddd">
+ <I><% mt("Starting balance on [_1]", time2str($date_format, $date) ) |h %></I>
+ (<A HREF="javascript:void(0);" onClick="show_history();"><% mt('show prior history') |h %></A>)
+ </TD>
+
+ <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+ <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+ <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+ <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+ <TD CLASS="grid" BGCOLOR="#dddddd" ALIGN="right"><I><% $balance_forward %></I></TD>
+ </TR>
+</%def>
+<%shared>
my $conf = new FS::Conf;
my $date_format = $conf->config('date_format') || '%m/%d/%Y';
+my $money_char = $conf->config('money_char') || '$';
+</%shared>
+<%init>
+
+my( $cust_main ) = @_;
+my $custnum = $cust_main->custnum;
my $curuser = $FS::CurrentUser::CurrentUser;
#'target' => $target, #XXX
};
}
+#declined batch payments
+foreach my $cust_pay_batch (
+ $cust_main->cust_pay_batch(hashref => {status => 'Declined'})
+) {
+ my $pay_batch = $cust_pay_batch->pay_batch;
+ push @history, {
+ 'date' => $pay_batch->upload,
+ 'desc' => include('payment_history/attempted_batch_payment.html', $cust_pay_batch, %opt),
+ 'void_payment' => $cust_pay_batch->amount,
+ };
+}
#credits (some false laziness w/payments)
foreach my $cust_credit ($cust_main->cust_credit) {
}
+# sort in forward order first, and calculate running balances
+my $years = $conf->config('payment_history-years') || 2;
+my $older_than = time - $years * 31556926; #60*60*24*365.2422
+my $balance = 0;
+
+@history = sort { $a->{date} <=> $b->{date} } @history;
+my $i = 0;
+my $balance_forward;
+foreach my $item (@history) {
+ $balance += $item->{'charge'} if exists $item->{'charge'};
+ $balance -= $item->{'payment'} if exists $item->{'payment'};
+ $balance -= $item->{'credit'} if exists $item->{'credit'};
+ $balance += $item->{'refund'} if exists $item->{'refund'};
+ $balance = sprintf("%.2f", $balance);
+ $balance =~ s/^\-0\.00$/0.00/;
+ $item->{'balance'} = $balance;
+
+ if ( $item->{'date'} < $older_than ) {
+ $item->{'hide'} = 1;
+ } elsif ( $history[$i-1]->{'hide'} ) {
+ # this is the end of the hidden section
+ $history[$i-1]->{'balance_forward'} = 1;
+ }
+ $i++;
+}
+if ( @history and $history[-1]->{'hide'} ) {
+ # then everything is hidden
+ $history[-1]->{'balance_forward'} = 1;
+}
+
+# then sort in user-pref order
+if ( $curuser->option('history_order') eq 'newest' ) {
+ @history = sort { $b->{date} <=> $a->{date} } @history;
+} # else it's already oldest-first, and there are no other options yet
+
sub translate_payby {
my ($payby,$payinfo) = (shift,shift);
my %payby = (
--- /dev/null
+<I><% mt('Payment attempt') |h %> <% $info |h %></I>
+<%init>
+
+my( $cust_pay_batch, %opt ) = @_;
+
+my ($payby,$payinfo) = translate_payinfo($cust_pay_batch);
+$payby = translate_payby($payby,$payinfo);
+my $info = $payby ? "($payby$payinfo)" : '';
+
+$info .= ': '. $cust_pay_batch->error_message
+ if length($cust_pay_batch->error_message);
+
+</%init>
<% mt('Service #') |h %><B><% $svcnum %></B>
% my $url = $opt{'edit_url'} || $p. 'edit/'. $opt{'table'}. '.cgi?';
-| <& /view/elements/svc_edit_link.html, 'svc' => $svc_x, 'edit_url' => $url &>
+<& /view/elements/svc_edit_link.html, 'svc' => $svc_x, 'edit_url' => $url &>
<BR>
<% ntable("#cccccc") %><TR><TD><% ntable("#cccccc",2) %>
+% my @inventory_items = $svc_x->inventory_item;
% foreach my $f ( @$fields ) {
%
-% my($field, $type, $value, $hack_strict_refs);
+% my($field, $type, $value);
% if ( ref($f) ) {
% $field = $f->{'field'};
-% $hack_strict_refs = \&{ $f->{'value'} } if $f->{'value'};
-% $value = $f->{'value'} ? &$hack_strict_refs($svc_x) : $svc_x->$field;
% $type = $f->{'type'} || 'text';
+% if ( $f->{'value_callback'} ) {
+% my $hack_strict_refs = \&{ $f->{'value_callback'} };
+% $value = &$hack_strict_refs($svc_x);
+% } else {
+% $value = exists($f->{'value'}) ? $f->{'value'} : $svc_x->$field;
+% }
% } else {
% $field = $f;
-% $value = $svc_x->$field;
% $type = 'text';
+% $value = $svc_x->$field;
% }
%
% my $columndef = $part_svc->part_svc_column($field);
+% if ( $columndef->columnflag =~ /^[MA]$/ && $columndef->columnvalue =~ /,/ )
+% {
+% # inventory-select field with multiple classes
+% # show the class name to disambiguate
+% my ($item) = grep { $_->svc_field eq $field } @inventory_items;
+% my $class = qsearchs('inventory_class', { classnum => $item->classnum });
+% $value .= ' <i>('. $class->classname . ')</i>' if $class;
+% }
% unless ($columndef->columnflag eq 'F' && !length($columndef->columnvalue)) {
<TR>
)
</%doc>
-<% $devices %>
+%if ( @devices || $num_part_device || $table eq 'dsl_device' ) {
+% my $svcnum = $svc_x->svcnum;
+
+ Devices
+ (<A HREF="<%$p%>edit/<%$table%>.html?svcnum=<%$svcnum%>">Add device</A>)
+ <BR>
+
+% if ( @devices ) {
+
+ <SCRIPT>
+ function areyousure(href) {
+ if (confirm("Are you sure you want to delete this device?") == true)
+ window.location.href = href;
+ }
+ </SCRIPT>
+
+ <& /elements/table-grid.html &>
+ <TR>
+% if ( $table eq 'phone_device' ) {
+ <TH CLASS="grid" BGCOLOR="#cccccc">Type</TH>
+% }
+ <TH CLASS="grid" BGCOLOR="#cccccc">MAC Addr</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
+ </TR>
+
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor = '';
+%
+% foreach my $device ( @devices ) {
+%
+% if ( $bgcolor eq $bgcolor1 ) {
+% $bgcolor = $bgcolor2;
+% } else {
+% $bgcolor = $bgcolor1;
+% }
+%
+% my $td = qq(<TD CLASS="grid" BGCOLOR="$bgcolor">);
+%
+% my $devicenum = $device->devicenum;
+% my $export_links = '';
+% $export_links = join( '<BR>', @{ $device->export_links } )
+% if $device->can('export_links');
+
+ <TR>
+% if ( $table eq 'phone_device' ) { #$devices->can('part_device')
+ <% $td %><% $device->part_device->devicename |h %></TD>
+% }
+ <% $td %><% $device->mac_addr %></TD>
+ <% $td %><% $export_links %></TD>
+ <% $td %>(
+% unless ( $opt{'no_edit'} ) {
+ <A HREF="<%$p%>edit/<%$table%>.html?<%$devicenum%>">edit</A> |
+% }
+ <A HREF="javascript:areyousure('<%$p%>misc/delete-<%$table%>.html?<%$devicenum%>')">delete</A>
+ )</TD>
+ </TR>
+% }
+ </TABLE>
+ <BR>
+
+% }
+ <BR>
+%}
<%init>
- my %opt = @_;
- my $table = $opt{'table'}; #part_device, dsl_device
- my $svc_x = $opt{'svc_x'};
-
- my $devices = '';
-
- my $num_part_device = 0;
- if ( $table eq 'phone_device' ) {
- my $sth = dbh->prepare("SELECT COUNT(*) FROM part_device")
- #WHERE disabled = '' OR disabled IS NULL;");
- or die dbh->errstr;
- $sth->execute or die $sth->errstr;
- $num_part_device = $sth->fetchrow_arrayref->[0];
+my %opt = @_;
+my $table = $opt{'table'}; #part_device, dsl_device
+my $svc_x = $opt{'svc_x'};
+
+my $num_part_device = 0;
+if ( $table eq 'phone_device' ) {
+ my $sth = dbh->prepare("SELECT COUNT(*) FROM part_device")
+ #WHERE disabled = '' OR disabled IS NULL;");
+ or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+ $num_part_device = $sth->fetchrow_arrayref->[0];
}
- my @devices = $svc_x->$table();
-
- #should move the below to proper mason code above instead of making $devices
- if ( @devices || $num_part_device || $table eq 'dsl_device' ) {
- my $svcnum = $svc_x->svcnum;
- $devices .=
- qq[Devices (<A HREF="${p}edit/$table.html?svcnum=$svcnum">Add device</A>)<BR>];
- if ( @devices ) {
-
- $devices .= qq!
- <SCRIPT>
- function areyousure(href) {
- if (confirm("Are you sure you want to delete this device?") == true)
- window.location.href = href;
- }
- </SCRIPT>
- !;
-
-
- $devices .=
- include('/elements/table-grid.html').
- '<TR>';
-
- $devices .=
- '<TH CLASS="grid" BGCOLOR="#cccccc">Type</TH>'
- if $table eq 'phone_device';
-
- $devices .=
- '<TH CLASS="grid" BGCOLOR="#cccccc">MAC Addr</TH>'.
- '<TH CLASS="grid" BGCOLOR="#cccccc"></TH>'.
- '<TH CLASS="grid" BGCOLOR="#cccccc"></TH>'.
- '</TR>';
- my $bgcolor1 = '#eeeeee';
- my $bgcolor2 = '#ffffff';
- my $bgcolor = '';
-
- foreach my $device ( @devices ) {
-
- if ( $bgcolor eq $bgcolor1 ) {
- $bgcolor = $bgcolor2;
- } else {
- $bgcolor = $bgcolor1;
- }
- my $td = qq(<TD CLASS="grid" BGCOLOR="$bgcolor">);
-
- my $devicenum = $device->devicenum;
- my $export_links = join( '<BR>', @{ $device->export_links } )
- if $device->can('export_links');
-
- $devices .= '<TR>';
- $devices .= $td. $device->part_device->devicename. '</TD>'
- if $table eq 'phone_device'; #$devices->can('part_device');
-
- $devices .= $td. $device->mac_addr. '</TD>'.
- $td. $export_links. '</TD>'.
- "$td( ";
-
- $devices .= qq(<A HREF="${p}edit/$table.html?$devicenum">edit</A> | )
- unless $opt{'no_edit'};
-
- $devices .= qq(<A HREF="javascript:areyousure('${p}misc/delete-$table.html?$devicenum')">delete</A>).
- ' )</TD>'.
- '</TR>';
- }
- $devices .= '</TABLE><BR>';
- }
- $devices .= '<BR>';
- }
+my @devices = $svc_x->$table();
</%init>
window.location.href = '<% $cancel_url %>';
}
</SCRIPT>
-<A HREF="<% $edit_url %>"><% mt("Edit this [_1]", $label) |h %></A> |
-<A HREF="javascript:areyousure_delete()"><% mt('Unprovision this Service') |h %></A>
+% if ( $curuser->access_right('Provision customer service') ) {
+| <A HREF="<% $edit_url %>"><% mt("Edit this [_1]", $label) |h %></A>
+% }
+% if ( $curuser->access_right('Unprovision customer service') ) {
+| <A HREF="javascript:areyousure_delete()"><% mt('Unprovision this Service') |h %></A>
+% }
% }
<%init>
my %opt = @_;
my $cust_svc = $svc_x->cust_svc; # always exists
my $cancel_date = $cust_svc->pkg_cancel_date;
my ($label) = $cust_svc->label;
+my $curuser = $FS::CurrentUser::CurrentUser;
</%init>
% foreach my $key ( sort {$a cmp $b} keys %$hashref ) {
<TR>
<TD ALIGN="right"><% $key |h %></TD>
- <TD BGCOLOR="#ffffff"><% $hashref->{$key} |h %></TD>
+ <TD BGCOLOR="#ffffff">
+% if ( ref($hashref->{$key}) eq 'ARRAY' ) {
+% foreach (@{ $hashref->{$key} }) {
+ <% $_ |h %><BR>
+% }
+% } else {
+ <% $hashref->{$key} |h %>
+% }
+ </TD>
</TR>
% }
--- /dev/null
+<% $content %>\
+<%init>
+
+#false laziness w/elements/cust_bill-typeset
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Generate quotation'); #View quotations ?
+
+my $quotationnum = $cgi->param('quotationnum');
+
+my $conf = new FS::Conf;
+
+my $quotation = qsearchs({
+ 'select' => 'quotation.*',
+ 'table' => 'quotation',
+ #'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => { 'quotationnum' => $quotationnum },
+ #'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+});
+die "Quotation #$quotationnum not found!" unless $quotation;
+
+my $content = $quotation->print_pdf(); #\%opt);
+
+http_header('Content-Type' => 'application/pdf');
+http_header('Content-Disposition' => "filename=$quotationnum.pdf" );
+http_header('Content-Length' => length($content) );
+http_header('Cache-control' => 'max-age=60' );
+
+</%init>
% }
+
<& svc_acct/radius_usage.html,
'svc_acct' => $svc_acct,
'part_svc' => $part_svc,
%gopt,
&>
+
<& svc_acct/change_svc_form.html,
'part_svc' => \@part_svc,
'svcnum' => $svcnum,
&>
<% mt('Service #') |h %><B><% $svcnum %></B>
-|
<& /view/elements/svc_edit_link.html, 'svc' => $svc_acct &>
<& svc_acct/change_svc.html,
'part_svc' => \@part_svc,
%gopt,
&>
+</FORM>
+
+
<& svc_acct/basics.html,
'svc_acct' => $svc_acct,
'part_svc' => $part_svc,
my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '.
' LEFT JOIN cust_pkg USING ( pkgnum ) '.
' LEFT JOIN cust_main USING ( custnum ) ';
-
-my($query) = $cgi->keywords;
+my $query;
+if ( $cgi->keywords ) {
+ ($query) = $cgi->keywords;
+} else {
+ $query = $cgi->param('svcnum');
+}
$query =~ /^(\d+)$/;
my $svcnum = $1;
my $svc_acct = qsearchs({
% if ( $password =~ /^\*\w+\* (.*)$/ ) {
% $password = $1;
% $show_pw .= '<I>('. mt('login disabled') .')</I> ';
-% }
+% }
% if ( ! $password
% && $svc_acct->_password_encryption ne 'plain'
% && $svc_acct->_password
% {
% $show_pw .= '<I>('. uc($svc_acct->_password_encryption). ' '.mt('encrypted').')</I>';
% } elsif ( $conf->exists('showpasswords') ) {
-% $show_pw .= '<PRE>'. encode_entities($password). '</PRE>';
+% $show_pw .= '<SPAN >'. encode_entities($password). '</PRE>';
% } else {
+% $password = '';
% $show_pw .= '<I>('. mt('hidden') .')</I>';
-% }
-% $password = '';
-<& /view/elements/tr.html, label=>mt('Password'), value=>$show_pw &>
-
+% }
+<TR>
+ <TD ALIGN="right"><% mt('Password') %></TD>
+ <TD STYLE="background-color: #ffffff; white-space: nowrap">
+ <% $show_pw %>
+% my $curuser = $FS::CurrentUser::CurrentUser;
+% if ( $curuser->access_right('Provision customer service') or
+% ($curuser->access_right('Edit password') and
+% ! $part_svc->restrict_edit_password) )
+% {
+ <& /elements/change_password.html,
+ 'svc_acct' => $svc_acct,
+ 'curr_value' => $password,
+ &>
+% }
+ </TD>
+</TR>
% if ( $conf->exists('security_phrase') ) {
<& /view/elements/tr.html, label=>mt('Security phrase'), value=>$svc_acct->sec_phrase &>
my @fields = (
'description',
- { field => 'routernum', value => \&router },
+ { field => 'routernum', value_callback => \&router },
'speed_down',
'speed_up',
- { field => 'ip_addr', value => \&ip_addr },
- { field => 'sectornum', value => \§ornum },
- { field => 'mac_addr', value => \&mac_addr },
+ { field => 'ip_addr', value_callback => \&ip_addr },
+ { field => 'sectornum', value_callback => \§ornum },
+ { field => 'mac_addr', value_callback => \&mac_addr },
#'latitude',
#'longitude',
- { field => 'coordinates', value => \&coordinates },
+ { field => 'coordinates', value_callback => \&coordinates },
'altitude',
+
+ 'radio_serialnum',
+ 'radio_location',
+ 'poe_location',
+ 'rssi',
+ 'suid',
+ { field => 'shared_svcnum', value_callback=> \&shared_svcnum, }, #value_callback =>
+
'vlan_profile',
'authkey',
'plan_id',
);
push @fields,
- { field => 'usergroup', value => \&usergroup }
+ { field => 'usergroup', value_callback => \&usergroup }
if $conf->exists('svc_broadband-radius');
sub router {
);
}
+sub shared_svcnum {
+ my $svc_broadband = shift;
+ return '' unless $svc_broadband->shared_svcnum;
+
+ my $shared_svc_broadband =
+ qsearchs('svc_broadband', { 'svcnum' => $svc_broadband->shared_svcnum,
+ }
+ #agent virt?
+ )
+ or return '';
+ my $shared_cust_pkg = $shared_svc_broadband->cust_svc->cust_pkg;
+
+ $shared_svc_broadband->label.
+ ( $shared_cust_pkg
+ ? ' ('. $shared_cust_pkg->cust_main->name. ')'
+ : ''
+ );
+}
+
sub svc_callback {
# trying to move to the callback style
my ($cgi, $svc_x, $part_svc, $cust_pkg, $fields, $opt) = @_;
+
+ if ( $part_svc->part_svc_column('latitude')->columnflag eq 'F'
+ && $part_svc->part_svc_column('longitude')->columnflag eq 'F'
+ )
+ {
+ @$fields = grep { !ref($_) || $_->{field} ne 'coordinates' } @$fields;
+ }
+
# again, we assume at most one of these exports per part_svc
my ($nas_export) = $part_svc->part_export('broadband_nas');
if ( $nas_export ) {
my @fields = (
{ field=>'privatekey',
- value=> sub {
+ value_callback=> sub {
my $svc_cert = shift;
if ( $svc_cert->privatekey && $svc_cert->check_privatekey ) {
'<FONT COLOR="#33ff33">Verification OK</FONT>';
qw( common_name organization organization_unit city state country cert_contact
),
{ 'field'=>'csr',
- 'value'=> sub {
+ 'value_callback'=> sub {
my $svc_cert = shift;
if ( $svc_cert->csr ) {
},
},
{ 'field'=>'certificate',
- 'value'=> sub {
+ 'value_callback'=> sub {
my $svc_cert = shift;
if ( $svc_cert->certificate ) {
},
},
{ 'field'=>'cacert',
- 'value'=> sub {
+ 'value_callback'=> sub {
my $svc_cert = shift;
if ( $svc_cert->cacert ) {
: $fields->{$_}
);
} keys %$fields;
+
+$labels{'display_hw_addr'} = 'Hardware address';
+
my $model = { field => 'typenum',
type => 'text',
- value => sub { $_[0]->hardware_type->description }
+ value_callback => sub { $_[0]->hardware_type->description }
};
my $status = { field => 'statusnum',
type => 'text',
- value => sub { $_[0]->status_label }
+ value_callback => sub { $_[0]->status_label }
};
my $note = { field => 'note',
type => 'text',
- value => sub { encode_entities($_[0]->note) }
+ value_callback => sub { encode_entities($_[0]->note) }
};
my @fields = (
);
} keys %$fields;
-my @fields = qw( countrycode phonenum );
+my @fields = qw( countrycode phonenum sim_imsi );
push @fields, 'domain' if $conf->exists('svc_phone-domain');
-push @fields, qw( pbx_title sip_password pin phone_name forwarddst email );
+push @fields, qw( pbx_title );
+
+if ( $conf->exists('showpasswords') ) {
+ push @fields, qw( sip_password );
+} else {
+ push @fields, { 'field' => 'sip_password', #'_HIDDEN_sip_password',
+ 'type' => 'fixed',
+ 'value' => '<I>('. mt('hidden') .')</I>',
+ };
+}
+
+push @fields, qw( pin phone_name forwarddst email );
if ( $conf->exists('svc_phone-lnp') ) {
push @fields, 'lnp_status',
share/html/Elements/CustomerFields
share/html/Search/Elements/ConditionRow # bugfix for select options list
share/html/Search/Elements/PickBasics
+
+#avoid cloning TimeWorked and related fields
+lib/RT/CustomField.pm
+share/html/Admin/CustomFields/Modify.html
+share/html/Ticket/Create.html
my $self = shift;
my %args = @_;
+ my $from = $args{From};
+
if ( RT->Config->Get('UseFriendlyFromLine') ) {
my $friendly_name = $self->GetFriendlyName(%args);
- $self->SetHeader(
- 'From',
+ $from =
sprintf(
RT->Config->Get('FriendlyFromLineFormat'),
$self->MIMEEncodeString(
$friendly_name, RT->Config->Get('EmailOutputEncoding')
),
$args{From}
- ),
- );
- } else {
- $self->SetHeader( 'From', $args{From} );
+ );
}
+
+ $self->SetHeader( 'From', $from );
+
+ #also set Sender:, otherwise MTAs add a nonsensical value like rt@machine,
+ #and then Outlook prepends "rt@machine on behalf of" to the From: header
+ $self->SetHeader( 'Sender', $from );
}
=head2 GetFriendlyName
$self->SetUILocation( $args{'UILocation'} );
}
+ if ( exists $args{'NoClone'} ) {
+ $self->SetNoClone( $args{'NoClone'} );
+ }
+
return ($rv, $msg) unless exists $args{'Queue'};
# Compat code -- create a new ObjectCustomField mapping
}
}
+sub NoClone {
+ my $self = shift;
+ $self->FirstAttribute('NoClone') ? 1 : '';
+}
-
-
+sub SetNoClone {
+ my $self = shift;
+ my $value = shift;
+ if ( $value ) {
+ return $self->SetAttribute( Name => 'NoClone', Content => 1 );
+ } else {
+ return $self->DeleteAttribute('NoClone');
+ }
+}
=head2 id
</td></tr>
<tr><td class="label"> </td><td>
+<input type="checkbox" class="checkbox" name="YesClone" value="1" <% $YesCloneChecked |n%> />
+<&|/l&>Copy this field to new tickets</&>
+</td></tr>
+
+<tr><td class="label"> </td><td>
<input type="hidden" class="hidden" name="SetEnabled" value="1" />
<input type="checkbox" class="checkbox" name="Enabled" value="1" <% $EnabledChecked |n%> />
<&|/l&>Enabled (Unchecking this box disables this custom field)</&>
IncludeContentForValue => $IncludeContentForValue,
BasedOn => $BasedOn,
Disabled => !$Enabled,
+ NoClone => !$YesClone,
);
if (!$val) {
push @results, loc("Could not create CustomField: [_1]", $msg);
if ( $ARGS{'Update'} && $id ne 'new' ) {
#we're asking about enabled on the web page but really care about disabled.
$ARGS{'Disabled'} = $Enabled? 0 : 1;
+ # likewise
+ $ARGS{'NoClone'} = $YesClone ? 0 : 1;
$ARGS{'Required'} ||= 0;
- my @attribs = qw(Disabled Required Pattern Name TypeComposite LookupType Description LinkValueTo IncludeContentForValue);
+ my @attribs = qw(Disabled Required Pattern Name TypeComposite LookupType Description LinkValueTo IncludeContentForValue NoClone);
push @results, UpdateRecordObject(
AttributesRef => \@attribs,
Object => $CustomFieldObj,
my $RequiredChecked = '';
$RequiredChecked = qq[checked="checked"] if $CustomFieldObj->Required;
+my $YesCloneChecked = qq[checked="checked"];
+$YesCloneChecked = '' if $CustomFieldObj->NoClone;
+
+
my @CFvalidations = (
'(?#Mandatory).',
'(?#Digits)^[\d.]+$',
$IncludeContentForValue => undef
$BasedOn => undef
$UILocation => undef
+$YesClone => undef
</%ARGS>
};
$clone->{$_} = $CloneTicketObj->$_()
- for qw/Owner Subject FinalPriority TimeEstimated TimeWorked
- Status TimeLeft/;
+ for qw/Owner Subject FinalPriority Status/;
+ # not TimeWorked, TimeEstimated, or TimeLeft
$clone->{$_} = $CloneTicketObj->$_->AsString
for grep { $CloneTicketObj->$_->Unix }
my $cfs = $CloneTicketObj->QueueObj->TicketCustomFields();
while ( my $cf = $cfs->Next ) {
+ next if $cf->FirstAttribute('NoClone');
my $cf_id = $cf->id;
my $cf_values = $CloneTicketObj->CustomFieldValues( $cf->id );
my @cf_values;