From: Ivan Kohler
Date: Fri, 10 May 2013 19:55:52 +0000 (-0700)
Subject: merge NG auth, RT#21563
X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=63973c641c4be00765fa27e55c57cc5b9aa4da19;hp=0832972047a36d19ffcf7d1462abc48de7045d3d
merge NG auth, RT#21563
---
diff --git a/FS/FS.pm b/FS/FS.pm
index 741d8159f..042c756d0 100644
--- a/FS/FS.pm
+++ b/FS/FS.pm
@@ -3,7 +3,7 @@ package FS;
use strict;
use vars qw($VERSION);
-$VERSION = '3.0';
+$VERSION = '4.0git';
#find missing entries in this file with:
# for a in `ls *pm | cut -d. -f1`; do grep 'L' ../FS.pm >/dev/null || echo "missing $a" ; done
@@ -233,6 +233,8 @@ L - Package class class
L - Package definition class
+L - Package definition localization class
+
L - Package definition link class
L - Tax class class
diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm
index a60d033d6..373617e36 100644
--- a/FS/FS/AccessRight.pm
+++ b/FS/FS/AccessRight.pm
@@ -132,6 +132,7 @@ tie my %rights, 'Tie::IxHash',
'Order customer package',
'One-time charge',
'Change customer package',
+ 'Detach customer package',
'Bulk change customer packages',
'Edit customer package dates',
'Discount customer package', #NEW
@@ -305,6 +306,8 @@ tie my %rights, 'Tie::IxHash',
'Usage: Call Detail Records (CDRs)',
'Usage: Unrateable CDRs',
'Usage: Time worked',
+ { rightname=>'Employees: Commission Report', global=>1 },
+ { rightname=>'Employees: Audit Report', global=>1 },
#{ rightname => 'List customers of all agents', global=>1 },
],
diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index 08e506cf1..01e0ebc33 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -50,7 +50,7 @@ $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
@@ -636,11 +636,12 @@ sub billing_history {
push @history, {
'type' => 'Line item',
- 'description' => $_->desc. ( $_->sdate && $_->edate
- ? ' '. time2str('%d-%b-%Y', $_->sdate).
- ' To '. time2str('%d-%b-%Y', $_->edate)
- : ''
- ),
+ 'description' => $_->desc( $cust_main->locale ).
+ ( $_->sdate && $_->edate
+ ? ' '. time2str('%d-%b-%Y', $_->sdate).
+ ' To '. time2str('%d-%b-%Y', $_->edate)
+ : ''
+ ),
'amount' => sprintf('%.2f', $_->setup + $_->recur ),
'date' => $cust_bill->_date,
'date_pretty' => time2str('%m/%d/%Y', $cust_bill->_date ),
@@ -1584,7 +1585,7 @@ sub list_pkgs {
my $primary_cust_svc = $_->primary_cust_svc;
+{ $_->hash,
$_->part_pkg->hash,
- pkg_label => $_->pkg_label,
+ pkg_label => $_->pkg_locale,
status => $_->status,
part_svc =>
[ map { $_->hashref }
@@ -1698,7 +1699,7 @@ sub list_svcs {
'svcdb' => $svcdb,
'label' => $label,
'value' => $value,
- 'pkg_label' => $cust_pkg->pkg_label,
+ 'pkg_label' => $cust_pkg->pkg_locale,
'pkg_status' => $cust_pkg->status,
'readonly' => ($part_svc->selfservice_access eq 'readonly'),
);
diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm
index 1dbb20bc7..57091c4fe 100644
--- a/FS/FS/ClientAPI/Signup.pm
+++ b/FS/FS/ClientAPI/Signup.pm
@@ -98,7 +98,7 @@ sub signup_info {
my @signup_bools = qw( no_company recommend_daytime recommend_email );
- my @signup_server_scalars = qw( default_pkgpart default_svcpart );
+ my @signup_server_scalars = qw( default_pkgpart default_svcpart default_domsvc );
my @selfservice_textareas = qw( head body_header body_footer );
@@ -670,7 +670,7 @@ sub new_customer {
my $svc = new FS::svc_acct {
'svcpart' => $svcpart,
map { $_ => $packet->{$_} }
- qw( username _password sec_phrase popnum ),
+ qw( username _password sec_phrase popnum domsvc ),
};
my @acct_snarf;
@@ -946,15 +946,27 @@ sub capture_payment {
}
my $cust_main = $cust_pay_pending->cust_main;
- my $bill_error =
- $cust_main->realtime_botpp_capture( $cust_pay_pending,
- %{$packet->{data}},
- apply => 1,
- );
+ if ( $packet->{cancel} ) {
+ # the user has chosen not to make this payment
+ # (probably should be a separate API call, but I don't want to duplicate
+ # all of the above...which should eventually go away)
+ my $error = $cust_pay_pending->delete;
+ # don't show any errors related to this; they're not meaningful
+ warn "error canceling pending payment $paypendingnum: $error\n" if $error;
+ return { 'error' => '_cancel',
+ 'session_id' => $cust_pay_pending->session_id };
+ } else {
+ # create the payment
+ my $bill_error =
+ $cust_main->realtime_botpp_capture( $cust_pay_pending,
+ %{$packet->{data}},
+ apply => 1,
+ );
- return { 'error' => ( $bill_error->{bill_error} ? '_decline' : '' ),
- %$bill_error,
- };
+ return { 'error' => ( $bill_error->{bill_error} ? '_decline' : '' ),
+ %$bill_error,
+ };
+ }
}
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 831ffe2a3..3c445200c 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -1038,6 +1038,9 @@ sub reason_type_options {
'select_hash' => [
'%b %o, %Y' => 'Mon DDth, YYYY',
'%e %b %Y' => 'DD Mon YYYY',
+ '%m/%d/%Y' => 'MM/DD/YYYY',
+ '%d/%m/%Y' => 'DD/MM/YYYY',
+ '%Y/%m/%d' => 'YYYY/MM/DD',
],
},
@@ -2098,7 +2101,7 @@ and customer address. Include units.',
'section' => 'self-service',
'description' => 'Acceptable payment types for the signup server',
'type' => 'selectmultiple',
- 'select_enum' => [ qw(CARD DCRD CHEK DCHK LECB PREPAY BILL COMP) ],
+ 'select_enum' => [ qw(CARD DCRD CHEK DCHK LECB PREPAY PPAL BILL COMP) ],
},
{
@@ -2159,11 +2162,18 @@ and customer address. Include units.',
{
'key' => 'signup_server-default_svcpart',
'section' => 'self-service',
- 'description' => 'Default service definition for the signup server - only necessary for services that trigger special provisioning widgets (such as DID provisioning).',
+ 'description' => 'Default service definition for the signup server - only necessary for services that trigger special provisioning widgets (such as DID provisioning or domain selection).',
'type' => 'select-part_svc',
},
{
+ 'key' => 'signup_server-default_domsvc',
+ 'section' => 'self-service',
+ 'description' => 'If specified, the default domain svcpart for signup (useful when domain is set to selectable choice).',
+ 'type' => 'text',
+ },
+
+ {
'key' => 'signup_server-mac_addr_svcparts',
'section' => 'self-service',
'description' => 'Service definitions which can receive mac addresses (current mapped to username for svc_acct).',
@@ -2472,7 +2482,7 @@ and customer address. Include units.',
'section' => 'billing',
'description' => 'Available payment types.',
'type' => 'selectmultiple',
- 'select_enum' => [ qw(CARD DCRD CHEK DCHK LECB BILL CASH WEST MCRD COMP) ],
+ 'select_enum' => [ qw(CARD DCRD CHEK DCHK LECB BILL CASH WEST MCRD PPAL COMP) ],
},
{
@@ -2480,7 +2490,7 @@ and customer address. Include units.',
'section' => 'UI',
'description' => 'Default payment type. HIDE disables display of billing information and sets customers to BILL.',
'type' => 'select',
- 'select_enum' => [ '', qw(CARD DCRD CHEK DCHK LECB BILL CASH WEST MCRD COMP HIDE) ],
+ 'select_enum' => [ '', qw(CARD DCRD CHEK DCHK LECB BILL CASH WEST MCRD PPAL COMP HIDE) ],
},
{
@@ -3823,6 +3833,13 @@ and customer address. Include units.',
'type' => 'checkbox',
},
+ {
+ 'key' => 'fuzzy-fuzziness',
+ 'section' => 'UI',
+ 'description' => 'Set the "fuzziness" of fuzzy searching (see the String::Approx manpage for details). Defaults to 10%',
+ 'type' => 'text',
+ },
+
{ 'key' => 'pkg_referral',
'section' => '',
'description' => 'Enable package-specific advertising sources.',
@@ -4124,6 +4141,13 @@ and customer address. Include units.',
},
{
+ 'key' => 'always_show_tax',
+ 'section' => 'invoicing',
+ 'description' => 'Show a line for tax on the invoice even when the tax is zero. Optionally provide text for the tax name to show.',
+ 'type' => [ qw(checkbox text) ],
+ },
+
+ {
'key' => 'address_standardize_method',
'section' => 'UI', #???
'description' => 'Method for standardizing customer addresses.',
@@ -5198,6 +5222,13 @@ and customer address. Include units.',
},
{
+ 'key' => 'invoice_payment_details',
+ 'section' => 'invoicing',
+ 'description' => 'When displaying payments on an invoice, show the payment method used, including the check or credit card number. Credit card numbers will be masked.',
+ 'type' => 'checkbox',
+ },
+
+ {
'key' => 'cust_main-status_module',
'section' => 'UI',
'description' => 'Which module to use for customer status display. The "Classic" module (the default) considers accounts with cancelled recurring packages but un-cancelled one-time charges Inactive. The "Recurring" module considers those customers Cancelled. Similarly for customers with suspended recurring packages but one-time charges.', #other differences?
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 43e9b0653..6653fb7e1 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -77,7 +77,7 @@ if ( -e $addl_handler_use_file ) {
use HTML::TableExtract qw(tree);
use HTML::FormatText;
use HTML::Defang;
- use JSON;
+ use JSON::XS;
# use XMLRPC::Transport::HTTP;
# use XMLRPC::Lite; # for XMLRPC::Serializer
use MIME::Base64;
@@ -160,6 +160,7 @@ if ( -e $addl_handler_use_file ) {
use FS::cust_credit;
use FS::cust_credit_bill;
use FS::cust_main;
+ use FS::h_cust_main;
use FS::cust_main::Search qw(smart_search);
use FS::cust_main::Import;
use FS::cust_main_county;
@@ -337,6 +338,7 @@ if ( -e $addl_handler_use_file ) {
use FS::part_pkg_usage_class;
use FS::part_pkg_usage;
use FS::cdr_cust_pkg_usage;
+ use FS::part_pkg_msgcat;
# Sammath Naur
if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index a868d4892..15636af9c 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -458,7 +458,13 @@ sub qsearch {
# grep defined( $record->{$_} ) && $record->{$_} ne '', @fields
# ) or croak "Error executing \"$statement\": ". $sth->errstr;
- $sth->execute or croak "Error executing \"$statement\": ". $sth->errstr;
+ my $ok = $sth->execute;
+ if (!$ok) {
+ my $error = "Error executing \"$statement\"";
+ $error .= ' (' . join(', ', map {"'$_'"} @value) . ')' if @value;
+ $error .= ': '. $sth->errstr;
+ croak $error;
+ }
my $table = $stable[0];
my $pkey = '';
@@ -1788,6 +1794,8 @@ sub batch_import {
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 );
}
diff --git a/FS/FS/Report/FCC_477.pm b/FS/FS/Report/FCC_477.pm
index 49bb8a852..fd088148b 100644
--- a/FS/FS/Report/FCC_477.pm
+++ b/FS/FS/Report/FCC_477.pm
@@ -22,26 +22,26 @@ Documentation.
=cut
@upload = qw(
- <200kpbs
- 200-768kpbs
+ <200kbps
+ 200-768kbps
768kbps-1.5mbps
1.5-3mpbs
3-6mbps
6-10mbps
10-25mbps
25-100mbps
- >100bmps
+ >100mbps
);
@download = qw(
- 200-768kpbs
+ 200-768kbps
768kbps-1.5mbps
- 1.5-3mpbs
+ 1.5-3mbps
3-6mbps
6-10mbps
10-25mbps
25-100mbps
- >100bmps
+ >100mbps
);
@technology = (
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 633e59cb6..28c7fc465 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1721,6 +1721,7 @@ sub tables_hashref {
'custnum', 'int', '', '', '', '',
'pkgpart', 'int', '', '', '', '',
'pkgbatch', 'varchar', 'NULL', $char_d, '', '',
+ 'contactnum', 'int', 'NULL', '', '', '',
'locationnum', 'int', 'NULL', '', '', '',
'otaker', 'varchar', 'NULL', 32, '', '',
'usernum', 'int', 'NULL', '', '', '',
@@ -1742,6 +1743,7 @@ sub tables_hashref {
'change_pkgnum', 'int', 'NULL', '', '', '',
'change_pkgpart', 'int', 'NULL', '', '', '',
'change_locationnum', 'int', 'NULL', '', '', '',
+ 'change_custnum', 'int', 'NULL', '', '', '',
'main_pkgnum', 'int', 'NULL', '', '', '',
'pkglinknum', 'int', 'NULL', '', '', '',
'manual_flag', 'char', 'NULL', 1, '', '',
@@ -2011,6 +2013,19 @@ sub tables_hashref {
],
},
+ 'part_pkg_msgcat' => {
+ 'columns' => [
+ 'pkgpartmsgnum', 'serial', '', '', '', '',
+ 'pkgpart', 'int', '', '', '', '',
+ 'locale', 'varchar', '', 16, '', '',
+ 'pkg', 'varchar', '', $char_d, '', '', #longer/no limit?
+ 'comment', 'varchar', 'NULL', 2*$char_d, '', '', #longer/no limit?
+ ],
+ 'primary_key' => 'pkgpartmsgnum',
+ 'unique' => [ [ 'pkgpart', 'locale' ] ],
+ 'index' => [],
+ },
+
'part_pkg_link' => {
'columns' => [
'pkglinknum', 'serial', '', '', '', '',
@@ -2715,9 +2730,10 @@ sub tables_hashref {
'columns' => [
'exportnum', 'serial', '', '', '', '',
'exportname', 'varchar', 'NULL', $char_d, '', '',
- 'machine', 'varchar', 'NULL', $char_d, '', '',
+ 'machine', 'varchar', 'NULL', $char_d, '', '',
'exporttype', 'varchar', '', $char_d, '', '',
'nodomain', 'char', 'NULL', 1, '', '',
+ 'default_machine','int', 'NULL', '', '', '',
],
'primary_key' => 'exportnum',
'unique' => [],
@@ -2894,22 +2910,28 @@ sub tables_hashref {
'svc_broadband' => {
'columns' => [
- 'svcnum', 'int', '', '', '', '',
- 'description', 'varchar', 'NULL', $char_d, '', '',
- 'routernum', 'int', 'NULL', '', '', '',
- 'blocknum', 'int', 'NULL', '', '', '',
- 'sectornum', 'int', 'NULL', '', '', '',
- 'speed_up', 'int', 'NULL', '', '', '',
- 'speed_down', 'int', 'NULL', '', '', '',
- 'ip_addr', 'varchar', 'NULL', 15, '', '',
- 'mac_addr', 'varchar', 'NULL', 12, '', '',
- 'authkey', 'varchar', 'NULL', 32, '', '',
- 'latitude', 'decimal', 'NULL', '10,7', '', '',
- 'longitude', 'decimal', 'NULL', '10,7', '', '',
- 'altitude', 'decimal', 'NULL', '', '', '',
- 'vlan_profile', 'varchar', 'NULL', $char_d, '', '',
- 'performance_profile', 'varchar', 'NULL', $char_d, '', '',
- 'plan_id', 'varchar', 'NULL', $char_d, '', '',
+ 'svcnum', 'int', '', '', '', '',
+ 'description', 'varchar', 'NULL', $char_d, '', '',
+ 'routernum', 'int', 'NULL', '', '', '',
+ 'blocknum', 'int', 'NULL', '', '', '',
+ 'sectornum', 'int', 'NULL', '', '', '',
+ 'speed_up', 'int', 'NULL', '', '', '',
+ 'speed_down', 'int', 'NULL', '', '', '',
+ 'ip_addr', 'varchar', 'NULL', 15, '', '',
+ 'mac_addr', 'varchar', 'NULL', 12, '', '',
+ 'authkey', 'varchar', 'NULL', 32, '', '',
+ 'latitude', 'decimal', 'NULL', '10,7', '', '',
+ 'longitude', 'decimal', 'NULL', '10,7', '', '',
+ 'altitude', 'decimal', 'NULL', '', '', '',
+ 'vlan_profile', 'varchar', 'NULL', $char_d, '', '',
+ 'performance_profile', 'varchar', 'NULL', $char_d, '', '',
+ 'plan_id', 'varchar', 'NULL', $char_d, '', '',
+ 'radio_serialnum', 'varchar', 'NULL', $char_d, '', '',
+ 'radio_location', 'varchar', 'NULL', 2*$char_d, '', '',
+ 'poe_location', 'varchar', 'NULL', 2*$char_d, '', '',
+ 'rssi', 'int', 'NULL', '', '', '',
+ 'suid', 'int', 'NULL', '', '', '',
+ 'shared_svcnum', 'int', 'NULL', '', '', '',
],
'primary_key' => 'svcnum',
'unique' => [ [ 'ip_addr' ], [ 'mac_addr' ] ],
@@ -3246,7 +3268,8 @@ sub tables_hashref {
'gateway_username', 'varchar', 'NULL', $char_d, '', '',
'gateway_password', 'varchar', 'NULL', $char_d, '', '',
'gateway_action', 'varchar', 'NULL', $char_d, '', '',
- 'gateway_callback_url', 'varchar', 'NULL', $char_d, '', '',
+ 'gateway_callback_url', 'varchar', 'NULL', 255, '', '',
+ 'gateway_cancel_url', 'varchar', 'NULL', 255, '', '',
'disabled', 'char', 'NULL', 1, '', '',
],
'primary_key' => 'gatewaynum',
diff --git a/FS/FS/TemplateItem_Mixin.pm b/FS/FS/TemplateItem_Mixin.pm
index 324f05248..8b0e16a2d 100644
--- a/FS/FS/TemplateItem_Mixin.pm
+++ b/FS/FS/TemplateItem_Mixin.pm
@@ -52,10 +52,10 @@ line item, and for generic taxes, simply returns "Tax".
=cut
sub desc {
- my $self = shift;
+ my( $self, $locale ) = @_;
if ( $self->pkgnum > 0 ) {
- $self->itemdesc || $self->part_pkg->pkg;
+ $self->itemdesc || $self->part_pkg->pkg_locale($locale);
} else {
my $desc = $self->itemdesc || 'Tax';
$desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm
index e3958a436..dd1796c7d 100644
--- a/FS/FS/Template_Mixin.pm
+++ b/FS/FS/Template_Mixin.pm
@@ -597,12 +597,54 @@ sub print_generic {
# info from customer's last invoice before this one, for some
# summary formats
$invoice_data{'last_bill'} = {};
- my $last_bill = $pr_cust_bill[-1];
+ # returns the last unpaid bill, not the last bill
+ #my $last_bill = $pr_cust_bill[-1];
+ # THIS returns the customer's last bill before this one
+ my $last_bill = qsearchs({
+ 'table' => 'cust_bill',
+ 'hashref' => { 'custnum' => $self->custnum,
+ 'invnum' => { op => '<', value => $self->invnum },
+ },
+ 'order_by' => ' ORDER BY invnum DESC LIMIT 1'
+ });
if ( $last_bill ) {
$invoice_data{'last_bill'} = {
'_date' => $last_bill->_date, #unformatted
# all we need for now
};
+ my (@payments, @credits);
+ # for formats that itemize previous payments
+ foreach my $cust_pay ( qsearch('cust_pay', {
+ 'custnum' => $self->custnum,
+ '_date' => { op => '>=',
+ value => $last_bill->_date }
+ } ) )
+ {
+ next if $cust_pay->_date > $self->_date;
+ push @payments, {
+ '_date' => $cust_pay->_date,
+ 'date' => time2str($date_format, $cust_pay->_date),
+ 'payinfo' => $cust_pay->payby_payinfo_pretty,
+ 'amount' => sprintf('%.2f', $cust_pay->paid),
+ };
+ # not concerned about applications
+ }
+ foreach my $cust_credit ( qsearch('cust_credit', {
+ 'custnum' => $self->custnum,
+ '_date' => { op => '>=',
+ value => $last_bill->_date }
+ } ) )
+ {
+ next if $cust_credit->_date > $self->_date;
+ push @credits, {
+ '_date' => $cust_credit->_date,
+ 'date' => time2str($date_format, $cust_credit->_date),
+ 'creditreason'=> $cust_credit->cust_credit->reason,
+ 'amount' => sprintf('%.2f', $cust_credit->amount),
+ };
+ }
+ $invoice_data{'previous_payments'} = \@payments;
+ $invoice_data{'previous_credits'} = \@credits;
}
my $summarypage = '';
@@ -687,6 +729,11 @@ sub print_generic {
my $other_money_char = $other_money_chars{$format};
$invoice_data{'dollar'} = $other_money_char;
+ my %minus_signs = ( 'latex' => '$-$',
+ 'html' => '−',
+ 'template' => '- ' );
+ my $minus = $minus_signs{$format};
+
my @detail_items = ();
my @total_items = ();
my @buf = ();
@@ -971,7 +1018,8 @@ sub print_generic {
warn "$me adding taxes\n"
if $DEBUG > 1;
- foreach my $tax ( $self->_items_tax ) {
+ my @items_tax = $self->_items_tax;
+ foreach my $tax ( @items_tax ) {
$taxtotal += $tax->{'amount'};
@@ -1006,7 +1054,7 @@ sub print_generic {
}
- if ( $taxtotal ) {
+ if ( @items_tax ) {
my $total = {};
$total->{'total_item'} = $self->mt('Sub-total');
$total->{'total_amount'} =
@@ -1106,7 +1154,7 @@ sub print_generic {
my $total;
$total->{'total_item'} = &$escape_function($credit->{'description'});
$credittotal += $credit->{'amount'};
- $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
+ $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
$adjusttotal += $credit->{'amount'};
if ( $multisection ) {
my $money = $old_latex ? '' : $money_char;
@@ -1137,7 +1185,7 @@ sub print_generic {
my $total = {};
$total->{'total_item'} = &$escape_function($payment->{'description'});
$paymenttotal += $payment->{'amount'};
- $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
+ $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
$adjusttotal += $payment->{'amount'};
if ( $multisection ) {
my $money = $old_latex ? '' : $money_char;
@@ -2129,7 +2177,17 @@ sub _taxsort {
sub _items_tax {
my $self = shift;
my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
- $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+ my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+
+ if ( $self->conf->exists('always_show_tax') ) {
+ my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
+ if (0 == grep { $_->{description} eq $itemdesc } @items) {
+ push @items,
+ { 'description' => $itemdesc,
+ 'amount' => 0.00 };
+ }
+ }
+ @items;
}
=item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
@@ -2181,6 +2239,7 @@ sub _items_cust_bill_pkg {
my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
# and location labels
+ my $locale = $cust_main->locale;
my @b = ();
my ($s, $r, $u) = ( undef, undef, undef );
@@ -2225,7 +2284,7 @@ sub _items_cust_bill_pkg {
my $type = $display->type;
- my $desc = $cust_bill_pkg->desc;
+ my $desc = $cust_bill_pkg->desc( $cust_main->locale );
$desc = substr($desc, 0, $maxlength). '...'
if $format eq 'latex' && length($desc) > $maxlength;
diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm
index c11e6c951..f63854ca0 100644
--- a/FS/FS/UI/Web.pm
+++ b/FS/FS/UI/Web.pm
@@ -472,23 +472,26 @@ sub cust_fields_subs {
my $unlinked_warn = 0;
return map {
my $f = $_;
- if( $unlinked_warn++ ) {
+ if ( $unlinked_warn++ ) {
+
sub {
my $record = shift;
- if( $record->custnum ) {
- $record->$f(@_);
- }
- else {
+ if ( $record->custnum ) {
+ encode_entities( $record->$f(@_) );
+ } else {
'(unlinked)'
};
- }
- }
- else {
+ };
+
+ } else {
+
sub {
my $record = shift;
- $record->$f(@_) if $record->custnum;
- }
+ $record->custnum ? encode_entities( $record->$f(@_) ) : '';
+ };
+
}
+
} @cust_fields;
}
@@ -578,7 +581,7 @@ use vars qw($DEBUG);
use Carp;
use Storable qw(nfreeze);
use MIME::Base64;
-use JSON;
+use JSON::XS;
use FS::CurrentUser;
use FS::Record qw(qsearchs);
use FS::queue;
@@ -723,10 +726,7 @@ sub job_status {
@return = ( 'error', $job ? $job->statustext : $jobnum );
}
- #to_json(\@return); #waiting on deb 5.0 for new JSON.pm?
- #silence the warning though
- my $to_json = JSON->can('to_json') || JSON->can('objToJson');
- &$to_json(\@return);
+ encode_json \@return;
}
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index fea53a235..cda3198eb 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -294,6 +294,9 @@ sub upgrade_data {
#insert default tower_sector if not present
'tower' => [],
+ #repair improperly deleted services
+ 'cust_svc' => [],
+
#routernum/blocknum
'svc_broadband' => [],
diff --git a/FS/FS/access_right.pm b/FS/FS/access_right.pm
index 0e8bf45a9..5bcf92214 100644
--- a/FS/FS/access_right.pm
+++ b/FS/FS/access_right.pm
@@ -229,7 +229,10 @@ sub _upgrade_data { # class method
'Usage: Unrateable CDRs',
],
'Provision customer service' => [ 'Edit password' ],
-
+ 'Financial reports' => [ 'Employees: Commission Report',
+ 'Employees: Audit Report',
+ ],
+ 'Change customer package' => 'Detach customer package',
;
foreach my $old_acl ( keys %onetime ) {
diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm
index 3794d3f1d..9b322093b 100644
--- a/FS/FS/agent.pm
+++ b/FS/FS/agent.pm
@@ -216,7 +216,7 @@ an attempt will be made to select a gateway suited for the taxes paid on
the invoice.
The I and I options can be used to influence the choice
-as well. Presently only 'CC' and 'ECHECK' methods are meaningful.
+as well. Presently only 'CC', 'ECHECK', and 'PAYPAL' methods are meaningful.
When the I is 'CC' then the card number in I can direct
this routine to route to a gateway suited for that type of card.
@@ -246,13 +246,17 @@ sub payment_gateway {
}
#look for an agent gateway override first
- my $cardtype;
- if ( $options{method} && $options{method} eq 'CC' && $options{payinfo} ) {
- $cardtype = cardtype($options{payinfo});
- } elsif ( $options{method} && $options{method} eq 'ECHECK' ) {
- $cardtype = 'ACH';
- } else {
- $cardtype = $options{method} || '';
+ my $cardtype = '';
+ if ( $options{method} ) {
+ if ( $options{method} eq 'CC' && $options{payinfo} ) {
+ $cardtype = cardtype($options{payinfo});
+ } elsif ( $options{method} eq 'ECHECK' ) {
+ $cardtype = 'ACH';
+ } elsif ( $options{method} eq 'PAYPAL' ) {
+ $cardtype = 'PayPal';
+ } else {
+ $cardtype = $options{method}
+ }
}
my $override =
diff --git a/FS/FS/cdr/gsm_tap3_12.pm b/FS/FS/cdr/gsm_tap3_12.pm
index b1496ac92..275e7b35c 100644
--- a/FS/FS/cdr/gsm_tap3_12.pm
+++ b/FS/FS/cdr/gsm_tap3_12.pm
@@ -6,6 +6,7 @@ use vars qw( %info %TZ );
use Time::Local;
#use Data::Dumper;
+#false laziness w/huawei_softx3000.pm
%TZ = (
'+0000' => 'XXX-0',
'+0100' => 'XXX-1',
diff --git a/FS/FS/cdr/huawei_softx3000.pm b/FS/FS/cdr/huawei_softx3000.pm
new file mode 100644
index 000000000..e66af43a9
--- /dev/null
+++ b/FS/FS/cdr/huawei_softx3000.pm
@@ -0,0 +1,2689 @@
+package FS::cdr::huawei_softx3000;
+use base qw( FS::cdr );
+
+use strict;
+use vars qw( %info %TZ );
+use subs qw( ts24008_number TimeStamp );
+use Time::Local;
+use FS::Record qw( qsearch );
+use FS::cdr_calltype;
+
+#false laziness w/gsm_tap3_12.pm
+%TZ = (
+ '+0000' => 'XXX-0',
+ '+0100' => 'XXX-1',
+ '+0200' => 'XXX-2',
+ '+0300' => 'XXX-3',
+ '+0400' => 'XXX-4',
+ '+0500' => 'XXX-5',
+ '+0600' => 'XXX-6',
+ '+0700' => 'XXX-7',
+ '+0800' => 'XXX-8',
+ '+0900' => 'XXX-9',
+ '+1000' => 'XXX-10',
+ '+1100' => 'XXX-11',
+ '+1200' => 'XXX-12',
+ '-0000' => 'XXX+0',
+ '-0100' => 'XXX+1',
+ '-0200' => 'XXX+2',
+ '-0300' => 'XXX+3',
+ '-0400' => 'XXX+4',
+ '-0500' => 'XXX+5',
+ '-0600' => 'XXX+6',
+ '-0700' => 'XXX+7',
+ '-0800' => 'XXX+8',
+ '-0900' => 'XXX+9',
+ '-1000' => 'XXX+10',
+ '-1100' => 'XXX+11',
+ '-1200' => 'XXX+12',
+);
+
+%info = (
+ 'name' => 'Huawei SoftX3000', #V100R006C05 ?
+ 'weight' => 160,
+ 'type' => 'asn.1',
+ 'import_fields' => [],
+ 'asn_format' => {
+ 'spec' => _asn_spec(),
+ 'macro' => 'CallEventDataFile',
+ 'header_buffer' => sub {
+ #my $CallEventDataFile = shift;
+
+ my %cdr_calltype = ( map { $_->calltypename => $_->calltypenum }
+ qsearch('cdr_calltype', {})
+ );
+
+ { cdr_calltype => \%cdr_calltype,
+ };
+
+ },
+ 'arrayref' => sub { shift->{'callEventRecords'} },
+ 'row_callback' => sub {
+ my( $row, $buffer ) = @_;
+ my @keys = keys %$row;
+ $buffer->{'key'} = $keys[0];
+ },
+ 'map' => {
+ 'src' => huawei_field('callingNumber', ts24008_number, ),
+
+ 'dst' => huawei_field('calledNumber', ts24008_number, ),
+
+ 'startdate' => huawei_field(['answerTime','deliveryTime'], TimeStamp),
+ 'answerdate' => huawei_field(['answerTime','deliveryTime'], TimeStamp),
+ 'enddate' => huawei_field('releaseTime', TimeStamp),
+ 'duration' => huawei_field('callDuration'),
+ 'billsec' => huawei_field('callDuration'),
+ #'disposition' => #diagnostics?
+ #'accountcode'
+ #'charged_party' => # 0 or 1, do something with this?
+ 'calltypenum' => sub {
+ my($rec, $buf) = @_;
+ my $key = $buf->{key};
+ $buf->{'cdr_calltype'}{ $key };
+ },
+ #'carrierid' =>
+ },
+
+ },
+);
+
+sub huawei_field {
+ my $field = shift;
+ my $decode = $_[0] ? shift : '';
+ return sub {
+ my($rec, $buf) = @_;
+
+ my $key = $buf->{key};
+
+ $field = ref($field) ? $field : [ $field ];
+ my $value = '';
+ foreach my $f (@$field) {
+ $value = $rec->{$key}{$f} and last;
+ }
+
+ $decode
+ ? &{ $decode }( $value )
+ : $value;
+
+ };
+}
+
+sub ts24008_number {
+ # This type contains the binary coded decimal representation of
+ # a directory number e.g. calling/called/connected/translated number.
+ # The encoding of the octet string is in accordance with the
+ # the elements "Calling party BCD number", "Called party BCD number"
+ # and "Connected number" defined in TS 24.008.
+ # This encoding includes type of number and number plan information
+ # together with a BCD encoded digit string.
+ # It may also contain both a presentation and screening indicator
+ # (octet 3a).
+ # For the avoidance of doubt, this field does not include
+ # octets 1 and 2, the element name and length, as this would be
+ # redundant.
+ #
+ #type id (per TS 24.008 page 490):
+ # low nybble: "numbering plan identification"
+ # high nybble: "type of number"
+ # 0 unknown
+ # 1 international
+ # 2 national
+ # 3 network specific
+ # 4 dedicated access, short code
+ # 5 reserved
+ # 6 reserved
+ # 7 reserved for extension
+ # (bit 8 "extension")
+ return sub {
+ my( $type_id, $value ) = unpack 'Ch*', shift;
+ $value =~ s/f$//; # If the called party BCD number contains an odd number
+ # of digits, bits 5 to 8 of the last octet shall be
+ # filled with an end mark coded as "1111".
+ $value;
+ };
+}
+
+sub TimeStamp {
+ # The contents of this field are a compact form of the UTCTime format
+ # containing local time plus an offset to universal time. Binary coded
+ # decimal encoding is employed for the digits to reduce the storage and
+ # transmission overhead
+ # e.g. YYMMDDhhmmssShhmm
+ # where
+ # YY = Year 00 to 99 BCD encoded
+ # MM = Month 01 to 12 BCD encoded
+ # DD = Day 01 to 31 BCD encoded
+ # hh = hour 00 to 23 BCD encoded
+ # mm = minute 00 to 59 BCD encoded
+ # ss = second 00 to 59 BCD encoded
+ # S = Sign 0 = "+", "-" ASCII encoded
+ # hh = hour 00 to 23 BCD encoded
+ # mm = minute 00 to 59 BCD encoded
+ return sub {
+ my($year, $mon, $day, $hour, $min, $sec, $tz_sign, $tz_hour, $tz_min, $dst)=
+ unpack 'H2H2H2H2H2H2AH2H2C', shift;
+ #warn "$year/$mon/$day $hour:$min:$sec $tz_sign$tz_hour$tz_min $dst\n";
+ return 0 unless $year; #y2100 bug
+ local($ENV{TZ}) = $TZ{ "$tz_sign$tz_hour$tz_min" };
+ timelocal($sec, $min, $hour, $day, $mon-1, $year);
+ };
+}
+
+sub _asn_spec {
+ <<'END';
+
+--DEFINITIONS IMPLICIT TAGS ::=
+
+--BEGIN
+
+--------------------------------------------------------------------------------
+--
+-- CALL AND EVENT RECORDS
+--
+------------------------------------------------------------------------------
+--Font: verdana 8
+
+CallEventRecord ::= CHOICE
+{
+ moCallRecord [0] MOCallRecord,
+ mtCallRecord [1] MTCallRecord,
+ roamingRecord [2] RoamingRecord,
+ incGatewayRecord [3] IncGatewayRecord,
+ outGatewayRecord [4] OutGatewayRecord,
+ transitRecord [5] TransitCallRecord,
+ moSMSRecord [6] MOSMSRecord,
+ mtSMSRecord [7] MTSMSRecord,
+ ssActionRecord [10] SSActionRecord,
+ hlrIntRecord [11] HLRIntRecord,
+ commonEquipRecord [14] CommonEquipRecord,
+ recTypeExtensions [15] ManagementExtensions,
+ termCAMELRecord [16] TermCAMELRecord,
+ mtLCSRecord [17] MTLCSRecord,
+ moLCSRecord [18] MOLCSRecord,
+ niLCSRecord [19] NILCSRecord,
+ forwardCallRecord [100] MOCallRecord
+}
+
+MOCallRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedIMEI [2] IMEI OPTIONAL,
+ servedMSISDN [3] MSISDN OPTIONAL,
+ callingNumber [4] CallingNumber OPTIONAL,
+ calledNumber [5] CalledNumber OPTIONAL,
+ translatedNumber [6] TranslatedNumber OPTIONAL,
+ connectedNumber [7] ConnectedNumber OPTIONAL,
+ roamingNumber [8] RoamingNumber OPTIONAL,
+ recordingEntity [9] RecordingEntity OPTIONAL,
+ mscIncomingROUTE [10] ROUTE OPTIONAL,
+ mscOutgoingROUTE [11] ROUTE OPTIONAL,
+ location [12] LocationAreaAndCell OPTIONAL,
+ changeOfLocation [13] SEQUENCE OF LocationChange OPTIONAL,
+ basicService [14] BasicServiceCode OPTIONAL,
+ transparencyIndicator [15] TransparencyInd OPTIONAL,
+ changeOfService [16] SEQUENCE OF ChangeOfService OPTIONAL,
+ supplServicesUsed [17] SEQUENCE OF SuppServiceUsed OPTIONAL,
+ aocParameters [18] AOCParameters OPTIONAL,
+ changeOfAOCParms [19] SEQUENCE OF AOCParmChange OPTIONAL,
+ msClassmark [20] Classmark OPTIONAL,
+ changeOfClassmark [21] ChangeOfClassmark OPTIONAL,
+ seizureTime [22] TimeStamp OPTIONAL,
+ answerTime [23] TimeStamp OPTIONAL,
+ releaseTime [24] TimeStamp OPTIONAL,
+ callDuration [25] CallDuration OPTIONAL,
+ radioChanRequested [27] RadioChanRequested OPTIONAL,
+ radioChanUsed [28] TrafficChannel OPTIONAL,
+ changeOfRadioChan [29] ChangeOfRadioChannel OPTIONAL,
+ causeForTerm [30] CauseForTerm OPTIONAL,
+ diagnostics [31] Diagnostics OPTIONAL,
+ callReference [32] CallReference OPTIONAL,
+ sequenceNumber [33] SequenceNumber OPTIONAL,
+ additionalChgInfo [34] AdditionalChgInfo OPTIONAL,
+ recordExtensions [35] ManagementExtensions OPTIONAL,
+ gsm-SCFAddress [36] Gsm-SCFAddress OPTIONAL,
+ serviceKey [37] ServiceKey OPTIONAL,
+ networkCallReference [38] NetworkCallReference OPTIONAL,
+ mSCAddress [39] MSCAddress OPTIONAL,
+ cAMELInitCFIndicator [40] CAMELInitCFIndicator OPTIONAL,
+ defaultCallHandling [41] DefaultCallHandling OPTIONAL,
+ fnur [45] Fnur OPTIONAL,
+ aiurRequested [46] AiurRequested OPTIONAL,
+ speechVersionSupported [49] SpeechVersionIdentifier OPTIONAL,
+ speechVersionUsed [50] SpeechVersionIdentifier OPTIONAL,
+ numberOfDPEncountered [51] INTEGER OPTIONAL,
+ levelOfCAMELService [52] LevelOfCAMELService OPTIONAL,
+ freeFormatData [53] FreeFormatData OPTIONAL,
+ cAMELCallLegInformation [54] SEQUENCE OF CAMELInformation OPTIONAL,
+ freeFormatDataAppend [55] BOOLEAN OPTIONAL,
+ defaultCallHandling-2 [56] DefaultCallHandling OPTIONAL,
+ gsm-SCFAddress-2 [57] Gsm-SCFAddress OPTIONAL,
+ serviceKey-2 [58] ServiceKey OPTIONAL,
+ freeFormatData-2 [59] FreeFormatData OPTIONAL,
+ freeFormatDataAppend-2 [60] BOOLEAN OPTIONAL,
+ systemType [61] SystemType OPTIONAL,
+ rateIndication [62] RateIndication OPTIONAL,
+ partialRecordType [69] PartialRecordType OPTIONAL,
+ guaranteedBitRate [70] GuaranteedBitRate OPTIONAL,
+ maximumBitRate [71] MaximumBitRate OPTIONAL,
+ modemType [139] ModemType OPTIONAL,
+ classmark3 [140] Classmark3 OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ callingChargeAreaCode [145] ChargeAreaCode OPTIONAL,
+ calledChargeAreaCode [146] ChargeAreaCode OPTIONAL,
+ mscOutgoingCircuit [166] MSCCIC OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ callEmlppPriority [170] EmlppPriority OPTIONAL,
+ callerDefaultEmlppPriority [171] EmlppPriority OPTIONAL,
+ eaSubscriberInfo [174] EASubscriberInfo OPTIONAL,
+ selectedCIC [175] SelectedCIC OPTIONAL,
+ optimalRoutingFlag [177] NULL OPTIONAL,
+ optimalRoutingLateForwardFlag [178] NULL OPTIONAL,
+ optimalRoutingEarlyForwardFlag [179] NULL OPTIONAL,
+ portedflag [180] PortedFlag OPTIONAL,
+ calledIMSI [181] IMSI OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ changeOfglobalAreaID [189] SEQUENCE OF ChangeOfglobalAreaID OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ intermediatemccmnc [193] MCCMNC OPTIONAL,
+ lastmccmnc [194] MCCMNC OPTIONAL,
+ cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL,
+ cUGInterlockCode [196] CUGInterlockCode OPTIONAL,
+ cUGOutgoingAccessUsed [197] CUGOutgoingAccessUsed OPTIONAL,
+ cUGIndex [198] CUGIndex OPTIONAL,
+ interactionWithIP [199] InteractionWithIP OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ setupTime [201] TimeStamp OPTIONAL,
+ alertingTime [202] TimeStamp OPTIONAL,
+ voiceIndicator [203] VoiceIndicator OPTIONAL,
+ bCategory [204] BCategory OPTIONAL,
+ callType [205] CallType OPTIONAL
+}
+
+--at moc callingNumber is the same as served msisdn except basic msisdn != calling number such as MSP service
+
+MTCallRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedIMEI [2] IMEI OPTIONAL,
+ servedMSISDN [3] CalledNumber OPTIONAL,
+ callingNumber [4] CallingNumber OPTIONAL,
+ connectedNumber [5] ConnectedNumber OPTIONAL,
+ recordingEntity [6] RecordingEntity OPTIONAL,
+ mscIncomingROUTE [7] ROUTE OPTIONAL,
+ mscOutgoingROUTE [8] ROUTE OPTIONAL,
+ location [9] LocationAreaAndCell OPTIONAL,
+ changeOfLocation [10] SEQUENCE OF LocationChange OPTIONAL,
+ basicService [11] BasicServiceCode OPTIONAL,
+ transparencyIndicator [12] TransparencyInd OPTIONAL,
+ changeOfService [13] SEQUENCE OF ChangeOfService OPTIONAL,
+ supplServicesUsed [14] SEQUENCE OF SuppServiceUsed OPTIONAL,
+ aocParameters [15] AOCParameters OPTIONAL,
+ changeOfAOCParms [16] SEQUENCE OF AOCParmChange OPTIONAL,
+ msClassmark [17] Classmark OPTIONAL,
+ changeOfClassmark [18] ChangeOfClassmark OPTIONAL,
+ seizureTime [19] TimeStamp OPTIONAL,
+ answerTime [20] TimeStamp OPTIONAL,
+ releaseTime [21] TimeStamp OPTIONAL,
+ callDuration [22] CallDuration OPTIONAL,
+ radioChanRequested [24] RadioChanRequested OPTIONAL,
+ radioChanUsed [25] TrafficChannel OPTIONAL,
+ changeOfRadioChan [26] ChangeOfRadioChannel OPTIONAL,
+ causeForTerm [27] CauseForTerm OPTIONAL,
+ diagnostics [28] Diagnostics OPTIONAL,
+ callReference [29] CallReference OPTIONAL,
+ sequenceNumber [30] SequenceNumber OPTIONAL,
+ additionalChgInfo [31] AdditionalChgInfo OPTIONAL,
+ recordExtensions [32] ManagementExtensions OPTIONAL,
+ networkCallReference [33] NetworkCallReference OPTIONAL,
+ mSCAddress [34] MSCAddress OPTIONAL,
+ fnur [38] Fnur OPTIONAL,
+ aiurRequested [39] AiurRequested OPTIONAL,
+ speechVersionSupported [42] SpeechVersionIdentifier OPTIONAL,
+ speechVersionUsed [43] SpeechVersionIdentifier OPTIONAL,
+ gsm-SCFAddress [44] Gsm-SCFAddress OPTIONAL,
+ serviceKey [45] ServiceKey OPTIONAL,
+ systemType [46] SystemType OPTIONAL,
+ rateIndication [47] RateIndication OPTIONAL,
+ partialRecordType [54] PartialRecordType OPTIONAL,
+ guaranteedBitRate [55] GuaranteedBitRate OPTIONAL,
+ maximumBitRate [56] MaximumBitRate OPTIONAL,
+ initialCallAttemptFlag [137] NULL OPTIONAL,
+ ussdCallBackFlag [138] NULL OPTIONAL,
+ modemType [139] ModemType OPTIONAL,
+ classmark3 [140] Classmark3 OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ callingChargeAreaCode [145]ChargeAreaCode OPTIONAL,
+ calledChargeAreaCode [146]ChargeAreaCode OPTIONAL,
+ defaultCallHandling [150] DefaultCallHandling OPTIONAL,
+ freeFormatData [151] FreeFormatData OPTIONAL,
+ freeFormatDataAppend [152] BOOLEAN OPTIONAL,
+ numberOfDPEncountered [153] INTEGER OPTIONAL,
+ levelOfCAMELService [154] LevelOfCAMELService OPTIONAL,
+ roamingNumber [160] RoamingNumber OPTIONAL,
+ mscIncomingCircuit [166] MSCCIC OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ callEmlppPriority [170] EmlppPriority OPTIONAL,
+ calledDefaultEmlppPriority [171] EmlppPriority OPTIONAL,
+ eaSubscriberInfo [174] EASubscriberInfo OPTIONAL,
+ selectedCIC [175] SelectedCIC OPTIONAL,
+ optimalRoutingFlag [177] NULL OPTIONAL,
+ portedflag [180] PortedFlag OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ changeOfglobalAreaID [189] SEQUENCE OF ChangeOfglobalAreaID OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ intermediatemccmnc [193] MCCMNC OPTIONAL,
+ lastmccmnc [194] MCCMNC OPTIONAL,
+ cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL,
+ cUGInterlockCode [196] CUGInterlockCode OPTIONAL,
+ cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL,
+ cUGIndex [198] CUGIndex OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ redirectingnumber [201] RedirectingNumber OPTIONAL,
+ redirectingcounter [202] RedirectingCounter OPTIONAL,
+ setupTime [203] TimeStamp OPTIONAL,
+ alertingTime [204] TimeStamp OPTIONAL,
+ calledNumber [205] CalledNumber OPTIONAL,
+ voiceIndicator [206] VoiceIndicator OPTIONAL,
+ bCategory [207] BCategory OPTIONAL,
+ callType [208] CallType OPTIONAL
+}
+
+RoamingRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedMSISDN [2] MSISDN OPTIONAL,
+ callingNumber [3] CallingNumber OPTIONAL,
+ roamingNumber [4] RoamingNumber OPTIONAL,
+ recordingEntity [5] RecordingEntity OPTIONAL,
+ mscIncomingROUTE [6] ROUTE OPTIONAL,
+ mscOutgoingROUTE [7] ROUTE OPTIONAL,
+ basicService [8] BasicServiceCode OPTIONAL,
+ transparencyIndicator [9] TransparencyInd OPTIONAL,
+ changeOfService [10] SEQUENCE OF ChangeOfService OPTIONAL,
+ supplServicesUsed [11] SEQUENCE OF SuppServiceUsed OPTIONAL,
+ seizureTime [12] TimeStamp OPTIONAL,
+ answerTime [13] TimeStamp OPTIONAL,
+ releaseTime [14] TimeStamp OPTIONAL,
+ callDuration [15] CallDuration OPTIONAL,
+ causeForTerm [17] CauseForTerm OPTIONAL,
+ diagnostics [18] Diagnostics OPTIONAL,
+ callReference [19] CallReference OPTIONAL,
+ sequenceNumber [20] SequenceNumber OPTIONAL,
+ recordExtensions [21] ManagementExtensions OPTIONAL,
+ networkCallReference [22] NetworkCallReference OPTIONAL,
+ mSCAddress [23] MSCAddress OPTIONAL,
+ partialRecordType [30] PartialRecordType OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ callingChargeAreaCode [145] ChargeAreaCode OPTIONAL,
+ calledChargeAreaCode [146] ChargeAreaCode OPTIONAL,
+ mscOutgoingCircuit [166] MSCCIC OPTIONAL,
+ mscIncomingCircuit [167] MSCCIC OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ callEmlppPriority [170] EmlppPriority OPTIONAL,
+ eaSubscriberInfo [174] EASubscriberInfo OPTIONAL,
+ selectedCIC [175] SelectedCIC OPTIONAL,
+ optimalRoutingFlag [177] NULL OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL,
+ cUGInterlockCode [196] CUGInterlockCode OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL
+}
+
+TermCAMELRecord ::= SET
+{
+ recordtype [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedMSISDN [2] MSISDN OPTIONAL,
+ recordingEntity [3] RecordingEntity OPTIONAL,
+ interrogationTime [4] TimeStamp OPTIONAL,
+ destinationRoutingAddress [5] DestinationRoutingAddress OPTIONAL,
+ gsm-SCFAddress [6] Gsm-SCFAddress OPTIONAL,
+ serviceKey [7] ServiceKey OPTIONAL,
+ networkCallReference [8] NetworkCallReference OPTIONAL,
+ mSCAddress [9] MSCAddress OPTIONAL,
+ defaultCallHandling [10] DefaultCallHandling OPTIONAL,
+ recordExtensions [11] ManagementExtensions OPTIONAL,
+ calledNumber [12] CalledNumber OPTIONAL,
+ callingNumber [13] CallingNumber OPTIONAL,
+ mscIncomingROUTE [14] ROUTE OPTIONAL,
+ mscOutgoingROUTE [15] ROUTE OPTIONAL,
+ seizureTime [16] TimeStamp OPTIONAL,
+ answerTime [17] TimeStamp OPTIONAL,
+ releaseTime [18] TimeStamp OPTIONAL,
+ callDuration [19] CallDuration OPTIONAL,
+ causeForTerm [21] CauseForTerm OPTIONAL,
+ diagnostics [22] Diagnostics OPTIONAL,
+ callReference [23] CallReference OPTIONAL,
+ sequenceNumber [24] SequenceNumber OPTIONAL,
+ numberOfDPEncountered [25] INTEGER OPTIONAL,
+ levelOfCAMELService [26] LevelOfCAMELService OPTIONAL,
+ freeFormatData [27] FreeFormatData OPTIONAL,
+ cAMELCallLegInformation [28] SEQUENCE OF CAMELInformation OPTIONAL,
+ freeFormatDataAppend [29] BOOLEAN OPTIONAL,
+ mscServerIndication [30] BOOLEAN OPTIONAL,
+ defaultCallHandling-2 [31] DefaultCallHandling OPTIONAL,
+ gsm-SCFAddress-2 [32] Gsm-SCFAddress OPTIONAL,
+ serviceKey-2 [33] ServiceKey OPTIONAL,
+ freeFormatData-2 [34] FreeFormatData OPTIONAL,
+ freeFormatDataAppend-2 [35] BOOLEAN OPTIONAL,
+ partialRecordType [42] PartialRecordType OPTIONAL,
+ basicService [130] BasicServiceCode OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL
+}
+
+IncGatewayRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ callingNumber [1] CallingNumber OPTIONAL,
+ calledNumber [2] CalledNumber OPTIONAL,
+ recordingEntity [3] RecordingEntity OPTIONAL,
+ mscIncomingROUTE [4] ROUTE OPTIONAL,
+ mscOutgoingROUTE [5] ROUTE OPTIONAL,
+ seizureTime [6] TimeStamp OPTIONAL,
+ answerTime [7] TimeStamp OPTIONAL,
+ releaseTime [8] TimeStamp OPTIONAL,
+ callDuration [9] CallDuration OPTIONAL,
+ causeForTerm [11] CauseForTerm OPTIONAL,
+ diagnostics [12] Diagnostics OPTIONAL,
+ callReference [13] CallReference OPTIONAL,
+ sequenceNumber [14] SequenceNumber OPTIONAL,
+ recordExtensions [15] ManagementExtensions OPTIONAL,
+ partialRecordType [22] PartialRecordType OPTIONAL,
+ iSDN-BC [23] ISDN-BC OPTIONAL,
+ lLC [24] LLC OPTIONAL,
+ hLC [25] HLC OPTIONAL,
+ basicService [130] BasicServiceCode OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ rateIndication [159] RateIndication OPTIONAL,
+ roamingNumber [160] RoamingNumber OPTIONAL,
+ mscIncomingCircuit [167] MSCCIC OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ callEmlppPriority [170] EmlppPriority OPTIONAL,
+ eaSubscriberInfo [174] EASubscriberInfo OPTIONAL,
+ selectedCIC [175] SelectedCIC OPTIONAL,
+ cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL,
+ cUGInterlockCode [196] CUGInterlockCode OPTIONAL,
+ cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL,
+ mscIncomingRouteAttribute [198] RouteAttribute OPTIONAL,
+ mscOutgoingRouteAttribute [199] RouteAttribute OPTIONAL,
+ networkCallReference [200] NetworkCallReference OPTIONAL,
+ setupTime [201] TimeStamp OPTIONAL,
+ alertingTime [202] TimeStamp OPTIONAL,
+ voiceIndicator [203] VoiceIndicator OPTIONAL,
+ bCategory [204] BCategory OPTIONAL,
+ callType [205] CallType OPTIONAL
+}
+
+OutGatewayRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ callingNumber [1] CallingNumber OPTIONAL,
+ calledNumber [2] CalledNumber OPTIONAL,
+ recordingEntity [3] RecordingEntity OPTIONAL,
+ mscIncomingROUTE [4] ROUTE OPTIONAL,
+ mscOutgoingROUTE [5] ROUTE OPTIONAL,
+ seizureTime [6] TimeStamp OPTIONAL,
+ answerTime [7] TimeStamp OPTIONAL,
+ releaseTime [8] TimeStamp OPTIONAL,
+ callDuration [9] CallDuration OPTIONAL,
+ causeForTerm [11] CauseForTerm OPTIONAL,
+ diagnostics [12] Diagnostics OPTIONAL,
+ callReference [13] CallReference OPTIONAL,
+ sequenceNumber [14] SequenceNumber OPTIONAL,
+ recordExtensions [15] ManagementExtensions OPTIONAL,
+ partialRecordType [22] PartialRecordType OPTIONAL,
+ basicService [130] BasicServiceCode OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ rateIndication [159] RateIndication OPTIONAL,
+ roamingNumber [160] RoamingNumber OPTIONAL,
+ mscOutgoingCircuit [166] MSCCIC OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ eaSubscriberInfo [174] EASubscriberInfo OPTIONAL,
+ selectedCIC [175] SelectedCIC OPTIONAL,
+ callEmlppPriority [170] EmlppPriority OPTIONAL,
+ cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL,
+ cUGInterlockCode [196] CUGInterlockCode OPTIONAL,
+ cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL,
+ mscIncomingRouteAttribute [198] RouteAttribute OPTIONAL,
+ mscOutgoingRouteAttribute [199] RouteAttribute OPTIONAL,
+ networkCallReference [200] NetworkCallReference OPTIONAL,
+ setupTime [201] TimeStamp OPTIONAL,
+ alertingTime [202] TimeStamp OPTIONAL,
+ voiceIndicator [203] VoiceIndicator OPTIONAL,
+ bCategory [204] BCategory OPTIONAL,
+ callType [205] CallType OPTIONAL
+}
+
+TransitCallRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ recordingEntity [1] RecordingEntity OPTIONAL,
+ mscIncomingROUTE [2] ROUTE OPTIONAL,
+ mscOutgoingROUTE [3] ROUTE OPTIONAL,
+ callingNumber [4] CallingNumber OPTIONAL,
+ calledNumber [5] CalledNumber OPTIONAL,
+ isdnBasicService [6] BasicService OPTIONAL,
+ seizureTime [7] TimeStamp OPTIONAL,
+ answerTime [8] TimeStamp OPTIONAL,
+ releaseTime [9] TimeStamp OPTIONAL,
+ callDuration [10] CallDuration OPTIONAL,
+ causeForTerm [12] CauseForTerm OPTIONAL,
+ diagnostics [13] Diagnostics OPTIONAL,
+ callReference [14] CallReference OPTIONAL,
+ sequenceNumber [15] SequenceNumber OPTIONAL,
+ recordExtensions [16] ManagementExtensions OPTIONAL,
+ partialRecordType [23] PartialRecordType OPTIONAL,
+ basicService [130] BasicServiceCode OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ rateIndication [159] RateIndication OPTIONAL,
+ mscOutgoingCircuit [166] MSCCIC OPTIONAL,
+ mscIncomingCircuit [167] MSCCIC OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ callEmlppPriority [170] EmlppPriority OPTIONAL,
+ eaSubscriberInfo [174] EASubscriberInfo OPTIONAL,
+ selectedCIC [175] SelectedCIC OPTIONAL,
+ cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL,
+ cUGInterlockCode [196] CUGInterlockCode OPTIONAL,
+ cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL,
+ mscIncomingRouteAttribute [198] RouteAttribute OPTIONAL,
+ mscOutgoingRouteAttribute [199] RouteAttribute OPTIONAL,
+ networkCallReference [200] NetworkCallReference OPTIONAL,
+ setupTime [201] TimeStamp OPTIONAL,
+ alertingTime [202] TimeStamp OPTIONAL,
+ voiceIndicator [203] VoiceIndicator OPTIONAL,
+ bCategory [204] BCategory OPTIONAL,
+ callType [205] CallType OPTIONAL
+}
+
+MOSMSRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedIMEI [2] IMEI OPTIONAL,
+ servedMSISDN [3] MSISDN OPTIONAL,
+ msClassmark [4] Classmark OPTIONAL,
+ serviceCentre [5] AddressString OPTIONAL,
+ recordingEntity [6] RecordingEntity OPTIONAL,
+ location [7] LocationAreaAndCell OPTIONAL,
+ messageReference [8] MessageReference OPTIONAL,
+ originationTime [9] TimeStamp OPTIONAL,
+ smsResult [10] SMSResult OPTIONAL,
+ recordExtensions [11] ManagementExtensions OPTIONAL,
+ destinationNumber [12] SmsTpDestinationNumber OPTIONAL,
+ cAMELSMSInformation [13] CAMELSMSInformation OPTIONAL,
+ systemType [14] SystemType OPTIONAL,
+ basicService [130] BasicServiceCode OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ classmark3 [140] Classmark3 OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ smsUserDataType [195] SmsUserDataType OPTIONAL,
+ smstext [196] SMSTEXT OPTIONAL,
+ maximumNumberOfSMSInTheConcatenatedSMS [197] MaximumNumberOfSMSInTheConcatenatedSMS OPTIONAL,
+ concatenatedSMSReferenceNumber [198] ConcatenatedSMSReferenceNumber OPTIONAL,
+ sequenceNumberOfTheCurrentSMS [199] SequenceNumberOfTheCurrentSMS OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ callReference [201] CallReference OPTIONAL
+}
+
+MTSMSRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ serviceCentre [1] AddressString OPTIONAL,
+ servedIMSI [2] IMSI OPTIONAL,
+ servedIMEI [3] IMEI OPTIONAL,
+ servedMSISDN [4] MSISDN OPTIONAL,
+ msClassmark [5] Classmark OPTIONAL,
+ recordingEntity [6] RecordingEntity OPTIONAL,
+ location [7] LocationAreaAndCell OPTIONAL,
+ deliveryTime [8] TimeStamp OPTIONAL,
+ smsResult [9] SMSResult OPTIONAL,
+ recordExtensions [10] ManagementExtensions OPTIONAL,
+ systemType [11] SystemType OPTIONAL,
+ cAMELSMSInformation [12] CAMELSMSInformation OPTIONAL,
+ basicService [130] BasicServiceCode OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ classmark3 [140] Classmark3 OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ smsUserDataType [195] SmsUserDataType OPTIONAL,
+ smstext [196] SMSTEXT OPTIONAL,
+ maximumNumberOfSMSInTheConcatenatedSMS [197] MaximumNumberOfSMSInTheConcatenatedSMS OPTIONAL,
+ concatenatedSMSReferenceNumber [198] ConcatenatedSMSReferenceNumber OPTIONAL,
+ sequenceNumberOfTheCurrentSMS [199] SequenceNumberOfTheCurrentSMS OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ origination [201] CallingNumber OPTIONAL,
+ callReference [202] CallReference OPTIONAL
+}
+
+HLRIntRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedMSISDN [2] MSISDN OPTIONAL,
+ recordingEntity [3] RecordingEntity OPTIONAL,
+ basicService [4] BasicServiceCode OPTIONAL,
+ routingNumber [5] RoutingNumber OPTIONAL,
+ interrogationTime [6] TimeStamp OPTIONAL,
+ numberOfForwarding [7] NumberOfForwarding OPTIONAL,
+ interrogationResult [8] HLRIntResult OPTIONAL,
+ recordExtensions [9] ManagementExtensions OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ callReference [169] CallReference OPTIONAL
+}
+
+SSActionRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedIMEI [2] IMEI OPTIONAL,
+ servedMSISDN [3] MSISDN OPTIONAL,
+ msClassmark [4] Classmark OPTIONAL,
+ recordingEntity [5] RecordingEntity OPTIONAL,
+ location [6] LocationAreaAndCell OPTIONAL,
+ basicServices [7] BasicServices OPTIONAL,
+ supplService [8] SS-Code OPTIONAL,
+ ssAction [9] SSActionType OPTIONAL,
+ ssActionTime [10] TimeStamp OPTIONAL,
+ ssParameters [11] SSParameters OPTIONAL,
+ ssActionResult [12] SSActionResult OPTIONAL,
+ callReference [13] CallReference OPTIONAL,
+ recordExtensions [14] ManagementExtensions OPTIONAL,
+ systemType [15] SystemType OPTIONAL,
+ ussdCodingScheme [126] UssdCodingScheme OPTIONAL,
+ ussdString [127] SEQUENCE OF UssdString OPTIONAL,
+ ussdNotifyCounter [128] UssdNotifyCounter OPTIONAL,
+ ussdRequestCounter [129] UssdRequestCounter OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ classmark3 [140] Classmark3 OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL
+}
+
+CommonEquipRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ equipmentType [1] EquipmentType OPTIONAL,
+ equipmentId [2] EquipmentId OPTIONAL,
+ servedIMSI [3] IMSI OPTIONAL,
+ servedMSISDN [4] MSISDN OPTIONAL,
+ recordingEntity [5] RecordingEntity OPTIONAL,
+ basicService [6] BasicServiceCode OPTIONAL,
+ changeOfService [7] SEQUENCE OF ChangeOfService OPTIONAL,
+ supplServicesUsed [8] SEQUENCE OF SuppServiceUsed OPTIONAL,
+ seizureTime [9] TimeStamp OPTIONAL,
+ releaseTime [10] TimeStamp OPTIONAL,
+ callDuration [11] CallDuration OPTIONAL,
+ callReference [12] CallReference OPTIONAL,
+ sequenceNumber [13] SequenceNumber OPTIONAL,
+ recordExtensions [14] ManagementExtensions OPTIONAL,
+ systemType [15] SystemType OPTIONAL,
+ rateIndication [16] RateIndication OPTIONAL,
+ fnur [17] Fnur OPTIONAL,
+ partialRecordType [18] PartialRecordType OPTIONAL,
+ causeForTerm [100] CauseForTerm OPTIONAL,
+ diagnostics [101] Diagnostics OPTIONAL,
+ servedIMEI [102] IMEI OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL
+}
+
+------------------------------------------------------------------------------
+--
+-- OBSERVED IMEI TICKETS
+--
+------------------------------------------------------------------------------
+
+ObservedIMEITicket ::= SET
+{
+ servedIMEI [0] IMEI,
+ imeiStatus [1] IMEIStatus,
+ servedIMSI [2] IMSI,
+ servedMSISDN [3] MSISDN OPTIONAL,
+ recordingEntity [4] RecordingEntity,
+ eventTime [5] TimeStamp,
+ location [6] LocationAreaAndCell,
+ imeiCheckEvent [7] IMEICheckEvent OPTIONAL,
+ callReference [8] CallReference OPTIONAL,
+ recordExtensions [9] ManagementExtensions OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL
+}
+
+
+
+------------------------------------------------------------------------------
+--
+-- LOCATION SERICE TICKETS
+--
+------------------------------------------------------------------------------
+
+MTLCSRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ recordingEntity [1] RecordingEntity OPTIONAL,
+ lcsClientType [2] LCSClientType OPTIONAL,
+ lcsClientIdentity [3] LCSClientIdentity OPTIONAL,
+ servedIMSI [4] IMSI OPTIONAL,
+ servedMSISDN [5] MSISDN OPTIONAL,
+ locationType [6] LocationType OPTIONAL,
+ lcsQos [7] LCSQoSInfo OPTIONAL,
+ lcsPriority [8] LCS-Priority OPTIONAL,
+ mlc-Number [9] ISDN-AddressString OPTIONAL,
+ eventTimeStamp [10] TimeStamp OPTIONAL,
+ measureDuration [11] CallDuration OPTIONAL,
+ notificationToMSUser [12] NotificationToMSUser OPTIONAL,
+ privacyOverride [13] NULL OPTIONAL,
+ location [14] LocationAreaAndCell OPTIONAL,
+ locationEstimate [15] Ext-GeographicalInformation OPTIONAL,
+ positioningData [16] PositioningData OPTIONAL,
+ lcsCause [17] LCSCause OPTIONAL,
+ diagnostics [18] Diagnostics OPTIONAL,
+ systemType [19] SystemType OPTIONAL,
+ recordExtensions [20] ManagementExtensions OPTIONAL,
+ causeForTerm [21] CauseForTerm OPTIONAL,
+ lcsReferenceNumber [101] CallReferenceNumber OPTIONAL,
+ servedIMEI [102] IMEI OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ callReference [201] CallReference OPTIONAL
+}
+
+MOLCSRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ recordingEntity [1] RecordingEntity OPTIONAL,
+ lcsClientType [2] LCSClientType OPTIONAL,
+ lcsClientIdentity [3] LCSClientIdentity OPTIONAL,
+ servedIMSI [4] IMSI OPTIONAL,
+ servedMSISDN [5] MSISDN OPTIONAL,
+ molr-Type [6] MOLR-Type OPTIONAL,
+ lcsQos [7] LCSQoSInfo OPTIONAL,
+ lcsPriority [8] LCS-Priority OPTIONAL,
+ mlc-Number [9] ISDN-AddressString OPTIONAL,
+ eventTimeStamp [10] TimeStamp OPTIONAL,
+ measureDuration [11] CallDuration OPTIONAL,
+ location [12] LocationAreaAndCell OPTIONAL,
+ locationEstimate [13] Ext-GeographicalInformation OPTIONAL,
+ positioningData [14] PositioningData OPTIONAL,
+ lcsCause [15] LCSCause OPTIONAL,
+ diagnostics [16] Diagnostics OPTIONAL,
+ systemType [17] SystemType OPTIONAL,
+ recordExtensions [18] ManagementExtensions OPTIONAL,
+ causeForTerm [19] CauseForTerm OPTIONAL,
+ lcsReferenceNumber [101] CallReferenceNumber OPTIONAL,
+ servedIMEI [102] IMEI OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ callReference [201] CallReference OPTIONAL
+}
+
+NILCSRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ recordingEntity [1] RecordingEntity OPTIONAL,
+ lcsClientType [2] LCSClientType OPTIONAL,
+ lcsClientIdentity [3] LCSClientIdentity OPTIONAL,
+ servedIMSI [4] IMSI OPTIONAL,
+ servedMSISDN [5] MSISDN OPTIONAL,
+ servedIMEI [6] IMEI OPTIONAL,
+ emsDigits [7] ISDN-AddressString OPTIONAL,
+ emsKey [8] ISDN-AddressString OPTIONAL,
+ lcsQos [9] LCSQoSInfo OPTIONAL,
+ lcsPriority [10] LCS-Priority OPTIONAL,
+ mlc-Number [11] ISDN-AddressString OPTIONAL,
+ eventTimeStamp [12] TimeStamp OPTIONAL,
+ measureDuration [13] CallDuration OPTIONAL,
+ location [14] LocationAreaAndCell OPTIONAL,
+ locationEstimate [15] Ext-GeographicalInformation OPTIONAL,
+ positioningData [16] PositioningData OPTIONAL,
+ lcsCause [17] LCSCause OPTIONAL,
+ diagnostics [18] Diagnostics OPTIONAL,
+ systemType [19] SystemType OPTIONAL,
+ recordExtensions [20] ManagementExtensions OPTIONAL,
+ causeForTerm [21] CauseForTerm OPTIONAL,
+ lcsReferenceNumber [101] CallReferenceNumber OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ callReference [201] CallReference OPTIONAL
+}
+
+
+------------------------------------------------------------------------------
+--
+-- FTAM / FTP / TFTP FILE CONTENTS
+--
+------------------------------------------------------------------------------
+
+CallEventDataFile ::= SEQUENCE
+{
+ headerRecord [0] HeaderRecord,
+ callEventRecords [1] SEQUENCE OF CallEventRecord,
+ trailerRecord [2] TrailerRecord,
+ extensions [3] ManagementExtensions
+}
+
+ObservedIMEITicketFile ::= SEQUENCE
+{
+ productionDateTime [0] TimeStamp,
+ observedIMEITickets [1] SEQUENCE OF ObservedIMEITicket,
+ noOfRecords [2] INTEGER,
+ extensions [3] ManagementExtensions
+}
+
+HeaderRecord ::= SEQUENCE
+{
+ productionDateTime [0] TimeStamp,
+ recordingEntity [1] RecordingEntity,
+ extensions [2] ManagementExtensions
+}
+
+TrailerRecord ::= SEQUENCE
+{
+ productionDateTime [0] TimeStamp,
+ recordingEntity [1] RecordingEntity,
+ firstCallDateTime [2] TimeStamp,
+ lastCallDateTime [3] TimeStamp,
+ noOfRecords [4] INTEGER,
+ extensions [5] ManagementExtensions
+}
+
+
+------------------------------------------------------------------------------
+--
+-- COMMON DATA TYPES
+--
+------------------------------------------------------------------------------
+
+AdditionalChgInfo ::= SEQUENCE
+{
+ chargeIndicator [0] ChargeIndicator OPTIONAL,
+ chargeParameters [1] OCTET STRING OPTIONAL
+}
+
+AddressString ::= OCTET STRING -- (SIZE (1..maxAddressLength))
+ -- This type is used to represent a number for addressing
+ -- purposes. It is composed of
+ -- a) one octet for nature of address, and numbering plan
+ -- indicator.
+ -- b) digits of an address encoded as TBCD-String.
+
+ -- a) The first octet includes a one bit extension indicator, a
+ -- 3 bits nature of address indicator and a 4 bits numbering
+ -- plan indicator, encoded as follows:
+
+ -- bit 8: 1 (no extension)
+
+ -- bits 765: nature of address indicator
+ -- 000 unknown
+ -- 001 international number
+ -- 010 national significant number
+ -- 011 network specific number
+ -- 100 subscriber number
+ -- 101 reserved
+ -- 110 abbreviated number
+ -- 111 reserved for extension
+
+ -- bits 4321: numbering plan indicator
+ -- 0000 unknown
+ -- 0001 ISDN/Telephony Numbering Plan (Rec CCITT E.164)
+ -- 0010 spare
+ -- 0011 data numbering plan (CCITT Rec X.121)
+ -- 0100 telex numbering plan (CCITT Rec F.69)
+ -- 0101 spare
+ -- 0110 land mobile numbering plan (CCITT Rec E.212)
+ -- 0111 spare
+ -- 1000 national numbering plan
+ -- 1001 private numbering plan
+ -- 1111 reserved for extension
+
+ -- all other values are reserved.
+
+ -- b) The following octets representing digits of an address
+ -- encoded as a TBCD-STRING.
+
+-- maxAddressLength INTEGER ::= 20
+
+AiurRequested ::= ENUMERATED
+{
+ --
+ -- See Bearer Capability TS 24.008
+ -- (note that value "4" is intentionally missing
+ -- because it is not used in TS 24.008)
+ --
+
+ aiur09600BitsPerSecond (1),
+ aiur14400BitsPerSecond (2),
+ aiur19200BitsPerSecond (3),
+ aiur28800BitsPerSecond (5),
+ aiur38400BitsPerSecond (6),
+ aiur43200BitsPerSecond (7),
+ aiur57600BitsPerSecond (8),
+ aiur38400BitsPerSecond1 (9),
+ aiur38400BitsPerSecond2 (10),
+ aiur38400BitsPerSecond3 (11),
+ aiur38400BitsPerSecond4 (12)
+}
+
+AOCParameters ::= SEQUENCE
+{
+ --
+ -- See TS 22.024.
+ --
+ e1 [1] EParameter OPTIONAL,
+ e2 [2] EParameter OPTIONAL,
+ e3 [3] EParameter OPTIONAL,
+ e4 [4] EParameter OPTIONAL,
+ e5 [5] EParameter OPTIONAL,
+ e6 [6] EParameter OPTIONAL,
+ e7 [7] EParameter OPTIONAL
+}
+
+AOCParmChange ::= SEQUENCE
+{
+ changeTime [0] TimeStamp,
+ newParameters [1] AOCParameters
+}
+
+BasicService ::= OCTET STRING -- (SIZE(1))
+
+--This parameter identifies the ISDN Basic service as defined in ETSI specification ETS 300 196.
+-- allServices '00'h
+-- speech '01'h
+-- unrestricteDigtalInfo '02'h
+-- audio3k1HZ '03'h
+-- unrestricteDigtalInfowithtoneandannoucement '04'h
+-- telephony3k1HZ '20'h
+-- teletext '21'h
+-- telefaxGroup4Class1 '22'h
+-- videotextSyntaxBased '23'h
+-- videotelephony '24'h
+-- telefaxGroup2-3 '25'h
+-- telephony7kHZ '26'h
+
+
+
+BasicServices ::= SET OF BasicServiceCode
+
+BasicServiceCode ::= CHOICE
+{
+ bearerService [2] BearerServiceCode,
+ teleservice [3] TeleserviceCode
+}
+
+
+TeleserviceCode ::= OCTET STRING -- (SIZE (1))
+ -- This type is used to represent the code identifying a single
+ -- teleservice, a group of teleservices, or all teleservices. The
+ -- services are defined in TS GSM 02.03.
+ -- The internal structure is defined as follows:
+
+ -- bits 87654321: group (bits 8765) and specific service
+ -- (bits 4321)
+
+-- allTeleservices (0x00),
+-- allSpeechTransmissionServices (0x10),
+-- telephony (0x11),
+-- emergencyCalls (0x12),
+--
+-- allShortMessageServices (0x20),
+-- shortMessageMT-PP (0x21),
+-- shortMessageMO-PP (0x22),
+--
+-- allFacsimileTransmissionServices (0x60),
+-- facsimileGroup3AndAlterSpeech (0x61),
+-- automaticFacsimileGroup3 (0x62),
+-- facsimileGroup4 (0x63),
+--
+-- The following non-hierarchical Compound Teleservice Groups
+-- are defined in TS GSM 02.30:
+-- allDataTeleservices (0x70),
+-- covers Teleservice Groups 'allFacsimileTransmissionServices'
+-- and 'allShortMessageServices'
+-- allTeleservices-ExeptSMS (0x80),
+-- covers Teleservice Groups 'allSpeechTransmissionServices' and
+-- 'allFacsimileTransmissionServices'
+--
+-- Compound Teleservice Group Codes are only used in call
+-- independent supplementary service operations, i.e. they
+-- are not used in InsertSubscriberData or in
+-- DeleteSubscriberData messages.
+--
+-- allVoiceGroupCallServices (0x90),
+-- voiceGroupCall (0x91),
+-- voiceBroadcastCall (0x92),
+--
+-- allPLMN-specificTS (0xd0),
+-- plmn-specificTS-1 (0xd1),
+-- plmn-specificTS-2 (0xd2),
+-- plmn-specificTS-3 (0xd3),
+-- plmn-specificTS-4 (0xd4),
+-- plmn-specificTS-5 (0xd5),
+-- plmn-specificTS-6 (0xd6),
+-- plmn-specificTS-7 (0xd7),
+-- plmn-specificTS-8 (0xd8),
+-- plmn-specificTS-9 (0xd9),
+-- plmn-specificTS-A (0xda),
+-- plmn-specificTS-B (0xdb),
+-- plmn-specificTS-C (0xdc),
+-- plmn-specificTS-D (0xdd),
+-- plmn-specificTS-E (0xde),
+-- plmn-specificTS-F (0xdf)
+
+
+BearerServiceCode ::= OCTET STRING -- (SIZE (1))
+ -- This type is used to represent the code identifying a single
+ -- bearer service, a group of bearer services, or all bearer
+ -- services. The services are defined in TS 3GPP TS 22.002 [3].
+ -- The internal structure is defined as follows:
+ --
+ -- plmn-specific bearer services:
+ -- bits 87654321: defined by the HPLMN operator
+
+ -- rest of bearer services:
+ -- bit 8: 0 (unused)
+ -- bits 7654321: group (bits 7654), and rate, if applicable
+ -- (bits 321)
+
+-- allBearerServices (0x00),
+-- allDataCDA-Services (0x10),
+-- dataCDA-300bps (0x11),
+-- dataCDA-1200bps (0x12),
+-- dataCDA-1200-75bps (0x13),
+-- dataCDA-2400bps (0x14),
+-- dataCDA-4800bps (0x15),
+-- dataCDA-9600bps (0x16),
+-- general-dataCDA (0x17),
+--
+-- allDataCDS-Services (0x18),
+-- dataCDS-1200bps (0x1a),
+-- dataCDS-2400bps (0x1c),
+-- dataCDS-4800bps (0x1d),
+-- dataCDS-9600bps (0x1e),
+-- general-dataCDS (0x1f),
+--
+-- allPadAccessCA-Services (0x20),
+-- padAccessCA-300bps (0x21),
+-- padAccessCA-1200bps (0x22),
+-- padAccessCA-1200-75bps (0x23),
+-- padAccessCA-2400bps (0x24),
+-- padAccessCA-4800bps (0x25),
+-- padAccessCA-9600bps (0x26),
+-- general-padAccessCA (0x27),
+--
+-- allDataPDS-Services (0x28),
+-- dataPDS-2400bps (0x2c),
+-- dataPDS-4800bps (0x2d),
+-- dataPDS-9600bps (0x2e),
+-- general-dataPDS (0x2f),
+--
+-- allAlternateSpeech-DataCDA (0x30),
+--
+-- allAlternateSpeech-DataCDS (0x38),
+--
+-- allSpeechFollowedByDataCDA (0x40),
+--
+-- allSpeechFollowedByDataCDS (0x48),
+--
+-- The following non-hierarchical Compound Bearer Service
+-- Groups are defined in TS GSM 02.30:
+-- allDataCircuitAsynchronous (0x50),
+-- covers "allDataCDA-Services", "allAlternateSpeech-DataCDA" and
+-- "allSpeechFollowedByDataCDA"
+-- allDataCircuitSynchronous (0x58),
+-- covers "allDataCDS-Services", "allAlternateSpeech-DataCDS" and
+-- "allSpeechFollowedByDataCDS"
+-- allAsynchronousServices (0x60),
+-- covers "allDataCDA-Services", "allAlternateSpeech-DataCDA",
+-- "allSpeechFollowedByDataCDA" and "allPadAccessCDA-Services"
+-- allSynchronousServices (0x68),
+-- covers "allDataCDS-Services", "allAlternateSpeech-DataCDS",
+-- "allSpeechFollowedByDataCDS" and "allDataPDS-Services"
+--
+-- Compound Bearer Service Group Codes are only used in call
+-- independent supplementary service operations, i.e. they
+-- are not used in InsertSubscriberData or in
+-- DeleteSubscriberData messages.
+--
+-- allPLMN-specificBS (0xd0),
+-- plmn-specificBS-1 (0xd1),
+-- plmn-specificBS-2 (0xd2),
+-- plmn-specificBS-3 (0xd3),
+-- plmn-specificBS-4 (0xd4),
+-- plmn-specificBS-5 (0xd5),
+-- plmn-specificBS-6 (0xd6),
+-- plmn-specificBS-7 (0xd7),
+-- plmn-specificBS-8 (0xd8),
+-- plmn-specificBS-9 (0xd9),
+-- plmn-specificBS-A (0xda),
+-- plmn-specificBS-B (0xdb),
+-- plmn-specificBS-C (0xdc),
+-- plmn-specificBS-D (0xdd),
+-- plmn-specificBS-E (0xde),
+-- plmn-specificBS-F (0xdf)
+
+
+BCDDirectoryNumber ::= OCTET STRING
+ -- This type contains the binary coded decimal representation of
+ -- a directory number e.g. calling/called/connected/translated number.
+ -- The encoding of the octet string is in accordance with the
+ -- the elements "Calling party BCD number", "Called party BCD number"
+ -- and "Connected number" defined in TS 24.008.
+ -- This encoding includes type of number and number plan information
+ -- together with a BCD encoded digit string.
+ -- It may also contain both a presentation and screening indicator
+ -- (octet 3a).
+ -- For the avoidance of doubt, this field does not include
+ -- octets 1 and 2, the element name and length, as this would be
+ -- redundant.
+
+CallDuration ::= INTEGER
+ --
+ -- The call duration in seconds.
+ -- For successful calls this is the chargeable duration.
+ -- For call attempts this is the call holding time.
+ --
+
+CallEventRecordType ::= ENUMERATED -- INTEGER
+{
+ moCallRecord (0),
+ mtCallRecord (1),
+ roamingRecord (2),
+ incGatewayRecord (3),
+ outGatewayRecord (4),
+ transitCallRecord (5),
+ moSMSRecord (6),
+ mtSMSRecord (7),
+ ssActionRecord (10),
+ hlrIntRecord (11),
+ commonEquipRecord (14),
+ moTraceRecord (15),
+ mtTraceRecord (16),
+ termCAMELRecord (17),
+ mtLCSRecord (23),
+ moLCSRecord (24),
+ niLCSRecord (25),
+ forwardCallRecord (100)
+}
+
+CalledNumber ::= BCDDirectoryNumber
+
+CallingNumber ::= BCDDirectoryNumber
+
+CallingPartyCategory ::= Category
+
+CallReference ::= OCTET STRING -- (SIZE (1..8))
+
+CallReferenceNumber ::= OCTET STRING -- (SIZE (1..8))
+
+CAMELDestinationNumber ::= DestinationRoutingAddress
+
+CAMELInformation ::= SET
+{
+ cAMELDestinationNumber [1] CAMELDestinationNumber OPTIONAL,
+ connectedNumber [2] ConnectedNumber OPTIONAL,
+ roamingNumber [3] RoamingNumber OPTIONAL,
+ mscOutgoingROUTE [4] ROUTE OPTIONAL,
+ seizureTime [5] TimeStamp OPTIONAL,
+ answerTime [6] TimeStamp OPTIONAL,
+ releaseTime [7] TimeStamp OPTIONAL,
+ callDuration [8] CallDuration OPTIONAL,
+ dataVolume [9] DataVolume OPTIONAL,
+ cAMELInitCFIndicator [10] CAMELInitCFIndicator OPTIONAL,
+ causeForTerm [11] CauseForTerm OPTIONAL,
+ cAMELModification [12] ChangedParameters OPTIONAL,
+ freeFormatData [13] FreeFormatData OPTIONAL,
+ diagnostics [14] Diagnostics OPTIONAL,
+ freeFormatDataAppend [15] BOOLEAN OPTIONAL,
+ freeFormatData-2 [16] FreeFormatData OPTIONAL,
+ freeFormatDataAppend-2 [17] BOOLEAN OPTIONAL
+}
+
+CAMELSMSInformation ::= SET
+{
+ gsm-SCFAddress [1] Gsm-SCFAddress OPTIONAL,
+ serviceKey [2] ServiceKey OPTIONAL,
+ defaultSMSHandling [3] DefaultSMS-Handling OPTIONAL,
+ freeFormatData [4] FreeFormatData OPTIONAL,
+ callingPartyNumber [5] CallingNumber OPTIONAL,
+ destinationSubscriberNumber [6] CalledNumber OPTIONAL,
+ cAMELSMSCAddress [7] AddressString OPTIONAL,
+ smsReferenceNumber [8] CallReferenceNumber OPTIONAL
+}
+
+CAMELInitCFIndicator ::= ENUMERATED
+{
+ noCAMELCallForwarding (0),
+ cAMELCallForwarding (1)
+}
+
+CAMELModificationParameters ::= SET
+ --
+ -- The list contains only parameters changed due to CAMEL call
+ -- handling.
+ --
+{
+ callingPartyNumber [0] CallingNumber OPTIONAL,
+ callingPartyCategory [1] CallingPartyCategory OPTIONAL,
+ originalCalledPartyNumber [2] OriginalCalledNumber OPTIONAL,
+ genericNumbers [3] GenericNumbers OPTIONAL,
+ redirectingPartyNumber [4] RedirectingNumber OPTIONAL,
+ redirectionCounter [5] NumberOfForwarding OPTIONAL
+}
+
+
+Category ::= OCTET STRING -- (SIZE(1))
+ --
+ -- The internal structure is defined in ITU-T Rec Q.763.
+ --see subscribe category
+
+CauseForTerm ::= ENUMERATED -- INTEGER
+ --
+ -- Cause codes from 16 up to 31 are defined in TS 32.015 as 'CauseForRecClosing'
+ -- (cause for record closing).
+ -- There is no direct correlation between these two types.
+ -- LCS related causes belong to the MAP error causes acc. TS 29.002.
+ --
+{
+ normalRelease (0),
+ partialRecord (1),
+ partialRecordCallReestablishment (2),
+ unsuccessfulCallAttempt (3),
+ stableCallAbnormalTermination (4),
+ cAMELInitCallRelease (5),
+ unauthorizedRequestingNetwork (52),
+ unauthorizedLCSClient (53),
+ positionMethodFailure (54),
+ unknownOrUnreachableLCSClient (58)
+}
+
+CellId ::= OCTET STRING -- (SIZE(2))
+ --
+ -- Coded according to TS 24.008
+ --
+
+ChangedParameters ::= SET
+{
+ changeFlags [0] ChangeFlags,
+ changeList [1] CAMELModificationParameters OPTIONAL
+}
+
+ChangeFlags ::= BIT STRING
+-- {
+-- callingPartyNumberModified (0),
+-- callingPartyCategoryModified (1),
+-- originalCalledPartyNumberModified (2),
+-- genericNumbersModified (3),
+-- redirectingPartyNumberModified (4),
+-- redirectionCounterModified (5)
+-- }
+
+ChangeOfClassmark ::= SEQUENCE
+{
+ classmark [0] Classmark,
+ changeTime [1] TimeStamp
+}
+
+ChangeOfRadioChannel ::= SEQUENCE
+{
+ radioChannel [0] TrafficChannel,
+ changeTime [1] TimeStamp,
+ speechVersionUsed [2] SpeechVersionIdentifier OPTIONAL
+}
+
+ChangeOfService ::= SEQUENCE
+{
+ basicService [0] BasicServiceCode,
+ transparencyInd [1] TransparencyInd OPTIONAL,
+ changeTime [2] TimeStamp,
+ rateIndication [3] RateIndication OPTIONAL,
+ fnur [4] Fnur OPTIONAL
+}
+
+ChannelCoding ::= ENUMERATED
+{
+ tchF4800 (1),
+ tchF9600 (2),
+ tchF14400 (3)
+}
+
+ChargeIndicator ::= ENUMERATED -- INTEGER
+{
+ noIndication (0),
+ noCharge (1),
+ charge (2)
+}
+
+Classmark ::= OCTET STRING
+ --
+ -- See Mobile station classmark 2 or 3 TS 24.008
+ --
+
+ConnectedNumber ::= BCDDirectoryNumber
+
+DataVolume ::= INTEGER
+ --
+ -- The volume of data transferred in segments of 64 octets.
+ --
+
+Day ::= INTEGER -- (1..31)
+
+--DayClass ::= ObjectInstance
+
+--DayClasses ::= SET OF DayClass
+
+--DayDefinition ::= SEQUENCE
+--{
+-- day [0] DayOfTheWeek,
+-- dayClass [1] ObjectInstance
+--}
+
+--DayDefinitions ::= SET OF DayDefinition
+
+--DateDefinition ::= SEQUENCE
+--{
+-- month [0] Month,
+-- day [1] Day,
+-- dayClass [2] ObjectInstance
+--}
+
+--DateDefinitions ::= SET OF DateDefinition
+
+--DayOfTheWeek ::= ENUMERATED
+--{
+-- allDays (0),
+-- sunday (1),
+-- monday (2),
+-- tuesday (3),
+-- wednesday (4),
+-- thursday (5),
+-- friday (6),
+-- saturday (7)
+--}
+
+DestinationRoutingAddress ::= BCDDirectoryNumber
+
+DefaultCallHandling ::= ENUMERATED
+{
+ continueCall (0),
+ releaseCall (1)
+}
+ -- exception handling:
+ -- reception of values in range 2-31 shall be treated as "continueCall"
+ -- reception of values greater than 31 shall be treated as "releaseCall"
+
+DeferredLocationEventType ::= BIT STRING
+-- {
+-- msAvailable (0)
+-- } (SIZE (1..16))
+
+ -- exception handling
+ -- a ProvideSubscriberLocation-Arg containing other values than listed above in
+ -- DeferredLocationEventType shall be rejected by the receiver with a return error cause of
+ -- unexpected data value.
+
+Diagnostics ::= CHOICE
+{
+ gsm0408Cause [0] INTEGER,
+ -- See TS 24.008
+ gsm0902MapErrorValue [1] INTEGER,
+ -- Note: The value to be stored here corresponds to
+ -- the local values defined in the MAP-Errors and
+ -- MAP-DialogueInformation modules, for full details
+ -- see TS 29.002.
+ ccittQ767Cause [2] INTEGER,
+ -- See ITU-T Q.767
+ networkSpecificCause [3] ManagementExtension,
+ -- To be defined by network operator
+ manufacturerSpecificCause [4] ManagementExtension
+ -- To be defined by manufacturer
+}
+
+DefaultSMS-Handling ::= ENUMERATED
+{
+ continueTransaction (0) ,
+ releaseTransaction (1)
+}
+-- exception handling:
+-- reception of values in range 2-31 shall be treated as "continueTransaction"
+-- reception of values greater than 31 shall be treated as "releaseTransaction"
+
+--Destinations ::= SET OF AE-title
+
+EmergencyCallIndEnable ::= BOOLEAN
+
+EmergencyCallIndication ::= SEQUENCE
+{
+ cellId [0] CellId,
+ callerId [1] IMSIorIMEI
+}
+
+EParameter ::= INTEGER -- (0..1023)
+ --
+ -- Coded according to TS 22.024 and TS 24.080
+ --
+
+EquipmentId ::= INTEGER
+
+Ext-GeographicalInformation ::= OCTET STRING -- (SIZE (1..maxExt-GeographicalInformation))
+ -- Refers to geographical Information defined in 3G TS 23.032.
+ -- This is composed of 1 or more octets with an internal structure according to
+ -- 3G TS 23.032
+ -- Octet 1: Type of shape, only the following shapes in 3G TS 23.032 are allowed:
+ -- (a) Ellipsoid point with uncertainty circle
+ -- (b) Ellipsoid point with uncertainty ellipse
+ -- (c) Ellipsoid point with altitude and uncertainty ellipsoid
+ -- (d) Ellipsoid Arc
+ -- (e) Ellipsoid Point
+ -- Any other value in octet 1 shall be treated as invalid
+ -- Octets 2 to 8 for case (a) - Ellipsoid point with uncertainty circle
+ -- Degrees of Latitude 3 octets
+ -- Degrees of Longitude 3 octets
+ -- Uncertainty code 1 octet
+ -- Octets 2 to 11 for case (b) - Ellipsoid point with uncertainty ellipse:
+ -- Degrees of Latitude 3 octets
+ -- Degrees of Longitude 3 octets
+ -- Uncertainty semi-major axis 1 octet
+ -- Uncertainty semi-minor axis 1 octet
+ -- Angle of major axis 1 octet
+ -- Confidence 1 octet
+ -- Octets 2 to 14 for case (c) - Ellipsoid point with altitude and uncertainty ellipsoid
+ -- Degrees of Latitude 3 octets
+ -- Degrees of Longitude 3 octets
+ -- Altitude 2 octets
+ -- Uncertainty semi-major axis 1 octet
+ -- Uncertainty semi-minor axis 1 octet
+ -- Angle of major axis 1 octet
+ -- Uncertainty altitude 1 octet
+ -- Confidence 1 octet
+ -- Octets 2 to 13 for case (d) - Ellipsoid Arc
+ -- Degrees of Latitude 3 octets
+ -- Degrees of Longitude 3 octets
+ -- Inner radius 2 octets
+ -- Uncertainty radius 1 octet
+ -- Offset angle 1 octet
+ -- Included angle 1 octet
+ -- Confidence 1 octet
+ -- Octets 2 to 7 for case (e) - Ellipsoid Point
+ -- Degrees of Latitude 3 octets
+ -- Degrees of Longitude 3 octets
+ --
+ -- An Ext-GeographicalInformation parameter comprising more than one octet and
+ -- containing any other shape or an incorrect number of octets or coding according
+ -- to 3G TS 23.032 shall be treated as invalid data by a receiver.
+ --
+ -- An Ext-GeographicalInformation parameter comprising one octet shall be discarded
+ -- by the receiver if an Add-GeographicalInformation parameter is received
+ -- in the same message.
+ --
+ -- An Ext-GeographicalInformation parameter comprising one octet shall be treated as
+ -- invalid data by the receiver if an Add-GeographicalInformation parameter is not
+ -- received in the same message.
+
+-- maxExt-GeographicalInformation INTEGER ::= 20
+ -- the maximum length allows for further shapes in 3G TS 23.032 to be included in later
+ -- versions of 3G TS 29.002
+
+EquipmentType ::= ENUMERATED -- INTEGER
+{
+ conferenceBridge (0)
+}
+
+FileType ::= ENUMERATED -- INTEGER
+{
+ callRecords (1),
+ traceRecords (9),
+ observedIMEITicket (14)
+}
+
+Fnur ::= ENUMERATED
+{
+ --
+ -- See Bearer Capability TS 24.008
+ --
+ fnurNotApplicable (0),
+ fnur9600-BitsPerSecond (1),
+ fnur14400BitsPerSecond (2),
+ fnur19200BitsPerSecond (3),
+ fnur28800BitsPerSecond (4),
+ fnur38400BitsPerSecond (5),
+ fnur48000BitsPerSecond (6),
+ fnur56000BitsPerSecond (7),
+ fnur64000BitsPerSecond (8),
+ fnur33600BitsPerSecond (9),
+ fnur32000BitsPerSecond (10),
+ fnur31200BitsPerSecond (11)
+}
+
+ForwardToNumber ::= AddressString
+
+FreeFormatData ::= OCTET STRING -- (SIZE(1..160))
+ --
+ -- Free formated data as sent in the FCI message
+ -- See TS 29.078
+ --
+
+GenericNumber ::= BCDDirectoryNumber
+
+GenericNumbers ::= SET OF GenericNumber
+
+Gsm-SCFAddress ::= ISDNAddressString
+ --
+ -- See TS 29.002
+ --
+
+HLRIntResult ::= Diagnostics
+
+Horizontal-Accuracy ::= OCTET STRING -- (SIZE (1))
+ -- bit 8 = 0
+ -- bits 7-1 = 7 bit Uncertainty Code defined in 3G TS 23.032. The horizontal location
+ -- error should be less than the error indicated by the uncertainty code with 67%
+ -- confidence.
+
+HotBillingTag ::= ENUMERATED --INTEGER
+{
+ noHotBilling (0),
+ hotBilling (1)
+}
+
+HSCSDParmsChange ::= SEQUENCE
+{
+ changeTime [0] TimeStamp,
+ hSCSDChanAllocated [1] NumOfHSCSDChanAllocated,
+ initiatingParty [2] InitiatingParty OPTIONAL,
+ aiurRequested [3] AiurRequested OPTIONAL,
+ chanCodingUsed [4] ChannelCoding,
+ hSCSDChanRequested [5] NumOfHSCSDChanRequested OPTIONAL
+}
+
+
+IMEI ::= TBCD-STRING -- (SIZE (8))
+ -- Refers to International Mobile Station Equipment Identity
+ -- and Software Version Number (SVN) defined in TS GSM 03.03.
+ -- If the SVN is not present the last octet shall contain the
+ -- digit 0 and a filler.
+ -- If present the SVN shall be included in the last octet.
+
+IMSI ::= TBCD-STRING -- (SIZE (3..8))
+ -- digits of MCC, MNC, MSIN are concatenated in this order.
+
+IMEICheckEvent ::= ENUMERATED -- INTEGER
+{
+ mobileOriginatedCall (0),
+ mobileTerminatedCall (1),
+ smsMobileOriginating (2),
+ smsMobileTerminating (3),
+ ssAction (4),
+ locationUpdate (5)
+}
+
+IMEIStatus ::= ENUMERATED
+{
+ greyListedMobileEquipment (0),
+ blackListedMobileEquipment (1),
+ nonWhiteListedMobileEquipment (2)
+}
+
+IMSIorIMEI ::= CHOICE
+{
+ imsi [0] IMSI,
+ imei [1] IMEI
+}
+
+InitiatingParty ::= ENUMERATED
+{
+ network (0),
+ subscriber (1)
+}
+
+ISDN-AddressString ::= AddressString -- (SIZE (1..maxISDN-AddressLength))
+ -- This type is used to represent ISDN numbers.
+
+-- maxISDN-AddressLength INTEGER ::= 9
+
+LCSCause ::= OCTET STRING -- (SIZE(1))
+ --
+ -- See LCS Cause Value, 3GPP TS 49.031
+ --
+
+LCS-Priority ::= OCTET STRING -- (SIZE (1))
+ -- 0 = highest priority
+ -- 1 = normal priority
+ -- all other values treated as 1
+
+LCSClientIdentity ::= SEQUENCE
+{
+ lcsClientExternalID [0] LCSClientExternalID OPTIONAL,
+ lcsClientDialedByMS [1] AddressString OPTIONAL,
+ lcsClientInternalID [2] LCSClientInternalID OPTIONAL
+}
+
+LCSClientExternalID ::= SEQUENCE
+{
+ externalAddress [0] AddressString OPTIONAL
+-- extensionContainer [1] ExtensionContainer OPTIONAL
+}
+
+LCSClientInternalID ::= ENUMERATED
+{
+ broadcastService (0),
+ o-andM-HPLMN (1),
+ o-andM-VPLMN (2),
+ anonymousLocation (3),
+ targetMSsubscribedService (4)
+}
+ -- for a CAMEL phase 3 PLMN operator client, the value targetMSsubscribedService shall be used
+
+LCSClientType ::= ENUMERATED
+{
+ emergencyServices (0),
+ valueAddedServices (1),
+ plmnOperatorServices (2),
+ lawfulInterceptServices (3)
+}
+ -- exception handling:
+ -- unrecognized values may be ignored if the LCS client uses the privacy override
+ -- otherwise, an unrecognized value shall be treated as unexpected data by a receiver
+ -- a return error shall then be returned if received in a MAP invoke
+
+LCSQoSInfo ::= SEQUENCE
+{
+ horizontal-accuracy [0] Horizontal-Accuracy OPTIONAL,
+ verticalCoordinateRequest [1] NULL OPTIONAL,
+ vertical-accuracy [2] Vertical-Accuracy OPTIONAL,
+ responseTime [3] ResponseTime OPTIONAL
+}
+
+LevelOfCAMELService ::= BIT STRING
+-- {
+-- basic (0),
+-- callDurationSupervision (1),
+-- onlineCharging (2)
+-- }
+
+LocationAreaAndCell ::= SEQUENCE
+{
+ locationAreaCode [0] LocationAreaCode,
+ cellIdentifier [1] CellId
+--
+-- For 2G the content of the Cell Identifier is defined by the Cell Id
+-- refer TS 24.008 and for 3G by the Service Area Code refer TS 25.413.
+--
+
+}
+
+LocationAreaCode ::= OCTET STRING -- (SIZE(2))
+ --
+ -- See TS 24.008
+ --
+
+LocationChange ::= SEQUENCE
+{
+ location [0] LocationAreaAndCell,
+ changeTime [1] TimeStamp
+}
+
+Location-info ::= SEQUENCE
+{
+ mscNumber [1] MscNo OPTIONAL,
+ location-area [2] LocationAreaCode,
+ cell-identification [3] CellId OPTIONAL
+}
+
+LocationType ::= SEQUENCE
+{
+locationEstimateType [0] LocationEstimateType,
+ deferredLocationEventType [1] DeferredLocationEventType OPTIONAL
+}
+
+LocationEstimateType ::= ENUMERATED
+{
+ currentLocation (0),
+ currentOrLastKnownLocation (1),
+ initialLocation (2),
+ activateDeferredLocation (3),
+ cancelDeferredLocation (4)
+}
+ -- exception handling:
+ -- a ProvideSubscriberLocation-Arg containing an unrecognized LocationEstimateType
+ -- shall be rejected by the receiver with a return error cause of unexpected data value
+
+LocUpdResult ::= Diagnostics
+
+ManagementExtensions ::= SET OF ManagementExtension
+
+ManagementExtension ::= SEQUENCE
+{
+ identifier OBJECT IDENTIFIER,
+ significance [1] BOOLEAN , -- DEFAULT FALSE,
+ information [2] OCTET STRING
+}
+
+
+MCCMNC ::= OCTET STRING -- (SIZE(3))
+ --
+ -- This type contains the mobile country code (MCC) and the mobile
+ -- network code (MNC) of a PLMN.
+ --
+
+RateIndication ::= OCTET STRING -- (SIZE(1))
+
+--0 no rate adaption
+--1 V.110, I.460/X.30
+--2 ITU-T X.31 flag stuffing
+--3 V.120
+--7 H.223 & H.245
+--11 PIAFS
+
+
+MessageReference ::= OCTET STRING
+
+Month ::= INTEGER -- (1..12)
+
+MOLR-Type ::= INTEGER
+--0 locationEstimate
+--1 assistanceData
+--2 deCipheringKeys
+
+MSCAddress ::= AddressString
+
+MscNo ::= ISDN-AddressString
+ --
+ -- See TS 23.003
+ --
+
+MSISDN ::= ISDN-AddressString
+ --
+ -- See TS 23.003
+ --
+
+MSPowerClasses ::= SET OF RFPowerCapability
+
+NetworkCallReference ::= CallReferenceNumber
+ -- See TS 29.002
+ --
+
+NetworkSpecificCode ::= INTEGER
+ --
+ -- To be defined by network operator
+ --
+
+NetworkSpecificServices ::= SET OF NetworkSpecificCode
+
+NotificationToMSUser ::= ENUMERATED
+{
+ notifyLocationAllowed (0),
+ notifyAndVerify-LocationAllowedIfNoResponse (1),
+ notifyAndVerify-LocationNotAllowedIfNoResponse (2),
+ locationNotAllowed (3)
+}
+ -- exception handling:
+ -- At reception of any other value than the ones listed the receiver shall ignore
+ -- NotificationToMSUser.
+
+NumberOfForwarding ::= INTEGER -- (1..5)
+
+NumOfHSCSDChanRequested ::= INTEGER
+
+NumOfHSCSDChanAllocated ::= INTEGER
+
+ObservedIMEITicketEnable ::= BOOLEAN
+
+OriginalCalledNumber ::= BCDDirectoryNumber
+
+OriginDestCombinations ::= SET OF OriginDestCombination
+
+OriginDestCombination ::= SEQUENCE
+{
+ origin [0] INTEGER OPTIONAL,
+ destination [1] INTEGER OPTIONAL
+ --
+ -- Note that these values correspond to the contents
+ -- of the attributes originId and destinationId
+ -- respectively. At least one of the two must be present.
+ --
+}
+
+PartialRecordTimer ::= INTEGER
+
+PartialRecordType ::= ENUMERATED
+{
+ timeLimit (0),
+ serviceChange (1),
+ locationChange (2),
+ classmarkChange (3),
+ aocParmChange (4),
+ radioChannelChange (5),
+ hSCSDParmChange (6),
+ changeOfCAMELDestination (7),
+ firstHotBill (20),
+ severalSSOperationBill (21)
+}
+
+PartialRecordTypes ::= SET OF PartialRecordType
+
+PositioningData ::= OCTET STRING -- (SIZE(1..33))
+ --
+ -- See Positioning Data IE (octet 3..n), 3GPP TS 49.031
+ --
+
+RadioChannelsRequested ::= SET OF RadioChanRequested
+
+RadioChanRequested ::= ENUMERATED
+{
+ --
+ -- See Bearer Capability TS 24.008
+ --
+ halfRateChannel (0),
+ fullRateChannel (1),
+ dualHalfRatePreferred (2),
+ dualFullRatePreferred (3)
+}
+
+--RecordClassDestination ::= CHOICE
+--{
+-- osApplication [0] AE-title,
+-- fileType [1] FileType
+--}
+
+--RecordClassDestinations ::= SET OF RecordClassDestination
+
+RecordingEntity ::= AddressString
+
+RecordingMethod ::= ENUMERATED
+{
+ inCallRecord (0),
+ inSSRecord (1)
+}
+
+RedirectingNumber ::= BCDDirectoryNumber
+
+RedirectingCounter ::= INTEGER
+
+ResponseTime ::= SEQUENCE
+{
+ responseTimeCategory ResponseTimeCategory
+}
+ -- note: an expandable SEQUENCE simplifies later addition of a numeric response time.
+
+ResponseTimeCategory ::= ENUMERATED
+{
+ lowdelay (0),
+ delaytolerant (1)
+}
+ -- exception handling:
+ -- an unrecognized value shall be treated the same as value 1 (delaytolerant)
+
+RFPowerCapability ::= INTEGER
+ --
+ -- This field contains the RF power capability of the Mobile station
+ -- classmark 1 and 2 of TS 24.008 expressed as an integer.
+ --
+
+RoamingNumber ::= ISDN-AddressString
+ --
+ -- See TS 23.003
+ --
+
+RoutingNumber ::= CHOICE
+{
+ roaming [1] RoamingNumber,
+ forwarded [2] ForwardToNumber
+}
+
+Service ::= CHOICE
+{
+ teleservice [1] TeleserviceCode,
+ bearerService [2] BearerServiceCode,
+ supplementaryService [3] SS-Code,
+ networkSpecificService [4] NetworkSpecificCode
+}
+
+ServiceDistanceDependencies ::= SET OF ServiceDistanceDependency
+
+ServiceDistanceDependency ::= SEQUENCE
+{
+ aocService [0] INTEGER,
+ chargingZone [1] INTEGER OPTIONAL
+ --
+ -- Note that these values correspond to the contents
+ -- of the attributes aocServiceId and zoneId
+ -- respectively.
+ --
+}
+
+ServiceKey ::= INTEGER -- (0..2147483647)
+
+SimpleIntegerName ::= INTEGER
+
+SimpleStringName ::= GraphicString
+
+SMSResult ::= Diagnostics
+
+SmsTpDestinationNumber ::= OCTET STRING
+ --
+ -- This type contains the binary coded decimal representation of
+ -- the SMS address field the encoding of the octet string is in
+ -- accordance with the definition of address fields in TS 23.040.
+ -- This encoding includes type of number and numbering plan indication
+ -- together with the address value range.
+ --
+
+SpeechVersionIdentifier ::= OCTET STRING -- (SIZE(1))
+-- see GSM 08.08
+
+-- 000 0001 GSM speech full rate version 1
+-- 001 0001 GSM speech full rate version 2 used for enhanced full rate
+-- 010 0001 GSM speech full rate version 3 for future use
+-- 000 0101 GSM speech half rate version 1
+-- 001 0101 GSM speech half rate version 2 for future use
+-- 010 0101 GSM speech half rate version 3 for future use
+
+SSActionResult ::= Diagnostics
+
+SSActionType ::= ENUMERATED
+{
+ registration (0),
+ erasure (1),
+ activation (2),
+ deactivation (3),
+ interrogation (4),
+ invocation (5),
+ passwordRegistration (6),
+ ussdInvocation (7)
+}
+
+-- ussdInvocation (7) include ussd phase 1,phase 2
+
+--SS Request = SSActionType
+
+SS-Code ::= OCTET STRING -- (SIZE (1))
+ -- This type is used to represent the code identifying a single
+ -- supplementary service, a group of supplementary services, or
+ -- all supplementary services. The services and abbreviations
+ -- used are defined in TS 3GPP TS 22.004 [5]. The internal structure is
+ -- defined as follows:
+ --
+ -- bits 87654321: group (bits 8765), and specific service
+ -- (bits 4321) ussd = ff
+
+-- allSS (0x00),
+-- reserved for possible future use
+-- all SS
+--
+-- allLineIdentificationSS (0x10),
+-- reserved for possible future use
+-- all line identification SS
+--
+-- calling-line-identification-presentation (0x11),
+-- calling line identification presentation
+-- calling-line-identification-restriction (0x12),
+-- calling line identification restriction
+-- connected-line-identification-presentation (0x13),
+-- connected line identification presentation
+-- connected-line-identification-restriction (0x14),
+-- connected line identification restriction
+-- malicious-call-identification (0x15),
+-- reserved for possible future use
+-- malicious call identification
+--
+-- allNameIdentificationSS (0x18),
+-- all name identification SS
+-- calling-name-presentation (0x19),
+-- calling name presentation
+--
+-- SS-Codes '00011010'B, to '00011111'B, are reserved for future
+-- NameIdentification Supplementary Service use.
+--
+-- allForwardingSS (0x20),
+-- all forwarding SS
+-- call-forwarding-unconditional (0x21),
+-- call forwarding unconditional
+-- call-deflection (0x24),
+-- call deflection
+-- allCondForwardingSS (0x28),
+-- all conditional forwarding SS
+-- call-forwarding-on-mobile-subscriber-busy (0x29),
+-- call forwarding on mobile subscriber busy
+-- call-forwarding-on-no-reply (0x2a),
+-- call forwarding on no reply
+-- call-forwarding-on-mobile-subscriber-not-reachable (0x2b),
+-- call forwarding on mobile subscriber not reachable
+--
+-- allCallOfferingSS (0x30),
+-- reserved for possible future use
+-- all call offering SS includes also all forwarding SS
+--
+-- explicit-call-transfer (0x31),
+-- explicit call transfer
+-- mobile-access-hunting (0x32),
+-- reserved for possible future use
+-- mobile access hunting
+--
+-- allCallCompletionSS (0x40),
+-- reserved for possible future use
+-- all Call completion SS
+--
+-- call-waiting (0x41),
+-- call waiting
+-- call-hold (0x42),
+-- call hold
+-- completion-of-call-to-busy-subscribers-originating-side (0x43),
+-- completion of call to busy subscribers, originating side
+-- completion-of-call-to-busy-subscribers-destination-side (0x44),
+-- completion of call to busy subscribers, destination side
+-- this SS-Code is used only in InsertSubscriberData and DeleteSubscriberData
+--
+-- multicall (0x45),
+-- multicall
+--
+-- allMultiPartySS (0x50),
+-- reserved for possible future use
+-- all multiparty SS
+--
+-- multiPTY (0x51),
+-- multiparty
+--
+-- allCommunityOfInterest-SS (0x60),
+-- reserved for possible future use
+-- all community of interest SS
+-- closed-user-group (0x61),
+-- closed user group
+--
+-- allChargingSS (0x70),
+-- reserved for possible future use
+-- all charging SS
+-- advice-of-charge-information (0x71),
+-- advice of charge information
+-- advice-of-charge-charging (0x72),
+-- advice of charge charging
+--
+-- allAdditionalInfoTransferSS (0x80),
+-- reserved for possible future use
+-- all additional information transfer SS
+-- uUS1-user-to-user-signalling (0x81),
+-- UUS1 user-to-user signalling
+-- uUS2-user-to-user-signalling (0x82),
+-- UUS2 user-to-user signalling
+-- uUS3-user-to-user-signalling (0x83),
+-- UUS3 user-to-user signalling
+--
+-- allBarringSS (0x90),
+-- all barring SS
+-- barringOfOutgoingCalls (0x91),
+-- barring of outgoing calls
+-- barring-of-all-outgoing-calls (0x92),
+-- barring of all outgoing calls
+-- barring-of-outgoing-international-calls (0x93),
+-- barring of outgoing international calls
+-- boicExHC (0x94),
+-- barring of outgoing international calls except those directed
+-- to the home PLMN
+-- barringOfIncomingCalls (0x99),
+-- barring of incoming calls
+-- barring-of-all-incoming-calls (0x9a),
+-- barring of all incoming calls
+-- barring-of-incoming-calls-when-roaming-outside-home-PLMN-Country (0x9b),
+-- barring of incoming calls when roaming outside home PLMN
+-- Country
+--
+-- allCallPrioritySS (0xa0),
+-- reserved for possible future use
+-- all call priority SS
+-- enhanced-Multilevel-Precedence-Pre-emption-EMLPP-service (0xa1),
+-- enhanced Multilevel Precedence Pre-emption 'EMLPP) service
+--
+-- allLCSPrivacyException (0xb0),
+-- all LCS Privacy Exception Classes
+-- universal (0xb1),
+-- allow location by any LCS client
+-- callrelated (0xb2),
+-- allow location by any value added LCS client to which a call
+-- is established from the target MS
+-- callunrelated (0xb3),
+-- allow location by designated external value added LCS clients
+-- plmnoperator (0xb4),
+-- allow location by designated PLMN operator LCS clients
+--
+-- allMOLR-SS (0xc0),
+-- all Mobile Originating Location Request Classes
+-- basicSelfLocation (0xc1),
+-- allow an MS to request its own location
+-- autonomousSelfLocation (0xc2),
+-- allow an MS to perform self location without interaction
+-- with the PLMN for a predetermined period of time
+-- transferToThirdParty (0xc3),
+-- allow an MS to request transfer of its location to another LCS client
+--
+-- allPLMN-specificSS (0xf0),
+-- plmn-specificSS-1 (0xf1),
+-- plmn-specificSS-2 (0xf2),
+-- plmn-specificSS-3 (0xf3),
+-- plmn-specificSS-4 (0xf4),
+-- plmn-specificSS-5 (0xf5),
+-- plmn-specificSS-6 (0xf6),
+-- plmn-specificSS-7 (0xf7),
+-- plmn-specificSS-8 (0xf8),
+-- plmn-specificSS-9 (0xf9),
+-- plmn-specificSS-A (0xfa),
+-- plmn-specificSS-B (0xfb),
+-- plmn-specificSS-C (0xfc),
+-- plmn-specificSS-D (0xfd),
+-- plmn-specificSS-E (0xfe),
+-- ussd (0xff)
+
+
+SSParameters ::= CHOICE
+{
+ forwardedToNumber [0] ForwardToNumber,
+ unstructuredData [1] OCTET STRING
+}
+
+SupplServices ::= SET OF SS-Code
+
+SuppServiceUsed ::= SEQUENCE
+{
+ ssCode [0] SS-Code OPTIONAL,
+ ssTime [1] TimeStamp OPTIONAL
+}
+
+SwitchoverTime ::= SEQUENCE
+{
+ hour INTEGER , -- (0..23),
+ minute INTEGER , -- (0..59),
+ second INTEGER -- (0..59)
+}
+
+SystemType ::= ENUMERATED
+ -- "unknown" is not to be used in PS domain.
+{
+ unknown (0),
+ iuUTRAN (1),
+ gERAN (2)
+}
+
+TBCD-STRING ::= OCTET STRING
+ -- This type (Telephony Binary Coded Decimal String) is used to
+ -- represent several digits from 0 through 9, *, #, a, b, c, two
+ -- digits per octet, each digit encoded 0000 to 1001 (0 to 9),
+ -- 1010 (*), 1011 (#), 1100 (a), 1101 (b) or 1110 (c); 1111 used
+ -- as filler when there is an odd number of digits.
+
+ -- bits 8765 of octet n encoding digit 2n
+ -- bits 4321 of octet n encoding digit 2(n-1) +1
+
+TariffId ::= INTEGER
+
+TariffPeriod ::= SEQUENCE
+{
+ switchoverTime [0] SwitchoverTime,
+ tariffId [1] INTEGER
+ -- Note that the value of tariffId corresponds
+ -- to the attribute tariffId.
+}
+
+TariffPeriods ::= SET OF TariffPeriod
+
+TariffSystemStatus ::= ENUMERATED
+{
+ available (0), -- available for modification
+ checked (1), -- "frozen" and checked
+ standby (2), -- "frozen" awaiting activation
+ active (3) -- "frozen" and active
+}
+
+
+TimeStamp ::= OCTET STRING -- (SIZE(9))
+ --
+ -- The contents of this field are a compact form of the UTCTime format
+ -- containing local time plus an offset to universal time. Binary coded
+ -- decimal encoding is employed for the digits to reduce the storage and
+ -- transmission overhead
+ -- e.g. YYMMDDhhmmssShhmm
+ -- where
+ -- YY = Year 00 to 99 BCD encoded
+ -- MM = Month 01 to 12 BCD encoded
+ -- DD = Day 01 to 31 BCD encoded
+ -- hh = hour 00 to 23 BCD encoded
+ -- mm = minute 00 to 59 BCD encoded
+ -- ss = second 00 to 59 BCD encoded
+ -- S = Sign 0 = "+", "-" ASCII encoded
+ -- hh = hour 00 to 23 BCD encoded
+ -- mm = minute 00 to 59 BCD encoded
+ --
+
+TrafficChannel ::= ENUMERATED
+{
+ fullRate (0),
+ halfRate (1)
+}
+
+TranslatedNumber ::= BCDDirectoryNumber
+
+TransparencyInd ::= ENUMERATED
+{
+ transparent (0),
+ nonTransparent (1)
+}
+
+ROUTE ::= CHOICE
+{
+ rOUTENumber [0] INTEGER,
+ rOUTEName [1] GraphicString
+}
+
+--rOUTEName 1 10 octet
+
+TSChangeover ::= SEQUENCE
+{
+ newActiveTS [0] INTEGER,
+ newStandbyTS [1] INTEGER,
+-- changeoverTime [2] GeneralizedTime OPTIONAL,
+ authkey [3] OCTET STRING OPTIONAL,
+ checksum [4] OCTET STRING OPTIONAL,
+ versionNumber [5] OCTET STRING OPTIONAL
+ -- Note that if the changeover time is not
+ -- specified then the change is immediate.
+}
+
+TSCheckError ::= SEQUENCE
+{
+ errorId [0] TSCheckErrorId
+ --fail [1] ANY DEFINED BY errorId OPTIONAL
+}
+
+TSCheckErrorId ::= CHOICE
+{
+ globalForm [0] OBJECT IDENTIFIER,
+ localForm [1] INTEGER
+}
+
+TSCheckResult ::= CHOICE
+{
+ success [0] NULL,
+ fail [1] SET OF TSCheckError
+}
+
+TSCopyTariffSystem ::= SEQUENCE
+{
+ oldTS [0] INTEGER,
+ newTS [1] INTEGER
+}
+
+TSNextChange ::= CHOICE
+{
+ noChangeover [0] NULL,
+ tsChangeover [1] TSChangeover
+}
+
+TypeOfSubscribers ::= ENUMERATED
+{
+ home (0), -- HPLMN subscribers
+ visiting (1), -- roaming subscribers
+ all (2)
+}
+
+TypeOfTransaction ::= ENUMERATED
+{
+ successful (0),
+ unsuccessful (1),
+ all (2)
+}
+
+Vertical-Accuracy ::= OCTET STRING -- (SIZE (1))
+ -- bit 8 = 0
+ -- bits 7-1 = 7 bit Vertical Uncertainty Code defined in 3G TS 23.032.
+ -- The vertical location error should be less than the error indicated
+ -- by the uncertainty code with 67% confidence.
+
+ISDNAddressString ::= AddressString
+
+EmlppPriority ::= OCTET STRING -- (SIZE (1))
+
+--priorityLevelA EMLPP-Priority ::= 6
+--priorityLevelB EMLPP-Priority ::= 5
+--priorityLevel0 EMLPP-Priority ::= 0
+--priorityLevel1 EMLPP-Priority ::= 1
+--priorityLevel2 EMLPP-Priority ::= 2
+--priorityLevel3 EMLPP-Priority ::= 3
+--priorityLevel4 EMLPP-Priority ::= 4
+--See 29.002
+
+
+EASubscriberInfo ::= OCTET STRING -- (SIZE (3))
+ -- The internal structure is defined by the Carrier Identification
+ -- parameter in ANSI T1.113.3. Carrier codes between "000" and "999" may
+ -- be encoded as 3 digits using "000" to "999" or as 4 digits using
+ -- "0000" to "0999". Carrier codes between "1000" and "9999" are encoded
+ -- using 4 digits.
+
+SelectedCIC ::= OCTET STRING -- (SIZE (3))
+
+PortedFlag ::= ENUMERATED
+{
+ numberNotPorted (0),
+ numberPorted (1)
+}
+
+SubscriberCategory ::= OCTET STRING -- (SIZE (1))
+-- unknownuser = 0x00,
+-- frenchuser = 0x01,
+-- englishuser = 0x02,
+-- germanuser = 0x03,
+-- russianuser = 0x04,
+-- spanishuser = 0x05,
+-- specialuser = 0x06,
+-- reserveuser = 0x09,
+-- commonuser = 0x0a,
+-- superioruser = 0x0b,
+-- datacalluser = 0x0c,
+-- testcalluser = 0x0d,
+-- spareuser = 0x0e,
+-- payphoneuser = 0x0f,
+-- coinuser = 0x20,
+-- isup224 = 0xe0
+
+
+CUGOutgoingAccessIndicator ::= ENUMERATED
+{
+ notCUGCall (0),
+ cUGCall (1)
+}
+
+CUGInterlockCode ::= OCTET STRING -- (SIZE (4))
+
+--
+
+CUGOutgoingAccessUsed ::= ENUMERATED
+{
+ callInTheSameCUGGroup (0),
+ callNotInTheSameCUGGroup (1)
+}
+
+SMSTEXT ::= OCTET STRING
+
+MSCCIC ::= INTEGER -- (0..65535)
+
+RNCorBSCId ::= OCTET STRING -- (SIZE (3))
+--octet order is the same as RANAP/BSSAP signaling
+--if spc is coded as 14bit, then OCTET STRING1 will filled with 00 ,for example rnc id = 123 will be coded as 00 01 23
+--OCTET STRING1
+--OCTET STRING2
+--OCTET STRING3
+
+MSCId ::= OCTET STRING -- (SIZE (3))
+--National network format , octet order is the same as ISUP signaling
+--if spc is coded as 14bit, then OCTET STRING1 will filled with 00,,for example rnc id = 123 will be coded as 00 01 23
+--OCTET STRING1
+--OCTET STRING2
+--OCTET STRING3
+
+EmergencyCallFlag ::= ENUMERATED
+{
+ notEmergencyCall (0),
+ emergencyCall (1)
+}
+
+CUGIncomingAccessUsed ::= ENUMERATED
+{
+ callInTheSameCUGGroup (0),
+ callNotInTheSameCUGGroup (1)
+}
+
+SmsUserDataType ::= OCTET STRING -- (SIZE (1))
+--
+--00 concatenated-short-messages-8-bit-reference-number
+--01 special-sms-message-indication
+--02 reserved
+--03 Value not used to avoid misinterpretation as
+--04 characterapplication-port-addressing-scheme-8-bit-address
+--05 application-port-addressing-scheme-16-bit-address
+--06 smsc-control-parameters
+--07 udh-source-indicator
+--08 concatenated-short-message-16-bit-reference-number
+--09 wireless-control-message-protocol
+--0A text-formatting
+--0B predefined-sound
+--0C user-defined-sound-imelody-max-128-bytes
+--0D predefined-animation
+--0E large-animation-16-16-times-4-32-4-128-bytes
+--0F small-animation-8-8-times-4-8-4-32-bytes
+--10 large-picture-32-32-128-bytes
+--11 small-picture-16-16-32-bytes
+--12 variable-picture
+--13 User prompt indicator
+--14 Extended Object
+--15 Reused Extended Object
+--16 Compression Control
+--17 Object Distribution Indicator
+--18 Standard WVG object
+--19 Character Size WVG object
+--1A Extended Object Data Request Command
+--1B-1F Reserved for future EMS features (see subclause 3.10)
+--20 RFC 822 E-Mail Header
+--21 Hyperlink format element
+--22 Reply Address Element
+--23 - 6F Reserved for future use
+--70 - 7F (U)SIM Toolkit Security Headers
+--80 - 9F SME to SME specific use
+--A0 - BF Reserved for future use
+--C0 - DF SC specific use
+--E0 - FE Reserved for future use
+--FF normal SMS
+
+ConcatenatedSMSReferenceNumber ::= INTEGER -- (0..65535)
+
+MaximumNumberOfSMSInTheConcatenatedSMS ::= INTEGER -- (0..255)
+
+SequenceNumberOfTheCurrentSMS ::= INTEGER -- (0..255)
+
+SequenceNumber ::= INTEGER
+
+--(1... )
+--
+
+DisconnectParty ::= ENUMERATED
+{
+ callingPartyRelease (0),
+ calledPartyRelease (1),
+ networkRelease (2)
+}
+
+ChargedParty ::= ENUMERATED
+{
+ callingParty (0),
+ calledParty (1)
+}
+
+ChargeAreaCode ::= OCTET STRING -- (SIZE (1..3))
+
+CUGIndex ::= OCTET STRING -- (SIZE (2))
+
+GuaranteedBitRate ::= ENUMERATED
+{
+ gBR14400BitsPerSecond (1), -- BS20 non-transparent
+ gBR28800BitsPerSecond (2), -- BS20 non-transparent and transparent,
+ -- BS30 transparent and multimedia
+ gBR32000BitsPerSecond (3), -- BS30 multimedia
+ gBR33600BitsPerSecond (4), -- BS30 multimedia
+ gBR56000BitsPerSecond (5), -- BS30 transparent and multimedia
+ gBR57600BitsPerSecond (6), -- BS20 non-transparent
+ gBR64000BitsPerSecond (7), -- BS30 transparent and multimedia
+
+ gBR12200BitsPerSecond (106), -- AMR speech
+ gBR10200BitsPerSecond (107), -- AMR speech
+ gBR7950BitsPerSecond (108), -- AMR speech
+ gBR7400BitsPerSecond (109), -- AMR speech
+ gBR6700BitsPerSecond (110), -- AMR speech
+ gBR5900BitsPerSecond (111), -- AMR speech
+ gBR5150BitsPerSecond (112), -- AMR speech
+ gBR4750BitsPerSecond (113) -- AMR speech
+}
+
+MaximumBitRate ::= ENUMERATED
+{
+ mBR14400BitsPerSecond (1), -- BS20 non-transparent
+ mBR28800BitsPerSecond (2), -- BS20 non-transparent and transparent,
+ -- BS30 transparent and multimedia
+ mBR32000BitsPerSecond (3), -- BS30 multimedia
+ mBR33600BitsPerSecond (4), -- BS30 multimedia
+ mBR56000BitsPerSecond (5), -- BS30 transparent and multimedia
+ mBR57600BitsPerSecond (6), -- BS20 non-transparent
+ mBR64000BitsPerSecond (7), -- BS30 transparent and multimedia
+
+ mBR12200BitsPerSecond (106), -- AMR speech
+ mBR10200BitsPerSecond (107), -- AMR speech
+ mBR7950BitsPerSecond (108), -- AMR speech
+ mBR7400BitsPerSecond (109), -- AMR speech
+ mBR6700BitsPerSecond (110), -- AMR speech
+ mBR5900BitsPerSecond (111), -- AMR speech
+ mBR5150BitsPerSecond (112), -- AMR speech
+ mBR4750BitsPerSecond (113) -- AMR speech
+}
+
+
+HLC ::= OCTET STRING
+
+-- this parameter is a 1:1 copy of the contents (i.e. starting with octet 3) of the "high layer compatibility" parameter of ITU-T Q.931 [35].
+
+LLC ::= OCTET STRING
+
+-- this parameter is a 1:1 copy of the contents (i.e. starting with octet 3) of the "low layer compatibility" parameter of ITU-T Q.931 [35].
+
+
+ISDN-BC ::= OCTET STRING
+
+-- this parameter is a 1:1 copy of the contents (i.e. starting with octet 3) of the "bearer capability" parameter of ITU-T Q.931 [35].
+
+ModemType ::= ENUMERATED
+{
+ none-modem (0),
+ modem-v21 (1),
+ modem-v22 (2),
+ modem-v22-bis (3),
+ modem-v23 (4),
+ modem-v26-ter (5),
+ modem-v32 (6),
+ modem-undef-interface (7),
+ modem-autobauding1 (8),
+ no-other-modem-type (31),
+ modem-v34 (33)
+}
+
+UssdCodingScheme ::= OCTET STRING
+
+UssdString ::= OCTET STRING
+
+UssdNotifyCounter ::= INTEGER -- (0..255)
+
+UssdRequestCounter ::= INTEGER -- (0..255)
+
+Classmark3 ::= OCTET STRING -- (SIZE(2))
+
+OptimalRoutingDestAddress ::= BCDDirectoryNumber
+
+GAI ::= OCTET STRING -- (SIZE(7))
+--such as 64 F0 00 00 ABCD 1234
+
+ChangeOfglobalAreaID ::= SEQUENCE
+{
+ location [0] GAI,
+ changeTime [1] TimeStamp
+}
+
+InteractionWithIP ::= NULL
+
+RouteAttribute ::= ENUMERATED
+{
+ cas (0),
+ tup (1),
+ isup (2),
+ pra (3),
+ bicc (4),
+ sip (5),
+ others (255)
+}
+
+VoiceIndicator ::= ENUMERATED
+{
+ sendToneByLocalMsc (0) ,
+ sendToneByOtherMsc (1),
+ voiceNoIndication (3)
+}
+
+BCategory ::= ENUMERATED
+{
+ subscriberFree (0),
+ subscriberBusy (1),
+ subscriberNoIndication (3)
+}
+
+CallType ::= ENUMERATED
+{
+ unknown (0),
+ internal (1),
+ incoming (2),
+ outgoing (3),
+ tandem (4)
+}
+
+-- END
+END
+}
+
+1;
+
diff --git a/FS/FS/contact_Mixin.pm b/FS/FS/contact_Mixin.pm
new file mode 100644
index 000000000..6e8f315b9
--- /dev/null
+++ b/FS/FS/contact_Mixin.pm
@@ -0,0 +1,19 @@
+package FS::contact_Mixin;
+
+use strict;
+use FS::Record qw( qsearchs );
+use FS::contact;
+
+=item contact_obj
+
+Returns the contact object, if any (see L).
+
+=cut
+
+sub contact_obj {
+ my $self = shift;
+ return '' unless $self->contactnum;
+ qsearchs( 'contact', { 'contactnum' => $self->contactnum } );
+}
+
+1;
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index 9bab4938c..fc6a7ddbe 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -110,9 +110,11 @@ Customer info at invoice generation time
=over 4
-=item previous_balance
+=item billing_balance - the customer's balance at the time the invoice was
+generated (not including charges on this invoice)
-=item billing_balance
+=item previous_balance - the billing_balance of this customer's previous
+invoice plus the charges on that invoice
=back
@@ -2144,6 +2146,7 @@ sub print_csv {
$self->custnum,
$cust_main->first,
$cust_main->last,
+ $cust_main->company,
$cust_main->address1,
$cust_main->address2,
$cust_main->city,
@@ -3137,11 +3140,16 @@ sub _items_payments {
#something more elaborate if $_->amount ne ->cust_pay->paid ?
+ my $desc = $self->mt('Payment received').' '.
+ time2str($date_format,$_->cust_pay->_date );
+ $desc .= $self->mt(' via ' . $_->cust_pay->payby_payinfo_pretty)
+ if ( $self->conf->exists('invoice_payment_details') );
+
push @b, {
- 'description' => $self->mt('Payment received').' '.
- time2str($date_format,$_->cust_pay->_date ),
+ 'description' => $desc,
'amount' => sprintf("%.2f", $_->amount )
};
+
}
@b;
diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm
index 716c0983e..0c8c0bbbf 100644
--- a/FS/FS/cust_bill_pkg.pm
+++ b/FS/FS/cust_bill_pkg.pm
@@ -1104,16 +1104,12 @@ sub upgrade_tax_location {
delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
$hash{custnum} = $h_cust_main->custnum;
- my $tax_loc = qsearchs('cust_location', \%hash) # unlikely
- || FS::cust_location->new({ %hash });
- if ( !$tax_loc->locationnum ) {
- $tax_loc->disabled('Y');
- my $error = $tax_loc->insert;
- if ( $error ) {
- warn "couldn't create historical location record for cust#".
- $h_cust_main->custnum.": $error\n";
- next INVOICE;
- }
+ my $tax_loc = FS::cust_location->new(\%hash);
+ my $error = $tax_loc->find_or_insert || $tax_loc->disable_if_unused;
+ if ( $error ) {
+ warn "couldn't create historical location record for cust#".
+ $h_cust_main->custnum.": $error\n";
+ next INVOICE;
}
my $exempt_cust = 1 if $h_cust_main->tax;
diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm
index b25163f36..4560716d5 100644
--- a/FS/FS/cust_location.pm
+++ b/FS/FS/cust_location.pm
@@ -5,7 +5,7 @@ use strict;
use vars qw( $import );
use Locale::Country;
use FS::UID qw( dbh driver_name );
-use FS::Record qw( qsearch ); #qsearchs );
+use FS::Record qw( qsearch qsearchs );
use FS::Conf;
use FS::prospect_main;
use FS::cust_main;
@@ -104,6 +104,95 @@ points to. You can ask the object for a copy with the I method.
sub table { 'cust_location'; }
+=item find_or_insert
+
+Finds an existing location matching the customer and address values in this
+location, if one exists, and sets the contents of this location equal to that
+one (including its locationnum).
+
+If an existing location is not found, this one I be inserted. (This is a
+change from the "new_or_existing" method that this replaces.)
+
+The following fields are considered "essential" and I match: custnum,
+address1, address2, city, county, state, zip, country, location_number,
+location_type, location_kind. Disabled locations will be found only if this
+location is set to disabled.
+
+If 'coord_auto' is null, and latitude and longitude are not null, then
+latitude and longitude are also essential fields.
+
+All other fields are considered "non-essential". If a non-essential field is
+empty in this location, it will be ignored in determining whether an existing
+location matches.
+
+If a non-essential field is non-empty in this location, existing locations
+that contain a different non-empty value for that field will not match. An
+existing location in which the field is I will match, but will be
+updated in-place with the value of that field.
+
+Returns an error string if inserting or updating a location failed.
+
+It is unfortunately hard to determine if this created a new location or not.
+
+=cut
+
+sub find_or_insert {
+ my $self = shift;
+
+ my @essential = (qw(custnum address1 address2 city county state zip country
+ location_number location_type location_kind disabled));
+
+ if ( !$self->coord_auto and $self->latitude and $self->longitude ) {
+ push @essential, qw(latitude longitude);
+ # but NOT coord_auto; if the latitude and longitude match the geocoded
+ # values then that's good enough
+ }
+
+ # put nonempty, nonessential fields/values into this hash
+ my %nonempty = map { $_ => $self->get($_) }
+ grep {$self->get($_)} $self->fields;
+ delete @nonempty{@essential};
+ delete $nonempty{'locationnum'};
+
+ my %hash = map { $_ => $self->get($_) } @essential;
+ my @matches = qsearch('cust_location', \%hash);
+
+ # consider candidate locations
+ MATCH: foreach my $old (@matches) {
+ my $reject = 0;
+ foreach my $field (keys %nonempty) {
+ my $old_value = $old->get($field);
+ if ( length($old_value) > 0 ) {
+ if ( $field eq 'latitude' or $field eq 'longitude' ) {
+ # special case, because these are decimals
+ if ( abs($old_value - $nonempty{$field}) > 0.000001 ) {
+ $reject = 1;
+ }
+ } elsif ( $old_value ne $nonempty{$field} ) {
+ $reject = 1;
+ }
+ } else {
+ # it's empty in $old, has a value in $self
+ $old->set($field, $nonempty{$field});
+ }
+ next MATCH if $reject;
+ } # foreach $field
+
+ if ( $old->modified ) {
+ my $error = $old->replace;
+ return $error if $error;
+ }
+ # set $self equal to $old
+ foreach ($self->fields) {
+ $self->set($_, $old->get($_));
+ }
+ return "";
+ }
+
+ # didn't find a match
+ return $self->insert;
+}
+
=item insert
Adds this record to the database. If there is an error, returns the error,
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 3c0702fc1..1d6e84588 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -390,7 +390,7 @@ sub insert {
$payby = 'PREP' if $amount;
- } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
+ } elsif ( $self->payby =~ /^(CASH|WEST|MCRD|PPAL)$/ ) {
$payby = $1;
$self->payby('BILL');
@@ -1509,43 +1509,17 @@ sub replace {
my $old_loc = $old->$l;
my $new_loc = $self->$l;
- if ( !$new_loc->locationnum ) {
- # changing location
- # If the new location is all empty fields, or if it's identical to
- # the old location in all fields, don't replace.
- my @nonempty = grep { $new_loc->$_ } $self->location_fields;
- next if !@nonempty;
- my @unlike = grep { $new_loc->$_ ne $old_loc->$_ } $self->location_fields;
-
- if ( @unlike or $old_loc->disabled ) {
- warn " changed $l fields: ".join(',',@unlike)."\n"
- if $DEBUG;
- $new_loc->set(custnum => $self->custnum);
-
- # insert it--the old location will be disabled later
- my $error = $new_loc->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
-
- } else {
- # no fields have changed and $old_loc isn't disabled, so don't change it
- next;
- }
-
- }
- elsif ( $new_loc->custnum ne $self->custnum or $new_loc->prospectnum ) {
+ # find the existing location if there is one
+ $new_loc->set('custnum' => $self->custnum);
+ my $error = $new_loc->find_or_insert;
+ if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "$l belongs to customer ".$new_loc->custnum;
+ return $error;
}
- # else the new location belongs to this customer so we're good
-
- # set the foo_locationnum now that we have one.
$self->set($l.'num', $new_loc->locationnum);
-
} #for $l
+ # replace the customer record
my $error = $self->SUPER::replace($old);
if ( $error ) {
@@ -2021,7 +1995,8 @@ sub check {
if ( $self->paydate eq '' || $self->paydate eq '-' ) {
return "Expiration date required"
- unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
+ # shouldn't payinfo_check do this?
+ unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD|PPAL)$/;
$self->paydate('');
} else {
my( $m, $y );
diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm
index 939a625c7..814802b34 100644
--- a/FS/FS/cust_main/Billing.pm
+++ b/FS/FS/cust_main/Billing.pm
@@ -437,6 +437,24 @@ sub bill {
my @part_pkg = $cust_pkg->part_pkg->self_and_bill_linked;
$options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
+ # if this package was changed from another package,
+ # and it hasn't been billed since then,
+ # and package balances are enabled,
+ if ( $cust_pkg->change_pkgnum
+ and $cust_pkg->change_date >= ($cust_pkg->last_bill || 0)
+ and $cust_pkg->change_date < $invoice_time
+ and $conf->exists('pkg-balances') )
+ {
+ # _transfer_balance will also create the appropriate credit
+ my @transfer_items = $self->_transfer_balance($cust_pkg);
+ # $part_pkg[0] is the "real" part_pkg
+ my $pass = ($cust_pkg->no_auto || $part_pkg[0]->no_auto) ?
+ 'no_auto' : '';
+ push @{ $cust_bill_pkg{$pass} }, @transfer_items;
+ # treating this as recur, just because most charges are recur...
+ ${$total_recur{$pass}} += $_->recur foreach @transfer_items;
+ }
+
foreach my $part_pkg ( @part_pkg ) {
$cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
@@ -1220,24 +1238,107 @@ sub _make_lines {
}
-# This is _handle_taxes. It's called once for each cust_bill_pkg generated
-# from _make_lines, along with the part_pkg, cust_pkg, invoice time, the
-# non-overridden pkgpart, a flag indicating whether the package is being
-# canceled, and a partridge in a pear tree.
-#
-# The most important argument is 'taxlisthash'. This is shared across the
-# entire invoice. It looks like this:
-# {
-# 'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
-# 'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
-# }
-#
-# 'cust_main_county' can also be 'tax_rate'. The first object in the array
-# is always the cust_main_county or tax_rate identified by the key.
-#
-# That "..." is a list of FS::cust_bill_pkg objects that will be fed to
-# the 'taxline' method to calculate the amount of the tax. This doesn't
-# happen until calculate_taxes, though.
+=item _transfer_balance TO_PKG [ FROM_PKGNUM ]
+
+Takes one argument, a cust_pkg object that is being billed. This will
+be called only if the package was created by a package change, and has
+not been billed since the package change, and package balance tracking
+is enabled. The second argument can be an alternate package number to
+transfer the balance from; this should not be used externally.
+
+Transfers the balance from the previous package (now canceled) to
+this package, by crediting one package and creating an invoice item for
+the other. Inserts the credit and returns the invoice item (so that it
+can be added to an invoice that's being built).
+
+If the previous package was never billed, and was also created by a package
+change, then this will also transfer the balance from I previous
+package, and so on, until reaching a package that either has been billed
+or was not created by a package change.
+
+=cut
+
+my $balance_transfer_reason;
+
+sub _transfer_balance {
+ my $self = shift;
+ my $cust_pkg = shift;
+ my $from_pkgnum = shift || $cust_pkg->change_pkgnum;
+ my $from_pkg = FS::cust_pkg->by_key($from_pkgnum);
+
+ my @transfers;
+
+ # if $from_pkg is not the first package in the chain, and it was never
+ # billed, walk back
+ if ( $from_pkg->change_pkgnum and scalar($from_pkg->cust_bill_pkg) == 0 ) {
+ @transfers = $self->_transfer_balance($cust_pkg, $from_pkg->change_pkgnum);
+ }
+
+ my $prev_balance = $self->balance_pkgnum($from_pkgnum);
+ if ( $prev_balance != 0 ) {
+ $balance_transfer_reason ||= FS::reason->new_or_existing(
+ 'reason' => 'Package balance transfer',
+ 'type' => 'Internal adjustment',
+ 'class' => 'R'
+ );
+
+ my $credit = FS::cust_credit->new({
+ 'custnum' => $self->custnum,
+ 'amount' => abs($prev_balance),
+ 'reasonnum' => $balance_transfer_reason->reasonnum,
+ '_date' => $cust_pkg->change_date,
+ });
+
+ my $cust_bill_pkg = FS::cust_bill_pkg->new({
+ 'setup' => 0,
+ 'recur' => abs($prev_balance),
+ #'sdate' => $from_pkg->last_bill, # not sure about this
+ #'edate' => $cust_pkg->change_date,
+ 'itemdesc' => $self->mt('Previous Balance, [_1]',
+ $from_pkg->part_pkg->pkg),
+ });
+
+ if ( $prev_balance > 0 ) {
+ # credit the old package, charge the new one
+ $credit->set('pkgnum', $from_pkgnum);
+ $cust_bill_pkg->set('pkgnum', $cust_pkg->pkgnum);
+ } else {
+ # the reverse
+ $credit->set('pkgnum', $cust_pkg->pkgnum);
+ $cust_bill_pkg->set('pkgnum', $from_pkgnum);
+ }
+ my $error = $credit->insert;
+ die "error transferring package balance from #".$from_pkgnum.
+ " to #".$cust_pkg->pkgnum.": $error\n" if $error;
+
+ push @transfers, $cust_bill_pkg;
+ } # $prev_balance != 0
+
+ return @transfers;
+}
+
+=item _handle_taxes PART_PKG TAXLISTHASH CUST_BILL_PKG CUST_PKG TIME PKGPART [ OPTIONS ]
+
+This is _handle_taxes. It's called once for each cust_bill_pkg generated
+from _make_lines, along with the part_pkg, cust_pkg, invoice time, the
+non-overridden pkgpart, a flag indicating whether the package is being
+canceled, and a partridge in a pear tree.
+
+The most important argument is 'taxlisthash'. This is shared across the
+entire invoice. It looks like this:
+{
+ 'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
+ 'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
+}
+
+'cust_main_county' can also be 'tax_rate'. The first object in the array
+is always the cust_main_county or tax_rate identified by the key.
+
+That "..." is a list of FS::cust_bill_pkg objects that will be fed to
+the 'taxline' method to calculate the amount of the tax. This doesn't
+happen until calculate_taxes, though.
+
+=cut
sub _handle_taxes {
my $self = shift;
diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm
index 804969b16..1caa3e5af 100644
--- a/FS/FS/cust_main/Billing_Realtime.pm
+++ b/FS/FS/cust_main/Billing_Realtime.pm
@@ -111,7 +111,7 @@ L for supported gateways.
Required arguments in the hashref are I, and I
-Available methods are: I, I and I
+Available methods are: I, I, I, and I
Available optional arguments are: I, I, I, I, I, I, I
@@ -317,6 +317,7 @@ my %bop_method2payby = (
'CC' => 'CARD',
'ECHECK' => 'CHEK',
'LEC' => 'LECB',
+ 'PAYPAL' => 'PPAL',
);
sub realtime_bop {
@@ -612,6 +613,7 @@ sub realtime_bop {
%$bop_content,
'reference' => $cust_pay_pending->paypendingnum, #for now
'callback_url' => $payment_gateway->gateway_callback_url,
+ 'cancel_url' => $payment_gateway->gateway_cancel_url,
'email' => $email,
%content, #after
);
diff --git a/FS/FS/cust_main/Packages.pm b/FS/FS/cust_main/Packages.pm
index 588f8a19f..8484df50e 100644
--- a/FS/FS/cust_main/Packages.pm
+++ b/FS/FS/cust_main/Packages.pm
@@ -4,9 +4,11 @@ use strict;
use vars qw( $DEBUG $me );
use List::Util qw( min );
use FS::UID qw( dbh );
-use FS::Record qw( qsearch );
+use FS::Record qw( qsearch qsearchs );
use FS::cust_pkg;
use FS::cust_svc;
+use FS::contact; # for attach_pkgs
+use FS::cust_location; #
$DEBUG = 0;
$me = '[FS::cust_main::Packages]';
@@ -87,7 +89,7 @@ sub order_pkg {
if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'};
my %insert_params = map { $opt->{$_} ? ( $_ => $opt->{$_} ) : () }
- qw( ticket_subject ticket_queue );
+ qw( ticket_subject ticket_queue allow_pkgpart );
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
@@ -100,17 +102,45 @@ sub order_pkg {
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- if ( $opt->{'cust_location'} &&
- ( ! $cust_pkg->locationnum || $cust_pkg->locationnum == -1 ) ) {
- my $error = $opt->{'cust_location'}->insert;
+ if ( $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->contactnum($opt->{'contact'}->contactnum);
+
+ #} else {
+ #
+ # $cust_pkg->contactnum();
+
+ }
+
+ if ( $opt->{'locationnum'} and $opt->{'locationnum'} != -1 ) {
+
+ $cust_pkg->locationnum($opt->{'locationnum'});
+
+ } elsif ( $opt->{'cust_location'} ) {
+
+ my $error = $opt->{'cust_location'}->find_or_insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "inserting cust_location (transaction rolled back): $error";
}
$cust_pkg->locationnum($opt->{'cust_location'}->locationnum);
- }
- else {
+
+ } else {
+
$cust_pkg->locationnum($self->ship_locationnum);
+
}
$cust_pkg->custnum( $self->custnum );
@@ -164,6 +194,7 @@ sub order_pkg {
'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 ) {
@@ -259,6 +290,108 @@ sub order_pkgs {
''; #no error
}
+=item attach_pkgs
+
+Merges this customer's package's into the target customer and then cancels them.
+
+=cut
+
+sub attach_pkgs {
+ my( $self, $new_custnum ) = @_;
+
+ #mostly false laziness w/ merge
+
+ return "Can't attach packages to self" if $self->custnum == $new_custnum;
+
+ my $new_cust_main = qsearchs( 'cust_main', { 'custnum' => $new_custnum } )
+ or return "Invalid new customer number: $new_custnum";
+
+ return 'Access denied: "Merge customer across agents" access right required to merge into a customer of a different agent'
+ if $self->agentnum != $new_cust_main->agentnum
+ && ! $FS::CurrentUser::CurrentUser->access_right('Merge customer across agents');
+
+ 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;
+
+ if ( qsearch('agent', { 'agent_custnum' => $self->custnum } ) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Can't merge a master agent customer";
+ }
+
+ #use FS::access_user
+ if ( qsearch('access_user', { 'user_custnum' => $self->custnum } ) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Can't merge a master employee customer";
+ }
+
+ if ( qsearch('cust_pay_pending', { 'custnum' => $self->custnum,
+ 'status' => { op=>'!=', value=>'done' },
+ }
+ )
+ ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Can't merge a customer with pending payments";
+ }
+
+ #end of false laziness
+
+ #pull in contact
+
+ my %contact_hash = ( 'first' => $self->first,
+ 'last' => $self->get('last'),
+ 'custnum' => $new_custnum,
+ 'disabled' => '',
+ );
+
+ my $contact = qsearchs( 'contact', \%contact_hash)
+ || new FS::contact \%contact_hash;
+ unless ( $contact->contactnum ) {
+ my $error = $contact->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
+
+ my $cust_location = $cust_pkg->cust_location || $self->ship_location;
+ my %loc_hash = $cust_location->hash;
+ $loc_hash{'locationnum'} = '';
+ $loc_hash{'custnum'} = $new_custnum;
+ $loc_hash{'disabled'} = '';
+ my $new_cust_location = qsearchs( 'cust_location', \%loc_hash)
+ || new FS::cust_location \%loc_hash;
+
+ my $pkg_or_error = $cust_pkg->change( {
+ 'keep_dates' => 1,
+ 'cust_main' => $new_cust_main,
+ 'contactnum' => $contact->contactnum,
+ 'cust_location' => $new_cust_location,
+ } );
+
+ my $error = ref($pkg_or_error) ? '' : $pkg_or_error;
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ ''; #no error
+
+}
+
=item all_pkgs [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
Returns all packages (see L) for this customer.
diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm
index d8f620f90..e0c7080fe 100644
--- a/FS/FS/cust_main/Search.pm
+++ b/FS/FS/cust_main/Search.pm
@@ -624,14 +624,14 @@ sub search {
# parse without census tract checkbox
##
- push @where, "(censustract = '' or censustract is null)"
+ push @where, "(ship_location.censustract = '' or ship_location.censustract is null)"
if $params->{'no_censustract'};
##
# parse with hardcoded tax location checkbox
##
- push @where, "geocode is not null"
+ push @where, "ship_location.geocode is not null"
if $params->{'with_geocode'};
##
@@ -841,7 +841,7 @@ sub search {
'ON (cust_main.'.$pre.'locationnum = '.$pre.'location.locationnum) ';
}
- my $count_query = "SELECT COUNT(*) FROM cust_main $extra_sql";
+ my $count_query = "SELECT COUNT(*) FROM cust_main $addl_from $extra_sql";
my @select = (
'cust_main.custnum',
@@ -857,7 +857,8 @@ sub search {
if ($params->{'flattened_pkgs'}) {
#my $pkg_join = '';
- $addl_from .= ' LEFT JOIN cust_pkg USING ( custnum ) ';
+ $addl_from .=
+ ' LEFT JOIN cust_pkg ON ( cust_main.custnum = cust_pkg.custnum ) ';
if ($dbh->{Driver}->{Name} eq 'Pg') {
@@ -926,6 +927,8 @@ sub search {
'extra_headers' => \@extra_headers,
'extra_fields' => \@extra_fields,
};
+ warn Data::Dumper::Dumper($sql_query);
+ $sql_query;
}
@@ -953,6 +956,11 @@ sub fuzzy_search {
my @cust_main = ();
+ my @fuzzy_mod = 'i';
+ my $conf = new FS::Conf;
+ my $fuzziness = $conf->config('fuzzy-fuzziness');
+ push @fuzzy_mod, $fuzziness if $fuzziness;
+
check_and_rebuild_fuzzyfiles();
foreach my $field ( keys %$fuzzy ) {
@@ -960,7 +968,7 @@ sub fuzzy_search {
next unless scalar(@$all);
my %match = ();
- $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) );
+ $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, \@fuzzy_mod, @$all ) );
next if !keys(%match);
my $in_matches = 'IN (' .
diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm
index 9a4990a9d..a61d67e11 100644
--- a/FS/FS/cust_main_county.pm
+++ b/FS/FS/cust_main_county.pm
@@ -147,13 +147,10 @@ If the taxname field is set, it will look like
If the taxclass is set, then it will be
"Anytown, Alameda County, CA, US (International)".
-Currently it will not contain the district, even if the city+county+state
-is not unique.
-
-OPTIONS may contain "no_taxclass" (hides taxclass) and/or "no_city"
-(hides city). It may also contain "out", in which case, if this
-region (district+city+county+state+country) contains no non-zero
-taxes, the label will read "Out of taxable region(s)".
+OPTIONS may contain "with_taxclass", "with_city", and "with_district" to show
+those fields. It may also contain "out", in which case, if this region
+(district+city+county+state+country) contains no non-zero taxes, the label
+will read "Out of taxable region(s)".
=cut
@@ -175,12 +172,15 @@ sub label {
my $label = $self->country;
$label = $self->state.", $label" if $self->state;
$label = $self->county." County, $label" if $self->county;
- if (!$opt{no_city}) {
+ if ($opt{with_city}) {
$label = $self->city.", $label" if $self->city;
+ if ($opt{with_district} and $self->district) {
+ $label = $self->district . ", $label";
+ }
}
# ugly labels when taxclass and taxname are both non-null...
# but this is how the tax report does it
- if (!$opt{no_taxclass}) {
+ if ($opt{with_taxclass}) {
$label = "$label (".$self->taxclass.')' if $self->taxclass;
}
$label = $self->taxname." ($label)" if $self->taxname;
diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm
index 4491f780c..da9143909 100644
--- a/FS/FS/cust_pay.pm
+++ b/FS/FS/cust_pay.pm
@@ -1062,6 +1062,8 @@ sub _upgrade_data { #class method
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
@@ -1079,7 +1081,7 @@ sub _upgrade_data { #class method
}
} #$object
} #$table
- FS::upgrade_journal->set_done('cust_pay__parse_paybatch');
+ FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
}
}
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 19337c4d6..4dced5461 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -1,7 +1,8 @@
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);
@@ -17,6 +18,7 @@ use FS::CurrentUser;
use FS::cust_svc;
use FS::part_pkg;
use FS::cust_main;
+use FS::contact;
use FS::cust_location;
use FS::pkg_svc;
use FS::cust_bill_pkg;
@@ -225,7 +227,7 @@ Create a new billing item. To add the item to the database, see L<"insert">.
=cut
sub table { 'cust_pkg'; }
-sub cust_linked { $_[0]->cust_main_custnum; }
+sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
sub cust_unlinked_msg {
my $self = shift;
"WARNING: can't find cust_main.custnum ". $self->custnum.
@@ -267,6 +269,12 @@ a ticket will be added to this customer with this subject
an optional queue name for ticket additions
+=item allow_pkgpart
+
+Don't check the legality of the package definition. This should be used
+when performing a package change that doesn't change the pkgpart (i.e.
+a location change).
+
=back
=cut
@@ -274,7 +282,8 @@ an optional queue name for ticket additions
sub insert {
my( $self, %options ) = @_;
- my $error = $self->check_pkgpart;
+ my $error;
+ $error = $self->check_pkgpart unless $options{'allow_pkgpart'};
return $error if $error;
my $part_pkg = $self->part_pkg;
@@ -613,7 +622,7 @@ sub check {
$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')
@@ -654,14 +663,19 @@ sub check {
=item check_pkgpart
+Check the pkgpart to make sure it's allowed with the reg_code and/or
+promo_code of the package (if present) and with the customer's agent.
+Called from C, unless we are doing a package change that doesn't
+affect pkgpart.
+
=cut
sub check_pkgpart {
my $self = shift;
- my $error = $self->ut_numbern('pkgpart');
- return $error if $error;
+ # my $error = $self->ut_numbern('pkgpart'); # already done
+ my $error;
if ( $self->reg_code ) {
unless ( grep { $self->pkgpart == $_->pkgpart }
@@ -848,6 +862,7 @@ sub cancel {
my %hash = $self->hash;
$date ? ($hash{'expire'} = $date) : ($hash{'cancel'} = $cancel_time);
+ $hash{'change_custnum'} = $options{'change_custnum'};
my $new = new FS::cust_pkg ( \%hash );
$error = $new->replace( $self, options => { $self->options } );
if ( $error ) {
@@ -981,6 +996,7 @@ sub uncancel {
my $error = $cust_pkg->insert(
'change' => 1, #supresses any referral credit to a referring customer
+ 'allow_pkgpart' => 1, # allow this even if the package def is disabled
);
if ($error) {
$dbh->rollback if $oldAutoCommit;
@@ -1022,15 +1038,20 @@ sub uncancel {
$dbh->rollback if $oldAutoCommit;
return $svc_error;
} else {
+ # if we've failed to insert the svc_x object, svc_Common->insert
+ # will have removed the cust_svc already. if not, then both records
+ # were inserted but we failed for some other reason (export, most
+ # likely). in that case, report the error and delete the records.
push @svc_errors, $svc_error;
- # is this necessary? svc_Common::insert already deletes the
- # cust_svc if inserting svc_x fails.
my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $svc_x->svcnum });
if ( $cust_svc ) {
- my $cs_error = $cust_svc->delete;
- if ( $cs_error ) {
+ # except if export_insert failed, export_delete probably won't be
+ # much better
+ local $FS::svc_Common::noexport_hack = 1;
+ my $cleanup_error = $svc_x->delete; # also deletes cust_svc
+ if ( $cleanup_error ) { # and if THAT fails, then run away
$dbh->rollback if $oldAutoCommit;
- return $cs_error;
+ return $cleanup_error;
}
}
} # svc_fatal
@@ -1683,6 +1704,11 @@ New locationnum, to change the location for this package.
New FS::cust_location object, to create a new location and assign it
to this package.
+=item cust_main
+
+New FS::cust_main object, to create a new customer and assign the new package
+to it.
+
=item pkgpart
New pkgpart (see L).
@@ -1747,9 +1773,8 @@ sub change {
$hash{"change_$_"} = $self->$_()
foreach qw( pkgnum pkgpart locationnum );
- if ( $opt->{'cust_location'} &&
- ( ! $opt->{'locationnum'} || $opt->{'locationnum'} == -1 ) ) {
- $error = $opt->{'cust_location'}->insert;
+ if ( $opt->{'cust_location'} ) {
+ $error = $opt->{'cust_location'}->find_or_insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "inserting cust_location (transaction rolled back): $error";
@@ -1757,6 +1782,12 @@ sub change {
$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;
my $keep_dates = $opt->{'keep_dates'};
# Special case. If the pkgpart is changing, and the customer is
@@ -1781,15 +1812,37 @@ sub change {
# (i.e. customer default location)
$opt->{'locationnum'} = $self->locationnum if !exists($opt->{'locationnum'});
+ # usually this doesn't matter. the two cases where it does are:
+ # 1. unused_credit_change + pkgpart change + setup fee on the new package
+ # and
+ # 2. (more importantly) changing a package before it's billed
+ $hash{'waive_setup'} = $self->waive_setup;
+
+ my $custnum = $self->custnum;
+ if ( $opt->{cust_main} ) {
+ my $cust_main = $opt->{cust_main};
+ unless ( $cust_main->custnum ) {
+ my $error = $cust_main->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "inserting cust_main (transaction rolled back): $error";
+ }
+ }
+ $custnum = $cust_main->custnum;
+ }
+
+ $hash{'contactnum'} = $opt->{'contactnum'} if $opt->{'contactnum'};
+
# Create the new package.
my $cust_pkg = new FS::cust_pkg {
- custnum => $self->custnum,
- pkgpart => ( $opt->{'pkgpart'} || $self->pkgpart ),
- refnum => ( $opt->{'refnum'} || $self->refnum ),
- locationnum => ( $opt->{'locationnum'} ),
+ custnum => $custnum,
+ pkgpart => ( $opt->{'pkgpart'} || $self->pkgpart ),
+ refnum => ( $opt->{'refnum'} || $self->refnum ),
+ 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;
@@ -1847,6 +1900,23 @@ sub change {
}
}
+ # 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;
@@ -1864,7 +1934,7 @@ sub change {
my $new = FS::cust_pkg->new({
pkgpart => $link->dst_pkgpart,
pkglinknum => $link->pkglinknum,
- custnum => $self->custnum,
+ custnum => $custnum,
main_pkgnum => $cust_pkg->pkgnum,
locationnum => $cust_pkg->locationnum,
start_date => $cust_pkg->start_date,
@@ -1874,14 +1944,14 @@ sub change {
contract_end => $cust_pkg->contract_end,
refnum => $cust_pkg->refnum,
discountnum => $cust_pkg->discountnum,
- waive_setup => $cust_pkg->waive_setup
+ 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;
+ $error = $new->insert( allow_pkgpart => $same_pkgpart );
# transfer services
if ( $old ) {
$error ||= $old->transfer($new);
@@ -1905,9 +1975,10 @@ sub change {
#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,
- nobill => $keep_dates
+ quiet => 1,
+ unused_credit => $unused_credit,
+ nobill => $keep_dates,
+ change_custnum => ( $self->custnum != $custnum ? $custnum : '' ),
);
if ($error) {
$dbh->rollback if $oldAutoCommit;
@@ -2079,6 +2150,18 @@ sub old_cust_pkg {
qsearchs('cust_pkg', { 'pkgnum' => $self->change_pkgnum } );
}
+=item change_cust_main
+
+Returns the customter this package was detached to, if any.
+
+=cut
+
+sub change_cust_main {
+ my $self = shift;
+ return '' unless $self->change_custnum;
+ qsearchs('cust_main', { 'custnum' => $self->change_custnum } );
+}
+
=item calc_setup
Calls the I of the FS::part_pkg object associated with this billing
@@ -2639,7 +2722,7 @@ sub statuscolor {
=item pkg_label
Returns a label for this package. (Currently "pkgnum: pkg - comment" or
-"pkg-comment" depending on user preference).
+"pkg - comment" depending on user preference).
=cut
@@ -2666,6 +2749,17 @@ sub pkg_label_long {
$label;
}
+=item pkg_locale
+
+Returns a customer-localized label for this package.
+
+=cut
+
+sub pkg_locale {
+ my $self = shift;
+ $self->part_pkg->pkg_locale( $self->cust_main->locale );
+}
+
=item primary_cust_svc
Returns a primary service (as FS::cust_svc object) if one can be identified.
diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index bbf4eedf8..627410729 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -863,38 +863,82 @@ sub smart_search_param {
my @or =
map { my $table = $_;
my $search_sql = "FS::$table"->search_sql($string);
- " ( svcdb = '$table'
- AND 0 < ( SELECT COUNT(*) FROM $table
- WHERE $table.svcnum = cust_svc.svcnum
- AND $search_sql
- )
- ) ";
+
+ "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
+ "FROM $table WHERE $search_sql";
}
FS::part_svc->svc_tables;
if ( $string =~ /^(\d+)$/ ) {
- unshift @or, " ( agent_svcid IS NOT NULL AND agent_svcid = $1 ) ";
+ unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
}
- my @extra_sql = ' ( '. join(' OR ', @or). ' ) ';
+ my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
+ " ON (svc_all.svcnum = cust_svc.svcnum) ";
+
+ my @extra_sql;
push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
'null_right' => 'View/link unlinked services'
);
my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
#for agentnum
- my $addl_from = ' LEFT JOIN cust_pkg USING ( pkgnum )'.
+ $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
' LEFT JOIN part_svc USING ( svcpart )';
(
'table' => 'cust_svc',
+ 'select' => 'svc_all.svcnum AS svcnum, '.
+ 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb',
'addl_from' => $addl_from,
'hashref' => {},
'extra_sql' => $extra_sql,
);
}
+sub _upgrade_data {
+ my $class = shift;
+
+ # fix missing (deleted by mistake) svc_x records
+ warn "searching for missing svc_x records...\n";
+ my %search = (
+ 'table' => 'cust_svc',
+ 'select' => 'cust_svc.*',
+ 'addl_from' => ' LEFT JOIN ( ' .
+ join(' UNION ',
+ map { "SELECT svcnum FROM $_" }
+ FS::part_svc->svc_tables
+ ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
+ 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
+ );
+ my @svcs = qsearch(\%search);
+ warn "found ".scalar(@svcs)."\n";
+
+ local $FS::Record::nowarn_classload = 1; # for h_svc_
+ local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
+
+ my %h_search = (
+ 'hashref' => { history_action => 'delete' },
+ 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
+ );
+ foreach my $cust_svc (@svcs) {
+ my $svcnum = $cust_svc->svcnum;
+ my $svcdb = $cust_svc->part_svc->svcdb;
+ $h_search{'hashref'}{'svcnum'} = $svcnum;
+ $h_search{'table'} = "h_$svcdb";
+ my $h_svc_x = qsearchs(\%h_search)
+ or next;
+ my $class = "FS::$svcdb";
+ my $new_svc_x = $class->new({ $h_svc_x->hash });
+ my $error = $new_svc_x->insert;
+ warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
+ if $error;
+ }
+
+ '';
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/cust_tax_location.pm b/FS/FS/cust_tax_location.pm
index 1a9bf5a41..4293b2c90 100644
--- a/FS/FS/cust_tax_location.pm
+++ b/FS/FS/cust_tax_location.pm
@@ -199,13 +199,15 @@ sub batch_import {
if (exists($hash->{actionflag}) && $hash->{actionflag} eq 'D') {
delete($hash->{actionflag});
- my $cust_tax_location = qsearchs('cust_tax_location', $hash);
+ my @cust_tax_location = qsearch('cust_tax_location', $hash);
return "Can't find cust_tax_location to delete: ".
join(" ", map { "$_ => ". $hash->{$_} } @fields)
- unless $cust_tax_location;
+ unless scalar(@cust_tax_location) || $param->{'delete_only'} ;
- my $error = $cust_tax_location->delete;
- return $error if $error;
+ foreach my $cust_tax_location (@cust_tax_location) {
+ my $error = $cust_tax_location->delete;
+ return $error if $error;
+ }
delete($hash->{$_}) foreach (keys %$hash);
}
@@ -234,13 +236,15 @@ sub batch_import {
if (exists($hash->{actionflag}) && $hash->{actionflag} eq 'D') {
delete($hash->{actionflag});
- my $cust_tax_location = qsearchs('cust_tax_location', $hash);
+ my @cust_tax_location = qsearch('cust_tax_location', $hash);
return "Can't find cust_tax_location to delete: ".
join(" ", map { "$_ => ". $hash->{$_} } @fields)
- unless $cust_tax_location;
+ unless scalar(@cust_tax_location) || $param->{'delete_only'} ;
- my $error = $cust_tax_location->delete;
- return $error if $error;
+ foreach my $cust_tax_location (@cust_tax_location) {
+ my $error = $cust_tax_location->delete;
+ return $error if $error;
+ }
delete($hash->{$_}) foreach (keys %$hash);
}
diff --git a/FS/FS/export_svc.pm b/FS/FS/export_svc.pm
index 0370f5f0b..b08f8f7c3 100644
--- a/FS/FS/export_svc.pm
+++ b/FS/FS/export_svc.pm
@@ -5,6 +5,7 @@ use vars qw( @ISA );
use FS::Record qw( qsearch qsearchs dbh );
use FS::part_export;
use FS::part_svc;
+use FS::svc_export_machine;
@ISA = qw(FS::Record);
@@ -209,6 +210,19 @@ sub insert {
} #end of duplicate check, whew
$error = $self->SUPER::insert;
+
+ my $part_export = $self->part_export;
+ if ( !$error and $part_export->default_machine ) {
+ foreach my $cust_svc ( $self->part_svc->cust_svc ) {
+ my $svc_export_machine = FS::svc_export_machine->new({
+ 'exportnum' => $self->exportnum,
+ 'svcnum' => $cust_svc->svcnum,
+ 'machinenum' => $part_export->default_machine,
+ });
+ $error ||= $svc_export_machine->insert;
+ }
+ }
+
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
@@ -251,7 +265,23 @@ Delete this record from the database.
=cut
-# the delete method can be inherited from FS::Record
+sub delete {
+ my $self = shift;
+ my $dbh = dbh;
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+
+ my $error = $self->SUPER::delete;
+ foreach ($self->svc_export_machine) {
+ $error ||= $_->delete;
+ }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+}
+
=item replace OLD_RECORD
@@ -307,6 +337,24 @@ sub part_svc {
qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
}
+=item svc_export_machine
+
+Returns all export hostname records (L) for this
+combination of svcpart and exportnum.
+
+=cut
+
+sub svc_export_machine {
+ my $self = shift;
+ qsearch({
+ 'table' => 'svc_export_machine',
+ 'select' => 'svc_export_machine.*',
+ 'addl_from' => 'JOIN cust_svc USING (svcnum)',
+ 'hashref' => { 'exportnum' => $self->exportnum },
+ 'extra_sql' => ' AND cust_svc.svcpart = '.$self->svcpart,
+ });
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/part_event/Action/fee.pm b/FS/FS/part_event/Action/fee.pm
index 68288d090..cd9e200c8 100644
--- a/FS/FS/part_event/Action/fee.pm
+++ b/FS/FS/part_event/Action/fee.pm
@@ -17,14 +17,25 @@ sub option_fields {
type=>'checkbox', value=>'Y' },
'nextbill' => { label=>'Hold late fee until next invoice',
type=>'checkbox', value=>'Y' },
+ 'limit_to_credit'=>
+ { label=>"Charge no more than the customer's credit balance",
+ type=>'checkbox', value=>'Y' },
);
}
sub default_weight { 10; }
sub _calc_fee {
- #my( $self, $cust_object ) = @_;
- my $self = shift;
+ my( $self, $cust_object ) = @_;
+ if ( $self->option('limit_to_credit') ) {
+ my $balance = $cust_object->cust_main->balance;
+ if ( $balance >= 0 ) {
+ return 0;
+ } elsif ( (-1 * $balance) < $self->option('charge') ) {
+ return -1 * $balance;
+ }
+ }
+
$self->option('charge');
}
@@ -44,6 +55,9 @@ sub do_action {
'setuptax' => $self->option('setuptax'),
);
+ # amazingly, FS::cust_main::charge will allow a charge of zero
+ return '' if $charge{'amount'} == 0;
+
#unless its more than N months away?
$charge{'start_date'} = $cust_main->next_bill_date
if $self->option('nextbill');
diff --git a/FS/FS/part_event/Condition/cust_bill_owed_percent.pm b/FS/FS/part_event/Condition/cust_bill_owed_percent.pm
new file mode 100644
index 000000000..e06b511ef
--- /dev/null
+++ b/FS/FS/part_event/Condition/cust_bill_owed_percent.pm
@@ -0,0 +1,50 @@
+package FS::part_event::Condition::cust_bill_owed_percent;
+
+use strict;
+use FS::cust_bill;
+
+use base qw( FS::part_event::Condition );
+
+sub description {
+ 'Percentage owed on specific invoice';
+}
+
+sub eventtable_hashref {
+ { 'cust_main' => 0,
+ 'cust_bill' => 1,
+ 'cust_pkg' => 0,
+ };
+}
+
+sub option_fields {
+ (
+ 'owed' => { 'label' => 'Percentage of invoice owed over',
+ 'type' => 'percentage',
+ 'value' => '0', #default
+ },
+ );
+}
+
+sub condition {
+ #my($self, $cust_bill, %opt) = @_;
+ my($self, $cust_bill) = @_;
+
+ my $percent = $self->option('owed') || 0;
+ my $over = sprintf('%.2f',
+ $cust_bill->charged * $percent / 100);
+
+ $cust_bill->owed > $over;
+}
+
+sub condition_sql {
+ my( $class, $table ) = @_;
+
+ # forces the option to be an integer--do we care?
+ my $percent = $class->condition_sql_option_integer('owed');
+
+ my $owed_sql = FS::cust_bill->owed_sql;
+
+ "$owed_sql > CAST( cust_bill.charged * $percent / 100 AS DECIMAL(10,2) )";
+}
+
+1;
diff --git a/FS/FS/part_event/Condition/inactive_age.pm b/FS/FS/part_event/Condition/inactive_age.pm
new file mode 100644
index 000000000..8918a1a3c
--- /dev/null
+++ b/FS/FS/part_event/Condition/inactive_age.pm
@@ -0,0 +1,46 @@
+package FS::part_event::Condition::inactive_age;
+
+use strict;
+use base qw( FS::part_event::Condition );
+use FS::Record qw( qsearch );
+
+sub description { 'Days without billing activity' }
+
+sub option_fields {
+ (
+ 'age' => { 'label' => 'No activity within',
+ 'type' => 'freq',
+ },
+ # flags to select kinds of activity,
+ # like if you just want "no payments since"?
+ # not relevant yet
+ );
+}
+
+sub condition {
+ my( $self, $obj, %opt ) = @_;
+ my $custnum = $obj->custnum;
+ my $age = $self->option_age_from('age', $opt{'time'} );
+
+ foreach my $t (qw(cust_bill cust_pay cust_credit cust_refund)) {
+ my $class = "FS::$t";
+ return 0 if $class->count("custnum = $custnum AND _date >= $age");
+ }
+ 1;
+}
+
+sub condition_sql {
+ my( $class, $table, %opt ) = @_;
+ my $age = $class->condition_sql_option_age_from('age', $opt{'time'});
+ my @sql;
+ for my $t (qw(cust_bill cust_pay cust_credit cust_refund)) {
+ push @sql,
+ "NOT EXISTS( SELECT 1 FROM $t ".
+ "WHERE $t.custnum = cust_main.custnum AND $t._date >= $age".
+ ")";
+ }
+ join(' AND ', @sql);
+}
+
+1;
+
diff --git a/FS/FS/part_event/Condition/once_perinv.pm b/FS/FS/part_event/Condition/once_perinv.pm
index f85a05665..1ee53b812 100644
--- a/FS/FS/part_event/Condition/once_perinv.pm
+++ b/FS/FS/part_event/Condition/once_perinv.pm
@@ -12,6 +12,15 @@ sub description { "Run only once for each time the package has been billed"; }
# Run the event, at most, a number of times equal to the number of
# distinct invoices that contain line items from this package.
+sub option_fields {
+ (
+ 'paid' => { 'label' => 'Only count paid bills',
+ 'type' => 'checkbox',
+ 'value' => 'Y',
+ },
+ )
+}
+
sub eventtable_hashref {
{ 'cust_main' => 0,
'cust_bill' => 0,
@@ -22,9 +31,15 @@ sub eventtable_hashref {
sub condition {
my($self, $cust_pkg, %opt) = @_;
- my %invnum;
- $invnum{$_->invnum} = 1
- foreach ( qsearch('cust_bill_pkg', { 'pkgnum' => $cust_pkg->pkgnum }) );
+ my @cust_bill_pkg = qsearch('cust_bill_pkg', { pkgnum=>$cust_pkg->pkgnum });
+
+ @cust_bill_pkg = grep { ($_->owed_setup + $_->owed_recur) == 0 }
+ @cust_bill_pkg
+ if $self->option('paid');
+
+ my %invnum = ();
+ $invnum{$_->invnum} = 1 foreach @cust_bill_pkg;
+
my @events = qsearch( {
'table' => 'cust_event',
'hashref' => { 'eventpart' => $self->eventpart,
@@ -40,6 +55,9 @@ sub condition {
sub condition_sql {
my( $self, $table ) = @_;
+ #paid flag not yet implemented here, but that's okay, a partial optimization
+ # is better than none
+
"(
( SELECT COUNT(distinct(invnum))
FROM cust_bill_pkg
diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm
index 15ce9c059..28cb1419d 100644
--- a/FS/FS/part_export.pm
+++ b/FS/FS/part_export.pm
@@ -125,31 +125,14 @@ sub insert {
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- my $error = $self->SUPER::insert(@_);
+ my $error = $self->SUPER::insert(@_)
+ || $self->replace;
+ # use replace to do all the part_export_machine and default_machine stuff
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
}
- #kinda false laziness with process_m2name
- my @machines = map { $_ =~ s/^\s+//; $_ =~ s/\s+$//; $_ }
- grep /\S/,
- split /[\n\r]{1,2}/,
- $self->part_export_machine_textarea;
-
- foreach my $machine ( @machines ) {
-
- my $part_export_machine = new FS::part_export_machine {
- 'exportnum' => $self->exportnum,
- 'machine' => $machine,
- };
- $error = $part_export_machine->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
- }
-
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
}
@@ -217,6 +200,7 @@ or modified.
sub replace {
my $self = shift;
+ my $old = $self->replace_old;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
@@ -228,12 +212,7 @@ sub replace {
my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
-
- my $error = $self->SUPER::replace(@_);
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
+ my $error;
if ( $self->part_export_machine_textarea ) {
@@ -258,6 +237,10 @@ sub replace {
}
}
+ if ( $self->default_machine_name eq $machine ) {
+ $self->default_machine( $part_export_machine{$machine}->machinenum );
+ }
+
delete $part_export_machine{$machine}; #so we don't disable it below
} else {
@@ -272,11 +255,13 @@ sub replace {
return $error;
}
+ if ( $self->default_machine_name eq $machine ) {
+ $self->default_machine( $part_export_machine->machinenum );
+ }
}
}
-
foreach my $part_export_machine ( values %part_export_machine ) {
$part_export_machine->disabled('Y');
$error = $part_export_machine->replace;
@@ -286,6 +271,48 @@ sub replace {
}
}
+ if ( $old->machine ne '_SVC_MACHINE' ) {
+ # then set up the default for any already-attached export_svcs
+ foreach my $export_svc ( $self->export_svc ) {
+ my @svcs = qsearch('cust_svc', { 'svcpart' => $export_svc->svcpart });
+ foreach my $cust_svc ( @svcs ) {
+ my $svc_export_machine = FS::svc_export_machine->new({
+ 'exportnum' => $self->exportnum,
+ 'svcnum' => $cust_svc->svcnum,
+ 'machinenum' => $self->default_machine,
+ });
+ $error ||= $svc_export_machine->insert;
+ }
+ }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ } # if switching to selectable hosts
+
+ } elsif ( $old->machine eq '_SVC_MACHINE' ) {
+ # then we're switching from selectable to non-selectable
+ foreach my $svc_export_machine (
+ qsearch('svc_export_machine', { 'exportnum' => $self->exportnum })
+ ) {
+ $error ||= $svc_export_machine->delete;
+ }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
+ $error = $self->SUPER::replace(@_);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ if ( $self->machine eq '_SVC_MACHINE' and ! $self->default_machine ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "no default export host selected";
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -308,6 +335,13 @@ sub check {
|| $self->ut_domainn('machine')
|| $self->ut_alpha('exporttype')
;
+
+ if ( $self->machine eq '_SVC_MACHINE' ) {
+ $error ||= $self->ut_numbern('default_machine')
+ } else {
+ $self->set('default_machine', '');
+ }
+
return $error if $error;
$self->nodomain =~ /^(Y?)$/ or return "Illegal nodomain: ". $self->nodomain;
@@ -471,7 +505,9 @@ sub _rebless {
$self;
}
-=item svc_machine
+=item svc_machine SVC_X
+
+Return the export hostname for SVC_X.
=cut
@@ -483,14 +519,33 @@ sub svc_machine {
my $svc_export_machine = qsearchs('svc_export_machine', {
'svcnum' => $svc_x->svcnum,
'exportnum' => $self->exportnum,
- })
- #would only happen if you add this export to existing services without a
- #machine set then try to run exports without setting it... right?
- or die "No hostname selected for ".($self->exportname || $self->exporttype);
+ });
+
+ if (!$svc_export_machine) {
+ warn "No hostname selected for ".($self->exportname || $self->exporttype);
+ return $self->default_export_machine->machine;
+ }
return $svc_export_machine->part_export_machine->machine;
}
+=item default_export_machine
+
+Return the default export hostname for this export.
+
+=cut
+
+sub default_export_machine {
+ my $self = shift;
+ my $machinenum = $self->default_machine;
+ if ( $machinenum ) {
+ my $default_machine = FS::part_export_machine->by_key($machinenum);
+ return $default_machine->machine if $default_machine;
+ }
+ # this should not happen
+ die "no default export hostname for export ".$self->exportnum;
+}
+
#these should probably all go away, just let the subclasses define em
=item export_insert SVC_OBJECT
@@ -703,6 +758,55 @@ sub _upgrade_data { #class method
$error = $opt->replace;
die $error if $error;
}
+ # for exports that have selectable hostnames, make sure all services
+ # have a hostname selected
+ foreach my $part_export (
+ qsearch('part_export', { 'machine' => '_SVC_MACHINE' })
+ ) {
+
+ my $exportnum = $part_export->exportnum;
+ my $machinenum = $part_export->default_machine;
+ if (!$machinenum) {
+ my ($first) = $part_export->part_export_machine;
+ if (!$first) {
+ # user intervention really is required.
+ die "Export $exportnum has no hostname options defined.\n".
+ "You must correct this before upgrading.\n";
+ }
+ # warn about this, because we might not choose the right one
+ warn "Export $exportnum (". $part_export->exporttype.
+ ") has no default hostname. Setting to ".$first->machine."\n";
+ $machinenum = $first->machinenum;
+ $part_export->set('default_machine', $machinenum);
+ my $error = $part_export->replace;
+ die $error if $error;
+ }
+
+ # the service belongs to a service def that uses this export
+ # and there is not a hostname selected for this export for that service
+ my $join = ' JOIN export_svc USING ( svcpart )'.
+ ' LEFT JOIN svc_export_machine'.
+ ' ON ( cust_svc.svcnum = svc_export_machine.svcnum'.
+ ' AND export_svc.exportnum = svc_export_machine.exportnum )';
+
+ my @svcs = qsearch( {
+ 'select' => 'cust_svc.*',
+ 'table' => 'cust_svc',
+ 'addl_from' => $join,
+ 'extra_sql' => ' WHERE svcexportmachinenum IS NULL'.
+ ' AND export_svc.exportnum = '.$part_export->exportnum,
+ } );
+ foreach my $cust_svc (@svcs) {
+ my $svc_export_machine = FS::svc_export_machine->new({
+ 'exportnum' => $exportnum,
+ 'machinenum' => $machinenum,
+ 'svcnum' => $cust_svc->svcnum,
+ });
+ my $error = $svc_export_machine->insert;
+ die $error if $error;
+ }
+ }
+
# pass downstream
my %exports_in_use;
$exports_in_use{ref $_} = 1 foreach qsearch('part_export', {});
diff --git a/FS/FS/part_export/http_status.pm b/FS/FS/part_export/http_status.pm
index 80139e776..5c4a8d074 100644
--- a/FS/FS/part_export/http_status.pm
+++ b/FS/FS/part_export/http_status.pm
@@ -129,7 +129,7 @@ sub export_setstatus_listdel {
}
sub export_setstatus_listX {
- my( $self, $svc_x, $action, $list, $address ) = @_;
+ my( $self, $svc_x, $action, $list, $address_item ) = @_;
my $option;
if ( $list =~ /^[WA]/i ) { #Whitelist/Allow
@@ -139,8 +139,16 @@ sub export_setstatus_listX {
}
$option .= $action. '_url';
- $address = Email::Valid->address($address)
- or die "address failed $Email::Valid::Details check.\n";
+ my $address;
+ unless ( $address = Email::Valid->address($address_item) ) {
+
+ if ( $address_item =~ /^(\@[\w\-\.]+\.\w{2,63})$/ ) { # "@domain"
+ $address = $1;
+ } else {
+ die "address failed $Email::Valid::Details check.\n";
+ }
+
+ }
#some false laziness w/export_getstatus above
my $url;
diff --git a/FS/FS/part_export/huawei_hlr.pm b/FS/FS/part_export/huawei_hlr.pm
index 007981880..aa09a1c64 100644
--- a/FS/FS/part_export/huawei_hlr.pm
+++ b/FS/FS/part_export/huawei_hlr.pm
@@ -18,16 +18,16 @@ $DEBUG = 0;
@ISA = qw(FS::part_export);
tie my %options, 'Tie::IxHash',
- 'opname' => { label=>'Operator login' },
- 'pwd' => { label=>'Operator password' },
+ 'opname' => { label=>'Operator login (required)' },
+ 'pwd' => { label=>'Operator password (required)' },
'tplid' => { label=>'Template number' },
'hlrsn' => { label=>'HLR serial number' },
'k4sno' => { label=>'K4 serial number' },
- 'cardtype' => { label => 'Card type',
+ 'cardtype' => { label => 'Card type (required)',
type => 'select',
options=> ['SIM', 'USIM']
},
- 'alg' => { label => 'Authentication algorithm',
+ 'alg' => { label => 'Authentication algorithm (required)',
type => 'select',
options=> ['COMP128_1',
'COMP128_2',
@@ -314,8 +314,8 @@ sub import_sim {
# push IMSI/KI to the HLR
my $return = $self->command($socket,
@command,
- 'IMSI', $imsi,
- 'KIVALUE', $ki,
+ 'IMSI', qq{"$imsi"},
+ 'KIVALUE', qq{"$ki"},
@args
);
if ( $return->{success} ) {
diff --git a/FS/FS/part_export/shellcommands.pm b/FS/FS/part_export/shellcommands.pm
index f964af31c..9408d1454 100644
--- a/FS/FS/part_export/shellcommands.pm
+++ b/FS/FS/part_export/shellcommands.pm
@@ -243,12 +243,12 @@ sub _export_command {
${$_} = $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;
diff --git a/FS/FS/part_export/test.pm b/FS/FS/part_export/test.pm
new file mode 100644
index 000000000..126897c0b
--- /dev/null
+++ b/FS/FS/part_export/test.pm
@@ -0,0 +1,75 @@
+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' => <Test export. Do not use this in production systems.
+
This export either always succeeds, always fails (returning an error),
+or always dies, according to the "Result" option. It does nothing else; the
+purpose is purely to simulate success or failure within an export module.
+
The checkbox options can be used to turn the export off for certain
+actions, if this is needed.
+END
+);
+
+sub export_insert {
+ my $self = shift;
+ $self->run(@_) if $self->option('insert');
+}
+
+sub export_delete {
+ my $self = shift;
+ $self->run(@_) if $self->option('delete');
+}
+
+sub export_replace {
+ my $self = shift;
+ $self->run(@_) if $self->option('replace');
+}
+
+sub export_suspend {
+ my $self = shift;
+ $self->run(@_) if $self->option('suspend');
+}
+
+sub export_unsuspend {
+ my $self = shift;
+ $self->run(@_) if $self->option('unsuspend');
+}
+
+sub run {
+ my $self = shift;
+ my $svc_x = shift;
+ my $result = $self->option('result');
+ if ( $result eq 'failure' ) {
+ return $self->option('errormsg');
+ } elsif ( $result eq 'exception' ) {
+ die $self->option('errormsg');
+ } else {
+ return '';
+ }
+}
+
+1;
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 856a693dd..e788269f7 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -1,7 +1,8 @@
package FS::part_pkg;
+use base qw( FS::m2m_Common FS::o2m_Common FS::option_Common );
use strict;
-use vars qw( @ISA %plans $DEBUG $setup_hack $skip_pkg_svc_hack );
+use vars qw( %plans $DEBUG $setup_hack $skip_pkg_svc_hack );
use Carp qw(carp cluck confess);
use Scalar::Util qw( blessed );
use Time::Local qw( timelocal_nocheck );
@@ -16,6 +17,7 @@ use FS::type_pkgs;
use FS::part_pkg_option;
use FS::pkg_class;
use FS::agent;
+use FS::part_pkg_msgcat;
use FS::part_pkg_taxrate;
use FS::part_pkg_taxoverride;
use FS::part_pkg_taxproduct;
@@ -24,7 +26,6 @@ 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;
@@ -715,6 +716,35 @@ sub propagate {
join("\n", @error);
}
+=item pkg_locale LOCALE
+
+Returns a customer-viewable string representing this package for the given
+locale, from the part_pkg_msgcat table. If the given locale is empty or no
+localized string is found, returns the base pkg field.
+
+=cut
+
+sub pkg_locale {
+ my( $self, $locale ) = @_;
+ return $self->pkg unless $locale;
+ my $part_pkg_msgcat = $self->part_pkg_msgcat($locale) or return $self->pkg;
+ $part_pkg_msgcat->pkg;
+}
+
+=item part_pkg_msgcat LOCALE
+
+Like pkg_locale, but returns the FS::part_pkg_msgcat object itself.
+
+=cut
+
+sub part_pkg_msgcat {
+ my( $self, $locale ) = @_;
+ qsearchs( 'part_pkg_msgcat', {
+ pkgpart => $self->pkgpart,
+ locale => $locale,
+ });
+}
+
=item pkg_comment [ OPTION => VALUE... ]
Returns an (internal) string representing this package. Currently,
diff --git a/FS/FS/part_pkg/prorate_Mixin.pm b/FS/FS/part_pkg/prorate_Mixin.pm
index 153ed56cd..9efc7e89d 100644
--- a/FS/FS/part_pkg/prorate_Mixin.pm
+++ b/FS/FS/part_pkg/prorate_Mixin.pm
@@ -67,11 +67,11 @@ the base price per billing cycle.
Options:
- add_full_period: Bill for the time up to the prorate day plus one full
-billing period after that.
+ billing period after that.
- prorate_round_day: Round the current time to the nearest full day,
-instead of using the exact time.
+ instead of using the exact time.
- prorate_defer_bill: Don't bill the prorate interval until the prorate
-day arrives.
+ day arrives.
- prorate_verbose: Generate details to explain the prorate calculations.
=cut
@@ -104,7 +104,7 @@ sub calc_prorate {
$add_period = 1;
}
- # if the customer alreqady has a billing day-of-month established,
+ # if the customer already has a billing day-of-month established,
# and it's a valid cutoff day, try to respect it
my $next_bill_day;
if ( my $next_bill = $cust_pkg->cust_main->next_bill_date ) {
@@ -123,31 +123,46 @@ sub calc_prorate {
my $permonth = $charge / $self->freq;
my $months = ( ( $self->freq - 1 ) + ($mend-$mnow) / ($mend-$mstart) );
-
- if ( $self->option('prorate_verbose',1)
- and $months > 0 and $months < $self->freq ) {
- push @$details,
- 'Prorated (' . time2str('%b %d', $mnow) .
- ' - ' . time2str('%b %d', $mend) . '): ' . $money_char .
- sprintf('%.2f', $permonth * $months + 0.00000001 );
- }
+ # after this, $self->freq - 1 < $months <= $self->freq
# add a full period if currently billing for a partial period
# or periods up to freq_override if billing for an override interval
if ( ($param->{'freq_override'} || 0) > 1 ) {
$months += $param->{'freq_override'} - 1;
- }
- elsif ( $add_period && $months < $self->freq) {
+ # freq_override - 1 correct here?
+ # (probably only if freq == 1, yes?)
+ } elsif ( $add_period && $months < $self->freq ) {
+
+ # '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) ) {
- # calculate the prorated and add'l period charges
+ }
+
+ 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;
diff --git a/FS/FS/part_pkg/sqlradacct_daily.pm b/FS/FS/part_pkg/sqlradacct_daily.pm
index d99def21b..d0d3e1006 100644
--- a/FS/FS/part_pkg/sqlradacct_daily.pm
+++ b/FS/FS/part_pkg/sqlradacct_daily.pm
@@ -66,8 +66,13 @@ use Date::Format;
'default' => 0,
},
+ 'monthly_cap' => { 'name' => 'Monthly (billing frequency) cap on all overage charges'.
+ ' (0 means no cap)',
+ 'default' => 0,
+ },
+
},
- 'fieldorder' => [qw( recur_included_hours recur_hourly_charge recur_hourly_cap recur_included_input recur_input_charge recur_input_cap recur_included_output recur_output_charge recur_output_cap recur_included_total recur_total_charge recur_total_cap global_cap )],
+ 'fieldorder' => [qw( recur_included_hours recur_hourly_charge recur_hourly_cap recur_included_input recur_input_charge recur_input_cap recur_included_output recur_output_charge recur_output_cap recur_included_total recur_total_charge recur_total_cap global_cap monthly_cap )],
'weight' => 41,
);
@@ -79,7 +84,7 @@ sub price_info {
}
#hacked-up false laziness w/sqlradacct_hour,
-# but keeping it separate to start with is safer for existing folks
+# but keeping it separate to start with is safer for existing folks
sub calc_recur {
my($self, $cust_pkg, $sdate, $details ) = @_;
@@ -179,6 +184,10 @@ sub calc_recur {
$day_start = $tomorrow;
}
+ $charges = $self->option('monthly_cap')
+ if $self->option('monthly_cap')
+ && $charges > $self->option('monthly_cap');
+
$self->option('recur_fee') + $charges;
}
diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm
index 1c891b157..21c6a8a2c 100644
--- a/FS/FS/part_pkg/voip_cdr.pm
+++ b/FS/FS/part_pkg/voip_cdr.pm
@@ -157,10 +157,16 @@ tie my %detail_formats, 'Tie::IxHash',
'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: ',
},
- 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to: ',
+ 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to this cdrtypenum: ',
},
- 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to: ',
+ '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_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: ',
@@ -309,6 +315,7 @@ tie my %detail_formats, 'Tie::IxHash',
use_amaflags
use_carrierid
use_cdrtypenum ignore_cdrtypenum
+ use_calltypenum ignore_calltypenum
ignore_disposition disposition_in
skip_dcontext skip_dst_prefix
skip_dstchannel_prefix skip_src_length_more
@@ -420,6 +427,7 @@ sub calc_usage {
'disable_src' => $self->option('disable_src'),
'default_prefix' => $self->option('default_prefix'),
'cdrtypenum' => $self->option('use_cdrtypenum'),
+ 'calltypenum' => $self->option('use_calltypenum'),
'status' => '',
'for_update' => 1,
); # $last_bill, $$sdate )
@@ -487,6 +495,7 @@ sub calc_usage {
}
#returns a reason why not to rate this CDR, or false if the CDR is chargeable
+# lots of false laziness w/voip_inbound
sub check_chargable {
my( $self, $cdr, %flags ) = @_;
@@ -520,6 +529,15 @@ sub check_chargable {
if length($self->option_cacheable('ignore_cdrtypenum'))
&& $cdr->cdrtypenum eq $self->option_cacheable('ignore_cdrtypenum'); #eq otherwise 0 matches ''
+ # unlike everything else, use_calltypenum is applied in FS::svc_x::get_cdrs.
+ return "calltypenum != ". $self->option_cacheable('use_calltypenum')
+ if length($self->option_cacheable('use_calltypenum'))
+ && $cdr->calltypenum ne $self->option_cacheable('use_calltypenum'); #ne otherwise 0 matches ''
+
+ return "calltypenum == ". $self->option_cacheable('ignore_calltypenum')
+ if length($self->option_cacheable('ignore_calltypenum'))
+ && $cdr->calltypenum eq $self->option_cacheable('ignore_calltypenum'); #eq otherwise 0 matches ''
+
return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )"
if $self->option_cacheable('skip_dcontext') =~ /\S/
&& grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext'));
diff --git a/FS/FS/part_pkg/voip_inbound.pm b/FS/FS/part_pkg/voip_inbound.pm
index 9054f7b99..525db804d 100644
--- a/FS/FS/part_pkg/voip_inbound.pm
+++ b/FS/FS/part_pkg/voip_inbound.pm
@@ -60,15 +60,21 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities();
'type' => 'checkbox',
},
- 'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to: ',
+ 'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: ',
},
- 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to: ',
+ 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to this cdrtypenum: ',
},
- 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to: ',
+ 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to this cdrtypenum: ',
},
+ 'use_calltypenum' => { 'name' => 'Only charge for CDRs where the CDR Call Type is set to this cdrtypenum: ',
+ },
+
+ 'ignore_calltypenum' => { 'name' => 'Do not charge for CDRs where the CDR Call Type is set to this cdrtypenum: ',
+ },
+
'ignore_disposition' => { 'name' => 'Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: ',
},
@@ -147,6 +153,7 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities();
use_amaflags
use_carrierid
use_cdrtypenum ignore_cdrtypenum
+ use_calltypenum ignore_calltypenum
ignore_disposition disposition_in
skip_dcontext skip_dstchannel_prefix
skip_dst_length_less skip_lastapp
@@ -329,67 +336,58 @@ sub calc_usage {
}
#returns a reason why not to rate this CDR, or false if the CDR is chargeable
+# lots of false laziness w/voip_cdr...
sub check_chargable {
my( $self, $cdr, %flags ) = @_;
- #should have some better way of checking these options from a hash
- #or something
-
- my @opt = qw(
- use_amaflags
- use_carrierid
- use_cdrtypenum
- ignore_cdrtypenum
- disposition_in
- ignore_disposition
- skip_dcontext
- skip_dstchannel_prefix
- skip_dst_length_less
- skip_lastapp
- );
- foreach my $opt (grep !exists($flags{option_cache}->{$_}), @opt ) {
- $flags{option_cache}->{$opt} = $self->option($opt, 1);
- }
- my %opt = %{ $flags{option_cache} };
-
return 'amaflags != 2'
- if $opt{'use_amaflags'} && $cdr->amaflags != 2;
-
- return "disposition NOT IN ( $opt{'disposition_in'} )"
- if $opt{'disposition_in'} =~ /\S/
- && !grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $opt{'disposition_in'});
-
- return "disposition IN ( $opt{'ignore_disposition'} )"
- if $opt{'ignore_disposition'} =~ /\S/
- && grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $opt{'ignore_disposition'});
-
- return "carrierid != $opt{'use_carrierid'}"
- if length($opt{'use_carrierid'})
- && $cdr->carrierid ne $opt{'use_carrierid'}; #ne otherwise 0 matches ''
+ if $self->option_cacheable('use_amaflags') && $cdr->amaflags != 2;
- return "cdrtypenum != $opt{'use_cdrtypenum'}"
- if length($opt{'use_cdrtypenum'})
- && $cdr->cdrtypenum ne $opt{'use_cdrtypenum'}; #ne otherwise 0 matches ''
-
- return "cdrtypenum == $opt{'ignore_cdrtypenum'}"
- if length($opt{'ignore_cdrtypenum'})
- && $cdr->cdrtypenum eq $opt{'ignore_cdrtypenum'}; #eq otherwise 0 matches ''
+ return "disposition NOT IN ( ". $self->option_cacheable('disposition_in')." )"
+ if $self->option_cacheable('disposition_in') =~ /\S/
+ && !grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('disposition_in'));
+
+ return "disposition IN ( ". $self->option_cacheable('ignore_disposition')." )"
+ if $self->option_cacheable('ignore_disposition') =~ /\S/
+ && grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('ignore_disposition'));
+
+ return "carrierid NOT IN ( ". $self->option_cacheable('use_carrierid'). " )"
+ if $self->option_cacheable('use_carrierid') =~ /\S/
+ && !grep { $cdr->carrierid eq $_ } split(/\s*,\s*/, $self->option_cacheable('use_carrierid')); #eq otherwise 0 matches ''
+
+ # unlike everything else, use_cdrtypenum is applied in FS::svc_x::get_cdrs.
+ return "cdrtypenum != ". $self->option_cacheable('use_cdrtypenum')
+ if length($self->option_cacheable('use_cdrtypenum'))
+ && $cdr->cdrtypenum ne $self->option_cacheable('use_cdrtypenum'); #ne otherwise 0 matches ''
+
+ return "cdrtypenum == ". $self->option_cacheable('ignore_cdrtypenum')
+ if length($self->option_cacheable('ignore_cdrtypenum'))
+ && $cdr->cdrtypenum eq $self->option_cacheable('ignore_cdrtypenum'); #eq otherwise 0 matches ''
+
+ # unlike everything else, use_calltypenum is applied in FS::svc_x::get_cdrs.
+ return "calltypenum != ". $self->option_cacheable('use_calltypenum')
+ if length($self->option_cacheable('use_calltypenum'))
+ && $cdr->calltypenum ne $self->option_cacheable('use_calltypenum'); #ne otherwise 0 matches ''
+
+ return "calltypenum == ". $self->option_cacheable('ignore_calltypenum')
+ if length($self->option_cacheable('ignore_calltypenum'))
+ && $cdr->calltypenum eq $self->option_cacheable('ignore_calltypenum'); #eq otherwise 0 matches ''
- return "dcontext IN ( $opt{'skip_dcontext'} )"
- if $opt{'skip_dcontext'} =~ /\S/
- && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $opt{'skip_dcontext'});
+ return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )"
+ if $self->option_cacheable('skip_dcontext') =~ /\S/
+ && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext'));
- my $len_prefix = length($opt{'skip_dstchannel_prefix'});
- return "dstchannel starts with $opt{'skip_dstchannel_prefix'}"
+ my $len_prefix = length($self->option_cacheable('skip_dstchannel_prefix'));
+ return "dstchannel starts with ". $self->option_cacheable('skip_dstchannel_prefix')
if $len_prefix
- && substr($cdr->dstchannel,0,$len_prefix) eq $opt{'skip_dstchannel_prefix'};
+ && substr($cdr->dstchannel,0,$len_prefix) eq $self->option_cacheable('skip_dstchannel_prefix');
- my $dst_length = $opt{'skip_dst_length_less'};
+ my $dst_length = $self->option_cacheable('skip_dst_length_less');
return "destination less than $dst_length digits"
if $dst_length && length($cdr->dst) < $dst_length;
- return "lastapp is $opt{'skip_lastapp'}"
- if length($opt{'skip_lastapp'}) && $cdr->lastapp eq $opt{'skip_lastapp'};
+ return "lastapp is ". $self->option_cacheable('skip_lastapp')
+ if length($self->option_cacheable('skip_lastapp')) && $cdr->lastapp eq $self->option_cacheable('skip_lastapp');
#all right then, rate it
'';
diff --git a/FS/FS/part_pkg_msgcat.pm b/FS/FS/part_pkg_msgcat.pm
new file mode 100644
index 000000000..7c00c26ac
--- /dev/null
+++ b/FS/FS/part_pkg_msgcat.pm
@@ -0,0 +1,138 @@
+package FS::part_pkg_msgcat;
+
+use strict;
+use base qw( FS::Record );
+use FS::Locales;
+#use FS::Record qw( qsearch qsearchs );
+use FS::part_pkg;
+
+=head1 NAME
+
+FS::part_pkg_msgcat - Object methods for part_pkg_msgcat records
+
+=head1 SYNOPSIS
+
+ use FS::part_pkg_msgcat;
+
+ $record = new FS::part_pkg_msgcat \%hash;
+ $record = new FS::part_pkg_msgcat { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_msgcat object represents localized labels of a package
+definition. FS::part_pkg_msgcat inherits from FS::Record. The following
+fields are currently supported:
+
+=over 4
+
+=item pkgpartmsgnum
+
+primary key
+
+=item pkgpart
+
+Package definition
+
+=item locale
+
+locale
+
+=item pkg
+
+Localized package name (customer-viewable)
+
+=item comment
+
+Localized package comment (non-customer-viewable), optional
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'part_pkg_msgcat'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('pkgpartmsgnum')
+ || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart')
+ || $self->ut_enum('locale', [ FS::Locales->locales ] )
+ || $self->ut_text('pkg')
+ || $self->ut_textn('comment')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg_taxrate.pm b/FS/FS/part_pkg_taxrate.pm
index c83f700d9..a73272040 100644
--- a/FS/FS/part_pkg_taxrate.pm
+++ b/FS/FS/part_pkg_taxrate.pm
@@ -5,8 +5,7 @@ use vars qw( @ISA );
use Date::Parse;
use DateTime;
use DateTime::Format::Strptime;
-use FS::UID qw(dbh);
-use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( qsearch qsearchs dbh );
use FS::part_pkg_taxproduct;
use FS::Misc qw(csv_from_fixed);
@@ -310,8 +309,8 @@ sub batch_import {
}
}
- my $part_pkg_taxrate = qsearchs('part_pkg_taxrate', $hash);
- unless ( $part_pkg_taxrate ) {
+ my @part_pkg_taxrate = qsearch('part_pkg_taxrate', $hash);
+ unless ( scalar(@part_pkg_taxrate) || $param->{'delete_only'} ) {
if ( $hash->{taxproductnum} ) {
my $taxproduct =
qsearchs( 'part_pkg_taxproduct',
@@ -324,8 +323,10 @@ sub batch_import {
join(" ", map { "$_ => *". $hash->{$_}. '*' } keys(%$hash) );
}
- my $error = $part_pkg_taxrate->delete;
- return $error if $error;
+ foreach my $part_pkg_taxrate (@part_pkg_taxrate) {
+ my $error = $part_pkg_taxrate->delete;
+ return $error if $error;
+ }
delete($hash->{$_}) foreach (keys %$hash);
}
diff --git a/FS/FS/pay_batch/BoM.pm b/FS/FS/pay_batch/BoM.pm
index a3708d477..b609df351 100644
--- a/FS/FS/pay_batch/BoM.pm
+++ b/FS/FS/pay_batch/BoM.pm
@@ -59,7 +59,7 @@ $name = 'BoM';
footer => sub {
my ($pay_batch, $batchcount, $batchtotal) = @_;
sprintf( "YD%08u%014.0f%55s\n", $batchcount, $batchtotal*100, ""). #80
- sprintf( "Z%014u%05u%014u%05u%40s", #80 now
+ sprintf( "Z%014.0f%05u%014u%05u%40s", #80 now
$batchtotal*100, $batchcount, "0", "0", "");
},
);
diff --git a/FS/FS/pay_batch/nacha.pm b/FS/FS/pay_batch/nacha.pm
index d0758f4b6..c069082c7 100644
--- a/FS/FS/pay_batch/nacha.pm
+++ b/FS/FS/pay_batch/nacha.pm
@@ -47,7 +47,7 @@ $DEBUG = 0;
my $origin = $1;
my $company = $conf->config('company_name', $pay_batch->agentnum);
- $company = substr($company. (' 'x23), 0, 23);
+ $company = substr(uc($company). (' 'x23), 0, 23);
my $now = time;
diff --git a/FS/FS/payby.pm b/FS/FS/payby.pm
index d1961a58d..e223a050f 100644
--- a/FS/FS/payby.pm
+++ b/FS/FS/payby.pm
@@ -208,6 +208,7 @@ sub longname {
'CARD' => 'CC',
'CHEK' => 'ECHECK',
'MCRD' => 'CC',
+ 'PPAL' => 'PAYPAL',
);
sub payby2bop {
diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm
index 9879a3abd..82632526d 100644
--- a/FS/FS/payinfo_Mixin.pm
+++ b/FS/FS/payinfo_Mixin.pm
@@ -44,26 +44,18 @@ For Refunds (cust_refund):
For Payments (cust_pay):
'CARD' (credit cards), 'CHEK' (electronic check/ACH),
'LECB' (phone bill billing), 'BILL' (billing), 'PREP' (prepaid card),
-'CASH' (cash), 'WEST' (Western Union), or 'MCRD' (Manual credit card)
+'CASH' (cash), 'WEST' (Western Union), 'MCRD' (Manual credit card),
+'PPAL' (PayPal)
'COMP' (free) is depricated as a payment type in cust_pay
=cut
-# was this supposed to do something?
-
-#sub payby {
-# my($self,$payby) = @_;
-# if ( defined($payby) ) {
-# $self->setfield('payby', $payby);
-# }
-# return $self->getfield('payby')
-#}
-
=item payinfo
Payment information (payinfo) can be one of the following types:
-Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L)
+Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username)
+prepayment identifier (see L), PayPal transaction ID
=cut
@@ -267,6 +259,8 @@ sub payby_payinfo_pretty {
'Western Union'; #. $self->payinfo;
} elsif ( $self->payby eq 'MCRD' ) {
'Manual credit card'; #. $self->payinfo;
+ } elsif ( $self->payby eq 'PPAL' ) {
+ 'PayPal transaction#' . $self->order_number;
} else {
$self->payby. ' '. $self->payinfo;
}
diff --git a/FS/FS/payinfo_transaction_Mixin.pm b/FS/FS/payinfo_transaction_Mixin.pm
index 093891e93..50659ac1e 100644
--- a/FS/FS/payinfo_transaction_Mixin.pm
+++ b/FS/FS/payinfo_transaction_Mixin.pm
@@ -73,10 +73,7 @@ sub _parse_paybatch {
my $payment_gateway =
qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
- die "payment gateway $gatewaynum not found" #?
- unless $payment_gateway;
-
- $processor = $payment_gateway->gateway_module;
+ $processor = $payment_gateway->gateway_module if $payment_gateway;
}
diff --git a/FS/FS/payment_gateway.pm b/FS/FS/payment_gateway.pm
index 4a7585e24..e94a62cf4 100644
--- a/FS/FS/payment_gateway.pm
+++ b/FS/FS/payment_gateway.pm
@@ -41,7 +41,7 @@ currently supported:
=item gateway_namespace - Business::OnlinePayment, Business::OnlineThirdPartyPayment, or Business::BatchPayment
-=item gateway_module - Business::OnlinePayment:: module name
+=item gateway_module - Business::OnlinePayment:: (or other) module name
=item gateway_username - payment gateway username
@@ -51,6 +51,14 @@ currently supported:
=item disabled - Disabled flag, empty or 'Y'
+=item gateway_callback_url - For ThirdPartyPayment only, set to the URL that
+the user should be redirected to on a successful payment. This will be sent
+as a transaction parameter (named "callback_url").
+
+=item gateway_cancel_url - For ThirdPartyPayment only, set to the URL that
+the user should be redirected to if they cancel the transaction. PayPal
+requires this; other gateways ignore it.
+
=item auto_resolve_status - For BatchPayment only, set to 'approve' to
auto-approve unresolved payments after some number of days, 'reject' to
auto-decline them, or null to do nothing.
@@ -128,6 +136,7 @@ sub check {
|| $self->ut_textn('gateway_username')
|| $self->ut_anything('gateway_password')
|| $self->ut_textn('gateway_callback_url') # a bit too permissive
+ || $self->ut_textn('gateway_cancel_url')
|| $self->ut_enum('disabled', [ '', 'Y' ] )
|| $self->ut_enum('auto_resolve_status', [ '', 'approve', 'reject' ])
|| $self->ut_numbern('auto_resolve_days')
@@ -152,8 +161,8 @@ sub check {
}
# this little kludge mimics FS::CGI::popurl
- $self->gateway_callback_url($self->gateway_callback_url. '/')
- if ( $self->gateway_callback_url && $self->gateway_callback_url !~ /\/$/ );
+ #$self->gateway_callback_url($self->gateway_callback_url. '/')
+ # if ( $self->gateway_callback_url && $self->gateway_callback_url !~ /\/$/ );
$self->SUPER::check;
}
diff --git a/FS/FS/reason.pm b/FS/FS/reason.pm
index a9a7d745d..e6b20db8f 100644
--- a/FS/FS/reason.pm
+++ b/FS/FS/reason.pm
@@ -139,6 +139,43 @@ sub reasontype {
=back
+=head1 CLASS METHODS
+
+=over 4
+
+=item new_or_existing reason => REASON, type => TYPE, class => CLASS
+
+Fetches the reason matching these parameters if there is one. If not,
+inserts one. Will also insert the reason type if necessary. CLASS must
+be one of 'C' (cancel reasons), 'R' (credit reasons), or 'S' (suspend reasons).
+
+This will die if anything fails.
+
+=cut
+
+sub new_or_existing {
+ my $class = shift;
+ my %opt = @_;
+
+ my $error = '';
+ my %hash = ('class' => $opt{'class'}, 'type' => $opt{'type'});
+ my $reason_type = qsearchs('reason_type', \%hash)
+ || FS::reason_type->new(\%hash);
+
+ $error = $reason_type->insert unless $reason_type->typenum;
+ die "error inserting reason type: $error\n" if $error;
+
+ %hash = ('reason_type' => $reason_type->typenum, 'reason' => $opt{'reason'});
+ my $reason = qsearchs('reason', \%hash)
+ || FS::reason->new(\%hash);
+
+ $error = $reason->insert unless $reason->reasonnum;
+ die "error inserting reason: $error\n" if $error;
+
+ $reason;
+}
+
+
=head1 BUGS
Here by termintes. Don't use on wooden computers.
diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm
index e36dbbd69..26d6e5b72 100644
--- a/FS/FS/svc_acct.pm
+++ b/FS/FS/svc_acct.pm
@@ -1895,12 +1895,14 @@ sub email {
$self->username. '@'. $self->domain(@_);
}
+
=item acct_snarf
Returns an array of FS::acct_snarf records associated with the account.
=cut
+# unused as originally intended, but now by Communigate Pro "RPOP"
sub acct_snarf {
my $self = shift;
qsearch({
diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm
index 01495ca39..002aa55ce 100755
--- a/FS/FS/svc_broadband.pm
+++ b/FS/FS/svc_broadband.pm
@@ -103,10 +103,10 @@ sub table_info {
'ip_field' => 'ip_addr',
'fields' => {
'svcnum' => 'Service',
- 'description' => 'Descriptive label for this particular device',
- 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
- 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
- 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
+ 'description' => 'Descriptive label',
+ 'speed_down' => 'Download speed (Kbps)',
+ 'speed_up' => 'Upload speed (Kbps)',
+ 'ip_addr' => 'IP address',
'blocknum' =>
{ 'label' => 'Address block',
'type' => 'select',
@@ -134,6 +134,15 @@ sub table_info {
disable_inventory => 1,
multiple => 1,
},
+ 'radio_serialnum' => 'Radio Serial Number',
+ 'radio_location' => 'Radio Location',
+ 'poe_location' => 'POE Location',
+ 'rssi' => 'RSSI',
+ 'suid' => 'SUID',
+ 'shared_svcnum' => { label => 'Shared Service',
+ type => 'search-svc_broadband',
+ disable_inventory => 1,
+ },
},
};
}
@@ -225,15 +234,31 @@ sub search_sql {
my( $class, $string ) = @_;
if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
$class->search_sql_field('ip_addr', $string );
- }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
+ } elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
$class->search_sql_field('mac_addr', uc($string));
- }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
+ } elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
$class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
+ } elsif ( $string =~ /^(\d+)$/ ) {
+ my $table = $class->table;
+ "$table.svcnum = $1";
} else {
'1 = 0'; #false
}
}
+=item smart_search STRING
+
+=cut
+
+sub smart_search {
+ my( $class, $string ) = @_;
+ qsearch({
+ 'table' => $class->table, #'svc_broadband',
+ 'hashref' => {},
+ 'extra_sql' => 'WHERE '. $class->search_sql($string),
+ });
+}
+
=item label
Returns the IP address.
@@ -330,6 +355,12 @@ sub check {
|| $self->ut_sfloatn('altitude')
|| $self->ut_textn('vlan_profile')
|| $self->ut_textn('plan_id')
+ || $self->ut_alphan('radio_serialnum')
+ || $self->ut_textn('radio_location')
+ || $self->ut_textn('poe_location')
+ || $self->ut_snumbern('rssi')
+ || $self->ut_numbern('suid')
+ || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
;
return $error if $error;
diff --git a/FS/FS/svc_export_machine.pm b/FS/FS/svc_export_machine.pm
index 10f7b6821..7ca20ccb6 100644
--- a/FS/FS/svc_export_machine.pm
+++ b/FS/FS/svc_export_machine.pm
@@ -40,6 +40,10 @@ fields are currently supported:
primary key
+=item exportnum
+
+Export definition, see L
+
=item svcnum
Customer service, see L
diff --git a/FS/FS/svc_hardware.pm b/FS/FS/svc_hardware.pm
index 96502e41e..b28cc9ef5 100644
--- a/FS/FS/svc_hardware.pm
+++ b/FS/FS/svc_hardware.pm
@@ -105,9 +105,13 @@ sub search_sql {
my ($class, $string) = @_;
my @where = ();
- my $ip = NetAddr::IP->new($string);
- if ( $ip ) {
- push @where, $class->search_sql_field('ip_addr', $ip->addr);
+ if ( $string =~ /^[\d\.:]+$/ ) {
+ # if the string isn't an IP address, this will waste several seconds
+ # attempting a DNS lookup. so try to filter those out.
+ my $ip = NetAddr::IP->new($string);
+ if ( $ip ) {
+ push @where, $class->search_sql_field('ip_addr', $ip->addr);
+ }
}
if ( $string =~ /^(\w+)$/ ) {
diff --git a/FS/FS/svc_pbx.pm b/FS/FS/svc_pbx.pm
index 4182a1315..66e51da71 100644
--- a/FS/FS/svc_pbx.pm
+++ b/FS/FS/svc_pbx.pm
@@ -292,7 +292,9 @@ to allow title to indicate a range of IP addresses.
=item begin, end: Start and end of date range, as unix timestamp.
-=item cdrtypenum: Only return CDRs with this type number.
+=item cdrtypenum: Only return CDRs with this type.
+
+=item calltypenum: Only return CDRs with this call type.
=back
@@ -310,6 +312,9 @@ sub psearch_cdrs {
if ($options{'cdrtypenum'}) {
$hash{'cdrtypenum'} = $options{'cdrtypenum'};
}
+ if ($options{'calltypenum'}) {
+ $hash{'calltypenum'} = $options{'calltypenum'};
+ }
my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
diff --git a/FS/FS/svc_phone.pm b/FS/FS/svc_phone.pm
index f28002cc4..bab8537bb 100644
--- a/FS/FS/svc_phone.pm
+++ b/FS/FS/svc_phone.pm
@@ -288,9 +288,8 @@ sub insert {
#false laziness w/cust_pkg.pm... move this to location_Mixin? that would
#make it more of a base class than a mixin... :)
- if ( $options{'cust_location'}
- && ( ! $self->locationnum || $self->locationnum == -1 ) ) {
- my $error = $options{'cust_location'}->insert;
+ if ( $options{'cust_location'} ) {
+ my $error = $options{'cust_location'}->find_or_insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "inserting cust_location (transaction rolled back): $error";
@@ -684,7 +683,9 @@ with the chosen prefix.
=item begin, end: Start and end of a date range, as unix timestamp.
-=item cdrtypenum: Only return CDRs with this type number.
+=item cdrtypenum: Only return CDRs with this type.
+
+=item calltypenum: Only return CDRs with this call type.
=item disable_src => 1: Only match on "charged_party", not "src".
@@ -735,6 +736,9 @@ sub psearch_cdrs {
if ($options{'cdrtypenum'}) {
$hash{'cdrtypenum'} = $options{'cdrtypenum'};
}
+ if ($options{'calltypenum'}) {
+ $hash{'calltypenum'} = $options{'calltypenum'};
+ }
my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
diff --git a/FS/FS/tax_class.pm b/FS/FS/tax_class.pm
index bfec2c06c..d68e7e30c 100644
--- a/FS/FS/tax_class.pm
+++ b/FS/FS/tax_class.pm
@@ -5,6 +5,8 @@ use vars qw( @ISA );
use FS::UID qw(dbh);
use FS::Record qw( qsearch qsearchs );
use FS::Misc qw( csv_from_fixed );
+use FS::part_pkg_taxrate;
+use FS::part_pkg_taxoverride;
@ISA = qw(FS::Record);
@@ -83,20 +85,53 @@ Delete this record from the database.
sub delete {
my $self = shift;
- return "Can't delete a tax class which has tax rates!"
- if qsearch( 'tax_rate', { 'taxclassnum' => $self->taxclassnum } );
-
- return "Can't delete a tax class which has package tax rates!"
- if qsearch( 'part_pkg_taxrate', { 'taxclassnum' => $self->taxclassnum } );
-
return "Can't delete a tax class which has package tax rates!"
if qsearch( 'part_pkg_taxrate', { 'taxclassnumtaxed' => $self->taxclassnum } );
return "Can't delete a tax class which has package tax overrides!"
if qsearch( 'part_pkg_taxoverride', { 'taxclassnum' => $self->taxclassnum } );
- $self->SUPER::delete(@_);
-
+ 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;
+
+ foreach my $tax_rate (
+ qsearch( 'tax_rate', { taxclassnum=>$self->taxclassnum } )
+ ) {
+ my $error = $tax_rate->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ foreach my $part_pkg_taxrate (
+ qsearch( 'part_pkg_taxrate', { taxclassnum=>$self->taxclassnum } )
+ ) {
+ my $error = $part_pkg_taxrate->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ my $error = $self->SUPER::delete(@_);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ '';
+
}
=item replace OLD_RECORD
@@ -253,14 +288,23 @@ sub batch_import {
}
}
- my $tax_class =
- new FS::tax_class( { 'data_vendor' => 'cch',
- 'taxclass' => $type->[0].':'.$cat->[0],
- 'description' => $type->[1].':'.$cat->[1],
- } );
- my $error = $tax_class->insert;
- return $error if $error;
+ my %hash = ( 'data_vendor' => 'cch',
+ 'taxclass' => $type->[0].':'.$cat->[0],
+ 'description' => $type->[1].':'.$cat->[1],
+ );
+ unless ( qsearchs('tax_class', \%hash) ) {
+ my $tax_class = new FS::tax_class \%hash;
+ my $error = $tax_class->insert;
+
+ return "can't insert tax_class for ".
+ " old TAXTYPE ". $type->[0].':'.$type->[1].
+ " and new TAXCAT ". $cat->[0].':'. $cat->[1].
+ " : $error"
+ if $error;
+ }
+
$imported++;
+
}
}
@@ -283,7 +327,7 @@ sub batch_import {
'description' => $type->[1].':'.$cat->[1],
} );
my $error = $tax_class->insert;
- return $error if $error;
+ return "can't insert tax_class for new TAXTYPE $type and TAXCAT $cat: $error" if $error;
$imported++;
}
}
@@ -363,7 +407,7 @@ sub batch_import {
my $error = &{$endhook}();
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "can't insert tax_class for $line: $error";
+ return "can't run end hook: $error";
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -378,9 +422,6 @@ sub batch_import {
=head1 BUGS
- batch_import does not handle mixed I and D records in the same file for
- format cch-update
-
=head1 SEE ALSO
L, schema.html from the base documentation.
diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm
index a5a623d94..342c7cb0b 100644
--- a/FS/FS/tax_rate.pm
+++ b/FS/FS/tax_rate.pm
@@ -413,7 +413,7 @@ sub taxline {
}
my $maxtype = $self->maxtype || 0;
- if ($maxtype != 0 && $maxtype != 9) {
+ if ($maxtype != 0 && $maxtype != 1 && $maxtype != 9) {
return $self->_fatal_or_null( 'tax with "'.
$self->maxtype_name. '" threshold'
);
@@ -476,12 +476,12 @@ sub taxline {
}
- #
- # XXX insert exemption handling here
+ # XXX handle excessrate (use_excessrate) / excessfee /
+ # taxbase/feebase / taxmax/feemax
+ # and eventually exemptions
#
# the tax or fee is applied to taxbase or feebase and then
# the excessrate or excess fee is applied to taxmax or feemax
- #
$amount += $taxable_charged * $self->tax;
$amount += $taxable_units * $self->fee;
@@ -785,7 +785,8 @@ sub batch_import {
}
- for (grep { !exists($delete{$_}) } keys %insert) {
+ my @replace = grep { exists($delete{$_}) } keys %insert;
+ for (@replace) {
if ( $job ) { # progress bar
if ( time - $min_sec > $last ) {
my $error = $job->update_statustext(
@@ -799,20 +800,35 @@ sub batch_import {
}
}
- my $tax_rate = new FS::tax_rate( $insert{$_} );
- my $error = $tax_rate->insert;
+ my $old = qsearchs( 'tax_rate', $delete{$_} );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- my $hashref = $insert{$_};
- $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
- return "can't insert tax_rate for $line: $error";
+ if ( $old ) {
+
+ my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => '' });
+ $new->taxnum($old->taxnum);
+ my $error = $new->replace($old);
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ my $hashref = $insert{$_};
+ $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
+ return "can't replace tax_rate for $line: $error";
+ }
+
+ $imported++;
+
+ } else {
+
+ $old = delete $delete{$_};
+ warn "WARNING: can't find tax_rate to replace (inserting instead and continuing) for: ".
+ #join(" ", map { "$_ => ". $old->{$_} } @fields);
+ join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
}
$imported++;
}
- for (grep { exists($delete{$_}) } keys %insert) {
+ for (grep { !exists($delete{$_}) } keys %insert) {
if ( $job ) { # progress bar
if ( time - $min_sec > $last ) {
my $error = $job->update_statustext(
@@ -826,27 +842,17 @@ sub batch_import {
}
}
- my $old = qsearchs( 'tax_rate', $delete{$_} );
- unless ($old) {
- $dbh->rollback if $oldAutoCommit;
- $old = $delete{$_};
- return "can't find tax_rate to replace for: ".
- #join(" ", map { "$_ => ". $old->{$_} } @fields);
- join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
- }
- my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => '' });
- $new->taxnum($old->taxnum);
- my $error = $new->replace($old);
+ my $tax_rate = new FS::tax_rate( $insert{$_} );
+ my $error = $tax_rate->insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
my $hashref = $insert{$_};
$line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
- return "can't replace tax_rate for $line: $error";
+ return "can't insert tax_rate for $line: $error";
}
$imported++;
- $imported++;
}
for (grep { !exists($insert{$_}) } keys %delete) {
@@ -961,7 +967,7 @@ sub _perform_batch_import {
my $file = lc($name). 'file';
unless ($files{$file}) {
- $error = "No $name supplied";
+ #$error = "No $name supplied";
next;
}
next if $name eq 'DETAIL' && $format =~ /update/;
@@ -978,7 +984,7 @@ sub _perform_batch_import {
unlink $filename or warn "Can't delete $filename: $!"
unless $keep_cch_files;
push @insert_list, $name, $insertname, $import_sub, $format;
- if ( $name eq 'GEOCODE' ) { #handle this whole ordering issue better
+ if ( $name eq 'GEOCODE' || $name eq 'CODE' ) { #handle this whole ordering issue better
unshift @predelete_list, $name, $deletename, $import_sub, $format;
} else {
unshift @delete_list, $name, $deletename, $import_sub, $format;
@@ -996,10 +1002,17 @@ sub _perform_batch_import {
'DETAIL', "$dir/".$files{detailfile}, \&FS::tax_rate::batch_import, $format
if $format =~ /update/;
+ my %addl_param = ();
+ if ( $param->{'delete_only'} ) {
+ $addl_param{'delete_only'} = $param->{'delete_only'};
+ @insert_list = ()
+ }
+
$error ||= _perform_cch_tax_import( $job,
[ @predelete_list ],
[ @insert_list ],
[ @delete_list ],
+ \%addl_param,
);
@@ -1024,7 +1037,8 @@ sub _perform_batch_import {
sub _perform_cch_tax_import {
- my ( $job, $predelete_list, $insert_list, $delete_list ) = @_;
+ my ( $job, $predelete_list, $insert_list, $delete_list, $addl_param ) = @_;
+ $addl_param ||= {};
my $error = '';
foreach my $list ($predelete_list, $insert_list, $delete_list) {
@@ -1033,7 +1047,11 @@ sub _perform_cch_tax_import {
my $fmt = "$format-update";
$fmt = $format. ( lc($name) eq 'zip' ? '-zip' : '' );
open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
- $error ||= &{$method}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
+ my $param = { 'filehandle' => $fh,
+ 'format' => $fmt,
+ %$addl_param,
+ };
+ $error ||= &{$method}($param, $job);
close $fh;
}
}
diff --git a/FS/MANIFEST b/FS/MANIFEST
index d2b7013a4..ee184071e 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -494,6 +494,8 @@ FS/phone_type.pm
t/phone_type.t
FS/contact_email.pm
t/contact_email.t
+FS/contact_Mixin.pm
+t/contact_Mixin.t
FS/prospect_main.pm
t/prospect_main.t
FS/o2m_Common.pm
@@ -690,5 +692,7 @@ 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
FS/access_user_session.pm
t/access_user_session.t
diff --git a/FS/bin/freeside-cdr-sftp_and_import b/FS/bin/freeside-cdr-sftp_and_import
index c37ff11d6..a7452e87f 100755
--- a/FS/bin/freeside-cdr-sftp_and_import
+++ b/FS/bin/freeside-cdr-sftp_and_import
@@ -12,8 +12,8 @@ use FS::cdr;
# parse command line
###
-use vars qw( $opt_m $opt_p $opt_r $opt_e $opt_d $opt_v $opt_P $opt_a $opt_c $opt_g );
-getopts('c:m:p:r:e:d:v:P:ag');
+use vars qw( $opt_m $opt_p $opt_r $opt_e $opt_d $opt_v $opt_P $opt_a $opt_c $opt_g $opt_s );
+getopts('c:m:p:r:e:d:v:P:ags');
$opt_e ||= 'csv';
#$opt_e = ".$opt_e" unless $opt_e =~ /^\./;
@@ -116,31 +116,39 @@ foreach my $filename ( @$ls ) {
$import_options->{'cdrtypenum'} = $opt_c if $opt_c;
my $error = FS::cdr::batch_import($import_options);
+
if ( $error ) {
- unlink "$cachedir/$filename";
- unlink "$cachedir/$ungziped" if $opt_g;
- die $error;
- }
- if ( $opt_d ) {
- if($opt_m eq 'ftp') {
- my $ftp = ftp();
- $ftp->rename($filename, "$opt_d/$file_timestamp")
- or do {
- unlink "$cachedir/$filename";
- unlink "$cachedir/$ungziped" if $opt_g;
- die "Can't move $filename to $opt_d: ".$ftp->message . "\n";
- };
+ if ( $opt_s ) {
+ warn "$ungziped: $error\n";
+ } else {
+ unlink "$cachedir/$filename";
+ unlink "$cachedir/$ungziped" if $opt_g;
+ die $error;
}
- else {
- my $sftp = sftp();
- $sftp->rename($filename, "$opt_d/$file_timestamp")
- or do {
- unlink "$cachedir/$filename";
- unlink "$cachedir/$ungziped" if $opt_g;
- die "can't move $filename to $opt_d: ". $sftp->error . "\n";
- };
+
+ } else {
+
+ if ( $opt_d ) {
+ if ( $opt_m eq 'ftp') {
+ my $ftp = ftp();
+ $ftp->rename($filename, "$opt_d/$file_timestamp")
+ or do {
+ unlink "$cachedir/$filename";
+ unlink "$cachedir/$ungziped" if $opt_g;
+ die "Can't move $filename to $opt_d: ".$ftp->message . "\n";
+ };
+ } else {
+ my $sftp = sftp();
+ $sftp->rename($filename, "$opt_d/$file_timestamp")
+ or do {
+ unlink "$cachedir/$filename";
+ unlink "$cachedir/$ungziped" if $opt_g;
+ die "can't move $filename to $opt_d: ". $sftp->error . "\n";
+ };
+ }
}
+
}
unlink "$cachedir/$filename";
@@ -192,7 +200,7 @@ freeside-cdr-sftp_and_import - Download CDR files from a remote server via SFTP
cdr.sftp_and_import [ -m method ] [ -p prefix ] [ -e extension ]
[ -r remotefolder ] [ -d donefolder ] [ -v level ] [ -P port ]
- [ -a ] [ -c cdrtypenum ] user format [sftpuser@]servername
+ [ -a ] [ -g ] [ -s ] [ -c cdrtypenum ] user format [sftpuser@]servername
=head1 DESCRIPTION
@@ -220,6 +228,8 @@ or FTP and then import them into the database.
-g: File is gzipped
+-s: Warn and skip files which could not be imported rather than abort
+
user: freeside username
format: CDR format name
diff --git a/FS/bin/freeside-queued b/FS/bin/freeside-queued
index 2fd80255e..dcc6ac4ba 100644
--- a/FS/bin/freeside-queued
+++ b/FS/bin/freeside-queued
@@ -212,8 +212,10 @@ while (1) {
# don't put @args in the log, may expose passwords
$log->info('starting job ('.$ljob->job.')');
warn 'running "&'. $ljob->job. '('. join(', ', @args). ")\n" if $DEBUG;
+ local $FS::UID::AutoCommit = 0; # so that we can clean up failures
eval $eval; #throw away return value? suppose so
if ( $@ ) {
+ dbh->rollback;
my %hash = $ljob->hash;
$hash{'statustext'} = $@;
if ( $hash{'statustext'} =~ /\/misc\/queued_report/ ) { #use return?
@@ -225,8 +227,10 @@ while (1) {
my $fjob = new FS::queue( \%hash );
my $error = $fjob->replace($ljob);
die $error if $error;
+ dbh->commit; # for the status change only
} else {
$ljob->delete;
+ dbh->commit; # for the job itself
}
if ( UNIVERSAL::can(dbh, 'sprintProfile') ) {
diff --git a/FS/bin/freeside-upgrade b/FS/bin/freeside-upgrade
index 399c11994..5bd141538 100755
--- a/FS/bin/freeside-upgrade
+++ b/FS/bin/freeside-upgrade
@@ -123,6 +123,8 @@ my $cf;
while ( $cf = $cfsth->fetchrow_hashref ) {
my $tbl = $cf->{'dbtable'};
my $name = $cf->{'name'};
+ $name = lc($name) unless driver_name =~ /^mysql/i;
+
@statements = grep { $_ !~ /^\s*ALTER\s+TABLE\s+(h_|)$tbl\s+DROP\s+COLUMN\s+cf_$name\s*$/i }
@statements;
push @statements,
diff --git a/FS/t/contact_Mixin.t b/FS/t/contact_Mixin.t
new file mode 100644
index 000000000..89dcc37c5
--- /dev/null
+++ b/FS/t/contact_Mixin.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::contact_Mixin;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_pkg_msgcat.t b/FS/t/part_pkg_msgcat.t
new file mode 100644
index 000000000..541c16799
--- /dev/null
+++ b/FS/t/part_pkg_msgcat.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg_msgcat;
+$loaded=1;
+print "ok 1\n";
diff --git a/README b/README
index a2babf317..e68c06ffc 100644
--- a/README
+++ b/README
@@ -1,7 +1,7 @@
Freeside is a billing and administration package for Internet Service
Providers, VoIP providers and other online businesses.
-Copyright (C) 2005-2011 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
Additional copyright holders may be found in the docs/license.html file.
diff --git a/bin/3add b/bin/3add
new file mode 100755
index 000000000..8bc034d9c
--- /dev/null
+++ b/bin/3add
@@ -0,0 +1,19 @@
+#!/usr/bin/perl
+
+use Cwd;
+use String::ShellQuote;
+
+my $USER = $ENV{USER};
+
+my $dir = getcwd;
+( my $prefix = $dir ) =~ s(^/home/$USER/freeside/?)() or die $dir; #eventually from anywhere
+
+system join('',
+ "git add @ARGV ; ",
+ "( for file in @ARGV; do ",
+ "cp -i \$file /home/$USER/freeside3/$prefix/`dirname \$file`;",
+ "done ) && ",
+ "cd /home/$USER/freeside3/$prefix/ && ",
+ "git add @ARGV"
+);
+
diff --git a/bin/3commit b/bin/3commit
new file mode 100755
index 000000000..cd1db2174
--- /dev/null
+++ b/bin/3commit
@@ -0,0 +1,26 @@
+#!/usr/bin/perl
+
+# usage: 23commit 'log message' filename filename ...
+
+use Cwd;
+use String::ShellQuote;
+
+my $USER = $ENV{USER};
+
+my $dir = getcwd;
+( my $prefix = $dir ) =~ s(^/home/$USER/freeside/?)() or die $dir; #eventually from anywhere
+
+my $desc = shell_quote(shift @ARGV); # -m
+
+die "no files!" unless @ARGV;
+
+#warn "$prefix";
+
+#print <update_statustext( "0,Comparing to previous $name" );
+ #die $error if $error;
+ warn "processing $dir.new/$name.txt\n"; # if $DEBUG;
+ #my $olddir = $update ? "$dir.1" : "";
+ my $olddir = "$dir.1";
+ $difffile = FS::tax_rate::_perform_cch_diff( $name, "$dir.new", $olddir );
+ }
+ $difffile =~ s/^$cache_dir//;
+ push @list, "${name}file:$difffile";
+}
+
+# perform the import
+local $FS::tax_rate::keep_cch_files = 1;
+my $param = {
+ 'format' => 'cch-update',
+ 'uploaded_files' => join( ',', @list ),
+};
+my $error =
+ #_perform_batch_import( $job, encode_base64( nfreeze( $param ) ) );
+ FS::tax_rate::_perform_batch_import( '', encode_base64( nfreeze( $param ) ) );
+
+if ( $error ) {
+ warn "ERROR: $error\n";
+} else {
+ warn "success!\n";
+}
+
+#XXX do this manually
+#rename "$dir.new", "$dir"
+# or die "cch tax update processed, but can't rename $dir.new: $!\n";
+
diff --git a/bin/cch.redelete b/bin/cch.redelete
new file mode 100644
index 000000000..2cff389ad
--- /dev/null
+++ b/bin/cch.redelete
@@ -0,0 +1,52 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Storable qw( thaw nfreeze );
+use MIME::Base64;
+use FS::UID qw( adminsuidsetup );
+use FS::tax_rate;
+
+adminsuidsetup(shift);
+
+#my @namelist = qw( code detail geocode plus4 txmatrix zip );
+my @namelist = qw( plus4 txmatrix zip );
+
+my $cache_dir = '/usr/local/etc/freeside/cache.'. $FS::UID::datasrc. '/';
+my $dir = $cache_dir.'taxdata/cch';
+
+my @list = ();
+foreach my $name ( @namelist ) {
+ my $difffile = "$dir.new/$name.txt";
+ if (1) { # ($update) {
+ #my $error = $job->update_statustext( "0,Comparing to previous $name" );
+ #die $error if $error;
+ warn "processing $dir.new/$name.txt\n"; # if $DEBUG;
+ #my $olddir = $update ? "$dir.1" : "";
+ my $olddir = "$dir.1";
+ $difffile = FS::tax_rate::_perform_cch_diff( $name, "$dir.new", $olddir );
+ }
+ $difffile =~ s/^$cache_dir//;
+ push @list, "${name}file:$difffile";
+}
+
+# perform the import
+local $FS::tax_rate::keep_cch_files = 1;
+my $param = {
+ 'format' => 'cch-update',
+ 'uploaded_files' => join( ',', @list ),
+ 'delete_only' => 1,
+};
+my $error =
+ #_perform_batch_import( $job, encode_base64( nfreeze( $param ) ) );
+ FS::tax_rate::_perform_batch_import( '', encode_base64( nfreeze( $param ) ) );
+
+if ( $error ) {
+ warn "ERROR: $error\n";
+} else {
+ warn "success!\n";
+}
+
+#XXX do this manually
+#rename "$dir.new", "$dir"
+# or die "cch tax update processed, but can't rename $dir.new: $!\n";
+
diff --git a/conf/invoice_html b/conf/invoice_html
index 567385b06..cd348274f 100644
--- a/conf/invoice_html
+++ b/conf/invoice_html
@@ -132,8 +132,8 @@
$OUT .= '