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::'$a'>' ../FS.pm >/dev/null || echo "missing $a" ; done
L<FS::part_pkg> - Package definition class
+L<FS::part_pkg_msgcat> - Package definition localization class
+
L<FS::part_pkg_link> - Package definition link class
L<FS::part_pkg_taxclass> - Tax class class
'Order customer package',
'One-time charge',
'Change customer package',
+ 'Detach customer package',
'Bulk change customer packages',
'Edit customer package dates',
'Discount customer package', #NEW
'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 },
],
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
push @history, {
'type' => 'Line item',
- 'description' => $_->desc. ( $_->sdate && $_->edate
- ? ' '. time2str('%d-%b-%Y', $_->sdate).
- ' To '. time2str('%d-%b-%Y', $_->edate)
- : ''
- ),
+ 'description' => $_->desc( $cust_main->locale ).
+ ( $_->sdate && $_->edate
+ ? ' '. time2str('%d-%b-%Y', $_->sdate).
+ ' To '. time2str('%d-%b-%Y', $_->edate)
+ : ''
+ ),
'amount' => sprintf('%.2f', $_->setup + $_->recur ),
'date' => $cust_bill->_date,
'date_pretty' => time2str('%m/%d/%Y', $cust_bill->_date ),
my $primary_cust_svc = $_->primary_cust_svc;
+{ $_->hash,
$_->part_pkg->hash,
- pkg_label => $_->pkg_label,
+ pkg_label => $_->pkg_locale,
status => $_->status,
part_svc =>
[ map { $_->hashref }
'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'),
);
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 );
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;
}
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,
+ };
+ }
}
'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',
],
},
'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) ],
},
{
{
'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).',
'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) ],
},
{
'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) ],
},
{
'type' => 'checkbox',
},
+ {
+ 'key' => 'fuzzy-fuzziness',
+ 'section' => 'UI',
+ 'description' => 'Set the "fuzziness" of fuzzy searching (see the String::Approx manpage for details). Defaults to 10%',
+ 'type' => 'text',
+ },
+
{ 'key' => 'pkg_referral',
'section' => '',
'description' => 'Enable package-specific advertising sources.',
},
{
+ 'key' => '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.',
},
{
+ '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?
use HTML::TableExtract qw(tree);
use HTML::FormatText;
use HTML::Defang;
- use JSON;
+ use JSON::XS;
# use XMLRPC::Transport::HTTP;
# use XMLRPC::Lite; # for XMLRPC::Serializer
use MIME::Base64;
use FS::cust_credit;
use FS::cust_credit_bill;
use FS::cust_main;
+ use FS::h_cust_main;
use FS::cust_main::Search qw(smart_search);
use FS::cust_main::Import;
use FS::cust_main_county;
use FS::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 ) {
# 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 = '';
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 );
}
=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 = (
'custnum', 'int', '', '', '', '',
'pkgpart', 'int', '', '', '', '',
'pkgbatch', 'varchar', 'NULL', $char_d, '', '',
+ 'contactnum', 'int', 'NULL', '', '', '',
'locationnum', 'int', 'NULL', '', '', '',
'otaker', 'varchar', 'NULL', 32, '', '',
'usernum', 'int', 'NULL', '', '', '',
'change_pkgnum', 'int', 'NULL', '', '', '',
'change_pkgpart', 'int', 'NULL', '', '', '',
'change_locationnum', 'int', 'NULL', '', '', '',
+ 'change_custnum', 'int', 'NULL', '', '', '',
'main_pkgnum', 'int', 'NULL', '', '', '',
'pkglinknum', 'int', 'NULL', '', '', '',
'manual_flag', 'char', 'NULL', 1, '', '',
],
},
+ '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', '', '', '', '',
'columns' => [
'exportnum', 'serial', '', '', '', '',
'exportname', 'varchar', 'NULL', $char_d, '', '',
- 'machine', 'varchar', 'NULL', $char_d, '', '',
+ 'machine', 'varchar', 'NULL', $char_d, '', '',
'exporttype', 'varchar', '', $char_d, '', '',
'nodomain', 'char', 'NULL', 1, '', '',
+ 'default_machine','int', 'NULL', '', '', '',
],
'primary_key' => 'exportnum',
'unique' => [],
'svc_broadband' => {
'columns' => [
- 'svcnum', 'int', '', '', '', '',
- 'description', 'varchar', 'NULL', $char_d, '', '',
- 'routernum', 'int', 'NULL', '', '', '',
- 'blocknum', 'int', 'NULL', '', '', '',
- 'sectornum', 'int', 'NULL', '', '', '',
- 'speed_up', 'int', 'NULL', '', '', '',
- 'speed_down', 'int', 'NULL', '', '', '',
- 'ip_addr', 'varchar', 'NULL', 15, '', '',
- 'mac_addr', 'varchar', 'NULL', 12, '', '',
- 'authkey', 'varchar', 'NULL', 32, '', '',
- 'latitude', 'decimal', 'NULL', '10,7', '', '',
- 'longitude', 'decimal', 'NULL', '10,7', '', '',
- 'altitude', 'decimal', 'NULL', '', '', '',
- 'vlan_profile', 'varchar', 'NULL', $char_d, '', '',
- 'performance_profile', 'varchar', 'NULL', $char_d, '', '',
- 'plan_id', 'varchar', 'NULL', $char_d, '', '',
+ 'svcnum', 'int', '', '', '', '',
+ 'description', 'varchar', 'NULL', $char_d, '', '',
+ 'routernum', 'int', 'NULL', '', '', '',
+ 'blocknum', 'int', 'NULL', '', '', '',
+ 'sectornum', 'int', 'NULL', '', '', '',
+ 'speed_up', 'int', 'NULL', '', '', '',
+ 'speed_down', 'int', 'NULL', '', '', '',
+ 'ip_addr', 'varchar', 'NULL', 15, '', '',
+ 'mac_addr', 'varchar', 'NULL', 12, '', '',
+ 'authkey', 'varchar', 'NULL', 32, '', '',
+ 'latitude', 'decimal', 'NULL', '10,7', '', '',
+ 'longitude', 'decimal', 'NULL', '10,7', '', '',
+ 'altitude', 'decimal', 'NULL', '', '', '',
+ 'vlan_profile', 'varchar', 'NULL', $char_d, '', '',
+ 'performance_profile', 'varchar', 'NULL', $char_d, '', '',
+ 'plan_id', 'varchar', 'NULL', $char_d, '', '',
+ 'radio_serialnum', 'varchar', 'NULL', $char_d, '', '',
+ 'radio_location', 'varchar', 'NULL', 2*$char_d, '', '',
+ 'poe_location', 'varchar', 'NULL', 2*$char_d, '', '',
+ 'rssi', 'int', 'NULL', '', '', '',
+ 'suid', 'int', 'NULL', '', '', '',
+ 'shared_svcnum', 'int', 'NULL', '', '', '',
],
'primary_key' => 'svcnum',
'unique' => [ [ 'ip_addr' ], [ 'mac_addr' ] ],
'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',
=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/;
# 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 = '';
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 = ();
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'};
}
- if ( $taxtotal ) {
+ if ( @items_tax ) {
my $total = {};
$total->{'total_item'} = $self->mt('Sub-total');
$total->{'total_amount'} =
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;
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;
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
my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
# and location labels
+ my $locale = $cust_main->locale;
my @b = ();
my ($s, $r, $u) = ( undef, undef, undef );
my $type = $display->type;
- my $desc = $cust_bill_pkg->desc;
+ my $desc = $cust_bill_pkg->desc( $cust_main->locale );
$desc = substr($desc, 0, $maxlength). '...'
if $format eq 'latex' && length($desc) > $maxlength;
my $unlinked_warn = 0;
return map {
my $f = $_;
- if( $unlinked_warn++ ) {
+ if ( $unlinked_warn++ ) {
+
sub {
my $record = shift;
- if( $record->custnum ) {
- $record->$f(@_);
- }
- else {
+ if ( $record->custnum ) {
+ encode_entities( $record->$f(@_) );
+ } else {
'(unlinked)'
};
- }
- }
- else {
+ };
+
+ } else {
+
sub {
my $record = shift;
- $record->$f(@_) if $record->custnum;
- }
+ $record->custnum ? encode_entities( $record->$f(@_) ) : '';
+ };
+
}
+
} @cust_fields;
}
use Carp;
use Storable qw(nfreeze);
use MIME::Base64;
-use JSON;
+use JSON::XS;
use FS::CurrentUser;
use FS::Record qw(qsearchs);
use FS::queue;
@return = ( 'error', $job ? $job->statustext : $jobnum );
}
- #to_json(\@return); #waiting on deb 5.0 for new JSON.pm?
- #silence the warning though
- my $to_json = JSON->can('to_json') || JSON->can('objToJson');
- &$to_json(\@return);
+ encode_json \@return;
}
#insert default tower_sector if not present
'tower' => [],
+ #repair improperly deleted services
+ 'cust_svc' => [],
+
#routernum/blocknum
'svc_broadband' => [],
'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 ) {
the invoice.
The I<method> and I<payinfo> 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<method> is 'CC' then the card number in I<payinfo> can direct
this routine to route to a gateway suited for that type of card.
}
#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 =
use Time::Local;
#use Data::Dumper;
+#false laziness w/huawei_softx3000.pm
%TZ = (
'+0000' => 'XXX-0',
'+0100' => 'XXX-1',
--- /dev/null
+package FS::cdr::huawei_softx3000;
+use base qw( FS::cdr );
+
+use strict;
+use vars qw( %info %TZ );
+use subs qw( ts24008_number TimeStamp );
+use Time::Local;
+use FS::Record qw( qsearch );
+use FS::cdr_calltype;
+
+#false laziness w/gsm_tap3_12.pm
+%TZ = (
+ '+0000' => 'XXX-0',
+ '+0100' => 'XXX-1',
+ '+0200' => 'XXX-2',
+ '+0300' => 'XXX-3',
+ '+0400' => 'XXX-4',
+ '+0500' => 'XXX-5',
+ '+0600' => 'XXX-6',
+ '+0700' => 'XXX-7',
+ '+0800' => 'XXX-8',
+ '+0900' => 'XXX-9',
+ '+1000' => 'XXX-10',
+ '+1100' => 'XXX-11',
+ '+1200' => 'XXX-12',
+ '-0000' => 'XXX+0',
+ '-0100' => 'XXX+1',
+ '-0200' => 'XXX+2',
+ '-0300' => 'XXX+3',
+ '-0400' => 'XXX+4',
+ '-0500' => 'XXX+5',
+ '-0600' => 'XXX+6',
+ '-0700' => 'XXX+7',
+ '-0800' => 'XXX+8',
+ '-0900' => 'XXX+9',
+ '-1000' => 'XXX+10',
+ '-1100' => 'XXX+11',
+ '-1200' => 'XXX+12',
+);
+
+%info = (
+ 'name' => 'Huawei SoftX3000', #V100R006C05 ?
+ 'weight' => 160,
+ 'type' => 'asn.1',
+ 'import_fields' => [],
+ 'asn_format' => {
+ 'spec' => _asn_spec(),
+ 'macro' => 'CallEventDataFile',
+ 'header_buffer' => sub {
+ #my $CallEventDataFile = shift;
+
+ my %cdr_calltype = ( map { $_->calltypename => $_->calltypenum }
+ qsearch('cdr_calltype', {})
+ );
+
+ { cdr_calltype => \%cdr_calltype,
+ };
+
+ },
+ 'arrayref' => sub { shift->{'callEventRecords'} },
+ 'row_callback' => sub {
+ my( $row, $buffer ) = @_;
+ my @keys = keys %$row;
+ $buffer->{'key'} = $keys[0];
+ },
+ 'map' => {
+ 'src' => huawei_field('callingNumber', ts24008_number, ),
+
+ 'dst' => huawei_field('calledNumber', ts24008_number, ),
+
+ 'startdate' => huawei_field(['answerTime','deliveryTime'], TimeStamp),
+ 'answerdate' => huawei_field(['answerTime','deliveryTime'], TimeStamp),
+ 'enddate' => huawei_field('releaseTime', TimeStamp),
+ 'duration' => huawei_field('callDuration'),
+ 'billsec' => huawei_field('callDuration'),
+ #'disposition' => #diagnostics?
+ #'accountcode'
+ #'charged_party' => # 0 or 1, do something with this?
+ 'calltypenum' => sub {
+ my($rec, $buf) = @_;
+ my $key = $buf->{key};
+ $buf->{'cdr_calltype'}{ $key };
+ },
+ #'carrierid' =>
+ },
+
+ },
+);
+
+sub huawei_field {
+ my $field = shift;
+ my $decode = $_[0] ? shift : '';
+ return sub {
+ my($rec, $buf) = @_;
+
+ my $key = $buf->{key};
+
+ $field = ref($field) ? $field : [ $field ];
+ my $value = '';
+ foreach my $f (@$field) {
+ $value = $rec->{$key}{$f} and last;
+ }
+
+ $decode
+ ? &{ $decode }( $value )
+ : $value;
+
+ };
+}
+
+sub ts24008_number {
+ # This type contains the binary coded decimal representation of
+ # a directory number e.g. calling/called/connected/translated number.
+ # The encoding of the octet string is in accordance with the
+ # the elements "Calling party BCD number", "Called party BCD number"
+ # and "Connected number" defined in TS 24.008.
+ # This encoding includes type of number and number plan information
+ # together with a BCD encoded digit string.
+ # It may also contain both a presentation and screening indicator
+ # (octet 3a).
+ # For the avoidance of doubt, this field does not include
+ # octets 1 and 2, the element name and length, as this would be
+ # redundant.
+ #
+ #type id (per TS 24.008 page 490):
+ # low nybble: "numbering plan identification"
+ # high nybble: "type of number"
+ # 0 unknown
+ # 1 international
+ # 2 national
+ # 3 network specific
+ # 4 dedicated access, short code
+ # 5 reserved
+ # 6 reserved
+ # 7 reserved for extension
+ # (bit 8 "extension")
+ return sub {
+ my( $type_id, $value ) = unpack 'Ch*', shift;
+ $value =~ s/f$//; # If the called party BCD number contains an odd number
+ # of digits, bits 5 to 8 of the last octet shall be
+ # filled with an end mark coded as "1111".
+ $value;
+ };
+}
+
+sub TimeStamp {
+ # The contents of this field are a compact form of the UTCTime format
+ # containing local time plus an offset to universal time. Binary coded
+ # decimal encoding is employed for the digits to reduce the storage and
+ # transmission overhead
+ # e.g. YYMMDDhhmmssShhmm
+ # where
+ # YY = Year 00 to 99 BCD encoded
+ # MM = Month 01 to 12 BCD encoded
+ # DD = Day 01 to 31 BCD encoded
+ # hh = hour 00 to 23 BCD encoded
+ # mm = minute 00 to 59 BCD encoded
+ # ss = second 00 to 59 BCD encoded
+ # S = Sign 0 = "+", "-" ASCII encoded
+ # hh = hour 00 to 23 BCD encoded
+ # mm = minute 00 to 59 BCD encoded
+ return sub {
+ my($year, $mon, $day, $hour, $min, $sec, $tz_sign, $tz_hour, $tz_min, $dst)=
+ unpack 'H2H2H2H2H2H2AH2H2C', shift;
+ #warn "$year/$mon/$day $hour:$min:$sec $tz_sign$tz_hour$tz_min $dst\n";
+ return 0 unless $year; #y2100 bug
+ local($ENV{TZ}) = $TZ{ "$tz_sign$tz_hour$tz_min" };
+ timelocal($sec, $min, $hour, $day, $mon-1, $year);
+ };
+}
+
+sub _asn_spec {
+ <<'END';
+
+--DEFINITIONS IMPLICIT TAGS ::=
+
+--BEGIN
+
+--------------------------------------------------------------------------------
+--
+-- CALL AND EVENT RECORDS
+--
+------------------------------------------------------------------------------
+--Font: verdana 8
+
+CallEventRecord ::= CHOICE
+{
+ moCallRecord [0] MOCallRecord,
+ mtCallRecord [1] MTCallRecord,
+ roamingRecord [2] RoamingRecord,
+ incGatewayRecord [3] IncGatewayRecord,
+ outGatewayRecord [4] OutGatewayRecord,
+ transitRecord [5] TransitCallRecord,
+ moSMSRecord [6] MOSMSRecord,
+ mtSMSRecord [7] MTSMSRecord,
+ ssActionRecord [10] SSActionRecord,
+ hlrIntRecord [11] HLRIntRecord,
+ commonEquipRecord [14] CommonEquipRecord,
+ recTypeExtensions [15] ManagementExtensions,
+ termCAMELRecord [16] TermCAMELRecord,
+ mtLCSRecord [17] MTLCSRecord,
+ moLCSRecord [18] MOLCSRecord,
+ niLCSRecord [19] NILCSRecord,
+ forwardCallRecord [100] MOCallRecord
+}
+
+MOCallRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedIMEI [2] IMEI OPTIONAL,
+ servedMSISDN [3] MSISDN OPTIONAL,
+ callingNumber [4] CallingNumber OPTIONAL,
+ calledNumber [5] CalledNumber OPTIONAL,
+ translatedNumber [6] TranslatedNumber OPTIONAL,
+ connectedNumber [7] ConnectedNumber OPTIONAL,
+ roamingNumber [8] RoamingNumber OPTIONAL,
+ recordingEntity [9] RecordingEntity OPTIONAL,
+ mscIncomingROUTE [10] ROUTE OPTIONAL,
+ mscOutgoingROUTE [11] ROUTE OPTIONAL,
+ location [12] LocationAreaAndCell OPTIONAL,
+ changeOfLocation [13] SEQUENCE OF LocationChange OPTIONAL,
+ basicService [14] BasicServiceCode OPTIONAL,
+ transparencyIndicator [15] TransparencyInd OPTIONAL,
+ changeOfService [16] SEQUENCE OF ChangeOfService OPTIONAL,
+ supplServicesUsed [17] SEQUENCE OF SuppServiceUsed OPTIONAL,
+ aocParameters [18] AOCParameters OPTIONAL,
+ changeOfAOCParms [19] SEQUENCE OF AOCParmChange OPTIONAL,
+ msClassmark [20] Classmark OPTIONAL,
+ changeOfClassmark [21] ChangeOfClassmark OPTIONAL,
+ seizureTime [22] TimeStamp OPTIONAL,
+ answerTime [23] TimeStamp OPTIONAL,
+ releaseTime [24] TimeStamp OPTIONAL,
+ callDuration [25] CallDuration OPTIONAL,
+ radioChanRequested [27] RadioChanRequested OPTIONAL,
+ radioChanUsed [28] TrafficChannel OPTIONAL,
+ changeOfRadioChan [29] ChangeOfRadioChannel OPTIONAL,
+ causeForTerm [30] CauseForTerm OPTIONAL,
+ diagnostics [31] Diagnostics OPTIONAL,
+ callReference [32] CallReference OPTIONAL,
+ sequenceNumber [33] SequenceNumber OPTIONAL,
+ additionalChgInfo [34] AdditionalChgInfo OPTIONAL,
+ recordExtensions [35] ManagementExtensions OPTIONAL,
+ gsm-SCFAddress [36] Gsm-SCFAddress OPTIONAL,
+ serviceKey [37] ServiceKey OPTIONAL,
+ networkCallReference [38] NetworkCallReference OPTIONAL,
+ mSCAddress [39] MSCAddress OPTIONAL,
+ cAMELInitCFIndicator [40] CAMELInitCFIndicator OPTIONAL,
+ defaultCallHandling [41] DefaultCallHandling OPTIONAL,
+ fnur [45] Fnur OPTIONAL,
+ aiurRequested [46] AiurRequested OPTIONAL,
+ speechVersionSupported [49] SpeechVersionIdentifier OPTIONAL,
+ speechVersionUsed [50] SpeechVersionIdentifier OPTIONAL,
+ numberOfDPEncountered [51] INTEGER OPTIONAL,
+ levelOfCAMELService [52] LevelOfCAMELService OPTIONAL,
+ freeFormatData [53] FreeFormatData OPTIONAL,
+ cAMELCallLegInformation [54] SEQUENCE OF CAMELInformation OPTIONAL,
+ freeFormatDataAppend [55] BOOLEAN OPTIONAL,
+ defaultCallHandling-2 [56] DefaultCallHandling OPTIONAL,
+ gsm-SCFAddress-2 [57] Gsm-SCFAddress OPTIONAL,
+ serviceKey-2 [58] ServiceKey OPTIONAL,
+ freeFormatData-2 [59] FreeFormatData OPTIONAL,
+ freeFormatDataAppend-2 [60] BOOLEAN OPTIONAL,
+ systemType [61] SystemType OPTIONAL,
+ rateIndication [62] RateIndication OPTIONAL,
+ partialRecordType [69] PartialRecordType OPTIONAL,
+ guaranteedBitRate [70] GuaranteedBitRate OPTIONAL,
+ maximumBitRate [71] MaximumBitRate OPTIONAL,
+ modemType [139] ModemType OPTIONAL,
+ classmark3 [140] Classmark3 OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ callingChargeAreaCode [145] ChargeAreaCode OPTIONAL,
+ calledChargeAreaCode [146] ChargeAreaCode OPTIONAL,
+ mscOutgoingCircuit [166] MSCCIC OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ callEmlppPriority [170] EmlppPriority OPTIONAL,
+ callerDefaultEmlppPriority [171] EmlppPriority OPTIONAL,
+ eaSubscriberInfo [174] EASubscriberInfo OPTIONAL,
+ selectedCIC [175] SelectedCIC OPTIONAL,
+ optimalRoutingFlag [177] NULL OPTIONAL,
+ optimalRoutingLateForwardFlag [178] NULL OPTIONAL,
+ optimalRoutingEarlyForwardFlag [179] NULL OPTIONAL,
+ portedflag [180] PortedFlag OPTIONAL,
+ calledIMSI [181] IMSI OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ changeOfglobalAreaID [189] SEQUENCE OF ChangeOfglobalAreaID OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ intermediatemccmnc [193] MCCMNC OPTIONAL,
+ lastmccmnc [194] MCCMNC OPTIONAL,
+ cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL,
+ cUGInterlockCode [196] CUGInterlockCode OPTIONAL,
+ cUGOutgoingAccessUsed [197] CUGOutgoingAccessUsed OPTIONAL,
+ cUGIndex [198] CUGIndex OPTIONAL,
+ interactionWithIP [199] InteractionWithIP OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ setupTime [201] TimeStamp OPTIONAL,
+ alertingTime [202] TimeStamp OPTIONAL,
+ voiceIndicator [203] VoiceIndicator OPTIONAL,
+ bCategory [204] BCategory OPTIONAL,
+ callType [205] CallType OPTIONAL
+}
+
+--at moc callingNumber is the same as served msisdn except basic msisdn != calling number such as MSP service
+
+MTCallRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedIMEI [2] IMEI OPTIONAL,
+ servedMSISDN [3] CalledNumber OPTIONAL,
+ callingNumber [4] CallingNumber OPTIONAL,
+ connectedNumber [5] ConnectedNumber OPTIONAL,
+ recordingEntity [6] RecordingEntity OPTIONAL,
+ mscIncomingROUTE [7] ROUTE OPTIONAL,
+ mscOutgoingROUTE [8] ROUTE OPTIONAL,
+ location [9] LocationAreaAndCell OPTIONAL,
+ changeOfLocation [10] SEQUENCE OF LocationChange OPTIONAL,
+ basicService [11] BasicServiceCode OPTIONAL,
+ transparencyIndicator [12] TransparencyInd OPTIONAL,
+ changeOfService [13] SEQUENCE OF ChangeOfService OPTIONAL,
+ supplServicesUsed [14] SEQUENCE OF SuppServiceUsed OPTIONAL,
+ aocParameters [15] AOCParameters OPTIONAL,
+ changeOfAOCParms [16] SEQUENCE OF AOCParmChange OPTIONAL,
+ msClassmark [17] Classmark OPTIONAL,
+ changeOfClassmark [18] ChangeOfClassmark OPTIONAL,
+ seizureTime [19] TimeStamp OPTIONAL,
+ answerTime [20] TimeStamp OPTIONAL,
+ releaseTime [21] TimeStamp OPTIONAL,
+ callDuration [22] CallDuration OPTIONAL,
+ radioChanRequested [24] RadioChanRequested OPTIONAL,
+ radioChanUsed [25] TrafficChannel OPTIONAL,
+ changeOfRadioChan [26] ChangeOfRadioChannel OPTIONAL,
+ causeForTerm [27] CauseForTerm OPTIONAL,
+ diagnostics [28] Diagnostics OPTIONAL,
+ callReference [29] CallReference OPTIONAL,
+ sequenceNumber [30] SequenceNumber OPTIONAL,
+ additionalChgInfo [31] AdditionalChgInfo OPTIONAL,
+ recordExtensions [32] ManagementExtensions OPTIONAL,
+ networkCallReference [33] NetworkCallReference OPTIONAL,
+ mSCAddress [34] MSCAddress OPTIONAL,
+ fnur [38] Fnur OPTIONAL,
+ aiurRequested [39] AiurRequested OPTIONAL,
+ speechVersionSupported [42] SpeechVersionIdentifier OPTIONAL,
+ speechVersionUsed [43] SpeechVersionIdentifier OPTIONAL,
+ gsm-SCFAddress [44] Gsm-SCFAddress OPTIONAL,
+ serviceKey [45] ServiceKey OPTIONAL,
+ systemType [46] SystemType OPTIONAL,
+ rateIndication [47] RateIndication OPTIONAL,
+ partialRecordType [54] PartialRecordType OPTIONAL,
+ guaranteedBitRate [55] GuaranteedBitRate OPTIONAL,
+ maximumBitRate [56] MaximumBitRate OPTIONAL,
+ initialCallAttemptFlag [137] NULL OPTIONAL,
+ ussdCallBackFlag [138] NULL OPTIONAL,
+ modemType [139] ModemType OPTIONAL,
+ classmark3 [140] Classmark3 OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ callingChargeAreaCode [145]ChargeAreaCode OPTIONAL,
+ calledChargeAreaCode [146]ChargeAreaCode OPTIONAL,
+ defaultCallHandling [150] DefaultCallHandling OPTIONAL,
+ freeFormatData [151] FreeFormatData OPTIONAL,
+ freeFormatDataAppend [152] BOOLEAN OPTIONAL,
+ numberOfDPEncountered [153] INTEGER OPTIONAL,
+ levelOfCAMELService [154] LevelOfCAMELService OPTIONAL,
+ roamingNumber [160] RoamingNumber OPTIONAL,
+ mscIncomingCircuit [166] MSCCIC OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ callEmlppPriority [170] EmlppPriority OPTIONAL,
+ calledDefaultEmlppPriority [171] EmlppPriority OPTIONAL,
+ eaSubscriberInfo [174] EASubscriberInfo OPTIONAL,
+ selectedCIC [175] SelectedCIC OPTIONAL,
+ optimalRoutingFlag [177] NULL OPTIONAL,
+ portedflag [180] PortedFlag OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ changeOfglobalAreaID [189] SEQUENCE OF ChangeOfglobalAreaID OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ intermediatemccmnc [193] MCCMNC OPTIONAL,
+ lastmccmnc [194] MCCMNC OPTIONAL,
+ cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL,
+ cUGInterlockCode [196] CUGInterlockCode OPTIONAL,
+ cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL,
+ cUGIndex [198] CUGIndex OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ redirectingnumber [201] RedirectingNumber OPTIONAL,
+ redirectingcounter [202] RedirectingCounter OPTIONAL,
+ setupTime [203] TimeStamp OPTIONAL,
+ alertingTime [204] TimeStamp OPTIONAL,
+ calledNumber [205] CalledNumber OPTIONAL,
+ voiceIndicator [206] VoiceIndicator OPTIONAL,
+ bCategory [207] BCategory OPTIONAL,
+ callType [208] CallType OPTIONAL
+}
+
+RoamingRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedMSISDN [2] MSISDN OPTIONAL,
+ callingNumber [3] CallingNumber OPTIONAL,
+ roamingNumber [4] RoamingNumber OPTIONAL,
+ recordingEntity [5] RecordingEntity OPTIONAL,
+ mscIncomingROUTE [6] ROUTE OPTIONAL,
+ mscOutgoingROUTE [7] ROUTE OPTIONAL,
+ basicService [8] BasicServiceCode OPTIONAL,
+ transparencyIndicator [9] TransparencyInd OPTIONAL,
+ changeOfService [10] SEQUENCE OF ChangeOfService OPTIONAL,
+ supplServicesUsed [11] SEQUENCE OF SuppServiceUsed OPTIONAL,
+ seizureTime [12] TimeStamp OPTIONAL,
+ answerTime [13] TimeStamp OPTIONAL,
+ releaseTime [14] TimeStamp OPTIONAL,
+ callDuration [15] CallDuration OPTIONAL,
+ causeForTerm [17] CauseForTerm OPTIONAL,
+ diagnostics [18] Diagnostics OPTIONAL,
+ callReference [19] CallReference OPTIONAL,
+ sequenceNumber [20] SequenceNumber OPTIONAL,
+ recordExtensions [21] ManagementExtensions OPTIONAL,
+ networkCallReference [22] NetworkCallReference OPTIONAL,
+ mSCAddress [23] MSCAddress OPTIONAL,
+ partialRecordType [30] PartialRecordType OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ callingChargeAreaCode [145] ChargeAreaCode OPTIONAL,
+ calledChargeAreaCode [146] ChargeAreaCode OPTIONAL,
+ mscOutgoingCircuit [166] MSCCIC OPTIONAL,
+ mscIncomingCircuit [167] MSCCIC OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ callEmlppPriority [170] EmlppPriority OPTIONAL,
+ eaSubscriberInfo [174] EASubscriberInfo OPTIONAL,
+ selectedCIC [175] SelectedCIC OPTIONAL,
+ optimalRoutingFlag [177] NULL OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL,
+ cUGInterlockCode [196] CUGInterlockCode OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL
+}
+
+TermCAMELRecord ::= SET
+{
+ recordtype [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedMSISDN [2] MSISDN OPTIONAL,
+ recordingEntity [3] RecordingEntity OPTIONAL,
+ interrogationTime [4] TimeStamp OPTIONAL,
+ destinationRoutingAddress [5] DestinationRoutingAddress OPTIONAL,
+ gsm-SCFAddress [6] Gsm-SCFAddress OPTIONAL,
+ serviceKey [7] ServiceKey OPTIONAL,
+ networkCallReference [8] NetworkCallReference OPTIONAL,
+ mSCAddress [9] MSCAddress OPTIONAL,
+ defaultCallHandling [10] DefaultCallHandling OPTIONAL,
+ recordExtensions [11] ManagementExtensions OPTIONAL,
+ calledNumber [12] CalledNumber OPTIONAL,
+ callingNumber [13] CallingNumber OPTIONAL,
+ mscIncomingROUTE [14] ROUTE OPTIONAL,
+ mscOutgoingROUTE [15] ROUTE OPTIONAL,
+ seizureTime [16] TimeStamp OPTIONAL,
+ answerTime [17] TimeStamp OPTIONAL,
+ releaseTime [18] TimeStamp OPTIONAL,
+ callDuration [19] CallDuration OPTIONAL,
+ causeForTerm [21] CauseForTerm OPTIONAL,
+ diagnostics [22] Diagnostics OPTIONAL,
+ callReference [23] CallReference OPTIONAL,
+ sequenceNumber [24] SequenceNumber OPTIONAL,
+ numberOfDPEncountered [25] INTEGER OPTIONAL,
+ levelOfCAMELService [26] LevelOfCAMELService OPTIONAL,
+ freeFormatData [27] FreeFormatData OPTIONAL,
+ cAMELCallLegInformation [28] SEQUENCE OF CAMELInformation OPTIONAL,
+ freeFormatDataAppend [29] BOOLEAN OPTIONAL,
+ mscServerIndication [30] BOOLEAN OPTIONAL,
+ defaultCallHandling-2 [31] DefaultCallHandling OPTIONAL,
+ gsm-SCFAddress-2 [32] Gsm-SCFAddress OPTIONAL,
+ serviceKey-2 [33] ServiceKey OPTIONAL,
+ freeFormatData-2 [34] FreeFormatData OPTIONAL,
+ freeFormatDataAppend-2 [35] BOOLEAN OPTIONAL,
+ partialRecordType [42] PartialRecordType OPTIONAL,
+ basicService [130] BasicServiceCode OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL
+}
+
+IncGatewayRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ callingNumber [1] CallingNumber OPTIONAL,
+ calledNumber [2] CalledNumber OPTIONAL,
+ recordingEntity [3] RecordingEntity OPTIONAL,
+ mscIncomingROUTE [4] ROUTE OPTIONAL,
+ mscOutgoingROUTE [5] ROUTE OPTIONAL,
+ seizureTime [6] TimeStamp OPTIONAL,
+ answerTime [7] TimeStamp OPTIONAL,
+ releaseTime [8] TimeStamp OPTIONAL,
+ callDuration [9] CallDuration OPTIONAL,
+ causeForTerm [11] CauseForTerm OPTIONAL,
+ diagnostics [12] Diagnostics OPTIONAL,
+ callReference [13] CallReference OPTIONAL,
+ sequenceNumber [14] SequenceNumber OPTIONAL,
+ recordExtensions [15] ManagementExtensions OPTIONAL,
+ partialRecordType [22] PartialRecordType OPTIONAL,
+ iSDN-BC [23] ISDN-BC OPTIONAL,
+ lLC [24] LLC OPTIONAL,
+ hLC [25] HLC OPTIONAL,
+ basicService [130] BasicServiceCode OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ rateIndication [159] RateIndication OPTIONAL,
+ roamingNumber [160] RoamingNumber OPTIONAL,
+ mscIncomingCircuit [167] MSCCIC OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ callEmlppPriority [170] EmlppPriority OPTIONAL,
+ eaSubscriberInfo [174] EASubscriberInfo OPTIONAL,
+ selectedCIC [175] SelectedCIC OPTIONAL,
+ cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL,
+ cUGInterlockCode [196] CUGInterlockCode OPTIONAL,
+ cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL,
+ mscIncomingRouteAttribute [198] RouteAttribute OPTIONAL,
+ mscOutgoingRouteAttribute [199] RouteAttribute OPTIONAL,
+ networkCallReference [200] NetworkCallReference OPTIONAL,
+ setupTime [201] TimeStamp OPTIONAL,
+ alertingTime [202] TimeStamp OPTIONAL,
+ voiceIndicator [203] VoiceIndicator OPTIONAL,
+ bCategory [204] BCategory OPTIONAL,
+ callType [205] CallType OPTIONAL
+}
+
+OutGatewayRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ callingNumber [1] CallingNumber OPTIONAL,
+ calledNumber [2] CalledNumber OPTIONAL,
+ recordingEntity [3] RecordingEntity OPTIONAL,
+ mscIncomingROUTE [4] ROUTE OPTIONAL,
+ mscOutgoingROUTE [5] ROUTE OPTIONAL,
+ seizureTime [6] TimeStamp OPTIONAL,
+ answerTime [7] TimeStamp OPTIONAL,
+ releaseTime [8] TimeStamp OPTIONAL,
+ callDuration [9] CallDuration OPTIONAL,
+ causeForTerm [11] CauseForTerm OPTIONAL,
+ diagnostics [12] Diagnostics OPTIONAL,
+ callReference [13] CallReference OPTIONAL,
+ sequenceNumber [14] SequenceNumber OPTIONAL,
+ recordExtensions [15] ManagementExtensions OPTIONAL,
+ partialRecordType [22] PartialRecordType OPTIONAL,
+ basicService [130] BasicServiceCode OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ rateIndication [159] RateIndication OPTIONAL,
+ roamingNumber [160] RoamingNumber OPTIONAL,
+ mscOutgoingCircuit [166] MSCCIC OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ eaSubscriberInfo [174] EASubscriberInfo OPTIONAL,
+ selectedCIC [175] SelectedCIC OPTIONAL,
+ callEmlppPriority [170] EmlppPriority OPTIONAL,
+ cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL,
+ cUGInterlockCode [196] CUGInterlockCode OPTIONAL,
+ cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL,
+ mscIncomingRouteAttribute [198] RouteAttribute OPTIONAL,
+ mscOutgoingRouteAttribute [199] RouteAttribute OPTIONAL,
+ networkCallReference [200] NetworkCallReference OPTIONAL,
+ setupTime [201] TimeStamp OPTIONAL,
+ alertingTime [202] TimeStamp OPTIONAL,
+ voiceIndicator [203] VoiceIndicator OPTIONAL,
+ bCategory [204] BCategory OPTIONAL,
+ callType [205] CallType OPTIONAL
+}
+
+TransitCallRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ recordingEntity [1] RecordingEntity OPTIONAL,
+ mscIncomingROUTE [2] ROUTE OPTIONAL,
+ mscOutgoingROUTE [3] ROUTE OPTIONAL,
+ callingNumber [4] CallingNumber OPTIONAL,
+ calledNumber [5] CalledNumber OPTIONAL,
+ isdnBasicService [6] BasicService OPTIONAL,
+ seizureTime [7] TimeStamp OPTIONAL,
+ answerTime [8] TimeStamp OPTIONAL,
+ releaseTime [9] TimeStamp OPTIONAL,
+ callDuration [10] CallDuration OPTIONAL,
+ causeForTerm [12] CauseForTerm OPTIONAL,
+ diagnostics [13] Diagnostics OPTIONAL,
+ callReference [14] CallReference OPTIONAL,
+ sequenceNumber [15] SequenceNumber OPTIONAL,
+ recordExtensions [16] ManagementExtensions OPTIONAL,
+ partialRecordType [23] PartialRecordType OPTIONAL,
+ basicService [130] BasicServiceCode OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ originalCalledNumber [142] OriginalCalledNumber OPTIONAL,
+ rateIndication [159] RateIndication OPTIONAL,
+ mscOutgoingCircuit [166] MSCCIC OPTIONAL,
+ mscIncomingCircuit [167] MSCCIC OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ callEmlppPriority [170] EmlppPriority OPTIONAL,
+ eaSubscriberInfo [174] EASubscriberInfo OPTIONAL,
+ selectedCIC [175] SelectedCIC OPTIONAL,
+ cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL,
+ cUGInterlockCode [196] CUGInterlockCode OPTIONAL,
+ cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL,
+ mscIncomingRouteAttribute [198] RouteAttribute OPTIONAL,
+ mscOutgoingRouteAttribute [199] RouteAttribute OPTIONAL,
+ networkCallReference [200] NetworkCallReference OPTIONAL,
+ setupTime [201] TimeStamp OPTIONAL,
+ alertingTime [202] TimeStamp OPTIONAL,
+ voiceIndicator [203] VoiceIndicator OPTIONAL,
+ bCategory [204] BCategory OPTIONAL,
+ callType [205] CallType OPTIONAL
+}
+
+MOSMSRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedIMEI [2] IMEI OPTIONAL,
+ servedMSISDN [3] MSISDN OPTIONAL,
+ msClassmark [4] Classmark OPTIONAL,
+ serviceCentre [5] AddressString OPTIONAL,
+ recordingEntity [6] RecordingEntity OPTIONAL,
+ location [7] LocationAreaAndCell OPTIONAL,
+ messageReference [8] MessageReference OPTIONAL,
+ originationTime [9] TimeStamp OPTIONAL,
+ smsResult [10] SMSResult OPTIONAL,
+ recordExtensions [11] ManagementExtensions OPTIONAL,
+ destinationNumber [12] SmsTpDestinationNumber OPTIONAL,
+ cAMELSMSInformation [13] CAMELSMSInformation OPTIONAL,
+ systemType [14] SystemType OPTIONAL,
+ basicService [130] BasicServiceCode OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ classmark3 [140] Classmark3 OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ smsUserDataType [195] SmsUserDataType OPTIONAL,
+ smstext [196] SMSTEXT OPTIONAL,
+ maximumNumberOfSMSInTheConcatenatedSMS [197] MaximumNumberOfSMSInTheConcatenatedSMS OPTIONAL,
+ concatenatedSMSReferenceNumber [198] ConcatenatedSMSReferenceNumber OPTIONAL,
+ sequenceNumberOfTheCurrentSMS [199] SequenceNumberOfTheCurrentSMS OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ callReference [201] CallReference OPTIONAL
+}
+
+MTSMSRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ serviceCentre [1] AddressString OPTIONAL,
+ servedIMSI [2] IMSI OPTIONAL,
+ servedIMEI [3] IMEI OPTIONAL,
+ servedMSISDN [4] MSISDN OPTIONAL,
+ msClassmark [5] Classmark OPTIONAL,
+ recordingEntity [6] RecordingEntity OPTIONAL,
+ location [7] LocationAreaAndCell OPTIONAL,
+ deliveryTime [8] TimeStamp OPTIONAL,
+ smsResult [9] SMSResult OPTIONAL,
+ recordExtensions [10] ManagementExtensions OPTIONAL,
+ systemType [11] SystemType OPTIONAL,
+ cAMELSMSInformation [12] CAMELSMSInformation OPTIONAL,
+ basicService [130] BasicServiceCode OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ classmark3 [140] Classmark3 OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ smsUserDataType [195] SmsUserDataType OPTIONAL,
+ smstext [196] SMSTEXT OPTIONAL,
+ maximumNumberOfSMSInTheConcatenatedSMS [197] MaximumNumberOfSMSInTheConcatenatedSMS OPTIONAL,
+ concatenatedSMSReferenceNumber [198] ConcatenatedSMSReferenceNumber OPTIONAL,
+ sequenceNumberOfTheCurrentSMS [199] SequenceNumberOfTheCurrentSMS OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ origination [201] CallingNumber OPTIONAL,
+ callReference [202] CallReference OPTIONAL
+}
+
+HLRIntRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedMSISDN [2] MSISDN OPTIONAL,
+ recordingEntity [3] RecordingEntity OPTIONAL,
+ basicService [4] BasicServiceCode OPTIONAL,
+ routingNumber [5] RoutingNumber OPTIONAL,
+ interrogationTime [6] TimeStamp OPTIONAL,
+ numberOfForwarding [7] NumberOfForwarding OPTIONAL,
+ interrogationResult [8] HLRIntResult OPTIONAL,
+ recordExtensions [9] ManagementExtensions OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ callReference [169] CallReference OPTIONAL
+}
+
+SSActionRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ servedIMSI [1] IMSI OPTIONAL,
+ servedIMEI [2] IMEI OPTIONAL,
+ servedMSISDN [3] MSISDN OPTIONAL,
+ msClassmark [4] Classmark OPTIONAL,
+ recordingEntity [5] RecordingEntity OPTIONAL,
+ location [6] LocationAreaAndCell OPTIONAL,
+ basicServices [7] BasicServices OPTIONAL,
+ supplService [8] SS-Code OPTIONAL,
+ ssAction [9] SSActionType OPTIONAL,
+ ssActionTime [10] TimeStamp OPTIONAL,
+ ssParameters [11] SSParameters OPTIONAL,
+ ssActionResult [12] SSActionResult OPTIONAL,
+ callReference [13] CallReference OPTIONAL,
+ recordExtensions [14] ManagementExtensions OPTIONAL,
+ systemType [15] SystemType OPTIONAL,
+ ussdCodingScheme [126] UssdCodingScheme OPTIONAL,
+ ussdString [127] SEQUENCE OF UssdString OPTIONAL,
+ ussdNotifyCounter [128] UssdNotifyCounter OPTIONAL,
+ ussdRequestCounter [129] UssdRequestCounter OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ classmark3 [140] Classmark3 OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL
+}
+
+CommonEquipRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ equipmentType [1] EquipmentType OPTIONAL,
+ equipmentId [2] EquipmentId OPTIONAL,
+ servedIMSI [3] IMSI OPTIONAL,
+ servedMSISDN [4] MSISDN OPTIONAL,
+ recordingEntity [5] RecordingEntity OPTIONAL,
+ basicService [6] BasicServiceCode OPTIONAL,
+ changeOfService [7] SEQUENCE OF ChangeOfService OPTIONAL,
+ supplServicesUsed [8] SEQUENCE OF SuppServiceUsed OPTIONAL,
+ seizureTime [9] TimeStamp OPTIONAL,
+ releaseTime [10] TimeStamp OPTIONAL,
+ callDuration [11] CallDuration OPTIONAL,
+ callReference [12] CallReference OPTIONAL,
+ sequenceNumber [13] SequenceNumber OPTIONAL,
+ recordExtensions [14] ManagementExtensions OPTIONAL,
+ systemType [15] SystemType OPTIONAL,
+ rateIndication [16] RateIndication OPTIONAL,
+ fnur [17] Fnur OPTIONAL,
+ partialRecordType [18] PartialRecordType OPTIONAL,
+ causeForTerm [100] CauseForTerm OPTIONAL,
+ diagnostics [101] Diagnostics OPTIONAL,
+ servedIMEI [102] IMEI OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL
+}
+
+------------------------------------------------------------------------------
+--
+-- OBSERVED IMEI TICKETS
+--
+------------------------------------------------------------------------------
+
+ObservedIMEITicket ::= SET
+{
+ servedIMEI [0] IMEI,
+ imeiStatus [1] IMEIStatus,
+ servedIMSI [2] IMSI,
+ servedMSISDN [3] MSISDN OPTIONAL,
+ recordingEntity [4] RecordingEntity,
+ eventTime [5] TimeStamp,
+ location [6] LocationAreaAndCell,
+ imeiCheckEvent [7] IMEICheckEvent OPTIONAL,
+ callReference [8] CallReference OPTIONAL,
+ recordExtensions [9] ManagementExtensions OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL
+}
+
+
+
+------------------------------------------------------------------------------
+--
+-- LOCATION SERICE TICKETS
+--
+------------------------------------------------------------------------------
+
+MTLCSRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ recordingEntity [1] RecordingEntity OPTIONAL,
+ lcsClientType [2] LCSClientType OPTIONAL,
+ lcsClientIdentity [3] LCSClientIdentity OPTIONAL,
+ servedIMSI [4] IMSI OPTIONAL,
+ servedMSISDN [5] MSISDN OPTIONAL,
+ locationType [6] LocationType OPTIONAL,
+ lcsQos [7] LCSQoSInfo OPTIONAL,
+ lcsPriority [8] LCS-Priority OPTIONAL,
+ mlc-Number [9] ISDN-AddressString OPTIONAL,
+ eventTimeStamp [10] TimeStamp OPTIONAL,
+ measureDuration [11] CallDuration OPTIONAL,
+ notificationToMSUser [12] NotificationToMSUser OPTIONAL,
+ privacyOverride [13] NULL OPTIONAL,
+ location [14] LocationAreaAndCell OPTIONAL,
+ locationEstimate [15] Ext-GeographicalInformation OPTIONAL,
+ positioningData [16] PositioningData OPTIONAL,
+ lcsCause [17] LCSCause OPTIONAL,
+ diagnostics [18] Diagnostics OPTIONAL,
+ systemType [19] SystemType OPTIONAL,
+ recordExtensions [20] ManagementExtensions OPTIONAL,
+ causeForTerm [21] CauseForTerm OPTIONAL,
+ lcsReferenceNumber [101] CallReferenceNumber OPTIONAL,
+ servedIMEI [102] IMEI OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ callReference [201] CallReference OPTIONAL
+}
+
+MOLCSRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ recordingEntity [1] RecordingEntity OPTIONAL,
+ lcsClientType [2] LCSClientType OPTIONAL,
+ lcsClientIdentity [3] LCSClientIdentity OPTIONAL,
+ servedIMSI [4] IMSI OPTIONAL,
+ servedMSISDN [5] MSISDN OPTIONAL,
+ molr-Type [6] MOLR-Type OPTIONAL,
+ lcsQos [7] LCSQoSInfo OPTIONAL,
+ lcsPriority [8] LCS-Priority OPTIONAL,
+ mlc-Number [9] ISDN-AddressString OPTIONAL,
+ eventTimeStamp [10] TimeStamp OPTIONAL,
+ measureDuration [11] CallDuration OPTIONAL,
+ location [12] LocationAreaAndCell OPTIONAL,
+ locationEstimate [13] Ext-GeographicalInformation OPTIONAL,
+ positioningData [14] PositioningData OPTIONAL,
+ lcsCause [15] LCSCause OPTIONAL,
+ diagnostics [16] Diagnostics OPTIONAL,
+ systemType [17] SystemType OPTIONAL,
+ recordExtensions [18] ManagementExtensions OPTIONAL,
+ causeForTerm [19] CauseForTerm OPTIONAL,
+ lcsReferenceNumber [101] CallReferenceNumber OPTIONAL,
+ servedIMEI [102] IMEI OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ callReference [201] CallReference OPTIONAL
+}
+
+NILCSRecord ::= SET
+{
+ recordType [0] CallEventRecordType OPTIONAL,
+ recordingEntity [1] RecordingEntity OPTIONAL,
+ lcsClientType [2] LCSClientType OPTIONAL,
+ lcsClientIdentity [3] LCSClientIdentity OPTIONAL,
+ servedIMSI [4] IMSI OPTIONAL,
+ servedMSISDN [5] MSISDN OPTIONAL,
+ servedIMEI [6] IMEI OPTIONAL,
+ emsDigits [7] ISDN-AddressString OPTIONAL,
+ emsKey [8] ISDN-AddressString OPTIONAL,
+ lcsQos [9] LCSQoSInfo OPTIONAL,
+ lcsPriority [10] LCS-Priority OPTIONAL,
+ mlc-Number [11] ISDN-AddressString OPTIONAL,
+ eventTimeStamp [12] TimeStamp OPTIONAL,
+ measureDuration [13] CallDuration OPTIONAL,
+ location [14] LocationAreaAndCell OPTIONAL,
+ locationEstimate [15] Ext-GeographicalInformation OPTIONAL,
+ positioningData [16] PositioningData OPTIONAL,
+ lcsCause [17] LCSCause OPTIONAL,
+ diagnostics [18] Diagnostics OPTIONAL,
+ systemType [19] SystemType OPTIONAL,
+ recordExtensions [20] ManagementExtensions OPTIONAL,
+ causeForTerm [21] CauseForTerm OPTIONAL,
+ lcsReferenceNumber [101] CallReferenceNumber OPTIONAL,
+ additionalChgInfo [133] AdditionalChgInfo OPTIONAL,
+ chargedParty [141] ChargedParty OPTIONAL,
+ orgRNCorBSCId [167] RNCorBSCId OPTIONAL,
+ orgMSCId [168] MSCId OPTIONAL,
+ globalAreaID [188] GAI OPTIONAL,
+ subscriberCategory [190] SubscriberCategory OPTIONAL,
+ firstmccmnc [192] MCCMNC OPTIONAL,
+ hotBillingTag [200] HotBillingTag OPTIONAL,
+ callReference [201] CallReference OPTIONAL
+}
+
+
+------------------------------------------------------------------------------
+--
+-- FTAM / FTP / TFTP FILE CONTENTS
+--
+------------------------------------------------------------------------------
+
+CallEventDataFile ::= SEQUENCE
+{
+ headerRecord [0] HeaderRecord,
+ callEventRecords [1] SEQUENCE OF CallEventRecord,
+ trailerRecord [2] TrailerRecord,
+ extensions [3] ManagementExtensions
+}
+
+ObservedIMEITicketFile ::= SEQUENCE
+{
+ productionDateTime [0] TimeStamp,
+ observedIMEITickets [1] SEQUENCE OF ObservedIMEITicket,
+ noOfRecords [2] INTEGER,
+ extensions [3] ManagementExtensions
+}
+
+HeaderRecord ::= SEQUENCE
+{
+ productionDateTime [0] TimeStamp,
+ recordingEntity [1] RecordingEntity,
+ extensions [2] ManagementExtensions
+}
+
+TrailerRecord ::= SEQUENCE
+{
+ productionDateTime [0] TimeStamp,
+ recordingEntity [1] RecordingEntity,
+ firstCallDateTime [2] TimeStamp,
+ lastCallDateTime [3] TimeStamp,
+ noOfRecords [4] INTEGER,
+ extensions [5] ManagementExtensions
+}
+
+
+------------------------------------------------------------------------------
+--
+-- COMMON DATA TYPES
+--
+------------------------------------------------------------------------------
+
+AdditionalChgInfo ::= SEQUENCE
+{
+ chargeIndicator [0] ChargeIndicator OPTIONAL,
+ chargeParameters [1] OCTET STRING OPTIONAL
+}
+
+AddressString ::= OCTET STRING -- (SIZE (1..maxAddressLength))
+ -- This type is used to represent a number for addressing
+ -- purposes. It is composed of
+ -- a) one octet for nature of address, and numbering plan
+ -- indicator.
+ -- b) digits of an address encoded as TBCD-String.
+
+ -- a) The first octet includes a one bit extension indicator, a
+ -- 3 bits nature of address indicator and a 4 bits numbering
+ -- plan indicator, encoded as follows:
+
+ -- bit 8: 1 (no extension)
+
+ -- bits 765: nature of address indicator
+ -- 000 unknown
+ -- 001 international number
+ -- 010 national significant number
+ -- 011 network specific number
+ -- 100 subscriber number
+ -- 101 reserved
+ -- 110 abbreviated number
+ -- 111 reserved for extension
+
+ -- bits 4321: numbering plan indicator
+ -- 0000 unknown
+ -- 0001 ISDN/Telephony Numbering Plan (Rec CCITT E.164)
+ -- 0010 spare
+ -- 0011 data numbering plan (CCITT Rec X.121)
+ -- 0100 telex numbering plan (CCITT Rec F.69)
+ -- 0101 spare
+ -- 0110 land mobile numbering plan (CCITT Rec E.212)
+ -- 0111 spare
+ -- 1000 national numbering plan
+ -- 1001 private numbering plan
+ -- 1111 reserved for extension
+
+ -- all other values are reserved.
+
+ -- b) The following octets representing digits of an address
+ -- encoded as a TBCD-STRING.
+
+-- maxAddressLength INTEGER ::= 20
+
+AiurRequested ::= ENUMERATED
+{
+ --
+ -- See Bearer Capability TS 24.008
+ -- (note that value "4" is intentionally missing
+ -- because it is not used in TS 24.008)
+ --
+
+ aiur09600BitsPerSecond (1),
+ aiur14400BitsPerSecond (2),
+ aiur19200BitsPerSecond (3),
+ aiur28800BitsPerSecond (5),
+ aiur38400BitsPerSecond (6),
+ aiur43200BitsPerSecond (7),
+ aiur57600BitsPerSecond (8),
+ aiur38400BitsPerSecond1 (9),
+ aiur38400BitsPerSecond2 (10),
+ aiur38400BitsPerSecond3 (11),
+ aiur38400BitsPerSecond4 (12)
+}
+
+AOCParameters ::= SEQUENCE
+{
+ --
+ -- See TS 22.024.
+ --
+ e1 [1] EParameter OPTIONAL,
+ e2 [2] EParameter OPTIONAL,
+ e3 [3] EParameter OPTIONAL,
+ e4 [4] EParameter OPTIONAL,
+ e5 [5] EParameter OPTIONAL,
+ e6 [6] EParameter OPTIONAL,
+ e7 [7] EParameter OPTIONAL
+}
+
+AOCParmChange ::= SEQUENCE
+{
+ changeTime [0] TimeStamp,
+ newParameters [1] AOCParameters
+}
+
+BasicService ::= OCTET STRING -- (SIZE(1))
+
+--This parameter identifies the ISDN Basic service as defined in ETSI specification ETS 300 196.
+-- allServices '00'h
+-- speech '01'h
+-- unrestricteDigtalInfo '02'h
+-- audio3k1HZ '03'h
+-- unrestricteDigtalInfowithtoneandannoucement '04'h
+-- telephony3k1HZ '20'h
+-- teletext '21'h
+-- telefaxGroup4Class1 '22'h
+-- videotextSyntaxBased '23'h
+-- videotelephony '24'h
+-- telefaxGroup2-3 '25'h
+-- telephony7kHZ '26'h
+
+
+
+BasicServices ::= SET OF BasicServiceCode
+
+BasicServiceCode ::= CHOICE
+{
+ bearerService [2] BearerServiceCode,
+ teleservice [3] TeleserviceCode
+}
+
+
+TeleserviceCode ::= OCTET STRING -- (SIZE (1))
+ -- This type is used to represent the code identifying a single
+ -- teleservice, a group of teleservices, or all teleservices. The
+ -- services are defined in TS GSM 02.03.
+ -- The internal structure is defined as follows:
+
+ -- bits 87654321: group (bits 8765) and specific service
+ -- (bits 4321)
+
+-- allTeleservices (0x00),
+-- allSpeechTransmissionServices (0x10),
+-- telephony (0x11),
+-- emergencyCalls (0x12),
+--
+-- allShortMessageServices (0x20),
+-- shortMessageMT-PP (0x21),
+-- shortMessageMO-PP (0x22),
+--
+-- allFacsimileTransmissionServices (0x60),
+-- facsimileGroup3AndAlterSpeech (0x61),
+-- automaticFacsimileGroup3 (0x62),
+-- facsimileGroup4 (0x63),
+--
+-- The following non-hierarchical Compound Teleservice Groups
+-- are defined in TS GSM 02.30:
+-- allDataTeleservices (0x70),
+-- covers Teleservice Groups 'allFacsimileTransmissionServices'
+-- and 'allShortMessageServices'
+-- allTeleservices-ExeptSMS (0x80),
+-- covers Teleservice Groups 'allSpeechTransmissionServices' and
+-- 'allFacsimileTransmissionServices'
+--
+-- Compound Teleservice Group Codes are only used in call
+-- independent supplementary service operations, i.e. they
+-- are not used in InsertSubscriberData or in
+-- DeleteSubscriberData messages.
+--
+-- allVoiceGroupCallServices (0x90),
+-- voiceGroupCall (0x91),
+-- voiceBroadcastCall (0x92),
+--
+-- allPLMN-specificTS (0xd0),
+-- plmn-specificTS-1 (0xd1),
+-- plmn-specificTS-2 (0xd2),
+-- plmn-specificTS-3 (0xd3),
+-- plmn-specificTS-4 (0xd4),
+-- plmn-specificTS-5 (0xd5),
+-- plmn-specificTS-6 (0xd6),
+-- plmn-specificTS-7 (0xd7),
+-- plmn-specificTS-8 (0xd8),
+-- plmn-specificTS-9 (0xd9),
+-- plmn-specificTS-A (0xda),
+-- plmn-specificTS-B (0xdb),
+-- plmn-specificTS-C (0xdc),
+-- plmn-specificTS-D (0xdd),
+-- plmn-specificTS-E (0xde),
+-- plmn-specificTS-F (0xdf)
+
+
+BearerServiceCode ::= OCTET STRING -- (SIZE (1))
+ -- This type is used to represent the code identifying a single
+ -- bearer service, a group of bearer services, or all bearer
+ -- services. The services are defined in TS 3GPP TS 22.002 [3].
+ -- The internal structure is defined as follows:
+ --
+ -- plmn-specific bearer services:
+ -- bits 87654321: defined by the HPLMN operator
+
+ -- rest of bearer services:
+ -- bit 8: 0 (unused)
+ -- bits 7654321: group (bits 7654), and rate, if applicable
+ -- (bits 321)
+
+-- allBearerServices (0x00),
+-- allDataCDA-Services (0x10),
+-- dataCDA-300bps (0x11),
+-- dataCDA-1200bps (0x12),
+-- dataCDA-1200-75bps (0x13),
+-- dataCDA-2400bps (0x14),
+-- dataCDA-4800bps (0x15),
+-- dataCDA-9600bps (0x16),
+-- general-dataCDA (0x17),
+--
+-- allDataCDS-Services (0x18),
+-- dataCDS-1200bps (0x1a),
+-- dataCDS-2400bps (0x1c),
+-- dataCDS-4800bps (0x1d),
+-- dataCDS-9600bps (0x1e),
+-- general-dataCDS (0x1f),
+--
+-- allPadAccessCA-Services (0x20),
+-- padAccessCA-300bps (0x21),
+-- padAccessCA-1200bps (0x22),
+-- padAccessCA-1200-75bps (0x23),
+-- padAccessCA-2400bps (0x24),
+-- padAccessCA-4800bps (0x25),
+-- padAccessCA-9600bps (0x26),
+-- general-padAccessCA (0x27),
+--
+-- allDataPDS-Services (0x28),
+-- dataPDS-2400bps (0x2c),
+-- dataPDS-4800bps (0x2d),
+-- dataPDS-9600bps (0x2e),
+-- general-dataPDS (0x2f),
+--
+-- allAlternateSpeech-DataCDA (0x30),
+--
+-- allAlternateSpeech-DataCDS (0x38),
+--
+-- allSpeechFollowedByDataCDA (0x40),
+--
+-- allSpeechFollowedByDataCDS (0x48),
+--
+-- The following non-hierarchical Compound Bearer Service
+-- Groups are defined in TS GSM 02.30:
+-- allDataCircuitAsynchronous (0x50),
+-- covers "allDataCDA-Services", "allAlternateSpeech-DataCDA" and
+-- "allSpeechFollowedByDataCDA"
+-- allDataCircuitSynchronous (0x58),
+-- covers "allDataCDS-Services", "allAlternateSpeech-DataCDS" and
+-- "allSpeechFollowedByDataCDS"
+-- allAsynchronousServices (0x60),
+-- covers "allDataCDA-Services", "allAlternateSpeech-DataCDA",
+-- "allSpeechFollowedByDataCDA" and "allPadAccessCDA-Services"
+-- allSynchronousServices (0x68),
+-- covers "allDataCDS-Services", "allAlternateSpeech-DataCDS",
+-- "allSpeechFollowedByDataCDS" and "allDataPDS-Services"
+--
+-- Compound Bearer Service Group Codes are only used in call
+-- independent supplementary service operations, i.e. they
+-- are not used in InsertSubscriberData or in
+-- DeleteSubscriberData messages.
+--
+-- allPLMN-specificBS (0xd0),
+-- plmn-specificBS-1 (0xd1),
+-- plmn-specificBS-2 (0xd2),
+-- plmn-specificBS-3 (0xd3),
+-- plmn-specificBS-4 (0xd4),
+-- plmn-specificBS-5 (0xd5),
+-- plmn-specificBS-6 (0xd6),
+-- plmn-specificBS-7 (0xd7),
+-- plmn-specificBS-8 (0xd8),
+-- plmn-specificBS-9 (0xd9),
+-- plmn-specificBS-A (0xda),
+-- plmn-specificBS-B (0xdb),
+-- plmn-specificBS-C (0xdc),
+-- plmn-specificBS-D (0xdd),
+-- plmn-specificBS-E (0xde),
+-- plmn-specificBS-F (0xdf)
+
+
+BCDDirectoryNumber ::= OCTET STRING
+ -- This type contains the binary coded decimal representation of
+ -- a directory number e.g. calling/called/connected/translated number.
+ -- The encoding of the octet string is in accordance with the
+ -- the elements "Calling party BCD number", "Called party BCD number"
+ -- and "Connected number" defined in TS 24.008.
+ -- This encoding includes type of number and number plan information
+ -- together with a BCD encoded digit string.
+ -- It may also contain both a presentation and screening indicator
+ -- (octet 3a).
+ -- For the avoidance of doubt, this field does not include
+ -- octets 1 and 2, the element name and length, as this would be
+ -- redundant.
+
+CallDuration ::= INTEGER
+ --
+ -- The call duration in seconds.
+ -- For successful calls this is the chargeable duration.
+ -- For call attempts this is the call holding time.
+ --
+
+CallEventRecordType ::= ENUMERATED -- INTEGER
+{
+ moCallRecord (0),
+ mtCallRecord (1),
+ roamingRecord (2),
+ incGatewayRecord (3),
+ outGatewayRecord (4),
+ transitCallRecord (5),
+ moSMSRecord (6),
+ mtSMSRecord (7),
+ ssActionRecord (10),
+ hlrIntRecord (11),
+ commonEquipRecord (14),
+ moTraceRecord (15),
+ mtTraceRecord (16),
+ termCAMELRecord (17),
+ mtLCSRecord (23),
+ moLCSRecord (24),
+ niLCSRecord (25),
+ forwardCallRecord (100)
+}
+
+CalledNumber ::= BCDDirectoryNumber
+
+CallingNumber ::= BCDDirectoryNumber
+
+CallingPartyCategory ::= Category
+
+CallReference ::= OCTET STRING -- (SIZE (1..8))
+
+CallReferenceNumber ::= OCTET STRING -- (SIZE (1..8))
+
+CAMELDestinationNumber ::= DestinationRoutingAddress
+
+CAMELInformation ::= SET
+{
+ cAMELDestinationNumber [1] CAMELDestinationNumber OPTIONAL,
+ connectedNumber [2] ConnectedNumber OPTIONAL,
+ roamingNumber [3] RoamingNumber OPTIONAL,
+ mscOutgoingROUTE [4] ROUTE OPTIONAL,
+ seizureTime [5] TimeStamp OPTIONAL,
+ answerTime [6] TimeStamp OPTIONAL,
+ releaseTime [7] TimeStamp OPTIONAL,
+ callDuration [8] CallDuration OPTIONAL,
+ dataVolume [9] DataVolume OPTIONAL,
+ cAMELInitCFIndicator [10] CAMELInitCFIndicator OPTIONAL,
+ causeForTerm [11] CauseForTerm OPTIONAL,
+ cAMELModification [12] ChangedParameters OPTIONAL,
+ freeFormatData [13] FreeFormatData OPTIONAL,
+ diagnostics [14] Diagnostics OPTIONAL,
+ freeFormatDataAppend [15] BOOLEAN OPTIONAL,
+ freeFormatData-2 [16] FreeFormatData OPTIONAL,
+ freeFormatDataAppend-2 [17] BOOLEAN OPTIONAL
+}
+
+CAMELSMSInformation ::= SET
+{
+ gsm-SCFAddress [1] Gsm-SCFAddress OPTIONAL,
+ serviceKey [2] ServiceKey OPTIONAL,
+ defaultSMSHandling [3] DefaultSMS-Handling OPTIONAL,
+ freeFormatData [4] FreeFormatData OPTIONAL,
+ callingPartyNumber [5] CallingNumber OPTIONAL,
+ destinationSubscriberNumber [6] CalledNumber OPTIONAL,
+ cAMELSMSCAddress [7] AddressString OPTIONAL,
+ smsReferenceNumber [8] CallReferenceNumber OPTIONAL
+}
+
+CAMELInitCFIndicator ::= ENUMERATED
+{
+ noCAMELCallForwarding (0),
+ cAMELCallForwarding (1)
+}
+
+CAMELModificationParameters ::= SET
+ --
+ -- The list contains only parameters changed due to CAMEL call
+ -- handling.
+ --
+{
+ callingPartyNumber [0] CallingNumber OPTIONAL,
+ callingPartyCategory [1] CallingPartyCategory OPTIONAL,
+ originalCalledPartyNumber [2] OriginalCalledNumber OPTIONAL,
+ genericNumbers [3] GenericNumbers OPTIONAL,
+ redirectingPartyNumber [4] RedirectingNumber OPTIONAL,
+ redirectionCounter [5] NumberOfForwarding OPTIONAL
+}
+
+
+Category ::= OCTET STRING -- (SIZE(1))
+ --
+ -- The internal structure is defined in ITU-T Rec Q.763.
+ --see subscribe category
+
+CauseForTerm ::= ENUMERATED -- INTEGER
+ --
+ -- Cause codes from 16 up to 31 are defined in TS 32.015 as 'CauseForRecClosing'
+ -- (cause for record closing).
+ -- There is no direct correlation between these two types.
+ -- LCS related causes belong to the MAP error causes acc. TS 29.002.
+ --
+{
+ normalRelease (0),
+ partialRecord (1),
+ partialRecordCallReestablishment (2),
+ unsuccessfulCallAttempt (3),
+ stableCallAbnormalTermination (4),
+ cAMELInitCallRelease (5),
+ unauthorizedRequestingNetwork (52),
+ unauthorizedLCSClient (53),
+ positionMethodFailure (54),
+ unknownOrUnreachableLCSClient (58)
+}
+
+CellId ::= OCTET STRING -- (SIZE(2))
+ --
+ -- Coded according to TS 24.008
+ --
+
+ChangedParameters ::= SET
+{
+ changeFlags [0] ChangeFlags,
+ changeList [1] CAMELModificationParameters OPTIONAL
+}
+
+ChangeFlags ::= BIT STRING
+-- {
+-- callingPartyNumberModified (0),
+-- callingPartyCategoryModified (1),
+-- originalCalledPartyNumberModified (2),
+-- genericNumbersModified (3),
+-- redirectingPartyNumberModified (4),
+-- redirectionCounterModified (5)
+-- }
+
+ChangeOfClassmark ::= SEQUENCE
+{
+ classmark [0] Classmark,
+ changeTime [1] TimeStamp
+}
+
+ChangeOfRadioChannel ::= SEQUENCE
+{
+ radioChannel [0] TrafficChannel,
+ changeTime [1] TimeStamp,
+ speechVersionUsed [2] SpeechVersionIdentifier OPTIONAL
+}
+
+ChangeOfService ::= SEQUENCE
+{
+ basicService [0] BasicServiceCode,
+ transparencyInd [1] TransparencyInd OPTIONAL,
+ changeTime [2] TimeStamp,
+ rateIndication [3] RateIndication OPTIONAL,
+ fnur [4] Fnur OPTIONAL
+}
+
+ChannelCoding ::= ENUMERATED
+{
+ tchF4800 (1),
+ tchF9600 (2),
+ tchF14400 (3)
+}
+
+ChargeIndicator ::= ENUMERATED -- INTEGER
+{
+ noIndication (0),
+ noCharge (1),
+ charge (2)
+}
+
+Classmark ::= OCTET STRING
+ --
+ -- See Mobile station classmark 2 or 3 TS 24.008
+ --
+
+ConnectedNumber ::= BCDDirectoryNumber
+
+DataVolume ::= INTEGER
+ --
+ -- The volume of data transferred in segments of 64 octets.
+ --
+
+Day ::= INTEGER -- (1..31)
+
+--DayClass ::= ObjectInstance
+
+--DayClasses ::= SET OF DayClass
+
+--DayDefinition ::= SEQUENCE
+--{
+-- day [0] DayOfTheWeek,
+-- dayClass [1] ObjectInstance
+--}
+
+--DayDefinitions ::= SET OF DayDefinition
+
+--DateDefinition ::= SEQUENCE
+--{
+-- month [0] Month,
+-- day [1] Day,
+-- dayClass [2] ObjectInstance
+--}
+
+--DateDefinitions ::= SET OF DateDefinition
+
+--DayOfTheWeek ::= ENUMERATED
+--{
+-- allDays (0),
+-- sunday (1),
+-- monday (2),
+-- tuesday (3),
+-- wednesday (4),
+-- thursday (5),
+-- friday (6),
+-- saturday (7)
+--}
+
+DestinationRoutingAddress ::= BCDDirectoryNumber
+
+DefaultCallHandling ::= ENUMERATED
+{
+ continueCall (0),
+ releaseCall (1)
+}
+ -- exception handling:
+ -- reception of values in range 2-31 shall be treated as "continueCall"
+ -- reception of values greater than 31 shall be treated as "releaseCall"
+
+DeferredLocationEventType ::= BIT STRING
+-- {
+-- msAvailable (0)
+-- } (SIZE (1..16))
+
+ -- exception handling
+ -- a ProvideSubscriberLocation-Arg containing other values than listed above in
+ -- DeferredLocationEventType shall be rejected by the receiver with a return error cause of
+ -- unexpected data value.
+
+Diagnostics ::= CHOICE
+{
+ gsm0408Cause [0] INTEGER,
+ -- See TS 24.008
+ gsm0902MapErrorValue [1] INTEGER,
+ -- Note: The value to be stored here corresponds to
+ -- the local values defined in the MAP-Errors and
+ -- MAP-DialogueInformation modules, for full details
+ -- see TS 29.002.
+ ccittQ767Cause [2] INTEGER,
+ -- See ITU-T Q.767
+ networkSpecificCause [3] ManagementExtension,
+ -- To be defined by network operator
+ manufacturerSpecificCause [4] ManagementExtension
+ -- To be defined by manufacturer
+}
+
+DefaultSMS-Handling ::= ENUMERATED
+{
+ continueTransaction (0) ,
+ releaseTransaction (1)
+}
+-- exception handling:
+-- reception of values in range 2-31 shall be treated as "continueTransaction"
+-- reception of values greater than 31 shall be treated as "releaseTransaction"
+
+--Destinations ::= SET OF AE-title
+
+EmergencyCallIndEnable ::= BOOLEAN
+
+EmergencyCallIndication ::= SEQUENCE
+{
+ cellId [0] CellId,
+ callerId [1] IMSIorIMEI
+}
+
+EParameter ::= INTEGER -- (0..1023)
+ --
+ -- Coded according to TS 22.024 and TS 24.080
+ --
+
+EquipmentId ::= INTEGER
+
+Ext-GeographicalInformation ::= OCTET STRING -- (SIZE (1..maxExt-GeographicalInformation))
+ -- Refers to geographical Information defined in 3G TS 23.032.
+ -- This is composed of 1 or more octets with an internal structure according to
+ -- 3G TS 23.032
+ -- Octet 1: Type of shape, only the following shapes in 3G TS 23.032 are allowed:
+ -- (a) Ellipsoid point with uncertainty circle
+ -- (b) Ellipsoid point with uncertainty ellipse
+ -- (c) Ellipsoid point with altitude and uncertainty ellipsoid
+ -- (d) Ellipsoid Arc
+ -- (e) Ellipsoid Point
+ -- Any other value in octet 1 shall be treated as invalid
+ -- Octets 2 to 8 for case (a) - Ellipsoid point with uncertainty circle
+ -- Degrees of Latitude 3 octets
+ -- Degrees of Longitude 3 octets
+ -- Uncertainty code 1 octet
+ -- Octets 2 to 11 for case (b) - Ellipsoid point with uncertainty ellipse:
+ -- Degrees of Latitude 3 octets
+ -- Degrees of Longitude 3 octets
+ -- Uncertainty semi-major axis 1 octet
+ -- Uncertainty semi-minor axis 1 octet
+ -- Angle of major axis 1 octet
+ -- Confidence 1 octet
+ -- Octets 2 to 14 for case (c) - Ellipsoid point with altitude and uncertainty ellipsoid
+ -- Degrees of Latitude 3 octets
+ -- Degrees of Longitude 3 octets
+ -- Altitude 2 octets
+ -- Uncertainty semi-major axis 1 octet
+ -- Uncertainty semi-minor axis 1 octet
+ -- Angle of major axis 1 octet
+ -- Uncertainty altitude 1 octet
+ -- Confidence 1 octet
+ -- Octets 2 to 13 for case (d) - Ellipsoid Arc
+ -- Degrees of Latitude 3 octets
+ -- Degrees of Longitude 3 octets
+ -- Inner radius 2 octets
+ -- Uncertainty radius 1 octet
+ -- Offset angle 1 octet
+ -- Included angle 1 octet
+ -- Confidence 1 octet
+ -- Octets 2 to 7 for case (e) - Ellipsoid Point
+ -- Degrees of Latitude 3 octets
+ -- Degrees of Longitude 3 octets
+ --
+ -- An Ext-GeographicalInformation parameter comprising more than one octet and
+ -- containing any other shape or an incorrect number of octets or coding according
+ -- to 3G TS 23.032 shall be treated as invalid data by a receiver.
+ --
+ -- An Ext-GeographicalInformation parameter comprising one octet shall be discarded
+ -- by the receiver if an Add-GeographicalInformation parameter is received
+ -- in the same message.
+ --
+ -- An Ext-GeographicalInformation parameter comprising one octet shall be treated as
+ -- invalid data by the receiver if an Add-GeographicalInformation parameter is not
+ -- received in the same message.
+
+-- maxExt-GeographicalInformation INTEGER ::= 20
+ -- the maximum length allows for further shapes in 3G TS 23.032 to be included in later
+ -- versions of 3G TS 29.002
+
+EquipmentType ::= ENUMERATED -- INTEGER
+{
+ conferenceBridge (0)
+}
+
+FileType ::= ENUMERATED -- INTEGER
+{
+ callRecords (1),
+ traceRecords (9),
+ observedIMEITicket (14)
+}
+
+Fnur ::= ENUMERATED
+{
+ --
+ -- See Bearer Capability TS 24.008
+ --
+ fnurNotApplicable (0),
+ fnur9600-BitsPerSecond (1),
+ fnur14400BitsPerSecond (2),
+ fnur19200BitsPerSecond (3),
+ fnur28800BitsPerSecond (4),
+ fnur38400BitsPerSecond (5),
+ fnur48000BitsPerSecond (6),
+ fnur56000BitsPerSecond (7),
+ fnur64000BitsPerSecond (8),
+ fnur33600BitsPerSecond (9),
+ fnur32000BitsPerSecond (10),
+ fnur31200BitsPerSecond (11)
+}
+
+ForwardToNumber ::= AddressString
+
+FreeFormatData ::= OCTET STRING -- (SIZE(1..160))
+ --
+ -- Free formated data as sent in the FCI message
+ -- See TS 29.078
+ --
+
+GenericNumber ::= BCDDirectoryNumber
+
+GenericNumbers ::= SET OF GenericNumber
+
+Gsm-SCFAddress ::= ISDNAddressString
+ --
+ -- See TS 29.002
+ --
+
+HLRIntResult ::= Diagnostics
+
+Horizontal-Accuracy ::= OCTET STRING -- (SIZE (1))
+ -- bit 8 = 0
+ -- bits 7-1 = 7 bit Uncertainty Code defined in 3G TS 23.032. The horizontal location
+ -- error should be less than the error indicated by the uncertainty code with 67%
+ -- confidence.
+
+HotBillingTag ::= ENUMERATED --INTEGER
+{
+ noHotBilling (0),
+ hotBilling (1)
+}
+
+HSCSDParmsChange ::= SEQUENCE
+{
+ changeTime [0] TimeStamp,
+ hSCSDChanAllocated [1] NumOfHSCSDChanAllocated,
+ initiatingParty [2] InitiatingParty OPTIONAL,
+ aiurRequested [3] AiurRequested OPTIONAL,
+ chanCodingUsed [4] ChannelCoding,
+ hSCSDChanRequested [5] NumOfHSCSDChanRequested OPTIONAL
+}
+
+
+IMEI ::= TBCD-STRING -- (SIZE (8))
+ -- Refers to International Mobile Station Equipment Identity
+ -- and Software Version Number (SVN) defined in TS GSM 03.03.
+ -- If the SVN is not present the last octet shall contain the
+ -- digit 0 and a filler.
+ -- If present the SVN shall be included in the last octet.
+
+IMSI ::= TBCD-STRING -- (SIZE (3..8))
+ -- digits of MCC, MNC, MSIN are concatenated in this order.
+
+IMEICheckEvent ::= ENUMERATED -- INTEGER
+{
+ mobileOriginatedCall (0),
+ mobileTerminatedCall (1),
+ smsMobileOriginating (2),
+ smsMobileTerminating (3),
+ ssAction (4),
+ locationUpdate (5)
+}
+
+IMEIStatus ::= ENUMERATED
+{
+ greyListedMobileEquipment (0),
+ blackListedMobileEquipment (1),
+ nonWhiteListedMobileEquipment (2)
+}
+
+IMSIorIMEI ::= CHOICE
+{
+ imsi [0] IMSI,
+ imei [1] IMEI
+}
+
+InitiatingParty ::= ENUMERATED
+{
+ network (0),
+ subscriber (1)
+}
+
+ISDN-AddressString ::= AddressString -- (SIZE (1..maxISDN-AddressLength))
+ -- This type is used to represent ISDN numbers.
+
+-- maxISDN-AddressLength INTEGER ::= 9
+
+LCSCause ::= OCTET STRING -- (SIZE(1))
+ --
+ -- See LCS Cause Value, 3GPP TS 49.031
+ --
+
+LCS-Priority ::= OCTET STRING -- (SIZE (1))
+ -- 0 = highest priority
+ -- 1 = normal priority
+ -- all other values treated as 1
+
+LCSClientIdentity ::= SEQUENCE
+{
+ lcsClientExternalID [0] LCSClientExternalID OPTIONAL,
+ lcsClientDialedByMS [1] AddressString OPTIONAL,
+ lcsClientInternalID [2] LCSClientInternalID OPTIONAL
+}
+
+LCSClientExternalID ::= SEQUENCE
+{
+ externalAddress [0] AddressString OPTIONAL
+-- extensionContainer [1] ExtensionContainer OPTIONAL
+}
+
+LCSClientInternalID ::= ENUMERATED
+{
+ broadcastService (0),
+ o-andM-HPLMN (1),
+ o-andM-VPLMN (2),
+ anonymousLocation (3),
+ targetMSsubscribedService (4)
+}
+ -- for a CAMEL phase 3 PLMN operator client, the value targetMSsubscribedService shall be used
+
+LCSClientType ::= ENUMERATED
+{
+ emergencyServices (0),
+ valueAddedServices (1),
+ plmnOperatorServices (2),
+ lawfulInterceptServices (3)
+}
+ -- exception handling:
+ -- unrecognized values may be ignored if the LCS client uses the privacy override
+ -- otherwise, an unrecognized value shall be treated as unexpected data by a receiver
+ -- a return error shall then be returned if received in a MAP invoke
+
+LCSQoSInfo ::= SEQUENCE
+{
+ horizontal-accuracy [0] Horizontal-Accuracy OPTIONAL,
+ verticalCoordinateRequest [1] NULL OPTIONAL,
+ vertical-accuracy [2] Vertical-Accuracy OPTIONAL,
+ responseTime [3] ResponseTime OPTIONAL
+}
+
+LevelOfCAMELService ::= BIT STRING
+-- {
+-- basic (0),
+-- callDurationSupervision (1),
+-- onlineCharging (2)
+-- }
+
+LocationAreaAndCell ::= SEQUENCE
+{
+ locationAreaCode [0] LocationAreaCode,
+ cellIdentifier [1] CellId
+--
+-- For 2G the content of the Cell Identifier is defined by the Cell Id
+-- refer TS 24.008 and for 3G by the Service Area Code refer TS 25.413.
+--
+
+}
+
+LocationAreaCode ::= OCTET STRING -- (SIZE(2))
+ --
+ -- See TS 24.008
+ --
+
+LocationChange ::= SEQUENCE
+{
+ location [0] LocationAreaAndCell,
+ changeTime [1] TimeStamp
+}
+
+Location-info ::= SEQUENCE
+{
+ mscNumber [1] MscNo OPTIONAL,
+ location-area [2] LocationAreaCode,
+ cell-identification [3] CellId OPTIONAL
+}
+
+LocationType ::= SEQUENCE
+{
+locationEstimateType [0] LocationEstimateType,
+ deferredLocationEventType [1] DeferredLocationEventType OPTIONAL
+}
+
+LocationEstimateType ::= ENUMERATED
+{
+ currentLocation (0),
+ currentOrLastKnownLocation (1),
+ initialLocation (2),
+ activateDeferredLocation (3),
+ cancelDeferredLocation (4)
+}
+ -- exception handling:
+ -- a ProvideSubscriberLocation-Arg containing an unrecognized LocationEstimateType
+ -- shall be rejected by the receiver with a return error cause of unexpected data value
+
+LocUpdResult ::= Diagnostics
+
+ManagementExtensions ::= SET OF ManagementExtension
+
+ManagementExtension ::= SEQUENCE
+{
+ identifier OBJECT IDENTIFIER,
+ significance [1] BOOLEAN , -- DEFAULT FALSE,
+ information [2] OCTET STRING
+}
+
+
+MCCMNC ::= OCTET STRING -- (SIZE(3))
+ --
+ -- This type contains the mobile country code (MCC) and the mobile
+ -- network code (MNC) of a PLMN.
+ --
+
+RateIndication ::= OCTET STRING -- (SIZE(1))
+
+--0 no rate adaption
+--1 V.110, I.460/X.30
+--2 ITU-T X.31 flag stuffing
+--3 V.120
+--7 H.223 & H.245
+--11 PIAFS
+
+
+MessageReference ::= OCTET STRING
+
+Month ::= INTEGER -- (1..12)
+
+MOLR-Type ::= INTEGER
+--0 locationEstimate
+--1 assistanceData
+--2 deCipheringKeys
+
+MSCAddress ::= AddressString
+
+MscNo ::= ISDN-AddressString
+ --
+ -- See TS 23.003
+ --
+
+MSISDN ::= ISDN-AddressString
+ --
+ -- See TS 23.003
+ --
+
+MSPowerClasses ::= SET OF RFPowerCapability
+
+NetworkCallReference ::= CallReferenceNumber
+ -- See TS 29.002
+ --
+
+NetworkSpecificCode ::= INTEGER
+ --
+ -- To be defined by network operator
+ --
+
+NetworkSpecificServices ::= SET OF NetworkSpecificCode
+
+NotificationToMSUser ::= ENUMERATED
+{
+ notifyLocationAllowed (0),
+ notifyAndVerify-LocationAllowedIfNoResponse (1),
+ notifyAndVerify-LocationNotAllowedIfNoResponse (2),
+ locationNotAllowed (3)
+}
+ -- exception handling:
+ -- At reception of any other value than the ones listed the receiver shall ignore
+ -- NotificationToMSUser.
+
+NumberOfForwarding ::= INTEGER -- (1..5)
+
+NumOfHSCSDChanRequested ::= INTEGER
+
+NumOfHSCSDChanAllocated ::= INTEGER
+
+ObservedIMEITicketEnable ::= BOOLEAN
+
+OriginalCalledNumber ::= BCDDirectoryNumber
+
+OriginDestCombinations ::= SET OF OriginDestCombination
+
+OriginDestCombination ::= SEQUENCE
+{
+ origin [0] INTEGER OPTIONAL,
+ destination [1] INTEGER OPTIONAL
+ --
+ -- Note that these values correspond to the contents
+ -- of the attributes originId and destinationId
+ -- respectively. At least one of the two must be present.
+ --
+}
+
+PartialRecordTimer ::= INTEGER
+
+PartialRecordType ::= ENUMERATED
+{
+ timeLimit (0),
+ serviceChange (1),
+ locationChange (2),
+ classmarkChange (3),
+ aocParmChange (4),
+ radioChannelChange (5),
+ hSCSDParmChange (6),
+ changeOfCAMELDestination (7),
+ firstHotBill (20),
+ severalSSOperationBill (21)
+}
+
+PartialRecordTypes ::= SET OF PartialRecordType
+
+PositioningData ::= OCTET STRING -- (SIZE(1..33))
+ --
+ -- See Positioning Data IE (octet 3..n), 3GPP TS 49.031
+ --
+
+RadioChannelsRequested ::= SET OF RadioChanRequested
+
+RadioChanRequested ::= ENUMERATED
+{
+ --
+ -- See Bearer Capability TS 24.008
+ --
+ halfRateChannel (0),
+ fullRateChannel (1),
+ dualHalfRatePreferred (2),
+ dualFullRatePreferred (3)
+}
+
+--RecordClassDestination ::= CHOICE
+--{
+-- osApplication [0] AE-title,
+-- fileType [1] FileType
+--}
+
+--RecordClassDestinations ::= SET OF RecordClassDestination
+
+RecordingEntity ::= AddressString
+
+RecordingMethod ::= ENUMERATED
+{
+ inCallRecord (0),
+ inSSRecord (1)
+}
+
+RedirectingNumber ::= BCDDirectoryNumber
+
+RedirectingCounter ::= INTEGER
+
+ResponseTime ::= SEQUENCE
+{
+ responseTimeCategory ResponseTimeCategory
+}
+ -- note: an expandable SEQUENCE simplifies later addition of a numeric response time.
+
+ResponseTimeCategory ::= ENUMERATED
+{
+ lowdelay (0),
+ delaytolerant (1)
+}
+ -- exception handling:
+ -- an unrecognized value shall be treated the same as value 1 (delaytolerant)
+
+RFPowerCapability ::= INTEGER
+ --
+ -- This field contains the RF power capability of the Mobile station
+ -- classmark 1 and 2 of TS 24.008 expressed as an integer.
+ --
+
+RoamingNumber ::= ISDN-AddressString
+ --
+ -- See TS 23.003
+ --
+
+RoutingNumber ::= CHOICE
+{
+ roaming [1] RoamingNumber,
+ forwarded [2] ForwardToNumber
+}
+
+Service ::= CHOICE
+{
+ teleservice [1] TeleserviceCode,
+ bearerService [2] BearerServiceCode,
+ supplementaryService [3] SS-Code,
+ networkSpecificService [4] NetworkSpecificCode
+}
+
+ServiceDistanceDependencies ::= SET OF ServiceDistanceDependency
+
+ServiceDistanceDependency ::= SEQUENCE
+{
+ aocService [0] INTEGER,
+ chargingZone [1] INTEGER OPTIONAL
+ --
+ -- Note that these values correspond to the contents
+ -- of the attributes aocServiceId and zoneId
+ -- respectively.
+ --
+}
+
+ServiceKey ::= INTEGER -- (0..2147483647)
+
+SimpleIntegerName ::= INTEGER
+
+SimpleStringName ::= GraphicString
+
+SMSResult ::= Diagnostics
+
+SmsTpDestinationNumber ::= OCTET STRING
+ --
+ -- This type contains the binary coded decimal representation of
+ -- the SMS address field the encoding of the octet string is in
+ -- accordance with the definition of address fields in TS 23.040.
+ -- This encoding includes type of number and numbering plan indication
+ -- together with the address value range.
+ --
+
+SpeechVersionIdentifier ::= OCTET STRING -- (SIZE(1))
+-- see GSM 08.08
+
+-- 000 0001 GSM speech full rate version 1
+-- 001 0001 GSM speech full rate version 2 used for enhanced full rate
+-- 010 0001 GSM speech full rate version 3 for future use
+-- 000 0101 GSM speech half rate version 1
+-- 001 0101 GSM speech half rate version 2 for future use
+-- 010 0101 GSM speech half rate version 3 for future use
+
+SSActionResult ::= Diagnostics
+
+SSActionType ::= ENUMERATED
+{
+ registration (0),
+ erasure (1),
+ activation (2),
+ deactivation (3),
+ interrogation (4),
+ invocation (5),
+ passwordRegistration (6),
+ ussdInvocation (7)
+}
+
+-- ussdInvocation (7) include ussd phase 1,phase 2
+
+--SS Request = SSActionType
+
+SS-Code ::= OCTET STRING -- (SIZE (1))
+ -- This type is used to represent the code identifying a single
+ -- supplementary service, a group of supplementary services, or
+ -- all supplementary services. The services and abbreviations
+ -- used are defined in TS 3GPP TS 22.004 [5]. The internal structure is
+ -- defined as follows:
+ --
+ -- bits 87654321: group (bits 8765), and specific service
+ -- (bits 4321) ussd = ff
+
+-- allSS (0x00),
+-- reserved for possible future use
+-- all SS
+--
+-- allLineIdentificationSS (0x10),
+-- reserved for possible future use
+-- all line identification SS
+--
+-- calling-line-identification-presentation (0x11),
+-- calling line identification presentation
+-- calling-line-identification-restriction (0x12),
+-- calling line identification restriction
+-- connected-line-identification-presentation (0x13),
+-- connected line identification presentation
+-- connected-line-identification-restriction (0x14),
+-- connected line identification restriction
+-- malicious-call-identification (0x15),
+-- reserved for possible future use
+-- malicious call identification
+--
+-- allNameIdentificationSS (0x18),
+-- all name identification SS
+-- calling-name-presentation (0x19),
+-- calling name presentation
+--
+-- SS-Codes '00011010'B, to '00011111'B, are reserved for future
+-- NameIdentification Supplementary Service use.
+--
+-- allForwardingSS (0x20),
+-- all forwarding SS
+-- call-forwarding-unconditional (0x21),
+-- call forwarding unconditional
+-- call-deflection (0x24),
+-- call deflection
+-- allCondForwardingSS (0x28),
+-- all conditional forwarding SS
+-- call-forwarding-on-mobile-subscriber-busy (0x29),
+-- call forwarding on mobile subscriber busy
+-- call-forwarding-on-no-reply (0x2a),
+-- call forwarding on no reply
+-- call-forwarding-on-mobile-subscriber-not-reachable (0x2b),
+-- call forwarding on mobile subscriber not reachable
+--
+-- allCallOfferingSS (0x30),
+-- reserved for possible future use
+-- all call offering SS includes also all forwarding SS
+--
+-- explicit-call-transfer (0x31),
+-- explicit call transfer
+-- mobile-access-hunting (0x32),
+-- reserved for possible future use
+-- mobile access hunting
+--
+-- allCallCompletionSS (0x40),
+-- reserved for possible future use
+-- all Call completion SS
+--
+-- call-waiting (0x41),
+-- call waiting
+-- call-hold (0x42),
+-- call hold
+-- completion-of-call-to-busy-subscribers-originating-side (0x43),
+-- completion of call to busy subscribers, originating side
+-- completion-of-call-to-busy-subscribers-destination-side (0x44),
+-- completion of call to busy subscribers, destination side
+-- this SS-Code is used only in InsertSubscriberData and DeleteSubscriberData
+--
+-- multicall (0x45),
+-- multicall
+--
+-- allMultiPartySS (0x50),
+-- reserved for possible future use
+-- all multiparty SS
+--
+-- multiPTY (0x51),
+-- multiparty
+--
+-- allCommunityOfInterest-SS (0x60),
+-- reserved for possible future use
+-- all community of interest SS
+-- closed-user-group (0x61),
+-- closed user group
+--
+-- allChargingSS (0x70),
+-- reserved for possible future use
+-- all charging SS
+-- advice-of-charge-information (0x71),
+-- advice of charge information
+-- advice-of-charge-charging (0x72),
+-- advice of charge charging
+--
+-- allAdditionalInfoTransferSS (0x80),
+-- reserved for possible future use
+-- all additional information transfer SS
+-- uUS1-user-to-user-signalling (0x81),
+-- UUS1 user-to-user signalling
+-- uUS2-user-to-user-signalling (0x82),
+-- UUS2 user-to-user signalling
+-- uUS3-user-to-user-signalling (0x83),
+-- UUS3 user-to-user signalling
+--
+-- allBarringSS (0x90),
+-- all barring SS
+-- barringOfOutgoingCalls (0x91),
+-- barring of outgoing calls
+-- barring-of-all-outgoing-calls (0x92),
+-- barring of all outgoing calls
+-- barring-of-outgoing-international-calls (0x93),
+-- barring of outgoing international calls
+-- boicExHC (0x94),
+-- barring of outgoing international calls except those directed
+-- to the home PLMN
+-- barringOfIncomingCalls (0x99),
+-- barring of incoming calls
+-- barring-of-all-incoming-calls (0x9a),
+-- barring of all incoming calls
+-- barring-of-incoming-calls-when-roaming-outside-home-PLMN-Country (0x9b),
+-- barring of incoming calls when roaming outside home PLMN
+-- Country
+--
+-- allCallPrioritySS (0xa0),
+-- reserved for possible future use
+-- all call priority SS
+-- enhanced-Multilevel-Precedence-Pre-emption-EMLPP-service (0xa1),
+-- enhanced Multilevel Precedence Pre-emption 'EMLPP) service
+--
+-- allLCSPrivacyException (0xb0),
+-- all LCS Privacy Exception Classes
+-- universal (0xb1),
+-- allow location by any LCS client
+-- callrelated (0xb2),
+-- allow location by any value added LCS client to which a call
+-- is established from the target MS
+-- callunrelated (0xb3),
+-- allow location by designated external value added LCS clients
+-- plmnoperator (0xb4),
+-- allow location by designated PLMN operator LCS clients
+--
+-- allMOLR-SS (0xc0),
+-- all Mobile Originating Location Request Classes
+-- basicSelfLocation (0xc1),
+-- allow an MS to request its own location
+-- autonomousSelfLocation (0xc2),
+-- allow an MS to perform self location without interaction
+-- with the PLMN for a predetermined period of time
+-- transferToThirdParty (0xc3),
+-- allow an MS to request transfer of its location to another LCS client
+--
+-- allPLMN-specificSS (0xf0),
+-- plmn-specificSS-1 (0xf1),
+-- plmn-specificSS-2 (0xf2),
+-- plmn-specificSS-3 (0xf3),
+-- plmn-specificSS-4 (0xf4),
+-- plmn-specificSS-5 (0xf5),
+-- plmn-specificSS-6 (0xf6),
+-- plmn-specificSS-7 (0xf7),
+-- plmn-specificSS-8 (0xf8),
+-- plmn-specificSS-9 (0xf9),
+-- plmn-specificSS-A (0xfa),
+-- plmn-specificSS-B (0xfb),
+-- plmn-specificSS-C (0xfc),
+-- plmn-specificSS-D (0xfd),
+-- plmn-specificSS-E (0xfe),
+-- ussd (0xff)
+
+
+SSParameters ::= CHOICE
+{
+ forwardedToNumber [0] ForwardToNumber,
+ unstructuredData [1] OCTET STRING
+}
+
+SupplServices ::= SET OF SS-Code
+
+SuppServiceUsed ::= SEQUENCE
+{
+ ssCode [0] SS-Code OPTIONAL,
+ ssTime [1] TimeStamp OPTIONAL
+}
+
+SwitchoverTime ::= SEQUENCE
+{
+ hour INTEGER , -- (0..23),
+ minute INTEGER , -- (0..59),
+ second INTEGER -- (0..59)
+}
+
+SystemType ::= ENUMERATED
+ -- "unknown" is not to be used in PS domain.
+{
+ unknown (0),
+ iuUTRAN (1),
+ gERAN (2)
+}
+
+TBCD-STRING ::= OCTET STRING
+ -- This type (Telephony Binary Coded Decimal String) is used to
+ -- represent several digits from 0 through 9, *, #, a, b, c, two
+ -- digits per octet, each digit encoded 0000 to 1001 (0 to 9),
+ -- 1010 (*), 1011 (#), 1100 (a), 1101 (b) or 1110 (c); 1111 used
+ -- as filler when there is an odd number of digits.
+
+ -- bits 8765 of octet n encoding digit 2n
+ -- bits 4321 of octet n encoding digit 2(n-1) +1
+
+TariffId ::= INTEGER
+
+TariffPeriod ::= SEQUENCE
+{
+ switchoverTime [0] SwitchoverTime,
+ tariffId [1] INTEGER
+ -- Note that the value of tariffId corresponds
+ -- to the attribute tariffId.
+}
+
+TariffPeriods ::= SET OF TariffPeriod
+
+TariffSystemStatus ::= ENUMERATED
+{
+ available (0), -- available for modification
+ checked (1), -- "frozen" and checked
+ standby (2), -- "frozen" awaiting activation
+ active (3) -- "frozen" and active
+}
+
+
+TimeStamp ::= OCTET STRING -- (SIZE(9))
+ --
+ -- The contents of this field are a compact form of the UTCTime format
+ -- containing local time plus an offset to universal time. Binary coded
+ -- decimal encoding is employed for the digits to reduce the storage and
+ -- transmission overhead
+ -- e.g. YYMMDDhhmmssShhmm
+ -- where
+ -- YY = Year 00 to 99 BCD encoded
+ -- MM = Month 01 to 12 BCD encoded
+ -- DD = Day 01 to 31 BCD encoded
+ -- hh = hour 00 to 23 BCD encoded
+ -- mm = minute 00 to 59 BCD encoded
+ -- ss = second 00 to 59 BCD encoded
+ -- S = Sign 0 = "+", "-" ASCII encoded
+ -- hh = hour 00 to 23 BCD encoded
+ -- mm = minute 00 to 59 BCD encoded
+ --
+
+TrafficChannel ::= ENUMERATED
+{
+ fullRate (0),
+ halfRate (1)
+}
+
+TranslatedNumber ::= BCDDirectoryNumber
+
+TransparencyInd ::= ENUMERATED
+{
+ transparent (0),
+ nonTransparent (1)
+}
+
+ROUTE ::= CHOICE
+{
+ rOUTENumber [0] INTEGER,
+ rOUTEName [1] GraphicString
+}
+
+--rOUTEName 1 10 octet
+
+TSChangeover ::= SEQUENCE
+{
+ newActiveTS [0] INTEGER,
+ newStandbyTS [1] INTEGER,
+-- changeoverTime [2] GeneralizedTime OPTIONAL,
+ authkey [3] OCTET STRING OPTIONAL,
+ checksum [4] OCTET STRING OPTIONAL,
+ versionNumber [5] OCTET STRING OPTIONAL
+ -- Note that if the changeover time is not
+ -- specified then the change is immediate.
+}
+
+TSCheckError ::= SEQUENCE
+{
+ errorId [0] TSCheckErrorId
+ --fail [1] ANY DEFINED BY errorId OPTIONAL
+}
+
+TSCheckErrorId ::= CHOICE
+{
+ globalForm [0] OBJECT IDENTIFIER,
+ localForm [1] INTEGER
+}
+
+TSCheckResult ::= CHOICE
+{
+ success [0] NULL,
+ fail [1] SET OF TSCheckError
+}
+
+TSCopyTariffSystem ::= SEQUENCE
+{
+ oldTS [0] INTEGER,
+ newTS [1] INTEGER
+}
+
+TSNextChange ::= CHOICE
+{
+ noChangeover [0] NULL,
+ tsChangeover [1] TSChangeover
+}
+
+TypeOfSubscribers ::= ENUMERATED
+{
+ home (0), -- HPLMN subscribers
+ visiting (1), -- roaming subscribers
+ all (2)
+}
+
+TypeOfTransaction ::= ENUMERATED
+{
+ successful (0),
+ unsuccessful (1),
+ all (2)
+}
+
+Vertical-Accuracy ::= OCTET STRING -- (SIZE (1))
+ -- bit 8 = 0
+ -- bits 7-1 = 7 bit Vertical Uncertainty Code defined in 3G TS 23.032.
+ -- The vertical location error should be less than the error indicated
+ -- by the uncertainty code with 67% confidence.
+
+ISDNAddressString ::= AddressString
+
+EmlppPriority ::= OCTET STRING -- (SIZE (1))
+
+--priorityLevelA EMLPP-Priority ::= 6
+--priorityLevelB EMLPP-Priority ::= 5
+--priorityLevel0 EMLPP-Priority ::= 0
+--priorityLevel1 EMLPP-Priority ::= 1
+--priorityLevel2 EMLPP-Priority ::= 2
+--priorityLevel3 EMLPP-Priority ::= 3
+--priorityLevel4 EMLPP-Priority ::= 4
+--See 29.002
+
+
+EASubscriberInfo ::= OCTET STRING -- (SIZE (3))
+ -- The internal structure is defined by the Carrier Identification
+ -- parameter in ANSI T1.113.3. Carrier codes between "000" and "999" may
+ -- be encoded as 3 digits using "000" to "999" or as 4 digits using
+ -- "0000" to "0999". Carrier codes between "1000" and "9999" are encoded
+ -- using 4 digits.
+
+SelectedCIC ::= OCTET STRING -- (SIZE (3))
+
+PortedFlag ::= ENUMERATED
+{
+ numberNotPorted (0),
+ numberPorted (1)
+}
+
+SubscriberCategory ::= OCTET STRING -- (SIZE (1))
+-- unknownuser = 0x00,
+-- frenchuser = 0x01,
+-- englishuser = 0x02,
+-- germanuser = 0x03,
+-- russianuser = 0x04,
+-- spanishuser = 0x05,
+-- specialuser = 0x06,
+-- reserveuser = 0x09,
+-- commonuser = 0x0a,
+-- superioruser = 0x0b,
+-- datacalluser = 0x0c,
+-- testcalluser = 0x0d,
+-- spareuser = 0x0e,
+-- payphoneuser = 0x0f,
+-- coinuser = 0x20,
+-- isup224 = 0xe0
+
+
+CUGOutgoingAccessIndicator ::= ENUMERATED
+{
+ notCUGCall (0),
+ cUGCall (1)
+}
+
+CUGInterlockCode ::= OCTET STRING -- (SIZE (4))
+
+--
+
+CUGOutgoingAccessUsed ::= ENUMERATED
+{
+ callInTheSameCUGGroup (0),
+ callNotInTheSameCUGGroup (1)
+}
+
+SMSTEXT ::= OCTET STRING
+
+MSCCIC ::= INTEGER -- (0..65535)
+
+RNCorBSCId ::= OCTET STRING -- (SIZE (3))
+--octet order is the same as RANAP/BSSAP signaling
+--if spc is coded as 14bit, then OCTET STRING1 will filled with 00 ,for example rnc id = 123 will be coded as 00 01 23
+--OCTET STRING1
+--OCTET STRING2
+--OCTET STRING3
+
+MSCId ::= OCTET STRING -- (SIZE (3))
+--National network format , octet order is the same as ISUP signaling
+--if spc is coded as 14bit, then OCTET STRING1 will filled with 00,,for example rnc id = 123 will be coded as 00 01 23
+--OCTET STRING1
+--OCTET STRING2
+--OCTET STRING3
+
+EmergencyCallFlag ::= ENUMERATED
+{
+ notEmergencyCall (0),
+ emergencyCall (1)
+}
+
+CUGIncomingAccessUsed ::= ENUMERATED
+{
+ callInTheSameCUGGroup (0),
+ callNotInTheSameCUGGroup (1)
+}
+
+SmsUserDataType ::= OCTET STRING -- (SIZE (1))
+--
+--00 concatenated-short-messages-8-bit-reference-number
+--01 special-sms-message-indication
+--02 reserved
+--03 Value not used to avoid misinterpretation as <LF>
+--04 characterapplication-port-addressing-scheme-8-bit-address
+--05 application-port-addressing-scheme-16-bit-address
+--06 smsc-control-parameters
+--07 udh-source-indicator
+--08 concatenated-short-message-16-bit-reference-number
+--09 wireless-control-message-protocol
+--0A text-formatting
+--0B predefined-sound
+--0C user-defined-sound-imelody-max-128-bytes
+--0D predefined-animation
+--0E large-animation-16-16-times-4-32-4-128-bytes
+--0F small-animation-8-8-times-4-8-4-32-bytes
+--10 large-picture-32-32-128-bytes
+--11 small-picture-16-16-32-bytes
+--12 variable-picture
+--13 User prompt indicator
+--14 Extended Object
+--15 Reused Extended Object
+--16 Compression Control
+--17 Object Distribution Indicator
+--18 Standard WVG object
+--19 Character Size WVG object
+--1A Extended Object Data Request Command
+--1B-1F Reserved for future EMS features (see subclause 3.10)
+--20 RFC 822 E-Mail Header
+--21 Hyperlink format element
+--22 Reply Address Element
+--23 - 6F Reserved for future use
+--70 - 7F (U)SIM Toolkit Security Headers
+--80 - 9F SME to SME specific use
+--A0 - BF Reserved for future use
+--C0 - DF SC specific use
+--E0 - FE Reserved for future use
+--FF normal SMS
+
+ConcatenatedSMSReferenceNumber ::= INTEGER -- (0..65535)
+
+MaximumNumberOfSMSInTheConcatenatedSMS ::= INTEGER -- (0..255)
+
+SequenceNumberOfTheCurrentSMS ::= INTEGER -- (0..255)
+
+SequenceNumber ::= INTEGER
+
+--(1... )
+--
+
+DisconnectParty ::= ENUMERATED
+{
+ callingPartyRelease (0),
+ calledPartyRelease (1),
+ networkRelease (2)
+}
+
+ChargedParty ::= ENUMERATED
+{
+ callingParty (0),
+ calledParty (1)
+}
+
+ChargeAreaCode ::= OCTET STRING -- (SIZE (1..3))
+
+CUGIndex ::= OCTET STRING -- (SIZE (2))
+
+GuaranteedBitRate ::= ENUMERATED
+{
+ gBR14400BitsPerSecond (1), -- BS20 non-transparent
+ gBR28800BitsPerSecond (2), -- BS20 non-transparent and transparent,
+ -- BS30 transparent and multimedia
+ gBR32000BitsPerSecond (3), -- BS30 multimedia
+ gBR33600BitsPerSecond (4), -- BS30 multimedia
+ gBR56000BitsPerSecond (5), -- BS30 transparent and multimedia
+ gBR57600BitsPerSecond (6), -- BS20 non-transparent
+ gBR64000BitsPerSecond (7), -- BS30 transparent and multimedia
+
+ gBR12200BitsPerSecond (106), -- AMR speech
+ gBR10200BitsPerSecond (107), -- AMR speech
+ gBR7950BitsPerSecond (108), -- AMR speech
+ gBR7400BitsPerSecond (109), -- AMR speech
+ gBR6700BitsPerSecond (110), -- AMR speech
+ gBR5900BitsPerSecond (111), -- AMR speech
+ gBR5150BitsPerSecond (112), -- AMR speech
+ gBR4750BitsPerSecond (113) -- AMR speech
+}
+
+MaximumBitRate ::= ENUMERATED
+{
+ mBR14400BitsPerSecond (1), -- BS20 non-transparent
+ mBR28800BitsPerSecond (2), -- BS20 non-transparent and transparent,
+ -- BS30 transparent and multimedia
+ mBR32000BitsPerSecond (3), -- BS30 multimedia
+ mBR33600BitsPerSecond (4), -- BS30 multimedia
+ mBR56000BitsPerSecond (5), -- BS30 transparent and multimedia
+ mBR57600BitsPerSecond (6), -- BS20 non-transparent
+ mBR64000BitsPerSecond (7), -- BS30 transparent and multimedia
+
+ mBR12200BitsPerSecond (106), -- AMR speech
+ mBR10200BitsPerSecond (107), -- AMR speech
+ mBR7950BitsPerSecond (108), -- AMR speech
+ mBR7400BitsPerSecond (109), -- AMR speech
+ mBR6700BitsPerSecond (110), -- AMR speech
+ mBR5900BitsPerSecond (111), -- AMR speech
+ mBR5150BitsPerSecond (112), -- AMR speech
+ mBR4750BitsPerSecond (113) -- AMR speech
+}
+
+
+HLC ::= OCTET STRING
+
+-- this parameter is a 1:1 copy of the contents (i.e. starting with octet 3) of the "high layer compatibility" parameter of ITU-T Q.931 [35].
+
+LLC ::= OCTET STRING
+
+-- this parameter is a 1:1 copy of the contents (i.e. starting with octet 3) of the "low layer compatibility" parameter of ITU-T Q.931 [35].
+
+
+ISDN-BC ::= OCTET STRING
+
+-- this parameter is a 1:1 copy of the contents (i.e. starting with octet 3) of the "bearer capability" parameter of ITU-T Q.931 [35].
+
+ModemType ::= ENUMERATED
+{
+ none-modem (0),
+ modem-v21 (1),
+ modem-v22 (2),
+ modem-v22-bis (3),
+ modem-v23 (4),
+ modem-v26-ter (5),
+ modem-v32 (6),
+ modem-undef-interface (7),
+ modem-autobauding1 (8),
+ no-other-modem-type (31),
+ modem-v34 (33)
+}
+
+UssdCodingScheme ::= OCTET STRING
+
+UssdString ::= OCTET STRING
+
+UssdNotifyCounter ::= INTEGER -- (0..255)
+
+UssdRequestCounter ::= INTEGER -- (0..255)
+
+Classmark3 ::= OCTET STRING -- (SIZE(2))
+
+OptimalRoutingDestAddress ::= BCDDirectoryNumber
+
+GAI ::= OCTET STRING -- (SIZE(7))
+--such as 64 F0 00 00 ABCD 1234
+
+ChangeOfglobalAreaID ::= SEQUENCE
+{
+ location [0] GAI,
+ changeTime [1] TimeStamp
+}
+
+InteractionWithIP ::= NULL
+
+RouteAttribute ::= ENUMERATED
+{
+ cas (0),
+ tup (1),
+ isup (2),
+ pra (3),
+ bicc (4),
+ sip (5),
+ others (255)
+}
+
+VoiceIndicator ::= ENUMERATED
+{
+ sendToneByLocalMsc (0) ,
+ sendToneByOtherMsc (1),
+ voiceNoIndication (3)
+}
+
+BCategory ::= ENUMERATED
+{
+ subscriberFree (0),
+ subscriberBusy (1),
+ subscriberNoIndication (3)
+}
+
+CallType ::= ENUMERATED
+{
+ unknown (0),
+ internal (1),
+ incoming (2),
+ outgoing (3),
+ tandem (4)
+}
+
+-- END
+END
+}
+
+1;
+
--- /dev/null
+package FS::contact_Mixin;
+
+use strict;
+use FS::Record qw( qsearchs );
+use FS::contact;
+
+=item contact_obj
+
+Returns the contact object, if any (see L<FS::contact>).
+
+=cut
+
+sub contact_obj {
+ my $self = shift;
+ return '' unless $self->contactnum;
+ qsearchs( 'contact', { 'contactnum' => $self->contactnum } );
+}
+
+1;
=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
$self->custnum,
$cust_main->first,
$cust_main->last,
+ $cust_main->company,
$cust_main->address1,
$cust_main->address2,
$cust_main->city,
#something more elaborate if $_->amount ne ->cust_pay->paid ?
+ my $desc = $self->mt('Payment received').' '.
+ time2str($date_format,$_->cust_pay->_date );
+ $desc .= $self->mt(' via ' . $_->cust_pay->payby_payinfo_pretty)
+ if ( $self->conf->exists('invoice_payment_details') );
+
push @b, {
- 'description' => $self->mt('Payment received').' '.
- time2str($date_format,$_->cust_pay->_date ),
+ 'description' => $desc,
'amount' => sprintf("%.2f", $_->amount )
};
+
}
@b;
delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
$hash{custnum} = $h_cust_main->custnum;
- my $tax_loc = qsearchs('cust_location', \%hash) # unlikely
- || FS::cust_location->new({ %hash });
- 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;
use vars qw( $import );
use Locale::Country;
use FS::UID qw( dbh driver_name );
-use FS::Record qw( qsearch ); #qsearchs );
+use FS::Record qw( qsearch qsearchs );
use FS::Conf;
use FS::prospect_main;
use FS::cust_main;
sub table { 'cust_location'; }
+=item 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<will> be inserted. (This is a
+change from the "new_or_existing" method that this replaces.)
+
+The following fields are considered "essential" and I<must> 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<empty> 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,
$payby = 'PREP' if $amount;
- } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
+ } elsif ( $self->payby =~ /^(CASH|WEST|MCRD|PPAL)$/ ) {
$payby = $1;
$self->payby('BILL');
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 ) {
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 );
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 );
}
-# 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<its> 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;
Required arguments in the hashref are I<method>, and I<amount>
-Available methods are: I<CC>, I<ECHECK> and I<LEC>
+Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
'CC' => 'CARD',
'ECHECK' => 'CHEK',
'LEC' => 'LECB',
+ 'PAYPAL' => 'PPAL',
);
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
);
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]';
if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'};
my %insert_params = map { $opt->{$_} ? ( $_ => $opt->{$_} ) : () }
- qw( ticket_subject ticket_queue );
+ qw( ticket_subject ticket_queue allow_pkgpart );
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- if ( $opt->{'cust_location'} &&
- ( ! $cust_pkg->locationnum || $cust_pkg->locationnum == -1 ) ) {
- my $error = $opt->{'cust_location'}->insert;
+ if ( $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 );
'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 ) {
''; #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<FS::cust_pkg>) for this customer.
# 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'};
##
'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',
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') {
'extra_headers' => \@extra_headers,
'extra_fields' => \@extra_fields,
};
+ warn Data::Dumper::Dumper($sql_query);
+ $sql_query;
}
my @cust_main = ();
+ my @fuzzy_mod = 'i';
+ my $conf = new FS::Conf;
+ my $fuzziness = $conf->config('fuzzy-fuzziness');
+ push @fuzzy_mod, $fuzziness if $fuzziness;
+
check_and_rebuild_fuzzyfiles();
foreach my $field ( keys %$fuzzy ) {
next unless scalar(@$all);
my %match = ();
- $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) );
+ $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, \@fuzzy_mod, @$all ) );
next if !keys(%match);
my $in_matches = 'IN (' .
If the taxclass is set, then it will be
"Anytown, Alameda County, CA, US (International)".
-Currently it will not contain the district, even if the city+county+state
-is not unique.
-
-OPTIONS may contain "no_taxclass" (hides taxclass) and/or "no_city"
-(hides city). It may also contain "out", in which case, if this
-region (district+city+county+state+country) contains no non-zero
-taxes, the label will read "Out of taxable region(s)".
+OPTIONS may contain "with_taxclass", "with_city", and "with_district" to show
+those fields. It may also contain "out", in which case, if this region
+(district+city+county+state+country) contains no non-zero taxes, the label
+will read "Out of taxable region(s)".
=cut
my $label = $self->country;
$label = $self->state.", $label" if $self->state;
$label = $self->county." County, $label" if $self->county;
- if (!$opt{no_city}) {
+ if ($opt{with_city}) {
$label = $self->city.", $label" if $self->city;
+ if ($opt{with_district} and $self->district) {
+ $label = $self->district . ", $label";
+ }
}
# ugly labels when taxclass and taxname are both non-null...
# but this is how the tax report does it
- if (!$opt{no_taxclass}) {
+ if ($opt{with_taxclass}) {
$label = "$label (".$self->taxclass.')' if $self->taxclass;
}
$label = $self->taxname." ($label)" if $self->taxname;
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
}
} #$object
} #$table
- FS::upgrade_journal->set_done('cust_pay__parse_paybatch');
+ FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
}
}
package FS::cust_pkg;
use strict;
-use base qw( FS::otaker_Mixin FS::cust_main_Mixin FS::location_Mixin
+use base qw( FS::otaker_Mixin FS::cust_main_Mixin
+ FS::contact_Mixin FS::location_Mixin
FS::m2m_Common FS::option_Common );
use vars qw($disable_agentcheck $DEBUG $me);
use Carp qw(cluck);
use 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;
=cut
sub table { 'cust_pkg'; }
-sub cust_linked { $_[0]->cust_main_custnum; }
+sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
sub cust_unlinked_msg {
my $self = shift;
"WARNING: can't find cust_main.custnum ". $self->custnum.
an optional queue name for ticket additions
+=item allow_pkgpart
+
+Don't check the legality of the package definition. This should be used
+when performing a package change that doesn't change the pkgpart (i.e.
+a location change).
+
=back
=cut
sub insert {
my( $self, %options ) = @_;
- my $error = $self->check_pkgpart;
+ my $error;
+ $error = $self->check_pkgpart unless $options{'allow_pkgpart'};
return $error if $error;
my $part_pkg = $self->part_pkg;
$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')
=item check_pkgpart
+Check the pkgpart to make sure it's allowed with the reg_code and/or
+promo_code of the package (if present) and with the customer's agent.
+Called from C<insert>, unless we are doing a package change that doesn't
+affect pkgpart.
+
=cut
sub check_pkgpart {
my $self = shift;
- my $error = $self->ut_numbern('pkgpart');
- return $error if $error;
+ # my $error = $self->ut_numbern('pkgpart'); # already done
+ my $error;
if ( $self->reg_code ) {
unless ( grep { $self->pkgpart == $_->pkgpart }
my %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 ) {
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;
$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
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<FS::part_pkg>).
$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";
$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
# (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;
}
}
+ # transfer discounts, if we're not changing pkgpart
+ if ( $same_pkgpart ) {
+ foreach my $old_discount ($self->cust_pkg_discount_active) {
+ # don't remove the old discount, we may still need to bill that package.
+ my $new_discount = new FS::cust_pkg_discount {
+ 'pkgnum' => $cust_pkg->pkgnum,
+ 'discountnum' => $old_discount->discountnum,
+ 'months_used' => $old_discount->months_used,
+ };
+ $error = $new_discount->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error transferring discounts: $error";
+ }
+ }
+ }
+
# Order any supplemental packages.
my $part_pkg = $cust_pkg->part_pkg;
my @old_supp_pkgs = $self->supplemental_pkgs;
my $new = 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,
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);
#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;
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<calc_setup> of the FS::part_pkg object associated with this billing
=item pkg_label
Returns a label for this package. (Currently "pkgnum: pkg - comment" or
-"pkg-comment" depending on user preference).
+"pkg - comment" depending on user preference).
=cut
$label;
}
+=item pkg_locale
+
+Returns a customer-localized label for this package.
+
+=cut
+
+sub pkg_locale {
+ my $self = shift;
+ $self->part_pkg->pkg_locale( $self->cust_main->locale );
+}
+
=item primary_cust_svc
Returns a primary service (as FS::cust_svc object) if one can be identified.
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
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);
}
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);
}
use FS::Record qw( qsearch qsearchs dbh );
use FS::part_export;
use FS::part_svc;
+use FS::svc_export_machine;
@ISA = qw(FS::Record);
} #end of duplicate check, whew
$error = $self->SUPER::insert;
+
+ my $part_export = $self->part_export;
+ if ( !$error and $part_export->default_machine ) {
+ foreach my $cust_svc ( $self->part_svc->cust_svc ) {
+ my $svc_export_machine = FS::svc_export_machine->new({
+ 'exportnum' => $self->exportnum,
+ 'svcnum' => $cust_svc->svcnum,
+ 'machinenum' => $part_export->default_machine,
+ });
+ $error ||= $svc_export_machine->insert;
+ }
+ }
+
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
=cut
-# the delete method can be inherited from FS::Record
+sub delete {
+ my $self = shift;
+ my $dbh = dbh;
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+
+ my $error = $self->SUPER::delete;
+ foreach ($self->svc_export_machine) {
+ $error ||= $_->delete;
+ }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+}
+
=item replace OLD_RECORD
qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
}
+=item svc_export_machine
+
+Returns all export hostname records (L<FS::svc_export_machine>) for this
+combination of svcpart and exportnum.
+
+=cut
+
+sub svc_export_machine {
+ my $self = shift;
+ qsearch({
+ 'table' => 'svc_export_machine',
+ 'select' => 'svc_export_machine.*',
+ 'addl_from' => 'JOIN cust_svc USING (svcnum)',
+ 'hashref' => { 'exportnum' => $self->exportnum },
+ 'extra_sql' => ' AND cust_svc.svcpart = '.$self->svcpart,
+ });
+}
+
=back
=head1 BUGS
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');
}
'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');
--- /dev/null
+package FS::part_event::Condition::cust_bill_owed_percent;
+
+use strict;
+use FS::cust_bill;
+
+use base qw( FS::part_event::Condition );
+
+sub description {
+ 'Percentage owed on specific invoice';
+}
+
+sub eventtable_hashref {
+ { 'cust_main' => 0,
+ 'cust_bill' => 1,
+ 'cust_pkg' => 0,
+ };
+}
+
+sub option_fields {
+ (
+ 'owed' => { 'label' => 'Percentage of invoice owed over',
+ 'type' => 'percentage',
+ 'value' => '0', #default
+ },
+ );
+}
+
+sub condition {
+ #my($self, $cust_bill, %opt) = @_;
+ my($self, $cust_bill) = @_;
+
+ my $percent = $self->option('owed') || 0;
+ my $over = sprintf('%.2f',
+ $cust_bill->charged * $percent / 100);
+
+ $cust_bill->owed > $over;
+}
+
+sub condition_sql {
+ my( $class, $table ) = @_;
+
+ # forces the option to be an integer--do we care?
+ my $percent = $class->condition_sql_option_integer('owed');
+
+ my $owed_sql = FS::cust_bill->owed_sql;
+
+ "$owed_sql > CAST( cust_bill.charged * $percent / 100 AS DECIMAL(10,2) )";
+}
+
+1;
--- /dev/null
+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;
+
# Run the event, at most, a number of times equal to the number of
# distinct invoices that contain line items from this package.
+sub option_fields {
+ (
+ 'paid' => { 'label' => 'Only count paid bills',
+ 'type' => 'checkbox',
+ 'value' => 'Y',
+ },
+ )
+}
+
sub eventtable_hashref {
{ 'cust_main' => 0,
'cust_bill' => 0,
sub condition {
my($self, $cust_pkg, %opt) = @_;
- my %invnum;
- $invnum{$_->invnum} = 1
- foreach ( qsearch('cust_bill_pkg', { 'pkgnum' => $cust_pkg->pkgnum }) );
+ my @cust_bill_pkg = qsearch('cust_bill_pkg', { pkgnum=>$cust_pkg->pkgnum });
+
+ @cust_bill_pkg = grep { ($_->owed_setup + $_->owed_recur) == 0 }
+ @cust_bill_pkg
+ if $self->option('paid');
+
+ my %invnum = ();
+ $invnum{$_->invnum} = 1 foreach @cust_bill_pkg;
+
my @events = qsearch( {
'table' => 'cust_event',
'hashref' => { 'eventpart' => $self->eventpart,
sub condition_sql {
my( $self, $table ) = @_;
+ #paid flag not yet implemented here, but that's okay, a partial optimization
+ # is better than none
+
"(
( SELECT COUNT(distinct(invnum))
FROM cust_bill_pkg
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- my $error = $self->SUPER::insert(@_);
+ my $error = $self->SUPER::insert(@_)
+ || $self->replace;
+ # use replace to do all the part_export_machine and default_machine stuff
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
}
- #kinda false laziness with process_m2name
- my @machines = map { $_ =~ s/^\s+//; $_ =~ s/\s+$//; $_ }
- grep /\S/,
- split /[\n\r]{1,2}/,
- $self->part_export_machine_textarea;
-
- foreach my $machine ( @machines ) {
-
- my $part_export_machine = new FS::part_export_machine {
- 'exportnum' => $self->exportnum,
- 'machine' => $machine,
- };
- $error = $part_export_machine->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
- }
-
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
}
sub replace {
my $self = shift;
+ my $old = $self->replace_old;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
-
- my $error = $self->SUPER::replace(@_);
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
+ my $error;
if ( $self->part_export_machine_textarea ) {
}
}
+ if ( $self->default_machine_name eq $machine ) {
+ $self->default_machine( $part_export_machine{$machine}->machinenum );
+ }
+
delete $part_export_machine{$machine}; #so we don't disable it below
} else {
return $error;
}
+ if ( $self->default_machine_name eq $machine ) {
+ $self->default_machine( $part_export_machine->machinenum );
+ }
}
}
-
foreach my $part_export_machine ( values %part_export_machine ) {
$part_export_machine->disabled('Y');
$error = $part_export_machine->replace;
}
}
+ if ( $old->machine ne '_SVC_MACHINE' ) {
+ # then set up the default for any already-attached export_svcs
+ foreach my $export_svc ( $self->export_svc ) {
+ my @svcs = qsearch('cust_svc', { 'svcpart' => $export_svc->svcpart });
+ foreach my $cust_svc ( @svcs ) {
+ my $svc_export_machine = FS::svc_export_machine->new({
+ 'exportnum' => $self->exportnum,
+ 'svcnum' => $cust_svc->svcnum,
+ 'machinenum' => $self->default_machine,
+ });
+ $error ||= $svc_export_machine->insert;
+ }
+ }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ } # if switching to selectable hosts
+
+ } elsif ( $old->machine eq '_SVC_MACHINE' ) {
+ # then we're switching from selectable to non-selectable
+ foreach my $svc_export_machine (
+ qsearch('svc_export_machine', { 'exportnum' => $self->exportnum })
+ ) {
+ $error ||= $svc_export_machine->delete;
+ }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
+ $error = $self->SUPER::replace(@_);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ if ( $self->machine eq '_SVC_MACHINE' and ! $self->default_machine ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "no default export host selected";
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
|| $self->ut_domainn('machine')
|| $self->ut_alpha('exporttype')
;
+
+ if ( $self->machine eq '_SVC_MACHINE' ) {
+ $error ||= $self->ut_numbern('default_machine')
+ } else {
+ $self->set('default_machine', '');
+ }
+
return $error if $error;
$self->nodomain =~ /^(Y?)$/ or return "Illegal nodomain: ". $self->nodomain;
$self;
}
-=item svc_machine
+=item svc_machine SVC_X
+
+Return the export hostname for SVC_X.
=cut
my $svc_export_machine = qsearchs('svc_export_machine', {
'svcnum' => $svc_x->svcnum,
'exportnum' => $self->exportnum,
- })
- #would only happen if you add this export to existing services without a
- #machine set then try to run exports without setting it... right?
- or die "No hostname selected for ".($self->exportname || $self->exporttype);
+ });
+
+ if (!$svc_export_machine) {
+ warn "No hostname selected for ".($self->exportname || $self->exporttype);
+ return $self->default_export_machine->machine;
+ }
return $svc_export_machine->part_export_machine->machine;
}
+=item default_export_machine
+
+Return the default export hostname for this export.
+
+=cut
+
+sub default_export_machine {
+ my $self = shift;
+ my $machinenum = $self->default_machine;
+ if ( $machinenum ) {
+ my $default_machine = FS::part_export_machine->by_key($machinenum);
+ return $default_machine->machine if $default_machine;
+ }
+ # this should not happen
+ die "no default export hostname for export ".$self->exportnum;
+}
+
#these should probably all go away, just let the subclasses define em
=item export_insert SVC_OBJECT
$error = $opt->replace;
die $error if $error;
}
+ # for exports that have selectable hostnames, make sure all services
+ # have a hostname selected
+ foreach my $part_export (
+ qsearch('part_export', { 'machine' => '_SVC_MACHINE' })
+ ) {
+
+ my $exportnum = $part_export->exportnum;
+ my $machinenum = $part_export->default_machine;
+ if (!$machinenum) {
+ my ($first) = $part_export->part_export_machine;
+ if (!$first) {
+ # user intervention really is required.
+ die "Export $exportnum has no hostname options defined.\n".
+ "You must correct this before upgrading.\n";
+ }
+ # warn about this, because we might not choose the right one
+ warn "Export $exportnum (". $part_export->exporttype.
+ ") has no default hostname. Setting to ".$first->machine."\n";
+ $machinenum = $first->machinenum;
+ $part_export->set('default_machine', $machinenum);
+ my $error = $part_export->replace;
+ die $error if $error;
+ }
+
+ # the service belongs to a service def that uses this export
+ # and there is not a hostname selected for this export for that service
+ my $join = ' JOIN export_svc USING ( svcpart )'.
+ ' LEFT JOIN svc_export_machine'.
+ ' ON ( cust_svc.svcnum = svc_export_machine.svcnum'.
+ ' AND export_svc.exportnum = svc_export_machine.exportnum )';
+
+ my @svcs = qsearch( {
+ 'select' => 'cust_svc.*',
+ 'table' => 'cust_svc',
+ 'addl_from' => $join,
+ 'extra_sql' => ' WHERE svcexportmachinenum IS NULL'.
+ ' AND export_svc.exportnum = '.$part_export->exportnum,
+ } );
+ foreach my $cust_svc (@svcs) {
+ my $svc_export_machine = FS::svc_export_machine->new({
+ 'exportnum' => $exportnum,
+ 'machinenum' => $machinenum,
+ 'svcnum' => $cust_svc->svcnum,
+ });
+ my $error = $svc_export_machine->insert;
+ die $error if $error;
+ }
+ }
+
# pass downstream
my %exports_in_use;
$exports_in_use{ref $_} = 1 foreach qsearch('part_export', {});
}
sub export_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
}
$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;
@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',
# 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} ) {
${$_} = $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;
--- /dev/null
+package FS::part_export::test;
+
+use strict;
+use vars qw(%options %info);
+use Tie::IxHash;
+use base qw(FS::part_export);
+
+tie %options, 'Tie::IxHash',
+ 'result' => { label => 'Result',
+ type => 'select',
+ options => [ 'success', 'failure', 'exception' ],
+ default => 'success',
+ },
+ 'errormsg'=> { label => 'Error message',
+ default => 'Test export' },
+ 'insert' => { label => 'Insert', type => 'checkbox', default => 1, },
+ 'delete' => { label => 'Delete', type => 'checkbox', default => 1, },
+ 'replace' => { label => 'Replace',type => 'checkbox', default => 1, },
+ 'suspend' => { label => 'Suspend',type => 'checkbox', default => 1, },
+ 'unsuspend'=>{ label => 'Unsuspend', type => 'checkbox', default => 1, },
+;
+
+%info = (
+ 'svc' => [ qw(svc_acct svc_broadband svc_phone svc_domain) ],
+ 'desc' => 'Test export for development',
+ 'options' => \%options,
+ 'notes' => <<END,
+<P>Test export. Do not use this in production systems.</P>
+<P>This export either always succeeds, always fails (returning an error),
+or always dies, according to the "Result" option. It does nothing else; the
+purpose is purely to simulate success or failure within an export module.</P>
+<P>The checkbox options can be used to turn the export off for certain
+actions, if this is needed.</P>
+END
+);
+
+sub export_insert {
+ my $self = shift;
+ $self->run(@_) if $self->option('insert');
+}
+
+sub export_delete {
+ my $self = shift;
+ $self->run(@_) if $self->option('delete');
+}
+
+sub export_replace {
+ my $self = shift;
+ $self->run(@_) if $self->option('replace');
+}
+
+sub export_suspend {
+ my $self = shift;
+ $self->run(@_) if $self->option('suspend');
+}
+
+sub export_unsuspend {
+ my $self = shift;
+ $self->run(@_) if $self->option('unsuspend');
+}
+
+sub run {
+ my $self = shift;
+ my $svc_x = shift;
+ my $result = $self->option('result');
+ if ( $result eq 'failure' ) {
+ return $self->option('errormsg');
+ } elsif ( $result eq 'exception' ) {
+ die $self->option('errormsg');
+ } else {
+ return '';
+ }
+}
+
+1;
package FS::part_pkg;
+use base qw( FS::m2m_Common FS::o2m_Common FS::option_Common );
use strict;
-use vars qw( @ISA %plans $DEBUG $setup_hack $skip_pkg_svc_hack );
+use vars qw( %plans $DEBUG $setup_hack $skip_pkg_svc_hack );
use Carp qw(carp cluck confess);
use Scalar::Util qw( blessed );
use Time::Local qw( timelocal_nocheck );
use FS::part_pkg_option;
use FS::pkg_class;
use FS::agent;
+use FS::part_pkg_msgcat;
use FS::part_pkg_taxrate;
use FS::part_pkg_taxoverride;
use FS::part_pkg_taxproduct;
use FS::part_pkg_usage;
use FS::part_pkg_vendor;
-@ISA = qw( FS::m2m_Common FS::option_Common );
$DEBUG = 0;
$setup_hack = 0;
$skip_pkg_svc_hack = 0;
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,
Options:
- add_full_period: Bill for the time up to the prorate day plus one full
-billing period after that.
+ billing period after that.
- prorate_round_day: Round the current time to the nearest full day,
-instead of using the exact time.
+ instead of using the exact time.
- prorate_defer_bill: Don't bill the prorate interval until the prorate
-day arrives.
+ day arrives.
- prorate_verbose: Generate details to explain the prorate calculations.
=cut
$add_period = 1;
}
- # if the customer alreqady has a billing day-of-month established,
+ # if the customer already has a billing day-of-month established,
# and it's a valid cutoff day, try to respect it
my $next_bill_day;
if ( my $next_bill = $cust_pkg->cust_main->next_bill_date ) {
my $permonth = $charge / $self->freq;
my $months = ( ( $self->freq - 1 ) + ($mend-$mnow) / ($mend-$mstart) );
-
- if ( $self->option('prorate_verbose',1)
- and $months > 0 and $months < $self->freq ) {
- push @$details,
- 'Prorated (' . time2str('%b %d', $mnow) .
- ' - ' . time2str('%b %d', $mend) . '): ' . $money_char .
- sprintf('%.2f', $permonth * $months + 0.00000001 );
- }
+ # after this, $self->freq - 1 < $months <= $self->freq
# add a full period if currently billing for a partial period
# or periods up to freq_override if billing for an override interval
if ( ($param->{'freq_override'} || 0) > 1 ) {
$months += $param->{'freq_override'} - 1;
- }
- elsif ( $add_period && $months < $self->freq) {
+ # freq_override - 1 correct here?
+ # (probably only if freq == 1, yes?)
+ } elsif ( $add_period && $months < $self->freq ) {
+
+ # '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;
'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,
);
}
#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 ) = @_;
$day_start = $tomorrow;
}
+ $charges = $self->option('monthly_cap')
+ if $self->option('monthly_cap')
+ && $charges > $self->option('monthly_cap');
+
$self->option('recur_fee') + $charges;
}
'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: ',
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
'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 )
}
#returns a reason why not to rate this CDR, or false if the CDR is chargeable
+# lots of false laziness w/voip_inbound
sub check_chargable {
my( $self, $cdr, %flags ) = @_;
if length($self->option_cacheable('ignore_cdrtypenum'))
&& $cdr->cdrtypenum eq $self->option_cacheable('ignore_cdrtypenum'); #eq otherwise 0 matches ''
+ # unlike everything else, use_calltypenum is applied in FS::svc_x::get_cdrs.
+ return "calltypenum != ". $self->option_cacheable('use_calltypenum')
+ if length($self->option_cacheable('use_calltypenum'))
+ && $cdr->calltypenum ne $self->option_cacheable('use_calltypenum'); #ne otherwise 0 matches ''
+
+ return "calltypenum == ". $self->option_cacheable('ignore_calltypenum')
+ if length($self->option_cacheable('ignore_calltypenum'))
+ && $cdr->calltypenum eq $self->option_cacheable('ignore_calltypenum'); #eq otherwise 0 matches ''
+
return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )"
if $self->option_cacheable('skip_dcontext') =~ /\S/
&& grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext'));
'type' => 'checkbox',
},
- 'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to: ',
+ 'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: ',
},
- 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to: ',
+ 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to this cdrtypenum: ',
},
- 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to: ',
+ 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to this cdrtypenum: ',
},
+ 'use_calltypenum' => { 'name' => 'Only charge for CDRs where the CDR Call Type is set to this cdrtypenum: ',
+ },
+
+ 'ignore_calltypenum' => { 'name' => 'Do not charge for CDRs where the CDR Call Type is set to this cdrtypenum: ',
+ },
+
'ignore_disposition' => { 'name' => 'Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: ',
},
use_amaflags
use_carrierid
use_cdrtypenum ignore_cdrtypenum
+ use_calltypenum ignore_calltypenum
ignore_disposition disposition_in
skip_dcontext skip_dstchannel_prefix
skip_dst_length_less skip_lastapp
}
#returns a reason why not to rate this CDR, or false if the CDR is chargeable
+# lots of false laziness w/voip_cdr...
sub check_chargable {
my( $self, $cdr, %flags ) = @_;
- #should have some better way of checking these options from a hash
- #or something
-
- my @opt = qw(
- use_amaflags
- use_carrierid
- use_cdrtypenum
- ignore_cdrtypenum
- disposition_in
- ignore_disposition
- skip_dcontext
- skip_dstchannel_prefix
- skip_dst_length_less
- skip_lastapp
- );
- foreach my $opt (grep !exists($flags{option_cache}->{$_}), @opt ) {
- $flags{option_cache}->{$opt} = $self->option($opt, 1);
- }
- my %opt = %{ $flags{option_cache} };
-
return 'amaflags != 2'
- if $opt{'use_amaflags'} && $cdr->amaflags != 2;
-
- return "disposition NOT IN ( $opt{'disposition_in'} )"
- if $opt{'disposition_in'} =~ /\S/
- && !grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $opt{'disposition_in'});
-
- return "disposition IN ( $opt{'ignore_disposition'} )"
- if $opt{'ignore_disposition'} =~ /\S/
- && grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $opt{'ignore_disposition'});
-
- return "carrierid != $opt{'use_carrierid'}"
- if length($opt{'use_carrierid'})
- && $cdr->carrierid ne $opt{'use_carrierid'}; #ne otherwise 0 matches ''
+ if $self->option_cacheable('use_amaflags') && $cdr->amaflags != 2;
- return "cdrtypenum != $opt{'use_cdrtypenum'}"
- if length($opt{'use_cdrtypenum'})
- && $cdr->cdrtypenum ne $opt{'use_cdrtypenum'}; #ne otherwise 0 matches ''
-
- return "cdrtypenum == $opt{'ignore_cdrtypenum'}"
- if length($opt{'ignore_cdrtypenum'})
- && $cdr->cdrtypenum eq $opt{'ignore_cdrtypenum'}; #eq otherwise 0 matches ''
+ return "disposition NOT IN ( ". $self->option_cacheable('disposition_in')." )"
+ if $self->option_cacheable('disposition_in') =~ /\S/
+ && !grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('disposition_in'));
+
+ return "disposition IN ( ". $self->option_cacheable('ignore_disposition')." )"
+ if $self->option_cacheable('ignore_disposition') =~ /\S/
+ && grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('ignore_disposition'));
+
+ return "carrierid NOT IN ( ". $self->option_cacheable('use_carrierid'). " )"
+ if $self->option_cacheable('use_carrierid') =~ /\S/
+ && !grep { $cdr->carrierid eq $_ } split(/\s*,\s*/, $self->option_cacheable('use_carrierid')); #eq otherwise 0 matches ''
+
+ # unlike everything else, use_cdrtypenum is applied in FS::svc_x::get_cdrs.
+ return "cdrtypenum != ". $self->option_cacheable('use_cdrtypenum')
+ if length($self->option_cacheable('use_cdrtypenum'))
+ && $cdr->cdrtypenum ne $self->option_cacheable('use_cdrtypenum'); #ne otherwise 0 matches ''
+
+ return "cdrtypenum == ". $self->option_cacheable('ignore_cdrtypenum')
+ if length($self->option_cacheable('ignore_cdrtypenum'))
+ && $cdr->cdrtypenum eq $self->option_cacheable('ignore_cdrtypenum'); #eq otherwise 0 matches ''
+
+ # unlike everything else, use_calltypenum is applied in FS::svc_x::get_cdrs.
+ return "calltypenum != ". $self->option_cacheable('use_calltypenum')
+ if length($self->option_cacheable('use_calltypenum'))
+ && $cdr->calltypenum ne $self->option_cacheable('use_calltypenum'); #ne otherwise 0 matches ''
+
+ return "calltypenum == ". $self->option_cacheable('ignore_calltypenum')
+ if length($self->option_cacheable('ignore_calltypenum'))
+ && $cdr->calltypenum eq $self->option_cacheable('ignore_calltypenum'); #eq otherwise 0 matches ''
- return "dcontext IN ( $opt{'skip_dcontext'} )"
- if $opt{'skip_dcontext'} =~ /\S/
- && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $opt{'skip_dcontext'});
+ return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )"
+ if $self->option_cacheable('skip_dcontext') =~ /\S/
+ && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext'));
- my $len_prefix = length($opt{'skip_dstchannel_prefix'});
- return "dstchannel starts with $opt{'skip_dstchannel_prefix'}"
+ my $len_prefix = length($self->option_cacheable('skip_dstchannel_prefix'));
+ return "dstchannel starts with ". $self->option_cacheable('skip_dstchannel_prefix')
if $len_prefix
- && substr($cdr->dstchannel,0,$len_prefix) eq $opt{'skip_dstchannel_prefix'};
+ && substr($cdr->dstchannel,0,$len_prefix) eq $self->option_cacheable('skip_dstchannel_prefix');
- my $dst_length = $opt{'skip_dst_length_less'};
+ my $dst_length = $self->option_cacheable('skip_dst_length_less');
return "destination less than $dst_length digits"
if $dst_length && length($cdr->dst) < $dst_length;
- return "lastapp is $opt{'skip_lastapp'}"
- if length($opt{'skip_lastapp'}) && $cdr->lastapp eq $opt{'skip_lastapp'};
+ return "lastapp is ". $self->option_cacheable('skip_lastapp')
+ if length($self->option_cacheable('skip_lastapp')) && $cdr->lastapp eq $self->option_cacheable('skip_lastapp');
#all right then, rate it
'';
--- /dev/null
+package FS::part_pkg_msgcat;
+
+use strict;
+use base qw( FS::Record );
+use FS::Locales;
+#use FS::Record qw( qsearch qsearchs );
+use FS::part_pkg;
+
+=head1 NAME
+
+FS::part_pkg_msgcat - Object methods for part_pkg_msgcat records
+
+=head1 SYNOPSIS
+
+ use FS::part_pkg_msgcat;
+
+ $record = new FS::part_pkg_msgcat \%hash;
+ $record = new FS::part_pkg_msgcat { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_msgcat object represents localized labels of a package
+definition. FS::part_pkg_msgcat inherits from FS::Record. The following
+fields are currently supported:
+
+=over 4
+
+=item pkgpartmsgnum
+
+primary key
+
+=item pkgpart
+
+Package definition
+
+=item locale
+
+locale
+
+=item pkg
+
+Localized package name (customer-viewable)
+
+=item comment
+
+Localized package comment (non-customer-viewable), optional
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'part_pkg_msgcat'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('pkgpartmsgnum')
+ || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart')
+ || $self->ut_enum('locale', [ FS::Locales->locales ] )
+ || $self->ut_text('pkg')
+ || $self->ut_textn('comment')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
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);
}
}
- 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',
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);
}
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", "");
},
);
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;
'CARD' => 'CC',
'CHEK' => 'ECHECK',
'MCRD' => 'CC',
+ 'PPAL' => 'PAYPAL',
);
sub payby2bop {
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<FS::prepay_credit>)
+Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username)
+prepayment identifier (see L<FS::prepay_credit>), PayPal transaction ID
=cut
'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;
}
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;
}
=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
=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.
|| $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')
}
# 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;
}
=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.
$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({
'ip_field' => 'ip_addr',
'fields' => {
'svcnum' => 'Service',
- 'description' => 'Descriptive label for this particular device',
- 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
- 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
- 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
+ 'description' => 'Descriptive label',
+ 'speed_down' => 'Download speed (Kbps)',
+ 'speed_up' => 'Upload speed (Kbps)',
+ 'ip_addr' => 'IP address',
'blocknum' =>
{ 'label' => 'Address block',
'type' => 'select',
disable_inventory => 1,
multiple => 1,
},
+ 'radio_serialnum' => 'Radio Serial Number',
+ 'radio_location' => 'Radio Location',
+ 'poe_location' => 'POE Location',
+ 'rssi' => 'RSSI',
+ 'suid' => 'SUID',
+ 'shared_svcnum' => { label => 'Shared Service',
+ type => 'search-svc_broadband',
+ disable_inventory => 1,
+ },
},
};
}
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.
|| $self->ut_sfloatn('altitude')
|| $self->ut_textn('vlan_profile')
|| $self->ut_textn('plan_id')
+ || $self->ut_alphan('radio_serialnum')
+ || $self->ut_textn('radio_location')
+ || $self->ut_textn('poe_location')
+ || $self->ut_snumbern('rssi')
+ || $self->ut_numbern('suid')
+ || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
;
return $error if $error;
primary key
+=item exportnum
+
+Export definition, see L<FS::part_export>
+
=item svcnum
Customer service, see L<FS::cust_svc>
my ($class, $string) = @_;
my @where = ();
- my $ip = NetAddr::IP->new($string);
- if ( $ip ) {
- push @where, $class->search_sql_field('ip_addr', $ip->addr);
+ if ( $string =~ /^[\d\.:]+$/ ) {
+ # if the string isn't an IP address, this will waste several seconds
+ # attempting a DNS lookup. so try to filter those out.
+ my $ip = NetAddr::IP->new($string);
+ if ( $ip ) {
+ push @where, $class->search_sql_field('ip_addr', $ip->addr);
+ }
}
if ( $string =~ /^(\w+)$/ ) {
=item begin, end: Start and end of date range, as unix timestamp.
-=item cdrtypenum: Only return CDRs with this type number.
+=item cdrtypenum: Only return CDRs with this type.
+
+=item calltypenum: Only return CDRs with this call type.
=back
if ($options{'cdrtypenum'}) {
$hash{'cdrtypenum'} = $options{'cdrtypenum'};
}
+ if ($options{'calltypenum'}) {
+ $hash{'calltypenum'} = $options{'calltypenum'};
+ }
my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
#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";
=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".
if ($options{'cdrtypenum'}) {
$hash{'cdrtypenum'} = $options{'cdrtypenum'};
}
+ if ($options{'calltypenum'}) {
+ $hash{'calltypenum'} = $options{'calltypenum'};
+ }
my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
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);
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
}
}
- 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++;
+
}
}
'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++;
}
}
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;
=head1 BUGS
- batch_import does not handle mixed I and D records in the same file for
- format cch-update
-
=head1 SEE ALSO
L<FS::Record>, schema.html from the base documentation.
}
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'
);
}
- #
- # 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;
}
- 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(
}
}
- 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(
}
}
- 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) {
my $file = lc($name). 'file';
unless ($files{$file}) {
- $error = "No $name supplied";
+ #$error = "No $name supplied";
next;
}
next if $name eq 'DETAIL' && $format =~ /update/;
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;
'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,
);
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) {
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;
}
}
t/phone_type.t
FS/contact_email.pm
t/contact_email.t
+FS/contact_Mixin.pm
+t/contact_Mixin.t
FS/prospect_main.pm
t/prospect_main.t
FS/o2m_Common.pm
t/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
# 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 =~ /^\./;
$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";
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
-g: File is gzipped
+-s: Warn and skip files which could not be imported rather than abort
+
user: freeside username
format: CDR format name
# don't put @args in the log, may expose passwords
$log->info('starting job ('.$ljob->job.')');
warn 'running "&'. $ljob->job. '('. join(', ', @args). ")\n" if $DEBUG;
+ local $FS::UID::AutoCommit = 0; # so that we can clean up failures
eval $eval; #throw away return value? suppose so
if ( $@ ) {
+ dbh->rollback;
my %hash = $ljob->hash;
$hash{'statustext'} = $@;
if ( $hash{'statustext'} =~ /\/misc\/queued_report/ ) { #use return?
my $fjob = new FS::queue( \%hash );
my $error = $fjob->replace($ljob);
die $error if $error;
+ dbh->commit; # for the status change only
} else {
$ljob->delete;
+ dbh->commit; # for the job itself
}
if ( UNIVERSAL::can(dbh, 'sprintProfile') ) {
while ( $cf = $cfsth->fetchrow_hashref ) {
my $tbl = $cf->{'dbtable'};
my $name = $cf->{'name'};
+ $name = lc($name) unless driver_name =~ /^mysql/i;
+
@statements = grep { $_ !~ /^\s*ALTER\s+TABLE\s+(h_|)$tbl\s+DROP\s+COLUMN\s+cf_$name\s*$/i }
@statements;
push @statements,
--- /dev/null
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::contact_Mixin;
+$loaded=1;
+print "ok 1\n";
--- /dev/null
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg_msgcat;
+$loaded=1;
+print "ok 1\n";
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.
--- /dev/null
+#!/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"
+);
+
--- /dev/null
+#!/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 <<END;
+system join('',
+ "( cd /home/$USER/freeside3/$prefix; git pull ) && ",
+ "git diff -u @ARGV | ( cd /home/$USER/freeside3/$prefix; patch -p1 ) ",
+ " && ( ( git pull && git commit -m $desc @ARGV && git push ); ",
+ "( cd /home/$USER/freeside3/$prefix; git commit -m $desc @ARGV && git push ) )"
+);
+
--- /dev/null
+#!/usr/bin/perl
+
+my $file = shift;
+
+chomp(my $dir = `pwd`);
+$dir =~ s/freeside(\/?)/freeside3$1/;
+warn $dir;
+
+#$cmd = "diff -u $file $dir/$file";
+$cmd = "diff -ubBw $dir/$file $file";
+print "$cmd\n";
+system($cmd);
+
--- /dev/null
+#!/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( code detail 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 ),
+};
+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";
+
--- /dev/null
+#!/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";
+
$OUT .= '<th align="center">' . emt('Ref') . '</th>'.
'<th align="left">' . emt('Description') . '</th>'.
( $unitprices
- ? '<th align="left">' . emt('Unit Price') . '</th>'.
- '<th align="left">' . emt('Quantity') . '</th>'
+ ? '<th align="right">' . emt('Unit Price') . '</th>'.
+ '<th align="right">' . emt('Quantity') . '</th>'
: '' ).
'<th align="right">' . emt('Amount') . '</th>';
}
( $line->{'ref'} ne $lastref ? $line->{'ref'} : '' ). '</td>'.
'<td align="left">'. $line->{'description'}. '</td>'.
( $unitprices
- ? '<td align="left">'. $line->{'unit_amount'}. '</td>'.
- '<td align="left">'. $line->{'quantity'}. '</td>'
+ ? '<td align="right">'. $line->{'unit_amount'}. '</td>'.
+ '<td align="right">'. $line->{'quantity'}. '</td>'
: ''
).
\newcommand{\FSdescriptionlength} { [@-- $unitprices ? '8.2cm' : '12.8cm' --@] }\r
\newcommand{\FSdescriptioncolumncount} { [@-- $unitprices ? '4' : '6' --@] }\r
\newcommand{\FSunitcolumns}{ [@-- \r
- $unitprices \r
- ? '\makebox[2.5cm][l]{\textbf{~~'.emt('Unit Price').'}}&\makebox[1.4cm]{\textbf{~'.emt('Quantity').'}}&' \r
+ $unitprices\r
+ ? '\makebox[2.5cm][r]{\textbf{~~' . emt('Unit Price') . '}} &' .\r
+ '\makebox[1.4cm]{\textbf{~' . emt('Quantity') . '}} & ' \r
: '' --@] }\r
\r
\newcommand{\FShead}{\r
\newcommand{\FSdesc}[5]{\r
\multicolumn{1}{c}{\rule{0pt}{2.5ex}\textbf{#1}} &\r
\multicolumn{[@-- $unitprices ? '4' : '6' --@]}{l}{\textbf{#2}} &\r
-[@-- $unitprices ? ' \multicolumn{1}{l}{\textbf{#3}} &'."\n".\r
+[@-- $unitprices ? ' \multicolumn{1}{r}{\textbf{\dollar #3}} &'."\n".\r
' \multicolumn{1}{r}{\textbf{#4}} &'."\n"\r
: ''\r
--@]\r
'<INPUT TYPE="hidden" NAME="domsvc" VALUE="'. $key. '"></TD></TR>'
}
- my $text .= qq!<TR><TD ALIGN="right">Domain</TD><TD><SELECT NAME="domsvc" SIZE=1 STYLE="width: 20em"><OPTION>(Choose Domain)!;
+ my $text .= qq!<TR><TD ALIGN="right">Domain</TD><TD><SELECT NAME="domsvc" SIZE=1 STYLE="width: 20em">!;
+ $text .= '<OPTION>(Choose Domain)' unless $domsvc;
foreach my $domain ( sort { $domains->{$a} cmp $domains->{$b} } keys %$domains ) {
$text .= qq!<OPTION VALUE="!. $domain. '"'.
<INPUT TYPE="hidden" NAME="session" VALUE="<%=$session_id%>">
<INPUT TYPE="hidden" NAME="action" VALUE="post_thirdparty_payment">
<INPUT TYPE="hidden" NAME="payby_method" VALUE="<%=
-$cgi->param('payby_method') =~ /(CC|ECHECK)/;
+$cgi->param('payby_method') =~ /(CC|ECHECK|PAYPAL)/;
$1 %>">
<TABLE BGCOLOR="#cccccc">
<TR>
<%= include('small_custview') %>
<BR>
-<%= unless ( $access_pkgnum ) {
- $OUT .= qq!Balance: <B>\$$balance</B><BR><BR>!;
- }
- '';
-%>
<%=
$OUT .= qq! <B><A HREF="${url}invoices">View All Invoices</A></B> !;
%>
<%= if ( $balance > 0 ) {
- if (scalar(grep $_, @hide_payment_fields)) {
+ if (scalar(grep $_, @hide_payment_fields)) { # this sucks
$OUT .= qq! <B><A HREF="${url}make_thirdparty_payment&payby_method=CC">Make a payment</A></B><BR><BR>!;
} else {
$OUT .= qq! <B><A HREF="${url}make_payment">Make a payment</A></B><BR>!;
url=>'customer_order_pkg', 'indent'=>2 };
}
+my %payby_mode;
+@payby_mode{@cust_paybys} = @hide_payment_fields;
+# $payby_mode{FOO} is true if FOO is thirdparty, false if it's B::OP,
+# nonexistent if it's not supported
+
if ( $balance > 0 ) { #XXXFIXME "enable selfservice prepay features" flag or something, eventually per-pkg or something really fancy
- #XXXFIXME still a bit sloppy for multi-gateway of differing namespace
- my $i = 0;
- while($i < scalar(@cust_paybys)) { last if $cust_paybys[$i] =~ /^CARD/; $i++ }
- if ( $cust_paybys[$i] && $cust_paybys[$i] =~ /^CARD/ ) {
+ if ( exists( $payby_mode{CARD} ) ) {
push @menu, { title => 'Recharge my account with a credit card',
- url => $hide_payment_fields[$i]
+ url => $payby_mode{CARD}
? 'make_thirdparty_payment&payby_method=CC'
: 'make_payment',
indent => 2,
}
}
- $i = 0;
- while($i < scalar(@cust_paybys)) { last if $cust_paybys[$i] =~ /^CHEK/; $i++ }
- if ( $cust_paybys[$i] && $cust_paybys[$i] =~ /^CHEK/ ) {
+ if ( exists( $payby_mode{CHEK} ) ) {
push @menu, { title => 'Recharge my account with a check',
- url => $hide_payment_fields[$i]
+ url => $payby_mode{CHEK}
? 'make_thirdparty_payment&payby_method=ECHECK'
: 'make_ach_payment',
indent => 2,
}
}
- push @menu, { title => 'Recharge my account with a prepaid card',
- url => 'recharge_prepay',
- indent => 2,
- }
- if grep(/^PREP/, @cust_paybys);
+ if ( exists( $payby_mode{PREP} ) ) {
+ push @menu, { title => 'Recharge my account with a prepaid card',
+ url => 'recharge_prepay',
+ indent => 2,
+ }
+ }
+ if ( exists( $payby_mode{PPAL} ) ) {
+ push @menu, { title => 'Recharge my account with PayPal',
+ url => 'make_thirdparty_payment&payby_method=PAYPAL',
+ indent => 2,
+ }
+ }
}
push @menu,
}
sub post_thirdparty_payment {
- $cgi->param('payby_method') =~ /^(CC|ECHECK)$/
+ $cgi->param('payby_method') =~ /^(CC|ECHECK|PAYPAL)$/
or die "illegal payby method";
my $method = $1;
$cgi->param('amount') =~ /^(\d+(\.\d*)?)$/
or die "illegal amount";
my $amount = $1;
+ # realtime_collect() returns the result from FS::cust_main->realtime_collect
+ # which returns realtime_bop()
+ # which returns a hashref of popup_url, collectitems, and reference
my $result = realtime_collect(
'session_id' => $session_id,
'method' => $method,
invoicing_list referral_custnum promo_code reg_code
override_ban_warn
pkgpart refnum agentnum
- username sec_phrase _password popnum
+ username sec_phrase _password popnum domsvc
mac_addr
countrycode phonenum sip_password pin prepaid_shortform
),
package FS::SelfService::_signupcgi;
use HTML::Entities;
-use FS::SelfService qw(regionselector expselect popselector didselector);
+use FS::SelfService qw( regionselector expselect popselector domainselector
+ didselector
+ );
<FONT SIZE="+1" COLOR="#ff0000"><%= encode_entities($error) %></FONT>
<FORM NAME="OneTrueForm" ACTION="<%= $self_url %>" METHOD=POST onSubmit="document.OneTrueForm.signup.disabled=true">
-<INPUT TYPE="hidden" NAME="prepaid_shortform" VALUE="<%= $prepaid_shortform %>">
+<INPUT TYPE="hidden" NAME="prepaid_shortform" VALUE="<%= encode_entities($prepaid_shortform) %>">
<INPUT TYPE="hidden" NAME="session" VALUE="<%= $session_id %>">
<INPUT TYPE="hidden" NAME="action" VALUE="process_signup">
<INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>">
%>
<%=
- $OUT = join("\n", map { qq|<input type="hidden" name="$_" />| } qw / promo_code reg_code pkgpart username _password _password2 sec_phrase popnum mac_addr countrycode phonenum sip_password pin / );
+ $OUT = join("\n", map { qq|<input type="hidden" name="$_" />| } qw / promo_code reg_code pkgpart username _password _password2 sec_phrase popnum domsvc mac_addr countrycode phonenum sip_password pin / );
%>
<%=
my( $account, $aba ) = split('@', $payinfo);
my %paybychecked = (
- 'CARD' => '<TABLE BGCOLOR="'. ( $box_bgcolor || '#c0c0c0' ). qq!" BORDER=0 CELLSPACING=0 WIDTH="100%"><TR><TD ALIGN="right"><font color="#ff0000">*</font> Card type</TD><TD>$cardselect</TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Card number</TD><TD><INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19></TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Expration</TD><TD>!. expselect("CARD", $paydate). qq!</TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Name on card</TD><TD><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname"></TD></TR>!,
+ 'CARD' => '<TABLE BGCOLOR="'. ( $box_bgcolor || '#c0c0c0' ). qq!" BORDER=0 CELLSPACING=0 WIDTH="100%"><TR><TD ALIGN="right"><font color="#ff0000">*</font> Card type</TD><TD>$cardselect</TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Card number</TD><TD><INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19></TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Expiration</TD><TD>!. expselect("CARD", $paydate). qq!</TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Name on card</TD><TD><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname"></TD></TR>!,
'DCRD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("DCRD", $paydate). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="$payname">!,
- 'CHEK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE="$account" MAXLENGTH=10> Type <SELECT NAME="CHEK_paytype">!. join('', map {qq!<OPTION VALUE="$_"!.($paytype eq $_ ? 'SELECTED' : '').">$_</OPTION>"} @paytypes). qq!</SELECT><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="$payname">!,
- 'DCHK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE="$account" MAXLENGTH=10> Type <SELECT NAME="DCHK_paytype">!. join('', map {qq!<OPTION VALUE="$_"!.($paytype eq $_ ? 'SELECTED' : '').">$_</OPTION>"} @paytypes). qq!</SELECT><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="">!,
+ 'CHEK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE="$account" MAXLENGTH=10> Type <SELECT NAME="CHEK_paytype">!. join('', map {qq!<OPTION VALUE="$_"!.($paytype eq $_ ? ' SELECTED' : '').">$_</OPTION>"} @paytypes). qq!</SELECT><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="$payname">!,
+ 'DCHK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE="$account" MAXLENGTH=10> Type <SELECT NAME="DCHK_paytype">!. join('', map {qq!<OPTION VALUE="$_"!.($paytype eq $_ ? ' SELECTED' : '').">$_</OPTION>"} @paytypes). qq!</SELECT><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="">!,
'LECB' => qq!Phone bill billing<BR>${r}Phone number <INPUT TYPE="text" BANE="LECB_payinfo" VALUE="$payinfo" MAXLENGTH=15 SIZE=16><INPUT TYPE="hidden" NAME="LECB_month" VALUE="12"><INPUT TYPE="hidden" NAME="LECB_year" VALUE="2037"><INPUT TYPE="hidden" NAME="LECB_payname" VALUE="">!,
'BILL' => qq!Billing<BR>P.O. <INPUT TYPE="text" NAME="BILL_payinfo" VALUE="$payinfo"><BR><INPUT TYPE="hidden" NAME="BILL_month" VALUE="12"><INPUT TYPE="hidden" NAME="BILL_year" VALUE="2037">Attention<INPUT TYPE="text" NAME="BILL_payname" VALUE="$payname">!,
'COMP' => qq!Complimentary<BR><font color="#ff0000">*</font>Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE="$payinfo"><BR><font color="#ff0000">*</font>Exp !. expselect("COMP", $paydate),
<TD ALIGN="right">Username</TD>
<TD><INPUT TYPE="text" NAME="username" VALUE="$username"></TD>
</TR>
+ENDOUT
+
+ $OUT .= domainselector( svcpart=>$default_svcpart, domsvc=>$default_domsvc )
+ if $default_svcpart;
+
+ $OUT .= <<ENDOUT;
<TR>
<TD ALIGN="right">Password</TD>
<TD><INPUT TYPE="password" NAME="_password" VALUE="$_password"></TD>
var signup_elements = new Array (
'promo_code', 'reg_code', 'pkgpart',
- 'username', '_password', '_password2', 'sec_phrase', 'popnum',
+ 'username', '_password', '_password2', 'sec_phrase', 'popnum', 'domsvc',
'mac_addr',
'countrycode', 'phonenum', 'sip_password', 'pin'
);
? '<I><FONT SIZE="-1">Billing Address</FONT></I><BR>'
: ''
%>
- <%= $first %> <%= $last %><BR>
- <%= $company ? $company.'<BR>' : '' %>
- <%= $address1 %><BR>
- <%= $address2 ? $address2.'<BR>' : '' %>
+ <%= encode_entities($first) %> <%= encode_entities($last) %><BR>
+ <%= $company ? encode_entities($company).'<BR>' : '' %>
+ <%= encode_entities($address1) %><BR>
+ <%= $address2 ? encode_entities($address2).'<BR>' : '' %>
<%= $city %>, <%= $state %> <%= $zip %><BR>
<%= $country && $country ne ($countrydefault||'US')
? $country.'<BR>'
map { $_ => scalar($cgi->param($_)) } $cgi->param
},
url => $cgi->self_url,
+ cancel => ($cgi->param('cancel') ? 1 : 0),
);
$error = $rv->{error};
-
-if ( $error eq '_decline' ) {
+
+if ( $error eq '_cancel' ) {
+ print_okay(%$rv);
+} elsif ( $error eq '_decline' ) {
print_decline();
} elsif ( $error ) {
print_verify();
$success_url .= '/signup.cgi?action=success';
}
- print $cgi->header( '-expires' => 'now' ),
- $success_template->fill_in( HASH => { success_url => $success_url } );
+ if ( $param{error} eq '_cancel' ) {
+ # then the payment was canceled, so don't show a message, just redirect
+ # (during signup, you really need a separate landing page for this case)
+ print $cgi->redirect($success_url);
+ } else {
+ print $cgi->header( '-expires' => 'now' ),
+ $success_template->fill_in( HASH => { success_url => $success_url } );
+ }
}
sub success_default { #html to use if you don't specify a success file
SetHandler perl-script
PerlHandler HTML::Mason
</DirectoryMatch>
+
+<DirectoryMatch "^%%%FREESIDE_DOCUMENT_ROOT%%%/rt/RTx/Statistics/.*/>
+ <FilesMatch Results.tsv>
+ SetHandler perl-script
+ PerlHandler HTML::Mason
+ </FilesMatch>
+</DirectoryMatch>
% "Switch",
% "Solo",
% 'ACH',
+% 'PayPal',
%) {
<OPTION VALUE="<% $cardtype %>"><% $cardtype || '(Default fallback)' %>
<INPUT TYPE="hidden" NAME="locationnum" VALUE="<% $locationnum %>">
<% ntable('#cccccc') %>
-<% include('/elements/location.html',
- 'object' => $cust_location,
- 'no_asterisks' => 1,
- ) %>
+<& /elements/location.html,
+ 'object' => $cust_location,
+ 'no_asterisks' => 1,
+ # these are service locations, so they need all this stuff
+ 'enable_coords' => 1,
+ 'enable_district' => 1,
+ 'enable_censustract' => 1,
+&>
+<& /elements/standardize_locations.html,
+ 'form' => 'EditLocationForm',
+ 'callback' => 'document.EditLocationForm.submit();',
+&>
</TABLE>
<BR>
<SCRIPT TYPE="text/javascript">
-function areyousure() {
- return confirm('Modify this service location?');
+function go() {
+% if ( FS::Conf->new->config('address_standardize_method') ) {
+ standardize_locations();
+% } else {
+ confirm('Modify this service location?') &&
+ document.EditLocationForm.submit();
+% }
}
</SCRIPT>
-<INPUT TYPE="submit" VALUE="Submit" onclick="return areyousure()">
-
+<INPUT TYPE="button" NAME="submitButton" VALUE="Submit" onclick="go()">
</FORM>
</BODY>
</HTML>
<% include('/elements/error.html') %>
+<SCRIPT TYPE="text/javascript">
+ function svc_machine_changed (what, layer) {
+ if ( what.checked ) {
+ var machine = document.getElementById(layer + "_machine");
+ var part_export_machine =
+ document.getElementById(layer + "_part_export_machine");
+ if ( what.value == 'Y' ) {
+ machine.disabled = true;
+ part_export_machine.disabled = false;
+ } else if ( what.value == 'N' ) {
+ machine.disabled = false;
+ part_export_machine.disabled = true;
+ }
+ }
+ }
+
+ function part_export_machine_changed (what, layer) {
+ var select_default = document.getElementById(layer + '_default_machine');
+ var selected = select_default.value;
+ select_default.options.length = 0;
+ var choices = what.value.split("\n");
+ for (var i = 0; i < choices.length; i++) {
+ select_default.options[i] = new Option(choices[i]);
+ }
+ select_default.value = selected;
+ }
+
+</SCRIPT>
<FORM NAME="dummy">
<INPUT TYPE="hidden" NAME="exportnum" VALUE="<% $part_export->exportnum %>">
'form_name' => 'dummy',
'form_action' => 'process/part_export.cgi',
'form_text' => [qw( exportnum exportname )],
-# 'form_checkbox' => [qw()],
'html_between' => "</TD></TR></TABLE>\n",
'layer_callback' => sub {
my $layer = shift;
if ( $exports->{$layer}{svc_machine} ) {
my( $N_CHK, $Y_CHK) = ( 'CHECKED', '' );
my( $machine_DISABLED, $pem_DISABLED) = ( '', 'DISABLED' );
- my $part_export_machine = '';
+ my @part_export_machine;
+ my $default_machine = '';
if ( $cgi->param('svc_machine') eq 'Y'
|| $machine eq '_SVC_MACHINE'
)
$machine_DISABLED = 'DISABLED';
$pem_DISABLED = '';
$machine = '';
- $part_export_machine =
- $cgi->param('part_export_machine')
- || join "\n",
+ @part_export_machine = $cgi->param('part_export_machine');
+ if (!@part_export_machine) {
+ @part_export_machine =
map $_->machine,
grep ! $_->disabled,
$part_export->part_export_machine;
+ }
+ $default_machine =
+ $cgi->param('default_machine_name')
+ || $part_export->default_export_machine;
}
- my $oc = qq(onChange="${layer}_svc_machine_changed(this)");
+ my $oc = qq(onChange="svc_machine_changed(this, '$layer')");
$html .= qq[
<INPUT TYPE="radio" NAME="svc_machine" VALUE="N" $N_CHK $oc>
<INPUT TYPE="text" NAME="machine" ID="${layer}_machine" VALUE="$machine" $machine_DISABLED>
<BR>
<INPUT TYPE="radio" NAME="svc_machine" VALUE="Y" $Y_CHK $oc>
- Selected in each customer service from these choices
- <TEXTAREA NAME="part_export_machine" ID="${layer}_part_export_machine" $pem_DISABLED>$part_export_machine</TEXTAREA>
-
- <SCRIPT TYPE="text/javascript">
- function ${layer}_svc_machine_changed (what) {
- if ( what.checked ) {
- var machine = document.getElementById("${layer}_machine");
- var part_export_machine = document.getElementById("${layer}_part_export_machine");
- if ( what.value == 'Y' ) {
- machine.disabled = true;
- part_export_machine.disabled = false;
- } else if ( what.value == 'N' ) {
- machine.disabled = false;
- part_export_machine.disabled = true;
- }
- }
- }
- </SCRIPT>
+ <DIV STYLE="display:inline-block; vertical-align: top; text-align: right">
+ Selected in each customer service from these choices:
+ <TEXTAREA STYLE="vertical-align: top" NAME="part_export_machine"
+ ID="${layer}_part_export_machine"
+ onchange="part_export_machine_changed(this, '$layer')"
+ $pem_DISABLED>] .
+
+ join("\n", @part_export_machine) .
+
+ qq[</TEXTAREA>
+ <BR>
+ Default:
+ <SELECT NAME="default_machine_name" ID="${layer}_default_machine">
];
+ foreach (@part_export_machine) {
+ $_ = encode_entities($_); # oh noes, XSS
+ my $sel = ($default_machine eq $_) ? ' SELECTED' : '';
+ $html .= qq!<OPTION VALUE="$_"$sel>$_</OPTION>\n!;
+ }
+ $html .= '</DIV></SELECT>'
} else {
$html .= qq(<INPUT TYPE="text" NAME="machine" VALUE="$machine">).
'<INPUT TYPE="hidden" NAME="svc_machine" VALUE=N">';
'labels' => {
'pkgpart' => 'Package Definition',
- 'pkg' => 'Package (customer-visible)',
+ 'pkg' => 'Package',
+ %locale_field_labels,
'comment' => 'Comment (customer-hidden)',
'classnum' => 'Package class',
'addon_classnum' => 'Restrict additional orders to package class',
size => 40, #32
maxlength => 50,
},
+ #@locale_fields,
{field=>'comment', type=>'text', size=>40 }, #32
{ field => 'agentnum',
type => 'select-agent',
my $conf = new FS::Conf;
my $taxproducts = $conf->exists('enable_taxproducts');
+my @locales = grep { ! /^en_/i } $conf->config('available-locales'); #should filter from the default locale lang instead of en_
+my %locale_labels = map {
+ ( $_ => 'Package -- '. FS::Locales->description($_) )
+} @locales;
+@locales =
+ sort { $locale_labels{$a} cmp $locale_labels{$b} }
+ @locales;
+
+my $n = 0;
+my %locale_field_labels = (
+ map {
+ ( 'pkgpartmsgnum'. $n++. '_pkg' => $locale_labels{$_} );
+ }
+ @locales
+);
+
my $sth = dbh->prepare("SELECT COUNT(*) FROM part_pkg_report_option".
" WHERE disabled IS NULL OR disabled = '' ")
or die dbh->errstr;
my $pkgpart = '';
+my $splice_locale_fields = sub {
+ my( $fields, $pkey_value_callback, $pkg_value_callback ) = @_;
+
+ my $n = 0;
+ my @locale_fields = (
+ map {
+ my $pkey_value= $pkey_value_callback ? &$pkey_value_callback($_) : '';
+ my $pkg_value = $pkg_value_callback
+ ? $pkg_value_callback eq 'cgiparam'
+ ? $cgi->param('pkgpartmsgnum'. $n. '_pkg')
+ : &$pkg_value_callback($_)
+ : '';
+ (
+ { field => 'pkgpartmsgnum'. $n,
+ type => 'hidden',
+ value => $pkey_value,
+ },
+ { field => 'pkgpartmsgnum'. $n. '_locale',
+ type => 'hidden',
+ value => $_,
+ },
+ { field => 'pkgpartmsgnum'. $n++. '_pkg',
+ type => 'text',
+ size => 40,
+ #maxlength => 50,
+ value => $pkg_value,
+ },
+ );
+
+ }
+ @locales
+ );
+ splice(@$fields, 7, 0, @locale_fields); #XXX 7 is arbitrary above
+
+};
+
my $error_callback = sub {
my($cgi, $object, $fields, $opt ) = @_;
$pkgpart = $object->pkgpart;
+ &$splice_locale_fields(
+ $fields,
+ sub {
+ my $locale = shift;
+ my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
+ $part_pkg_msgcat ? $part_pkg_msgcat->pkgpartmsgnum : '';
+ },
+ 'cgiparam'
+ );
+
};
my $new_hashref_callback = sub { { 'plan' => 'flat' }; };
$pkgpart = $object->pkgpart;
+ &$splice_locale_fields(
+ $fields,
+ sub {
+ my $locale = shift;
+ my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
+ $part_pkg_msgcat ? $part_pkg_msgcat->pkgpartmsgnum : '';
+ },
+ sub {
+ my $locale = shift;
+ my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
+ $part_pkg_msgcat ? $part_pkg_msgcat->pkg : '';
+ }
+ );
+
};
my $new_callback = sub {
$options{'suspend_bill'}=1 if $conf->exists('part_pkg-default_suspend_bill');
+ &$splice_locale_fields($fields, '', '');
+
};
my $clone_callback = sub {
foreach (qw( setup_fee recur_fee disable_line_item_date_ranges ));
$recur_disabled = $object->freq ? 0 : 1;
+
+ &$splice_locale_fields(
+ $fields,
+ '',
+ sub {
+ my $locale = shift;
+ my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
+ $part_pkg_msgcat ? $part_pkg_msgcat->pkg : '';
+ }
+ );
};
my $discount_error_callback = sub {
'gateway_action' => 'Action',
'gateway_options' => 'Options (Name/Value pairs, <BR>one element per line)',
'gateway_callback_url' => 'Callback URL',
+ 'gateway_cancel_url' => 'Cancel URL',
},
)
%>
<SCRIPT TYPE="text/javascript">
- var modulesForNamespace = <% to_json(\%modules_for_namespace, {canonical=>1}) %>;
- function changeNamespace(what) {
- var ns = what.value;
+ var modulesForNamespace = <% $json->encode(\%modules) %>;
+ function changeNamespace() {
+ var ns = document.getElementById('gateway_namespace').value;
var select_module = document.getElementById('gateway_module');
select_module.options.length = 0;
for (var x in modulesForNamespace[ns]) {
select_module.add(o, null);
}
}
+ window.onload = changeNamespace;
</SCRIPT>
<%init>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
-my %modules = (
- '2CheckOut' => 'Business::OnlinePayment',
- 'AuthorizeNet' => 'Business::OnlinePayment',
- 'BankOfAmerica' => 'Business::OnlinePayment', #deprecated?
- 'Beanstream' => 'Business::OnlinePayment',
- 'Capstone' => 'Business::OnlinePayment',
- 'Cardstream' => 'Business::OnlinePayment',
- 'CashCow' => 'Business::OnlinePayment',
- 'CyberSource' => 'Business::OnlinePayment',
- 'eSec' => 'Business::OnlinePayment',
- 'eSelectPlus' => 'Business::OnlinePayment',
- 'eWayShared' => 'Business::OnlineThirdPartyPayment',
- 'ElavonVirtualMerchant' => 'Business::OnlinePayment',
- 'Exact' => 'Business::OnlinePayment',
- 'iAuthorizer' => 'Business::OnlinePayment',
- 'Ingotz' => 'Business::OnlinePayment',
- 'InternetSecure' => 'Business::OnlinePayment',
- 'Interswitchng' => 'Business::OnlineThirdPartyPayment',
- 'IPaymentTPG' => 'Business::OnlinePayment',
- 'IPPay' => 'Business::OnlinePayment',
- 'Iridium' => 'Business::OnlinePayment',
- 'Jettis' => 'Business::OnlinePayment',
- 'Jety' => 'Business::OnlinePayment',
- 'LinkPoint' => 'Business::OnlinePayment',
- 'MerchantCommerce' => 'Business::OnlinePayment',
- 'Network1Financial' => 'Business::OnlinePayment',
- 'OCV' => 'Business::OnlinePayment',
- 'OpenECHO' => 'Business::OnlinePayment',
- 'PayConnect' => 'Business::OnlinePayment',
- 'PayflowPro' => 'Business::OnlinePayment',
- 'PaymenTech' => 'Business::OnlinePayment',
- 'PaymentsGateway' => 'Business::OnlinePayment',
- 'PayPal' => 'Business::OnlinePayment',
- #'PaySystems' => 'Business::OnlinePayment',
- 'PlugnPay' => 'Business::OnlinePayment',
- 'PPIPayMover' => 'Business::OnlinePayment',
- 'Protx' => 'Business::OnlinePayment', #now SagePay
- 'PXPost' => 'Business::OnlinePayment',
- 'SagePay' => 'Business::OnlinePayment',
- 'SecureHostingUPG' => 'Business::OnlinePayment',
- 'Skipjack' => 'Business::OnlinePayment',
- 'StGeorge' => 'Business::OnlinePayment',
- 'SurePay' => 'Business::OnlinePayment',
- 'TCLink' => 'Business::OnlinePayment',
- 'TransactionCentral' => 'Business::OnlinePayment',
- 'TransFirsteLink' => 'Business::OnlinePayment',
- 'Vanco' => 'Business::OnlinePayment',
- 'viaKLIX' => 'Business::OnlinePayment',
- 'VirtualNet' => 'Business::OnlinePayment',
- 'WesternACH' => 'Business::OnlinePayment',
- 'WorldPay' => 'Business::OnlinePayment',
-
- 'KeyBank' => 'Business::BatchPayment',
- 'Paymentech' => 'Business::BatchPayment',
- 'TD_EFT' => 'Business::BatchPayment',
+my $json = JSON::XS->new;
+$json->canonical(1);
+my %modules = (
+ 'Business::OnlinePayment' => [
+ '2CheckOut',
+ 'AuthorizeNet',
+ 'BankOfAmerica', #deprecated?
+ 'Beanstream',
+ 'Capstone',
+ 'Cardstream',
+ 'CashCow',
+ 'CyberSource',
+ 'eSec',
+ 'eSelectPlus',
+ 'ElavonVirtualMerchant',
+ 'Exact',
+ 'iAuthorizer',
+ 'Ingotz',
+ 'InternetSecure',
+ 'IPaymentTPG',
+ 'IPPay',
+ 'Iridium',
+ 'Jettis',
+ 'Jety',
+ 'LinkPoint',
+ 'MerchantCommerce',
+ 'Network1Financial',
+ 'OCV',
+ 'OpenECHO',
+ 'PayConnect',
+ 'PayflowPro',
+ 'PaymenTech',
+ 'PaymentsGateway',
+ 'PayPal',
+ #'PaySystems',
+ 'PlugnPay',
+ 'PPIPayMover',
+ 'Protx', #now SagePay
+ 'PXPost',
+ 'SagePay',
+ 'SecureHostingUPG',
+ 'Skipjack',
+ 'StGeorge',
+ 'SurePay',
+ 'TCLink',
+ 'TransactionCentral',
+ 'TransFirsteLink',
+ 'Vanco',
+ 'viaKLIX',
+ 'VirtualNet',
+ 'WesternACH',
+ 'WorldPay',
+ ],
+ 'Business::OnlineThirdPartyPayment' => [
+ 'eWayShared',
+ 'Interswitchng',
+ 'PayPal',
+ ],
+ 'Business::BatchPayment' => [
+ 'KeyBank',
+ 'Paymentech',
+ 'TD_EFT',
+ ],
);
-my %modules_for_namespace;
-for (keys %modules) {
- $modules_for_namespace{$modules{$_}} ||= [];
- push @{ $modules_for_namespace{$modules{$_}} }, $_;
-}
-
my @actions = (
'Normal Authorization',
'Authorization Only',
{
field => 'gateway_module',
type => 'select',
- options => [ sort { lc($a) cmp lc ($b) } keys %modules ],
+ # does it even make sense to list all modules here?
+ options => [ sort { lc($a) cmp lc ($b) }
+ map { @$_ } values %modules ],
},
'gateway_username',
'gateway_password',
size => 40,
},
{
+ field => 'gateway_cancel_url',
+ type => 'text',
+ size => 40,
+ },
+ {
field => 'gateway_options',
type => 'textarea',
rows => '12',
$change{'keep_dates'} = 1;
if ( $cgi->param('locationnum') == -1 ) {
- my $cust_location = new FS::cust_location {
+ my $cust_location = FS::cust_location->new({
'custnum' => $cust_pkg->custnum,
map { $_ => scalar($cgi->param($_)) }
qw( address1 address2 city county state zip country )
- };
+ });
$change{'cust_location'} = $cust_location;
}
my $new = FS::cust_location->new({
custnum => $cust_location->custnum,
prospectnum => $cust_location->prospectnum,
- map { $_ => scalar($cgi->param($_)) }
- qw( address1 address2 city county state zip country )
+ map { $_ => scalar($cgi->param($_)) } FS::cust_main->location_fields
});
-
-my $error = $cust_location->move_to($new);
+my $error = $new->find_or_insert;
+$error ||= $cust_location->move_to($new);
</%init>
<%once>
my $me = '[edit/process/cust_main.cgi]';
-my $DEBUG = 0;
+my $DEBUG = 1;
</%once>
<%init>
}
$hash{'custnum'} = $cgi->param('custnum');
warn Dumper \%hash if $DEBUG;
- # if we can qsearchs it, then it's unchanged, so use that
- $locations{$pre} = qsearchs('cust_location', \%hash)
- || FS::cust_location->new( \%hash );
-
+ $locations{$pre} = FS::cust_location->new(\%hash);
}
if ( ($cgi->param('same') || '') eq 'Y' ) {
--- /dev/null
+% if ($error) {
+% $cgi->param('error', $error);
+% $cgi->redirect(popurl(3). 'misc/detach_pkg.html?'. $cgi->query_string );
+% } else {
+
+ <% header(emt("Package detached")) %>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+ </BODY>
+ </HTML>
+
+% }
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('Change customer package');
+
+my $cust_pkg = qsearchs({
+ 'table' => 'cust_pkg',
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => { 'pkgnum' => scalar($cgi->param('pkgnum')), },
+ 'extra_sql' => ' AND '. $curuser->agentnums_sql,
+});
+die 'unknown pkgnum' unless $cust_pkg;
+
+my $cust_location = new FS::cust_location {
+ map { $_ => scalar($cgi->param($_)) } FS::cust_main->location_fields
+};
+
+my $cust_main = new FS::cust_main {
+ ( map { ( $_, scalar($cgi->param($_)) ) } fields('cust_main') ),
+ ( map { ( "ship_$_", '' ) } FS::cust_main->location_fields ),
+ 'bill_location' => $cust_location,
+ 'ship_location' => $cust_location,
+};
+
+my $pkg_or_error = $cust_pkg->change( {
+ 'keep_dates' => 1,
+ 'cust_main' => $cust_main,
+} );
+
+my $error = ref($pkg_or_error) ? '' : $pkg_or_error;
+
+</%init>
if ( $cgi->param('svc_machine') eq 'Y' ) {
$new->machine('_SVC_MACHINE');
$new->part_export_machine_textarea( $cgi->param('part_export_machine') );
+ $new->default_machine_name( $cgi->param('default_machine_name') );
}
my $error;
'precheck_callback' => $precheck_callback,
'args_callback' => $args_callback,
'process_m2m' => \@process_m2m,
+ 'process_o2m' => \@process_o2m,
)
%>
<%init>
};
}
+my @process_o2m = (
+ {
+ 'table' => 'part_pkg_msgcat',
+ 'fields' => [qw( locale pkg )],
+ },
+);
+
</%init>
my @options = split(/\r?\n/, $cgi->param('gateway_options') );
pop @options
if scalar(@options) % 2 && $options[-1] =~ /^\s*$/;
+ @options = ( {} ) if !@options;
(@options)
};
$cgi->param('refnum') =~ /^(\d*)$/
or die 'illegal refnum '. $cgi->param('refnum');
my $refnum = $1;
+$cgi->param('contactnum') =~ /^(\-?\d*)$/
+ or die 'illegal contactnum '. $cgi->param('contactnum');
+my $contactnum = $1;
$cgi->param('locationnum') =~ /^(\-?\d*)$/
or die 'illegal locationnum '. $cgi->param('locationnum');
my $locationnum = $1;
: ''
),
'refnum' => $refnum,
+ 'contactnum' => $contactnum,
'locationnum' => $locationnum,
'discountnum' => $discountnum,
#for the create a new discount case
my %opt = ( 'cust_pkg' => $cust_pkg );
+ if ( $contactnum == -1 ) {
+ my $contact = FS::contact->new({
+ 'custnum' => scalar($cgi->param('custnum')),
+ map { $_ => scalar($cgi->param("contactnum_$_")) } qw( first last )
+ });
+ $opt{'contact'} = $contact;
+ }
+
if ( $locationnum == -1 ) {
- my $cust_location = new FS::cust_location {
+ my $cust_location = FS::cust_location->new({
map { $_ => scalar($cgi->param($_)) }
- qw( custnum address1 address2 city county state zip country geocode )
- };
+ ('custnum', FS::cust_main->location_fields)
+ });
$opt{'cust_location'} = $cust_location;
}
my %opt = ();
if ( $cgi->param('locationnum') == -1 ) {
- my $cust_location = new FS::cust_location {
+ my $cust_location = FS::cust_location->new({
map { $_ => scalar($cgi->param($_)) }
qw( custnum address1 address2 city county state zip country )
- };
+ });
$opt{'cust_location'} = $cust_location;
}
{ field=>'sectornum', type=>'select-tower_sector', },
{ field=>'routernum', type=>'select-router_block_ip' },
{ field=>'mac_addr' , type=>'input-mac_addr' },
- qw( latitude longitude altitude vlan_profile
- performance_profile authkey plan_id )
+ qw(
+ latitude longitude altitude
+ radio_serialnum radio_location poe_location rssi suid
+ ),
+ { field=>'shared_svcnum', type=>'search-svc_broadband', },
+ qw( vlan_profile performance_profile authkey plan_id ),
);
if ( $conf->exists('svc_broadband-radius') ) {
var <%$pre%>set_rownum;
var <%$pre%>addRow;
var <%$pre%>deleteRow;
-var <%$pre%>fieldorder = <% to_json($fieldorder) %>;
+var <%$pre%>fieldorder = <% encode_json($fieldorder) %>;
function <%$pre%>possiblyAddRow_factory(obj) {
var callback = obj.onchange;
<%$pre%>template.appendChild(delete_cell);
// preload rows
- var rows = <% to_json(\@rows) %>;
+ var rows = <% encode_json(\@rows) %>;
for (var i = 0; i < rows.length; i++) {
<%$pre%>addRow(rows[i]);
}
<INPUT TYPE="hidden" NAME="<%$name%>" ID="<%$id%>" VALUE="<% $curr_value %>">
- <TABLE>
+ <TABLE STYLE="display:inline">
<TR>
-% if ( @contact_class ) {
+% if ( @contact_class && ! $opt{name_only} ) {
<TD>
<SELECT NAME="<%$name%>_classnum" <% $onchange %>>
<OPTION VALUE="">
$label{'comment'} = 'Comment';
-my @fields = keys %label;
+my @fields = $opt{'name_only'} ? qw( first last ) : keys %label;
</%init>
ObjectCustomFieldValues.ObjectId = cust_tickets.Id
)
GROUP BY cust_tickets.custnum, ObjectCustomFieldValues.Content";
- #warn $sql."\n";
} else { # no custom_priority_field
$sql =
"SELECT cust_tickets.custnum,
my $sth = dbh->prepare($sql) or die dbh->errstr;
$sth->execute or die $sth->errstr;
while ( my $row = $sth->fetchrow_hashref ) {
- #warn to_json($row)."\n";
$num_tickets_by_priority{ $row->{priority} }->{ $row->{custnum} } =
$row->{num_tickets};
}
}
-#warn Dumper \%num_tickets_by_priority;
</%init>
'Advanced ticket reports' => [ $fsurl.'rt/Search/Build.html?NewQuery=1', 'List tickets by any criteria' ],
;
-tie my %report_employees, 'Tie::IxHash',
- 'Employee Commission Report' => [ $fsurl.'search/report_employee_commission.html', '' ],
- 'Employee Audit Report' => [ $fsurl.'search/report_employee_audit.html', 'Employee audit report' ],
+tie my %report_employees, 'Tie::IxHash';
+$report_employees{'Employee Commission Report'} = [ $fsurl.'search/report_employee_commission.html', '' ]
+ if $curuser->access_right('Employees: Commission Report');
+$report_employees{'Employee Audit Report'} = [ $fsurl.'search/report_employee_audit.html', 'Employee audit report' ]
+ if $curuser->access_right('Employees: Audit Report');
;
tie my %report_bill_event, 'Tie::IxHash',
if $conf->config('ticket_system')
;#&& FS::TicketSystem->access_right(\%session, 'Something');
$report_menu{'Employees'} = [ \%report_employees, 'Employee reports' ]
- if $curuser->access_right('Financial reports');
+ if keys %report_employees;
$report_menu{'Billing events'} = [ \%report_bill_event, 'Billing events' ]
if $curuser->access_right('Billing event reports');
$report_menu{'Financial'} = [ \%report_financial, 'Financial reports' ]
--- /dev/null
+<%doc>
+
+Example:
+
+ include( '/elements/search-svc_broadband.html,
+ 'field' => 'svcnum',
+ #slightly deprecated old synonym for field#'field_name'=>'svcnum',
+ 'find_button' => 1, #add a "find" button to the field
+ 'curr_value' => 54, #current value
+ 'value => 32, #deprecated synonym for curr_value
+ );
+
+</%doc>
+<INPUT TYPE="hidden" NAME="<% $field %>" ID="<% $field %>" VALUE="<% $value %>">
+
+<!-- some false laziness w/ misc/batch-cust_pay.html, though not as bad as i'd thought at first... -->
+
+<INPUT TYPE = "text"
+ NAME = "<% $field %>_search"
+ ID = "<% $field %>_search"
+ SIZE = "32"
+ VALUE="<% $svc_broadband ? $svc_broadband->label : '(svcnum, ip or mac)' %>"
+ onFocus="clearhint_<% $field %>_search(this);"
+ onClick="clearhint_<% $field %>_search(this);"
+ onChange="smart_<% $field %>_search(this);"
+>
+
+% if ( $opt{'find_button'} ) {
+ <INPUT TYPE = "button"
+ VALUE = 'Find',
+ NAME = "<% $field %>_findbutton"
+ onClick = "smart_<% $field %>_search(this.form.<% $field %>_search);"
+ >
+% }
+
+<SELECT NAME="<% $field %>_select" ID="<% $field %>_select" STYLE="color:#ff0000; display:none" onChange="select_<% $field %>(this);">
+</SELECT>
+
+<% include('/elements/xmlhttp.html',
+ 'url' => $p. 'misc/xmlhttp-svc_broadband-search.cgi',
+ 'subs' => [ 'smart_search' ],
+ )
+%>
+
+<SCRIPT TYPE="text/javascript">
+
+ function clearhint_<% $field %>_search (what) {
+
+ what.style.color = '#000000';
+
+ if ( what.value == '(svcnum, ip or mac)' )
+ what.value = '';
+
+ if ( what.value.indexOf('Service not found: ') == 0 )
+ what.value = what.value.substr(20);
+
+ }
+
+ var <% $field %>_search_active = false;
+
+ function smart_<% $field %>_search(what) {
+
+ if ( <% $field %>_search_active )
+ return;
+
+ var service = what.value;
+
+ if ( service == 'searching...' || service == ''
+ || service.indexOf('Service not found: ') == 0 )
+ return;
+
+ if ( what.getAttribute('magic') == 'nosearch' ) {
+ what.setAttribute('magic', '');
+ return;
+ }
+
+ //what.value = 'searching...'
+ what.disabled = true;
+ what.style.color= '#000000';
+ what.style.backgroundColor = '#dddddd';
+
+ var service_select = document.getElementById('<% $field %>_select');
+
+ //alert("search for customer " + customer);
+
+ function <% $field %>_search_update(services) {
+
+ //alert('customers returned: ' + customers);
+
+ var serviceArray = eval('(' + services + ')');
+
+ what.disabled = false;
+ what.style.backgroundColor = '#ffffff';
+
+ if ( serviceArray.length == 0 ) {
+
+ what.form.<% $field %>.value = '';
+
+ what.value = 'Service not found: ' + what.value;
+ what.style.color = '#ff0000';
+
+ what.style.display = '';
+ service_select.style.display = 'none';
+
+ } else if ( serviceArray.length == 1 ) {
+
+ //alert('one customer found: ' + customerArray[0]);
+
+ what.form.<% $field %>.value = serviceArray[0][0];
+ what.value = serviceArray[0][1];
+
+ what.style.display = '';
+ service_select.style.display = 'none';
+
+ } else {
+
+ //alert('multiple customers found, have to create select dropdown');
+
+ //blank the current list
+ for ( var i = service_select.length; i >= 0; i-- )
+ service_select.options[i] = null;
+
+ opt(service_select, '', 'Multiple services match "' + service + '" - select one', '#ff0000');
+
+ //add the multiple services
+ for ( var s = 0; s < serviceArray.length; s++ )
+ opt(service_select, serviceArray[s][0], serviceArray[s][1], '#000000');
+
+ opt(service_select, 'cancel', '(Edit search string)', '#000000');
+
+ what.style.display = 'none';
+ service_select.style.display = '';
+
+ }
+
+ <% $field %>_search_active = false;
+
+ }
+
+ <% $field %>_search_active = true;
+
+ smart_search( service, <% $field %>_search_update );
+
+
+ }
+
+ function select_<% $field %> (what) {
+
+ var svcnum = what.options[what.selectedIndex].value;
+ var service = what.options[what.selectedIndex].text;
+
+ var service_obj = document.getElementById('<% $field %>_search');
+
+ if ( svcnum == '' ) {
+ //what.style.color = '#ff0000';
+
+ } else if ( svcnum == 'cancel' ) {
+
+ service_obj.style.color = '#000000';
+
+ what.style.display = 'none';
+ service_obj.style.display = '';
+ service_obj.focus();
+
+ } else {
+
+ what.form.<% $field %>.value = svcnum;
+
+ service_obj.value = service;
+ service_obj.style.color = '#000000';
+
+ what.style.display = 'none';
+ service_obj.style.display = '';
+
+ }
+
+ }
+
+ function opt(what,value,text,color) {
+ var optionName = new Option(text, value, false, false);
+ optionName.style.color = color;
+ var length = what.length;
+ what.options[length] = optionName;
+ }
+
+</SCRIPT>
+<%init>
+
+my( %opt ) = @_;
+
+my $field = $opt{'field'} || $opt{'field_name'} || 'svcnum';
+
+my $value = $opt{'curr_value'} || $opt{'value'};
+
+my $svc_broadband = '';
+if ( $value ) {
+ $svc_broadband = qsearchs({
+ 'table' => 'svc_broadband',
+ 'hashref' => { 'svcnum' => $value },
+ #have to join to cust_main for an agentnum 'extra_sql' => " AND ". $FS::CurrentUser::CurrentUser->agentnums_sql,
+ });
+}
+
+</%init>
my $pre = $opt{prefix} || '';
my $tiers = $opt{tiers} or die "no tiers defined";
-#my $json = JSON->new()->canonical(); #sort
-# something super weird and broken going on with JSON's auto-loading, just
-# using JSON alone errors out with
-# Can't locate object method "new" via package "null" (perhaps you forgot to
-# load "null"?)
-# yes, "null", not "JSON". so instead, using JSON::XS explicity...
-use JSON::XS;
my $json = JSON::XS->new();
$json->canonical;
$children_of{$key}->{''} = $tier->{empty_label};
}
}
+ # ensure that there's always at least one empty label
+ $children_of{''}->{''} = $tier->{empty_label};
}
$tier->{by_key} = \%children_of;
}
$date_noinit = 1;
}
else {
- $include = "input-$include" if $include =~ /^(text|money)$/;
+ $include = "input-$include" if $include =~ /^(text|money|percentage)$/;
$include = "tr-$include" unless $include eq 'hidden';
$html .= include( "/elements/$include.html",
%$lf,
--- /dev/null
+<& tr-td-label.html, @_ &>
+
+ <TD <% $colspan %> <% $cell_style %> ID="<% $opt{input_id} || $opt{id}.'_input0' %>"><& search-svc_broadband.html, @_ &></TD>
+
+</TR>
+
+<%init>
+
+my %opt = @_;
+
+my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
+
+my $colspan = $opt{'colspan'} ? 'COLSPAN="'.$opt{'colspan'}.'"' : '';
+
+</%init>
--- /dev/null
+<%doc>
+
+Example:
+
+ include('/elements/tr-select-contact.html',
+ 'cgi' => $cgi,
+
+ 'cust_main' => $cust_main,
+ #or
+ 'prospect_main' => $prospect_main,
+
+ #optional
+ 'empty_label' => '(default contact)',
+ )
+
+</%doc>
+
+<SCRIPT TYPE="text/javascript">
+
+ function contact_disable(what) {
+% for (@contact_fields) {
+ what.form.<%$_%>.disabled = true;
+ var ftype = what.form.<%$_%>.tagName;
+ if( ftype == 'SELECT') changeSelect(what.form.<%$_%>, '');
+ else what.form.<%$_%>.value = '';
+ if( ftype != 'SELECT') what.form.<%$_%>.style.backgroundColor = '#dddddd';
+% }
+ }
+
+ function contact_clear(what) {
+% for (@contact_fields) {
+ var ftype = what.form.<%$_%>.tagName;
+ if( ftype == 'INPUT' ) what.form.<%$_%>.value = '';
+% }
+ }
+
+ function contact_enable(what) {
+% for (@contact_fields) {
+ what.form.<%$_%>.disabled = false;
+ var ftype = what.form.<%$_%>.tagName;
+ if( ftype != 'SELECT') what.form.<%$_%>.style.backgroundColor = '#ffffff';
+% }
+ }
+
+ function contactnum_changed(what) {
+ var contactnum = what.options[what.selectedIndex].value;
+ if ( contactnum == -1 ) { //Add new contact
+ contact_clear(what);
+
+ contact_enable(what);
+ return;
+ }
+
+% if ( $editable ) {
+ if ( contactnum == 0 ) {
+% }
+
+% #sleep/wait until dropdowns are updated?
+ contact_disable(what);
+
+% if ( $editable ) {
+ } else {
+
+% #sleep/wait until dropdowns are updated?
+ contact_enable(what);
+
+ }
+% }
+
+ }
+
+ function changeSelect(what, value) {
+ for ( var i=0; i<what.length; i++) {
+ if ( what.options[i].value == value ) {
+ what.selectedIndex = i;
+ }
+ }
+ }
+
+</SCRIPT>
+
+<TR>
+ <<%$th%> ALIGN="right" VALIGN="top"><% $opt{'label'} || emt('Service contact') %></<%$th%>>
+ <TD VALIGN="top" COLSPAN=7>
+ <SELECT NAME = "contactnum"
+ ID = "contactnum"
+ STYLE = "vertical-align:top;margin:3px"
+ onchange = "contactnum_changed(this);"
+ >
+% if ( $cust_main ) {
+ <OPTION VALUE=""><% $opt{'empty_label'} || '(customer default)' |h %>
+% }
+%
+% foreach my $contact ( @contact ) {
+ <OPTION VALUE="<% $contact->contactnum %>"
+ <% $contactnum == $contact->contactnum ? 'SELECTED' : '' %>
+ ><% $contact->line |h %>
+% }
+% if ( $addnew ) {
+ <OPTION VALUE="-1"
+ <% $contactnum == -1 ? 'SELECTED' : '' %>
+ >New contact
+% }
+ </SELECT>
+
+<% include('/elements/contact.html',
+ 'object' => $contact,
+ #'onchange' ? probably not
+ 'disabled' => $disabled,
+ 'name_only' => 1,
+ )
+%>
+
+ </TD>
+</TR>
+
+<SCRIPT TYPE="text/javascript">
+ contactnum_changed(document.getElementById('contactnum'));
+</SCRIPT>
+<%init>
+
+#based on / kinda false laziness w/tr-select-cust_contact.html
+
+my $conf = new FS::Conf;
+
+my %opt = @_;
+my $cgi = $opt{'cgi'};
+my $cust_pkg = $opt{'cust_pkg'};
+my $cust_main = $opt{'cust_main'};
+my $prospect_main = $opt{'prospect_main'};
+die "cust_main or prospect_main required" unless $cust_main or $prospect_main;
+
+my $contactnum = '';
+if ( $cgi->param('error') ) {
+ $cgi->param('contactnum') =~ /^(\-?\d*)$/ or die "illegal contactnum";
+ $contactnum = $1;
+} else {
+ if ( length($opt{'curr_value'}) ) {
+ $contactnum = $opt{'curr_value'};
+ } elsif ($prospect_main) {
+ my @cust_contact = $prospect_main->cust_contact;
+ $contactnum = $cust_contact[0]->contactnum if scalar(@cust_contact)==1;
+ } else { #$cust_main
+ $cgi->param('contactnum') =~ /^(\-?\d*)$/ or die "illegal contactnum";
+ $contactnum = $1;
+ }
+}
+
+##probably could use explicit controls
+#my $editable = $cust_main ? 0 : 1; #could use explicit control
+my $editable = 0;
+my $addnew = $cust_main ? 1 : ( $contactnum>0 ? 0 : 1 );
+
+my @contact_fields = map "contactnum_$_", qw( first last );
+
+my $contact; #the one that shows by default in the contact edit space
+if ( $contactnum && $contactnum > 0 ) {
+ $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
+ or die "unknown contactnum";
+} else {
+ $contact = new FS::contact;
+ if ( $contactnum == -1 ) {
+ $contact->$_( $cgi->param($_) ) foreach @contact_fields; #XXX
+ } elsif ( $cust_pkg && $cust_pkg->contactnum ) {
+ my $pkg_contact = $cust_pkg->contact_obj;
+ $contact->$_( $pkg_contact->$_ ) foreach @contact_fields; #XXX why are we making a new one gagain??
+ $opt{'empty_label'} ||= 'package contact: '.$pkg_contact->line;
+ } elsif ( $cust_main ) {
+ $contact = new FS::contact; #I think
+ }
+}
+
+my $contact_sort = sub {
+ lc($a->last) cmp lc($b->last)
+ or lc($a->first) cmp lc($b->first)
+};
+
+my @contact;
+push @contact, $cust_main->cust_contact if $cust_main;
+push @contact, $prospect_main->contact if $prospect_main;
+push @contact, $contact
+ if !$cust_main && $contact && $contact->contactnum > 0
+ && ! grep { $_->contactnum == $contact->contactnum } @contact;
+
+@contact = sort $contact_sort grep !$_->disabled, @contact;
+
+$contact = $contact[0]
+ if ( $prospect_main )
+ && !$opt{'is_optional'}
+ && @contact;
+
+my $disabled =
+ ( $contactnum < 0
+ || ( $editable && $contactnum )
+ || ( $prospect_main
+ && !$opt{'is_optional'} && !@contact && $addnew
+ )
+ )
+ ? ''
+ : 'DISABLED';
+
+my $th = $opt{'no_bold'} ? 'TD' : 'TH';
+
+</%init>
}
}
+ var location_fields = <% encode_json(\@location_fields) %>;
function update_location( string ) {
- var hash = eval('('+string+')');
- document.getElementById('address1').value = hash['address1'];
- document.getElementById('city').value = hash['city'];
- document.getElementById('zip').value = hash['zip'];
-
-% if ( $opt{'alt_format'} ) {
- changeSelect( document.getElementById('location_kind'), hash['location_kind']);
- changeSelect( document.getElementById('location_type'), hash['location_type']);
- document.getElementById('location_number').value = hash['location_number'];
-% } else {
- document.getElementById('address2').value = hash['address2'];
-% }
-
- var country_el = document.getElementById('country');
-
- changeSelect( country_el, hash['country'] );
-
- country_changed( country_el,
+ var hash = JSON.parse(string);
+ for(var i = 0; i < location_fields.length; i++) {
+ var f = location_fields[i];
+ if (hash[f] && document.getElementById(f)) {
+ document.getElementById(f).value = hash[f];
+ }
+ }
+ country_changed( document.getElementById('country'),
fix_state_factory( hash['state'],
hash['county']
)
<TD COLSPAN=7>
<SELECT NAME = "locationnum"
ID = "locationnum"
- onChange = "locationnum_changed(this);"
+ onchange = "locationnum_changed(this);"
>
% if ( $cust_main ) {
<OPTION VALUE="<% $cust_main->ship_locationnum %>"><% $opt{'empty_label'} || '(default service address)' |h %>
my $editable = $cust_main ? 0 : 1; #could use explicit control
my $addnew = $cust_main ? 1 : ( $locationnum>0 ? 0 : 1 );
-my @location_fields = qw( address1 address2 city county state zip country
- latitude longitude
- );
+my @location_fields = FS::cust_main->location_fields;
if ( $opt{'alt_format'} ) {
push @location_fields, qw( location_type location_number location_kind );
}
'' => '',
1 => 'VoIP without Broadband',
2 => 'VoIP with Broadband',
- 3 => 'Wholesale VoIP'
+ 3 => 'Wholesale VoIP',
+ 4 => 'Local Exchange (non-VoIP)',
);
</%init>
-<% objToJson(\@areacodes) %>
+<% encode_json(\@areacodes) %>\
<%init>
my( $state, $svcpart ) = $cgi->param('arg');
function custnum_update_callback(rownum, prefix) {
var custnum = document.getElementById('custnum'+rownum).value;
- document.getElementById('enable_app'+rownum).disabled = (
- custnum == 0 ||
- num_open_invoices[rownum] < 2
- );
+ // if there is a custnum and more than one open invoice, enable
+ // (and check) the box
+ var show_applications = (custnum > 0 && num_open_invoices[rownum] > 1);
+ var enable_app_checkbox = document.getElementById('enable_app'+rownum);
+ enable_app_checkbox.disabled = show_applications;
+
% if ( $use_discounts ) {
select_discount_term(rownum, prefix);
% }
function invnum_update_callback(rownum, prefix) {
custnum_update_callback(rownum, prefix);
- var enable = document.getElementById('enable_app'+rownum);
- enable.checked = true;
- toggle_application_row.call(enable);
}
function select_discount_term(row, prefix) {
next.call(this, rownum);
}
);
+ } else {
+ var row = document.getElementById('row'+rownum);
+ var table_rows = row.parentNode.rows;
+ for (i = row.sectionRowIndex; i < table_rows.count; i++) {
+ if ( table_rows[i].id.indexof('row'+rownum+'.') > -1 ) {
+ table_rows.removeChild(table_rows[i]);
+ } else {
+ break;
+ }
+ }
+ lock_payment_row(rownum, false);
}
}
$query =~ /^(\d+)$/;
my $svcnum = $1;
-#my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$svcnum});
-#die "Unknown svcnum!" unless $svc_acct;
-
+my $error = '';
my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$svcnum});
-die "Unknown svcnum!" unless $cust_svc;
-my $cust_pkg = $cust_svc->cust_pkg;
-if ( $cust_pkg ) {
- errorpage( 'This account has already been audited. Cancel the '.
- qq!<A HREF="${p}view/cust_main.cgi?!. $cust_pkg->custnum.
- '#cust_pkg'. $cust_pkg->pkgnum. '">'.
- 'package</A> instead.');
-}
+if ( $cust_svc ) {
+ my $cust_pkg = $cust_svc->cust_pkg;
+ if ( $cust_pkg ) {
+ errorpage( 'This account has already been audited. Cancel the '.
+ qq!<A HREF="${p}view/cust_main.cgi?!. $cust_pkg->custnum.
+ '#cust_pkg'. $cust_pkg->pkgnum. '">'.
+ 'package</A> instead.'); #'
+ }
-my $error = $cust_svc->cancel;
+ $error = $cust_svc->cancel;
+} else {
+ # the rare obscure case: svc_x without cust_svc
+ my $svc_x;
+ foreach my $svcdb (FS::part_svc->svc_tables) {
+ $svc_x = qsearchs($svcdb, { 'svcnum' => $svcnum });
+ last if $svc_x;
+ }
+ if ( $svc_x ) {
+ $error = $svc_x->return_inventory
+ || $svc_x->FS::Record::delete;
+ } else {
+ # the svcnum really doesn't exist
+ $error = "svcnum $svcnum not found";
+ }
+}
</%init>
--- /dev/null
+<& /elements/header-popup.html, mt("Change Package Contact") &>
+
+<& /elements/error.html &>
+
+<FORM ACTION="<% $p %>misc/process/change_pkg_contact.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
+
+<% ntable('#cccccc') %>
+
+ <TR>
+ <TH ALIGN="right"><% mt('Package') |h %></TH>
+ <TD COLSPAN=7 BGCOLOR="#dddddd">
+ <% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B> - <% $part_pkg->comment |h %>
+ </TD>
+ </TR>
+
+% if ( $cust_pkg->contactnum ) {
+ <TR>
+ <TH ALIGN="right"><% mt('Current Contact') %></TH>
+ <TD COLSPAN=7 BGCOLOR="#dddddd">
+ <% $cust_pkg->contact_obj->line |h %>
+ </TD>
+ </TR>
+% }
+
+<& /elements/tr-select-contact.html,
+ 'label' => mt('New Contact'), #XXX test
+ 'cgi' => $cgi,
+ 'cust_main' => $cust_pkg->cust_main,
+&>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE = "submit"
+ VALUE = "<% $cust_pkg->contactnum ? mt("Change contact") : mt("Add contact") |h %>"
+>
+
+</FORM>
+</BODY>
+</HTML>
+
+<%init>
+
+my $conf = new FS::Conf;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('Change customer package');
+
+my $pkgnum = scalar($cgi->param('pkgnum'));
+$pkgnum =~ /^(\d+)$/ or die "illegal pkgnum $pkgnum";
+$pkgnum = $1;
+
+my $cust_pkg =
+ qsearchs({
+ 'table' => 'cust_pkg',
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => { 'pkgnum' => $pkgnum },
+ 'extra_sql' => ' AND '. $curuser->agentnums_sql,
+ }) or die "unknown pkgnum $pkgnum";
+
+my $cust_main = $cust_pkg->cust_main
+ or die "can't get cust_main record for custnum ". $cust_pkg->custnum.
+ " ( pkgnum ". cust_pkg->pkgnum. ")";
+
+my $part_pkg = $cust_pkg->part_pkg;
+
+</%init>
% map { $value{$_} = $location{$_} } qw ( city state )
% if $location{country} eq 'CA';
%
-% my $value = encode_entities(objToJson({ %value })
+% my $value = encode_entities(encode_json({ %value })
% );
% my $content = '';
% $content .= $location->$_. ' ' x ( $max{$_} - length($location->$_) )
-<% objToJson( \@return ) %>
+<% encode_json( \@return ) %>\
<%init>
my( $custnum, $prospectnum, $classnum ) = $cgi->param('arg');
} );
die "No customer # $custnum" unless $cust_main;
- $error = $cust_main->merge($new_custnum);
+ if ( $cgi->param('merge') eq 'Y' ) {
+
+ #old-style merge: everything + delete old customer
+ $error = $cust_main->merge($new_custnum);
+
+ } else {
+
+ #new-style attach: move packages 3.0 style, that's it
+ $error = $cust_main->attach_pkgs($new_custnum);
+
+ }
} else {
$error = 'Select a customer to merge into';
--- /dev/null
+<%init>
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Edit customer note');
+
+my ($notenum) = $cgi->keywords;
+$notenum =~ /^\d+$/ or die "bad notenum '$notenum'";
+my $note = FS::cust_main_note->by_key($notenum)
+ or die "notenum '$notenum' not found";
+$note->delete;
+</%init>
+<% $cgi->redirect($p.'view/cust_main.cgi?'.$note->custnum) %>
--- /dev/null
+<& /elements/header-popup.html, mt("Detach Package to New Customer") &>
+
+<SCRIPT TYPE="text/javascript" SRC="../elements/order_pkg.js"></SCRIPT>
+
+<& /elements/error.html &>
+
+<FORM NAME="OrderPkgForm" ACTION="<% $p %>edit/process/detach-cust_pkg.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
+% foreach my $f (qw( agentnum refnum )) {
+ <INPUT TYPE="hidden" NAME="<% $f %>" VALUE="<% $cust_main->$f() %>">
+% }
+<INPUT TYPE="hidden" NAME="referral_custnum" VALUE="<% $cust_main->custnum %>">
+% foreach my $f (FS::cust_main->location_fields) {
+ <INPUT TYPE="hidden" NAME="<% $f %>" VALUE="<% $loc->$f() |h %>">
+% }
+
+<% ntable('#cccccc') %>
+
+ <TR>
+ <TH ALIGN="right"><% mt('Package') |h %></TH>
+ <TD COLSPAN=7 BGCOLOR="#dddddd">
+ <% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B> - <% $part_pkg->comment |h %>
+ </TD>
+ </TR>
+
+% #always should be present for detaching, yes? #if ( $cust_pkg->contactnum ) {
+% my $cust_contact = $cust_pkg->contact_obj;
+
+ <INPUT TYPE="hidden" NAME="first" VALUE="<% $cust_contact->get('first') |h %>">
+ <INPUT TYPE="hidden" NAME="last" VALUE="<% $cust_contact->get('last') |h %>">
+
+ <TR>
+ <TH ALIGN="right"><% mt('Name') %></TH>
+ <TD COLSPAN=7 BGCOLOR="#dddddd">
+ <% $cust_pkg->contact_obj->line |h %>
+ </TD>
+ </TR>
+% #}
+
+ <TR>
+ <TH ALIGN="right" VALIGN="top"><% mt('Address') %></TH>
+ <TD COLSPAN=7 BGCOLOR="#dddddd">
+
+ <% $loc->location_label( 'join_string' => '<BR>',
+ 'double_space' => ' ',
+ 'escape_function' => \&encode_entities,
+ 'countrydefault' => $countrydefault,
+ )
+ %>
+ </TD>
+ </TR>
+
+</TABLE>
+
+%#XXX payment info
+%#XXX should be sticky on errors...
+<& /edit/cust_main/billing.html, FS::cust_main->new({}),
+ invoicing_list => [],
+
+&>
+
+<BR>
+<BR>
+<INPUT NAME = "submitButton"
+ TYPE = "submit"
+ VALUE = "<% mt("Detach package") |h %>"
+>
+
+%#and a cancel button? or is the popup close sufficient?
+
+</FORM>
+</BODY>
+</HTML>
+
+<%init>
+
+my $conf = new FS::Conf;
+my $countrydefault = $conf->config('countrydefault') || 'US';
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied"
+ unless $curuser->access_right('Change customer package');
+
+my $pkgnum = scalar($cgi->param('pkgnum'));
+$pkgnum =~ /^(\d+)$/ or die "illegal pkgnum $pkgnum";
+$pkgnum = $1;
+
+my $cust_pkg =
+ qsearchs({
+ 'table' => 'cust_pkg',
+ 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+ 'hashref' => { 'pkgnum' => $pkgnum },
+ 'extra_sql' => ' AND '. $curuser->agentnums_sql,
+ }) or die "unknown pkgnum $pkgnum";
+
+my $loc = $cust_pkg->cust_location_or_main;
+
+my $cust_main = $cust_pkg->cust_main
+ or die "can't get cust_main record for custnum ". $cust_pkg->custnum.
+ " ( pkgnum ". cust_pkg->pkgnum. ")";
+
+my $part_pkg = $cust_pkg->part_pkg;
+
+</%init>
-<% objToJson(\@exchanges) %>
+<% encode_json(\@exchanges) %>\
<%init>
my( $areacode, $svcpart ) = $cgi->param('arg');
-<% objToJson(\%hash) %>
+<% encode_json(\%hash) %>\
<%init>
my $locationnum = $cgi->param('arg');
my %hash = ();
%hash = map { $_ => $cust_location->$_() }
- qw( address1 address2 city county state zip country
- location_kind location_type location_number )
+ ( FS::cust_main->location_fields,
+ qw( location_kind location_type location_number )
+ )
if $cust_location;
</%init>
-<% objToJson(\@macs) %>
+<% encode_json(\@macs) %>\
<%init>
# XXX: this should be agent-virtualized / limited
-<% objToJson( $return ) %>
+<% encode_json( $return ) %>\
<%init>
my $return;
-<% include('/elements/header-popup.html', 'Merge customer' ) %>
+<& /elements/header-popup.html, 'Merge customer' &>
-<% include('/elements/error.html') %>
+<& /elements/error.html &>
<FORM NAME="cust_merge_popup" ID="cust_merge_popup" ACTION="<% popurl(1) %>cust_main-merge.html" METHOD=POST onSubmit="submit_merge(); return false;">
<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
<TABLE BORDER="0" CELLSPACING="2" STYLE="margin-left:auto; margin-right:auto">
- <% include('/elements/tr-search-cust_main.html',
+
+ <& /elements/tr-search-cust_main.html,
'label' => 'Merge into: ',
'field' => 'new_custnum',
'find_button' => 1,
'curr_value' => scalar($cgi->param('new_custnum')),
- )
- %>
+ &>
+
+% if ( $conf->exists('deletecustomers') ) {
+
+% if ( scalar($cust_main->ncancelled_pkgs) ) {
+ <TR>
+ <TD COLSPAN=2>
+ <& /elements/radio.html,
+ 'field' => 'merge',
+ 'value' => '',
+ 'curr_value' => scalar($cgi->param('merge')),
+ &>
+ Merge packages only.
+ </TD>
+ </TR>
+% } else {
+% $cgi->param('merge', 'Y');
+% }
+
+ <TR>
+ <TD COLSPAN=2>
+ <& /elements/radio.html,
+ 'field' => 'merge',
+ 'value' => 'Y',
+ 'curr_value' => scalar($cgi->param('merge')),
+ &>
+ Merge invoices, payments/credits, notes, tickets and delete this customer.
+ </TD>
+ </TR>
+% }
+
</TABLE>
<P ALIGN="CENTER">
<%init>
+my $conf = new FS::Conf;
+
$cgi->param('custnum') =~ /^(\d+)$/ or die 'illegal custnum';
my $custnum = $1;
&>
% }
+<& /elements/tr-select-contact.html,
+ 'cgi' => $cgi,
+ 'cust_main' => $cust_main,
+ 'prospect_main' => $prospect_main,
+&>
+
% if ( $cgi->param('lock_locationnum') ) {
<INPUT TYPE = "hidden"
-<% objToJson(\@output) %>
+<% encode_json(\@output) %>\
<%init>
my $conf = new FS::Conf;
-<% objToJson(\@phonenums) %>
+<% encode_json(\@phonenums) %>\
<%init>
my( $exchangestring, $svcpart ) = $cgi->param('arg');
--- /dev/null
+<% header(emt("Package contact $past_method")) %>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+ </BODY>
+</HTML>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Change customer package');
+
+#untaint pkgnum
+my $pkgnum = $cgi->param('pkgnum');
+$pkgnum =~ /^(\d+)$/ or die "Illegal pkgnum";
+$pkgnum = $1;
+
+my $cust_pkg = qsearchs( 'cust_pkg', {'pkgnum'=>$pkgnum} ); #needs agent virt
+
+my $contactnum = $cgi->param('contactnum');
+$contactnum =~ /^(-?\d*)$/ or die "Illegal contactnum";
+$contactnum = $1;
+
+my $past_method = $cust_pkg->contactnum ? 'changed' : 'added';
+
+my $error = '';
+
+if ( $contactnum == -1 ) {
+
+ #little false laziness w/edit/process/quick-cust_pkg.cgi, also the whole
+ # thing should be a single transaction
+ my $contact = new FS::contact {
+ 'custnum' => $cust_pkg->custnum,
+ map { $_ => scalar($cgi->param("contactnum_$_")) } qw( first last )
+ };
+ $error = $contact->insert;
+ $cust_pkg->contactnum( $contact->contactnum );
+
+} else {
+ $cust_pkg->contactnum($contactnum);
+}
+
+$error ||= $cust_pkg->replace;
+
+if ($error) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "change_pkg_contact.html?". $cgi->query_string );
+}
+
+</%init>
-<% objToJson(\@regions) %>
+<% encode_json(\@regions) %>\
<%init>
my( $state, $svcpart ) = $cgi->param('arg');
-<% encode_json($return) %>
+<% encode_json($return) %>\
<%init>
local $SIG{__DIE__}; #disable Mason error trap
-<% objToJson($return) %>
+<% encode_json($return) %>\
<%init>
my $DEBUG = 0;
-<% encode_json(\@return) %>
+<% encode_json(\@return) %>\
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
my @return;
if ( $cgi->param('sub') eq 'custnum_search_open' ) {
my $custnum = $cgi->param('arg');
- #warn "searching invoices for $custnum\n";
- my $cust_main = FS::cust_main->by_key($custnum);
- @return = map {
- +{ $_->hash,
- 'owed' => $_->owed }
- } $cust_main->open_cust_bill
- if $curuser->agentnums_href->{ $cust_main->agentnum };
+ if ( $custnum =~ /^(\d+)$/ ) {
+#warn "searching invoices for $custnum\n";
+ my $cust_main = FS::cust_main->by_key($custnum);
+ @return = map {
+ +{ $_->hash,
+ 'owed' => $_->owed }
+ } $cust_main->open_cust_bill
+ if $curuser->agentnums_href->{ $cust_main->agentnum };
+ }
}
</%init>
-<% to_json($return) %>
+<% encode_json($return) %>\
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
-<% objToJson($return) %>
+<% encode_json($return) %>\
<%init>
my %arg = $cgi->param('arg');
% }
% }
%
-<% objToJson($return) %>
+<% encode_json($return) %>\
% }
<%init>
-<% JSON::to_json(\@result) %>\
+<% encode_json(\@result) %>\
<%init>
die 'access denied'
unless $FS::CurrentUser::CurrentUser->access_right('Edit customer');
% # cust_main-agent_custid-format') eq 'ww?d+'
% $return = findbycustnum_or_agent_custid($1);
% }
-<% objToJson($return) %>
+<% encode_json($return) %>\
% } elsif ( $sub eq 'smart_search' ) {
%
% my $string = $cgi->param('arg');
% @cust_main
% ];
%
-<% objToJson($return) %>
+<% encode_json($return) %>\
% } elsif ( $sub eq 'invnum_search' ) {
%
% my $string = $cgi->param('arg');
% if ( $string =~ /^(\d+)$/ ) {
% my $inv = qsearchs('cust_bill', { 'invnum' => $1 });
% my $return = $inv ? findbycustnum($inv->custnum) : [];
-<% objToJson($return) %>
+<% encode_json($return) %>\
% } else { #return nothing
[]
% }
% city => $_->city,
% };
% }
-<% objToJson($return) %>
+<% encode_json($return) %>\
% }
<%init>
-<% objToJson($return) %>
+<% encode_json($return) %>\
<%init>
my $conf = new FS::Conf;
--- /dev/null
+% if ( $sub eq 'smart_search' ) {
+%
+% my $string = $cgi->param('arg');
+% my @svc_broadband = FS::svc_broadband->smart_search( $string );
+% my $return = [ map { my $cust_pkg = $_->cust_svc->cust_pkg;
+% [ $_->svcnum,
+% $_->label. ( $cust_pkg
+% ? ' ('. $cust_pkg->cust_main->name. ')'
+% : ''
+% ),
+% ];
+% }
+% @svc_broadband,
+% ];
+%
+<% encode_json($return) %>\
+% }
+<%init>
+
+my $sub = $cgi->param('sub');
+
+</%init>
<Form_477_submission xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://specialreports.fcc.gov/wcb/Form477/XMLSchema-instance/form_477_upload_Schema.xsd" >
% } else { #html
<& /elements/header.html, "FCC Form 477 Results - $state" &>
+%# XXX when we stop supporting IE8, add this to freeside.css using :nth-child
+%# selectors, and remove it from everywhere else
+<STYLE TYPE="text/css">
+.grid TH { background-color: #cccccc; padding: 0px 3px 2px; text-align: right }
+.row0 TD { background-color: #eeeeee; padding: 0px 3px 2px; text-align: right }
+.row1 TD { background-color: #ffffff; padding: 0px 3px 2px; text-align: right }
+</STYLE>
+
<TABLE WIDTH="100%">
<TR>
<TD></TD>
% if ( $type eq 'xml' ) {
<<% 'Part_IA_'. chr(65 + $tech) %>>
% }
-<& "477part${part}_summary.html", 'tech_code' => $tech, 'url' => $url &>
-<& "477part${part}_detail.html", 'tech_code' => $tech, 'url' => $url &>
+<& "477part${part}.html",
+ 'tech_code' => $tech,
+ 'url' => $url,
+ 'type' => $type
+&>
% if ( $type eq 'xml' ) {
</<% 'Part_IA_'. chr(65 + $tech) %>>
% }
--- /dev/null
+% if ( $opt{'type'} eq 'xml' ) {
+%# container element <Part_IA_$tech> is in 477.html
+% my $col = 'a';
+% foreach ( @summary_row ) {
+% my $el = $xml_prefix . $col . '1'; # PartIA_Aa1, PartIA_Ab1, etc.
+ <<% $el %>><% $_ %><<% "/$el" %>>
+% $col++;
+% }
+% foreach my $col_data ( @data ) {
+% my $row = 1;
+% foreach my $cell ( @$col_data ) {
+% my $el = $xml_prefix . $col . $row; # PartIA_Af1, PartIA_Af2...
+ <<% $el %>><% $cell->[0] %><<% "/$el" %>>
+% if ( $percentages ) {
+% $el = $xml_percent . $col . $row; # Part_p_IA_Af1, ...
+ <<% $el %>><% $cell->[1] %><<% "/$el" %>>
+% }
+% $row++;
+% } # foreach $cell
+% $col++;
+% } # foreach $col_data
+% } else { # not XML
+
+<H2><% $title %> totals</H2>
+<& /elements/table-grid.html &>
+ <TR>
+% foreach ( 'Total Connections',
+% '% owned loop',
+% '% billed to end users',
+% '% residential',
+% '% residential > 200 kbps') {
+ <TH WIDTH="20%"><% $_ |h %></TH>
+% }
+ </TR>
+ <TR CLASS="row0">
+% foreach ( @summary_row ) {
+ <TD><% $_ %></TD>
+% }
+ </TR>
+</TABLE>
+<H2><% $title %> breakdown by speed</H2>
+<TABLE CLASS="grid" CELLSPACING=0>
+ <TR>
+ <TH WIDTH="12%"></TH>
+% for (my $col = 0; $col < scalar(@download_option); $col++) {
+ <TH WIDTH="11%">
+ <% $FS::Report::FCC_477::download[$col] |h %>
+ </TH>
+% }
+ </TR>
+% for (my $row = 0; $row < scalar(@upload_option); $row++) {
+ <TR CLASS="row<% $row % 2%>">
+ <TD STYLE="text-align: left; font-weight: bold">
+% if ( $asymmetric ) {
+ <% $FS::Report::FCC_477::upload[$row] |h %>
+% }
+ </TD>
+% for (my $col = 0; $col < scalar(@download_option); $col++) {
+ <TD>
+ <% $data[$col][$row][0] %>
+% if ( $percentages ) {
+ <BR><% $data[$col][$row][1] %>
+% }
+ </TD>
+% } # for $col
+ </TR>
+% } # for $row
+</TABLE>
+% }
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('List packages');
+
+my %opt = @_;
+my %search_hash;
+
+for ( qw(agentnum state) ) {
+ $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
+}
+$search_hash{'status'} = 'active';
+$search_hash{'country'} = 'US';
+$search_hash{'classnum'} = [ $cgi->param('classnum') ];
+
+# arrays of report_option_ numbers, running parallel to
+# the download and upload speed arrays
+my @download_option = $cgi->param('part1_column_option');
+my @upload_option = $cgi->param('part1_row_option');
+
+my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi);
+
+my $total_count = 0;
+my $total_residential = 0;
+my $above_200 = 0;
+my $tech_code = $opt{tech_code};
+my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown';
+my $title = "Part IA $technology";
+my $xml_prefix = 'PartIA_'. chr(65 + $tech_code);
+my $xml_percent = 'Part_p_IA_'. chr(65 + $tech_code); # yes, seriously
+
+# whether to show the results as a matrix (upload speeds in rows) or a single
+# row
+my $asymmetric = 1;
+if ( $technology eq 'Symmetric xDSL' or $technology eq 'Other Wireline' ) {
+ $asymmetric = 0;
+ @upload_option = ( undef );
+}
+# whether to show residential percentages in each cell of the matrix
+my $percentages = ($technology eq 'Terrestrial Mobile Wireless');
+
+my $query = FS::cust_pkg->search(\%search_hash);
+my $count_query = $query->{'count_query'};
+
+my $is_residential = " AND COALESCE(cust_main.company, '') = ''";
+my $has_option = sub {
+ my $optionnum = shift;
+ $optionnum =~ /^\d+$/ ?
+ " AND EXISTS(
+ SELECT 1 FROM part_pkg_option
+ WHERE part_pkg_option.pkgpart = part_pkg.pkgpart
+ AND optionname = 'report_option_$optionnum'
+ AND optionvalue = '1'
+ )" : '';
+};
+
+# limit to those that have technology option $tech_code
+$count_query .= $has_option->($technology_option[$tech_code]);
+
+my @data;
+for ( my $row = 0; $row < scalar @upload_option; $row++ ) {
+ for ( my $col = 0; $col < scalar @download_option; $col++ ) {
+
+ my $this_count_query = $count_query .
+ $has_option->($upload_option[$row]) .
+ $has_option->($download_option[$col]);
+
+ my $count = FS::Record->scalar_sql($this_count_query);
+ my $residential = FS::Record->scalar_sql($this_count_query . $is_residential);
+
+ my $percent = sprintf('%.2f', $count ? 100 * $residential / $count : 0);
+ $data[$col][$row] = [ $count, $percent ];
+
+ $total_count += $count;
+ $total_residential += $residential;
+ $above_200 += $residential if $row > 0 or !$asymmetric;
+ }
+}
+
+my $total_percentage =
+ sprintf("%.2f", $total_count ? 100*$total_residential/$total_count : 0);
+
+my $above_200_percentage =
+ sprintf("%.2f", $total_count ? 100*$above_200/$total_count : 0);
+
+my @summary_row = (
+ $total_count,
+ 100.00, # own local loop--consistent with previous practice, but probably wrong
+ 100.00, # billed to end user--also wrong
+ $total_percentage, # residential percentage
+ $above_200_percentage,
+);
+
+</%init>
+++ /dev/null
-<& elements/search.html,
- 'html_init' => $html_init,
- 'name' => 'lines',
- 'query' => $query,
- 'count_query' => $count_query,
- 'really_disable_download' => 1,
- 'disable_download' => 1,
- 'nohtmlheader' => 1,
- 'disable_total' => 1,
- 'header' => [ '', @column_option_name ],
- 'xml_elements' => [ @xml_elements ],
- 'xml_omit_empty' => 1,
- 'fields' => [ @fields ],
-
-&>
-<%init>
-
-my $curuser = $FS::CurrentUser::CurrentUser;
-
-die "access denied"
- unless $curuser->access_right('List packages');
-
-my %opt = @_;
-my %search_hash = ();
-
-for ( qw(agentnum magic state) ) {
- $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
-}
-$search_hash{'country'} = 'US';
-
-$search_hash{'classnum'} = [ $cgi->param('classnum') ];
-
-my @column_option = grep { /^\d+/ } $cgi->param('part1_column_option')
- if $cgi->param('part1_column_option');
-
-my @row_option = grep { /^\d+/ } $cgi->param('part1_row_option')
- if $cgi->param('part1_row_option');
-
-my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi);
-
-my @column_option_name = scalar(@column_option)
- ? ( map { my $part_pkg_report_option =
- qsearchs({ 'table' => 'part_pkg_report_option',
- 'hashref' => { num => $_ },
- });
- $part_pkg_report_option ? $part_pkg_report_option->name
- : 'no such report option';
- } @column_option
- )
- : ( 'all packages' );
-
-my $where = join(' OR ', map { "num = $_" } @row_option );
-my %row_option_name = $where ?
- ( map { $_->num => $_->name }
- qsearch({ 'table' => 'part_pkg_report_option',
- 'hashref' => {},
- 'extra_sql' => "WHERE $where",
- })
- ) :
- ();
-
-my $tech_code = $opt{tech_code};
-my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown';
-my $html_init = "<H2>Part IA $technology breakdown by speeds</H2>";
-my $xml_prefix = 'PartIA_'. chr(65 + $tech_code);
-
-if ($cgi->param('_type') eq 'xml') {
- #rotate data pi/2
- my @temp = @column_option;
- @column_option = @row_option;
- @row_option = @temp;
-}
-
-my $query = 'SELECT '. join(' UNION ALL SELECT ',@row_option);
-my $count_query = 'SELECT '. scalar(@row_option);
-
-my $xml_element = 'OOPS, I was never set';
-my $rowchar = 101; # 'e' -- rows are columns! (pi/2)
-
-my $value = sub {
- my ($rowref, $column) = (shift, shift);
- my $row = $rowref->[0];
-
- if ($column eq 'name') {
- return $row_option_name{$row} || 'no such report option';
- } elsif ( $column =~ /^(\d+)$/ ) {
- my @report_option = ( $row || '',
- $column_option[$column] || '',
- $technology_option[$tech_code] || '',
- );
-
- my ( $count, $residential ) = FS::cust_pkg->fcc_477_count(
- { %search_hash, 'report_option' => join(',', @report_option) }
- );
-
- my $percentage = sprintf('%.2f', $count ? 100 * $residential / $count : 0);
- my $return = $count;
-
- if ($cgi->param('_type') eq 'xml') {
- $rowchar++ if $column == 0;
- $xml_element = $xml_prefix. chr($rowchar). ($column+1);
- $return = '' if $count == 0 and $cgi->param('_type') eq 'xml';
- } else {
- $return .= "<BR>$percentage% residential";
- }
-
- return $return;
- } else {
- return '<FONT SIZE="+1" COLOR="#ff0000">Bad call to column_value</FONT>';
- }
-};
-
-my @fields = map { my $ci = $_; sub { &{$value}(shift, $ci); } }
- ( 'name', (0 .. $#column_option) );
-shift @fields if $cgi->param('_type') eq 'xml';
-
-my @xml_elements = ( # -- columns are rows! (pi/2)
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
- sub { return $xml_element; },
-);
-
-</%init>
+++ /dev/null
-<& elements/search.html,
- 'html_init' => $html_init,
- 'name' => 'lines',
- 'query' => 'SELECT 1',
- 'count_query' => 'SELECT 1',
- 'really_disable_download' => 1,
- 'disable_download' => 1,
- 'nohtmlheader' => 1,
- 'disable_total' => 1,
- 'header' => [
- 'Total Connections',
- '% owned loop',
- '% billed to end users',
- '% residential',
- '% residential > 200kbps',
- ],
- 'xml_elements' => [
- $xml_prefix. 'a1',
- $xml_prefix. 'b1',
- $xml_prefix. 'c1',
- $xml_prefix. 'd1',
- $xml_prefix. 'e1',
- ],
- 'fields' => [
- sub { $total_count },
- sub { '100.00' },
- sub { '100.00' },
- sub { $total_percentage },
- sub { $above_200_percentage },
- ],
-
-&>
-<%init>
-
-my $curuser = $FS::CurrentUser::CurrentUser;
-
-die "access denied"
- unless $curuser->access_right('List packages');
-
-my %opt = @_;
-my %search_hash = ();
-
-for ( qw(agentnum magic state) ) {
- $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
-}
-$search_hash{'country'} = 'US';
-$search_hash{'classnum'} = [ $cgi->param('classnum') ];
-
-my @column_option = grep { /^\d+$/ } $cgi->param('part1_column_option')
- if $cgi->param('part1_column_option');
-
-my @row_option = grep { /^\d+$/ } $cgi->param('part1_row_option')
- if $cgi->param('part1_row_option');
-
-my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi);
-
-my $total_count = 0;
-my $total_residential = 0;
-my $above_200 = 0;
-my $tech_code = $opt{tech_code};
-my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown';
-my $html_init = "<H2>Part IA $technology totals</H2>";
-my $xml_prefix = 'PartIA_'. chr(65 + $tech_code);
-
-my $not_first_row = 0; # ugh;
-foreach my $row ( @row_option ) {
- foreach my $column ( @column_option ) {
-
- my @report_option = ( $row || '-1', $column || '-1', $technology_option[$tech_code] );
-
- my ( $count, $residential ) = FS::cust_pkg->fcc_477_count(
- { %search_hash, 'report_option' => join(',', @report_option) }
- );
-
- $total_count += $count;
- $total_residential += $residential;
- $above_200 += $residential if $not_first_row;
- }
- $not_first_row++;
-}
-
-my $total_percentage =
- sprintf("%.2f", $total_count ? 100*$total_residential/$total_count : 0);
-
-my $above_200_percentage =
- sprintf("%.2f", $total_count ? 100*$above_200/$total_count : 0);
-
-
-</%init>
-<& elements/search.html,
- 'html_init' => $html_init,
- 'name' => 'lines',
- 'query' => $query,
- 'count_query' => 'SELECT 11',
- 'really_disable_download' => 1,
- 'disable_download' => 1,
- 'nohtmlheader' => 1,
- 'disable_total' => 1,
- 'header' => [ @headers ],
- 'xml_elements' => [ @xml_elements ],
- 'fields' => [ @fields ],
-
-&>
+% if ( $cgi->param('_type') eq 'xml' ) {
+% my @cols = qw(a b c d);
+% for ( my $row = 0; $row < scalar(@rows); $row++ ) {
+% for my $col (0..3) {
+% if ( exists($data[$col][$row]) and $data[$col][$row] > 0 ) {
+<PartII_<% $row + 1 %><% $cols[$col] %>>\
+<% $data[$col][$row] %>\
+</PartII_<% $row + 1 %><% $cols[$col] %>>
+% }
+% } #for $col
+% } #for $row
+% } else { # HTML mode
+% # fake up the search-html.html header
+<H2>Part IIA</H2>
+<TABLE>
+ <TR><TD VALIGN="bottom"><BR></TD></TR>
+ <TR><TD COLSPAN=2>
+ <TABLE CLASS="grid" CELLSPACING=0>
+ <TR>
+% foreach (@row1_headers) {
+ <TH><% $_ %></TH>
+% }
+ </TR>
+% my $row = 0;
+% foreach my $rowhead (@rows) {
+ <TR CLASS="row<%$row % 2%>">
+ <TD STYLE="text-align: left; font-weight: bold"><% $rowhead %></TD>
+% for my $col (0..3) {
+ <TD>
+% if ( exists($data[$col][$row]) ) {
+ <% $data[$col][$row] %>
+% }
+ </TD>
+% } # for $col
+ </TR>
+% $row++;
+% } #for $rowhead
+ </TABLE>
+ </TD></TR>
+</TABLE>
+% } #XML/HTML
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
die "access denied"
unless $curuser->access_right('List packages');
-my $html_init = '<H2>Part IIA</H2>';
my %search_hash = ();
-
-for ( qw(agentnum magic state) ) {
- $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
-}
-$search_hash{'country'} = 'US';
-$search_hash{'classnum'} = [ $cgi->param('classnum') ];
-
-my @row_option = grep { /^\d+$/ } $cgi->param('part2a_row_option')
- if $cgi->param('part2a_row_option');
-
-# fudge in two rows of LD carrier
-unshift @row_option, $row_option[0];
-
-# fudge in the first pair of rows
-unshift @row_option, '';
-unshift @row_option, '';
-
-my $query = 'SELECT '. join(' UNION SELECT ', 1..11);
-my $total_count = 0;
-my $column_value = sub {
- my $row = shift;
-
- my @report_option = ( $row_option[$row - 1] || '' );
-
- my $sql_query = FS::cust_pkg->search(
- { %search_hash, 'report_option' => join(',', @report_option) }
- );
-
- my $count_sql = delete($sql_query->{'count_query'});
- if ( $row == 2 || $row == 4 ) {
- $count_sql =~ s/COUNT\(\*\) FROM/sum(COALESCE(CASE WHEN cust_main.company IS NULL OR cust_main.company = '' THEN CASE WHEN part_pkg.fcc_ds0s IS NOT NULL AND part_pkg.fcc_ds0s > 0 THEN part_pkg.fcc_ds0s WHEN pkg_class.fcc_ds0s IS NOT NULL AND pkg_class.fcc_ds0s > 0 THEN pkg_class.fcc_ds0s ELSE 0 END ELSE 0 END, 0) ) FROM/
- or die "couldn't parse count_sql";
- } else {
- $count_sql =~ s/COUNT\(\*\) FROM/sum(COALESCE(CASE WHEN part_pkg.fcc_ds0s IS NOT NULL AND part_pkg.fcc_ds0s > 0 THEN part_pkg.fcc_ds0s WHEN pkg_class.fcc_ds0s IS NOT NULL AND pkg_class.fcc_ds0s > 0 THEN pkg_class.fcc_ds0s ELSE 0 END, 0)) FROM/
- or die "couldn't parse count_sql";
- }
-
- my $count_sth = dbh->prepare($count_sql)
- or die "Error preparing $count_sql: ". dbh->errstr;
- $count_sth->execute
- or die "Error executing $count_sql: ". $count_sth->errstr;
- my $count_arrayref = $count_sth->fetchrow_arrayref;
- my $count = $count_arrayref->[0];
+$search_hash{'agentnum'} = $cgi->param('agentnum');
+$search_hash{'state'} = $cgi->param('state');
+$search_hash{'classnum'} = [ $cgi->param('classnum') ];
+$search_hash{'status'} = 'active';
- $total_count = $count if $row == 1;
- $count = sprintf('%.2f', $total_count ? 100*$count/$total_count : 0)
- if $row != 1;
+my @row_option;
+foreach ($cgi->param('part2a_row_option')) {
+ push @row_option, (/^\d+$/ ? $_ : undef);
+}
- return "$count";
+my $is_residential = "AND COALESCE(cust_main.company, '') = ''";
+my $has_report_option = sub {
+ map {
+ defined($row_option[$_]) ?
+ " AND EXISTS(
+ SELECT 1 FROM part_pkg_option
+ WHERE part_pkg_option.pkgpart = part_pkg.pkgpart
+ AND optionname = 'report_option_" . $row_option[$_]."'
+ AND optionvalue = '1'
+ )" : ' AND FALSE'
+ } @_
};
-my @headers = (
- '',
- 'End user lines',
- 'UNE-P replacement',
- 'UNE (unswitched)',
- 'UNE-P',
+# an arrayref for each column
+my @data;
+# get the skeleton of the query
+my $sql_query = FS::cust_pkg->search(\%search_hash);
+my $from_where = $sql_query->{'count_query'};
+$from_where =~ s/^SELECT COUNT\(\*\) //;
+
+# for row 1
+my $query_ds0 = "SELECT SUM(COALESCE(part_pkg.fcc_ds0s, pkg_class.fcc_ds0s, 0))
+ $from_where AND fcc_voip_class = '4'"; # 4 = Local Exchange
+
+my $total_lines = FS::Record->scalar_sql($query_ds0);
+# always return zero for the number of resold lines, until an actual ILEC
+# starts using this report
+
+@data = (
+ [ $total_lines ],
+ [ 0 ],
+ [ 0 ],
+ [ 0 ],
);
-my @xml_elements = (
- sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}a" },
- sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}b" },
- sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}c" },
- sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}d" },
+my @row_conds = (
+ $is_residential,
+ $has_report_option->(0), # LD carrier
+ ($has_report_option->(0))[0] . $is_residential,
+ $has_report_option->(1..7),
);
+if ( $total_lines > 0 ) {
+ foreach (@row_conds) {
+ my $sql = $query_ds0 . $_;
+ my $lines = FS::Record->scalar_sql($sql);
+ my $percent = sprintf('%.2f', 100 * $lines / $total_lines);
+ push @{ $data[0] }, $percent;
+ }
+}
my @rows = (
'lines',
'% residential',
'% LD carrier',
- '% residential and LD carrier',
- '% own loops',
- '% obtained unswitched UNE loops',
+ '% residential and LD',
+ '% owned loops',
+ '% unswitched UNE',
'% UNE-P',
'% UNE-P replacement',
'% FTTP',
'% wireless',
);
-my @fields = (
- sub { my $row = shift; $rows[$row->[0] - 1]; },
- sub { my $row = shift; &{$column_value}($row->[0]); },
- sub { 0; },
- sub { 0; },
- sub { 0; },
+my @row1_headers = (
+ '',
+ 'End user lines',
+ 'UNE-P replacement',
+ 'unswitched UNE',
+ 'UNE-P',
);
-shift @fields if $cgi->param('_type') eq 'xml';
</%init>
% for ( my $row = 0; $row < scalar(@rows); $row++ ) {
% for my $col (0..2) {
% if ( exists($data[$col][$row]) ) {
-<PartII_<% $row %><% $cols[$col] %>>
+<PartII_<% $row + 1 %><% $cols[$col] %>>\
+<% $data[$col][$row] %>\
+</PartII_<% $row + 1 %><% $cols[$col] %>>
% }
-</PartII_<% $row %><% $cols[$col] %>>
% } #for $col
% } #for $row
% } else { # HTML mode
<TABLE>
<TR><TD VALIGN="bottom"><BR></TD></TR>
<TR><TD COLSPAN=2>
- <TABLE CLASS="grid" CELLSPACING=0 STYLE="border: 1px solid #cccccc;" BGCOLOR="#cccccc">
+ <TABLE CLASS="grid" CELLSPACING=0>
<TR>
% foreach (@headers) {
- <TH class="grid"><% $_ %></TH>
+ <TH><% $_ %></TH>
% }
</TR>
-% my @bgcolor = ('eeeeee','ffffff');
% my $row = 0;
% foreach my $rowhead (@rows) {
- <TR>
- <TD CLASS="grid" BGCOLOR="#<% $bgcolor[$row % 2] %>"><% $rowhead %></TD>
+ <TR CLASS="row<% $row % 2 %>">
+ <TD STYLE="text-align: left; font-weight: bold"><% $rowhead %></TD>
% for my $col (0..2) {
- <TD CLASS="grid" BGCOLOR="#<% $bgcolor[$row % 2] %>">
+ <TD>
% if ( exists($data[$col][$row]) ) {
<% $data[$col][$row] %>
% }
+% if ( $cgi->param('_type') =~ /^xml$/ ) {
+<zip_code>
+% }
<& elements/search.html,
'html_init' => $html_init,
'name' => 'zip code',
&>
+% if ( $cgi->param('_type') =~ /^xml$/ ) {
+</zip_code>
+% }
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
%# still not a good way to do rows grouped by some field in a search.html
%# report
+% if ( $type eq 'xls' ) {
+<% $data %>\
+% } else {
<& /elements/header.html, $title &>
+<P ALIGN="right" CLASS="noprint">
+Download full results<BR>
+as <A HREF="<% $cgi->self_url %>;_type=xls">Excel spreadsheet</A></P>
<BR>
<STYLE TYPE="text/css">
td.cust_head {
</TR>
</TABLE>
<& /elements/footer.html &>
+% }
<%init>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
my $money_char = FS::Conf->new->config('money_char') || '$';
-#my $count_query =
-# 'SELECT COUNT(*) FROM cust_pkg '.$query->{'addl_from'}.$query->{'extra_sql'}.
-# ' AND EXISTS(SELECT 1 FROM cust_bill_pkg JOIN cust_bill USING (invnum) '.
-# ' WHERE cust_bill_pkg.pkgnum = cust_pkg.pkgnum AND '.
-# "cust_bill._date >= $begin AND cust_bill._date < $end".
-# ')';
+my $data = '';
+my $type = $cgi->param('_type');
+if ( $type eq 'xls') {
+ # some false laziness with the above...
+ my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format;
+ my $filename = 'agent_commission' . $format->{extension};
+ http_header('Content-Type' => $format->{mime_type});
+ http_header('Content-Disposition' => qq!attachment;filename="$filename"!);
+ my $XLS = IO::Scalar->new(\$data);
+ my $workbook = $format->{class}->new($XLS);
+ my $worksheet = $workbook->add_worksheet(substr($title, 0, 31));
+
+ my $cust_head_format = $workbook->add_format(
+ bold => 1,
+ underline => 1,
+ text_wrap => 0,
+ bg_color => 'white',
+ );
+
+ my $col_head_format = $workbook->add_format(
+ bold => 1,
+ align => 'center',
+ bg_color => 'silver'
+ );
+
+ my @format;
+ foreach (0, 1) {
+ my %bg = (bg_color => $_ ? 'white' : 'silver');
+ $format[$_] = {
+ 'text' => $workbook->add_format(%bg),
+ 'money' => $workbook->add_format(%bg, num_format => $money_char.'#0.00'),
+ 'percent' => $workbook->add_format(%bg, num_format => '0.00%'),
+ };
+ }
+ my $total_format = $workbook->add_format(
+ bg_color => 'yellow',
+ num_format => $money_char.'#0.00',
+ top => 1
+ );
+
+ my ($r, $c) = (0, 0);
+ foreach (qw(Package Sales Percentage Commission)) {
+ $worksheet->write($r, $c++, $_, $col_head_format);
+ }
+ $r++;
+
+ my ($custnum, $sales, $commission, $row, $bgcolor) = (0, 0, 0, 0);
+ my $label_length = 0;
+ foreach my $cust_pkg ( @cust_pkg ) {
+ if ( $custnum ne $cust_pkg->custnum ) {
+ # start of a new customer section
+ my $cust_main = $cust_pkg->cust_main;
+ my $label = $cust_main->custnum . ': '. $cust_main->name;
+ $bgcolor = 0;
+ $worksheet->set_row($r, 20);
+ $worksheet->merge_range($r, 0, $r, 3, $label, $cust_head_format);
+ $r++;
+ }
+ $c = 0;
+ my $percent = $cust_pkg->percent / 100;
+ $worksheet->write($r, $c++, $cust_pkg->pkg_label, $format[$bgcolor]{text});
+ $worksheet->write($r, $c++, $cust_pkg->sum_charged, $format[$bgcolor]{money});
+ $worksheet->write($r, $c++, $percent, $format[$bgcolor]{percent});
+ $worksheet->write($r, $c++, ($cust_pkg->sum_charged * $percent),
+ $format[$bgcolor]{money});
+
+ $label_length = max($label_length, length($cust_pkg->pkg_label));
+ $sales += $cust_pkg->sum_charged;
+ $commission += $cust_pkg->sum_charged * $cust_pkg->percent / 100;
+ $row++;
+ $bgcolor = 1-$bgcolor;
+ $custnum = $cust_pkg->custnum;
+ $r++;
+ }
+
+ $c = 0;
+ $label_length = max($label_length, 20);
+ $worksheet->set_column($c, $c, $label_length);
+ $worksheet->write($r, $c++, mt('[quant,_1,package] with commission', $row),
+ $total_format);
+ $worksheet->set_column($c, $c + 2, 11);
+ $worksheet->write($r, $c++, $sales, $total_format);
+ $worksheet->write($r, $c++, '', $total_format);
+ $worksheet->write($r, $c++, $commission, $total_format);
+
+ $workbook->close;
+}
</%init>
$search{'refnum'} = $1;
}
- if ( $cgi->param('cust_classnum') ) {
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
$search{'cust_classnum'} = [ $cgi->param('cust_classnum') ];
}
$title = $part_referral->referral. " $title";
}
-if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
- push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, prepaid_income.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
push @where, "cust_main.refnum = $1";
}
-# cust_classnum
-if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
- push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
+
# custnum
if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
push @where, "cust_main.custnum = $1";
push @where, 'cust_main.refnum IN ('.join(',', @refnum).')';
}
-my @cust_classnums = grep /^\d+$/, $cgi->param('cust_classnum');
-if ( @cust_classnums ) {
- push @where, 'cust_main.classnum IN ('.join(',', @cust_classnums).')';
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
+ if @classnums;
}
if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
$title = $part_referral->referral. " $title";
}
-if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
- push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
$title = $part_referral->referral. " $title";
}
-if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
- push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
% my $pkg_rowspan = shift @pkg_rowspans;
<% $n1 %><TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ROWSPAN="<% $pkg_rowspan%>">
- <A HREF="<% $pkgview %>"><FONT SIZE=-1><% $pkg_comment %></FONT></A>
+ <A HREF="<% $pkgview %>"><FONT SIZE=-1><% $pkg_comment |h %></FONT></A>
</TD>
% my $n2 = '';
-<% include( 'elements/cust_pay_or_refund.html',
+<& elements/cust_pay_or_refund.html,
'thing' => 'pay_pending',
'amount_field' => 'paid',
'name_singular' => 'pending payment',
$status_sub,
],
'redirect_empty' => $redirect_empty,
- )
-%>
+&>
<%init>
my %statusaction = (
my $extra_sql = ' WHERE '. join(' AND ', @extra_sql );
$sql_query = {
+ 'select' => 'svcnum',
'table' => 'cust_svc',
'addl_from' => $addl_from,
'hashref' => {},
}
$sql_query->{'select'} = join(', ',
- 'cust_svc.*',
- 'part_svc.*',
+ $sql_query->{'select'},
+ #'part_svc.*',
'cust_main.custnum',
FS::UI::Web::cust_sql_fields(),
);
my $link = sub {
my $cust_svc = shift;
- my $url = svc_url(
- 'm' => $m,
- 'action' => 'view',
- #'part_svc' => $cust_svc->part_svc,
- 'svcdb' => $cust_svc->svcdb, #we have it from the joined search
- #'svc' => $cust_svc, #redundant
- 'query' => '',
- );
+ my $url;
+ if ( $cust_svc->svcpart ) {
+ $url = svc_url(
+ 'm' => $m,
+ 'action' => 'view',
+ 'svcdb' => $cust_svc->svcdb, #we have it from the joined search
+ 'query' => '',
+ );
+ } else { # bizarre unlinked service case
+ $url = $p.'view/svc_Common.html?svcdb='.$cust_svc->svcdb.';svcnum=';
+ }
[ $url, 'svcnum' ];
};
$title .= 'Customer Accounting Summary Report';
-my @cust_classnums = grep /^\d+$/, $cgi->param('cust_classnum');
-
my @items = ('netsales', 'cashflow');
my @params = ( [], [] );
my $setuprecur = '';
}
}
$search_hash{'classnum'} = [ $cgi->param('cust_classnum') ]
- if $cgi->param('cust_classnum');
+ if grep { $_ eq 'cust_classnum' } $cgi->param;
my $query = FS::cust_main::Search->search(\%search_hash);
my @custs = qsearch($query);
Example:
- include( 'elements/cust_main_dayranges.html',
+ <& elements/cust_main_dayranges.html,
'title' => 'Accounts Receivable Aging Summary',
'range_sub' => $mysub,
- )
+ &>
my $mysub = sub {
my( $start, $end ) = @_;
$row->{'rangecol_60_90'} ),
sprintf( $money_char.'%.2f',
$row->{'rangecol_90_0'} ),
- sprintf( '<b>'. $money_char.'%.2f'. '</b>',
+ sprintf( '<b>'.$money_char.'%.2f</b>',
$row->{'rangecol_0_0'} ),
('') x @pay_labels,
],
'', '', '', '', 'b',
( map '', @pay_labels ),
],
+ 'xls_format' => [ (map '', FS::UI::Web::cust_styles),
+ '', '', '', '', { bold => 1 },
+ ],
'color' => [
FS::UI::Web::cust_colors(),
'',
push @where, FS::cust_main->$method();
}
+# cust_classnum (false laziness w/prepaid_income.html, elements/cust_pay_or_refund.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
+ if @classnums;
+}
+
#here is the agent virtualization
push @where, $FS::CurrentUser::CurrentUser->agentnums_sql;
$title = $part_referral->referral. " $title";
}
- if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
- push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+ # cust_classnum (false laziness w/ elements/cust_main_dayranges.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+ if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
my $rows = $args{'rows'};
my %opt = %{ $args{'opt'} };
+my $style = $opt{'style'};
+
my $override = scalar(@$rows) >= 65536 ? 'XLSX' : '';
my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format($override);
bg_color => 55, #22,
bottom => 3,
);
+my $footer_format = $workbook->add_format(
+ italic => 1,
+ locked => 1,
+ bg_color => 55,
+ top => 3,
+);
my $default_format = $workbook->add_format(locked => 0);
my %money_format;
my %date_format;
xl_parse_date_init();
+my %bold_format;
+
my $writer = sub {
# Wrapper for $worksheet->write.
# Do any massaging of the value/format here.
my ($r, $c, $value, $format) = @_;
+ #warn "writer called with format $format\n";
+
+ if ( $style->[$c] eq 'b' or $value =~ /<b>/i ) { # the only one in common use
+ $value =~ s[</?b>][]ig;
+ if ( !exists($bold_format{$format}) ) {
+ $bold_format{$format} = $workbook->add_format();
+ $bold_format{$format}->copy($format);
+ $bold_format{$format}->set_bold();
+ }
+ $format = $bold_format{$format};
+ }
+
# convert HTML entities
# both Spreadsheet::WriteExcel and Excel::Writer::XLSX accept UTF-8 strings
$value = decode_entities($value);
# String: replace line breaks with newlines
$value =~ s/<BR>/\n/gi;
}
+ #warn "writing with format $format\n";
$worksheet->write($r, $c, $value, $format);
};
if ( ref($item) eq 'CODE' ) {
$item = &{$item}();
}
- $writer->( $r, $c++, $item, $header_format );
+ $writer->( $r, $c++, $item, $footer_format );
}
}
my $limit = '';
my($confmax, $maxrecords, $offset );
-unless ( $type =~ /^(csv|\w*.xls)$/) {
+unless ( $type =~ /^(csv|xml|\w*.xls)$/) {
# html mode
unless (exists($opt{count_query}) && length($opt{count_query})) {
( $opt{count_query} = $opt{query} ) =~
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+ unless $FS::CurrentUser::CurrentUser->access_right('Employees: Audit Report');
my %tables = (
cust_pay => 'Payments',
-<% include( 'elements/cust_pay_or_refund.html',
+<& elements/cust_pay_or_refund.html,
'table' => 'h_cust_pay',
'amount_field' => 'paid',
'name_singular' => 'payment',
'name_verb' => 'paid',
'pre_header' => [ 'Transaction', 'By' ],
'pre_fields' => [ 'history_action', 'history_user' ],
- )
-%>
+&>
my $curuser = $FS::CurrentUser::CurrentUser;
die "access denied"
- unless $curuser->access_right('Financial reports');
+ unless $curuser->access_right('Employees: Commission Report'); #that's all this does so far
my $conf = new FS::Conf;
my $money_char = $conf->config('money_char') || '$';
push @where, FS::cust_main->cust_status_sql . " = '$status'";
}
-if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
$link .= ";cust_classnum=$_" foreach @classnums;
- push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
+ push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
<INPUT TYPE="hidden" NAME="magic" VALUE="_date">
<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
-<TABLE BGCOLOR="#cccccc" CELLSPACING=0
+<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
% unless ( $custnum ) {
<& /elements/tr-select-agent.html,
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+ unless $FS::CurrentUser::CurrentUser->access_right('Employees: Audit Report');
my %tables = (
cust_pay => 'Payments',
<%init>
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+ unless $FS::CurrentUser::CurrentUser->access_right('Employees: Commission Report');
</%init>
<& /elements/tr-select-cust_main-status.html,
'label' => emt('Customer Status'),
&>
-
+
+ <& /elements/tr-select-cust_class.html,
+ 'label' => emt('Customer class'),
+ 'field' => 'cust_classnum',
+ 'multiple' => 1,
+ 'pre_options' => [ '' => emt('(none)') ],
+ 'all_selected' => 1,
+ &>
+
<TR>
<TD ALIGN="right"><% mt('Customers') |h %></TD>
<TD>
my $out = 'Out of taxable region(s)';
my %label_opt = ( out => 1 ); #enable 'Out of Taxable Region' label
-$label_opt{no_city} = 1 unless $cgi->param('show_cities');
-$label_opt{no_taxclass} = 1 unless $cgi->param('show_taxclasses');
+$label_opt{with_city} = 1 if $cgi->param('show_cities');
+$label_opt{with_district} = 1 if $cgi->param('show_districts');
+
+$label_opt{with_taxclass} = 1 if $cgi->param('show_taxclasses');
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
my $tot_credit = 0;
my @loc_params = qw(country state county);
-push @loc_params, qw(city district) if $cgi->param('show_cities');
+push @loc_params, 'city' if $cgi->param('show_cities');
+push @loc_params, 'district' if $cgi->param('show_districts');
foreach my $r ( qsearch({ 'table' => 'cust_main_county', })) {
my $taxnum = $r->taxnum;
}
if ( $cgi->param('show_taxclasses') ) {
- my $base_label = $r->label(%label_opt, 'no_taxclass' => 1);
+ my $base_label = $r->label(%label_opt, 'with_taxclass' => 0);
$base_regions{$base_label} ||=
{
label => $base_label,
% if ( $city ) {
<TR>
- <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_cities" VALUE="1"></TD>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_cities" VALUE="1" onclick="toggle_show_cities(this)"></TD>
<TD>Show cities</TD>
</TR>
+ <TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_districts" VALUE="1" DISABLED></TD>
+ <TD>Show districts</TD>
+ </TR>
+ <SCRIPT TYPE="text/javascript">
+ function toggle_show_cities() {
+ what = document.getElementsByName('show_cities')[0];
+ what.form.show_districts.disabled = !what.checked;
+ what.form.show_districts.checked = what.checked;
+ }
+ toggle_show_cities();
+ </SCRIPT>
% }
% if ( $conf->exists('enable_taxclasses') ) {
-<% include( 'elements/cust_main_dayranges.html',
+<& elements/cust_main_dayranges.html,
#'title' => 'Prepaid Balance Aging Summary', #???
'title' => 'Unapplied Payments Aging Summary',
'range_sub' => \&unapplied_payments,
- )
-%>
+&>
<%init>
die "access denied"
push @where, "cust_bill._date >= $beginning",
"cust_bill._date <= $ending";
-if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
- push @where, "cust_main.agentnum = $1";
-}
-
-if ( $cgi->param('cust_classnum') ) {
- my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
- push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+ my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+ push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+ join(',', map { $_ || '0' } @classnums ).
+ ' )'
if @classnums;
}
&> |
% }
-% if ( $curuser->access_right('Merge customer') ) {
+% if ( $curuser->access_right('Merge customer')
+% and ( scalar($cust_main->ncancelled_pkgs)
+% || $conf->exists('deletecustomers')
+% )
+% )
+% {
<& /elements/popup_link-cust_main.html,
{ 'action' => $p. 'misc/merge_cust.html',
'label' => emt('Merge this customer'),
'actionlabel' => emt('Merge customer'),
'cust_main' => $cust_main,
- 'width' => 480,
- 'height' => 192,
+ 'width' => 569,
+ 'height' => 210,
}
&> |
% }
'svc_external' => 'External service',
'svc_phone' => 'Phone',
'phone_device' => 'Phone device',
+ 'cust_pkg_discount' => 'Discount',
#? it gets provisioned anyway 'phone_avail' => 'Phone',
;
-my $svc_join = 'JOIN cust_svc USING ( svcnum ) JOIN cust_pkg USING ( pkgnum )';
+my $pkg_join = "JOIN cust_pkg USING ( pkgnum )";
+my $svc_join = "JOIN cust_svc USING ( svcnum ) $pkg_join";
my %table_join = (
'svc_acct' => $svc_join,
'svc_external' => $svc_join,
'svc_phone' => $svc_join,
'phone_device' => $svc_join,
+ 'cust_pkg_discount'=> $pkg_join,
);
my $curuser = $FS::CurrentUser::CurrentUser;
-die "access deined"
+die "access denied"
unless $curuser->access_right('View customer history');
# find out the beginning of this customer history, if possible
% }
</SPAN></TH></TR>
% if (@$packages) {
-<& packages/section.html, 'packages' => $packages &>
+<& packages/section.html, 'packages' => $packages, 'cust_main' => $cust_main &>
% }
</TABLE><BR>
% } #foreach $locationnum
%
% my $edit = '';
% if ($curuser->access_right('Edit customer note') ) {
-% $edit = qq! <A HREF="javascript:void(0);" $clickjs>(!.emt('edit').')</A>';
+% my $delete_url = $fsurl.'misc/delete-note.html?'.$notenum;
+% $edit = qq! <A HREF="javascript:void(0);" $clickjs>(!.emt('edit').')</A>'.
+% qq! <A HREF="$delete_url" !.
+% qq! onclick="return confirm('Delete this note?')">!.
+% '('.emt('delete').')</A>';
% }
%
% if ( $last_classnum != $note->classnum && !$skipheader ) {
<TR>
<TD COLSPAN=2>
-% if ( $conf->exists('cust_pkg-group_by_location') and $show_location ) {
+% if ( $conf->exists('cust_pkg-group_by_location') ) {
<& locations.html,
'cust_main' => $cust_main,
'packages' => $packages,
<& packages/section.html,
'cust_main' => $cust_main,
'packages' => $packages,
- 'show_location' => $show_location,
&>
</TABLE>
% }
my( $packages, $num_old_packages ) = get_packages($cust_main, $conf);
-
-my $show_location = $conf->exists('cust_pkg-always_show_location')
- || (grep $_->locationnum ne $cust_main->ship_locationnum, @$packages);
-
my $countrydefault = scalar($conf->config('countrydefault')) || 'US';
#subroutines
--- /dev/null
+% if ( $contact ) {
+ <% $contact->line |h %>
+% if ( $show_change_link ) {
+ <FONT SIZE=-1>
+ ( <%pkg_change_contact_link($cust_pkg)%> )
+ </FONT>
+% }
+% if ( $show_detach_link ) {
+ <FONT SIZE=-1>
+ ( <%pkg_detach_link($cust_pkg)%> )
+ </FONT>
+% }
+% } elsif ( $show_contact_link ) {
+ <FONT SIZE=-1>
+ ( <%pkg_add_contact_link($cust_pkg)%> )
+ </FONT>
+% }
+<%init>
+
+my $conf = new FS::Conf;
+my %opt = @_;
+
+my $cust_pkg = $opt{'cust_pkg'};
+
+my $show_change_link =
+ ! $cust_pkg->get('cancel')
+ && $FS::CurrentUser::CurrentUser->access_right('Change customer package');
+
+my $show_detach_link =
+ ! $cust_pkg->get('cancel')
+ && $FS::CurrentUser::CurrentUser->access_right('Detach customer package');
+
+my $show_contact_link =
+ ! $cust_pkg->get('cancel')
+ ; #&& $FS::CurrentUser::CurrentUser->access_right('Add package contact'); #or something like that
+
+my $contact = $cust_pkg->contact_obj;
+
+sub pkg_change_contact_link {
+ my $cust_pkg = shift;
+ #my $pkgpart = $cust_pkg->pkgpart;
+ include( '/elements/popup_link-cust_pkg.html',
+ 'action' => $p. "misc/change_pkg_contact.html",
+ 'label' => emt('Change'), # contact'),
+ 'actionlabel' => emt('Change'),
+ 'cust_pkg' => $cust_pkg,
+ 'width' => 616,
+ 'height' => 220,
+ );
+}
+
+sub pkg_add_contact_link {
+ my $cust_pkg = shift;
+ #my $pkgpart = $cust_pkg->pkgpart;
+ include( '/elements/popup_link-cust_pkg.html',
+ 'action' => $p. "misc/change_pkg_contact.html",
+ 'label' => emt('Add contact'),
+ 'actionlabel' => emt('Add contact'),
+ 'cust_pkg' => $cust_pkg,
+ 'width' => 616,
+ 'height' => 192,
+ );
+}
+
+sub pkg_detach_link {
+ my $cust_pkg = shift;
+ #my $pkgpart = $cust_pkg->pkgpart;
+ include( '/elements/popup_link-cust_pkg.html',
+ 'action' => $p. "misc/detach_pkg.html",
+ 'label' => emt('Detach'),
+ 'actionlabel' => emt('Detach'),
+ 'cust_pkg' => $cust_pkg,
+ 'width' => 616,
+ 'height' => 684,
+ );
+}
+
+#sub edit_contact_link {
+# my $contactnum = shift;
+# include( '/elements/popup_link.html',
+# 'action' => $p. "edit/cust_contact.cgi?contactnum=$contactnum",
+# 'label' => emt('Edit contact'),
+# 'actionlabel' => emt('Edit'),
+# );
+#}
+
+</%init>
-<TD CLASS="inv" BGCOLOR="<% $bgcolor %>" WIDTH="20%">
-
-% unless ( $cust_pkg->locationnum ) {
- <I><FONT SIZE=-1>(<% mt('default service address') |h %>)</FONT><BR>
+% if ( $default ) {
+ <DIV STYLE="font-style: italic; font-size: small">
% }
<% $loc->location_label( 'join_string' => '<BR>',
</FONT>
% }
-% unless ( $cust_pkg->locationnum ) {
- </I>
+% if ( $default ) {
+ </DIV>
% }
% if ( ! $cust_pkg->get('cancel')
</FONT>
% }
-</TD>
<%init>
my $conf = new FS::Conf;
my %opt = @_;
-my $bgcolor = $opt{'bgcolor'};
my $cust_pkg = $opt{'cust_pkg'};
my $countrydefault = $opt{'countrydefault'} || 'US';
my $statedefault = $opt{'statedefault'}
|| ($countrydefault eq 'US' ? 'CA' : '');
my $loc = $cust_pkg->cust_location_or_main;
+# dubious--they should all have a location now
+my $default = $cust_pkg->locationnum == $opt{'cust_main'}->ship_locationnum;
sub pkg_change_location_link {
my $cust_pkg = shift;
% !$supplemental and
% $part_pkg->freq ne '0' ) {
<TR>
-% if ( !$opt{'show_location'} ) {
- <TD><FONT SIZE="-1">
- ( <% pkg_change_location_link($cust_pkg) %> )
- </FONT></TD>
-% }
% if ( FS::Conf->new->exists('invoice-unitprice') ) {
<TD><FONT SIZE="-1">
( <% pkg_change_quantity_link($cust_pkg) %> )
% #my $width = $show_location ? 'WIDTH="25%"' : 'WIDTH="33%"';
<TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Package') |h %></TH>
<TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Status') |h %></TH>
-% if ( $show_location ) {
- <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Location') |h %></TH>
-% }
+ <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Contact/Location') |h %></TH>
<TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Services') |h %></TH>
</TR>
% foreach my $cust_pkg (@$packages) {
<& .packagerow, $cust_pkg,
'cust_main' => $opt{'cust_main'},
+ 'bgcolor' => $opt{'bgcolor'},
%conf_opt
&>
% }
<!--pkgnum: <% $cust_pkg->pkgnum %>-->
<TR CLASS="row<%$row % 2%>">
<& package.html, %iopt &>
- <& status.html, %iopt &>
-% if ( $iopt{'show_location'} ) {
- <& location.html, %iopt &>
-% }
+ <& status.html, %iopt &>
+ <TD CLASS="inv" BGCOLOR="<% $iopt{bgcolor} %>" WIDTH="20%" VALIGN="top">
+ <& contact.html, %iopt &><BR>
+ <& location.html, %iopt &>
+ </TD>
<& services.html, %iopt &>
</TR>
% $row++;
my $curuser = $FS::CurrentUser::CurrentUser;
my $packages = $opt{'packages'};
-my $show_location = $opt{'show_location'};
# Sort order is hardcoded for now, can change this if needed.
@$packages = sort {
( $a->getfield('pkgnum') <=> $b->getfield('pkgnum') )
} @$packages;
+my %change_custnum = map { $_->change_custnum => 1 }
+ grep { $_->change_custnum }
+ grep { $_->getfield('cancel') }
+ @$packages;
+
+my $pkg_attached = ( scalar(keys %change_custnum) == 1
+ && ! grep { ! $_->getfield('cancel') } @$packages
+ );
+
my $countrydefault = scalar($conf->config('countrydefault')) || 'US';
my %conf_opt = (
|| $curuser->option('cust_pkg-display_times')),
#for status.html
'cust_pkg-show_autosuspend' => $conf->exists('cust_pkg-show_autosuspend'),
+ 'pkg_attached' => $pkg_attached,
#for status.html pkg-balances
'pkg-balances' => $conf->exists('pkg-balances'),
'money_char' => ( $conf->config('money_char') || '$' ),
'manage_link_loc' => scalar($conf->config('svc_broadband-manage_link_loc')),
'manage_link-new_window' => $conf->exists('svc_broadband-manage_link-new_window'),
'maestro-status_test' => $conf->exists('maestro-status_test'),
- 'cust_pkg-large_pkg_size' => $conf->config('cust_pkg-large_pkg_size'),
+ 'cust_pkg-large_pkg_size' => scalar($conf->config('cust_pkg-large_pkg_size')),
- # for packages.html Change location link
- 'show_location' => $show_location,
);
</%init>
-<TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+<TD CLASS="inv" BGCOLOR="<% $bgcolor %>" VALIGN="top">
<TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
%#this should use cust_pkg->status and cust_pkg->statuscolor eventually
<% pkg_status_row($cust_pkg, emt('Cancelled'), 'cancel', 'color'=>'FF0000', %opt ) %>
+ <% pkg_status_row_detached($cust_pkg, %opt) %>
+
<% pkg_reason_row($cust_pkg, $cpr, color => 'ff0000', %opt) %>
% unless ( $cust_pkg->get('setup') ) {
% }
%
-% if ( $part_pkg->freq and !$supplemental ) { #?
+% if ( $part_pkg->freq && !$supplemental && !$cust_pkg->change_custnum ) { #?
<TR>
<TD COLSPAN=<%$opt{colspan}%>>
$html;
}
+sub pkg_status_row_detached {
+ my( $cust_pkg, %opt ) = @_;
+
+ return '' unless $cust_pkg->change_custnum;
+
+ my $html = '';
+
+ my $cust_main = $cust_pkg->change_cust_main;
+ if ( $cust_main ) {
+
+ my $cust_link = '<A HREF="cust_main.cgi?'. $cust_pkg->change_custnum. '">'.
+ encode_entities( $cust_main->name ).
+ '</A>';
+
+ my $what = $opt{'pkg_attached'} ? 'Attached' : 'Detached';
+
+ $html .= pkg_status_row_colspan( $cust_pkg,
+ emt("$what to customer #[_1]: ",
+ $cust_pkg->change_custnum
+ ).
+ $cust_link,
+ '',
+ 'size' => '-1',
+ 'align' => 'right',
+ 'colspan' => 4,
+ );
+ }
+
+ $html;
+}
+
sub pkg_status_row_noauto {
my( $cust_pkg, %opt ) = @_;
my $part_pkg = $opt{'part_pkg'};
%#display payment history
-%my $money_char = $conf->config('money_char') || '$';
-%
-%sub balance_forward_row {
-% my( $b, $date, $money_char ) = @_;
-% ( my $balance_forward = $money_char. $b ) =~ s/^\$\-/- \$/;
-
- <TR ID="balance_forward_row">
- <TD CLASS="grid" BGCOLOR="#dddddd">
- <% time2str($date_format, $date) %>
- </TD>
-
- <TD CLASS="grid" BGCOLOR="#dddddd">
- <I><% mt("Starting balance on [_1]", time2str($date_format, $date) ) |h %></I>
- (<A HREF="javascript:void(0);" onClick="show_history();"><% mt('show prior history') |h %></A>)
- </TD>
-
- <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
- <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
- <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
- <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
- <TD CLASS="grid" BGCOLOR="#dddddd" ALIGN="right"><I><% $balance_forward %></I></TD>
-
- </TR>
-%}
-%
-%my $balance = 0;
%my %target = ();
%
-%my $years = $conf->config('payment_history-years') || 2;
-%my $older_than = time - $years * 31556926; #60*60*24*365.2422
%my $hidden = 0;
%my $seen = 0;
%my $old_history = 0;
%
% $lastdate = $item->{'date'};
%
-% my $display;
-% if ( $item->{'date'} < $older_than ) {
+% my $display = '';
+% if ( $item->{'hide'} ) {
% $display = ' STYLE="display:none" ';
-% $hidden = 1;
-% } else {
-%
-% $display = '';
-%
-% if ( $hidden && ! $seen++ ) {
-% balance_forward_row($balance, $item->{'date'}, $money_char);
-% }
-%
% }
%
% if ( $bgcolor eq $bgcolor1 ) {
%
% my $target = exists($item->{'target'}) ? $item->{'target'} : '';
%
-% $balance += $item->{'charge'} if exists $item->{'charge'};
-% $balance -= $item->{'payment'} if exists $item->{'payment'};
-% $balance -= $item->{'credit'} if exists $item->{'credit'};
-% $balance += $item->{'refund'} if exists $item->{'refund'};
-% $balance = sprintf("%.2f", $balance);
-% $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
-% ( my $showbalance = $money_char. $balance ) =~ s/^\$\-/- \$/;
-%
-%
-
+% my $showbalance = $money_char . $item->{'balance'};
+% $showbalance =~ s/^\$\-/- \$/;
<TR <% $display ? $display.' ID="old_history'.$old_history++.'"' : ''%>>
<TD VALIGN="top" CLASS="grid" BGCOLOR="<% $bgcolor %>">
<% $showbalance %>
</TD>
</TR>
-% }
-%if ( scalar(@history) && $hidden && ! $seen++ ) {
-% balance_forward_row($balance, $lastdate, $money_char);
-%}
+% if ( $item->{'balance_forward'} ) {
+<& .balance_forward_row, $item->{'balance'}, $item->{'date'} &>
+% }
+%} # foreach $item
</TABLE>
</TD>
}
</SCRIPT>
+<%def .balance_forward_row>
+% my( $b, $date ) = @_;
+% ( my $balance_forward = $money_char. $b ) =~ s/^\$\-/- \$/;
-<%init>
+ <TR ID="balance_forward_row">
+ <TD CLASS="grid" BGCOLOR="#dddddd">
+ <% time2str($date_format, $date) %>
+ </TD>
-my( $cust_main ) = @_;
-my $custnum = $cust_main->custnum;
+ <TD CLASS="grid" BGCOLOR="#dddddd">
+ <I><% mt("Starting balance on [_1]", time2str($date_format, $date) ) |h %></I>
+ (<A HREF="javascript:void(0);" onClick="show_history();"><% mt('show prior history') |h %></A>)
+ </TD>
+
+ <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+ <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+ <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+ <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+ <TD CLASS="grid" BGCOLOR="#dddddd" ALIGN="right"><I><% $balance_forward %></I></TD>
+ </TR>
+</%def>
+<%shared>
my $conf = new FS::Conf;
my $date_format = $conf->config('date_format') || '%m/%d/%Y';
+my $money_char = $conf->config('money_char') || '$';
+</%shared>
+<%init>
+
+my( $cust_main ) = @_;
+my $custnum = $cust_main->custnum;
my $curuser = $FS::CurrentUser::CurrentUser;
}
-# sort history
+# sort in forward order first, and calculate running balances
+my $years = $conf->config('payment_history-years') || 2;
+my $older_than = time - $years * 31556926; #60*60*24*365.2422
+my $balance = 0;
+
+@history = sort { $a->{date} <=> $b->{date} } @history;
+my $i = 0;
+my $balance_forward;
+foreach my $item (@history) {
+ $balance += $item->{'charge'} if exists $item->{'charge'};
+ $balance -= $item->{'payment'} if exists $item->{'payment'};
+ $balance -= $item->{'credit'} if exists $item->{'credit'};
+ $balance += $item->{'refund'} if exists $item->{'refund'};
+ $balance = sprintf("%.2f", $balance);
+ $balance =~ s/^\-0\.00$/0.00/;
+ $item->{'balance'} = $balance;
+
+ if ( $item->{'date'} < $older_than ) {
+ $item->{'hide'} = 1;
+ } elsif ( $history[$i-1]->{'hide'} ) {
+ # this is the end of the hidden section
+ $history[$i-1]->{'balance_forward'} = 1;
+ }
+ $i++;
+}
+if ( @history and $history[-1]->{'hide'} ) {
+ # then everything is hidden
+ $history[-1]->{'balance_forward'} = 1;
+}
+
+# then sort in user-pref order
if ( $curuser->option('history_order') eq 'newest' ) {
@history = sort { $b->{date} <=> $a->{date} } @history;
-} else {
- @history = sort { $a->{date} <=> $b->{date} } @history;
-} # no other sort orders for now
+} # else it's already oldest-first, and there are no other options yet
sub translate_payby {
my ($payby,$payinfo) = (shift,shift);
% }
<% mt('Service #') |h %><B><% $svcnum %></B>
-% my $url = $opt{'edit_url'} || $p. 'edit/'. $opt{'table'}. '.cgi?';
+% if ( $custnum ) {
+% my $url = $opt{'edit_url'} || $p. 'edit/'. $opt{'table'}. '.cgi?';
<& /view/elements/svc_edit_link.html, 'svc' => $svc_x, 'edit_url' => $url &>
+% }
<BR>
<% ntable("#cccccc") %><TR><TD><% ntable("#cccccc",2) %>
% }
+% if ( $cust_svc ) {
<& /elements/table-tickets.html, object => $cust_svc &>
+% }
<% joblisting({'svcnum'=>$svcnum}, 1) %>
my $svcnum;
if ( $cgi->param('svcnum') ) {
- $cgi->param('svcnum') =~ /^(\d+)$/ or die "unparsable svcnum";
+ $cgi->param('svcnum') =~ /^(\d+)$/ or die "unparseable svcnum";
$svcnum = $1;
} else {
my($query) = $cgi->keywords;
}) or die "Unknown svcnum $svcnum in ". $opt{'table'}. " table\n";
my $cust_svc = $svc_x->cust_svc;
-my($label, $value, $svcdb) = $cust_svc->label;
+my ($label, $value, $svcdb, $part_svc );
+my $labels = $opt{labels}; #not -> here
-my $part_svc = $cust_svc->part_svc;
+if ( $cust_svc ) {
+ ($label, $value, $svcdb) = $cust_svc->label;
-#false laziness w/edit/svc_Common.html
-#override default labels with service-definition labels if applicable
-my $labels = $opt{labels}; #not -> here
-foreach my $field ( keys %$labels ) {
- my $col = $part_svc->part_svc_column($field);
- $labels->{$field} = $col->columnlabel if $col->columnlabel !~ /^\s*$/;
+ $part_svc = $cust_svc->part_svc;
+
+ #false laziness w/edit/svc_Common.html
+ #override default labels with service-definition labels if applicable
+ foreach my $field ( keys %$labels ) {
+ my $col = $part_svc->part_svc_column($field);
+ $labels->{$field} = $col->columnlabel if $col->columnlabel !~ /^\s*$/;
+ }
+} else {
+ $label = "Unlinked $table";
+ $value = $svc_x->label;
+ $svcdb = $table;
+ # just to satisfy callbacks
+ $part_svc = FS::part_svc->new({ svcpart => 0, svcdb => $table });
}
-my $pkgnum = $cust_svc->pkgnum;
+my $pkgnum = $cust_svc->pkgnum if $cust_svc;
my($cust_pkg, $custnum);
if ($pkgnum) {
# false laziness w/edit/svc_Common.html
-$cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unparsable svcdb";
+$cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unparseable svcdb";
my $table = $1;
require "FS/$table.pm";
% }
+
<& svc_acct/radius_usage.html,
'svc_acct' => $svc_acct,
'part_svc' => $part_svc,
%gopt,
&>
+
<& svc_acct/change_svc_form.html,
'part_svc' => \@part_svc,
'svcnum' => $svcnum,
%gopt,
&>
+</FORM>
+
+
<& svc_acct/basics.html,
'svc_acct' => $svc_acct,
'part_svc' => $part_svc,
#'longitude',
{ field => 'coordinates', value_callback => \&coordinates },
'altitude',
+
+ 'radio_serialnum',
+ 'radio_location',
+ 'poe_location',
+ 'rssi',
+ 'suid',
+ { field => 'shared_svcnum', value_callback=> \&shared_svcnum, }, #value_callback =>
+
'vlan_profile',
'authkey',
'plan_id',
);
}
+sub shared_svcnum {
+ my $svc_broadband = shift;
+ return '' unless $svc_broadband->shared_svcnum;
+
+ my $shared_svc_broadband =
+ qsearchs('svc_broadband', { 'svcnum' => $svc_broadband->shared_svcnum,
+ }
+ #agent virt?
+ )
+ or return '';
+ my $shared_cust_pkg = $shared_svc_broadband->cust_svc->cust_pkg;
+
+ $shared_svc_broadband->label.
+ ( $shared_cust_pkg
+ ? ' ('. $shared_cust_pkg->cust_main->name. ')'
+ : ''
+ );
+}
+
sub svc_callback {
# trying to move to the callback style
my ($cgi, $svc_x, $part_svc, $cust_pkg, $fields, $opt) = @_;
+
+ if ( $part_svc->part_svc_column('latitude')->columnflag eq 'F'
+ && $part_svc->part_svc_column('longitude')->columnflag eq 'F'
+ )
+ {
+ @$fields = grep { !ref($_) || $_->{field} ne 'coordinates' } @$fields;
+ }
+
# again, we assume at most one of these exports per part_svc
my ($nas_export) = $part_svc->part_export('broadband_nas');
if ( $nas_export ) {