diff options
400 files changed, 16188 insertions, 5014 deletions
@@ -3,7 +3,7 @@ package FS; use strict; use vars qw($VERSION); -$VERSION = '3.0git'; +$VERSION = '3.1git'; #find missing entries in this file with: # for a in `ls *pm | cut -d. -f1`; do grep 'L<FS::'$a'>' ../FS.pm >/dev/null || echo "missing $a" ; done @@ -231,6 +231,8 @@ L<FS::pkg_class> - Package class class 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 diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 66624e179..bfb39b4ad 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -162,6 +162,7 @@ tie my %rights, 'Tie::IxHash', 'Recharge customer service', #NEW 'Unprovision customer service', 'Change customer service', #NEWNEW + 'Edit password', 'Edit usage', #NEW 'Edit home dir', #NEW 'Edit www config', #NEW @@ -182,6 +183,7 @@ tie my %rights, 'Tie::IxHash', 'Unvoid invoices', 'Delete invoices', 'View customer tax exemptions', #yow + 'Edit customer tax exemptions', #NEWNEW 'Add customer tax adjustment', #new, but no need to phase in 'View customer batched payments', #NEW 'View customer pending payments', #NEW @@ -212,6 +214,7 @@ tie my %rights, 'Tie::IxHash', ### 'Customer credit and refund rights' => [ 'Post credit', + 'Credit line items', #NEWNEWNEW 'Apply credit', #NEWNEW { rightname=>'Unapply credit', desc=>'Enable "unapplication" of unclosed credits.' }, #aka unapplycredits { rightname=>'Delete credit', desc=>'Enable deletion of unclosed credits. Be very careful! Only delete credits that were data-entry errors, not adjustments.' }, #aka. deletecredits Optionally specify one or more comma-separated email addresses to be notified when a credit is deleted. @@ -293,6 +296,7 @@ tie my %rights, 'Tie::IxHash', 'Services: Hardware', 'Services: Hardware: Advanced search', 'Services: Phone numbers', + 'Services: Phone numbers: Advanced search', 'Services: PBXs', 'Services: Ports', 'Services: Mailing lists', @@ -301,6 +305,8 @@ tie my %rights, 'Tie::IxHash', 'Usage: Call Detail Records (CDRs)', 'Usage: Unrateable CDRs', 'Usage: Time worked', + { rightname=>'Employees: Commission Report', global=>1 }, + { rightname=>'Employees: Audit Report', global=>1 }, #{ rightname => 'List customers of all agents', global=>1 }, ], @@ -339,6 +345,8 @@ tie my %rights, 'Tie::IxHash', 'Edit package definitions', { rightname=>'Edit global package definitions', global=>1 }, + 'Bulk edit package definitions', + 'Edit billing events', { rightname=>'Edit global billing events', global=>1 }, diff --git a/FS/FS/ClientAPI/Bulk.pm b/FS/FS/ClientAPI/Bulk.pm deleted file mode 100644 index ec617df76..000000000 --- a/FS/FS/ClientAPI/Bulk.pm +++ /dev/null @@ -1,384 +0,0 @@ -package FS::ClientAPI::Bulk; - -use strict; - -use vars qw( $DEBUG $cache ); -use Date::Parse; -use FS::Record qw( qsearchs ); -use FS::Conf; -use FS::ClientAPI_SessionCache; -use FS::cust_main; -use FS::cust_pkg; -use FS::cust_svc; -use FS::svc_acct; -use FS::svc_external; -use FS::cust_recon; -use Data::Dumper; - -$DEBUG = 1; - -sub _cache { - $cache ||= new FS::ClientAPI_SessionCache ( { - 'namespace' => 'FS::ClientAPI::Agent', #yes, share session_ids - } ); -} - -sub _izoom_ftp_row_fixup { - my $hash = shift; - - my @addr_fields = qw( address1 address2 city state zip ); - my @fields = ( qw( agent_custid username _password first last ), - @addr_fields, - map { "ship_$_" } @addr_fields ); - - $hash->{$_} =~ s/[&\/\*'"]/_/g foreach @fields; - - #$hash->{action} = '' if $hash->{action} eq 'R'; #unsupported for ftp - - $hash->{refnum} = 1; #ahem - $hash->{country} = 'US'; - $hash->{ship_country} = 'US'; - $hash->{payby} = 'LECB'; - $hash->{payinfo} = $hash->{daytime}; - $hash->{ship_fax} = '' if ( !$hash->{sms} || $hash->{sms} eq 'F' ); - - my $has_ship = - grep { $hash->{"ship_$_"} && - (! $hash->{$_} || $hash->{"ship_$_"} ne $hash->{$_} ) - } - ( @addr_fields, 'fax' ); - - if ( $has_ship ) { - foreach ( @addr_fields, qw( first last ) ) { - $hash->{"ship_$_"} = $hash->{$_} unless $hash->{"ship_$_"}; - } - } - - delete $hash->{sms}; - - ''; - -}; - -sub _izoom_ftp_result { - my ($hash, $error) = @_; - my $cust_main = - qsearchs( 'cust_main', { 'agent_custid' => $hash->{agent_custid}, - 'agentnum' => $hash->{agentnum} - } - ); - - my $custnum = $cust_main ? $cust_main->custnum : ''; - my @response = ( $hash->{action}, $hash->{agent_custid}, $custnum ); - - if ( $error ) { - push @response, ( 'ERROR', $error ); - } else { - push @response, ( 'OK', 'OK' ); - } - - join( ',', @response ); - -} - -sub _izoom_ftp_badaction { - "Invalid action: $_[0] record: @_ "; -} - -sub _izoom_soap_row_fixup { _izoom_ftp_row_fixup(@_) }; - -sub _izoom_soap_result { - my ($hash, $error) = @_; - - if ( $hash->{action} eq 'R' ) { - if ( $error ) { - return "Please check errors:\n $error"; # odd extra space - } else { - return join(' ', "Everything ok.", $hash->{pkg}, $hash->{adjourn} ); - } - } - - my $pkg = $hash->{pkg} || $hash->{saved_pkg} || ''; - if ( $error ) { - return join(' ', $hash->{agent_custid}, $error ); - } else { - return join(' ', $hash->{agent_custid}, $pkg, $hash->{adjourn} ); - } - -} - -sub _izoom_soap_badaction { - "Unknown action '$_[13]' "; -} - -my %format = ( - 'izoom-ftp' => { - 'fields' => [ qw ( action agent_custid username _password - daytime ship_fax sms first last - address1 address2 city state zip - pkg adjourn ship_address1 ship_address2 - ship_city ship_state ship_zip ) ], - 'fixup' => sub { _izoom_ftp_row_fixup(@_) }, - 'result' => sub { _izoom_ftp_result(@_) }, - 'action' => sub { _izoom_ftp_badaction(@_) }, - }, - 'izoom-soap' => { - 'fields' => [ qw ( agent_custid username _password - daytime first last address1 address2 - city state zip pkg action adjourn - ship_fax sms ship_address1 ship_address2 - ship_city ship_state ship_zip ) ], - 'fixup' => sub { _izoom_soap_row_fixup(@_) }, - 'result' => sub { _izoom_soap_result(@_) }, - 'action' => sub { _izoom_soap_badaction(@_) }, - }, -); - -sub processrow { - my $p = shift; - - my $session = _cache->get($p->{'session_id'}) - or return { 'error' => "Can't resume session" }; #better error message - - my $conf = new FS::Conf; - my $format = $conf->config('selfservice-bulk_format', $session->{agentnum}) - || 'izoom-soap'; - my ( @row ) = @{ $p->{row} }; - - warn "processrow called with '". join("' '", @row). "'\n" if $DEBUG; - - return { 'error' => "unknown format: $format" } - unless exists $format{$format}; - - return { 'error' => "Invalid record record length: ". scalar(@row). - "record: @row " #sic - } - unless scalar(@row) == scalar(@{$format{$format}{fields}}); - - my %hash = ( 'agentnum' => $session->{agentnum} ); - my $error; - - foreach my $field ( @{ $format{ $format }{ fields } } ) { - $hash{$field} = shift @row; - } - - $error ||= &{ $format{ $format }{ fixup } }( \%hash ); - - # put in the fixup routine? - if ( 'R' eq $hash{action} ) { - warn "processing reconciliation\n" if $DEBUG; - $error ||= process_recon($hash{agentnum}, $hash{agent_custid}); - } elsif ( 'P' eq $hash{action} ) { - # do nothing - } elsif( 'D' eq $hash{action} ) { - $hash{promo_pkg} = 'disk-1-'. $session->{agent}; - } elsif ( 'S' eq $hash{action} ) { - $hash{promo_pkg} = 'disk-2-'. $session->{agent}; - $hash{saved_pkg} = $hash{pkg}; - $hash{pkg} = ''; - } else { - $error ||= &{ $format{ $format }{ action } }( @row ); - } - - warn "processing provision\n" if ($DEBUG && !$error && $hash{action} ne 'R'); - $error ||= provision( %hash ) unless $hash{action} eq 'R'; - - my $result = &{ $format{ $format }{ result } }( \%hash, $error ); - - warn "processrow returning '". join("' '", $result, $error). "'\n" - if $DEBUG; - - return { 'error' => $error, 'message' => $result }; - -} - -sub provision { - my %args = ( @_ ); - - delete $args{action}; - - my $cust_main = - qsearchs( 'cust_main', - { map { $_ => $args{$_} } qw ( agent_custid agentnum ) }, - ); - - unless ( $cust_main ) { - $cust_main = new FS::cust_main { %args }; - my $error = $cust_main->insert; - return $error if $error; - } - - my @pkgs = grep { $_->part_pkg->freq } $cust_main->ncancelled_pkgs; - if ( scalar(@pkgs) > 1 ) { - return "Invalid account, should not be more then one active package ". #sic - "but found: ". scalar(@pkgs). " packages."; - } - - my $part_pkg = qsearchs( 'part_pkg', { 'pkg' => $args{pkg} } ) - or return "Unknown pkgpart: $args{pkg}" - if $args{pkg}; - - - my $create_package = $args{pkg}; - if ( scalar(@pkgs) && $create_package ) { - my $pkg = pop(@pkgs); - - if ( $part_pkg->pkgpart != $pkg->pkgpart ) { - my @cust_bill_pkg = $pkg->cust_bill_pkg(); - if ( 1 == scalar(@cust_bill_pkg) ) { - my $cbp= pop(@cust_bill_pkg); - my $cust_bill = $cbp->cust_bill; - $cust_bill->delete(); #really? wouldn't a credit be better? - } - $pkg->cancel(); - } else { - $create_package = ''; - $pkg->setfield('adjourn', str2time($args{adjourn})); - my $error = $pkg->replace(); - return $error if $error; - } - } - - if ( $create_package ) { - my $cust_pkg = new FS::cust_pkg ( { - 'pkgpart' => $part_pkg->pkgpart, - 'adjourn' => str2time( $args{adjourn} ), - } ); - - my $svcpart = $part_pkg->svcpart('svc_acct'); - - my $svc_acct = new FS::svc_acct ( { - 'svcpart' => $svcpart, - 'username' => $args{username}, - '_password' => $args{_password}, - } ); - - my $error = $cust_main->order_pkg( cust_pkg => $cust_pkg, - svcs => [ $svc_acct ], - ); - return $error if $error; - } - - if ( $args{promo_pkg} ) { - my $part_pkg = - qsearchs( 'part_pkg', { 'promo_code' => $args{promo_pkg} } ) - or return "unknown pkgpart: $args{promo_pkg}"; - - my $svcpart = $part_pkg->svcpart('svc_external') - or return "unknown svcpart: svc_external"; - - my $cust_pkg = new FS::cust_pkg ( { - 'svcpart' => $svcpart, - 'pkgpart' => $part_pkg->pkgpart, - } ); - - my $svc_ext = new FS::svc_external ( { 'svcpart' => $svcpart } ); - - my $ticket_subject = 'Send setup disk to customer '. $cust_main->custnum; - my $error = $cust_main->order_pkg ( cust_pkg => $cust_pkg, - svcs => [ $svc_ext ], - noexport => 1, - ticket_subject => $ticket_subject, - ticket_queue => "disk-$args{agentnum}", - ); - return $error if $error; - } - - my $error = $cust_main->bill(); - return $error if $error; -} - -sub process_recon { - my ( $agentnum, $id ) = @_; - my @recs = split /;/, $id; - my $err = ''; - foreach my $rec ( @recs ) { - my @record = split /,/, $rec; - my $result = process_recon_record(@record, $agentnum); - $err .= "$result\n" if $result; - } - return $err; -} - -sub process_recon_record { - my ( $agent_custid, $username, $_password, $daytime, $first, $last, $address1, $address2, $city, $state, $zip, $pkg, $adjourn, $agentnum) = @_; - - warn "process_recon_record called with '". join("','", @_). "'\n" if $DEBUG; - - my ($cust_pkg, $package); - - my $cust_main = - qsearchs( 'cust_main', - { 'agent_custid' => $agent_custid, 'agentnum' => $agentnum }, - ); - - my $comments = ''; - if ( $cust_main ) { - my @cust_pkg = grep { $_->part_pkg->freq } $cust_main->ncancelled_pkgs; - if ( scalar(@cust_pkg) == 1) { - $cust_pkg = pop(@cust_pkg); - $package = $cust_pkg->part_pkg->pkg; - $comments = "$agent_custid wrong package, expected: $pkg found: $package" - if ( $pkg ne $package ); - } else { - $comments = "invalid account, should be one active package but found: ". - scalar(@cust_pkg). " packages."; - } - } else { - $comments = - "Customer not found agent_custid=$agent_custid, agentnum=$agentnum"; - } - - my $cust_recon = new FS::cust_recon( { - 'recondate' => time, - 'agentnum' => $agentnum, - 'first' => $first, - 'last' => $last, - 'address1' => $address1, - 'address2' => $address2, - 'city' => $city, - 'state' => $state, - 'zip' => $zip, - 'custnum' => $cust_main ? $cust_main->custnum : '', #really? - 'status' => $cust_main ? $cust_main->status : '', - 'pkg' => $package, - 'adjourn' => $cust_pkg ? $cust_pkg->adjourn : '', - 'agent_custid' => $agent_custid, # redundant? - 'agent_pkg' => $pkg, - 'agent_adjourn' => str2time($adjourn), - 'comments' => $comments, - } ); - - warn Dumper($cust_recon) if $DEBUG; - my $error = $cust_recon->insert; - return $error if $error; - - warn "process_recon_record returning $comments\n" if $DEBUG; - - $comments; - -} - -sub check_username { - my $p = shift; - - my $session = _cache->get($p->{'session_id'}) - or return { 'error' => "Can't resume session" }; #better error message - - my $svc_domain = qsearchs( 'svc_domain', { 'domain' => $p->{domain} } ) - or return { 'error' => 'Unknown domain '. $p->{domain} }; - - my $svc_acct = qsearchs( 'svc_acct', { 'username' => $p->{user}, - 'domsvc' => $svc_domain->svcnum, - }, - ); - - return { 'error' => $p->{user}. '@'. $p->{domain}. " alerady in use" } # sic - if $svc_acct; - - return { 'error' => '', - 'message' => $p->{user}. '@'. $p->{domain}. " is free" - }; -} - -1; diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index b02852b59..01e0ebc33 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -45,12 +45,12 @@ use FS::payby; use FS::acct_rt_transaction; use FS::msg_template; -$DEBUG = 0; +$DEBUG = 1; $me = '[FS::ClientAPI::MyAccount]'; use vars qw( @cust_main_editable_fields @location_editable_fields ); @cust_main_editable_fields = qw( - first last daytime night fax mobile + first last company daytime night fax mobile locale payby payinfo payname paystart_month paystart_year payissue payip ss paytype paystate stateid stateid_state @@ -121,6 +121,7 @@ sub skin_info { font title_color title_align title_size menu_bgcolor menu_fontsize ) ), + 'menu_disable' => [ $conf->config('selfservice-menu_disable',$agentnum) ], ( map { $_ => $conf->exists("selfservice-$_", $agentnum ) } qw( menu_skipblanks menu_skipheadings menu_nounderline no_logo ) ), @@ -635,11 +636,12 @@ sub billing_history { push @history, { 'type' => 'Line item', - 'description' => $_->desc. ( $_->sdate && $_->edate - ? ' '. time2str('%d-%b-%Y', $_->sdate). - ' To '. time2str('%d-%b-%Y', $_->edate) - : '' - ), + 'description' => $_->desc( $cust_main->locale ). + ( $_->sdate && $_->edate + ? ' '. time2str('%d-%b-%Y', $_->sdate). + ' To '. time2str('%d-%b-%Y', $_->edate) + : '' + ), 'amount' => sprintf('%.2f', $_->setup + $_->recur ), 'date' => $cust_bill->_date, 'date_pretty' => time2str('%m/%d/%Y', $cust_bill->_date ), @@ -1583,10 +1585,13 @@ sub list_pkgs { my $primary_cust_svc = $_->primary_cust_svc; +{ $_->hash, $_->part_pkg->hash, - pkg_label => $_->pkg_label, + pkg_label => $_->pkg_locale, status => $_->status, part_svc => - [ map $_->hashref, $_->available_part_svc ], + [ map { $_->hashref } + grep { $_->selfservice_access ne 'hidden' } + $_->available_part_svc + ], cust_svc => [ map { my $ref = { $_->hash, label => [ $_->label ], @@ -1600,7 +1605,9 @@ sub list_pkgs { $ref->{svchash}->{svcpart} = $_->part_svc->svcpart if $_->part_svc->svcdb eq 'svc_phone'; # hack $ref; - } $_->cust_svc + } + grep { $_->part_svc->selfservice_access ne 'hidden' } + $_->cust_svc ], primary_cust_svc => $primary_cust_svc @@ -1637,15 +1644,26 @@ sub list_svcs { } my @cust_svc = (); + my @cust_pkg_usage = (); #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) { foreach my $cust_pkg ( $p->{'ncancelled'} ? $cust_main->ncancelled_pkgs : $cust_main->unsuspended_pkgs ) { next if $pkgnum && $cust_pkg->pkgnum != $pkgnum; push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context + push @cust_pkg_usage, $cust_pkg->cust_pkg_usage; } @cust_svc = grep { $_->part_svc->selfservice_access ne 'hidden' } @cust_svc; + my %usage_pools; + foreach (@cust_pkg_usage) { + my $part = $_->part_pkg_usage; + my $tag = $part->description . ($part->shared ? 1 : 0); + my $row = $usage_pools{$tag} + ||= [ $part->description, 0, 0, $part->shared ? 1 : 0 ]; + $row->[1] += $_->minutes; # minutes remaining + $row->[2] += $part->minutes; # minutes total + } if ( $p->{'svcdb'} ) { my $svcdb = ref($p->{'svcdb'}) eq 'HASH' @@ -1681,7 +1699,7 @@ sub list_svcs { 'svcdb' => $svcdb, 'label' => $label, 'value' => $value, - 'pkg_label' => $cust_pkg->pkg_label, + 'pkg_label' => $cust_pkg->pkg_locale, 'pkg_status' => $cust_pkg->status, 'readonly' => ($part_svc->selfservice_access eq 'readonly'), ); @@ -1717,7 +1735,34 @@ sub list_svcs { } else { $hash{'name'} = $cust_main->name; } + } elsif ( $svcdb eq 'svc_phone' ) { + # could potentially show lots of things... + $hash{'outbound'} = 1; + $hash{'inbound'} = 0; + if ( $part_pkg->plan eq 'voip_inbound' ) { + $hash{'outbound'} = 0; + $hash{'inbound'} = 1; + } elsif ( $part_pkg->option('selfservice_inbound_format') + or $conf->config('selfservice-default_inbound_cdr_format') + ) { + $hash{'inbound'} = 1; + } + foreach (qw(inbound outbound)) { + # hmm...we can't filter by status here, because there might + # not be cdr_terminations at all. have to go by date. + # find all since the last bill date. + # XXX cdr types? we are going to need them. + if ( $hash{$_} ) { + my $sum_cdr = $svc_x->sum_cdrs( + 'inbound' => ( $_ eq 'inbound' ? 1 : 0 ), + 'begin' => ($cust_pkg->last_bill || 0), + 'nonzero' => 1, + ); + $hash{$_} = $sum_cdr->hashref; + } + } } + # elsif ( $svcdb eq 'svc_phone' || $svcdb eq 'svc_port' ) { # %hash = ( # %hash, @@ -1728,6 +1773,11 @@ sub list_svcs { } @cust_svc ], + 'usage_pools' => [ + map { $usage_pools{$_} } + sort { $a cmp $b } + keys %usage_pools + ], }; } @@ -1782,8 +1832,14 @@ sub svc_status_hash { } -sub set_svc_status_hash { - my $p = shift; +sub set_svc_status_hash { _svc_method_X(shift, 'export_setstatus') } +sub set_svc_status_listadd { _svc_method_X(shift, 'export_setstatus_listadd') } +sub set_svc_status_listdel { _svc_method_X(shift, 'export_setstatus_listdel') } +sub set_svc_status_vacationadd { _svc_method_X(shift, 'export_setstatus_vacationadd') } +sub set_svc_status_vacationdel { _svc_method_X(shift, 'export_setstatus_vacationdel') } + +sub _svc_method_X { + my( $p, $method ) = @_; my($context, $session, $custnum) = _custoragent_session_custnum($p); return { 'error' => $session } if $context eq 'error'; @@ -1792,16 +1848,15 @@ sub set_svc_status_hash { my $svc_x = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_acct') or return { 'error' => "Service not found" }; - warn "set_svc_status_hash ". join(' / ', map "$_=>".$p->{$_}, keys %$p ) + warn "$method ". join(' / ', map "$_=>".$p->{$_}, keys %$p ) if $DEBUG; - my $error = $svc_x->export_setstatus($p); #$p? returns error? + my $error = $svc_x->$method($p); #$p? returns error? return { 'error' => $error } if $error; return {}; #? { 'error' => '' } } - sub acct_forward_info { my $p = shift; @@ -1985,7 +2040,7 @@ sub _list_cdr_usage { # we have to return the results all at once... my($svc_phone, $begin, $end, %opt) = @_; map [ $_->downstream_csv(%opt, 'keeparray' => 1) ], - $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, ); + $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, %opt ); } sub list_cdr_usage { @@ -2015,18 +2070,21 @@ sub _usage_details { my %callback_opt; my $header = []; if ( $svcdb eq 'svc_phone' ) { - my $format = $cust_pkg->part_pkg->option('output_format') || ''; - $format = '' if $format =~ /^sum_/; - # sensible default if there is no format or it's a summary format - if ( $cust_pkg->part_pkg->plan eq 'voip_inbound' ) { - $format ||= 'source_default'; + my $conf = FS::Conf->new; + my $format = ''; + if ( $p->{inbound} ) { + $format = $cust_pkg->part_pkg->option('selfservice_inbound_format') + || $conf->config('selfservice-default_inbound_cdr_format') + || 'source_default'; $callback_opt{inbound} = 1; + } else { + $format = $cust_pkg->part_pkg->option('selfservice_format') + || $conf->config('selfservice-default_cdr_format') + || 'default'; } - else { - $format ||= 'default'; - } - + $callback_opt{format} = $format; + $callback_opt{use_clid} = 1; $header = [ split(',', FS::cdr::invoice_header($format) ) ]; } @@ -2085,6 +2143,7 @@ sub _usage_details { 'svcnum' => $p->{svcnum}, 'beginning' => $p->{beginning}, 'ending' => $p->{ending}, + 'inbound' => $p->{inbound}, 'previous' => ($previous > $start) ? $previous : $start, 'next' => ($next < $end) ? $next : $end, 'header' => $header, diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm index b7dcdbb64..1dbb20bc7 100644 --- a/FS/FS/ClientAPI/Signup.pm +++ b/FS/FS/ClientAPI/Signup.pm @@ -524,20 +524,13 @@ sub new_customer { my $template_cust = qsearchs('cust_main', { 'custnum' => $template_custnum } ); return { 'error' => 'Configuration error' } unless $template_cust; - #XXX Copy template customer's locations $cust_main = new FS::cust_main ( { 'agentnum' => $agentnum, 'refnum' => $packet->{refnum} || $conf->config('signup_server-default_refnum'), ( map { $_ => $template_cust->$_ } qw( - last first company address1 address2 - city county state zip country - daytime night fax - - ship_last ship_first ship_company ship_address1 ship_address2 - ship_city ship_county ship_state ship_zip ship_country - ship_daytime ship_night ship_fax + last first company daytime night fax ) ), @@ -555,6 +548,9 @@ sub new_customer { } ); + $bill_hash = { $template_cust->bill_location->location_hash }; + $ship_hash = { $template_cust->ship_location->location_hash }; + } else { $cust_main = new FS::cust_main ( { @@ -777,13 +773,15 @@ sub new_customer { # " new customer: $bill_error" # if $bill_error; - $bill_error = $cust_main->realtime_collect( - method => FS::payby->payby2bop( $packet->{payby} ), - depend_jobnum => $placeholder->jobnum, - selfservice => 1, - ); - #warn "$me error collecting from new customer: $bill_error" - # if $bill_error; + unless ( $packet->{payby} eq 'PREPAY' ) { + $bill_error = $cust_main->realtime_collect( + method => FS::payby->payby2bop( $packet->{payby} ), + depend_jobnum => $placeholder->jobnum, + selfservice => 1, + ); + #warn "$me error collecting from new customer: $bill_error" + # if $bill_error; + } if ($bill_error && ref($bill_error) eq 'HASH') { return { 'error' => '_collect', diff --git a/FS/FS/ClientAPI_XMLRPC.pm b/FS/FS/ClientAPI_XMLRPC.pm index 7dd20c652..d720db268 100644 --- a/FS/FS/ClientAPI_XMLRPC.pm +++ b/FS/FS/ClientAPI_XMLRPC.pm @@ -129,6 +129,10 @@ sub ss2clientapi { 'svc_status_html' => 'MyAccount/svc_status_html', 'svc_status_hash' => 'MyAccount/svc_status_hash', 'set_svc_status_hash' => 'MyAccount/set_svc_status_hash', + 'set_svc_status_listadd' => 'MyAccount/set_svc_status_listadd', + 'set_svc_status_listdel' => 'MyAccount/set_svc_status_listdel', + 'set_svc_status_vacationadd'=> 'MyAccount/set_svc_status_vacationadd', + 'set_svc_status_vacationdel'=> 'MyAccount/set_svc_status_vacationdel', 'acct_forward_info' => 'MyAccount/acct_forward_info', 'process_acct_forward' => 'MyAccount/process_acct_forward', 'list_dsl_devices' => 'MyAccount/list_dsl_devices', diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index d11916faf..6a19ff475 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -717,6 +717,18 @@ my %batch_gateway_options = ( }, ); +my @cdr_formats = ( + '' => '', + 'default' => 'Default', + 'source_default' => 'Default with source', + 'accountcode_default' => 'Default plus accountcode', + 'description_default' => 'Default with description field as destination', + 'basic' => 'Basic', + 'simple' => 'Simple', + 'simple2' => 'Simple with source', + 'accountcode_simple' => 'Simple with accountcode', +); + # takes the reason class (C, R, S) as an argument sub reason_type_options { my $reason_class = shift; @@ -985,6 +997,14 @@ sub reason_type_options { }, { + 'key' => 'currency', + 'section' => 'billing', + 'description' => 'Currency', + 'type' => 'select', + 'select_enum' => [ '', qw( USD AUD CAD DKK EUR GBP ILS JPY NZD XAF ) ], + }, + + { 'key' => 'business-batchpayment-test_transaction', 'section' => 'billing', 'description' => 'Turns on the Business::BatchPayment test_mode flag. Note that not all gateway modules support this flag; if yours does not, using the batch gateway will fail.', @@ -1509,8 +1529,18 @@ and customer address. Include units.', 'section' => 'invoicing', 'description' => 'Split invoice into sections and label according to package category when enabled.', 'type' => 'checkbox', + 'per_agent' => 1, }, + #quotations seem broken-ish with sections ATM? + #{ + # 'key' => 'quotation_sections', + # 'section' => 'invoicing', + # 'description' => 'Split quotations into sections and label according to package category when enabled.', + # 'type' => 'checkbox', + # 'per_agent' => 1, + #}, + { 'key' => 'usage_class_as_a_section', 'section' => 'invoicing', @@ -1608,6 +1638,7 @@ and customer address. Include units.', 'section' => 'required', 'description' => 'Print command for paper invoices, for example `lpr -h\'', 'type' => 'text', + 'per_agent' => 1, }, { @@ -2053,7 +2084,7 @@ and customer address. Include units.', 'key' => 'locale', 'section' => 'UI', 'description' => 'Default locale', - 'type' => 'select', + 'type' => 'select-sub', 'options_sub' => sub { map { $_ => FS::Locales->description($_) } FS::Locales->locales; }, @@ -3525,7 +3556,7 @@ and customer address. Include units.', 'section' => 'billing', 'description' => 'Default format for batches.', 'type' => 'select', - 'select_enum' => [ 'csv-td_canada_trust-merchant_pc_batch', + 'select_enum' => [ 'NACHA', 'csv-td_canada_trust-merchant_pc_batch', 'csv-chase_canada-E-xactBatch', 'BoM', 'PAP', 'paymentech', 'ach-spiritone', 'RBC' ] @@ -3587,9 +3618,9 @@ and customer address. Include units.', 'section' => 'billing', 'description' => 'Fixed (unchangeable) format for electronic check batches.', 'type' => 'select', - 'select_enum' => [ 'csv-td_canada_trust-merchant_pc_batch', 'BoM', 'PAP', - 'paymentech', 'ach-spiritone', 'RBC', 'td_eft1464', - 'eft_canada' + 'select_enum' => [ 'NACHA', 'csv-td_canada_trust-merchant_pc_batch', 'BoM', + 'PAP', 'paymentech', 'ach-spiritone', 'RBC', + 'td_eft1464', 'eft_canada' ] }, @@ -3643,13 +3674,6 @@ and customer address. Include units.', }, { - 'key' => 'batch-manual_approval', - 'section' => 'billing', - 'description' => 'Allow manual batch closure, which will approve all payments that do not yet have a status. This is not advised, but is needed for payment processors that provide a report of rejected rather than approved payments.', - 'type' => 'checkbox', - }, - - { 'key' => 'batchconfig-eft_canada', 'section' => 'billing', 'description' => 'Configuration for EFT Canada batching, four lines: 1. SFTP username, 2. SFTP password, 3. Transaction code, 4. Number of days to delay process date.', @@ -3658,6 +3682,34 @@ and customer address. Include units.', }, { + 'key' => 'batchconfig-nacha-destination', + 'section' => 'billing', + 'description' => 'Configuration for NACHA batching, Destination (9 digit transit routing number).', + 'type' => 'text', + }, + + { + 'key' => 'batchconfig-nacha-destination_name', + 'section' => 'billing', + 'description' => 'Configuration for NACHA batching, Destination (Bank Name, up to 23 characters).', + 'type' => 'text', + }, + + { + 'key' => 'batchconfig-nacha-origin', + 'section' => 'billing', + 'description' => 'Configuration for NACHA batching, Origin (your 10-digit company number, IRS tax ID recommended).', + 'type' => 'text', + }, + + { + 'key' => 'batch-manual_approval', + 'section' => 'billing', + 'description' => 'Allow manual batch closure, which will approve all payments that do not yet have a status. This is not advised unless needed for specific payment processors that provide a report of rejected rather than approved payments.', + 'type' => 'checkbox', + }, + + { 'key' => 'batch-spoolagent', 'section' => 'billing', 'description' => 'Store payment batches per-agent.', @@ -3722,20 +3774,6 @@ and customer address. Include units.', }, { - 'key' => 'cust_main-skeleton_tables', - 'section' => '', - 'description' => 'Tables which will have skeleton records inserted into them for each customer. Syntax for specifying tables is unfortunately a tricky perl data structure for now.', - 'type' => 'textarea', - }, - - { - 'key' => 'cust_main-skeleton_custnum', - 'section' => '', - 'description' => 'Customer number specifying the source data to copy into skeleton tables for new customers.', - 'type' => 'text', - }, - - { 'key' => 'cust_main-enable_birthdate', 'section' => 'UI', 'description' => 'Enable tracking of a birth date with each customer record', @@ -3785,6 +3823,13 @@ and customer address. Include units.', 'type' => 'checkbox', }, + { + 'key' => 'fuzzy-fuzziness', + 'section' => 'UI', + 'description' => 'Set the "fuzziness" of fuzzy searching (see the String::Approx manpage for details). Defaults to 10%', + 'type' => 'text', + }, + { 'key' => 'pkg_referral', 'section' => '', 'description' => 'Enable package-specific advertising sources.', @@ -3916,6 +3961,19 @@ and customer address. Include units.', }, { + 'key' => 'cust_bill-line_item-date_style-non_monthly', + 'section' => 'billing', + 'description' => 'If set, override cust_bill-line_item-date_style for non-monthly charges.', + 'type' => 'select', + 'select_hash' => [ '' => 'Default', + 'start_end' => 'STARTDATE-ENDDATE', + 'month_of' => 'Month of MONTHNAME', + 'X_month' => 'DATE_DESC MONTHNAME', + ], + 'per_agent' => 1, + }, + + { 'key' => 'cust_bill-line_item-date_description', 'section' => 'billing', 'description' => 'Text to display for "DATE_DESC" when using cust_bill-line_item-date_style DATE_DESC MONTHNAME.', @@ -3954,7 +4012,7 @@ and customer address. Include units.', 'type' => 'select', 'multiple' => 1, 'select_hash' => [ - 'address1' => 'Billing address', + #'address1' => 'Billing address', ], }, @@ -4128,7 +4186,7 @@ and customer address. Include units.', { 'key' => 'census_year', 'section' => 'UI', - 'description' => 'The year to use in census tract lookups', + 'description' => 'The year to use in census tract lookups. NOTE: you need to select 2012 for Year 2010 Census tract codes. A selection of 2011 or 2010 provides Year 2000 Census tract codes. Use the freeside-censustract-update tool if exisitng customers need to be changed.', 'type' => 'select', 'select_enum' => [ qw( 2012 2011 2010 ) ], }, @@ -4462,6 +4520,31 @@ and customer address. Include units.', }, { + 'key' => 'selfservice-menu_disable', + 'section' => 'self-service', + 'description' => 'Disable the selected menu entries in the self-service menu', + 'type' => 'selectmultiple', + 'select_enum' => [ #false laziness w/myaccount_menu.html + 'Overview', + 'Purchase', + 'Purchase additional package', + 'Recharge my account with a credit card', + 'Recharge my account with a check', + 'Recharge my account with a prepaid card', + 'View my usage', + 'Create a ticket', + 'Setup my services', + 'Change my information', + 'Change billing address', + 'Change service address', + 'Change payment information', + 'Change password(s)', + 'Logout', + ], + 'per_agent' => 1, + }, + + { 'key' => 'selfservice-menu_skipblanks', 'section' => 'self-service', 'description' => 'Skip blank (spacer) entries in the self-service menu', @@ -4547,23 +4630,6 @@ and customer address. Include units.', }, { - 'key' => 'selfservice-bulk_format', - 'section' => 'deprecated', - 'description' => 'Parameter arrangement for selfservice bulk features', - 'type' => 'select', - 'select_enum' => [ '', 'izoom-soap', 'izoom-ftp' ], - 'per_agent' => 1, - }, - - { - 'key' => 'selfservice-bulk_ftp_dir', - 'section' => 'deprecated', - 'description' => 'Enable bulk ftp provisioning in this folder', - 'type' => 'text', - 'per_agent' => 1, - }, - - { 'key' => 'signup-no_company', 'section' => 'self-service', 'description' => "Don't display a field for company name on signup.", @@ -4706,6 +4772,13 @@ and customer address. Include units.', }, { + 'key' => 'cdr-taqua-callerid_rewrite', + 'section' => 'telephony', + 'description' => 'For the Taqua CDR format, pull Caller ID blocking information from secondary CDRs.', + 'type' => 'checkbox', + }, + + { 'key' => 'cdr-asterisk_australia_rewrite', 'section' => 'telephony', 'description' => 'For Asterisk CDRs, assign CDR type numbers based on Australian conventions.', @@ -4713,6 +4786,13 @@ and customer address. Include units.', }, { + 'key' => 'cdr-gsm_tap3-sender', + 'section' => 'telephony', + 'description' => 'GSM TAP3 Sender network (5 letter code)', + 'type' => 'text', + }, + + { 'key' => 'cust_pkg-show_autosuspend', 'section' => 'UI', 'description' => 'Show package auto-suspend dates. Use with caution for now; can slow down customer view for large insallations.', @@ -4772,7 +4852,7 @@ and customer address. Include units.', { 'key' => 'svc_broadband-manage_link', 'section' => 'UI', - 'description' => 'URL for svc_broadband "Manage Device" link. The following substitutions are available: $ip_addr.', + 'description' => 'URL for svc_broadband "Manage Device" link. The following substitutions are available: $ip_addr and $mac_addr.', 'type' => 'text', }, @@ -4871,7 +4951,7 @@ and customer address. Include units.', { 'key' => 'pkg-balances', 'section' => 'billing', - 'description' => 'Enable experimental package balances. Not recommended for general use.', + 'description' => 'Enable per-package balances.', 'type' => 'checkbox', }, @@ -5125,6 +5205,13 @@ and customer address. Include units.', }, { + 'key' => 'invoice_payment_details', + 'section' => 'invoicing', + 'description' => 'When displaying payments on an invoice, show the payment method used, including the check or credit card number. Credit card numbers will be masked.', + 'type' => 'checkbox', + }, + + { 'key' => 'cust_main-status_module', 'section' => 'UI', 'description' => 'Which module to use for customer status display. The "Classic" module (the default) considers accounts with cancelled recurring packages but un-cancelled one-time charges Inactive. The "Recurring" module considers those customers Cancelled. Similarly for customers with suspended recurring packages but one-time charges.', #other differences? @@ -5146,6 +5233,13 @@ and customer address. Include units.', 'type' => 'checkbox', }, + { + 'key' => 'username-exclamation', + 'section' => 'username', + 'description' => 'Allow the exclamation character (!) in usernames.', + 'type' => 'checkbox', + }, + { 'key' => 'ie-compatibility_mode', 'section' => 'UI', @@ -5260,6 +5354,19 @@ and customer address. Include units.', $cdr_type ? $cdr_type->cdrtypename : ''; }, }, + + { + 'key' => 'cdr-minutes_priority', + 'section' => 'telephony', + 'description' => 'Priority rule for assigning included minutes to CDRs.', + 'type' => 'select', + 'select_hash' => [ + '' => 'No specific order', + 'time' => 'Chronological', + 'rate_high' => 'Highest rate first', + 'rate_low' => 'Lowest rate first', + ], + }, { 'key' => 'brand-agent', @@ -5283,6 +5390,22 @@ and customer address. Include units.', }, { + 'key' => 'selfservice-default_cdr_format', + 'section' => 'self-service', + 'description' => 'Format for showing outbound CDRs in self-service. The per-package option overrides this.', + 'type' => 'select', + 'select_hash' => \@cdr_formats, + }, + + { + 'key' => 'selfservice-default_inbound_cdr_format', + 'section' => 'self-service', + 'description' => 'Format for showing inbound CDRs in self-service. The per-package option overrides this. Leave blank to avoid showing these CDRs.', + 'type' => 'select', + 'select_hash' => \@cdr_formats, + }, + + { 'key' => 'logout-timeout', 'section' => 'UI', 'description' => 'If set, automatically log users out of the backoffice after this many minutes.', @@ -5307,6 +5430,13 @@ and customer address. Include units.', 'type' => 'text', }, + { + 'key' => 'report-cust_pay-select_time', + 'section' => 'UI', + 'description' => 'Enable time selection on payment and refund reports.', + 'type' => 'checkbox', + }, + { key => "apacheroot", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" }, { key => "apachemachine", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" }, { key => "apachemachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" }, diff --git a/FS/FS/Cron/bill.pm b/FS/FS/Cron/bill.pm index 6e110e852..98ce8fa73 100644 --- a/FS/FS/Cron/bill.pm +++ b/FS/FS/Cron/bill.pm @@ -201,7 +201,8 @@ sub bill_where { # generate where_pkg/where_event search clause ### - my $billtime = day_end($time); + my $conf = new FS::Conf; + my $billtime = $conf->exists('next-bill-ignore-time') ? day_end($time) : $time; # select * from cust_main where my $where_pkg = <<"END"; diff --git a/FS/FS/Cron/upload.pm b/FS/FS/Cron/upload.pm index 628c6801b..03ed366e2 100644 --- a/FS/FS/Cron/upload.pm +++ b/FS/FS/Cron/upload.pm @@ -470,7 +470,7 @@ sub spool_upload { } -=item send_report CONFIG PARAMS +=item prepare_report CONFIG PARAMS Retrieves the config value named CONFIG, parses it as a Text::Template, extracts "to" and "subject" headers, and returns a hash that can be passed diff --git a/FS/FS/L10N/en_us.pm b/FS/FS/L10N/en_us.pm index 6ad136be0..ed936a5d4 100644 --- a/FS/FS/L10N/en_us.pm +++ b/FS/FS/L10N/en_us.pm @@ -1,6 +1,8 @@ package FS::L10N::en_us; -use base qw(FS::L10N); +use base qw(FS::L10N::DBI); -our %Lexicon = ( _AUTO=>1 ); +#prevents english "translation" via FS::L10N::DBI, FS::Msgcat::_gettext already +# does the same sort of fallback +#our %Lexicon = ( _AUTO=>1 ); 1; diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 2bc1596f2..1553a42df 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -77,7 +77,7 @@ if ( -e $addl_handler_use_file ) { use HTML::TableExtract qw(tree); use HTML::FormatText; use HTML::Defang; - use JSON; + use JSON::XS; # use XMLRPC::Transport::HTTP; # use XMLRPC::Lite; # for XMLRPC::Serializer use MIME::Base64; @@ -160,6 +160,7 @@ if ( -e $addl_handler_use_file ) { use FS::cust_credit; use FS::cust_credit_bill; use FS::cust_main; + use FS::h_cust_main; use FS::cust_main::Search qw(smart_search); use FS::cust_main::Import; use FS::cust_main_county; @@ -332,6 +333,12 @@ if ( -e $addl_handler_use_file ) { use FS::GeocodeCache; use FS::log; use FS::log_context; + use FS::part_pkg_usage_class; + use FS::cust_pkg_usage; + use FS::part_pkg_usage_class; + use FS::part_pkg_usage; + use FS::cdr_cust_pkg_usage; + use FS::part_pkg_msgcat; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm index 096ec8a4c..de9fb522f 100644 --- a/FS/FS/Misc.pm +++ b/FS/FS/Misc.pm @@ -699,7 +699,8 @@ sub generate_ps { open(POSTSCRIPT, "<$file.ps") or die "can't open $file.ps: $! (error in LaTeX template?)\n"; - unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex"); + unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex") + unless $FS::CurrentUser::CurrentUser->option('save_tmp_typesetting'); my $ps = ''; @@ -757,7 +758,8 @@ sub generate_pdf { open(PDF, "<$file.pdf") or die "can't open $file.pdf: $! (error in LaTeX template?)\n"; - unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex"); + unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex") + unless $FS::CurrentUser::CurrentUser->option('save_tmp_typesetting'); my $pdf = ''; while (<PDF>) { @@ -800,16 +802,32 @@ sub _pslatex { } -=item do_print ARRAYREF +=item do_print ARRAYREF [, OPTION => VALUE ... ] Sends the lines in ARRAYREF to the printer. +Options available are: + +=over 4 + +=item agentnum + +Uses this agent's 'lpr' configuration setting override instead of the global +value. + +=item lpr + +Uses this command instead of the configured lpr command (overrides both the +global value and agentnum). + =cut sub do_print { - my $data = shift; + my( $data, %opt ) = @_; - my $lpr = $conf->config('lpr'); + my $lpr = ( exists($opt{'lpr'}) && $opt{'lpr'} ) + ? $opt{'lpr'} + : $conf->config('lpr', $opt{'agentnum'} ); my $outerr = ''; run3 $lpr, $data, \$outerr, \$outerr; diff --git a/FS/FS/Misc/DateTime.pm b/FS/FS/Misc/DateTime.pm index 9c12e6408..2fff90647 100644 --- a/FS/FS/Misc/DateTime.pm +++ b/FS/FS/Misc/DateTime.pm @@ -2,8 +2,8 @@ package FS::Misc::DateTime; use base qw( Exporter ); use vars qw( @EXPORT_OK ); -use POSIX; use Carp; +use Time::Local; use Date::Parse; use DateTime::Format::Natural; use FS::Conf; @@ -49,7 +49,7 @@ sub parse_datetime { #carp "WARNING: can't parse date: ". $parser->error; #return ''; #huh, very common, we still need the "partially" (fully enough for our purposes) parsed date. - $dt->epoch; + return $dt->epoch; } } else { return str2time($string, $tz); @@ -59,24 +59,17 @@ sub parse_datetime { =item day_end TIME -If the next-bill-ignore-time configuration setting is turned off, just -returns the passed-in value. - -If the next-bill-ignore-time configuration setting is turned on, parses TIME -as an integer UNIX timestamp and returns a new timestamp with the same date but -23:59:59 for the time. +Parses TIME as an integer UNIX timestamp and returns a new timestamp with the +same date but 23:59:59 for the time. =cut sub day_end { my $time = shift; - my $conf = new FS::Conf; - return $time unless $conf->exists('next-bill-ignore-time'); - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time); - mktime(59,59,23,$mday,$mon,$year,$wday,$yday,$isdst); + timelocal(59,59,23,$mday,$mon,$year); } =back diff --git a/FS/FS/Misc/Geo.pm b/FS/FS/Misc/Geo.pm index 5cb10b2fb..2ad8311d0 100644 --- a/FS/FS/Misc/Geo.pm +++ b/FS/FS/Misc/Geo.pm @@ -81,7 +81,7 @@ sub get_censustract_ffiec { my($zip5, $zip4) = split('-',$location->{zip}); - $year ||= '2011'; #2012 per http://transition.fcc.gov/form477/techfaqs.html soon/now? + $year ||= '2012'; my @ffiec_args = ( __VIEWSTATE => $viewstate, __EVENTVALIDATION => $eventvalidation, diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index ca68c3596..bdf3bcf3a 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -458,7 +458,13 @@ sub qsearch { # grep defined( $record->{$_} ) && $record->{$_} ne '', @fields # ) or croak "Error executing \"$statement\": ". $sth->errstr; - $sth->execute or croak "Error executing \"$statement\": ". $sth->errstr; + my $ok = $sth->execute; + if (!$ok) { + my $error = "Error executing \"$statement\""; + $error .= ' (' . join(', ', map {"'$_'"} @value) . ')' if @value; + $error .= ': '. $sth->errstr; + croak $error; + } my $table = $stable[0]; my $pkey = ''; @@ -1451,6 +1457,7 @@ sub process_batch_import { format_sep_chars => $opt->{format_sep_chars}, format_fixedlength_formats => $opt->{format_fixedlength_formats}, format_xml_formats => $opt->{format_xml_formats}, + format_asn_formats => $opt->{format_asn_formats}, format_row_callbacks => $opt->{format_row_callbacks}, #per-import job => $job, @@ -1533,8 +1540,9 @@ sub batch_import { my $file = $param->{file}; my $params = $param->{params} || {}; - my( $type, $header, $sep_char, $fixedlength_format, - $xml_format, $row_callback, @fields ); + my( $type, $header, $sep_char, + $fixedlength_format, $xml_format, $asn_format, + $row_callback, @fields ); my $postinsert_callback = ''; $postinsert_callback = $param->{'postinsert_callback'} @@ -1572,6 +1580,11 @@ sub batch_import { ? $param->{'format_xml_formats'}{ $param->{'format'} } : ''; + $asn_format = + $param->{'format_asn_formats'} + ? $param->{'format_asn_formats'}{ $param->{'format'} } + : ''; + $row_callback = $param->{'format_row_callbacks'} ? $param->{'format_row_callbacks'}{ $param->{'format'} } @@ -1611,11 +1624,12 @@ sub batch_import { my $count; my $parser; my @buffer = (); + my $asn_header_buffer; if ( $type eq 'csv' || $type eq 'fixedlength' ) { if ( $type eq 'csv' ) { - my %attr = (); + my %attr = ( 'binary' => 1, ); $attr{sep_char} = $sep_char if $sep_char; $parser = new Text::CSV_XS \%attr; @@ -1652,7 +1666,9 @@ sub batch_import { $count++; $row = $header || 0; + } elsif ( $type eq 'xml' ) { + # FS::pay_batch eval "use XML::Simple;"; die $@ if $@; @@ -1668,6 +1684,26 @@ sub batch_import { $rows = $rows->{$_} foreach @$xmlrow; $rows = [ $rows ] if ref($rows) ne 'ARRAY'; $count = @buffer = @$rows; + + } elsif ( $type eq 'asn.1' ) { + + eval "use Convert::ASN1"; + die $@ if $@; + + my $asn = Convert::ASN1->new; + $asn->prepare( $asn_format->{'spec'} ) or die $asn->error; + + $parser = $asn->find( $asn_format->{'macro'} ) or die $asn->error; + + my $data = slurp($file); + my $asn_output = $parser->decode( $data ) + or die "No ". $asn_format->{'macro'}. " found\n"; + + $asn_header_buffer = &{ $asn_format->{'header_buffer'} }( $asn_output ); + + my $rows = &{ $asn_format->{'arrayref'} }( $asn_output ); + $count = @buffer = @$rows; + } else { die "Unknown file type $type\n"; } @@ -1711,6 +1747,7 @@ sub batch_import { while (1) { my @columns = (); + my %hash = %$params; if ( $type eq 'csv' ) { last unless scalar(@buffer); @@ -1747,16 +1784,27 @@ sub batch_import { #warn $z++. ": $_\n" for @columns; } elsif ( $type eq 'xml' ) { + # $parser = [ 'Column0Key', 'Column1Key' ... ] last unless scalar(@buffer); my $row = shift @buffer; @columns = @{ $row }{ @$parser }; + + } elsif ( $type eq 'asn.1' ) { + + last unless scalar(@buffer); + my $row = shift @buffer; + &{ $asn_format->{row_callback} }( $row, $asn_header_buffer ) + if $asn_format->{row_callback}; + foreach my $key ( keys %{ $asn_format->{map} } ) { + $hash{$key} = &{ $asn_format->{map}{$key} }( $row, $asn_header_buffer ); + } + } else { die "Unknown file type $type\n"; } my @later = (); - my %hash = %$params; foreach my $field ( @fields ) { @@ -2051,11 +2099,18 @@ is an error, returns the error, otherwise returns false. sub ut_money { my($self,$field)=@_; - $self->setfield($field, 0) if $self->getfield($field) eq ''; - $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{2})?\s*$/ - or return "Illegal (money) $field: ". $self->getfield($field); - #$self->setfield($field, "$1$2$3" || 0); - $self->setfield($field, ( ($1||''). ($2||''). ($3||'') ) || 0); + + if ( $self->getfield($field) eq '' ) { + $self->setfield($field, 0); + } elsif ( $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{1})\s*$/ ) { + #handle one decimal place without barfing out + $self->setfield($field, ( ($1||''). ($2||''). ($3.'0') ) || 0); + } elsif ( $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{2})?\s*$/ ) { + $self->setfield($field, ( ($1||''). ($2||''). ($3||'') ) || 0); + } else { + return "Illegal (money) $field: ". $self->getfield($field); + } + ''; } @@ -2466,10 +2521,29 @@ sub ut_name { # warn "ut_name allowed alphanumerics: +(sort grep /\w/, map { chr() } 0..255), "\n"; $self->getfield($field) =~ /^([\w \,\.\-\']+)$/ or return gettext('illegal_name'). " $field: ". $self->getfield($field); - $self->setfield($field,$1); + my $name = $1; + $name =~ s/^\s+//; + $name =~ s/\s+$//; + $name =~ s/\s+/ /g; + $self->setfield($field, $name); ''; } +=item ut_namen COLUMN + +Check/untaint proper names; allows alphanumerics, spaces and the following +punctuation: , . - ' + +May not be null. + +=cut + +sub ut_namen { + my( $self, $field ) = @_; + return $self->setfield($field, '') if $self->getfield($field) =~ /^$/; + $self->ut_name($field); +} + =item ut_zip COLUMN Check/untaint zip codes. diff --git a/FS/FS/Report/FCC_477.pm b/FS/FS/Report/FCC_477.pm index 49bb8a852..fd088148b 100644 --- a/FS/FS/Report/FCC_477.pm +++ b/FS/FS/Report/FCC_477.pm @@ -22,26 +22,26 @@ Documentation. =cut @upload = qw( - <200kpbs - 200-768kpbs + <200kbps + 200-768kbps 768kbps-1.5mbps 1.5-3mpbs 3-6mbps 6-10mbps 10-25mbps 25-100mbps - >100bmps + >100mbps ); @download = qw( - 200-768kpbs + 200-768kbps 768kbps-1.5mbps - 1.5-3mpbs + 1.5-3mbps 3-6mbps 6-10mbps 10-25mbps 25-100mbps - >100bmps + >100mbps ); @technology = ( diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index cbcd27b46..eb73ccbc8 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -772,7 +772,7 @@ sub tables_hashref { 'format', 'char', 'NULL', 1, '', '', 'classnum', 'int', 'NULL', '', '', '', 'duration', 'int', 'NULL', '', 0, '', - 'phonenum', 'varchar', 'NULL', 15, '', '', + 'phonenum', 'varchar', 'NULL', 25, '', '', 'accountcode', 'varchar', 'NULL', 20, '', '', 'startdate', @date_type, '', '', 'regionname', 'varchar', 'NULL', $char_d, '', '', @@ -875,7 +875,7 @@ sub tables_hashref { 'format', 'char', 'NULL', 1, '', '', 'classnum', 'int', 'NULL', '', '', '', 'duration', 'int', 'NULL', '', 0, '', - 'phonenum', 'varchar', 'NULL', 15, '', '', + 'phonenum', 'varchar', 'NULL', 25, '', '', 'accountcode', 'varchar', 'NULL', 20, '', '', 'startdate', @date_type, '', '', 'regionname', 'varchar', 'NULL', $char_d, '', '', @@ -1080,6 +1080,7 @@ sub tables_hashref { 'locale', 'varchar', 'NULL', 16, '', '', 'calling_list_exempt', 'char', 'NULL', 1, '', '', 'invoice_noemail', 'char', 'NULL', 1, '', '', + 'message_noemail', 'char', 'NULL', 1, '', '', 'bill_locationnum', 'int', 'NULL', '', '', '', 'ship_locationnum', 'int', 'NULL', '', '', '', ], @@ -1225,6 +1226,8 @@ sub tables_hashref { 'quotation_pkg' => { 'columns' => [ 'quotationpkgnum', 'serial', '', '', '', '', + 'quotationnum', 'int', 'NULL', '', '', '', #shouldn't be null, + # but history... 'pkgpart', 'int', '', '', '', '', 'locationnum', 'int', 'NULL', '', '', '', 'start_date', @date_type, '', '', @@ -1688,13 +1691,14 @@ sub tables_hashref { 'zip', 'varchar', 'NULL', 10, '', '', 'country', 'char', '', 2, '', '', # 'trancode', 'int', '', '', '', '' - 'payby', 'char', '', 4, '', '', # CARD/BILL/COMP, should be - 'payinfo', 'varchar', '', 512, '', '', + 'payby', 'char', '', 4, '', '', + 'payinfo', 'varchar', 'NULL', 512, '', '', #'exp', @date_type, '', '' - 'exp', 'varchar', 'NULL', 11, '', '', + 'exp', 'varchar', 'NULL', 11, '', '', 'payname', 'varchar', 'NULL', $char_d, '', '', 'amount', @money_type, '', '', - 'status', 'varchar', 'NULL', $char_d, '', '', + 'status', 'varchar', 'NULL', $char_d, '', '', + 'error_message', 'varchar', 'NULL', $char_d, '', '', ], 'primary_key' => 'paybatchnum', 'unique' => [], @@ -1717,6 +1721,7 @@ sub tables_hashref { 'custnum', 'int', '', '', '', '', 'pkgpart', 'int', '', '', '', '', 'pkgbatch', 'varchar', 'NULL', $char_d, '', '', + 'contactnum', 'int', 'NULL', '', '', '', 'locationnum', 'int', 'NULL', '', '', '', 'otaker', 'varchar', 'NULL', 32, '', '', 'usernum', 'int', 'NULL', '', '', '', @@ -1738,6 +1743,8 @@ sub tables_hashref { 'change_pkgnum', 'int', 'NULL', '', '', '', 'change_pkgpart', 'int', 'NULL', '', '', '', 'change_locationnum', 'int', 'NULL', '', '', '', + 'main_pkgnum', 'int', 'NULL', '', '', '', + 'pkglinknum', 'int', 'NULL', '', '', '', 'manual_flag', 'char', 'NULL', 1, '', '', 'no_auto', 'char', 'NULL', 1, '', '', 'quantity', 'int', 'NULL', '', '', '', @@ -1812,6 +1819,30 @@ sub tables_hashref { 'index' => [ [ 'pkgnum' ], [ 'discountnum' ], [ 'usernum' ], ], }, + 'cust_pkg_usage' => { + 'columns' => [ + 'pkgusagenum', 'serial', '', '', '', '', + 'pkgnum', 'int', '', '', '', '', + 'minutes', 'int', '', '', '', '', + 'pkgusagepart', 'int', '', '', '', '', + ], + 'primary_key' => 'pkgusagenum', + 'unique' => [], + 'index' => [ [ 'pkgnum' ], [ 'pkgusagepart' ] ], + }, + + 'cdr_cust_pkg_usage' => { + 'columns' => [ + 'cdrusagenum', 'bigserial', '', '', '', '', + 'acctid', 'bigint', '', '', '', '', + 'pkgusagenum', 'int', '', '', '', '', + 'minutes', 'int', '', '', '', '', + ], + 'primary_key' => 'cdrusagenum', + 'unique' => [], + 'index' => [ [ 'pkgusagenum' ], [ 'acctid' ] ], + }, + 'cust_bill_pkg_discount' => { 'columns' => [ 'billpkgdiscountnum', 'serial', '', '', '', '', @@ -1981,6 +2012,19 @@ sub tables_hashref { ], }, + 'part_pkg_msgcat' => { + 'columns' => [ + 'pkgpartmsgnum', 'serial', '', '', '', '', + 'pkgpart', 'int', '', '', '', '', + 'locale', 'varchar', '', 16, '', '', + 'pkg', 'varchar', '', $char_d, '', '', #longer/no limit? + 'comment', 'varchar', 'NULL', 2*$char_d, '', '', #longer/no limit? + ], + 'primary_key' => 'pkgpartmsgnum', + 'unique' => [ [ 'pkgpart', 'locale' ] ], + 'index' => [], + }, + 'part_pkg_link' => { 'columns' => [ 'pkglinknum', 'serial', '', '', '', '', @@ -2109,7 +2153,8 @@ sub tables_hashref { 'preserve', 'char', 'NULL', 1, '', '', 'selfservice_access', 'varchar', 'NULL', $char_d, '', '', 'classnum', 'int', 'NULL', '', '', '', - ], + 'restrict_edit_password','char', 'NULL', 1, '', '', +], 'primary_key' => 'svcpart', 'unique' => [], 'index' => [ [ 'disabled' ] ], @@ -2257,6 +2302,7 @@ sub tables_hashref { 'cgp_sendmdnmode', 'varchar', 'NULL', $char_d, '', '',#SendMDNMode #mail #XXX RPOP settings + # ], 'primary_key' => 'svcnum', #'unique' => [ [ 'username', 'domsvc' ] ], @@ -2683,9 +2729,10 @@ sub tables_hashref { 'columns' => [ 'exportnum', 'serial', '', '', '', '', 'exportname', 'varchar', 'NULL', $char_d, '', '', - 'machine', 'varchar', 'NULL', $char_d, '', '', + 'machine', 'varchar', 'NULL', $char_d, '', '', 'exporttype', 'varchar', '', $char_d, '', '', 'nodomain', 'char', 'NULL', 1, '', '', + 'default_machine','int', 'NULL', '', '', '', ], 'primary_key' => 'exportnum', 'unique' => [], @@ -2862,22 +2909,28 @@ sub tables_hashref { 'svc_broadband' => { 'columns' => [ - 'svcnum', 'int', '', '', '', '', - 'description', 'varchar', 'NULL', $char_d, '', '', - 'routernum', 'int', 'NULL', '', '', '', - 'blocknum', 'int', 'NULL', '', '', '', - 'sectornum', 'int', 'NULL', '', '', '', - 'speed_up', 'int', 'NULL', '', '', '', - 'speed_down', 'int', 'NULL', '', '', '', - 'ip_addr', 'varchar', 'NULL', 15, '', '', - 'mac_addr', 'varchar', 'NULL', 12, '', '', - 'authkey', 'varchar', 'NULL', 32, '', '', - 'latitude', 'decimal', 'NULL', '10,7', '', '', - 'longitude', 'decimal', 'NULL', '10,7', '', '', - 'altitude', 'decimal', 'NULL', '', '', '', - 'vlan_profile', 'varchar', 'NULL', $char_d, '', '', - 'performance_profile', 'varchar', 'NULL', $char_d, '', '', - 'plan_id', 'varchar', 'NULL', $char_d, '', '', + 'svcnum', 'int', '', '', '', '', + 'description', 'varchar', 'NULL', $char_d, '', '', + 'routernum', 'int', 'NULL', '', '', '', + 'blocknum', 'int', 'NULL', '', '', '', + 'sectornum', 'int', 'NULL', '', '', '', + 'speed_up', 'int', 'NULL', '', '', '', + 'speed_down', 'int', 'NULL', '', '', '', + 'ip_addr', 'varchar', 'NULL', 15, '', '', + 'mac_addr', 'varchar', 'NULL', 12, '', '', + 'authkey', 'varchar', 'NULL', 32, '', '', + 'latitude', 'decimal', 'NULL', '10,7', '', '', + 'longitude', 'decimal', 'NULL', '10,7', '', '', + 'altitude', 'decimal', 'NULL', '', '', '', + 'vlan_profile', 'varchar', 'NULL', $char_d, '', '', + 'performance_profile', 'varchar', 'NULL', $char_d, '', '', + 'plan_id', 'varchar', 'NULL', $char_d, '', '', + 'radio_serialnum', 'varchar', 'NULL', $char_d, '', '', + 'radio_location', 'varchar', 'NULL', 2*$char_d, '', '', + 'poe_location', 'varchar', 'NULL', 2*$char_d, '', '', + 'rssi', 'int', 'NULL', '', '', '', + 'suid', 'int', 'NULL', '', '', '', + 'shared_svcnum', 'int', 'NULL', '', '', '', ], 'primary_key' => 'svcnum', 'unique' => [ [ 'ip_addr' ], [ 'mac_addr' ] ], @@ -3016,6 +3069,32 @@ sub tables_hashref { 'index' => [ [ 'disabled' ] ], }, + 'part_pkg_usage' => { + 'columns' => [ + 'pkgusagepart', 'serial', '', '', '', '', + 'pkgpart', 'int', '', '', '', '', + 'minutes', 'int', '', '', '', '', + 'priority', 'int', 'NULL', '', '', '', + 'shared', 'char', 'NULL', 1, '', '', + 'rollover', 'char', 'NULL', 1, '', '', + 'description', 'varchar', 'NULL', $char_d, '', '', + ], + 'primary_key' => 'pkgusagepart', + 'unique' => [], + 'index' => [ [ 'pkgpart' ] ], + }, + + 'part_pkg_usage_class' => { + 'columns' => [ + 'num', 'serial', '', '', '', '', + 'pkgusagepart', 'int', '', '', '', '', + 'classnum', 'int','NULL', '', '', '', + ], + 'primary_key' => 'num', + 'unique' => [ [ 'pkgusagepart', 'classnum' ] ], + 'index' => [], + }, + 'rate' => { 'columns' => [ 'ratenum', 'serial', '', '', '', '', @@ -3053,6 +3132,7 @@ sub tables_hashref { 'columns' => [ 'regionnum', 'serial', '', '', '', '', 'regionname', 'varchar', '', $char_d, '', '', + 'exact_match', 'char', 'NULL', 1, '', '', ], 'primary_key' => 'regionnum', 'unique' => [], @@ -3333,6 +3413,12 @@ sub tables_hashref { 'quantity', 'int', 'NULL', '', '', '', 'upstream_rateid', 'int', 'NULL', '', '', '', + + ### + # more fields, for GSM imports + ### + 'servicecode', 'int', 'NULL', '', '', '', + 'quantity_able', 'int', 'NULL', '', '', '', ### #and now for our own fields @@ -3341,8 +3427,9 @@ sub tables_hashref { 'cdrtypenum', 'int', 'NULL', '', '', '', 'charged_party', 'varchar', 'NULL', $char_d, '', '', + 'charged_party_imsi', 'varchar', 'NULL', $char_d, '', '', - 'upstream_price', 'decimal', 'NULL', '10,4', '', '', + 'upstream_price', 'decimal', 'NULL', '10,5', '', '', 'upstream_src_regionname', 'varchar', 'NULL', $char_d, '', '', 'upstream_dst_regionname', 'varchar', 'NULL', $char_d, '', '', @@ -3357,7 +3444,7 @@ sub tables_hashref { 'rated_classnum', 'int', 'NULL', '', '', '', 'rated_ratename', 'varchar', 'NULL', $char_d, '', '', - 'carrierid', 'int', 'NULL', '', '', '', + 'carrierid', 'bigint', 'NULL', '', '', '', # service it was matched to 'svcnum', 'int', 'NULL', '', '', '', @@ -3590,7 +3677,8 @@ sub tables_hashref { 'columns' => [ 'svcnum', 'int', '', '', '', '', 'countrycode', 'varchar', '', 3, '', '', - 'phonenum', 'varchar', '', 15, '', '', #12 ? + 'phonenum', 'varchar', '', 25, '', '', #12 ? + 'sim_imsi', 'varchar', 'NULL', 15, '', '', 'pin', 'varchar', 'NULL', $char_d, '', '', 'sip_password', 'varchar', 'NULL', $char_d, '', '', 'phone_name', 'varchar', 'NULL', $char_d, '', '', diff --git a/FS/FS/TemplateItem_Mixin.pm b/FS/FS/TemplateItem_Mixin.pm index 6d7ea26bc..8b0e16a2d 100644 --- a/FS/FS/TemplateItem_Mixin.pm +++ b/FS/FS/TemplateItem_Mixin.pm @@ -52,10 +52,10 @@ line item, and for generic taxes, simply returns "Tax". =cut sub desc { - my $self = shift; + my( $self, $locale ) = @_; if ( $self->pkgnum > 0 ) { - $self->itemdesc || $self->part_pkg->pkg; + $self->itemdesc || $self->part_pkg->pkg_locale($locale); } else { my $desc = $self->itemdesc || 'Tax'; $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/; @@ -271,10 +271,12 @@ sub cust_bill_pkg_display { } else { my $hashref = { 'billpkgnum' => $self->billpkgnum }; $hashref->{type} = $type if defined($type); + + my $order_by = $self->display_table_orderby || 'billpkgdisplaynum'; @result = qsearch ({ 'table' => $self->display_table, - 'hashref' => { 'billpkgnum' => $self->billpkgnum }, - 'order_by' => 'ORDER BY billpkgdisplaynum', + 'hashref' => $hashref, + 'order_by' => "ORDER BY $order_by", }); } diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index adab9d5e2..2e78f12f4 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -122,7 +122,9 @@ sub print_latex { UNLINK => 0, ) or die "can't open temp file: $!\n"; - my $agentnum = $self->cust_main->agentnum; + my $cust_main = $self->cust_main; + my $prospect_main = $self->prospect_main; + my $agentnum = $cust_main ? $cust_main->agentnum : $prospect_main->agentnum; if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) { print $lh $conf->config_binary("logo_${template}.eps", $agentnum) @@ -363,14 +365,6 @@ sub print_generic { my $date_format = $date_formats{$format}; - my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}' - }, - 'html' => sub { return '<b>'. shift(). '</b>' - }, - 'template' => sub { shift }, - ); - my $embolden_function = $embolden_functions{$format}; - my %newline_tokens = ( 'latex' => '\\\\', 'html' => '<br>', 'template' => "\n", @@ -584,16 +578,20 @@ sub print_generic { #my $balance_due = $self->owed + $pr_total - $cr_total; my $balance_due = $self->owed + $pr_total; - # the customer's current balance as shown on the invoice before this one - $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) ); + #these are used on the summary page only + + # the customer's current balance as shown on the invoice before this one + $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) ); - # the change in balance from that invoice to this one - $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) ); + # the change in balance from that invoice to this one + $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) ); - # the sum of amount owed on all previous invoices - $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total); + # the sum of amount owed on all previous invoices + # ($pr_total is used elsewhere but not as $previous_balance) + $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total); # the sum of amount owed on all invoices + # (this is used in the summary & on the payment coupon) $invoice_data{'balance'} = sprintf("%.2f", $balance_due); # info from customer's last invoice before this one, for some @@ -727,10 +725,11 @@ sub print_generic { my $adjusttotal = 0; - my $adjust_section = { 'description' => - $self->mt('Credits, Payments, and Adjustments'), - 'subtotal' => 0, # adjusted below - }; + my $adjust_section = { + 'description' => $self->mt('Credits, Payments, and Adjustments'), + 'adjust_section' => 1, + 'subtotal' => 0, # adjusted below + }; my $adjust_weight = _pkg_category($adjust_section->{description}) ? _pkg_category($adjust_section->{description})->weight : 0; @@ -738,7 +737,7 @@ sub print_generic { $adjust_section->{'sort_weight'} = $adjust_weight; my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y'; - my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum); + my $multisection = $conf->exists($tc.'_sections', $cust_main->agentnum); $invoice_data{'multisection'} = $multisection; my $late_sections = []; my $extra_sections = []; @@ -936,6 +935,7 @@ sub print_generic { $detail->{'sdate'} = $line_item->{'sdate'}; $detail->{'edate'} = $line_item->{'edate'}; $detail->{'seconds'} = $line_item->{'seconds'}; + $detail->{'svc_label'} = $line_item->{'svc_label'}; push @detail_items, $detail; push @buf, ( [ $detail->{'description'}, @@ -1033,9 +1033,33 @@ sub print_generic { $money_char. sprintf("%10.2f",$self->charged) ]; push @buf,['','']; - # calculate total, possibly including total owed on previous - # invoices - { + + ### + # Totals + ### + + my %embolden_functions = ( + 'latex' => sub { return '\textbf{'. shift(). '}' }, + 'html' => sub { return '<b>'. shift(). '</b>' }, + 'template' => sub { shift }, + ); + my $embolden_function = $embolden_functions{$format}; + + if ( $self->can('_items_total') ) { # quotations + + $self->_items_total(\@total_items); + + foreach ( @total_items ) { + $_->{'total_item'} = &$embolden_function( $_->{'total_item'} ); + $_->{'total_amount'} = &$embolden_function( $other_money_char. + $_->{'total_amount'} + ); + } + + } else { #normal invoice case + + # calculate total, possibly including total owed on previous + # invoices my $total = {}; my $item = 'Total'; $item = $conf->config('previous_balance-exclude_from_total') @@ -1066,126 +1090,128 @@ sub print_generic { sprintf( '%10.2f', $amount ) ]; push @buf,['','']; - } - # if we're showing previous invoices, also show previous - # credits and payments - if ( $self->enable_previous - and $self->can('_items_credits') - and $self->can('_items_payments') ) - { - #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments - - # credits - my $credittotal = 0; - foreach my $credit ( $self->_items_credits('trim_len'=>60) ) { + # if we're showing previous invoices, also show previous + # credits and payments + if ( $self->enable_previous + and $self->can('_items_credits') + and $self->can('_items_payments') ) + { + #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments + + # credits + my $credittotal = 0; + foreach my $credit ( $self->_items_credits('trim_len'=>60) ) { + + my $total; + $total->{'total_item'} = &$escape_function($credit->{'description'}); + $credittotal += $credit->{'amount'}; + $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'}; + $adjusttotal += $credit->{'amount'}; + if ( $multisection ) { + my $money = $old_latex ? '' : $money_char; + push @detail_items, { + ext_description => [], + ref => '', + quantity => '', + description => &$escape_function($credit->{'description'}), + amount => $money. $credit->{'amount'}, + product_code => '', + section => $adjust_section, + }; + } else { + push @total_items, $total; + } - my $total; - $total->{'total_item'} = &$escape_function($credit->{'description'}); - $credittotal += $credit->{'amount'}; - $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'}; - $adjusttotal += $credit->{'amount'}; - if ( $multisection ) { - my $money = $old_latex ? '' : $money_char; - push @detail_items, { - ext_description => [], - ref => '', - quantity => '', - description => &$escape_function($credit->{'description'}), - amount => $money. $credit->{'amount'}, - product_code => '', - section => $adjust_section, - }; - } else { - push @total_items, $total; } + $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal); - } - $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal); - - #credits (again) - foreach my $credit ( $self->_items_credits('trim_len'=>32) ) { - push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ]; - } + #credits (again) + foreach my $credit ( $self->_items_credits('trim_len'=>32) ) { + push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ]; + } - # payments - my $paymenttotal = 0; - foreach my $payment ( $self->_items_payments ) { - my $total = {}; - $total->{'total_item'} = &$escape_function($payment->{'description'}); - $paymenttotal += $payment->{'amount'}; - $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'}; - $adjusttotal += $payment->{'amount'}; + # payments + my $paymenttotal = 0; + foreach my $payment ( $self->_items_payments ) { + my $total = {}; + $total->{'total_item'} = &$escape_function($payment->{'description'}); + $paymenttotal += $payment->{'amount'}; + $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'}; + $adjusttotal += $payment->{'amount'}; + if ( $multisection ) { + my $money = $old_latex ? '' : $money_char; + push @detail_items, { + ext_description => [], + ref => '', + quantity => '', + description => &$escape_function($payment->{'description'}), + amount => $money. $payment->{'amount'}, + product_code => '', + section => $adjust_section, + }; + }else{ + push @total_items, $total; + } + push @buf, [ $payment->{'description'}, + $money_char. sprintf("%10.2f", $payment->{'amount'}), + ]; + } + $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal); + if ( $multisection ) { - my $money = $old_latex ? '' : $money_char; - push @detail_items, { - ext_description => [], - ref => '', - quantity => '', - description => &$escape_function($payment->{'description'}), - amount => $money. $payment->{'amount'}, - product_code => '', - section => $adjust_section, - }; - }else{ - push @total_items, $total; + $adjust_section->{'subtotal'} = $other_money_char. + sprintf('%.2f', $adjusttotal); + push @sections, $adjust_section + unless $adjust_section->{sort_weight}; } - push @buf, [ $payment->{'description'}, - $money_char. sprintf("%10.2f", $payment->{'amount'}), - ]; - } - $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal); - - if ( $multisection ) { - $adjust_section->{'subtotal'} = $other_money_char. - sprintf('%.2f', $adjusttotal); - push @sections, $adjust_section - unless $adjust_section->{sort_weight}; - } - # create Balance Due message - { - my $total; - $total->{'total_item'} = &$embolden_function($self->balance_due_msg); - $total->{'total_amount'} = - &$embolden_function( - $other_money_char. sprintf('%.2f', $summarypage - ? $self->charged + - $self->billing_balance - : $self->owed + $pr_total - ) - ); - if ( $multisection && !$adjust_section->{sort_weight} ) { - $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '. - $total->{'total_amount'}; - }else{ - push @total_items, $total; + # create Balance Due message + { + my $total; + $total->{'total_item'} = &$embolden_function($self->balance_due_msg); + $total->{'total_amount'} = + &$embolden_function( + $other_money_char. sprintf('%.2f', #why? $summarypage + # ? $self->charged + + # $self->billing_balance + # : + $self->owed + $pr_total + ) + ); + if ( $multisection && !$adjust_section->{sort_weight} ) { + $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '. + $total->{'total_amount'}; + }else{ + push @total_items, $total; + } + push @buf,['','-----------']; + push @buf,[$self->balance_due_msg, $money_char. + sprintf("%10.2f", $balance_due ) ]; } - push @buf,['','-----------']; - push @buf,[$self->balance_due_msg, $money_char. - sprintf("%10.2f", $balance_due ) ]; - } - if ( $conf->exists('previous_balance-show_credit') - and $cust_main->balance < 0 ) { - my $credit_total = { - 'total_item' => &$embolden_function($self->credit_balance_msg), - 'total_amount' => &$embolden_function( - $other_money_char. sprintf('%.2f', -$cust_main->balance) - ), - }; - if ( $multisection ) { - $adjust_section->{'posttotal'} .= $newline_token . - $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'}; - } - else { - push @total_items, $credit_total; + if ( $conf->exists('previous_balance-show_credit') + and $cust_main->balance < 0 ) { + my $credit_total = { + 'total_item' => &$embolden_function($self->credit_balance_msg), + 'total_amount' => &$embolden_function( + $other_money_char. sprintf('%.2f', -$cust_main->balance) + ), + }; + if ( $multisection ) { + $adjust_section->{'posttotal'} .= $newline_token . + $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'}; + } + else { + push @total_items, $credit_total; + } + push @buf,['','-----------']; + push @buf,[$self->credit_balance_msg, $money_char. + sprintf("%10.2f", -$cust_main->balance ) ]; } - push @buf,['','-----------']; - push @buf,[$self->credit_balance_msg, $money_char. - sprintf("%10.2f", -$cust_main->balance ) ]; } - } + + } #end of default total adding ! can('_items_total') if ( $multisection ) { if ( $conf->exists('svc_phone_sections') @@ -2042,6 +2068,11 @@ separate quantities, for some reason). =cut +sub _items_nontax { + my $self = shift; + grep { $_->pkgnum } $self->cust_bill_pkg; +} + sub _items_pkg { my $self = shift; my %options = @_; @@ -2049,7 +2080,7 @@ sub _items_pkg { warn "$me _items_pkg searching for all package line items\n" if $DEBUG > 1; - my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg; + my @cust_bill_pkg = $self->_items_nontax; warn "$me _items_pkg filtering line items\n" if $DEBUG > 1; @@ -2150,6 +2181,7 @@ sub _items_cust_bill_pkg { my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style # and location labels + my $locale = $cust_main->locale; my @b = (); my ($s, $r, $u) = ( undef, undef, undef ); @@ -2194,7 +2226,7 @@ sub _items_cust_bill_pkg { my $type = $display->type; - my $desc = $cust_bill_pkg->desc; + my $desc = $cust_bill_pkg->desc( $cust_main->locale ); $desc = substr($desc, 0, $maxlength). '...' if $format eq 'latex' && length($desc) > $maxlength; @@ -2260,13 +2292,16 @@ sub _items_cust_bill_pkg { || $cust_bill_pkg->recur_show_zero; my @d = (); + my $svc_label; unless ( $cust_pkg->part_pkg->hide_svc_detail || $cust_bill_pkg->hidden ) { - push @d, map &{$escape_function}($_), - $cust_pkg->h_labels_short($self->_date, undef, 'I') + my @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date, undef, 'I'); + push @d, @svc_labels unless $cust_bill_pkg->pkgpart_override; #don't redisplay services + $svc_label = $svc_labels[0]; if ( ! $cust_pkg->locationnum or $cust_pkg->locationnum != $cust_main->ship_locationnum ) { @@ -2296,6 +2331,7 @@ sub _items_cust_bill_pkg { unit_amount => $cust_bill_pkg->unitsetup, quantity => $cust_bill_pkg->quantity, ext_description => \@d, + svc_label => ($svc_label || ''), }; }; @@ -2318,16 +2354,25 @@ sub _items_cust_bill_pkg { my $description = ($is_summary && $type && $type eq 'U') ? "Usage charges" : $desc; + my $part_pkg = $cust_pkg->part_pkg; + #pry be a bit more efficient to look some of this conf stuff up # outside the loop unless ( $conf->exists('disable_line_item_date_ranges') - || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1) + || $part_pkg->option('disable_line_item_date_ranges',1) + || ! $cust_bill_pkg->sdate + || ! $cust_bill_pkg->edate ) { my $time_period; - my $date_style = $conf->config( 'cust_bill-line_item-date_style', + my $date_style = ''; + $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly', + $cust_main->agentnum + ) + if $part_pkg && $part_pkg->freq !~ /^1m?$/; + $date_style ||= $conf->config( 'cust_bill-line_item-date_style', $cust_main->agentnum - ); + ); if ( defined($date_style) && $date_style eq 'month_of' ) { $time_period = time2str('The month of %B', $cust_bill_pkg->sdate); } elsif ( defined($date_style) && $date_style eq 'X_month' ) { @@ -2345,6 +2390,7 @@ sub _items_cust_bill_pkg { my @d = (); my @seconds = (); # for display of usage info + my $svc_label = ''; #at least until cust_bill_pkg has "past" ranges in addition to #the "future" sdate/edate ones... see #3032 @@ -2353,7 +2399,7 @@ sub _items_cust_bill_pkg { push @dates, $prev->sdate if $prev; push @dates, undef if !$prev; - unless ( $cust_pkg->part_pkg->hide_svc_detail + unless ( $part_pkg->hide_svc_detail || $cust_bill_pkg->itemdesc || $cust_bill_pkg->hidden || $is_summary && $type && $type eq 'U' @@ -2363,11 +2409,11 @@ sub _items_cust_bill_pkg { warn "$me _items_cust_bill_pkg adding service details\n" if $DEBUG > 1; - push @d, map &{$escape_function}($_), - $cust_pkg->h_labels_short(@dates, 'I') - #$cust_bill_pkg->edate, - #$cust_bill_pkg->sdate) + my @svc_labels = map &{$escape_function}($_), + $cust_pkg->h_labels_short($self->_date, undef, 'I'); + push @d, @svc_labels unless $cust_bill_pkg->pkgpart_override; #don't redisplay services + $svc_label = $svc_labels[0]; warn "$me _items_cust_bill_pkg done adding service details\n" if $DEBUG > 1; @@ -2450,6 +2496,7 @@ sub _items_cust_bill_pkg { quantity => $cust_bill_pkg->quantity, %item_dates, ext_description => \@d, + svc_label => ($svc_label || ''), }; $r->{'seconds'} = \@seconds if grep {defined $_} @seconds; } diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm index c2ea0a61c..c8ad430b2 100644 --- a/FS/FS/UI/Web.pm +++ b/FS/FS/UI/Web.pm @@ -6,7 +6,7 @@ use Exporter; use Carp qw( confess ); use HTML::Entities; use FS::Conf; -use FS::Misc::DateTime qw( parse_datetime ); +use FS::Misc::DateTime qw( parse_datetime day_end ); use FS::Record qw(dbdef); use FS::cust_main; # are sql_balance and sql_date_balance in the right module? @@ -32,16 +32,16 @@ sub parse_beginning_ending { my $beginning = 0; if ( $cgi->param($prefix.'begin') =~ /^(\d+)$/ ) { $beginning = $1; - } elsif ( $cgi->param($prefix.'beginning') =~ /^([ 0-9\-\/]{1,64})$/ ) { + } elsif ( $cgi->param($prefix.'beginning') =~ /^([ 0-9\-\/\:]{1,64})$/ ) { $beginning = parse_datetime($1) || 0; } my $ending = 4294967295; #2^32-1 if ( $cgi->param($prefix.'end') =~ /^(\d+)$/ ) { $ending = $1 - 1; - } elsif ( $cgi->param($prefix.'ending') =~ /^([ 0-9\-\/]{1,64})$/ ) { - #probably need an option to turn off the + 86399 - $ending = parse_datetime($1) + 86399; + } elsif ( $cgi->param($prefix.'ending') =~ /^([ 0-9\-\/\:]{1,64})$/ ) { + $ending = parse_datetime($1); + $ending = day_end($ending) unless $ending =~ /:/; } ( $beginning, $ending ); @@ -235,20 +235,20 @@ sub cust_header { '(service) Name' => 'ship_contact', '(bill) Company' => 'company', '(service) Company' => 'ship_company', - 'Address 1' => 'address1', - 'Address 2' => 'address2', - 'City' => 'city', - 'State' => 'state', - 'Zip' => 'zip', + 'Address 1' => 'bill_address1', + 'Address 2' => 'bill_address2', + 'City' => 'bill_city', + 'State' => 'bill_state', + 'Zip' => 'bill_zip', 'Country' => 'country_full', 'Day phone' => 'daytime', # XXX should use msgcat, but how? 'Night phone' => 'night', # XXX should use msgcat, but how? 'Fax number' => 'fax', - '(bill) Address 1' => 'address1', - '(bill) Address 2' => 'address2', - '(bill) City' => 'city', - '(bill) State' => 'state', - '(bill) Zip' => 'zip', + '(bill) Address 1' => 'bill_address1', + '(bill) Address 2' => 'bill_address2', + '(bill) City' => 'bill_city', + '(bill) State' => 'bill_state', + '(bill) Zip' => 'bill_zip', '(bill) Country' => 'country_full', '(bill) Day phone' => 'daytime', # XXX should use msgcat, but how? '(bill) Night phone' => 'night', # XXX should use msgcat, but how? @@ -335,17 +335,21 @@ setting is supplied, the <B>cust-fields</B> configuration value. sub cust_sql_fields { my @fields = qw( last first company ); - push @fields, map "ship_$_", @fields; - push @fields, 'country'; +# push @fields, map "ship_$_", @fields; cust_header(@_); #inefficientish, but tiny lists and only run once per page - my @add_fields = qw( address1 address2 city state zip daytime night fax ); - push @fields, - grep { my $field = $_; grep { $_ eq $field } @cust_fields } - ( @add_fields, ( map "ship_$_", @add_fields ), 'payby' ); - + my @location_fields; + foreach my $field (qw( address1 address2 city state zip )) { + foreach my $pre ('bill_','ship_') { + if ( grep { $_ eq $pre.$field } @cust_fields ) { + push @location_fields, $pre.'location.'.$field.' AS '.$pre.$field; + } + } + } + + push @fields, 'payby' if grep { $_ eq 'payby'} @cust_fields; push @fields, 'agent_custid'; my @extra_fields = (); @@ -353,7 +357,71 @@ sub cust_sql_fields { push @extra_fields, FS::cust_main->balance_sql . " AS current_balance"; } - map("cust_main.$_", @fields), @extra_fields; + map("cust_main.$_", @fields), @location_fields, @extra_fields; +} + +=item join_cust_main [ TABLE[.CUSTNUM] ] [ LOCATION_TABLE[.LOCATIONNUM] ] + +Returns an SQL join phrase for the FROM clause so that the fields listed +in L<cust_sql_fields> will be available. Currently joins to cust_main +itself, as well as cust_location (under the aliases 'bill_location' and +'ship_location') if address fields are needed. L<cust_header()> should have +been called already. + +All of these will be left joins; if you want to exclude rows with no linked +cust_main record (or bill_location/ship_location), you can do so in the +WHERE clause. + +TABLE is the table containing the custnum field. If CUSTNUM (a field name +in that table) is specified, that field will be joined to cust_main.custnum. +Otherwise, this function will assume the field is named "custnum". If the +argument isn't present at all, the join will just say "USING (custnum)", +which might work. + +As a special case, if TABLE is 'cust_main', only the joins to cust_location +will be returned. + +LOCATION_TABLE is an optional table name to use for joining ship_location, +in case your query also includes package information and you want the +"service address" columns to reflect package addresses. + +=cut + +sub join_cust_main { + my ($cust_table, $location_table) = @_; + my ($custnum, $locationnum); + ($cust_table, $custnum) = split(/\./, $cust_table); + $custnum ||= 'custnum'; + ($location_table, $locationnum) = split(/\./, $location_table); + $locationnum ||= 'locationnum'; + + my $sql = ''; + if ( $cust_table ) { + $sql = " LEFT JOIN cust_main ON (cust_main.custnum = $cust_table.$custnum)" + unless $cust_table eq 'cust_main'; + } else { + $sql = " LEFT JOIN cust_main USING (custnum)"; + } + + if ( !@cust_fields or grep /^bill_/, @cust_fields ) { + + $sql .= ' LEFT JOIN cust_location bill_location'. + ' ON (bill_location.locationnum = cust_main.bill_locationnum)'; + + } + + if ( !@cust_fields or grep /^ship_/, @cust_fields ) { + + if (!$location_table) { + $location_table = 'cust_main'; + $locationnum = 'ship_locationnum'; + } + + $sql .= ' LEFT JOIN cust_location ship_location'. + " ON (ship_location.locationnum = $location_table.$locationnum) "; + } + + $sql; } =item cust_fields OBJECT [ CUST_FIELDS_VALUE ] @@ -404,23 +472,26 @@ sub cust_fields_subs { my $unlinked_warn = 0; return map { my $f = $_; - if( $unlinked_warn++ ) { + if ( $unlinked_warn++ ) { + sub { my $record = shift; - if( $record->custnum ) { - $record->$f(@_); - } - else { + if ( $record->custnum ) { + encode_entities( $record->$f(@_) ); + } else { '(unlinked)' }; - } - } - else { + }; + + } else { + sub { my $record = shift; - $record->$f(@_) if $record->custnum; - } + $record->custnum ? encode_entities( $record->$f(@_) ) : ''; + }; + } + } @cust_fields; } @@ -510,7 +581,7 @@ use vars qw($DEBUG); use Carp; use Storable qw(nfreeze); use MIME::Base64; -use JSON; +use JSON::XS; use FS::UID qw(getotaker); use FS::Record qw(qsearchs); use FS::queue; @@ -655,10 +726,7 @@ sub job_status { @return = ( 'error', $job ? $job->statustext : $jobnum ); } - #to_json(\@return); #waiting on deb 5.0 for new JSON.pm? - #silence the warning though - my $to_json = JSON->can('to_json') || JSON->can('objToJson'); - &$to_json(\@return); + encode_json \@return; } diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index fea53a235..cda3198eb 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -294,6 +294,9 @@ sub upgrade_data { #insert default tower_sector if not present 'tower' => [], + #repair improperly deleted services + 'cust_svc' => [], + #routernum/blocknum 'svc_broadband' => [], diff --git a/FS/FS/access_right.pm b/FS/FS/access_right.pm index 397b456ce..d370ba5d1 100644 --- a/FS/FS/access_right.pm +++ b/FS/FS/access_right.pm @@ -198,6 +198,10 @@ sub _upgrade_data { # class method 'New prospect' => 'Generate quotation', 'Delete invoices' => 'Void invoices', 'List invoices' => 'List quotations', + 'Post credit' => 'Credit line items', + #'View customer tax exemptions' => 'Edit customer tax exemptions', + 'Edit customer' => 'Edit customer tax exemptions', + 'Edit package definitions' => 'Bulk edit package definitions', 'List services' => [ 'Services: Accounts', 'Services: Domains', @@ -218,12 +222,17 @@ sub _upgrade_data { # class method 'Services: Accounts' => 'Services: Accounts: Advanced search', 'Services: Wireless broadband services' => 'Services: Wireless broadband services: Advanced search', 'Services: Hardware' => 'Services: Hardware: Advanced search', + 'Services: Phone numbers' => 'Services: Phone numbers: Advanced search', 'List rating data' => [ 'Usage: RADIUS sessions', 'Usage: Call Detail Records (CDRs)', 'Usage: Unrateable CDRs', ], - ; + 'Provision customer service' => [ 'Edit password' ], + 'Financial reports' => [ 'Employees: Commission Report', + 'Employees: Audit Report', + ], +; foreach my $old_acl ( keys %onetime ) { diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index fdec921ee..3ebe6c420 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -11,6 +11,7 @@ use Date::Parse; use Date::Format; use Time::Local; use List::Util qw( first min ); +use Text::CSV_XS; use FS::UID qw( dbh ); use FS::Conf; use FS::Record qw( qsearch qsearchs ); @@ -325,6 +326,10 @@ sub check { $self->billsec( $self->enddate - $self->answerdate ); } + if ( ! $self->enddate && $self->startdate && $self->duration ) { + $self->enddate( $self->startdate + $self->duration ); + } + $self->set_charged_party; #check the foreign keys even? @@ -421,12 +426,25 @@ sub set_charged_party { Sets the status to the provided string. If there is an error, returns the error, otherwise returns false. +If status is being changed from 'rated' to some other status, also removes +any usage allocations to this CDR. + =cut sub set_status { my($self, $status) = @_; + my $old_status = $self->freesidestatus; $self->freesidestatus($status); - $self->replace; + my $error = $self->replace; + if ( $old_status eq 'rated' and $status ne 'done' ) { + # deallocate any usage + foreach (qsearch('cdr_cust_pkg_usage', {acctid => $self->acctid})) { + my $cust_pkg_usage = $_->cust_pkg_usage; + $cust_pkg_usage->set('minutes', $cust_pkg_usage->minutes + $_->minutes); + $error ||= $cust_pkg_usage->replace || $_->delete; + } + } + $error; } =item set_status_and_rated_price STATUS RATED_PRICE [ SVCNUM [ OPTION => VALUE ... ] ] @@ -573,7 +591,7 @@ reference of the number of included minutes and will be decremented by the rated minutes of this CDR. region_group_included_minutes_hashref is required for prefix price plans which -have included minues (otehrwise unused/ignored). It should be set to an empty +have included minues (otherwise unused/ignored). It should be set to an empty hashref at the start of a month's rating and then preserved across CDRs. =cut @@ -598,6 +616,7 @@ our %interval_cache = (); # for timed rates sub rate_prefix { my( $self, %opt ) = @_; my $part_pkg = $opt{'part_pkg'} or return "No part_pkg specified"; + my $cust_pkg = $opt{'cust_pkg'}; my $da_rewrote = 0; # this will result in those CDRs being marked as done... is that @@ -625,7 +644,34 @@ sub rate_prefix { ); } + if ( $part_pkg->option_cacheable('skip_same_customer') + and ! $self->is_tollfree ) { + my ($dst_countrycode, $dst_number) = $self->parse_number( + column => 'dst', + international_prefix => $part_pkg->option_cacheable('international_prefix'), + domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'), + ); + my $dst_same_cust = FS::Record->scalar_sql( + 'SELECT COUNT(svc_phone.svcnum) AS count '. + 'FROM cust_pkg ' . + 'JOIN cust_svc USING (pkgnum) ' . + 'JOIN svc_phone USING (svcnum) ' . + 'WHERE svc_phone.countrycode = ' . dbh->quote($dst_countrycode) . + ' AND svc_phone.phonenum = ' . dbh->quote($dst_number) . + ' AND cust_pkg.custnum = ' . $cust_pkg->custnum, + ); + if ( $dst_same_cust > 0 ) { + warn "not charging for CDR (same source and destination customer)\n" if $DEBUG; + return $self->set_status_and_rated_price( 'skipped', + 0, + $opt{'svcnum'}, + ); + } + } + + + ### # look up rate details based on called station id # (or calling station id for toll free calls) @@ -823,11 +869,6 @@ sub rate_prefix { $seconds_left -= $charge_sec; - my $included_min = $opt{'region_group_included_min_hashref'} || {}; - - $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included - unless exists $included_min->{$regionnum}{$ratetimenum}; - my $granularity = $rate_detail->sec_granularity; my $minutes; @@ -845,20 +886,40 @@ sub rate_prefix { $seconds += $charge_sec; + if ( $rate_detail->min_included ) { + # the old, kind of deprecated way to do this + my $included_min = $opt{'region_group_included_min_hashref'} || {}; - my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0; + # by default, set the included minutes for this region/time to + # what's in the rate_detail + $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included + unless exists $included_min->{$regionnum}{$ratetimenum}; - ${$opt{region_group_included_min}} -= $minutes - if $region_group && $rate_detail->region_group; + # the way that doesn't work + #my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0; + + #${$opt{region_group_included_min}} -= $minutes + # if $region_group && $rate_detail->region_group; + + if ( $included_min->{$regionnum}{$ratetimenum} > $minutes ) { + $charge_sec = 0; + $included_min->{$regionnum}{$ratetimenum} -= $minutes; + } else { + $charge_sec -= ($included_min->{$regionnum}{$ratetimenum} * 60); + $included_min->{$regionnum}{$ratetimenum} = 0; + } + } else { + # the new way! + my $applied_min = $cust_pkg->apply_usage( + 'cdr' => $self, + 'rate_detail' => $rate_detail, + 'minutes' => $minutes + ); + # for now, usage pools deal only in whole minutes + $charge_sec -= $applied_min * 60; + } - $included_min->{$regionnum}{$ratetimenum} -= $minutes; - if ( - $included_min->{$regionnum}{$ratetimenum} <= 0 - && ( ${$opt{region_group_included_min}} <= 0 - || ! $rate_detail->region_group - ) - ) - { + if ( $charge_sec > 0 ) { #NOW do connection charges here... right? #my $conn_seconds = min($seconds_left, $rate_detail->conn_sec); @@ -871,16 +932,9 @@ sub rate_prefix { } #should preserve (display?) this - my $charge_min = 0 - $included_min->{$regionnum}{$ratetimenum} - ( $conn_seconds / 60 ); - $included_min->{$regionnum}{$ratetimenum} = 0; + my $charge_min = ( $charge_sec - $conn_seconds ) / 60; $charge += ($rate_detail->min_charge * $charge_min) if $charge_min > 0; #still not rounded - } elsif ( ${$opt{region_group_included_min}} > 0 - && $region_group - && $rate_detail->region_group - ) - { - $included_min->{$regionnum}{$ratetimenum} = 0 } # choose next rate_detail @@ -1168,6 +1222,8 @@ sub export_formats { length($price) ? ($opt{money_char} . $price) : ''; }; + my $src_sub = sub { $_[0]->clid || $_[0]->src }; + %export_formats = ( 'simple' => [ sub { time2str($date_format, shift->calldate_unix ) }, #DATE @@ -1182,7 +1238,7 @@ sub export_formats { sub { time2str($date_format, shift->calldate_unix ) }, #DATE sub { time2str('%r', shift->calldate_unix ) }, #TIME #'userfield', #USER - 'src', #called from + $src_sub, #called from 'dst', #NUMBER_DIALED $duration_sub, #DURATION #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE @@ -1191,7 +1247,7 @@ sub export_formats { 'accountcode_simple' => [ sub { time2str($date_format, shift->calldate_unix ) }, #DATE sub { time2str('%r', shift->calldate_unix ) }, #TIME - 'src', #called from + $src_sub, #called from 'accountcode', #NUMBER_DIALED $duration_sub, #DURATION $price_sub, @@ -1199,14 +1255,14 @@ sub export_formats { 'sum_duration' => [ # for summary formats, the CDR is a fictitious object containing the # total billsec and the phone number of the service - 'src', + $src_sub, sub { my($cdr, %opt) = @_; $opt{ratename} }, sub { my($cdr, %opt) = @_; $opt{count} }, sub { my($cdr, %opt) = @_; int($opt{seconds}/60).'m' }, $price_sub, ], 'sum_count' => [ - 'src', + $src_sub, sub { my($cdr, %opt) = @_; $opt{ratename} }, sub { my($cdr, %opt) = @_; $opt{count} }, $price_sub, @@ -1240,7 +1296,7 @@ sub export_formats { $price_sub, ], ); - $export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ]; + $export_formats{'source_default'} = [ $src_sub, @{ $export_formats{'default'} }, ]; $export_formats{'accountcode_default'} = [ @{ $export_formats{'default'} }[0,1], 'accountcode', @@ -1248,7 +1304,7 @@ sub export_formats { ]; my @default = @{ $export_formats{'default'} }; $export_formats{'description_default'} = - [ 'src', @default[0..2], + [ $src_sub, @default[0..2], sub { my($cdr, %opt) = @_; $cdr->description }, @default[4,5] ]; @@ -1286,8 +1342,6 @@ sub downstream_csv { #$opt{'money_char'} ||= $conf->config('money_char') || '$'; $opt{'money_char'} ||= FS::Conf->new->config('money_char') || '$'; - eval "use Text::CSV_XS;"; - die $@ if $@; my $csv = new Text::CSV_XS; my @columns = @@ -1578,6 +1632,11 @@ my %import_options = ( keys %cdr_info }, + 'format_asn_formats' => + { map { $_ => $cdr_info{$_}->{'asn_format'}; } + keys %cdr_info + }, + 'format_row_callbacks' => { map { $_ => $cdr_info{$_}->{'row_callback'}; } keys %cdr_info }, diff --git a/FS/FS/cdr/asterisk_skip_clid.pm b/FS/FS/cdr/asterisk_skip_clid.pm new file mode 100644 index 000000000..1a105b399 --- /dev/null +++ b/FS/FS/cdr/asterisk_skip_clid.pm @@ -0,0 +1,45 @@ +package FS::cdr::asterisk_skip_clid; + +use strict; +use vars qw(@ISA %info); +use FS::cdr qw(_cdr_date_parser_maker); + +@ISA = qw(FS::cdr); + +#http://www.the-asterisk-book.com/unstable/funktionen-cdr.html +my %amaflags = ( + DEFAULT => 0, + OMIT => 1, #asterisk 1.4+ + IGNORE => 1, #asterisk 1.2 + BILLING => 2, #asterisk 1.4+ + BILL => 2, #asterisk 1.2 + DOCUMENTATION => 3, + #? '' => 0, +); + +%info = ( + 'name' => 'Asterisk (skip Caller ID)', + 'weight' => 11, + 'import_fields' => [ + 'accountcode', + 'src', + 'dst', + 'dcontext', + 'SKIP_clid', + 'channel', + 'dstchannel', + 'lastapp', + 'lastdata', + _cdr_date_parser_maker('startdate'), + _cdr_date_parser_maker('answerdate'), + _cdr_date_parser_maker('enddate'), + 'duration', + 'billsec', + 'disposition', + sub { my($cdr, $amaflags) = @_; $cdr->amaflags($amaflags{$amaflags}); }, + 'uniqueid', + 'userfield', + ], +); + +1; diff --git a/FS/FS/cdr/gsm_tap3_12.pm b/FS/FS/cdr/gsm_tap3_12.pm new file mode 100644 index 000000000..275e7b35c --- /dev/null +++ b/FS/FS/cdr/gsm_tap3_12.pm @@ -0,0 +1,2079 @@ +package FS::cdr::gsm_tap3_12; +use base qw( FS::cdr ); + +use strict; +use vars qw( %info %TZ ); +use Time::Local; +#use Data::Dumper; + +#false laziness w/huawei_softx3000.pm +%TZ = ( + '+0000' => 'XXX-0', + '+0100' => 'XXX-1', + '+0200' => 'XXX-2', + '+0300' => 'XXX-3', + '+0400' => 'XXX-4', + '+0500' => 'XXX-5', + '+0600' => 'XXX-6', + '+0700' => 'XXX-7', + '+0800' => 'XXX-8', + '+0900' => 'XXX-9', + '+1000' => 'XXX-10', + '+1100' => 'XXX-11', + '+1200' => 'XXX-12', + '-0000' => 'XXX+0', + '-0100' => 'XXX+1', + '-0200' => 'XXX+2', + '-0300' => 'XXX+3', + '-0400' => 'XXX+4', + '-0500' => 'XXX+5', + '-0600' => 'XXX+6', + '-0700' => 'XXX+7', + '-0800' => 'XXX+8', + '-0900' => 'XXX+9', + '-1000' => 'XXX+10', + '-1100' => 'XXX+11', + '-1200' => 'XXX+12', +); + +%info = ( + 'name' => 'GSM TAP3 release 12', + 'weight' => 50, + 'type' => 'asn.1', + 'import_fields' => [], + 'asn_format' => { + 'spec' => _asn_spec(), + 'macro' => 'TransferBatch', #XXX & skip the Notification ones? + 'header_buffer' => sub { + my $TransferBatch = shift; + + my $networkInfo = $TransferBatch->{networkInfo}; + + my $recEntityInfo = $networkInfo->{recEntityInfo}; + my %recEntity = map { $_->{recEntityCode} => $_->{recEntityId} } @$recEntityInfo; + + my $utcTimeOffsetInfo = $networkInfo->{utcTimeOffsetInfo}; + my %utcTimeOffset = map { $_->{utcTimeOffsetCode} => $_->{utcTimeOffset} } @$utcTimeOffsetInfo; + + { recEntity => \%recEntity, + utcTimeOffset => \%utcTimeOffset, + tapDecimalPlaces => $TransferBatch->{accountingInfo}{tapDecimalPlaces}, + }; + }, + 'arrayref' => sub { shift->{'callEventDetails'}; }, + 'map' => { + 'startdate' => sub { my($row, $buffer) = @_; + my $callinfo = $row->{mobileOriginatedCall}{basicCallInformation}; + my $timestamp = $callinfo->{callEventStartTimeStamp}; + + my $localTimeStamp = $timestamp->{localTimeStamp}; + $localTimeStamp =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/ + or die "unparsable timestamp: $localTimeStamp\n"; #. Dumper($callinfo); + my($year, $mon, $day, $hour, $min, $sec) = ($1, $2, $3, $4, $5, $6); + + my $utcTimeOffsetCode = $timestamp->{utcTimeOffsetCode}; + my $utcTimeOffset = $buffer->{utcTimeOffset}{ $utcTimeOffsetCode }; + local($ENV{TZ}) = $TZ{ $utcTimeOffset }; + + timelocal($sec, $min, $hour, $day, $mon-1, $year); + }, + 'duration' => sub { shift->{mobileOriginatedCall}{basicCallInformation}{totalCallEventDuration} }, + 'billsec' => sub { shift->{mobileOriginatedCall}{basicCallInformation}{totalCallEventDuration} }, #same.. + 'src' => sub { shift->{mobileOriginatedCall}{basicCallInformation}{chargeableSubscriber}{simChargeableSubscriber}{msisdn} }, + 'charged_party_imsi' => sub { shift->{mobileOriginatedCall}{basicCallInformation}{chargeableSubscriber}{simChargeableSubscriber}{imsi} }, + 'dst' => sub { shift->{mobileOriginatedCall}{basicCallInformation}{destination}{calledNumber} }, #dialledDigits? + 'carrierid' => sub { my( $row, $buffer ) = @_; + my $recEntityCode = $row->{mobileOriginatedCall}{locationInformation}{networkLocation}{recEntityCode}; + $buffer->{recEntity}{ $recEntityCode }; + }, + 'userfield' => sub { shift->{mobileOriginatedCall}{operatorSpecInformation}[0] }, + 'servicecode' => sub { shift->{mobileOriginatedCall}{basicServiceUsedList}[0]{basicService}{serviceCode}{teleServiceCode} }, + 'upstream_price' => sub { my($row, $buffer) = @_; + sprintf('%.'.$buffer->{tapDecimalPlaces}.'f', + $row->{mobileOriginatedCall}{basicServiceUsedList}[0]{chargeInformationList}[0]{chargeDetailList}[0]{charge} + / ( 10 ** $buffer->{tapDecimalPlaces} ) + ) + }, + 'calltypenum' => sub { shift->{mobileOriginatedCall}{basicServiceUsedList}[0]{chargeInformationList}[0]{callTypeGroup}{callTypelevel1} }, + 'quantity' => sub { shift->{mobileOriginatedCall}{basicServiceUsedList}[0]{chargeInformationList}[0]{chargedUnits} }, + 'quantity_able' => sub { shift->{mobileOriginatedCall}{basicServiceUsedList}[0]{chargeInformationList}[0]{chargeableUnits} }, + }, + }, +); + +#accepts qsearch parameters as a hash or list of name/value pairs, but not +#old-style qsearch('cdr', { field=>'value' }) + +use Date::Format; +use FS::Conf; +sub tap3_12_export { + my %qsearch = (); + if ( ref($_[0]) eq 'HASH' ) { + %qsearch = %{ $_[0] }; + } else { + %qsearch = @_; + } + + #if these get huge we might need to get a count and do a paged search + my @cdrs = qsearch({ 'table'=>'cdr', %qsearch, 'order_by'=>'calldate ASC' }); + + my $conf = new FS::Conf; + + eval "use Convert::ASN1"; + die $@ if $@; + + my $asn = Convert::ASN1->new; + $asn->prepare( _asn_spec() ) or die $asn->error; + + my $TransferBatch = $asn->find('TransferBatch') or die $asn->error; + + my %hash = _TransferBatch(); #static information etc. + + my $now = time; + my $utcTimeOffset = time2str('%z', $now); + + ### + # accountingInfo + ### + + #mandatory + $hash{localCurrency} = $conf->config('currency') || 'USD'; + + ### + # batchControlInfo + ### + + #optional + $hash{batchControlInfo}->{fileCreationTimeStamp} = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $now), + 'utcTimeOffset' => $utcTimeOffset, + }; + + #The timestamp used to select calls for transfer. All call records available prior to the timestamp are transferred. + # This gives an indication to the HPMN as to how ‘up-to-date’ the information is. + $hash{batchControlInfo}->{transferCutOffTimeStamp} = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $cdrs[-1]->calldate_unix ), + 'utcTimeOffset' => $utcTimeOffset, + }; + + #The date and time at which the file was made available to the Recipient PMN. + # Physically this will normally be the timestamp when the file transfer + # commenced to the Recipient PMN, i.e. start of push, however on some systems + # this will be the timestamp when the file was made available to be pulled. + $hash{batchControlInfo}->{fileAvailableTimeStamp} = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $now), + 'utcTimeOffset' => $utcTimeOffset, + }; + + # A unique identifier used to determine the network which is the Sender of the data. + # The full list of codes in use is given in TADIG PRD TD.13: PMN Naming Conventions. + $hash{batchControlInfo}->{sender} = $conf->config('cdr-gsm_tap3-sender') || 'ZZZZZ'; #reserved: Y*, ZO-ZZ + + #XXX customer or agent field of some sort + # A unique identifier used to determine which network the data is being sent to, + # i.e. the Recipient. + # Derivation: GSM Association PRD TD.13: PMN Naming Conventions. + $hash{batchControlInfo}->{recipient} = 'GNQHT'; + + #XXX + #A unique reference which identifies each TAP Data Interchange sent by one PMN to another, specific, PMN. + # The sequence commences at 1 and is incremented by one for each subsequent TAP Data Interchange sent by the Sender PMN to a particular Recipient PMN. + # Separate sequence numbering must be used for Test Data and Chargeable Data. Having reached the maximum value (99999) the number must recycle to 1. + $hash{batchControlInfo}->{fileSequenceNumber} = '00178'; + + ### + # networkInfo + ### + + $hash{networkInfo}->{utcTimeOffsetInfo}[0]{utcTimeOffset} = $utcTimeOffset; + + #XXX recording entity IDs, referenced by recEntityCode + #$hash->{networkInfo}->{recEntityInfo}[0]{recEntityId} = '340010100'; + #$hash->{networkInfo}->{recEntityInfo}[1]{recEntityId} = '240556000000'; + + ### + # auditControlInfo + ### + + #mandatory + $hash{auditControlInfo}->{callEventDetailsCount} = scalar(@cdrs); + + #these two are optional + $hash{auditControlInfo}->{earliestCallTimeStamp} = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $cdrs[0]->calldate_unix), + 'utcTimeOffset' => $utcTimeOffset, + }; + $hash{auditControlInfo}->{latestCallTimeStamp} = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $cdrs[-1]->calldate_unix), + 'utcTimeOffset' => $utcTimeOffset, + }; + + #mandatory + my $totalCharge = 0; + $totalCharge += $_->rated_price foreach @cdrs; + $hash{totalCharge} = sprintf('%.5f', $totalCharge); + + ### + # callEventDetails + ### + + $hash{callEventDetails} = [ map tap3_12_export_cdr($_), @cdrs ]; + + ### + + $TransferBatch->encode( \%hash ); + +} + +sub _TransferBatch { + + #accounting related information + 'accountingInfo' => { + #mandatory + #'localCurrency' => 'USD', + 'tapDecimalPlaces' => 5, + 'currencyConversionInfo' => [ + { + 'numberOfDecimalPlaces' => 5, + 'exchangeRate' => 152549, #XXX ??? "exchange rate +VAT" ? + 'exchangeRateCode' => 1 + } + ], + #optional: may conditionally include taxation and discounting tables, and, optionally, TAP currency + }, + + 'batchControlInfo' => { + #mandatory + 'specificationVersionNumber' => 3, + 'releaseVersionNumber' => 12, + + #'sender' => 'MDGTM', + #'recipient' => 'GNQHT', + #'fileSequenceNumber' => '00178', + + #'transferCutOffTimeStamp' => { + # 'localTimeStamp' => '20121230050222', + # 'utcTimeOffset' => '+0300' + # }, + #'fileAvailableTimeStamp' => { + # 'localTimeStamp' => '20121230035052', + # 'utcTimeOffset' => '+0100' + # } + + #optional + #'fileCreationTimeStamp' => { + # 'localTimeStamp' => '20121230050222', + # 'utcTimeOffset' => '+0300' + # }, + + #optional: file type indicator which will only be present where the file represents test data + #optional: RAP File Sequence Number (used where the batch has previously been returned with a fatal error and is now being resubmitted) (not fileSequenceNumber?) + + #optional: beyond the scope of TAP and has been bilaterally agreed + #'operatorSpecInformation' => [ + # '', # '|File proc MTH LUXMA: 1285348027|' Operator Specific Information + # # probably just leave out + # ], + + + }, + + #Network Information is a group of related information which pertains to the Sender PMN + 'networkInfo' => { + #must be present where Recording Entity Codes are present within the TAP file + 'recEntityInfo' => [ + { + 'recEntityCode' => 1, + 'recEntityType' => 1, #MSC + #'recEntityId' => '340010100', + }, + { + 'recEntityCode' => 2, + 'recEntityType' => 2, #SMSC + #'recEntityId' => '240556000000', + }, + ], + #mandatory + 'utcTimeOffsetInfo' => [ + { + 'utcTimeOffsetCode' => 1, + #'utcTimeOffset' => '+0300', + } + ] + }, + + #identifies the end of the Transfer Batch + 'auditControlInfo' => { + #mandatory + #'callEventDetailsCount' => 4, + 'totalTaxValue' => 0, + 'totalDiscountValue' => 0, + #'totalCharge' => 50474, + + #these two are optional + #'earliestCallTimeStamp' => { + # 'localTimeStamp' => '20121229102501', + # 'utcTimeOffset' => '+0300' + # }, + #'latestCallTimeStamp' => { + # 'localTimeStamp' => '20121229102807', + # 'utcTimeOffset' => '+0300' + # } + #optional: beyond the scope of TAP and has been bilaterally agreed + #'operatorSpecInformation' => [ + # '', + # ], + }, +} + +sub tap3_12_export_cdr { + my $self = shift; + + #one of Mobile Originated Call, Mobile Terminated Call, Mobile Session, Messaging Event, Supplementary Service Event, Service Centre Usage, GPRS Call, Content Transaction or Location Service + # Each occurrence must have no more than one of these present + + { #either tele or bearer service usage originated by the mobile subscription (others?) + 'mobileOriginatedCall' => { + + #identifies the Network Location, which includes the MSC responsible for handling + # the call and, where appropriate, the Geographical Location of the mobile + 'locationInformation' => { + 'networkLocation' => { + 'recEntityCode' => $self->carrierid, #XXX Recording Entity (per 2.5, from "Reference Tables") + } + }, + + #Operator Specific Information: beyond the scope of TAP and has been bilaterally agreed + 'operatorSpecInformation' => [ + $self->userfield, ##'|Seq: 178 Loc: 1|' + ], + + #The type of service used together with all related charging information + 'basicServiceUsedList' => [ + { + #identifies the actual Basic Service used + 'basicService' => { + #one of Teleservice Code or Bearer Service Code as determined by the service type used + 'serviceCode' => { + #XXX + #00 All teleservices + #10 All Speech transmission services + #11 Telephony + #12 Emergency calls + #20 All SMS Services + #21 Short Message MT/PP + #22 Short Message MO/PP + #60 All Fax Services + #61 Facsimile Group 3 & alternative speech + #62 Automatic Facsimile Group 3 + #63 Automatic Facsimile Group 4 + #70 All data teleservices (compound) + #80 All teleservices except SMS (compound) + #90 All voice group call services + #91 Voice group call + #92 Voice broadcast call + 'teleServiceCode' => $self->servicecode, #'11' + + #Bearer Service Code + # Must be present within group Service Code where the type of service used + # was a bearer service. Must not be present when the type of service used + # was a tele service and, therefore, Teleservice Code is present. + # Group Bearer Codes, identifiable by the description ‘All’, should only + # be used where details of the specific services affected are not + # available from the network. + #00 All Bearer Services + #20 All Data Circuit Asynchronous Services + #21 Duplex Asynch. 300bps data circuit + #22 Duplex Asynch. 1200bps data circuit + #23 Duplex Asynch. 1200/75bps data circuit + #24 Duplex Asynch. 2400bps data circuit + #25 Duplex Asynch. 4800bps data circuit + #26 Duplex Asynch. 9600bps data circuit + #27 General Data Circuit Asynchronous Service + #30 All Data Circuit Synchronous Services + #32 Duplex Synch. 1200bps data circuit + #34 Duplex Synch. 2400bps data circuit + #35 Duplex Synch. 4800bps data circuit + #36 Duplex Synch. 9600bps data circuit + #37 General Data Circuit Synchronous Service + #40 All Dedicated PAD Access Services + #41 Duplex Asynch. 300bps PAD access + #42 Duplex Asynch. 1200bps PAD access + #43 Duplex Asynch. 1200/75bps PAD access + #44 Duplex Asynch. 2400bps PAD access + #45 Duplex Asynch. 4800bps PAD access + #46 Duplex Asynch. 9600bps PAD access + #47 General PAD Access Service + #50 All Dedicated Packet Access Services + #54 Duplex Synch. 2400bps PAD access + #55 Duplex Synch. 4800bps PAD access + #56 Duplex Synch. 9600bps PAD access + #57 General Packet Access Service + #60 All Alternat Speech/Asynchronous Services + #70 All Alternate Speech/Synchronous Services + #80 All Speech followed by Data Asynchronous Services + #90 All Speech followed by Data Synchronous Services + #A0 All Data Circuit Asynchronous Services (compound) + #B0 All Data Circuit Synchronous Services (compound) + #C0 All Asynchronous Services (compound) + } + #conditionally also contain the following for UMTS: Transparency Indicator, Fixed Network User + # Rate, User Protocol Indicator, Guaranteed Bit Rate and Maximum Bit Rate + }, + + #Charge information is provided for all chargeable elements except within Messaging Event and Mobile Session call events + # must contain Charged Item and at least one occurrence of Charge Detail + 'chargeInformationList' => [ + { + #XXX + #mandatory + # the charging principle applied and the unitisation of Chargeable Units. It + # is not intended to identify the service used. + #A: Call set up attempt + #C: Content + #D: Duration based charge + #E: Event based charge + #F: Fixed (one-off) charge + #L: Calendar (for example daily usage charge) + #V: Volume (outgoing) based charge + #W: Volume (incoming) based charge + #X: Volume (total volume) based charge + #(?? fields to be used as a basis for the calculation of the correct Charge + # A: Chargeable Units (if present) + # D,V,W,X: Chargeable Units + # C: Depends on the content + # E: Not Applicable + # F: Not Applicable + # L: Call Event Start Timestamp) + 'chargedItem' => 'D', + + # the IOT used by the VPMN to price the call + 'callTypeGroup' => { + + #The highest category call type in respect of the destination of the call + #0: Unknown/Not Applicable + #1: National + #2: International + #10: HGGSN/HP-GW + #11: VGGSN/VP-GW + #12: Other GGSN/Other P-GW + #100: WLAN + 'callTypeLevel1' => $self->calltypenum, + + #the sub category of Call Type Level 1 + #0: Unknown/Not Applicable + #1: Mobile + #2: PSTN + #3: Non Geographic + #4: Premium Rate + #5: Satellite destination + #6: Forwarded call + #7: Non forwarded call + #10: Broadband + #11: Narrowband + #12: Conversational + #13: Streaming + #14: Interactive + #15: Background + 'callTypeLevel2' => 0, + + #the sub category of Call Type Level 2 + 'callTypeLevel3' => 0, + }, + + #mandatory, at least one occurence must be present + #A repeating group detailing the Charge and/or charge element + # Note that, where a Charge has been levied, even where that Charge is zero, + # there must be one occurance, and only one, with a Charge Type of '00' + 'chargeDetailList' => [ + { + #mandatory + # after discounts have been deducted but before any tax is added + 'charge' => $self->rated_price * 100000, #XXX numberOfDecimalPlaces + + #mandatory + # the type of charge represented + #00: Total charge for Charge Information (the invoiceable value) + #01: Airtime charge + #02: reserved + #03: Toll charge + #04: Directory assistance + #05–20: reserved + #21: VPMN surcharge + #50: Total charge for Charge Information according to the published IOT + # Note that the use of value 50 is only for use by bilateral agreement, use without + # bilateral agreement can be treated as per reserved values, that is ‘out of range’ + #69–99: reserved + 'chargeType' => '00', + + #conditional + # the number of units which are chargeable within the Charge Detail, this may not + # correspond to the number of rounded units charged. + # The item Charged Item defines what the units represent. + 'chargeableUnits' => $self->quantity_able, + + #optional + # the rounded number of units which are actually charged for + 'chargedUnits' => $self->quantity, + } + ], + 'exchangeRateCode' => 1, #from header + } + ] + } + ], + + #MO Basic Call Information provides the basic detail of who made the call and where to in respect of mobile originated traffic. + 'basicCallInformation' => { + #mandatory + # the identification of the chargeable subscriber. + # The group must contain either the IMSI or the MIN of the Chargeable Subscriber, but not both. + 'chargeableSubscriber' => { + 'simChargeableSubscriber' => { + 'msisdn' => $self->charged_party, #src + 'imsi' => $self->charged_party_imsi, + } + }, + # the start of the call event + 'callEventStartTimeStamp' => { + 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $self->startdate), + 'utcTimeOffsetCode' => 1 + }, + + # the actual total duration of a call event as a number of seconds + 'totalCallEventDuration' => $self->duration, + + #conditional + # the number dialled by the subscriber (Called Number) + # or the SMSC Address in case of SMS usage or in cases involving supplementary services + # such as call forwarding or transfer etc., the number to which the call is routed + 'destination' => { + #the international representation of the destination + 'calledNumber' => $self->dst, + + #the actual digits as dialled by the subscriber, i.e. unmodified, in establishing a call + # This will contain ‘+’ and ‘#’ where appropriate. + #'dialledDigits' => '322221350' + }, + } + } + }; + +} + +sub _asn_spec { + <<'END'; +-- +-- +-- The following ASN.1 specification defines the abstract syntax for +-- +-- Data Record Format Version 03 +-- Release 12 +-- +-- The specification is structured as follows: +-- (1) structure of the Tap batch +-- (2) definition of the individual Tap ‘records’ +-- (3) Tap data items and groups of data items used within (2) +-- (4) Common, non-Tap data types +-- (5) Tap data items for content charging +-- +-- It is mainly a translation from the logical structure +-- diagrams. Where appropriate, names used within the +-- logical structure diagrams have been shortened. +-- For repeating data items the name as used within the logical +-- structure have been extended by adding ‘list’ or ‘table’ +-- (in some instances). +-- + + +-- TAP-0312 DEFINITIONS IMPLICIT TAGS ::= + +-- BEGIN + +-- +-- Structure of a Tap batch +-- + +DataInterChange ::= CHOICE +{ + transferBatch TransferBatch, + notification Notification, +... +} + +-- Batch Control Information must always, both logically and physically, +-- be the first group/item within Transfer Batch – this ensures that the +-- TAP release version can be readily identified. Any new groups/items +-- required may be inserted at any point after Batch Control Information + +TransferBatch ::= [APPLICATION 1] SEQUENCE +{ + batchControlInfo BatchControlInfo OPTIONAL, -- *m.m. + accountingInfo AccountingInfo OPTIONAL, + networkInfo NetworkInfo OPTIONAL, -- *m.m. + messageDescriptionInfo MessageDescriptionInfoList OPTIONAL, + callEventDetails CallEventDetailList OPTIONAL, -- *m.m. + auditControlInfo AuditControlInfo OPTIONAL, -- *m.m. +... +} + +Notification ::= [APPLICATION 2] SEQUENCE +{ + sender Sender OPTIONAL, -- *m.m. + recipient Recipient OPTIONAL, -- *m.m. + fileSequenceNumber FileSequenceNumber OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + fileCreationTimeStamp FileCreationTimeStamp OPTIONAL, + fileAvailableTimeStamp FileAvailableTimeStamp OPTIONAL, -- *m.m. + transferCutOffTimeStamp TransferCutOffTimeStamp OPTIONAL, -- *m.m. + specificationVersionNumber SpecificationVersionNumber OPTIONAL, -- *m.m. + releaseVersionNumber ReleaseVersionNumber OPTIONAL, -- *m.m. + fileTypeIndicator FileTypeIndicator OPTIONAL, + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +CallEventDetailList ::= [APPLICATION 3] SEQUENCE OF CallEventDetail + +CallEventDetail ::= CHOICE +{ + mobileOriginatedCall MobileOriginatedCall, + mobileTerminatedCall MobileTerminatedCall, + supplServiceEvent SupplServiceEvent, + serviceCentreUsage ServiceCentreUsage, + gprsCall GprsCall, + contentTransaction ContentTransaction, + locationService LocationService, + messagingEvent MessagingEvent, + mobileSession MobileSession, +... +} + +-- +-- Structure of the individual Tap records +-- + +BatchControlInfo ::= [APPLICATION 4] SEQUENCE +{ + sender Sender OPTIONAL, -- *m.m. + recipient Recipient OPTIONAL, -- *m.m. + fileSequenceNumber FileSequenceNumber OPTIONAL, -- *m.m. + fileCreationTimeStamp FileCreationTimeStamp OPTIONAL, + transferCutOffTimeStamp TransferCutOffTimeStamp OPTIONAL, -- *m.m. + fileAvailableTimeStamp FileAvailableTimeStamp OPTIONAL, -- *m.m. + specificationVersionNumber SpecificationVersionNumber OPTIONAL, -- *m.m. + releaseVersionNumber ReleaseVersionNumber OPTIONAL, -- *m.m. + fileTypeIndicator FileTypeIndicator OPTIONAL, + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +AccountingInfo ::= [APPLICATION 5] SEQUENCE +{ + taxation TaxationList OPTIONAL, + discounting DiscountingList OPTIONAL, + localCurrency LocalCurrency OPTIONAL, -- *m.m. + tapCurrency TapCurrency OPTIONAL, + currencyConversionInfo CurrencyConversionList OPTIONAL, + tapDecimalPlaces TapDecimalPlaces OPTIONAL, -- *m.m. +... +} + +NetworkInfo ::= [APPLICATION 6] SEQUENCE +{ + utcTimeOffsetInfo UtcTimeOffsetInfoList OPTIONAL, -- *m.m. + recEntityInfo RecEntityInfoList OPTIONAL, +... +} + +MessageDescriptionInfoList ::= [APPLICATION 8] SEQUENCE OF MessageDescriptionInformation + +MobileOriginatedCall ::= [APPLICATION 9] SEQUENCE +{ + basicCallInformation MoBasicCallInformation OPTIONAL, -- *m.m. + locationInformation LocationInformation OPTIONAL, -- *m.m. + equipmentIdentifier ImeiOrEsn OPTIONAL, + basicServiceUsedList BasicServiceUsedList OPTIONAL, -- *m.m. + supplServiceCode SupplServiceCode OPTIONAL, + thirdPartyInformation ThirdPartyInformation OPTIONAL, + camelServiceUsed CamelServiceUsed OPTIONAL, + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +MobileTerminatedCall ::= [APPLICATION 10] SEQUENCE +{ + basicCallInformation MtBasicCallInformation OPTIONAL, -- *m.m. + locationInformation LocationInformation OPTIONAL, -- *m.m. + equipmentIdentifier ImeiOrEsn OPTIONAL, + basicServiceUsedList BasicServiceUsedList OPTIONAL, -- *m.m. + camelServiceUsed CamelServiceUsed OPTIONAL, + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + + +SupplServiceEvent ::= [APPLICATION 11] SEQUENCE +{ + chargeableSubscriber ChargeableSubscriber OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + locationInformation LocationInformation OPTIONAL, -- *m.m. + equipmentIdentifier ImeiOrEsn OPTIONAL, + supplServiceUsed SupplServiceUsed OPTIONAL, -- *m.m. + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + + +ServiceCentreUsage ::= [APPLICATION 12] SEQUENCE +{ + basicInformation ScuBasicInformation OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + servingNetwork ServingNetwork OPTIONAL, + recEntityCode RecEntityCode OPTIONAL, -- *m.m. + chargeInformation ChargeInformation OPTIONAL, -- *m.m. + scuChargeType ScuChargeType OPTIONAL, -- *m.m. + scuTimeStamps ScuTimeStamps OPTIONAL, -- *m.m. + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +GprsCall ::= [APPLICATION 14] SEQUENCE +{ + gprsBasicCallInformation GprsBasicCallInformation OPTIONAL, -- *m.m. + gprsLocationInformation GprsLocationInformation OPTIONAL, -- *m.m. + equipmentIdentifier ImeiOrEsn OPTIONAL, + gprsServiceUsed GprsServiceUsed OPTIONAL, -- *m.m. + camelServiceUsed CamelServiceUsed OPTIONAL, + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +ContentTransaction ::= [APPLICATION 17] SEQUENCE +{ + contentTransactionBasicInfo ContentTransactionBasicInfo OPTIONAL, -- *m.m. + chargedPartyInformation ChargedPartyInformation OPTIONAL, -- *m.m. + servingPartiesInformation ServingPartiesInformation OPTIONAL, -- *m.m. + contentServiceUsed ContentServiceUsedList OPTIONAL, -- *m.m. + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +LocationService ::= [APPLICATION 297] SEQUENCE +{ + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + recEntityCode RecEntityCode OPTIONAL, -- *m.m. + callReference CallReference OPTIONAL, + trackingCustomerInformation TrackingCustomerInformation OPTIONAL, + lCSSPInformation LCSSPInformation OPTIONAL, + trackedCustomerInformation TrackedCustomerInformation OPTIONAL, + locationServiceUsage LocationServiceUsage OPTIONAL, -- *m.m. + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +MessagingEvent ::= [APPLICATION 433] SEQUENCE +{ + messagingEventService MessagingEventService OPTIONAL, -- *m.m. + chargedParty ChargedParty OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + simToolkitIndicator SimToolkitIndicator OPTIONAL, + geographicalLocation GeographicalLocation OPTIONAL, + eventReference EventReference OPTIONAL, -- *m.m. + + recEntityCodeList RecEntityCodeList OPTIONAL, -- *m.m. + networkElementList NetworkElementList OPTIONAL, + locationArea LocationArea OPTIONAL, + cellId CellId OPTIONAL, + serviceStartTimestamp ServiceStartTimestamp OPTIONAL, -- *m.m. + nonChargedParty NonChargedParty OPTIONAL, + exchangeRateCode ExchangeRateCode OPTIONAL, + callTypeGroup CallTypeGroup OPTIONAL, -- *m.m. + charge Charge OPTIONAL, -- *m.m. + taxInformationList TaxInformationList OPTIONAL, + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +MobileSession ::= [APPLICATION 434] SEQUENCE +{ + mobileSessionService MobileSessionService OPTIONAL, -- *m.m. + chargedParty ChargedParty OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + simToolkitIndicator SimToolkitIndicator OPTIONAL, + geographicalLocation GeographicalLocation OPTIONAL, + locationArea LocationArea OPTIONAL, + cellId CellId OPTIONAL, + eventReference EventReference OPTIONAL, -- *m.m. + + recEntityCodeList RecEntityCodeList OPTIONAL, -- *m.m. + serviceStartTimestamp ServiceStartTimestamp OPTIONAL, -- *m.m. + causeForTerm CauseForTerm OPTIONAL, + totalCallEventDuration TotalCallEventDuration OPTIONAL, -- *m.m. + nonChargedParty NonChargedParty OPTIONAL, + sessionChargeInfoList SessionChargeInfoList OPTIONAL, -- *m.m. + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + +AuditControlInfo ::= [APPLICATION 15] SEQUENCE +{ + earliestCallTimeStamp EarliestCallTimeStamp OPTIONAL, + latestCallTimeStamp LatestCallTimeStamp OPTIONAL, + totalCharge TotalCharge OPTIONAL, -- *m.m. + totalChargeRefund TotalChargeRefund OPTIONAL, + totalTaxRefund TotalTaxRefund OPTIONAL, + totalTaxValue TotalTaxValue OPTIONAL, -- *m.m. + totalDiscountValue TotalDiscountValue OPTIONAL, -- *m.m. + totalDiscountRefund TotalDiscountRefund OPTIONAL, + totalAdvisedChargeValueList TotalAdvisedChargeValueList OPTIONAL, + callEventDetailsCount CallEventDetailsCount OPTIONAL, -- *m.m. + operatorSpecInformation OperatorSpecInfoList OPTIONAL, +... +} + + +-- +-- Tap data items and groups of data items +-- + +AccessPointNameNI ::= [APPLICATION 261] AsciiString --(SIZE(1..63)) + +AccessPointNameOI ::= [APPLICATION 262] AsciiString --(SIZE(1..37)) + +ActualDeliveryTimeStamp ::= [APPLICATION 302] DateTime + +AddressStringDigits ::= BCDString + +AdvisedCharge ::= [APPLICATION 349] Charge + +AdvisedChargeCurrency ::= [APPLICATION 348] Currency + +AdvisedChargeInformation ::= [APPLICATION 351] SEQUENCE +{ + paidIndicator PaidIndicator OPTIONAL, + paymentMethod PaymentMethod OPTIONAL, + advisedChargeCurrency AdvisedChargeCurrency OPTIONAL, + advisedCharge AdvisedCharge OPTIONAL, -- *m.m. + commission Commission OPTIONAL, +... +} + +AgeOfLocation ::= [APPLICATION 396] INTEGER + +BasicService ::= [APPLICATION 36] SEQUENCE +{ + serviceCode BasicServiceCode OPTIONAL, -- *m.m. + transparencyIndicator TransparencyIndicator OPTIONAL, + fnur Fnur OPTIONAL, + userProtocolIndicator UserProtocolIndicator OPTIONAL, + guaranteedBitRate GuaranteedBitRate OPTIONAL, + maximumBitRate MaximumBitRate OPTIONAL, +... +} + +BasicServiceCode ::= [APPLICATION 426] CHOICE +{ + teleServiceCode TeleServiceCode, + bearerServiceCode BearerServiceCode, +... +} + +BasicServiceCodeList ::= [APPLICATION 37] SEQUENCE OF BasicServiceCode + +BasicServiceUsed ::= [APPLICATION 39] SEQUENCE +{ + basicService BasicService OPTIONAL, -- *m.m. + chargingTimeStamp ChargingTimeStamp OPTIONAL, + chargeInformationList ChargeInformationList OPTIONAL, -- *m.m. + hSCSDIndicator HSCSDIndicator OPTIONAL, +... +} + +BasicServiceUsedList ::= [APPLICATION 38] SEQUENCE OF BasicServiceUsed + +BearerServiceCode ::= [APPLICATION 40] HexString --(SIZE(2)) + +EventReference ::= [APPLICATION 435] AsciiString + + +CalledNumber ::= [APPLICATION 407] AddressStringDigits + +CalledPlace ::= [APPLICATION 42] AsciiString + +CalledRegion ::= [APPLICATION 46] AsciiString + +CallEventDetailsCount ::= [APPLICATION 43] INTEGER + +CallEventStartTimeStamp ::= [APPLICATION 44] DateTime + +CallingNumber ::= [APPLICATION 405] AddressStringDigits + +CallOriginator ::= [APPLICATION 41] SEQUENCE +{ + callingNumber CallingNumber OPTIONAL, + clirIndicator ClirIndicator OPTIONAL, + sMSOriginator SMSOriginator OPTIONAL, +... +} + +CallReference ::= [APPLICATION 45] OCTET STRING --(SIZE(1..8)) + +CallTypeGroup ::= [APPLICATION 258] SEQUENCE +{ + callTypeLevel1 CallTypeLevel1 OPTIONAL, -- *m.m. + callTypeLevel2 CallTypeLevel2 OPTIONAL, -- *m.m. + callTypeLevel3 CallTypeLevel3 OPTIONAL, -- *m.m. +... +} + +CallTypeLevel1 ::= [APPLICATION 259] INTEGER + +CallTypeLevel2 ::= [APPLICATION 255] INTEGER + +CallTypeLevel3 ::= [APPLICATION 256] INTEGER + +CamelDestinationNumber ::= [APPLICATION 404] AddressStringDigits + +CamelInvocationFee ::= [APPLICATION 422] AbsoluteAmount + +CamelServiceKey ::= [APPLICATION 55] INTEGER + +CamelServiceLevel ::= [APPLICATION 56] INTEGER + +CamelServiceUsed ::= [APPLICATION 57] SEQUENCE +{ + camelServiceLevel CamelServiceLevel OPTIONAL, + camelServiceKey CamelServiceKey OPTIONAL, -- *m.m. + defaultCallHandling DefaultCallHandlingIndicator OPTIONAL, + exchangeRateCode ExchangeRateCode OPTIONAL, + taxInformation TaxInformationList OPTIONAL, + discountInformation DiscountInformation OPTIONAL, + camelInvocationFee CamelInvocationFee OPTIONAL, + threeGcamelDestination ThreeGcamelDestination OPTIONAL, + cseInformation CseInformation OPTIONAL, +... +} + +CauseForTerm ::= [APPLICATION 58] INTEGER + +CellId ::= [APPLICATION 59] INTEGER + +Charge ::= [APPLICATION 62] AbsoluteAmount + +ChargeableSubscriber ::= [APPLICATION 427] CHOICE +{ + simChargeableSubscriber SimChargeableSubscriber, + minChargeableSubscriber MinChargeableSubscriber, +... +} + +ChargeableUnits ::= [APPLICATION 65] INTEGER + +ChargeDetail ::= [APPLICATION 63] SEQUENCE +{ + chargeType ChargeType OPTIONAL, -- *m.m. + charge Charge OPTIONAL, -- *m.m. + chargeableUnits ChargeableUnits OPTIONAL, + chargedUnits ChargedUnits OPTIONAL, + chargeDetailTimeStamp ChargeDetailTimeStamp OPTIONAL, +... +} + +ChargeDetailList ::= [APPLICATION 64] SEQUENCE OF ChargeDetail + +ChargeDetailTimeStamp ::= [APPLICATION 410] ChargingTimeStamp + +ChargedItem ::= [APPLICATION 66] AsciiString --(SIZE(1)) + +ChargedParty ::= [APPLICATION 436] SEQUENCE +{ + imsi Imsi OPTIONAL, -- *m.m. + msisdn Msisdn OPTIONAL, + publicUserId PublicUserId OPTIONAL, + homeBid HomeBid OPTIONAL, + homeLocationDescription HomeLocationDescription OPTIONAL, + imei Imei OPTIONAL, +... +} + +ChargedPartyEquipment ::= [APPLICATION 323] SEQUENCE +{ + equipmentIdType EquipmentIdType OPTIONAL, -- *m.m. + equipmentId EquipmentId OPTIONAL, -- *m.m. +... +} + +ChargedPartyHomeIdentification ::= [APPLICATION 313] SEQUENCE +{ + homeIdType HomeIdType OPTIONAL, -- *m.m. + homeIdentifier HomeIdentifier OPTIONAL, -- *m.m. +... +} + +ChargedPartyHomeIdList ::= [APPLICATION 314] SEQUENCE OF + ChargedPartyHomeIdentification + +ChargedPartyIdentification ::= [APPLICATION 309] SEQUENCE +{ + chargedPartyIdType ChargedPartyIdType OPTIONAL, -- *m.m. + chargedPartyIdentifier ChargedPartyIdentifier OPTIONAL, -- *m.m. +... +} + +ChargedPartyIdentifier ::= [APPLICATION 287] AsciiString + +ChargedPartyIdList ::= [APPLICATION 310] SEQUENCE OF ChargedPartyIdentification + +ChargedPartyIdType ::= [APPLICATION 305] INTEGER + +ChargedPartyInformation ::= [APPLICATION 324] SEQUENCE +{ + chargedPartyIdList ChargedPartyIdList OPTIONAL, -- *m.m. + chargedPartyHomeIdList ChargedPartyHomeIdList OPTIONAL, + chargedPartyLocationList ChargedPartyLocationList OPTIONAL, + chargedPartyEquipment ChargedPartyEquipment OPTIONAL, +... +} + +ChargedPartyLocation ::= [APPLICATION 320] SEQUENCE +{ + locationIdType LocationIdType OPTIONAL, -- *m.m. + locationIdentifier LocationIdentifier OPTIONAL, -- *m.m. +... +} + +ChargedPartyLocationList ::= [APPLICATION 321] SEQUENCE OF ChargedPartyLocation + +ChargedPartyStatus ::= [APPLICATION 67] INTEGER + +ChargedUnits ::= [APPLICATION 68] INTEGER + +ChargeInformation ::= [APPLICATION 69] SEQUENCE +{ + chargedItem ChargedItem OPTIONAL, -- *m.m. + exchangeRateCode ExchangeRateCode OPTIONAL, + callTypeGroup CallTypeGroup OPTIONAL, + chargeDetailList ChargeDetailList OPTIONAL, -- *m.m. + taxInformation TaxInformationList OPTIONAL, + discountInformation DiscountInformation OPTIONAL, +... +} + +ChargeInformationList ::= [APPLICATION 70] SEQUENCE OF ChargeInformation + +ChargeRefundIndicator ::= [APPLICATION 344] INTEGER + +ChargeType ::= [APPLICATION 71] NumberString --(SIZE(2..3)) + +ChargingId ::= [APPLICATION 72] INTEGER + +ChargingPoint ::= [APPLICATION 73] AsciiString --(SIZE(1)) + +ChargingTimeStamp ::= [APPLICATION 74] DateTime + +ClirIndicator ::= [APPLICATION 75] INTEGER + +Commission ::= [APPLICATION 350] Charge + +CompletionTimeStamp ::= [APPLICATION 76] DateTime + +ContentChargingPoint ::= [APPLICATION 345] INTEGER + +ContentProvider ::= [APPLICATION 327] SEQUENCE +{ + contentProviderIdType ContentProviderIdType OPTIONAL, -- *m.m. + contentProviderIdentifier ContentProviderIdentifier OPTIONAL, -- *m.m. +... +} + +ContentProviderIdentifier ::= [APPLICATION 292] AsciiString + +ContentProviderIdList ::= [APPLICATION 328] SEQUENCE OF ContentProvider + +ContentProviderIdType ::= [APPLICATION 291] INTEGER + +ContentProviderName ::= [APPLICATION 334] AsciiString + +ContentServiceUsed ::= [APPLICATION 352] SEQUENCE +{ + contentTransactionCode ContentTransactionCode OPTIONAL, -- *m.m. + contentTransactionType ContentTransactionType OPTIONAL, -- *m.m. + objectType ObjectType OPTIONAL, + transactionDescriptionSupp TransactionDescriptionSupp OPTIONAL, + transactionShortDescription TransactionShortDescription OPTIONAL, -- *m.m. + transactionDetailDescription TransactionDetailDescription OPTIONAL, + transactionIdentifier TransactionIdentifier OPTIONAL, -- *m.m. + transactionAuthCode TransactionAuthCode OPTIONAL, + dataVolumeIncoming DataVolumeIncoming OPTIONAL, + dataVolumeOutgoing DataVolumeOutgoing OPTIONAL, + totalDataVolume TotalDataVolume OPTIONAL, + chargeRefundIndicator ChargeRefundIndicator OPTIONAL, + contentChargingPoint ContentChargingPoint OPTIONAL, + chargeInformationList ChargeInformationList OPTIONAL, + advisedChargeInformation AdvisedChargeInformation OPTIONAL, +... +} + +ContentServiceUsedList ::= [APPLICATION 285] SEQUENCE OF ContentServiceUsed + +ContentTransactionBasicInfo ::= [APPLICATION 304] SEQUENCE +{ + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + orderPlacedTimeStamp OrderPlacedTimeStamp OPTIONAL, + requestedDeliveryTimeStamp RequestedDeliveryTimeStamp OPTIONAL, + actualDeliveryTimeStamp ActualDeliveryTimeStamp OPTIONAL, + totalTransactionDuration TotalTransactionDuration OPTIONAL, + transactionStatus TransactionStatus OPTIONAL, +... +} + +ContentTransactionCode ::= [APPLICATION 336] INTEGER + +ContentTransactionType ::= [APPLICATION 337] INTEGER + +CseInformation ::= [APPLICATION 79] OCTET STRING --(SIZE(1..40)) + +CurrencyConversion ::= [APPLICATION 106] SEQUENCE +{ + exchangeRateCode ExchangeRateCode OPTIONAL, -- *m.m. + numberOfDecimalPlaces NumberOfDecimalPlaces OPTIONAL, -- *m.m. + exchangeRate ExchangeRate OPTIONAL, -- *m.m. +... +} + +CurrencyConversionList ::= [APPLICATION 80] SEQUENCE OF CurrencyConversion + +CustomerIdentifier ::= [APPLICATION 364] AsciiString + +CustomerIdType ::= [APPLICATION 363] INTEGER + +DataVolume ::= INTEGER + +DataVolumeIncoming ::= [APPLICATION 250] DataVolume + +DataVolumeOutgoing ::= [APPLICATION 251] DataVolume + +-- +-- The following datatypes are used to denote timestamps. +-- Each timestamp consists of a local timestamp and a +-- corresponding UTC time offset. +-- Except for the timestamps used within the Batch Control +-- Information and the Audit Control Information +-- the UTC time offset is identified by a code referencing +-- the UtcTimeOffsetInfo. +-- + +-- +-- We start with the “short” datatype referencing the +-- UtcTimeOffsetInfo. +-- + +DateTime ::= SEQUENCE +{ + -- + -- Local timestamps are noted in the format + -- + -- CCYYMMDDhhmmss + -- + -- where CC = century (‘19’, ‘20’,...) + -- YY = year (‘00’ – ‘99’) + -- MM = month (‘01’, ‘02’, ... , ‘12’) + -- DD = day (‘01’, ‘02’, ... , ‘31’) + -- hh = hour (‘00’, ‘01’, ... , ‘23’) + -- mm = minutes (‘00’, ‘01’, ... , ‘59’) + -- ss = seconds (‘00’, ‘01’, ... , ‘59’) + -- + localTimeStamp LocalTimeStamp OPTIONAL, -- *m.m. + utcTimeOffsetCode UtcTimeOffsetCode OPTIONAL, -- *m.m. +... +} + +-- +-- The following version is the “long” datatype +-- containing the UTC time offset directly. +-- + +DateTimeLong ::= SEQUENCE +{ + localTimeStamp LocalTimeStamp OPTIONAL, -- *m.m. + utcTimeOffset UtcTimeOffset OPTIONAL, -- *m.m. +... +} + +DefaultCallHandlingIndicator ::= [APPLICATION 87] INTEGER + +DepositTimeStamp ::= [APPLICATION 88] DateTime + +Destination ::= [APPLICATION 89] SEQUENCE +{ + calledNumber CalledNumber OPTIONAL, + dialledDigits DialledDigits OPTIONAL, + calledPlace CalledPlace OPTIONAL, + calledRegion CalledRegion OPTIONAL, + sMSDestinationNumber SMSDestinationNumber OPTIONAL, +... +} + +DestinationNetwork ::= [APPLICATION 90] NetworkId + +DialledDigits ::= [APPLICATION 279] AsciiString + +Discount ::= [APPLICATION 412] DiscountValue + +DiscountableAmount ::= [APPLICATION 423] AbsoluteAmount + +DiscountApplied ::= [APPLICATION 428] CHOICE +{ + fixedDiscountValue FixedDiscountValue, + discountRate DiscountRate, +... +} + +DiscountCode ::= [APPLICATION 91] INTEGER + +DiscountInformation ::= [APPLICATION 96] SEQUENCE +{ + discountCode DiscountCode OPTIONAL, -- *m.m. + discount Discount OPTIONAL, + discountableAmount DiscountableAmount OPTIONAL, +... +} + +Discounting ::= [APPLICATION 94] SEQUENCE +{ + discountCode DiscountCode OPTIONAL, -- *m.m. + discountApplied DiscountApplied OPTIONAL, -- *m.m. +... +} + +DiscountingList ::= [APPLICATION 95] SEQUENCE OF Discounting + +DiscountRate ::= [APPLICATION 92] PercentageRate + +DiscountValue ::= AbsoluteAmount + +DistanceChargeBandCode ::= [APPLICATION 98] AsciiString --(SIZE(1)) + +EarliestCallTimeStamp ::= [APPLICATION 101] DateTimeLong + +ElementId ::= [APPLICATION 437] AsciiString + +ElementType ::= [APPLICATION 438] INTEGER + +EquipmentId ::= [APPLICATION 290] AsciiString + +EquipmentIdType ::= [APPLICATION 322] INTEGER + +Esn ::= [APPLICATION 103] NumberString + +ExchangeRate ::= [APPLICATION 104] INTEGER + +ExchangeRateCode ::= [APPLICATION 105] Code + +FileAvailableTimeStamp ::= [APPLICATION 107] DateTimeLong + +FileCreationTimeStamp ::= [APPLICATION 108] DateTimeLong + +FileSequenceNumber ::= [APPLICATION 109] NumberString --(SIZE(5)) + +FileTypeIndicator ::= [APPLICATION 110] AsciiString --(SIZE(1)) + +FixedDiscountValue ::= [APPLICATION 411] DiscountValue + +Fnur ::= [APPLICATION 111] INTEGER + +GeographicalLocation ::= [APPLICATION 113] SEQUENCE +{ + servingNetwork ServingNetwork OPTIONAL, + servingBid ServingBid OPTIONAL, + servingLocationDescription ServingLocationDescription OPTIONAL, +... +} + +GprsBasicCallInformation ::= [APPLICATION 114] SEQUENCE +{ + gprsChargeableSubscriber GprsChargeableSubscriber OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + gprsDestination GprsDestination OPTIONAL, -- *m.m. + callEventStartTimeStamp CallEventStartTimeStamp OPTIONAL, -- *m.m. + totalCallEventDuration TotalCallEventDuration OPTIONAL, -- *m.m. + causeForTerm CauseForTerm OPTIONAL, + partialTypeIndicator PartialTypeIndicator OPTIONAL, + pDPContextStartTimestamp PDPContextStartTimestamp OPTIONAL, + networkInitPDPContext NetworkInitPDPContext OPTIONAL, + chargingId ChargingId OPTIONAL, -- *m.m. +... +} + +GprsChargeableSubscriber ::= [APPLICATION 115] SEQUENCE +{ + chargeableSubscriber ChargeableSubscriber OPTIONAL, + pdpAddress PdpAddress OPTIONAL, + networkAccessIdentifier NetworkAccessIdentifier OPTIONAL, +... +} + +GprsDestination ::= [APPLICATION 116] SEQUENCE +{ + accessPointNameNI AccessPointNameNI OPTIONAL, -- *m.m. + accessPointNameOI AccessPointNameOI OPTIONAL, +... +} + +GprsLocationInformation ::= [APPLICATION 117] SEQUENCE +{ + gprsNetworkLocation GprsNetworkLocation OPTIONAL, -- *m.m. + homeLocationInformation HomeLocationInformation OPTIONAL, + geographicalLocation GeographicalLocation OPTIONAL, +... +} + +GprsNetworkLocation ::= [APPLICATION 118] SEQUENCE +{ + recEntity RecEntityCodeList OPTIONAL, -- *m.m. + locationArea LocationArea OPTIONAL, + cellId CellId OPTIONAL, +... +} + +GprsServiceUsed ::= [APPLICATION 121] SEQUENCE +{ + iMSSignallingContext IMSSignallingContext OPTIONAL, + dataVolumeIncoming DataVolumeIncoming OPTIONAL, -- *m.m. + dataVolumeOutgoing DataVolumeOutgoing OPTIONAL, -- *m.m. + chargeInformationList ChargeInformationList OPTIONAL, -- *m.m. +... +} + +GsmChargeableSubscriber ::= [APPLICATION 286] SEQUENCE +{ + imsi Imsi OPTIONAL, + msisdn Msisdn OPTIONAL, +... +} + +GuaranteedBitRate ::= [APPLICATION 420] OCTET STRING --(SIZE (1)) + +HomeBid ::= [APPLICATION 122] Bid + +HomeIdentifier ::= [APPLICATION 288] AsciiString + +HomeIdType ::= [APPLICATION 311] INTEGER + +HomeLocationDescription ::= [APPLICATION 413] LocationDescription + +HomeLocationInformation ::= [APPLICATION 123] SEQUENCE +{ + homeBid HomeBid OPTIONAL, -- *m.m. + homeLocationDescription HomeLocationDescription OPTIONAL, -- *m.m. +... +} + +HorizontalAccuracyDelivered ::= [APPLICATION 392] INTEGER + +HorizontalAccuracyRequested ::= [APPLICATION 385] INTEGER + +HSCSDIndicator ::= [APPLICATION 424] AsciiString --(SIZE(1)) + +Imei ::= [APPLICATION 128] BCDString --(SIZE(7..8)) + +ImeiOrEsn ::= [APPLICATION 429] CHOICE +{ + imei Imei, + esn Esn, +... +} + +Imsi ::= [APPLICATION 129] BCDString --(SIZE(3..8)) + +IMSSignallingContext ::= [APPLICATION 418] INTEGER + +InternetServiceProvider ::= [APPLICATION 329] SEQUENCE +{ + ispIdType IspIdType OPTIONAL, -- *m.m. + ispIdentifier IspIdentifier OPTIONAL, -- *m.m. +... +} + +InternetServiceProviderIdList ::= [APPLICATION 330] SEQUENCE OF InternetServiceProvider + +IspIdentifier ::= [APPLICATION 294] AsciiString + +IspIdType ::= [APPLICATION 293] INTEGER + +ISPList ::= [APPLICATION 378] SEQUENCE OF InternetServiceProvider + +NetworkIdType ::= [APPLICATION 331] INTEGER + +NetworkIdentifier ::= [APPLICATION 295] AsciiString + +Network ::= [APPLICATION 332] SEQUENCE +{ + networkIdType NetworkIdType OPTIONAL, -- *m.m. + networkIdentifier NetworkIdentifier OPTIONAL, -- *m.m. +... +} + +NetworkList ::= [APPLICATION 333] SEQUENCE OF Network + +LatestCallTimeStamp ::= [APPLICATION 133] DateTimeLong + +LCSQosDelivered ::= [APPLICATION 390] SEQUENCE +{ + lCSTransactionStatus LCSTransactionStatus OPTIONAL, + horizontalAccuracyDelivered HorizontalAccuracyDelivered OPTIONAL, + verticalAccuracyDelivered VerticalAccuracyDelivered OPTIONAL, + responseTime ResponseTime OPTIONAL, + positioningMethod PositioningMethod OPTIONAL, + trackingPeriod TrackingPeriod OPTIONAL, + trackingFrequency TrackingFrequency OPTIONAL, + ageOfLocation AgeOfLocation OPTIONAL, +... +} + +LCSQosRequested ::= [APPLICATION 383] SEQUENCE +{ + lCSRequestTimestamp LCSRequestTimestamp OPTIONAL, -- *m.m. + horizontalAccuracyRequested HorizontalAccuracyRequested OPTIONAL, + verticalAccuracyRequested VerticalAccuracyRequested OPTIONAL, + responseTimeCategory ResponseTimeCategory OPTIONAL, + trackingPeriod TrackingPeriod OPTIONAL, + trackingFrequency TrackingFrequency OPTIONAL, +... +} + +LCSRequestTimestamp ::= [APPLICATION 384] DateTime + +LCSSPIdentification ::= [APPLICATION 375] SEQUENCE +{ + contentProviderIdType ContentProviderIdType OPTIONAL, -- *m.m. + contentProviderIdentifier ContentProviderIdentifier OPTIONAL, -- *m.m. +... +} + +LCSSPIdentificationList ::= [APPLICATION 374] SEQUENCE OF LCSSPIdentification + +LCSSPInformation ::= [APPLICATION 373] SEQUENCE +{ + lCSSPIdentificationList LCSSPIdentificationList OPTIONAL, -- *m.m. + iSPList ISPList OPTIONAL, + networkList NetworkList OPTIONAL, +... +} + +LCSTransactionStatus ::= [APPLICATION 391] INTEGER + +LocalCurrency ::= [APPLICATION 135] Currency + +LocalTimeStamp ::= [APPLICATION 16] NumberString --(SIZE(14)) + +LocationArea ::= [APPLICATION 136] INTEGER + +LocationDescription ::= AsciiString + +LocationIdentifier ::= [APPLICATION 289] AsciiString + +LocationIdType ::= [APPLICATION 315] INTEGER + +LocationInformation ::= [APPLICATION 138] SEQUENCE +{ + networkLocation NetworkLocation OPTIONAL, -- *m.m. + homeLocationInformation HomeLocationInformation OPTIONAL, + geographicalLocation GeographicalLocation OPTIONAL, +... +} + +LocationServiceUsage ::= [APPLICATION 382] SEQUENCE +{ + lCSQosRequested LCSQosRequested OPTIONAL, -- *m.m. + lCSQosDelivered LCSQosDelivered OPTIONAL, + chargingTimeStamp ChargingTimeStamp OPTIONAL, + chargeInformationList ChargeInformationList OPTIONAL, -- *m.m. +... +} + +MaximumBitRate ::= [APPLICATION 421] OCTET STRING --(SIZE (1)) + +Mdn ::= [APPLICATION 253] NumberString + +MessageDescription ::= [APPLICATION 142] AsciiString + +MessageDescriptionCode ::= [APPLICATION 141] Code + +MessageDescriptionInformation ::= [APPLICATION 143] SEQUENCE +{ + messageDescriptionCode MessageDescriptionCode OPTIONAL, -- *m.m. + messageDescription MessageDescription OPTIONAL, -- *m.m. +... +} + +MessageStatus ::= [APPLICATION 144] INTEGER + +MessageType ::= [APPLICATION 145] INTEGER + +MessagingEventService ::= [APPLICATION 439] INTEGER + +Min ::= [APPLICATION 146] NumberString --(SIZE(2..15)) + +MinChargeableSubscriber ::= [APPLICATION 254] SEQUENCE +{ + min Min OPTIONAL, -- *m.m. + mdn Mdn OPTIONAL, +... +} + +MoBasicCallInformation ::= [APPLICATION 147] SEQUENCE +{ + chargeableSubscriber ChargeableSubscriber OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + destination Destination OPTIONAL, + destinationNetwork DestinationNetwork OPTIONAL, + callEventStartTimeStamp CallEventStartTimeStamp OPTIONAL, -- *m.m. + totalCallEventDuration TotalCallEventDuration OPTIONAL, -- *m.m. + simToolkitIndicator SimToolkitIndicator OPTIONAL, + causeForTerm CauseForTerm OPTIONAL, +... +} + +MobileSessionService ::= [APPLICATION 440] INTEGER + +Msisdn ::= [APPLICATION 152] BCDString --(SIZE(1..9)) + +MtBasicCallInformation ::= [APPLICATION 153] SEQUENCE +{ + chargeableSubscriber ChargeableSubscriber OPTIONAL, -- *m.m. + rapFileSequenceNumber RapFileSequenceNumber OPTIONAL, + callOriginator CallOriginator OPTIONAL, + originatingNetwork OriginatingNetwork OPTIONAL, + callEventStartTimeStamp CallEventStartTimeStamp OPTIONAL, -- *m.m. + totalCallEventDuration TotalCallEventDuration OPTIONAL, -- *m.m. + simToolkitIndicator SimToolkitIndicator OPTIONAL, + causeForTerm CauseForTerm OPTIONAL, +... +} + +NetworkAccessIdentifier ::= [APPLICATION 417] AsciiString + +NetworkElement ::= [APPLICATION 441] SEQUENCE +{ +elementType ElementType OPTIONAL, -- *m.m. +elementId ElementId OPTIONAL, -- *m.m. +... +} + +NetworkElementList ::= [APPLICATION 442] SEQUENCE OF NetworkElement + +NetworkId ::= AsciiString --(SIZE(1..6)) + +NetworkInitPDPContext ::= [APPLICATION 245] INTEGER + +NetworkLocation ::= [APPLICATION 156] SEQUENCE +{ + recEntityCode RecEntityCode OPTIONAL, -- *m.m. + callReference CallReference OPTIONAL, + locationArea LocationArea OPTIONAL, + cellId CellId OPTIONAL, +... +} + +NonChargedNumber ::= [APPLICATION 402] AsciiString + +NonChargedParty ::= [APPLICATION 443] SEQUENCE +{ + nonChargedPartyNumber NonChargedPartyNumber OPTIONAL, + nonChargedPublicUserId NonChargedPublicUserId OPTIONAL, +... +} + +NonChargedPartyNumber ::= [APPLICATION 444] AddressStringDigits + +NonChargedPublicUserId ::= [APPLICATION 445] AsciiString + +NumberOfDecimalPlaces ::= [APPLICATION 159] INTEGER + +ObjectType ::= [APPLICATION 281] INTEGER + +OperatorSpecInfoList ::= [APPLICATION 162] SEQUENCE OF OperatorSpecInformation + +OperatorSpecInformation ::= [APPLICATION 163] AsciiString + +OrderPlacedTimeStamp ::= [APPLICATION 300] DateTime + +OriginatingNetwork ::= [APPLICATION 164] NetworkId + +PacketDataProtocolAddress ::= [APPLICATION 165] AsciiString + +PaidIndicator ::= [APPLICATION 346] INTEGER + +PartialTypeIndicator ::= [APPLICATION 166] AsciiString --(SIZE(1)) + +PaymentMethod ::= [APPLICATION 347] INTEGER + +PdpAddress ::= [APPLICATION 167] PacketDataProtocolAddress + +PDPContextStartTimestamp ::= [APPLICATION 260] DateTime + +PlmnId ::= [APPLICATION 169] AsciiString --(SIZE(5)) + +PositioningMethod ::= [APPLICATION 395] INTEGER + +PriorityCode ::= [APPLICATION 170] INTEGER + +PublicUserId ::= [APPLICATION 446] AsciiString + +RapFileSequenceNumber ::= [APPLICATION 181] FileSequenceNumber + +RecEntityCode ::= [APPLICATION 184] Code + +RecEntityCodeList ::= [APPLICATION 185] SEQUENCE OF RecEntityCode + +RecEntityId ::= [APPLICATION 400] AsciiString + +RecEntityInfoList ::= [APPLICATION 188] SEQUENCE OF RecEntityInformation + +RecEntityInformation ::= [APPLICATION 183] SEQUENCE +{ + recEntityCode RecEntityCode OPTIONAL, -- *m.m. + recEntityType RecEntityType OPTIONAL, -- *m.m. + recEntityId RecEntityId OPTIONAL, -- *m.m. +... +} + +RecEntityType ::= [APPLICATION 186] INTEGER + +Recipient ::= [APPLICATION 182] PlmnId + +ReleaseVersionNumber ::= [APPLICATION 189] INTEGER + +RequestedDeliveryTimeStamp ::= [APPLICATION 301] DateTime + +ResponseTime ::= [APPLICATION 394] INTEGER + +ResponseTimeCategory ::= [APPLICATION 387] INTEGER + +ScuBasicInformation ::= [APPLICATION 191] SEQUENCE +{ + chargeableSubscriber ScuChargeableSubscriber OPTIONAL, -- *m.m. + chargedPartyStatus ChargedPartyStatus OPTIONAL, -- *m.m. + nonChargedNumber NonChargedNumber OPTIONAL, -- *m.m. + clirIndicator ClirIndicator OPTIONAL, + originatingNetwork OriginatingNetwork OPTIONAL, + destinationNetwork DestinationNetwork OPTIONAL, +... +} + +ScuChargeType ::= [APPLICATION 192] SEQUENCE +{ + messageStatus MessageStatus OPTIONAL, -- *m.m. + priorityCode PriorityCode OPTIONAL, -- *m.m. + distanceChargeBandCode DistanceChargeBandCode OPTIONAL, + messageType MessageType OPTIONAL, -- *m.m. + messageDescriptionCode MessageDescriptionCode OPTIONAL, -- *m.m. +... +} + +ScuTimeStamps ::= [APPLICATION 193] SEQUENCE +{ + depositTimeStamp DepositTimeStamp OPTIONAL, -- *m.m. + completionTimeStamp CompletionTimeStamp OPTIONAL, -- *m.m. + chargingPoint ChargingPoint OPTIONAL, -- *m.m. +... +} + +ScuChargeableSubscriber ::= [APPLICATION 430] CHOICE +{ + gsmChargeableSubscriber GsmChargeableSubscriber, + minChargeableSubscriber MinChargeableSubscriber, +... +} + +Sender ::= [APPLICATION 196] PlmnId + +ServiceStartTimestamp ::= [APPLICATION 447] DateTime + +ServingBid ::= [APPLICATION 198] Bid + +ServingLocationDescription ::= [APPLICATION 414] LocationDescription + +ServingNetwork ::= [APPLICATION 195] AsciiString + +ServingPartiesInformation ::= [APPLICATION 335] SEQUENCE +{ + contentProviderName ContentProviderName OPTIONAL, -- *m.m. + contentProviderIdList ContentProviderIdList OPTIONAL, + internetServiceProviderIdList InternetServiceProviderIdList OPTIONAL, + networkList NetworkList OPTIONAL, +... +} + +SessionChargeInfoList ::= [APPLICATION 448] SEQUENCE OF SessionChargeInformation + +SessionChargeInformation ::= [APPLICATION 449] SEQUENCE +{ +chargedItem ChargedItem OPTIONAL, -- *m.m. +exchangeRateCode ExchangeRateCode OPTIONAL, + callTypeGroup CallTypeGroup OPTIONAL, -- *m.m. + chargeDetailList ChargeDetailList OPTIONAL, -- *m.m. + taxInformationList TaxInformationList OPTIONAL, +... +} + +SimChargeableSubscriber ::= [APPLICATION 199] SEQUENCE +{ + imsi Imsi OPTIONAL, -- *m.m. + msisdn Msisdn OPTIONAL, +... +} + +SimToolkitIndicator ::= [APPLICATION 200] AsciiString --(SIZE(1)) + +SMSDestinationNumber ::= [APPLICATION 419] AsciiString + +SMSOriginator ::= [APPLICATION 425] AsciiString + +SpecificationVersionNumber ::= [APPLICATION 201] INTEGER + +SsParameters ::= [APPLICATION 204] AsciiString --(SIZE(1..40)) + +SupplServiceActionCode ::= [APPLICATION 208] INTEGER + +SupplServiceCode ::= [APPLICATION 209] HexString --(SIZE(2)) + +SupplServiceUsed ::= [APPLICATION 206] SEQUENCE +{ + supplServiceCode SupplServiceCode OPTIONAL, -- *m.m. + supplServiceActionCode SupplServiceActionCode OPTIONAL, -- *m.m. + ssParameters SsParameters OPTIONAL, + chargingTimeStamp ChargingTimeStamp OPTIONAL, + chargeInformation ChargeInformation OPTIONAL, + basicServiceCodeList BasicServiceCodeList OPTIONAL, +... +} + +TapCurrency ::= [APPLICATION 210] Currency + +TapDecimalPlaces ::= [APPLICATION 244] INTEGER + +TaxableAmount ::= [APPLICATION 398] AbsoluteAmount + +Taxation ::= [APPLICATION 216] SEQUENCE +{ + taxCode TaxCode OPTIONAL, -- *m.m. + taxType TaxType OPTIONAL, -- *m.m. + taxRate TaxRate OPTIONAL, + chargeType ChargeType OPTIONAL, + taxIndicator TaxIndicator OPTIONAL, +... +} + +TaxationList ::= [APPLICATION 211] SEQUENCE OF Taxation + +TaxCode ::= [APPLICATION 212] INTEGER + +TaxIndicator ::= [APPLICATION 432] AsciiString --(SIZE(1)) + +TaxInformation ::= [APPLICATION 213] SEQUENCE +{ + taxCode TaxCode OPTIONAL, -- *m.m. + taxValue TaxValue OPTIONAL, -- *m.m. + taxableAmount TaxableAmount OPTIONAL, +... +} + +TaxInformationList ::= [APPLICATION 214] SEQUENCE OF TaxInformation + +-- The TaxRate item is of a fixed length to ensure that the full 5 +-- decimal places is provided. + +TaxRate ::= [APPLICATION 215] NumberString --(SIZE(7)) + +TaxType ::= [APPLICATION 217] AsciiString --(SIZE(2)) + +TaxValue ::= [APPLICATION 397] AbsoluteAmount + +TeleServiceCode ::= [APPLICATION 218] HexString --(SIZE(2)) + +ThirdPartyInformation ::= [APPLICATION 219] SEQUENCE +{ + thirdPartyNumber ThirdPartyNumber OPTIONAL, + clirIndicator ClirIndicator OPTIONAL, +... +} + +ThirdPartyNumber ::= [APPLICATION 403] AddressStringDigits + +ThreeGcamelDestination ::= [APPLICATION 431] CHOICE +{ + camelDestinationNumber CamelDestinationNumber, + gprsDestination GprsDestination, +... +} + +TotalAdvisedCharge ::= [APPLICATION 356] AbsoluteAmount + +TotalAdvisedChargeRefund ::= [APPLICATION 357] AbsoluteAmount + +TotalAdvisedChargeValue ::= [APPLICATION 360] SEQUENCE +{ + advisedChargeCurrency AdvisedChargeCurrency OPTIONAL, + totalAdvisedCharge TotalAdvisedCharge OPTIONAL, -- *m.m. + totalAdvisedChargeRefund TotalAdvisedChargeRefund OPTIONAL, + totalCommission TotalCommission OPTIONAL, + totalCommissionRefund TotalCommissionRefund OPTIONAL, +... +} + +TotalAdvisedChargeValueList ::= [APPLICATION 361] SEQUENCE OF TotalAdvisedChargeValue + +TotalCallEventDuration ::= [APPLICATION 223] INTEGER + +TotalCharge ::= [APPLICATION 415] AbsoluteAmount + +TotalChargeRefund ::= [APPLICATION 355] AbsoluteAmount + +TotalCommission ::= [APPLICATION 358] AbsoluteAmount + +TotalCommissionRefund ::= [APPLICATION 359] AbsoluteAmount + +TotalDataVolume ::= [APPLICATION 343] DataVolume + +TotalDiscountRefund ::= [APPLICATION 354] AbsoluteAmount + +TotalDiscountValue ::= [APPLICATION 225] AbsoluteAmount + +TotalTaxRefund ::= [APPLICATION 353] AbsoluteAmount + +TotalTaxValue ::= [APPLICATION 226] AbsoluteAmount + +TotalTransactionDuration ::= [APPLICATION 416] TotalCallEventDuration + +TrackedCustomerEquipment ::= [APPLICATION 381] SEQUENCE +{ + equipmentIdType EquipmentIdType OPTIONAL, -- *m.m. + equipmentId EquipmentId OPTIONAL, -- *m.m. +... +} + +TrackedCustomerHomeId ::= [APPLICATION 377] SEQUENCE +{ + homeIdType HomeIdType OPTIONAL, -- *m.m. + homeIdentifier HomeIdentifier OPTIONAL, -- *m.m. +... +} + +TrackedCustomerHomeIdList ::= [APPLICATION 376] SEQUENCE OF TrackedCustomerHomeId + +TrackedCustomerIdentification ::= [APPLICATION 372] SEQUENCE +{ + customerIdType CustomerIdType OPTIONAL, -- *m.m. + customerIdentifier CustomerIdentifier OPTIONAL, -- *m.m. +... +} + +TrackedCustomerIdList ::= [APPLICATION 370] SEQUENCE OF TrackedCustomerIdentification + +TrackedCustomerInformation ::= [APPLICATION 367] SEQUENCE +{ + trackedCustomerIdList TrackedCustomerIdList OPTIONAL, -- *m.m. + trackedCustomerHomeIdList TrackedCustomerHomeIdList OPTIONAL, + trackedCustomerLocList TrackedCustomerLocList OPTIONAL, + trackedCustomerEquipment TrackedCustomerEquipment OPTIONAL, +... +} + +TrackedCustomerLocation ::= [APPLICATION 380] SEQUENCE +{ + locationIdType LocationIdType OPTIONAL, -- *m.m. + locationIdentifier LocationIdentifier OPTIONAL, -- *m.m. +... +} + +TrackedCustomerLocList ::= [APPLICATION 379] SEQUENCE OF TrackedCustomerLocation + +TrackingCustomerEquipment ::= [APPLICATION 371] SEQUENCE +{ + equipmentIdType EquipmentIdType OPTIONAL, -- *m.m. + equipmentId EquipmentId OPTIONAL, -- *m.m. +... +} + +TrackingCustomerHomeId ::= [APPLICATION 366] SEQUENCE +{ + homeIdType HomeIdType OPTIONAL, -- *m.m. + homeIdentifier HomeIdentifier OPTIONAL, -- *m.m. +... +} + +TrackingCustomerHomeIdList ::= [APPLICATION 365] SEQUENCE OF TrackingCustomerHomeId + +TrackingCustomerIdentification ::= [APPLICATION 362] SEQUENCE +{ + customerIdType CustomerIdType OPTIONAL, -- *m.m. + customerIdentifier CustomerIdentifier OPTIONAL, -- *m.m. +... +} + +TrackingCustomerIdList ::= [APPLICATION 299] SEQUENCE OF TrackingCustomerIdentification + +TrackingCustomerInformation ::= [APPLICATION 298] SEQUENCE +{ + trackingCustomerIdList TrackingCustomerIdList OPTIONAL, -- *m.m. + trackingCustomerHomeIdList TrackingCustomerHomeIdList OPTIONAL, + trackingCustomerLocList TrackingCustomerLocList OPTIONAL, + trackingCustomerEquipment TrackingCustomerEquipment OPTIONAL, +... +} + +TrackingCustomerLocation ::= [APPLICATION 369] SEQUENCE +{ + locationIdType LocationIdType OPTIONAL, -- *m.m. + locationIdentifier LocationIdentifier OPTIONAL, -- *m.m. +... +} + +TrackingCustomerLocList ::= [APPLICATION 368] SEQUENCE OF TrackingCustomerLocation + +TrackingFrequency ::= [APPLICATION 389] INTEGER + +TrackingPeriod ::= [APPLICATION 388] INTEGER + +TransactionAuthCode ::= [APPLICATION 342] AsciiString + +TransactionDescriptionSupp ::= [APPLICATION 338] INTEGER + +TransactionDetailDescription ::= [APPLICATION 339] AsciiString + +TransactionIdentifier ::= [APPLICATION 341] AsciiString + +TransactionShortDescription ::= [APPLICATION 340] AsciiString + +TransactionStatus ::= [APPLICATION 303] INTEGER + +TransferCutOffTimeStamp ::= [APPLICATION 227] DateTimeLong + +TransparencyIndicator ::= [APPLICATION 228] INTEGER + +UserProtocolIndicator ::= [APPLICATION 280] INTEGER + +UtcTimeOffset ::= [APPLICATION 231] AsciiString --(SIZE(5)) + +UtcTimeOffsetCode ::= [APPLICATION 232] Code + +UtcTimeOffsetInfo ::= [APPLICATION 233] SEQUENCE +{ + utcTimeOffsetCode UtcTimeOffsetCode OPTIONAL, -- *m.m. + utcTimeOffset UtcTimeOffset OPTIONAL, -- *m.m. +... +} + +UtcTimeOffsetInfoList ::= [APPLICATION 234] SEQUENCE OF UtcTimeOffsetInfo + +VerticalAccuracyDelivered ::= [APPLICATION 393] INTEGER + +VerticalAccuracyRequested ::= [APPLICATION 386] INTEGER + + +-- +-- Tagged common data types +-- + +-- +-- The AbsoluteAmount data type is used to +-- encode absolute revenue amounts. +-- The accuracy of all absolute amount values is defined +-- by the value of TapDecimalPlaces within the group +-- AccountingInfo for the entire TAP batch. +-- Note, that only amounts greater than or equal to zero are allowed. +-- The decimal number representing the amount is +-- derived from the encoded integer +-- value by division by 10^TapDecimalPlaces. +-- for example for TapDecimalPlaces = 3 the following values +-- will be derived: +-- 0 represents 0.000 +-- 12 represents 0.012 +-- 1234 represents 1.234 +-- for TapDecimalPlaces = 5 the following values will be +-- derived: +-- 0 represents 0.00000 +-- 1234 represents 0.01234 +-- 123456 represents 1.23456 +-- This data type is used to encode (total) +-- charges, (total) discount values and +-- (total) tax values. +-- +AbsoluteAmount ::= INTEGER + +Bid ::= AsciiString --(SIZE(5)) + +Code ::= INTEGER + +-- +-- Non-tagged common data types +-- +-- +-- Recommended common data types to be used for file encoding: +-- +-- The following definitions should be used for TAP file creation instead of +-- the default specifications (OCTET STRING) +-- +-- AsciiString ::= VisibleString +-- +-- Currency ::= VisibleString +-- +-- HexString ::= VisibleString +-- +-- NumberString ::= NumericString +-- +-- AsciiString contains visible ISO 646 characters. +-- Leading and trailing spaces must be discarded during processing. +-- An AsciiString cannot contain only spaces. + +AsciiString ::= OCTET STRING + +-- +-- The BCDString data type (Binary Coded Decimal String) is used to represent +-- several digits from 0 through 9, a, b, c, d, e. +-- Two digits are encoded per octet. The four leftmost bits of the octet represent +-- the first digit while the four remaining bits represent the following digit. +-- A single f must be used as a filler when the total number of digits to be +-- encoded is odd. +-- No other filler is allowed. + +BCDString ::= OCTET STRING + + +-- +-- The currency codes from ISO 4217 +-- are used to identify a currency +-- +Currency ::= OCTET STRING + +-- +-- HexString contains ISO 646 characters from 0 through 9, A, B, C, D, E, F. +-- + +HexString ::= OCTET STRING + +-- +-- NumberString contains ISO 646 characters from 0 through 9. +-- + +NumberString ::= OCTET STRING + + +-- +-- The PercentageRate data type is used to +-- encode percentage rates with an accuracy of 2 decimal places. +-- This data type is used to encode discount rates. +-- The decimal number representing the percentage +-- rate is obtained by dividing the integer value by 100 +-- Examples: +-- +-- 1500 represents 15.00 percent +-- 1 represents 0.01 percent +-- +PercentageRate ::= INTEGER + + +-- END +END +} + +1; diff --git a/FS/FS/cdr/huawei_softx3000.pm b/FS/FS/cdr/huawei_softx3000.pm new file mode 100644 index 000000000..e66af43a9 --- /dev/null +++ b/FS/FS/cdr/huawei_softx3000.pm @@ -0,0 +1,2689 @@ +package FS::cdr::huawei_softx3000; +use base qw( FS::cdr ); + +use strict; +use vars qw( %info %TZ ); +use subs qw( ts24008_number TimeStamp ); +use Time::Local; +use FS::Record qw( qsearch ); +use FS::cdr_calltype; + +#false laziness w/gsm_tap3_12.pm +%TZ = ( + '+0000' => 'XXX-0', + '+0100' => 'XXX-1', + '+0200' => 'XXX-2', + '+0300' => 'XXX-3', + '+0400' => 'XXX-4', + '+0500' => 'XXX-5', + '+0600' => 'XXX-6', + '+0700' => 'XXX-7', + '+0800' => 'XXX-8', + '+0900' => 'XXX-9', + '+1000' => 'XXX-10', + '+1100' => 'XXX-11', + '+1200' => 'XXX-12', + '-0000' => 'XXX+0', + '-0100' => 'XXX+1', + '-0200' => 'XXX+2', + '-0300' => 'XXX+3', + '-0400' => 'XXX+4', + '-0500' => 'XXX+5', + '-0600' => 'XXX+6', + '-0700' => 'XXX+7', + '-0800' => 'XXX+8', + '-0900' => 'XXX+9', + '-1000' => 'XXX+10', + '-1100' => 'XXX+11', + '-1200' => 'XXX+12', +); + +%info = ( + 'name' => 'Huawei SoftX3000', #V100R006C05 ? + 'weight' => 160, + 'type' => 'asn.1', + 'import_fields' => [], + 'asn_format' => { + 'spec' => _asn_spec(), + 'macro' => 'CallEventDataFile', + 'header_buffer' => sub { + #my $CallEventDataFile = shift; + + my %cdr_calltype = ( map { $_->calltypename => $_->calltypenum } + qsearch('cdr_calltype', {}) + ); + + { cdr_calltype => \%cdr_calltype, + }; + + }, + 'arrayref' => sub { shift->{'callEventRecords'} }, + 'row_callback' => sub { + my( $row, $buffer ) = @_; + my @keys = keys %$row; + $buffer->{'key'} = $keys[0]; + }, + 'map' => { + 'src' => huawei_field('callingNumber', ts24008_number, ), + + 'dst' => huawei_field('calledNumber', ts24008_number, ), + + 'startdate' => huawei_field(['answerTime','deliveryTime'], TimeStamp), + 'answerdate' => huawei_field(['answerTime','deliveryTime'], TimeStamp), + 'enddate' => huawei_field('releaseTime', TimeStamp), + 'duration' => huawei_field('callDuration'), + 'billsec' => huawei_field('callDuration'), + #'disposition' => #diagnostics? + #'accountcode' + #'charged_party' => # 0 or 1, do something with this? + 'calltypenum' => sub { + my($rec, $buf) = @_; + my $key = $buf->{key}; + $buf->{'cdr_calltype'}{ $key }; + }, + #'carrierid' => + }, + + }, +); + +sub huawei_field { + my $field = shift; + my $decode = $_[0] ? shift : ''; + return sub { + my($rec, $buf) = @_; + + my $key = $buf->{key}; + + $field = ref($field) ? $field : [ $field ]; + my $value = ''; + foreach my $f (@$field) { + $value = $rec->{$key}{$f} and last; + } + + $decode + ? &{ $decode }( $value ) + : $value; + + }; +} + +sub ts24008_number { + # This type contains the binary coded decimal representation of + # a directory number e.g. calling/called/connected/translated number. + # The encoding of the octet string is in accordance with the + # the elements "Calling party BCD number", "Called party BCD number" + # and "Connected number" defined in TS 24.008. + # This encoding includes type of number and number plan information + # together with a BCD encoded digit string. + # It may also contain both a presentation and screening indicator + # (octet 3a). + # For the avoidance of doubt, this field does not include + # octets 1 and 2, the element name and length, as this would be + # redundant. + # + #type id (per TS 24.008 page 490): + # low nybble: "numbering plan identification" + # high nybble: "type of number" + # 0 unknown + # 1 international + # 2 national + # 3 network specific + # 4 dedicated access, short code + # 5 reserved + # 6 reserved + # 7 reserved for extension + # (bit 8 "extension") + return sub { + my( $type_id, $value ) = unpack 'Ch*', shift; + $value =~ s/f$//; # If the called party BCD number contains an odd number + # of digits, bits 5 to 8 of the last octet shall be + # filled with an end mark coded as "1111". + $value; + }; +} + +sub TimeStamp { + # The contents of this field are a compact form of the UTCTime format + # containing local time plus an offset to universal time. Binary coded + # decimal encoding is employed for the digits to reduce the storage and + # transmission overhead + # e.g. YYMMDDhhmmssShhmm + # where + # YY = Year 00 to 99 BCD encoded + # MM = Month 01 to 12 BCD encoded + # DD = Day 01 to 31 BCD encoded + # hh = hour 00 to 23 BCD encoded + # mm = minute 00 to 59 BCD encoded + # ss = second 00 to 59 BCD encoded + # S = Sign 0 = "+", "-" ASCII encoded + # hh = hour 00 to 23 BCD encoded + # mm = minute 00 to 59 BCD encoded + return sub { + my($year, $mon, $day, $hour, $min, $sec, $tz_sign, $tz_hour, $tz_min, $dst)= + unpack 'H2H2H2H2H2H2AH2H2C', shift; + #warn "$year/$mon/$day $hour:$min:$sec $tz_sign$tz_hour$tz_min $dst\n"; + return 0 unless $year; #y2100 bug + local($ENV{TZ}) = $TZ{ "$tz_sign$tz_hour$tz_min" }; + timelocal($sec, $min, $hour, $day, $mon-1, $year); + }; +} + +sub _asn_spec { + <<'END'; + +--DEFINITIONS IMPLICIT TAGS ::= + +--BEGIN + +-------------------------------------------------------------------------------- +-- +-- CALL AND EVENT RECORDS +-- +------------------------------------------------------------------------------ +--Font: verdana 8 + +CallEventRecord ::= CHOICE +{ + moCallRecord [0] MOCallRecord, + mtCallRecord [1] MTCallRecord, + roamingRecord [2] RoamingRecord, + incGatewayRecord [3] IncGatewayRecord, + outGatewayRecord [4] OutGatewayRecord, + transitRecord [5] TransitCallRecord, + moSMSRecord [6] MOSMSRecord, + mtSMSRecord [7] MTSMSRecord, + ssActionRecord [10] SSActionRecord, + hlrIntRecord [11] HLRIntRecord, + commonEquipRecord [14] CommonEquipRecord, + recTypeExtensions [15] ManagementExtensions, + termCAMELRecord [16] TermCAMELRecord, + mtLCSRecord [17] MTLCSRecord, + moLCSRecord [18] MOLCSRecord, + niLCSRecord [19] NILCSRecord, + forwardCallRecord [100] MOCallRecord +} + +MOCallRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedIMEI [2] IMEI OPTIONAL, + servedMSISDN [3] MSISDN OPTIONAL, + callingNumber [4] CallingNumber OPTIONAL, + calledNumber [5] CalledNumber OPTIONAL, + translatedNumber [6] TranslatedNumber OPTIONAL, + connectedNumber [7] ConnectedNumber OPTIONAL, + roamingNumber [8] RoamingNumber OPTIONAL, + recordingEntity [9] RecordingEntity OPTIONAL, + mscIncomingROUTE [10] ROUTE OPTIONAL, + mscOutgoingROUTE [11] ROUTE OPTIONAL, + location [12] LocationAreaAndCell OPTIONAL, + changeOfLocation [13] SEQUENCE OF LocationChange OPTIONAL, + basicService [14] BasicServiceCode OPTIONAL, + transparencyIndicator [15] TransparencyInd OPTIONAL, + changeOfService [16] SEQUENCE OF ChangeOfService OPTIONAL, + supplServicesUsed [17] SEQUENCE OF SuppServiceUsed OPTIONAL, + aocParameters [18] AOCParameters OPTIONAL, + changeOfAOCParms [19] SEQUENCE OF AOCParmChange OPTIONAL, + msClassmark [20] Classmark OPTIONAL, + changeOfClassmark [21] ChangeOfClassmark OPTIONAL, + seizureTime [22] TimeStamp OPTIONAL, + answerTime [23] TimeStamp OPTIONAL, + releaseTime [24] TimeStamp OPTIONAL, + callDuration [25] CallDuration OPTIONAL, + radioChanRequested [27] RadioChanRequested OPTIONAL, + radioChanUsed [28] TrafficChannel OPTIONAL, + changeOfRadioChan [29] ChangeOfRadioChannel OPTIONAL, + causeForTerm [30] CauseForTerm OPTIONAL, + diagnostics [31] Diagnostics OPTIONAL, + callReference [32] CallReference OPTIONAL, + sequenceNumber [33] SequenceNumber OPTIONAL, + additionalChgInfo [34] AdditionalChgInfo OPTIONAL, + recordExtensions [35] ManagementExtensions OPTIONAL, + gsm-SCFAddress [36] Gsm-SCFAddress OPTIONAL, + serviceKey [37] ServiceKey OPTIONAL, + networkCallReference [38] NetworkCallReference OPTIONAL, + mSCAddress [39] MSCAddress OPTIONAL, + cAMELInitCFIndicator [40] CAMELInitCFIndicator OPTIONAL, + defaultCallHandling [41] DefaultCallHandling OPTIONAL, + fnur [45] Fnur OPTIONAL, + aiurRequested [46] AiurRequested OPTIONAL, + speechVersionSupported [49] SpeechVersionIdentifier OPTIONAL, + speechVersionUsed [50] SpeechVersionIdentifier OPTIONAL, + numberOfDPEncountered [51] INTEGER OPTIONAL, + levelOfCAMELService [52] LevelOfCAMELService OPTIONAL, + freeFormatData [53] FreeFormatData OPTIONAL, + cAMELCallLegInformation [54] SEQUENCE OF CAMELInformation OPTIONAL, + freeFormatDataAppend [55] BOOLEAN OPTIONAL, + defaultCallHandling-2 [56] DefaultCallHandling OPTIONAL, + gsm-SCFAddress-2 [57] Gsm-SCFAddress OPTIONAL, + serviceKey-2 [58] ServiceKey OPTIONAL, + freeFormatData-2 [59] FreeFormatData OPTIONAL, + freeFormatDataAppend-2 [60] BOOLEAN OPTIONAL, + systemType [61] SystemType OPTIONAL, + rateIndication [62] RateIndication OPTIONAL, + partialRecordType [69] PartialRecordType OPTIONAL, + guaranteedBitRate [70] GuaranteedBitRate OPTIONAL, + maximumBitRate [71] MaximumBitRate OPTIONAL, + modemType [139] ModemType OPTIONAL, + classmark3 [140] Classmark3 OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + callingChargeAreaCode [145] ChargeAreaCode OPTIONAL, + calledChargeAreaCode [146] ChargeAreaCode OPTIONAL, + mscOutgoingCircuit [166] MSCCIC OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + callEmlppPriority [170] EmlppPriority OPTIONAL, + callerDefaultEmlppPriority [171] EmlppPriority OPTIONAL, + eaSubscriberInfo [174] EASubscriberInfo OPTIONAL, + selectedCIC [175] SelectedCIC OPTIONAL, + optimalRoutingFlag [177] NULL OPTIONAL, + optimalRoutingLateForwardFlag [178] NULL OPTIONAL, + optimalRoutingEarlyForwardFlag [179] NULL OPTIONAL, + portedflag [180] PortedFlag OPTIONAL, + calledIMSI [181] IMSI OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + changeOfglobalAreaID [189] SEQUENCE OF ChangeOfglobalAreaID OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + intermediatemccmnc [193] MCCMNC OPTIONAL, + lastmccmnc [194] MCCMNC OPTIONAL, + cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL, + cUGInterlockCode [196] CUGInterlockCode OPTIONAL, + cUGOutgoingAccessUsed [197] CUGOutgoingAccessUsed OPTIONAL, + cUGIndex [198] CUGIndex OPTIONAL, + interactionWithIP [199] InteractionWithIP OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + setupTime [201] TimeStamp OPTIONAL, + alertingTime [202] TimeStamp OPTIONAL, + voiceIndicator [203] VoiceIndicator OPTIONAL, + bCategory [204] BCategory OPTIONAL, + callType [205] CallType OPTIONAL +} + +--at moc callingNumber is the same as served msisdn except basic msisdn != calling number such as MSP service + +MTCallRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedIMEI [2] IMEI OPTIONAL, + servedMSISDN [3] CalledNumber OPTIONAL, + callingNumber [4] CallingNumber OPTIONAL, + connectedNumber [5] ConnectedNumber OPTIONAL, + recordingEntity [6] RecordingEntity OPTIONAL, + mscIncomingROUTE [7] ROUTE OPTIONAL, + mscOutgoingROUTE [8] ROUTE OPTIONAL, + location [9] LocationAreaAndCell OPTIONAL, + changeOfLocation [10] SEQUENCE OF LocationChange OPTIONAL, + basicService [11] BasicServiceCode OPTIONAL, + transparencyIndicator [12] TransparencyInd OPTIONAL, + changeOfService [13] SEQUENCE OF ChangeOfService OPTIONAL, + supplServicesUsed [14] SEQUENCE OF SuppServiceUsed OPTIONAL, + aocParameters [15] AOCParameters OPTIONAL, + changeOfAOCParms [16] SEQUENCE OF AOCParmChange OPTIONAL, + msClassmark [17] Classmark OPTIONAL, + changeOfClassmark [18] ChangeOfClassmark OPTIONAL, + seizureTime [19] TimeStamp OPTIONAL, + answerTime [20] TimeStamp OPTIONAL, + releaseTime [21] TimeStamp OPTIONAL, + callDuration [22] CallDuration OPTIONAL, + radioChanRequested [24] RadioChanRequested OPTIONAL, + radioChanUsed [25] TrafficChannel OPTIONAL, + changeOfRadioChan [26] ChangeOfRadioChannel OPTIONAL, + causeForTerm [27] CauseForTerm OPTIONAL, + diagnostics [28] Diagnostics OPTIONAL, + callReference [29] CallReference OPTIONAL, + sequenceNumber [30] SequenceNumber OPTIONAL, + additionalChgInfo [31] AdditionalChgInfo OPTIONAL, + recordExtensions [32] ManagementExtensions OPTIONAL, + networkCallReference [33] NetworkCallReference OPTIONAL, + mSCAddress [34] MSCAddress OPTIONAL, + fnur [38] Fnur OPTIONAL, + aiurRequested [39] AiurRequested OPTIONAL, + speechVersionSupported [42] SpeechVersionIdentifier OPTIONAL, + speechVersionUsed [43] SpeechVersionIdentifier OPTIONAL, + gsm-SCFAddress [44] Gsm-SCFAddress OPTIONAL, + serviceKey [45] ServiceKey OPTIONAL, + systemType [46] SystemType OPTIONAL, + rateIndication [47] RateIndication OPTIONAL, + partialRecordType [54] PartialRecordType OPTIONAL, + guaranteedBitRate [55] GuaranteedBitRate OPTIONAL, + maximumBitRate [56] MaximumBitRate OPTIONAL, + initialCallAttemptFlag [137] NULL OPTIONAL, + ussdCallBackFlag [138] NULL OPTIONAL, + modemType [139] ModemType OPTIONAL, + classmark3 [140] Classmark3 OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + callingChargeAreaCode [145]ChargeAreaCode OPTIONAL, + calledChargeAreaCode [146]ChargeAreaCode OPTIONAL, + defaultCallHandling [150] DefaultCallHandling OPTIONAL, + freeFormatData [151] FreeFormatData OPTIONAL, + freeFormatDataAppend [152] BOOLEAN OPTIONAL, + numberOfDPEncountered [153] INTEGER OPTIONAL, + levelOfCAMELService [154] LevelOfCAMELService OPTIONAL, + roamingNumber [160] RoamingNumber OPTIONAL, + mscIncomingCircuit [166] MSCCIC OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + callEmlppPriority [170] EmlppPriority OPTIONAL, + calledDefaultEmlppPriority [171] EmlppPriority OPTIONAL, + eaSubscriberInfo [174] EASubscriberInfo OPTIONAL, + selectedCIC [175] SelectedCIC OPTIONAL, + optimalRoutingFlag [177] NULL OPTIONAL, + portedflag [180] PortedFlag OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + changeOfglobalAreaID [189] SEQUENCE OF ChangeOfglobalAreaID OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + intermediatemccmnc [193] MCCMNC OPTIONAL, + lastmccmnc [194] MCCMNC OPTIONAL, + cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL, + cUGInterlockCode [196] CUGInterlockCode OPTIONAL, + cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL, + cUGIndex [198] CUGIndex OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + redirectingnumber [201] RedirectingNumber OPTIONAL, + redirectingcounter [202] RedirectingCounter OPTIONAL, + setupTime [203] TimeStamp OPTIONAL, + alertingTime [204] TimeStamp OPTIONAL, + calledNumber [205] CalledNumber OPTIONAL, + voiceIndicator [206] VoiceIndicator OPTIONAL, + bCategory [207] BCategory OPTIONAL, + callType [208] CallType OPTIONAL +} + +RoamingRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedMSISDN [2] MSISDN OPTIONAL, + callingNumber [3] CallingNumber OPTIONAL, + roamingNumber [4] RoamingNumber OPTIONAL, + recordingEntity [5] RecordingEntity OPTIONAL, + mscIncomingROUTE [6] ROUTE OPTIONAL, + mscOutgoingROUTE [7] ROUTE OPTIONAL, + basicService [8] BasicServiceCode OPTIONAL, + transparencyIndicator [9] TransparencyInd OPTIONAL, + changeOfService [10] SEQUENCE OF ChangeOfService OPTIONAL, + supplServicesUsed [11] SEQUENCE OF SuppServiceUsed OPTIONAL, + seizureTime [12] TimeStamp OPTIONAL, + answerTime [13] TimeStamp OPTIONAL, + releaseTime [14] TimeStamp OPTIONAL, + callDuration [15] CallDuration OPTIONAL, + causeForTerm [17] CauseForTerm OPTIONAL, + diagnostics [18] Diagnostics OPTIONAL, + callReference [19] CallReference OPTIONAL, + sequenceNumber [20] SequenceNumber OPTIONAL, + recordExtensions [21] ManagementExtensions OPTIONAL, + networkCallReference [22] NetworkCallReference OPTIONAL, + mSCAddress [23] MSCAddress OPTIONAL, + partialRecordType [30] PartialRecordType OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + callingChargeAreaCode [145] ChargeAreaCode OPTIONAL, + calledChargeAreaCode [146] ChargeAreaCode OPTIONAL, + mscOutgoingCircuit [166] MSCCIC OPTIONAL, + mscIncomingCircuit [167] MSCCIC OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + callEmlppPriority [170] EmlppPriority OPTIONAL, + eaSubscriberInfo [174] EASubscriberInfo OPTIONAL, + selectedCIC [175] SelectedCIC OPTIONAL, + optimalRoutingFlag [177] NULL OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL, + cUGInterlockCode [196] CUGInterlockCode OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL +} + +TermCAMELRecord ::= SET +{ + recordtype [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedMSISDN [2] MSISDN OPTIONAL, + recordingEntity [3] RecordingEntity OPTIONAL, + interrogationTime [4] TimeStamp OPTIONAL, + destinationRoutingAddress [5] DestinationRoutingAddress OPTIONAL, + gsm-SCFAddress [6] Gsm-SCFAddress OPTIONAL, + serviceKey [7] ServiceKey OPTIONAL, + networkCallReference [8] NetworkCallReference OPTIONAL, + mSCAddress [9] MSCAddress OPTIONAL, + defaultCallHandling [10] DefaultCallHandling OPTIONAL, + recordExtensions [11] ManagementExtensions OPTIONAL, + calledNumber [12] CalledNumber OPTIONAL, + callingNumber [13] CallingNumber OPTIONAL, + mscIncomingROUTE [14] ROUTE OPTIONAL, + mscOutgoingROUTE [15] ROUTE OPTIONAL, + seizureTime [16] TimeStamp OPTIONAL, + answerTime [17] TimeStamp OPTIONAL, + releaseTime [18] TimeStamp OPTIONAL, + callDuration [19] CallDuration OPTIONAL, + causeForTerm [21] CauseForTerm OPTIONAL, + diagnostics [22] Diagnostics OPTIONAL, + callReference [23] CallReference OPTIONAL, + sequenceNumber [24] SequenceNumber OPTIONAL, + numberOfDPEncountered [25] INTEGER OPTIONAL, + levelOfCAMELService [26] LevelOfCAMELService OPTIONAL, + freeFormatData [27] FreeFormatData OPTIONAL, + cAMELCallLegInformation [28] SEQUENCE OF CAMELInformation OPTIONAL, + freeFormatDataAppend [29] BOOLEAN OPTIONAL, + mscServerIndication [30] BOOLEAN OPTIONAL, + defaultCallHandling-2 [31] DefaultCallHandling OPTIONAL, + gsm-SCFAddress-2 [32] Gsm-SCFAddress OPTIONAL, + serviceKey-2 [33] ServiceKey OPTIONAL, + freeFormatData-2 [34] FreeFormatData OPTIONAL, + freeFormatDataAppend-2 [35] BOOLEAN OPTIONAL, + partialRecordType [42] PartialRecordType OPTIONAL, + basicService [130] BasicServiceCode OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL +} + +IncGatewayRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + callingNumber [1] CallingNumber OPTIONAL, + calledNumber [2] CalledNumber OPTIONAL, + recordingEntity [3] RecordingEntity OPTIONAL, + mscIncomingROUTE [4] ROUTE OPTIONAL, + mscOutgoingROUTE [5] ROUTE OPTIONAL, + seizureTime [6] TimeStamp OPTIONAL, + answerTime [7] TimeStamp OPTIONAL, + releaseTime [8] TimeStamp OPTIONAL, + callDuration [9] CallDuration OPTIONAL, + causeForTerm [11] CauseForTerm OPTIONAL, + diagnostics [12] Diagnostics OPTIONAL, + callReference [13] CallReference OPTIONAL, + sequenceNumber [14] SequenceNumber OPTIONAL, + recordExtensions [15] ManagementExtensions OPTIONAL, + partialRecordType [22] PartialRecordType OPTIONAL, + iSDN-BC [23] ISDN-BC OPTIONAL, + lLC [24] LLC OPTIONAL, + hLC [25] HLC OPTIONAL, + basicService [130] BasicServiceCode OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + rateIndication [159] RateIndication OPTIONAL, + roamingNumber [160] RoamingNumber OPTIONAL, + mscIncomingCircuit [167] MSCCIC OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + callEmlppPriority [170] EmlppPriority OPTIONAL, + eaSubscriberInfo [174] EASubscriberInfo OPTIONAL, + selectedCIC [175] SelectedCIC OPTIONAL, + cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL, + cUGInterlockCode [196] CUGInterlockCode OPTIONAL, + cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL, + mscIncomingRouteAttribute [198] RouteAttribute OPTIONAL, + mscOutgoingRouteAttribute [199] RouteAttribute OPTIONAL, + networkCallReference [200] NetworkCallReference OPTIONAL, + setupTime [201] TimeStamp OPTIONAL, + alertingTime [202] TimeStamp OPTIONAL, + voiceIndicator [203] VoiceIndicator OPTIONAL, + bCategory [204] BCategory OPTIONAL, + callType [205] CallType OPTIONAL +} + +OutGatewayRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + callingNumber [1] CallingNumber OPTIONAL, + calledNumber [2] CalledNumber OPTIONAL, + recordingEntity [3] RecordingEntity OPTIONAL, + mscIncomingROUTE [4] ROUTE OPTIONAL, + mscOutgoingROUTE [5] ROUTE OPTIONAL, + seizureTime [6] TimeStamp OPTIONAL, + answerTime [7] TimeStamp OPTIONAL, + releaseTime [8] TimeStamp OPTIONAL, + callDuration [9] CallDuration OPTIONAL, + causeForTerm [11] CauseForTerm OPTIONAL, + diagnostics [12] Diagnostics OPTIONAL, + callReference [13] CallReference OPTIONAL, + sequenceNumber [14] SequenceNumber OPTIONAL, + recordExtensions [15] ManagementExtensions OPTIONAL, + partialRecordType [22] PartialRecordType OPTIONAL, + basicService [130] BasicServiceCode OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + rateIndication [159] RateIndication OPTIONAL, + roamingNumber [160] RoamingNumber OPTIONAL, + mscOutgoingCircuit [166] MSCCIC OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + eaSubscriberInfo [174] EASubscriberInfo OPTIONAL, + selectedCIC [175] SelectedCIC OPTIONAL, + callEmlppPriority [170] EmlppPriority OPTIONAL, + cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL, + cUGInterlockCode [196] CUGInterlockCode OPTIONAL, + cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL, + mscIncomingRouteAttribute [198] RouteAttribute OPTIONAL, + mscOutgoingRouteAttribute [199] RouteAttribute OPTIONAL, + networkCallReference [200] NetworkCallReference OPTIONAL, + setupTime [201] TimeStamp OPTIONAL, + alertingTime [202] TimeStamp OPTIONAL, + voiceIndicator [203] VoiceIndicator OPTIONAL, + bCategory [204] BCategory OPTIONAL, + callType [205] CallType OPTIONAL +} + +TransitCallRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + recordingEntity [1] RecordingEntity OPTIONAL, + mscIncomingROUTE [2] ROUTE OPTIONAL, + mscOutgoingROUTE [3] ROUTE OPTIONAL, + callingNumber [4] CallingNumber OPTIONAL, + calledNumber [5] CalledNumber OPTIONAL, + isdnBasicService [6] BasicService OPTIONAL, + seizureTime [7] TimeStamp OPTIONAL, + answerTime [8] TimeStamp OPTIONAL, + releaseTime [9] TimeStamp OPTIONAL, + callDuration [10] CallDuration OPTIONAL, + causeForTerm [12] CauseForTerm OPTIONAL, + diagnostics [13] Diagnostics OPTIONAL, + callReference [14] CallReference OPTIONAL, + sequenceNumber [15] SequenceNumber OPTIONAL, + recordExtensions [16] ManagementExtensions OPTIONAL, + partialRecordType [23] PartialRecordType OPTIONAL, + basicService [130] BasicServiceCode OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + originalCalledNumber [142] OriginalCalledNumber OPTIONAL, + rateIndication [159] RateIndication OPTIONAL, + mscOutgoingCircuit [166] MSCCIC OPTIONAL, + mscIncomingCircuit [167] MSCCIC OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + callEmlppPriority [170] EmlppPriority OPTIONAL, + eaSubscriberInfo [174] EASubscriberInfo OPTIONAL, + selectedCIC [175] SelectedCIC OPTIONAL, + cUGOutgoingAccessIndicator [195] CUGOutgoingAccessIndicator OPTIONAL, + cUGInterlockCode [196] CUGInterlockCode OPTIONAL, + cUGIncomingAccessUsed [197] CUGIncomingAccessUsed OPTIONAL, + mscIncomingRouteAttribute [198] RouteAttribute OPTIONAL, + mscOutgoingRouteAttribute [199] RouteAttribute OPTIONAL, + networkCallReference [200] NetworkCallReference OPTIONAL, + setupTime [201] TimeStamp OPTIONAL, + alertingTime [202] TimeStamp OPTIONAL, + voiceIndicator [203] VoiceIndicator OPTIONAL, + bCategory [204] BCategory OPTIONAL, + callType [205] CallType OPTIONAL +} + +MOSMSRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedIMEI [2] IMEI OPTIONAL, + servedMSISDN [3] MSISDN OPTIONAL, + msClassmark [4] Classmark OPTIONAL, + serviceCentre [5] AddressString OPTIONAL, + recordingEntity [6] RecordingEntity OPTIONAL, + location [7] LocationAreaAndCell OPTIONAL, + messageReference [8] MessageReference OPTIONAL, + originationTime [9] TimeStamp OPTIONAL, + smsResult [10] SMSResult OPTIONAL, + recordExtensions [11] ManagementExtensions OPTIONAL, + destinationNumber [12] SmsTpDestinationNumber OPTIONAL, + cAMELSMSInformation [13] CAMELSMSInformation OPTIONAL, + systemType [14] SystemType OPTIONAL, + basicService [130] BasicServiceCode OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + classmark3 [140] Classmark3 OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + smsUserDataType [195] SmsUserDataType OPTIONAL, + smstext [196] SMSTEXT OPTIONAL, + maximumNumberOfSMSInTheConcatenatedSMS [197] MaximumNumberOfSMSInTheConcatenatedSMS OPTIONAL, + concatenatedSMSReferenceNumber [198] ConcatenatedSMSReferenceNumber OPTIONAL, + sequenceNumberOfTheCurrentSMS [199] SequenceNumberOfTheCurrentSMS OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + callReference [201] CallReference OPTIONAL +} + +MTSMSRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + serviceCentre [1] AddressString OPTIONAL, + servedIMSI [2] IMSI OPTIONAL, + servedIMEI [3] IMEI OPTIONAL, + servedMSISDN [4] MSISDN OPTIONAL, + msClassmark [5] Classmark OPTIONAL, + recordingEntity [6] RecordingEntity OPTIONAL, + location [7] LocationAreaAndCell OPTIONAL, + deliveryTime [8] TimeStamp OPTIONAL, + smsResult [9] SMSResult OPTIONAL, + recordExtensions [10] ManagementExtensions OPTIONAL, + systemType [11] SystemType OPTIONAL, + cAMELSMSInformation [12] CAMELSMSInformation OPTIONAL, + basicService [130] BasicServiceCode OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + classmark3 [140] Classmark3 OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + smsUserDataType [195] SmsUserDataType OPTIONAL, + smstext [196] SMSTEXT OPTIONAL, + maximumNumberOfSMSInTheConcatenatedSMS [197] MaximumNumberOfSMSInTheConcatenatedSMS OPTIONAL, + concatenatedSMSReferenceNumber [198] ConcatenatedSMSReferenceNumber OPTIONAL, + sequenceNumberOfTheCurrentSMS [199] SequenceNumberOfTheCurrentSMS OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + origination [201] CallingNumber OPTIONAL, + callReference [202] CallReference OPTIONAL +} + +HLRIntRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedMSISDN [2] MSISDN OPTIONAL, + recordingEntity [3] RecordingEntity OPTIONAL, + basicService [4] BasicServiceCode OPTIONAL, + routingNumber [5] RoutingNumber OPTIONAL, + interrogationTime [6] TimeStamp OPTIONAL, + numberOfForwarding [7] NumberOfForwarding OPTIONAL, + interrogationResult [8] HLRIntResult OPTIONAL, + recordExtensions [9] ManagementExtensions OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + callReference [169] CallReference OPTIONAL +} + +SSActionRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + servedIMSI [1] IMSI OPTIONAL, + servedIMEI [2] IMEI OPTIONAL, + servedMSISDN [3] MSISDN OPTIONAL, + msClassmark [4] Classmark OPTIONAL, + recordingEntity [5] RecordingEntity OPTIONAL, + location [6] LocationAreaAndCell OPTIONAL, + basicServices [7] BasicServices OPTIONAL, + supplService [8] SS-Code OPTIONAL, + ssAction [9] SSActionType OPTIONAL, + ssActionTime [10] TimeStamp OPTIONAL, + ssParameters [11] SSParameters OPTIONAL, + ssActionResult [12] SSActionResult OPTIONAL, + callReference [13] CallReference OPTIONAL, + recordExtensions [14] ManagementExtensions OPTIONAL, + systemType [15] SystemType OPTIONAL, + ussdCodingScheme [126] UssdCodingScheme OPTIONAL, + ussdString [127] SEQUENCE OF UssdString OPTIONAL, + ussdNotifyCounter [128] UssdNotifyCounter OPTIONAL, + ussdRequestCounter [129] UssdRequestCounter OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + classmark3 [140] Classmark3 OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL +} + +CommonEquipRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + equipmentType [1] EquipmentType OPTIONAL, + equipmentId [2] EquipmentId OPTIONAL, + servedIMSI [3] IMSI OPTIONAL, + servedMSISDN [4] MSISDN OPTIONAL, + recordingEntity [5] RecordingEntity OPTIONAL, + basicService [6] BasicServiceCode OPTIONAL, + changeOfService [7] SEQUENCE OF ChangeOfService OPTIONAL, + supplServicesUsed [8] SEQUENCE OF SuppServiceUsed OPTIONAL, + seizureTime [9] TimeStamp OPTIONAL, + releaseTime [10] TimeStamp OPTIONAL, + callDuration [11] CallDuration OPTIONAL, + callReference [12] CallReference OPTIONAL, + sequenceNumber [13] SequenceNumber OPTIONAL, + recordExtensions [14] ManagementExtensions OPTIONAL, + systemType [15] SystemType OPTIONAL, + rateIndication [16] RateIndication OPTIONAL, + fnur [17] Fnur OPTIONAL, + partialRecordType [18] PartialRecordType OPTIONAL, + causeForTerm [100] CauseForTerm OPTIONAL, + diagnostics [101] Diagnostics OPTIONAL, + servedIMEI [102] IMEI OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL +} + +------------------------------------------------------------------------------ +-- +-- OBSERVED IMEI TICKETS +-- +------------------------------------------------------------------------------ + +ObservedIMEITicket ::= SET +{ + servedIMEI [0] IMEI, + imeiStatus [1] IMEIStatus, + servedIMSI [2] IMSI, + servedMSISDN [3] MSISDN OPTIONAL, + recordingEntity [4] RecordingEntity, + eventTime [5] TimeStamp, + location [6] LocationAreaAndCell, + imeiCheckEvent [7] IMEICheckEvent OPTIONAL, + callReference [8] CallReference OPTIONAL, + recordExtensions [9] ManagementExtensions OPTIONAL, + orgMSCId [168] MSCId OPTIONAL +} + + + +------------------------------------------------------------------------------ +-- +-- LOCATION SERICE TICKETS +-- +------------------------------------------------------------------------------ + +MTLCSRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + recordingEntity [1] RecordingEntity OPTIONAL, + lcsClientType [2] LCSClientType OPTIONAL, + lcsClientIdentity [3] LCSClientIdentity OPTIONAL, + servedIMSI [4] IMSI OPTIONAL, + servedMSISDN [5] MSISDN OPTIONAL, + locationType [6] LocationType OPTIONAL, + lcsQos [7] LCSQoSInfo OPTIONAL, + lcsPriority [8] LCS-Priority OPTIONAL, + mlc-Number [9] ISDN-AddressString OPTIONAL, + eventTimeStamp [10] TimeStamp OPTIONAL, + measureDuration [11] CallDuration OPTIONAL, + notificationToMSUser [12] NotificationToMSUser OPTIONAL, + privacyOverride [13] NULL OPTIONAL, + location [14] LocationAreaAndCell OPTIONAL, + locationEstimate [15] Ext-GeographicalInformation OPTIONAL, + positioningData [16] PositioningData OPTIONAL, + lcsCause [17] LCSCause OPTIONAL, + diagnostics [18] Diagnostics OPTIONAL, + systemType [19] SystemType OPTIONAL, + recordExtensions [20] ManagementExtensions OPTIONAL, + causeForTerm [21] CauseForTerm OPTIONAL, + lcsReferenceNumber [101] CallReferenceNumber OPTIONAL, + servedIMEI [102] IMEI OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + callReference [201] CallReference OPTIONAL +} + +MOLCSRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + recordingEntity [1] RecordingEntity OPTIONAL, + lcsClientType [2] LCSClientType OPTIONAL, + lcsClientIdentity [3] LCSClientIdentity OPTIONAL, + servedIMSI [4] IMSI OPTIONAL, + servedMSISDN [5] MSISDN OPTIONAL, + molr-Type [6] MOLR-Type OPTIONAL, + lcsQos [7] LCSQoSInfo OPTIONAL, + lcsPriority [8] LCS-Priority OPTIONAL, + mlc-Number [9] ISDN-AddressString OPTIONAL, + eventTimeStamp [10] TimeStamp OPTIONAL, + measureDuration [11] CallDuration OPTIONAL, + location [12] LocationAreaAndCell OPTIONAL, + locationEstimate [13] Ext-GeographicalInformation OPTIONAL, + positioningData [14] PositioningData OPTIONAL, + lcsCause [15] LCSCause OPTIONAL, + diagnostics [16] Diagnostics OPTIONAL, + systemType [17] SystemType OPTIONAL, + recordExtensions [18] ManagementExtensions OPTIONAL, + causeForTerm [19] CauseForTerm OPTIONAL, + lcsReferenceNumber [101] CallReferenceNumber OPTIONAL, + servedIMEI [102] IMEI OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + callReference [201] CallReference OPTIONAL +} + +NILCSRecord ::= SET +{ + recordType [0] CallEventRecordType OPTIONAL, + recordingEntity [1] RecordingEntity OPTIONAL, + lcsClientType [2] LCSClientType OPTIONAL, + lcsClientIdentity [3] LCSClientIdentity OPTIONAL, + servedIMSI [4] IMSI OPTIONAL, + servedMSISDN [5] MSISDN OPTIONAL, + servedIMEI [6] IMEI OPTIONAL, + emsDigits [7] ISDN-AddressString OPTIONAL, + emsKey [8] ISDN-AddressString OPTIONAL, + lcsQos [9] LCSQoSInfo OPTIONAL, + lcsPriority [10] LCS-Priority OPTIONAL, + mlc-Number [11] ISDN-AddressString OPTIONAL, + eventTimeStamp [12] TimeStamp OPTIONAL, + measureDuration [13] CallDuration OPTIONAL, + location [14] LocationAreaAndCell OPTIONAL, + locationEstimate [15] Ext-GeographicalInformation OPTIONAL, + positioningData [16] PositioningData OPTIONAL, + lcsCause [17] LCSCause OPTIONAL, + diagnostics [18] Diagnostics OPTIONAL, + systemType [19] SystemType OPTIONAL, + recordExtensions [20] ManagementExtensions OPTIONAL, + causeForTerm [21] CauseForTerm OPTIONAL, + lcsReferenceNumber [101] CallReferenceNumber OPTIONAL, + additionalChgInfo [133] AdditionalChgInfo OPTIONAL, + chargedParty [141] ChargedParty OPTIONAL, + orgRNCorBSCId [167] RNCorBSCId OPTIONAL, + orgMSCId [168] MSCId OPTIONAL, + globalAreaID [188] GAI OPTIONAL, + subscriberCategory [190] SubscriberCategory OPTIONAL, + firstmccmnc [192] MCCMNC OPTIONAL, + hotBillingTag [200] HotBillingTag OPTIONAL, + callReference [201] CallReference OPTIONAL +} + + +------------------------------------------------------------------------------ +-- +-- FTAM / FTP / TFTP FILE CONTENTS +-- +------------------------------------------------------------------------------ + +CallEventDataFile ::= SEQUENCE +{ + headerRecord [0] HeaderRecord, + callEventRecords [1] SEQUENCE OF CallEventRecord, + trailerRecord [2] TrailerRecord, + extensions [3] ManagementExtensions +} + +ObservedIMEITicketFile ::= SEQUENCE +{ + productionDateTime [0] TimeStamp, + observedIMEITickets [1] SEQUENCE OF ObservedIMEITicket, + noOfRecords [2] INTEGER, + extensions [3] ManagementExtensions +} + +HeaderRecord ::= SEQUENCE +{ + productionDateTime [0] TimeStamp, + recordingEntity [1] RecordingEntity, + extensions [2] ManagementExtensions +} + +TrailerRecord ::= SEQUENCE +{ + productionDateTime [0] TimeStamp, + recordingEntity [1] RecordingEntity, + firstCallDateTime [2] TimeStamp, + lastCallDateTime [3] TimeStamp, + noOfRecords [4] INTEGER, + extensions [5] ManagementExtensions +} + + +------------------------------------------------------------------------------ +-- +-- COMMON DATA TYPES +-- +------------------------------------------------------------------------------ + +AdditionalChgInfo ::= SEQUENCE +{ + chargeIndicator [0] ChargeIndicator OPTIONAL, + chargeParameters [1] OCTET STRING OPTIONAL +} + +AddressString ::= OCTET STRING -- (SIZE (1..maxAddressLength)) + -- This type is used to represent a number for addressing + -- purposes. It is composed of + -- a) one octet for nature of address, and numbering plan + -- indicator. + -- b) digits of an address encoded as TBCD-String. + + -- a) The first octet includes a one bit extension indicator, a + -- 3 bits nature of address indicator and a 4 bits numbering + -- plan indicator, encoded as follows: + + -- bit 8: 1 (no extension) + + -- bits 765: nature of address indicator + -- 000 unknown + -- 001 international number + -- 010 national significant number + -- 011 network specific number + -- 100 subscriber number + -- 101 reserved + -- 110 abbreviated number + -- 111 reserved for extension + + -- bits 4321: numbering plan indicator + -- 0000 unknown + -- 0001 ISDN/Telephony Numbering Plan (Rec CCITT E.164) + -- 0010 spare + -- 0011 data numbering plan (CCITT Rec X.121) + -- 0100 telex numbering plan (CCITT Rec F.69) + -- 0101 spare + -- 0110 land mobile numbering plan (CCITT Rec E.212) + -- 0111 spare + -- 1000 national numbering plan + -- 1001 private numbering plan + -- 1111 reserved for extension + + -- all other values are reserved. + + -- b) The following octets representing digits of an address + -- encoded as a TBCD-STRING. + +-- maxAddressLength INTEGER ::= 20 + +AiurRequested ::= ENUMERATED +{ + -- + -- See Bearer Capability TS 24.008 + -- (note that value "4" is intentionally missing + -- because it is not used in TS 24.008) + -- + + aiur09600BitsPerSecond (1), + aiur14400BitsPerSecond (2), + aiur19200BitsPerSecond (3), + aiur28800BitsPerSecond (5), + aiur38400BitsPerSecond (6), + aiur43200BitsPerSecond (7), + aiur57600BitsPerSecond (8), + aiur38400BitsPerSecond1 (9), + aiur38400BitsPerSecond2 (10), + aiur38400BitsPerSecond3 (11), + aiur38400BitsPerSecond4 (12) +} + +AOCParameters ::= SEQUENCE +{ + -- + -- See TS 22.024. + -- + e1 [1] EParameter OPTIONAL, + e2 [2] EParameter OPTIONAL, + e3 [3] EParameter OPTIONAL, + e4 [4] EParameter OPTIONAL, + e5 [5] EParameter OPTIONAL, + e6 [6] EParameter OPTIONAL, + e7 [7] EParameter OPTIONAL +} + +AOCParmChange ::= SEQUENCE +{ + changeTime [0] TimeStamp, + newParameters [1] AOCParameters +} + +BasicService ::= OCTET STRING -- (SIZE(1)) + +--This parameter identifies the ISDN Basic service as defined in ETSI specification ETS 300 196. +-- allServices '00'h +-- speech '01'h +-- unrestricteDigtalInfo '02'h +-- audio3k1HZ '03'h +-- unrestricteDigtalInfowithtoneandannoucement '04'h +-- telephony3k1HZ '20'h +-- teletext '21'h +-- telefaxGroup4Class1 '22'h +-- videotextSyntaxBased '23'h +-- videotelephony '24'h +-- telefaxGroup2-3 '25'h +-- telephony7kHZ '26'h + + + +BasicServices ::= SET OF BasicServiceCode + +BasicServiceCode ::= CHOICE +{ + bearerService [2] BearerServiceCode, + teleservice [3] TeleserviceCode +} + + +TeleserviceCode ::= OCTET STRING -- (SIZE (1)) + -- This type is used to represent the code identifying a single + -- teleservice, a group of teleservices, or all teleservices. The + -- services are defined in TS GSM 02.03. + -- The internal structure is defined as follows: + + -- bits 87654321: group (bits 8765) and specific service + -- (bits 4321) + +-- allTeleservices (0x00), +-- allSpeechTransmissionServices (0x10), +-- telephony (0x11), +-- emergencyCalls (0x12), +-- +-- allShortMessageServices (0x20), +-- shortMessageMT-PP (0x21), +-- shortMessageMO-PP (0x22), +-- +-- allFacsimileTransmissionServices (0x60), +-- facsimileGroup3AndAlterSpeech (0x61), +-- automaticFacsimileGroup3 (0x62), +-- facsimileGroup4 (0x63), +-- +-- The following non-hierarchical Compound Teleservice Groups +-- are defined in TS GSM 02.30: +-- allDataTeleservices (0x70), +-- covers Teleservice Groups 'allFacsimileTransmissionServices' +-- and 'allShortMessageServices' +-- allTeleservices-ExeptSMS (0x80), +-- covers Teleservice Groups 'allSpeechTransmissionServices' and +-- 'allFacsimileTransmissionServices' +-- +-- Compound Teleservice Group Codes are only used in call +-- independent supplementary service operations, i.e. they +-- are not used in InsertSubscriberData or in +-- DeleteSubscriberData messages. +-- +-- allVoiceGroupCallServices (0x90), +-- voiceGroupCall (0x91), +-- voiceBroadcastCall (0x92), +-- +-- allPLMN-specificTS (0xd0), +-- plmn-specificTS-1 (0xd1), +-- plmn-specificTS-2 (0xd2), +-- plmn-specificTS-3 (0xd3), +-- plmn-specificTS-4 (0xd4), +-- plmn-specificTS-5 (0xd5), +-- plmn-specificTS-6 (0xd6), +-- plmn-specificTS-7 (0xd7), +-- plmn-specificTS-8 (0xd8), +-- plmn-specificTS-9 (0xd9), +-- plmn-specificTS-A (0xda), +-- plmn-specificTS-B (0xdb), +-- plmn-specificTS-C (0xdc), +-- plmn-specificTS-D (0xdd), +-- plmn-specificTS-E (0xde), +-- plmn-specificTS-F (0xdf) + + +BearerServiceCode ::= OCTET STRING -- (SIZE (1)) + -- This type is used to represent the code identifying a single + -- bearer service, a group of bearer services, or all bearer + -- services. The services are defined in TS 3GPP TS 22.002 [3]. + -- The internal structure is defined as follows: + -- + -- plmn-specific bearer services: + -- bits 87654321: defined by the HPLMN operator + + -- rest of bearer services: + -- bit 8: 0 (unused) + -- bits 7654321: group (bits 7654), and rate, if applicable + -- (bits 321) + +-- allBearerServices (0x00), +-- allDataCDA-Services (0x10), +-- dataCDA-300bps (0x11), +-- dataCDA-1200bps (0x12), +-- dataCDA-1200-75bps (0x13), +-- dataCDA-2400bps (0x14), +-- dataCDA-4800bps (0x15), +-- dataCDA-9600bps (0x16), +-- general-dataCDA (0x17), +-- +-- allDataCDS-Services (0x18), +-- dataCDS-1200bps (0x1a), +-- dataCDS-2400bps (0x1c), +-- dataCDS-4800bps (0x1d), +-- dataCDS-9600bps (0x1e), +-- general-dataCDS (0x1f), +-- +-- allPadAccessCA-Services (0x20), +-- padAccessCA-300bps (0x21), +-- padAccessCA-1200bps (0x22), +-- padAccessCA-1200-75bps (0x23), +-- padAccessCA-2400bps (0x24), +-- padAccessCA-4800bps (0x25), +-- padAccessCA-9600bps (0x26), +-- general-padAccessCA (0x27), +-- +-- allDataPDS-Services (0x28), +-- dataPDS-2400bps (0x2c), +-- dataPDS-4800bps (0x2d), +-- dataPDS-9600bps (0x2e), +-- general-dataPDS (0x2f), +-- +-- allAlternateSpeech-DataCDA (0x30), +-- +-- allAlternateSpeech-DataCDS (0x38), +-- +-- allSpeechFollowedByDataCDA (0x40), +-- +-- allSpeechFollowedByDataCDS (0x48), +-- +-- The following non-hierarchical Compound Bearer Service +-- Groups are defined in TS GSM 02.30: +-- allDataCircuitAsynchronous (0x50), +-- covers "allDataCDA-Services", "allAlternateSpeech-DataCDA" and +-- "allSpeechFollowedByDataCDA" +-- allDataCircuitSynchronous (0x58), +-- covers "allDataCDS-Services", "allAlternateSpeech-DataCDS" and +-- "allSpeechFollowedByDataCDS" +-- allAsynchronousServices (0x60), +-- covers "allDataCDA-Services", "allAlternateSpeech-DataCDA", +-- "allSpeechFollowedByDataCDA" and "allPadAccessCDA-Services" +-- allSynchronousServices (0x68), +-- covers "allDataCDS-Services", "allAlternateSpeech-DataCDS", +-- "allSpeechFollowedByDataCDS" and "allDataPDS-Services" +-- +-- Compound Bearer Service Group Codes are only used in call +-- independent supplementary service operations, i.e. they +-- are not used in InsertSubscriberData or in +-- DeleteSubscriberData messages. +-- +-- allPLMN-specificBS (0xd0), +-- plmn-specificBS-1 (0xd1), +-- plmn-specificBS-2 (0xd2), +-- plmn-specificBS-3 (0xd3), +-- plmn-specificBS-4 (0xd4), +-- plmn-specificBS-5 (0xd5), +-- plmn-specificBS-6 (0xd6), +-- plmn-specificBS-7 (0xd7), +-- plmn-specificBS-8 (0xd8), +-- plmn-specificBS-9 (0xd9), +-- plmn-specificBS-A (0xda), +-- plmn-specificBS-B (0xdb), +-- plmn-specificBS-C (0xdc), +-- plmn-specificBS-D (0xdd), +-- plmn-specificBS-E (0xde), +-- plmn-specificBS-F (0xdf) + + +BCDDirectoryNumber ::= OCTET STRING + -- This type contains the binary coded decimal representation of + -- a directory number e.g. calling/called/connected/translated number. + -- The encoding of the octet string is in accordance with the + -- the elements "Calling party BCD number", "Called party BCD number" + -- and "Connected number" defined in TS 24.008. + -- This encoding includes type of number and number plan information + -- together with a BCD encoded digit string. + -- It may also contain both a presentation and screening indicator + -- (octet 3a). + -- For the avoidance of doubt, this field does not include + -- octets 1 and 2, the element name and length, as this would be + -- redundant. + +CallDuration ::= INTEGER + -- + -- The call duration in seconds. + -- For successful calls this is the chargeable duration. + -- For call attempts this is the call holding time. + -- + +CallEventRecordType ::= ENUMERATED -- INTEGER +{ + moCallRecord (0), + mtCallRecord (1), + roamingRecord (2), + incGatewayRecord (3), + outGatewayRecord (4), + transitCallRecord (5), + moSMSRecord (6), + mtSMSRecord (7), + ssActionRecord (10), + hlrIntRecord (11), + commonEquipRecord (14), + moTraceRecord (15), + mtTraceRecord (16), + termCAMELRecord (17), + mtLCSRecord (23), + moLCSRecord (24), + niLCSRecord (25), + forwardCallRecord (100) +} + +CalledNumber ::= BCDDirectoryNumber + +CallingNumber ::= BCDDirectoryNumber + +CallingPartyCategory ::= Category + +CallReference ::= OCTET STRING -- (SIZE (1..8)) + +CallReferenceNumber ::= OCTET STRING -- (SIZE (1..8)) + +CAMELDestinationNumber ::= DestinationRoutingAddress + +CAMELInformation ::= SET +{ + cAMELDestinationNumber [1] CAMELDestinationNumber OPTIONAL, + connectedNumber [2] ConnectedNumber OPTIONAL, + roamingNumber [3] RoamingNumber OPTIONAL, + mscOutgoingROUTE [4] ROUTE OPTIONAL, + seizureTime [5] TimeStamp OPTIONAL, + answerTime [6] TimeStamp OPTIONAL, + releaseTime [7] TimeStamp OPTIONAL, + callDuration [8] CallDuration OPTIONAL, + dataVolume [9] DataVolume OPTIONAL, + cAMELInitCFIndicator [10] CAMELInitCFIndicator OPTIONAL, + causeForTerm [11] CauseForTerm OPTIONAL, + cAMELModification [12] ChangedParameters OPTIONAL, + freeFormatData [13] FreeFormatData OPTIONAL, + diagnostics [14] Diagnostics OPTIONAL, + freeFormatDataAppend [15] BOOLEAN OPTIONAL, + freeFormatData-2 [16] FreeFormatData OPTIONAL, + freeFormatDataAppend-2 [17] BOOLEAN OPTIONAL +} + +CAMELSMSInformation ::= SET +{ + gsm-SCFAddress [1] Gsm-SCFAddress OPTIONAL, + serviceKey [2] ServiceKey OPTIONAL, + defaultSMSHandling [3] DefaultSMS-Handling OPTIONAL, + freeFormatData [4] FreeFormatData OPTIONAL, + callingPartyNumber [5] CallingNumber OPTIONAL, + destinationSubscriberNumber [6] CalledNumber OPTIONAL, + cAMELSMSCAddress [7] AddressString OPTIONAL, + smsReferenceNumber [8] CallReferenceNumber OPTIONAL +} + +CAMELInitCFIndicator ::= ENUMERATED +{ + noCAMELCallForwarding (0), + cAMELCallForwarding (1) +} + +CAMELModificationParameters ::= SET + -- + -- The list contains only parameters changed due to CAMEL call + -- handling. + -- +{ + callingPartyNumber [0] CallingNumber OPTIONAL, + callingPartyCategory [1] CallingPartyCategory OPTIONAL, + originalCalledPartyNumber [2] OriginalCalledNumber OPTIONAL, + genericNumbers [3] GenericNumbers OPTIONAL, + redirectingPartyNumber [4] RedirectingNumber OPTIONAL, + redirectionCounter [5] NumberOfForwarding OPTIONAL +} + + +Category ::= OCTET STRING -- (SIZE(1)) + -- + -- The internal structure is defined in ITU-T Rec Q.763. + --see subscribe category + +CauseForTerm ::= ENUMERATED -- INTEGER + -- + -- Cause codes from 16 up to 31 are defined in TS 32.015 as 'CauseForRecClosing' + -- (cause for record closing). + -- There is no direct correlation between these two types. + -- LCS related causes belong to the MAP error causes acc. TS 29.002. + -- +{ + normalRelease (0), + partialRecord (1), + partialRecordCallReestablishment (2), + unsuccessfulCallAttempt (3), + stableCallAbnormalTermination (4), + cAMELInitCallRelease (5), + unauthorizedRequestingNetwork (52), + unauthorizedLCSClient (53), + positionMethodFailure (54), + unknownOrUnreachableLCSClient (58) +} + +CellId ::= OCTET STRING -- (SIZE(2)) + -- + -- Coded according to TS 24.008 + -- + +ChangedParameters ::= SET +{ + changeFlags [0] ChangeFlags, + changeList [1] CAMELModificationParameters OPTIONAL +} + +ChangeFlags ::= BIT STRING +-- { +-- callingPartyNumberModified (0), +-- callingPartyCategoryModified (1), +-- originalCalledPartyNumberModified (2), +-- genericNumbersModified (3), +-- redirectingPartyNumberModified (4), +-- redirectionCounterModified (5) +-- } + +ChangeOfClassmark ::= SEQUENCE +{ + classmark [0] Classmark, + changeTime [1] TimeStamp +} + +ChangeOfRadioChannel ::= SEQUENCE +{ + radioChannel [0] TrafficChannel, + changeTime [1] TimeStamp, + speechVersionUsed [2] SpeechVersionIdentifier OPTIONAL +} + +ChangeOfService ::= SEQUENCE +{ + basicService [0] BasicServiceCode, + transparencyInd [1] TransparencyInd OPTIONAL, + changeTime [2] TimeStamp, + rateIndication [3] RateIndication OPTIONAL, + fnur [4] Fnur OPTIONAL +} + +ChannelCoding ::= ENUMERATED +{ + tchF4800 (1), + tchF9600 (2), + tchF14400 (3) +} + +ChargeIndicator ::= ENUMERATED -- INTEGER +{ + noIndication (0), + noCharge (1), + charge (2) +} + +Classmark ::= OCTET STRING + -- + -- See Mobile station classmark 2 or 3 TS 24.008 + -- + +ConnectedNumber ::= BCDDirectoryNumber + +DataVolume ::= INTEGER + -- + -- The volume of data transferred in segments of 64 octets. + -- + +Day ::= INTEGER -- (1..31) + +--DayClass ::= ObjectInstance + +--DayClasses ::= SET OF DayClass + +--DayDefinition ::= SEQUENCE +--{ +-- day [0] DayOfTheWeek, +-- dayClass [1] ObjectInstance +--} + +--DayDefinitions ::= SET OF DayDefinition + +--DateDefinition ::= SEQUENCE +--{ +-- month [0] Month, +-- day [1] Day, +-- dayClass [2] ObjectInstance +--} + +--DateDefinitions ::= SET OF DateDefinition + +--DayOfTheWeek ::= ENUMERATED +--{ +-- allDays (0), +-- sunday (1), +-- monday (2), +-- tuesday (3), +-- wednesday (4), +-- thursday (5), +-- friday (6), +-- saturday (7) +--} + +DestinationRoutingAddress ::= BCDDirectoryNumber + +DefaultCallHandling ::= ENUMERATED +{ + continueCall (0), + releaseCall (1) +} + -- exception handling: + -- reception of values in range 2-31 shall be treated as "continueCall" + -- reception of values greater than 31 shall be treated as "releaseCall" + +DeferredLocationEventType ::= BIT STRING +-- { +-- msAvailable (0) +-- } (SIZE (1..16)) + + -- exception handling + -- a ProvideSubscriberLocation-Arg containing other values than listed above in + -- DeferredLocationEventType shall be rejected by the receiver with a return error cause of + -- unexpected data value. + +Diagnostics ::= CHOICE +{ + gsm0408Cause [0] INTEGER, + -- See TS 24.008 + gsm0902MapErrorValue [1] INTEGER, + -- Note: The value to be stored here corresponds to + -- the local values defined in the MAP-Errors and + -- MAP-DialogueInformation modules, for full details + -- see TS 29.002. + ccittQ767Cause [2] INTEGER, + -- See ITU-T Q.767 + networkSpecificCause [3] ManagementExtension, + -- To be defined by network operator + manufacturerSpecificCause [4] ManagementExtension + -- To be defined by manufacturer +} + +DefaultSMS-Handling ::= ENUMERATED +{ + continueTransaction (0) , + releaseTransaction (1) +} +-- exception handling: +-- reception of values in range 2-31 shall be treated as "continueTransaction" +-- reception of values greater than 31 shall be treated as "releaseTransaction" + +--Destinations ::= SET OF AE-title + +EmergencyCallIndEnable ::= BOOLEAN + +EmergencyCallIndication ::= SEQUENCE +{ + cellId [0] CellId, + callerId [1] IMSIorIMEI +} + +EParameter ::= INTEGER -- (0..1023) + -- + -- Coded according to TS 22.024 and TS 24.080 + -- + +EquipmentId ::= INTEGER + +Ext-GeographicalInformation ::= OCTET STRING -- (SIZE (1..maxExt-GeographicalInformation)) + -- Refers to geographical Information defined in 3G TS 23.032. + -- This is composed of 1 or more octets with an internal structure according to + -- 3G TS 23.032 + -- Octet 1: Type of shape, only the following shapes in 3G TS 23.032 are allowed: + -- (a) Ellipsoid point with uncertainty circle + -- (b) Ellipsoid point with uncertainty ellipse + -- (c) Ellipsoid point with altitude and uncertainty ellipsoid + -- (d) Ellipsoid Arc + -- (e) Ellipsoid Point + -- Any other value in octet 1 shall be treated as invalid + -- Octets 2 to 8 for case (a) - Ellipsoid point with uncertainty circle + -- Degrees of Latitude 3 octets + -- Degrees of Longitude 3 octets + -- Uncertainty code 1 octet + -- Octets 2 to 11 for case (b) - Ellipsoid point with uncertainty ellipse: + -- Degrees of Latitude 3 octets + -- Degrees of Longitude 3 octets + -- Uncertainty semi-major axis 1 octet + -- Uncertainty semi-minor axis 1 octet + -- Angle of major axis 1 octet + -- Confidence 1 octet + -- Octets 2 to 14 for case (c) - Ellipsoid point with altitude and uncertainty ellipsoid + -- Degrees of Latitude 3 octets + -- Degrees of Longitude 3 octets + -- Altitude 2 octets + -- Uncertainty semi-major axis 1 octet + -- Uncertainty semi-minor axis 1 octet + -- Angle of major axis 1 octet + -- Uncertainty altitude 1 octet + -- Confidence 1 octet + -- Octets 2 to 13 for case (d) - Ellipsoid Arc + -- Degrees of Latitude 3 octets + -- Degrees of Longitude 3 octets + -- Inner radius 2 octets + -- Uncertainty radius 1 octet + -- Offset angle 1 octet + -- Included angle 1 octet + -- Confidence 1 octet + -- Octets 2 to 7 for case (e) - Ellipsoid Point + -- Degrees of Latitude 3 octets + -- Degrees of Longitude 3 octets + -- + -- An Ext-GeographicalInformation parameter comprising more than one octet and + -- containing any other shape or an incorrect number of octets or coding according + -- to 3G TS 23.032 shall be treated as invalid data by a receiver. + -- + -- An Ext-GeographicalInformation parameter comprising one octet shall be discarded + -- by the receiver if an Add-GeographicalInformation parameter is received + -- in the same message. + -- + -- An Ext-GeographicalInformation parameter comprising one octet shall be treated as + -- invalid data by the receiver if an Add-GeographicalInformation parameter is not + -- received in the same message. + +-- maxExt-GeographicalInformation INTEGER ::= 20 + -- the maximum length allows for further shapes in 3G TS 23.032 to be included in later + -- versions of 3G TS 29.002 + +EquipmentType ::= ENUMERATED -- INTEGER +{ + conferenceBridge (0) +} + +FileType ::= ENUMERATED -- INTEGER +{ + callRecords (1), + traceRecords (9), + observedIMEITicket (14) +} + +Fnur ::= ENUMERATED +{ + -- + -- See Bearer Capability TS 24.008 + -- + fnurNotApplicable (0), + fnur9600-BitsPerSecond (1), + fnur14400BitsPerSecond (2), + fnur19200BitsPerSecond (3), + fnur28800BitsPerSecond (4), + fnur38400BitsPerSecond (5), + fnur48000BitsPerSecond (6), + fnur56000BitsPerSecond (7), + fnur64000BitsPerSecond (8), + fnur33600BitsPerSecond (9), + fnur32000BitsPerSecond (10), + fnur31200BitsPerSecond (11) +} + +ForwardToNumber ::= AddressString + +FreeFormatData ::= OCTET STRING -- (SIZE(1..160)) + -- + -- Free formated data as sent in the FCI message + -- See TS 29.078 + -- + +GenericNumber ::= BCDDirectoryNumber + +GenericNumbers ::= SET OF GenericNumber + +Gsm-SCFAddress ::= ISDNAddressString + -- + -- See TS 29.002 + -- + +HLRIntResult ::= Diagnostics + +Horizontal-Accuracy ::= OCTET STRING -- (SIZE (1)) + -- bit 8 = 0 + -- bits 7-1 = 7 bit Uncertainty Code defined in 3G TS 23.032. The horizontal location + -- error should be less than the error indicated by the uncertainty code with 67% + -- confidence. + +HotBillingTag ::= ENUMERATED --INTEGER +{ + noHotBilling (0), + hotBilling (1) +} + +HSCSDParmsChange ::= SEQUENCE +{ + changeTime [0] TimeStamp, + hSCSDChanAllocated [1] NumOfHSCSDChanAllocated, + initiatingParty [2] InitiatingParty OPTIONAL, + aiurRequested [3] AiurRequested OPTIONAL, + chanCodingUsed [4] ChannelCoding, + hSCSDChanRequested [5] NumOfHSCSDChanRequested OPTIONAL +} + + +IMEI ::= TBCD-STRING -- (SIZE (8)) + -- Refers to International Mobile Station Equipment Identity + -- and Software Version Number (SVN) defined in TS GSM 03.03. + -- If the SVN is not present the last octet shall contain the + -- digit 0 and a filler. + -- If present the SVN shall be included in the last octet. + +IMSI ::= TBCD-STRING -- (SIZE (3..8)) + -- digits of MCC, MNC, MSIN are concatenated in this order. + +IMEICheckEvent ::= ENUMERATED -- INTEGER +{ + mobileOriginatedCall (0), + mobileTerminatedCall (1), + smsMobileOriginating (2), + smsMobileTerminating (3), + ssAction (4), + locationUpdate (5) +} + +IMEIStatus ::= ENUMERATED +{ + greyListedMobileEquipment (0), + blackListedMobileEquipment (1), + nonWhiteListedMobileEquipment (2) +} + +IMSIorIMEI ::= CHOICE +{ + imsi [0] IMSI, + imei [1] IMEI +} + +InitiatingParty ::= ENUMERATED +{ + network (0), + subscriber (1) +} + +ISDN-AddressString ::= AddressString -- (SIZE (1..maxISDN-AddressLength)) + -- This type is used to represent ISDN numbers. + +-- maxISDN-AddressLength INTEGER ::= 9 + +LCSCause ::= OCTET STRING -- (SIZE(1)) + -- + -- See LCS Cause Value, 3GPP TS 49.031 + -- + +LCS-Priority ::= OCTET STRING -- (SIZE (1)) + -- 0 = highest priority + -- 1 = normal priority + -- all other values treated as 1 + +LCSClientIdentity ::= SEQUENCE +{ + lcsClientExternalID [0] LCSClientExternalID OPTIONAL, + lcsClientDialedByMS [1] AddressString OPTIONAL, + lcsClientInternalID [2] LCSClientInternalID OPTIONAL +} + +LCSClientExternalID ::= SEQUENCE +{ + externalAddress [0] AddressString OPTIONAL +-- extensionContainer [1] ExtensionContainer OPTIONAL +} + +LCSClientInternalID ::= ENUMERATED +{ + broadcastService (0), + o-andM-HPLMN (1), + o-andM-VPLMN (2), + anonymousLocation (3), + targetMSsubscribedService (4) +} + -- for a CAMEL phase 3 PLMN operator client, the value targetMSsubscribedService shall be used + +LCSClientType ::= ENUMERATED +{ + emergencyServices (0), + valueAddedServices (1), + plmnOperatorServices (2), + lawfulInterceptServices (3) +} + -- exception handling: + -- unrecognized values may be ignored if the LCS client uses the privacy override + -- otherwise, an unrecognized value shall be treated as unexpected data by a receiver + -- a return error shall then be returned if received in a MAP invoke + +LCSQoSInfo ::= SEQUENCE +{ + horizontal-accuracy [0] Horizontal-Accuracy OPTIONAL, + verticalCoordinateRequest [1] NULL OPTIONAL, + vertical-accuracy [2] Vertical-Accuracy OPTIONAL, + responseTime [3] ResponseTime OPTIONAL +} + +LevelOfCAMELService ::= BIT STRING +-- { +-- basic (0), +-- callDurationSupervision (1), +-- onlineCharging (2) +-- } + +LocationAreaAndCell ::= SEQUENCE +{ + locationAreaCode [0] LocationAreaCode, + cellIdentifier [1] CellId +-- +-- For 2G the content of the Cell Identifier is defined by the Cell Id +-- refer TS 24.008 and for 3G by the Service Area Code refer TS 25.413. +-- + +} + +LocationAreaCode ::= OCTET STRING -- (SIZE(2)) + -- + -- See TS 24.008 + -- + +LocationChange ::= SEQUENCE +{ + location [0] LocationAreaAndCell, + changeTime [1] TimeStamp +} + +Location-info ::= SEQUENCE +{ + mscNumber [1] MscNo OPTIONAL, + location-area [2] LocationAreaCode, + cell-identification [3] CellId OPTIONAL +} + +LocationType ::= SEQUENCE +{ +locationEstimateType [0] LocationEstimateType, + deferredLocationEventType [1] DeferredLocationEventType OPTIONAL +} + +LocationEstimateType ::= ENUMERATED +{ + currentLocation (0), + currentOrLastKnownLocation (1), + initialLocation (2), + activateDeferredLocation (3), + cancelDeferredLocation (4) +} + -- exception handling: + -- a ProvideSubscriberLocation-Arg containing an unrecognized LocationEstimateType + -- shall be rejected by the receiver with a return error cause of unexpected data value + +LocUpdResult ::= Diagnostics + +ManagementExtensions ::= SET OF ManagementExtension + +ManagementExtension ::= SEQUENCE +{ + identifier OBJECT IDENTIFIER, + significance [1] BOOLEAN , -- DEFAULT FALSE, + information [2] OCTET STRING +} + + +MCCMNC ::= OCTET STRING -- (SIZE(3)) + -- + -- This type contains the mobile country code (MCC) and the mobile + -- network code (MNC) of a PLMN. + -- + +RateIndication ::= OCTET STRING -- (SIZE(1)) + +--0 no rate adaption +--1 V.110, I.460/X.30 +--2 ITU-T X.31 flag stuffing +--3 V.120 +--7 H.223 & H.245 +--11 PIAFS + + +MessageReference ::= OCTET STRING + +Month ::= INTEGER -- (1..12) + +MOLR-Type ::= INTEGER +--0 locationEstimate +--1 assistanceData +--2 deCipheringKeys + +MSCAddress ::= AddressString + +MscNo ::= ISDN-AddressString + -- + -- See TS 23.003 + -- + +MSISDN ::= ISDN-AddressString + -- + -- See TS 23.003 + -- + +MSPowerClasses ::= SET OF RFPowerCapability + +NetworkCallReference ::= CallReferenceNumber + -- See TS 29.002 + -- + +NetworkSpecificCode ::= INTEGER + -- + -- To be defined by network operator + -- + +NetworkSpecificServices ::= SET OF NetworkSpecificCode + +NotificationToMSUser ::= ENUMERATED +{ + notifyLocationAllowed (0), + notifyAndVerify-LocationAllowedIfNoResponse (1), + notifyAndVerify-LocationNotAllowedIfNoResponse (2), + locationNotAllowed (3) +} + -- exception handling: + -- At reception of any other value than the ones listed the receiver shall ignore + -- NotificationToMSUser. + +NumberOfForwarding ::= INTEGER -- (1..5) + +NumOfHSCSDChanRequested ::= INTEGER + +NumOfHSCSDChanAllocated ::= INTEGER + +ObservedIMEITicketEnable ::= BOOLEAN + +OriginalCalledNumber ::= BCDDirectoryNumber + +OriginDestCombinations ::= SET OF OriginDestCombination + +OriginDestCombination ::= SEQUENCE +{ + origin [0] INTEGER OPTIONAL, + destination [1] INTEGER OPTIONAL + -- + -- Note that these values correspond to the contents + -- of the attributes originId and destinationId + -- respectively. At least one of the two must be present. + -- +} + +PartialRecordTimer ::= INTEGER + +PartialRecordType ::= ENUMERATED +{ + timeLimit (0), + serviceChange (1), + locationChange (2), + classmarkChange (3), + aocParmChange (4), + radioChannelChange (5), + hSCSDParmChange (6), + changeOfCAMELDestination (7), + firstHotBill (20), + severalSSOperationBill (21) +} + +PartialRecordTypes ::= SET OF PartialRecordType + +PositioningData ::= OCTET STRING -- (SIZE(1..33)) + -- + -- See Positioning Data IE (octet 3..n), 3GPP TS 49.031 + -- + +RadioChannelsRequested ::= SET OF RadioChanRequested + +RadioChanRequested ::= ENUMERATED +{ + -- + -- See Bearer Capability TS 24.008 + -- + halfRateChannel (0), + fullRateChannel (1), + dualHalfRatePreferred (2), + dualFullRatePreferred (3) +} + +--RecordClassDestination ::= CHOICE +--{ +-- osApplication [0] AE-title, +-- fileType [1] FileType +--} + +--RecordClassDestinations ::= SET OF RecordClassDestination + +RecordingEntity ::= AddressString + +RecordingMethod ::= ENUMERATED +{ + inCallRecord (0), + inSSRecord (1) +} + +RedirectingNumber ::= BCDDirectoryNumber + +RedirectingCounter ::= INTEGER + +ResponseTime ::= SEQUENCE +{ + responseTimeCategory ResponseTimeCategory +} + -- note: an expandable SEQUENCE simplifies later addition of a numeric response time. + +ResponseTimeCategory ::= ENUMERATED +{ + lowdelay (0), + delaytolerant (1) +} + -- exception handling: + -- an unrecognized value shall be treated the same as value 1 (delaytolerant) + +RFPowerCapability ::= INTEGER + -- + -- This field contains the RF power capability of the Mobile station + -- classmark 1 and 2 of TS 24.008 expressed as an integer. + -- + +RoamingNumber ::= ISDN-AddressString + -- + -- See TS 23.003 + -- + +RoutingNumber ::= CHOICE +{ + roaming [1] RoamingNumber, + forwarded [2] ForwardToNumber +} + +Service ::= CHOICE +{ + teleservice [1] TeleserviceCode, + bearerService [2] BearerServiceCode, + supplementaryService [3] SS-Code, + networkSpecificService [4] NetworkSpecificCode +} + +ServiceDistanceDependencies ::= SET OF ServiceDistanceDependency + +ServiceDistanceDependency ::= SEQUENCE +{ + aocService [0] INTEGER, + chargingZone [1] INTEGER OPTIONAL + -- + -- Note that these values correspond to the contents + -- of the attributes aocServiceId and zoneId + -- respectively. + -- +} + +ServiceKey ::= INTEGER -- (0..2147483647) + +SimpleIntegerName ::= INTEGER + +SimpleStringName ::= GraphicString + +SMSResult ::= Diagnostics + +SmsTpDestinationNumber ::= OCTET STRING + -- + -- This type contains the binary coded decimal representation of + -- the SMS address field the encoding of the octet string is in + -- accordance with the definition of address fields in TS 23.040. + -- This encoding includes type of number and numbering plan indication + -- together with the address value range. + -- + +SpeechVersionIdentifier ::= OCTET STRING -- (SIZE(1)) +-- see GSM 08.08 + +-- 000 0001 GSM speech full rate version 1 +-- 001 0001 GSM speech full rate version 2 used for enhanced full rate +-- 010 0001 GSM speech full rate version 3 for future use +-- 000 0101 GSM speech half rate version 1 +-- 001 0101 GSM speech half rate version 2 for future use +-- 010 0101 GSM speech half rate version 3 for future use + +SSActionResult ::= Diagnostics + +SSActionType ::= ENUMERATED +{ + registration (0), + erasure (1), + activation (2), + deactivation (3), + interrogation (4), + invocation (5), + passwordRegistration (6), + ussdInvocation (7) +} + +-- ussdInvocation (7) include ussd phase 1,phase 2 + +--SS Request = SSActionType + +SS-Code ::= OCTET STRING -- (SIZE (1)) + -- This type is used to represent the code identifying a single + -- supplementary service, a group of supplementary services, or + -- all supplementary services. The services and abbreviations + -- used are defined in TS 3GPP TS 22.004 [5]. The internal structure is + -- defined as follows: + -- + -- bits 87654321: group (bits 8765), and specific service + -- (bits 4321) ussd = ff + +-- allSS (0x00), +-- reserved for possible future use +-- all SS +-- +-- allLineIdentificationSS (0x10), +-- reserved for possible future use +-- all line identification SS +-- +-- calling-line-identification-presentation (0x11), +-- calling line identification presentation +-- calling-line-identification-restriction (0x12), +-- calling line identification restriction +-- connected-line-identification-presentation (0x13), +-- connected line identification presentation +-- connected-line-identification-restriction (0x14), +-- connected line identification restriction +-- malicious-call-identification (0x15), +-- reserved for possible future use +-- malicious call identification +-- +-- allNameIdentificationSS (0x18), +-- all name identification SS +-- calling-name-presentation (0x19), +-- calling name presentation +-- +-- SS-Codes '00011010'B, to '00011111'B, are reserved for future +-- NameIdentification Supplementary Service use. +-- +-- allForwardingSS (0x20), +-- all forwarding SS +-- call-forwarding-unconditional (0x21), +-- call forwarding unconditional +-- call-deflection (0x24), +-- call deflection +-- allCondForwardingSS (0x28), +-- all conditional forwarding SS +-- call-forwarding-on-mobile-subscriber-busy (0x29), +-- call forwarding on mobile subscriber busy +-- call-forwarding-on-no-reply (0x2a), +-- call forwarding on no reply +-- call-forwarding-on-mobile-subscriber-not-reachable (0x2b), +-- call forwarding on mobile subscriber not reachable +-- +-- allCallOfferingSS (0x30), +-- reserved for possible future use +-- all call offering SS includes also all forwarding SS +-- +-- explicit-call-transfer (0x31), +-- explicit call transfer +-- mobile-access-hunting (0x32), +-- reserved for possible future use +-- mobile access hunting +-- +-- allCallCompletionSS (0x40), +-- reserved for possible future use +-- all Call completion SS +-- +-- call-waiting (0x41), +-- call waiting +-- call-hold (0x42), +-- call hold +-- completion-of-call-to-busy-subscribers-originating-side (0x43), +-- completion of call to busy subscribers, originating side +-- completion-of-call-to-busy-subscribers-destination-side (0x44), +-- completion of call to busy subscribers, destination side +-- this SS-Code is used only in InsertSubscriberData and DeleteSubscriberData +-- +-- multicall (0x45), +-- multicall +-- +-- allMultiPartySS (0x50), +-- reserved for possible future use +-- all multiparty SS +-- +-- multiPTY (0x51), +-- multiparty +-- +-- allCommunityOfInterest-SS (0x60), +-- reserved for possible future use +-- all community of interest SS +-- closed-user-group (0x61), +-- closed user group +-- +-- allChargingSS (0x70), +-- reserved for possible future use +-- all charging SS +-- advice-of-charge-information (0x71), +-- advice of charge information +-- advice-of-charge-charging (0x72), +-- advice of charge charging +-- +-- allAdditionalInfoTransferSS (0x80), +-- reserved for possible future use +-- all additional information transfer SS +-- uUS1-user-to-user-signalling (0x81), +-- UUS1 user-to-user signalling +-- uUS2-user-to-user-signalling (0x82), +-- UUS2 user-to-user signalling +-- uUS3-user-to-user-signalling (0x83), +-- UUS3 user-to-user signalling +-- +-- allBarringSS (0x90), +-- all barring SS +-- barringOfOutgoingCalls (0x91), +-- barring of outgoing calls +-- barring-of-all-outgoing-calls (0x92), +-- barring of all outgoing calls +-- barring-of-outgoing-international-calls (0x93), +-- barring of outgoing international calls +-- boicExHC (0x94), +-- barring of outgoing international calls except those directed +-- to the home PLMN +-- barringOfIncomingCalls (0x99), +-- barring of incoming calls +-- barring-of-all-incoming-calls (0x9a), +-- barring of all incoming calls +-- barring-of-incoming-calls-when-roaming-outside-home-PLMN-Country (0x9b), +-- barring of incoming calls when roaming outside home PLMN +-- Country +-- +-- allCallPrioritySS (0xa0), +-- reserved for possible future use +-- all call priority SS +-- enhanced-Multilevel-Precedence-Pre-emption-EMLPP-service (0xa1), +-- enhanced Multilevel Precedence Pre-emption 'EMLPP) service +-- +-- allLCSPrivacyException (0xb0), +-- all LCS Privacy Exception Classes +-- universal (0xb1), +-- allow location by any LCS client +-- callrelated (0xb2), +-- allow location by any value added LCS client to which a call +-- is established from the target MS +-- callunrelated (0xb3), +-- allow location by designated external value added LCS clients +-- plmnoperator (0xb4), +-- allow location by designated PLMN operator LCS clients +-- +-- allMOLR-SS (0xc0), +-- all Mobile Originating Location Request Classes +-- basicSelfLocation (0xc1), +-- allow an MS to request its own location +-- autonomousSelfLocation (0xc2), +-- allow an MS to perform self location without interaction +-- with the PLMN for a predetermined period of time +-- transferToThirdParty (0xc3), +-- allow an MS to request transfer of its location to another LCS client +-- +-- allPLMN-specificSS (0xf0), +-- plmn-specificSS-1 (0xf1), +-- plmn-specificSS-2 (0xf2), +-- plmn-specificSS-3 (0xf3), +-- plmn-specificSS-4 (0xf4), +-- plmn-specificSS-5 (0xf5), +-- plmn-specificSS-6 (0xf6), +-- plmn-specificSS-7 (0xf7), +-- plmn-specificSS-8 (0xf8), +-- plmn-specificSS-9 (0xf9), +-- plmn-specificSS-A (0xfa), +-- plmn-specificSS-B (0xfb), +-- plmn-specificSS-C (0xfc), +-- plmn-specificSS-D (0xfd), +-- plmn-specificSS-E (0xfe), +-- ussd (0xff) + + +SSParameters ::= CHOICE +{ + forwardedToNumber [0] ForwardToNumber, + unstructuredData [1] OCTET STRING +} + +SupplServices ::= SET OF SS-Code + +SuppServiceUsed ::= SEQUENCE +{ + ssCode [0] SS-Code OPTIONAL, + ssTime [1] TimeStamp OPTIONAL +} + +SwitchoverTime ::= SEQUENCE +{ + hour INTEGER , -- (0..23), + minute INTEGER , -- (0..59), + second INTEGER -- (0..59) +} + +SystemType ::= ENUMERATED + -- "unknown" is not to be used in PS domain. +{ + unknown (0), + iuUTRAN (1), + gERAN (2) +} + +TBCD-STRING ::= OCTET STRING + -- This type (Telephony Binary Coded Decimal String) is used to + -- represent several digits from 0 through 9, *, #, a, b, c, two + -- digits per octet, each digit encoded 0000 to 1001 (0 to 9), + -- 1010 (*), 1011 (#), 1100 (a), 1101 (b) or 1110 (c); 1111 used + -- as filler when there is an odd number of digits. + + -- bits 8765 of octet n encoding digit 2n + -- bits 4321 of octet n encoding digit 2(n-1) +1 + +TariffId ::= INTEGER + +TariffPeriod ::= SEQUENCE +{ + switchoverTime [0] SwitchoverTime, + tariffId [1] INTEGER + -- Note that the value of tariffId corresponds + -- to the attribute tariffId. +} + +TariffPeriods ::= SET OF TariffPeriod + +TariffSystemStatus ::= ENUMERATED +{ + available (0), -- available for modification + checked (1), -- "frozen" and checked + standby (2), -- "frozen" awaiting activation + active (3) -- "frozen" and active +} + + +TimeStamp ::= OCTET STRING -- (SIZE(9)) + -- + -- The contents of this field are a compact form of the UTCTime format + -- containing local time plus an offset to universal time. Binary coded + -- decimal encoding is employed for the digits to reduce the storage and + -- transmission overhead + -- e.g. YYMMDDhhmmssShhmm + -- where + -- YY = Year 00 to 99 BCD encoded + -- MM = Month 01 to 12 BCD encoded + -- DD = Day 01 to 31 BCD encoded + -- hh = hour 00 to 23 BCD encoded + -- mm = minute 00 to 59 BCD encoded + -- ss = second 00 to 59 BCD encoded + -- S = Sign 0 = "+", "-" ASCII encoded + -- hh = hour 00 to 23 BCD encoded + -- mm = minute 00 to 59 BCD encoded + -- + +TrafficChannel ::= ENUMERATED +{ + fullRate (0), + halfRate (1) +} + +TranslatedNumber ::= BCDDirectoryNumber + +TransparencyInd ::= ENUMERATED +{ + transparent (0), + nonTransparent (1) +} + +ROUTE ::= CHOICE +{ + rOUTENumber [0] INTEGER, + rOUTEName [1] GraphicString +} + +--rOUTEName 1 10 octet + +TSChangeover ::= SEQUENCE +{ + newActiveTS [0] INTEGER, + newStandbyTS [1] INTEGER, +-- changeoverTime [2] GeneralizedTime OPTIONAL, + authkey [3] OCTET STRING OPTIONAL, + checksum [4] OCTET STRING OPTIONAL, + versionNumber [5] OCTET STRING OPTIONAL + -- Note that if the changeover time is not + -- specified then the change is immediate. +} + +TSCheckError ::= SEQUENCE +{ + errorId [0] TSCheckErrorId + --fail [1] ANY DEFINED BY errorId OPTIONAL +} + +TSCheckErrorId ::= CHOICE +{ + globalForm [0] OBJECT IDENTIFIER, + localForm [1] INTEGER +} + +TSCheckResult ::= CHOICE +{ + success [0] NULL, + fail [1] SET OF TSCheckError +} + +TSCopyTariffSystem ::= SEQUENCE +{ + oldTS [0] INTEGER, + newTS [1] INTEGER +} + +TSNextChange ::= CHOICE +{ + noChangeover [0] NULL, + tsChangeover [1] TSChangeover +} + +TypeOfSubscribers ::= ENUMERATED +{ + home (0), -- HPLMN subscribers + visiting (1), -- roaming subscribers + all (2) +} + +TypeOfTransaction ::= ENUMERATED +{ + successful (0), + unsuccessful (1), + all (2) +} + +Vertical-Accuracy ::= OCTET STRING -- (SIZE (1)) + -- bit 8 = 0 + -- bits 7-1 = 7 bit Vertical Uncertainty Code defined in 3G TS 23.032. + -- The vertical location error should be less than the error indicated + -- by the uncertainty code with 67% confidence. + +ISDNAddressString ::= AddressString + +EmlppPriority ::= OCTET STRING -- (SIZE (1)) + +--priorityLevelA EMLPP-Priority ::= 6 +--priorityLevelB EMLPP-Priority ::= 5 +--priorityLevel0 EMLPP-Priority ::= 0 +--priorityLevel1 EMLPP-Priority ::= 1 +--priorityLevel2 EMLPP-Priority ::= 2 +--priorityLevel3 EMLPP-Priority ::= 3 +--priorityLevel4 EMLPP-Priority ::= 4 +--See 29.002 + + +EASubscriberInfo ::= OCTET STRING -- (SIZE (3)) + -- The internal structure is defined by the Carrier Identification + -- parameter in ANSI T1.113.3. Carrier codes between "000" and "999" may + -- be encoded as 3 digits using "000" to "999" or as 4 digits using + -- "0000" to "0999". Carrier codes between "1000" and "9999" are encoded + -- using 4 digits. + +SelectedCIC ::= OCTET STRING -- (SIZE (3)) + +PortedFlag ::= ENUMERATED +{ + numberNotPorted (0), + numberPorted (1) +} + +SubscriberCategory ::= OCTET STRING -- (SIZE (1)) +-- unknownuser = 0x00, +-- frenchuser = 0x01, +-- englishuser = 0x02, +-- germanuser = 0x03, +-- russianuser = 0x04, +-- spanishuser = 0x05, +-- specialuser = 0x06, +-- reserveuser = 0x09, +-- commonuser = 0x0a, +-- superioruser = 0x0b, +-- datacalluser = 0x0c, +-- testcalluser = 0x0d, +-- spareuser = 0x0e, +-- payphoneuser = 0x0f, +-- coinuser = 0x20, +-- isup224 = 0xe0 + + +CUGOutgoingAccessIndicator ::= ENUMERATED +{ + notCUGCall (0), + cUGCall (1) +} + +CUGInterlockCode ::= OCTET STRING -- (SIZE (4)) + +-- + +CUGOutgoingAccessUsed ::= ENUMERATED +{ + callInTheSameCUGGroup (0), + callNotInTheSameCUGGroup (1) +} + +SMSTEXT ::= OCTET STRING + +MSCCIC ::= INTEGER -- (0..65535) + +RNCorBSCId ::= OCTET STRING -- (SIZE (3)) +--octet order is the same as RANAP/BSSAP signaling +--if spc is coded as 14bit, then OCTET STRING1 will filled with 00 ,for example rnc id = 123 will be coded as 00 01 23 +--OCTET STRING1 +--OCTET STRING2 +--OCTET STRING3 + +MSCId ::= OCTET STRING -- (SIZE (3)) +--National network format , octet order is the same as ISUP signaling +--if spc is coded as 14bit, then OCTET STRING1 will filled with 00,,for example rnc id = 123 will be coded as 00 01 23 +--OCTET STRING1 +--OCTET STRING2 +--OCTET STRING3 + +EmergencyCallFlag ::= ENUMERATED +{ + notEmergencyCall (0), + emergencyCall (1) +} + +CUGIncomingAccessUsed ::= ENUMERATED +{ + callInTheSameCUGGroup (0), + callNotInTheSameCUGGroup (1) +} + +SmsUserDataType ::= OCTET STRING -- (SIZE (1)) +-- +--00 concatenated-short-messages-8-bit-reference-number +--01 special-sms-message-indication +--02 reserved +--03 Value not used to avoid misinterpretation as <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; + diff --git a/FS/FS/cdr/taqua62.pm b/FS/FS/cdr/taqua62.pm index 862018e9c..aa9463008 100644 --- a/FS/FS/cdr/taqua62.pm +++ b/FS/FS/cdr/taqua62.pm @@ -20,7 +20,9 @@ use FS::cdr qw(_cdr_date_parser_maker); my($cdr, $field, $conf, $hashref) = @_; $hashref->{skiprow} = 1 unless ($field == 0 && $cdr->disposition == 100 ) #regular CDR - || ($field == 1 && $cdr->lastapp eq 'acctcode'); #accountcode + || ($field == 1 && $cdr->lastapp eq 'acctcode') #accountcode + || ($field == 1 && $cdr->lastapp eq 'CallerId') #CID blocking + ; $cdr->cdrtypenum($field); }, diff --git a/FS/FS/cdr/telstra.pm b/FS/FS/cdr/telstra.pm index 9e644dbc8..603d5c40b 100644 --- a/FS/FS/cdr/telstra.pm +++ b/FS/FS/cdr/telstra.pm @@ -19,7 +19,7 @@ my %cdr_type_of = ( %info = ( 'name' => 'Telstra LinxOnline', - 'weight' => 20, + 'weight' => 215, 'header' => 1, 'type' => 'fixedlength', # Wholesale Usage Information Record format diff --git a/FS/FS/cdr/u4.pm b/FS/FS/cdr/u4.pm new file mode 100644 index 000000000..1b7a660e7 --- /dev/null +++ b/FS/FS/cdr/u4.pm @@ -0,0 +1,104 @@ +package FS::cdr::u4; + +use strict; +use vars qw(@ISA %info); +use FS::cdr qw(_cdr_date_parser_maker); + +@ISA = qw(FS::cdr); + +%info = ( + 'name' => 'U4', + 'weight' => 490, + 'type' => 'fixedlength', + 'fixedlength_format' => [qw( + CDRType:3:1:3 + MasterAccountID:12:4:15 + SubAccountID:12:16:27 + BillToNumber:18:28:45 + AccountCode:12:46:57 + CallDateStartTime:14:58:71 + TimeOfDay:1:72:72 + CalculatedSeconds:12:73:84 + City:30:85:114 + State:2:115:116 + Country:40:117:156 + Charges:21:157:177 + CallDirection:1:178:178 + CallIndicator:1:179:179 + ReportIndicator:1:180:180 + ANI:10:181:190 + DNIS:10:191:200 + PIN:16:201:216 + OrigNumber:10:217:226 + TermNumber:10:227:236 + DialedNumber:18:237:254 + DisplayNumber:18:255:272 + RecordSource:1:273:273 + LECInfoDigits:2:274:275 + OrigNPA:4:276:279 + OrigNXX:5:280:284 + OrigLATA:3:285:287 + OrigZone:1:288:288 + OrigCircuit:12:289:300 + OrigTrunkGroupCLLI:12:301:312 + TermNPA:4:313:316 + TermNXX:5:317:321 + TermLATA:3:322:324 + TermZone:1:325:325 + TermCircuit:12:326:337 + TermTrunkGroupCLLI:12:338:349 + TermOCN:5:350:354 + )], + # at least that's how they're defined in the spec we have. + # the real CDRs have several differences. + 'import_fields' => [ + '', #CDRType (for now always 'V') + '', #MasterAccountID + '', #SubAccountID + 'charged_party', #BillToNumber + 'accountcode', #AccountCode + _cdr_date_parser_maker('startdate'), + #CallDateTime + '', #TimeOfDay (always 'S') + sub { #CalculatedSeconds + my($cdr, $sec) = @_; + $cdr->duration($sec); + $cdr->billsec($sec); + }, + '', #City + '', #State + '', #Country + 'upstream_price', #Charges + sub { #CallDirection + my ($cdr, $dir) = @_; + $cdr->set('direction', $dir); + if ( $dir eq 'O' ) { + $cdr->set('src', $cdr->charged_party); + } elsif ( $dir eq 'I' ) { + $cdr->set('dst', $cdr->charged_party); + } + }, + '', #CallIndicator #calltype? + '', #ReportIndicator + sub { #ANI + # it appears that it's the "other" number, not necessarily ANI. + my ($cdr, $number) = @_; + if ( $cdr->direction eq 'O' ) { + $cdr->set('dst', $number); + } elsif ( $cdr->direction eq 'I' ) { + $cdr->set('src', $number); + } + }, + '', #DNIS + '', #PIN + '', #OrigNumber + '', #TermNumber + '', #DialedNumber + '', #DisplayNumber + '', #RecordSource + '', #LECInfoDigits + ('') x 13, + ], +); + +1; diff --git a/FS/FS/cdr_cust_pkg_usage.pm b/FS/FS/cdr_cust_pkg_usage.pm new file mode 100644 index 000000000..6ef7f2dea --- /dev/null +++ b/FS/FS/cdr_cust_pkg_usage.pm @@ -0,0 +1,124 @@ +package FS::cdr_cust_pkg_usage; + +use strict; +use base qw( FS::Record ); +use FS::Record qw( qsearch qsearchs ); + +=head1 NAME + +FS::cdr_cust_pkg_usage - Object methods for cdr_cust_pkg_usage records + +=head1 SYNOPSIS + + use FS::cdr_cust_pkg_usage; + + $record = new FS::cdr_cust_pkg_usage \%hash; + $record = new FS::cdr_cust_pkg_usage { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cdr_cust_pkg_usage object represents an allocation of included +usage minutes to a call. FS::cdr_cust_pkg_usage inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item cdrusagenum - primary key + +=item acctid - foreign key to cdr.acctid + +=item pkgusagenum - foreign key to cust_pkg_usage.pkgusagenum + +=item minutes - the number of minutes allocated + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new example. To add the example to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I<hash> method. + +=cut + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'cdr_cust_pkg_usage'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=item delete + +Delete this record from the database. + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=item check + +Checks all fields to make sure this is a valid example. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('cdrusagenum') + || $self->ut_foreign_key('acctid', 'cdr', 'acctid') + || $self->ut_foreign_key('pkgusagenum', 'cust_pkg_usage', 'pkgusagenum') + || $self->ut_number('minutes') + ; + return $error if $error; + + $self->SUPER::check; +} + +=item cust_pkg_usage + +Returns the L<FS::cust_pkg_usage> object that this usage allocation came from. + +=item cdr + +Returns the L<FS::cdr> object that the usage was applied to. + +=cut + +sub cust_pkg_usage { + FS::cust_pkg_usage->by_key($_[0]->pkgusagenum); +} + +sub cdr { + FS::cdr->by_key($_[0]->acctid); +} + +=back + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm index f84af425b..8fcd724a0 100644 --- a/FS/FS/contact.pm +++ b/FS/FS/contact.pm @@ -326,8 +326,8 @@ sub check { || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum') || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum') || $self->ut_foreign_keyn('classnum', 'contact_class', 'classnum') - || $self->ut_textn('last') - || $self->ut_textn('first') + || $self->ut_namen('last') + || $self->ut_namen('first') || $self->ut_textn('title') || $self->ut_textn('comment') || $self->ut_enum('disabled', [ '', 'Y' ]) diff --git a/FS/FS/contact_Mixin.pm b/FS/FS/contact_Mixin.pm new file mode 100644 index 000000000..6e8f315b9 --- /dev/null +++ b/FS/FS/contact_Mixin.pm @@ -0,0 +1,19 @@ +package FS::contact_Mixin; + +use strict; +use FS::Record qw( qsearchs ); +use FS::contact; + +=item contact_obj + +Returns the contact object, if any (see L<FS::contact>). + +=cut + +sub contact_obj { + my $self = shift; + return '' unless $self->contactnum; + qsearchs( 'contact', { 'contactnum' => $self->contactnum } ); +} + +1; diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index e7622d712..8b156c642 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1330,6 +1330,8 @@ invoice and all older invoices is greater than the specified amount. I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required) +I<lpr>, if specified, is passed to + =cut sub queueable_send { @@ -1354,6 +1356,7 @@ sub send { my( $template, $invoice_from, $notice_name ); my $agentnums = ''; my $balance_over = 0; + my $lpr = ''; if ( ref($_[0]) ) { my $opt = shift; @@ -1364,6 +1367,7 @@ sub send { $invoice_from = $opt->{'invoice_from'}; $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'}; $notice_name = $opt->{'notice_name'}; + $lpr = $opt->{'lpr'} } else { $template = scalar(@_) ? shift : ''; if ( scalar(@_) && $_[0] ) { @@ -1397,10 +1401,12 @@ sub send { if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list ) && ! $self->invoice_noemail; + $opt{'lpr'} = $lpr; #$self->print_invoice(\%opt) $self->print(\%opt) if grep { $_ eq 'POST' } @invoicing_list; #postal + #this has never been used post-$ORIGINAL_ISP afaik $self->fax_invoice(\%opt) if grep { $_ eq 'FAX' } @invoicing_list; #fax @@ -1564,14 +1570,16 @@ sub print { return if $self->hide; my $conf = $self->conf; - my( $template, $notice_name ); + my( $template, $notice_name, $lpr ); if ( ref($_[0]) ) { my $opt = shift; $template = $opt->{'template'} || ''; $notice_name = $opt->{'notice_name'} || 'Invoice'; + $lpr = $opt->{'lpr'} } else { $template = scalar(@_) ? shift : ''; $notice_name = 'Invoice'; + $lpr = ''; } my %opt = ( @@ -1584,7 +1592,11 @@ sub print { $self->batch_invoice(\%opt); } else { - do_print $self->lpr_data(\%opt); + do_print( + $self->lpr_data(\%opt), + 'agentnum' => $self->cust_main->agentnum, + 'lpr' => $lpr, + ); } } @@ -2118,10 +2130,13 @@ sub print_csv { $previous_balance = sprintf('%.2f', $previous_balance); my $totaldue = sprintf('%.2f', $self->owed + $previous_balance); my @items = map { - ($_->{pkgnum} || ''), - $_->{description}, - $_->{amount} - } $self->_items_pkg; + $_->{pkgnum}, + $_->{description}, + $_->{amount} + } + $self->_items_pkg, #_items_nontax? no sections or anything + # with this format + $self->_items_tax; $csv->combine( $cust_main->agentnum, @@ -3122,11 +3137,16 @@ sub _items_payments { #something more elaborate if $_->amount ne ->cust_pay->paid ? + my $desc = $self->mt('Payment received').' '. + time2str($date_format,$_->cust_pay->_date ); + $desc .= $self->mt(' via ' . $_->cust_pay->payby_payinfo_pretty) + if ( $self->conf->exists('invoice_payment_details') ); + push @b, { - 'description' => $self->mt('Payment received').' '. - time2str($date_format,$_->cust_pay->_date ), + 'description' => $desc, 'amount' => sprintf("%.2f", $_->amount ) }; + } @b; diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 716c0983e..d8cbf5915 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -1104,8 +1104,7 @@ sub upgrade_tax_location { delete @hash{qw(censustract censusyear latitude longitude coord_auto)}; $hash{custnum} = $h_cust_main->custnum; - my $tax_loc = qsearchs('cust_location', \%hash) # unlikely - || FS::cust_location->new({ %hash }); + my $tax_loc = FS::cust_location->new_or_existing(\%hash); if ( !$tax_loc->locationnum ) { $tax_loc->disabled('Y'); my $error = $tax_loc->insert; diff --git a/FS/FS/cust_bill_pkg_tax_location.pm b/FS/FS/cust_bill_pkg_tax_location.pm index 723d6e0a3..140982e53 100644 --- a/FS/FS/cust_bill_pkg_tax_location.pm +++ b/FS/FS/cust_bill_pkg_tax_location.pm @@ -215,10 +215,8 @@ sub cust_credit_bill_pkg { sub cust_main_county { my $self = shift; - my $result; - if ( $self->taxtype eq 'FS::cust_main_county' ) { - $result = qsearchs( 'cust_main_county', { 'taxnum' => $self->taxnum } ); - } + return '' unless $self->taxtype eq 'FS::cust_main_county'; + qsearchs( 'cust_main_county', { 'taxnum' => $self->taxnum } ); } sub _upgrade_data { diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 05d961c3f..ba279a26c 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -717,7 +717,7 @@ sub credit_lineitems { my %cust_bill_pkg = (); my %cust_credit_bill_pkg = (); my %taxlisthash = (); - my %unapplied_payments; #invoice numbers, and then billpaynums + my %unapplied_payments = (); #invoice numbers, and then billpaynums foreach my $billpkgnum ( @{$arg{billpkgnums}} ) { my $setuprecur = shift @{$arg{setuprecurs}}; my $amount = shift @{$arg{amounts}}; diff --git a/FS/FS/cust_credit_bill_pkg.pm b/FS/FS/cust_credit_bill_pkg.pm index 7427d09ab..3cb44a092 100644 --- a/FS/FS/cust_credit_bill_pkg.pm +++ b/FS/FS/cust_credit_bill_pkg.pm @@ -348,13 +348,13 @@ sub cust_bill_pkg { sub cust_bill_pkg_tax_Xlocation { my $self = shift; - if ($self->billpkg_tax_locationnum) { + if ($self->billpkgtaxlocationnum) { return qsearchs( 'cust_bill_pkg_tax_location', { 'billpkgtaxlocationnum' => $self->billpkgtaxlocationnum }, ); - } elsif ($self->billpkg_tax_rate_locationnum) { + } elsif ($self->billpkgtaxratelocationnum) { return qsearchs( 'cust_bill_pkg_tax_rate_location', { 'billpkgtaxratelocationnum' => $self->billpkgtaxratelocationnum }, diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm index b86529b3d..b12a161db 100644 --- a/FS/FS/cust_location.pm +++ b/FS/FS/cust_location.pm @@ -5,7 +5,7 @@ use strict; use vars qw( $import ); use Locale::Country; use FS::UID qw( dbh driver_name ); -use FS::Record qw( qsearch ); #qsearchs ); +use FS::Record qw( qsearch qsearchs ); use FS::Conf; use FS::prospect_main; use FS::cust_main; @@ -104,6 +104,35 @@ points to. You can ask the object for a copy with the I<hash> method. sub table { 'cust_location'; } +=item new_or_existing HASHREF + +Returns an existing location matching the customer and address fields in +HASHREF, if one exists; otherwise returns a new location containing those +fields. The following fields must match: address1, address2, city, county, +state, zip, country, geocode, disabled. Other fields are only required +to match if they're specified in HASHREF. + +The new location will not be inserted; the calling code must call C<insert> +(or a method such as C<move_to>) to insert it, and check for errors at that +point. + +=cut + +sub new_or_existing { + my $class = shift; + my %hash = ref($_[0]) ? %{$_[0]} : @_; + # if coords are empty, then it doesn't matter if they're auto or not + if ( !$hash{'latitude'} and !$hash{'longitude'} ) { + delete $hash{'coord_auto'}; + } + foreach ( qw(address1 address2 city county state zip country geocode + disabled ) ) { + # empty fields match only empty fields + $hash{$_} = '' if !defined($hash{$_}); + } + return qsearchs('cust_location', \%hash) || $class->new(\%hash); +} + =item insert Adds this record to the database. If there is an error, returns the error, @@ -479,6 +508,20 @@ sub location_label { $prefix . $self->SUPER::location_label(%opt); } +=item county_state_county + +Returns a string consisting of just the county, state and country. + +=cut + +sub county_state_country { + my $self = shift; + my $label = $self->country; + $label = $self->state.", $label" if $self->state; + $label = $self->county." County, $label" if $self->county; + $label; +} + =back =head1 CLASS METHODS diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 45d57cd79..2a4602e19 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -2,7 +2,6 @@ package FS::cust_main; require 5.006; use strict; - #FS::cust_main:_Marketgear when they're ready to move to 2.1 use base qw( FS::cust_main::Packages FS::cust_main::Status FS::cust_main::NationalID FS::cust_main::Billing FS::cust_main::Billing_Realtime @@ -551,14 +550,6 @@ sub insert { } } - if ( $self->can('start_copy_skel') ) { - my $error = $self->start_copy_skel; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - } - warn " ordering packages\n" if $DEBUG > 1; @@ -1787,12 +1778,19 @@ sub check { || $self->ut_floatn('credit_limit') || $self->ut_numbern('billday') || $self->ut_numbern('prorate_day') - || $self->ut_enum('edit_subject', [ '', 'Y' ] ) - || $self->ut_enum('calling_list_exempt', [ '', 'Y' ] ) - || $self->ut_enum('invoice_noemail', [ '', 'Y' ] ) + || $self->ut_flag('edit_subject') + || $self->ut_flag('calling_list_exempt') + || $self->ut_flag('invoice_noemail') + || $self->ut_flag('message_noemail') || $self->ut_enum('locale', [ '', FS::Locales->locales ]) ; + my $company = $self->company; + $company =~ s/^\s+//; + $company =~ s/\s+$//; + $company =~ s/\s+/ /g; + $self->company($company); + #barf. need message catalogs. i18n. etc. $error .= "Please select an advertising source." if $error =~ /^Illegal or empty \(numeric\) refnum: /; @@ -4086,15 +4084,34 @@ sub ship_contact_firstlast { $contact->get('first') . ' '. $contact->get('last'); } -=item country_full +#XXX this doesn't work in 3.x+ +#=item country_full +# +#Returns this customer's full country name +# +#=cut +# +#sub country_full { +# my $self = shift; +# code2country($self->country); +#} + +=item county_state_county [ PREFIX ] -Returns this customer's full country name +Returns a string consisting of just the county, state and country. =cut -sub country_full { +sub county_state_country { my $self = shift; - code2country($self->country); + my $locationnum; + if ( @_ && $_[0] && $self->has_ship_address ) { + $locationnum = $self->ship_locationnum; + } else { + $locationnum = $self->bill_locationnum; + } + my $cust_location = qsearchs('cust_location', { locationnum=>$locationnum }); + $cust_location->county_state_country; } =item geocode DATA_VENDOR @@ -4917,7 +4934,10 @@ sub queueable_print { sub print { my ($self, $template) = (shift, shift); - do_print [ $self->print_ps($template) ]; + do_print( + [ $self->print_ps($template) ], + 'agentnum' => $self->agentnum, + ); } #these three subs should just go away once agent stuff is all config overrides @@ -5059,12 +5079,12 @@ sub process_censustract_update { } #starting to take quite a while for big dbs +# (JRNL: journaled so it only happens once per database) # - seq scan of h_cust_main (yuck), but not going to index paycvv, so -# - seq scan of cust_main on signupdate... index signupdate? will that help? -# - seq scan of cust_main on paydate... index on substrings? maybe set an -# upgrade journal flag now that we have that, yyyy-m-dd paydates are ancient -# - seq scan of cust_main on payinfo.. certainly not going toi ndex that... -# upgrade journal again? this is also an ancient problem +# JRNL seq scan of cust_main on signupdate... index signupdate? will that help? +# JRNL seq scan of cust_main on paydate... index on substrings? maybe set an +# JRNL seq scan of cust_main on payinfo.. certainly not going toi ndex that... +# JRNL leading/trailing spaces in first, last, company # - otaker upgrade? journal and call it good? (double check to make sure # we're not still setting otaker here) # @@ -5119,10 +5139,30 @@ sub _upgrade_data { #class method local($ignore_banned_card) = 1; local($skip_fuzzyfiles) = 1; local($import) = 1; #prevent automatic geocoding (need its own variable?) - $class->_upgrade_otaker(%opts); FS::cust_main::Location->_upgrade_data(%opts); + unless ( FS::upgrade_journal->is_done('cust_main__trimspaces') ) { + + foreach my $cust_main ( qsearch({ + 'table' => 'cust_main', + 'hashref' => {}, + 'extra_sql' => 'WHERE '. + join(' OR ', + map "$_ LIKE ' %' OR $_ LIKE '% ' OR $_ LIKE '% %'", + qw( first last company ) + ), + }) ) { + my $error = $cust_main->replace; + die $error if $error; + } + + FS::upgrade_journal->set_done('cust_main__trimspaces'); + + } + + $class->_upgrade_otaker(%opts); + } =back diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index cd46c7332..939a625c7 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -116,8 +116,13 @@ sub bill_and_collect { $options{'actual_time'} ||= time; my $job = $options{'job'}; + my $actual_time = ( $conf->exists('next-bill-ignore-time') + ? day_end( $options{actual_time} ) + : $options{actual_time} + ); + $job->update_statustext('0,cleaning expired packages') if $job; - $error = $self->cancel_expired_pkgs( day_end( $options{actual_time} ) ); + $error = $self->cancel_expired_pkgs( $actual_time ); if ( $error ) { $error = "Error expiring custnum ". $self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } @@ -125,7 +130,7 @@ sub bill_and_collect { else { warn $error; } } - $error = $self->suspend_adjourned_pkgs( day_end( $options{actual_time} ) ); + $error = $self->suspend_adjourned_pkgs( $actual_time ); if ( $error ) { $error = "Error adjourning custnum ". $self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } @@ -133,7 +138,7 @@ sub bill_and_collect { else { warn $error; } } - $error = $self->unsuspend_resumed_pkgs( day_end( $options{actual_time} ) ); + $error = $self->unsuspend_resumed_pkgs( $actual_time ); if ( $error ) { $error = "Error resuming custnum ".$self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } @@ -410,6 +415,7 @@ sub bill { my @precommit_hooks = (); $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ]; #param checks? + foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) { next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart}; @@ -914,6 +920,11 @@ sub _make_lines { $cust_pkg->pkgpart($part_pkg->pkgpart); + my $cmp_time = ( $conf->exists('next-bill-ignore-time') + ? day_end( $time ) + : $time + ); + ### # bill setup ### @@ -927,7 +938,7 @@ sub _make_lines { and ( $options{'resetup'} || ( ! $cust_pkg->setup && ( ! $cust_pkg->start_date - || $cust_pkg->start_date <= day_end($time) + || $cust_pkg->start_date <= $cmp_time ) && ( ! $conf->exists('disable_setup_suspended_pkgs') || ( $conf->exists('disable_setup_suspended_pkgs') && @@ -975,7 +986,7 @@ sub _make_lines { && ! $cust_pkg->option('no_suspend_bill',1) ) and - ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= day_end($time) ) + ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time ) || ( $part_pkg->plan eq 'voip_cdr' && $part_pkg->option('bill_every_call') ) @@ -999,7 +1010,7 @@ sub _make_lines { #over two params! lets at least switch to a hashref for the rest... my $increment_next_bill = ( $part_pkg->freq ne '0' - && ( $cust_pkg->getfield('bill') || 0 ) <= day_end($time) + && ( $cust_pkg->getfield('bill') || 0 ) <= $cmp_time && !$options{cancel} ); my %param = ( %setup_param, @@ -1027,13 +1038,35 @@ sub _make_lines { if ( $@ ); #base_cancel??? - $unitrecur = $cust_pkg->part_pkg->base_recur || $recur; #XXX uuh + $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better if ( $increment_next_bill ) { - my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0); + my $next_bill; + + if ( my $main_pkg = $cust_pkg->main_pkg ) { + # supplemental package + # to keep in sync with the main package, simulate billing at + # its frequency + my $main_pkg_freq = $main_pkg->part_pkg->freq; + my $supp_pkg_freq = $part_pkg->freq; + my $ratio = $supp_pkg_freq / $main_pkg_freq; + if ( $ratio != int($ratio) ) { + # the UI should prevent setting up packages like this, but just + # in case + return "supplemental package period is not an integer multiple of main package period"; + } + $next_bill = $sdate; + for (1..$ratio) { + $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq ); + } + + } else { + # the normal case + $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0); return "unparsable frequency: ". $part_pkg->freq if $next_bill == -1; + } #pro-rating magic - if $recur_prog fiddled $sdate, want to use that # only for figuring next bill date, nothing else, so, reset $sdate again @@ -1796,8 +1829,9 @@ sub due_cust_event { #??? #my $DEBUG = $opt{'debug'} + $opt{'debug'} ||= 0; # silence some warnings local($DEBUG) = $opt{'debug'} - if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG; + if $opt{'debug'} > $DEBUG; $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG; warn "$me due_cust_event called with options ". diff --git a/FS/FS/cust_main/Location.pm b/FS/FS/cust_main/Location.pm index ba3513b2f..bd0af5348 100644 --- a/FS/FS/cust_main/Location.pm +++ b/FS/FS/cust_main/Location.pm @@ -167,15 +167,29 @@ sub _upgrade_data { $cust_main->set(bill_locationnum => $bill_location->locationnum); if ( $cust_main->get('ship_address1') ) { - my $ship_location = FS::cust_location->new( - { - custnum => $custnum, - map { $_ => $cust_main->get("ship_$_") } location_fields() + # detect duplicates + my $same = 1; + my $ship_location; + foreach (location_fields()) { + if ( length($cust_main->get("ship_$_")) and + $cust_main->get($_) ne $cust_main->get("ship_$_") ) { + $same = 0; } - ); - $error = $ship_location->insert; - die "error migrating service address for customer $custnum: $error" - if $error; + } + + if ( $same ) { + $ship_location = $bill_location; + } else { + $ship_location = FS::cust_location->new( + { + custnum => $custnum, + map { $_ => $cust_main->get("ship_$_") } location_fields() + } + ); + $error = $ship_location->insert; + die "error migrating service address for customer $custnum: $error" + if $error; + } $cust_main->set(ship_locationnum => $ship_location->locationnum); diff --git a/FS/FS/cust_main/Packages.pm b/FS/FS/cust_main/Packages.pm index 395cce7e0..f83bce915 100644 --- a/FS/FS/cust_main/Packages.pm +++ b/FS/FS/cust_main/Packages.pm @@ -29,6 +29,9 @@ These methods are available on FS::cust_main objects; Orders a single package. +Note that if the package definition has supplemental packages, those will +be ordered as well. + Options may be passed as a list of key/value pairs or as a hash reference. Options are: @@ -84,7 +87,7 @@ sub order_pkg { if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'}; my %insert_params = map { $opt->{$_} ? ( $_ => $opt->{$_} ) : () } - qw( ticket_subject ticket_queue ); + qw( ticket_subject ticket_queue allow_pkgpart ); local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -97,17 +100,48 @@ sub order_pkg { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - if ( $opt->{'cust_location'} && - ( ! $cust_pkg->locationnum || $cust_pkg->locationnum == -1 ) ) { - my $error = $opt->{'cust_location'}->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "inserting cust_location (transaction rolled back): $error"; + if ( $opt->{'contactnum'} and $opt->{'contactnum'} != -1 ) { + + $cust_pkg->contactnum($opt->{'contactnum'}); + + } elsif ( $opt->{'contact'} ) { + + if ( ! $opt->{'contact'}->contactnum ) { + # not inserted yet + my $error = $opt->{'contact'}->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "inserting contact (transaction rolled back): $error"; + } } - $cust_pkg->locationnum($opt->{'cust_location'}->locationnum); + $cust_pkg->contactnum($opt->{'contact'}->contactnum); + + #} else { + # + # $cust_pkg->contactnum(); + } - else { + + if ( $opt->{'locationnum'} and $opt->{'locationnum'} != -1 ) { + + $cust_pkg->locationnum($opt->{'locationnum'}); + + } elsif ( $opt->{'cust_location'} ) { + + if ( ! $opt->{'cust_location'}->locationnum ) { + # not inserted yet + my $error = $opt->{'cust_location'}->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "inserting cust_location (transaction rolled back): $error"; + } + } + $cust_pkg->locationnum($opt->{'cust_location'}->locationnum); + + } else { + $cust_pkg->locationnum($self->ship_locationnum); + } $cust_pkg->custnum( $self->custnum ); @@ -141,6 +175,35 @@ sub order_pkg { } } + # add supplemental packages, if any are needed + my $part_pkg = FS::part_pkg->by_key($cust_pkg->pkgpart); + foreach my $link ($part_pkg->supp_part_pkg_link) { + #warn "inserting supplemental package ".$link->dst_pkgpart; + my $pkg = FS::cust_pkg->new({ + 'pkgpart' => $link->dst_pkgpart, + 'pkglinknum' => $link->pkglinknum, + 'custnum' => $self->custnum, + 'main_pkgnum' => $cust_pkg->pkgnum, + 'locationnum' => $cust_pkg->locationnum, + # try to prevent as many surprises as possible + 'pkgbatch' => $cust_pkg->pkgbatch, + 'start_date' => $cust_pkg->start_date, + 'order_date' => $cust_pkg->order_date, + 'expire' => $cust_pkg->expire, + 'adjourn' => $cust_pkg->adjourn, + 'contract_end' => $cust_pkg->contract_end, + 'refnum' => $cust_pkg->refnum, + 'discountnum' => $cust_pkg->discountnum, + 'waive_setup' => $cust_pkg->waive_setup, + 'allow_pkgpart' => $opt->{'allow_pkgpart'}, + }); + $error = $self->order_pkg('cust_pkg' => $pkg); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "inserting supplemental package: $error"; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no error diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index 1047890c3..7dbb7a859 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -18,7 +18,8 @@ use FS::svc_acct; $DEBUG = 0; $me = '[FS::cust_main::Search]'; -@fuzzyfields = ( 'first', 'last', 'company', 'address1' ); +@fuzzyfields = ( 'cust_main.first', 'cust_main.last', 'cust_main.company', + 'cust_location.address1' ); install_callback FS::UID sub { $conf = new FS::Conf; @@ -339,7 +340,7 @@ sub smart_search { my %fuzopts = ( 'hashref' => \%options, 'select' => '', - 'extra_sql' => " AND $agentnums_sql", #agent virtualization + 'extra_sql' => "WHERE $agentnums_sql", #agent virtualization ); if ( $first && $last ) { @@ -355,7 +356,8 @@ sub smart_search { } if ( $conf->exists('address1-search') ) { push @cust_main, - FS::cust_main::Search->fuzzy_search( { 'address1' => $value }, %fuzopts ); + FS::cust_main::Search->fuzzy_search( + { 'cust_location.address1' => $value }, %fuzopts ); } } @@ -644,6 +646,16 @@ sub search { if $params->{'with_email'}; ## + # "with postal mail invoices" checkbox + ## + + push @where, + "EXISTS ( SELECT 1 FROM cust_main_invoice + WHERE cust_main_invoice.custnum = cust_main.custnum + AND dest = 'POST' )" + if $params->{'POST'}; + + ## # "without postal mail invoices" checkbox ## @@ -792,11 +804,19 @@ sub search { @tagnums = grep /^(\d+)$/, @tagnums; if ( @tagnums ) { + if ( $params->{'all_tags'} ) { + foreach ( @tagnums ) { + push @where, 'exists(select 1 from cust_tag where '. + 'cust_tag.custnum = cust_main.custnum and tagnum = '. + $_ . ')'; + } + } else { # matching any tag, not all my $tags_where = "0 < (select count(1) from cust_tag where " . " cust_tag.custnum = cust_main.custnum and tagnum in (" . join(',', @tagnums) . "))"; push @where, $tags_where; + } } } @@ -814,6 +834,12 @@ sub search { my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : ''; my $addl_from = ''; + # always make address fields available in results + for my $pre ('bill_', 'ship_') { + $addl_from .= + 'LEFT JOIN cust_location AS '.$pre.'location '. + 'ON (cust_main.'.$pre.'locationnum = '.$pre.'location.locationnum) '; + } my $count_query = "SELECT COUNT(*) FROM cust_main $extra_sql"; @@ -831,7 +857,8 @@ sub search { if ($params->{'flattened_pkgs'}) { #my $pkg_join = ''; - $addl_from .= ' LEFT JOIN cust_pkg USING ( custnum ) '; + $addl_from .= + ' LEFT JOIN cust_pkg ON ( cust_main.custnum = cust_pkg.custnum ) '; if ($dbh->{Driver}->{Name} eq 'Pg') { @@ -914,7 +941,8 @@ Additional options are the same as FS::Record::qsearch =cut sub fuzzy_search { - my( $self, $fuzzy ) = @_; + my $self = shift; + my $fuzzy = shift; # sensible defaults, then merge in any passed options my %fuzopts = ( 'table' => 'cust_main', @@ -926,6 +954,11 @@ sub fuzzy_search { my @cust_main = (); + my @fuzzy_mod = 'i'; + my $conf = new FS::Conf; + my $fuzziness = $conf->config('fuzzy-fuzziness'); + push @fuzzy_mod, $fuzziness if $fuzziness; + check_and_rebuild_fuzzyfiles(); foreach my $field ( keys %$fuzzy ) { @@ -933,32 +966,31 @@ sub fuzzy_search { next unless scalar(@$all); my %match = (); - $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) ); - - my @fcust = (); - foreach ( keys %match ) { - if ( $field eq 'address1' ) { - #because it lives outside the table - my $addl_from = $fuzopts{addl_from} . - 'JOIN cust_location USING (custnum)'; - my $extra_sql = $fuzopts{extra_sql} . - " AND cust_location.address1 = ".dbh->quote($_); - push @fcust, qsearch({ - %fuzopts, - 'addl_from' => $addl_from, - 'extra_sql' => $extra_sql, - }); - } else { - my $hash = $fuzopts{hashref}; - $hash->{$field} = $_; - push @fcust, qsearch({ - %fuzopts, - 'hashref' => $hash - }); - } + $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, \@fuzzy_mod, @$all ) ); + next if !keys(%match); + + my $in_matches = 'IN (' . + join(',', map { dbh->quote($_) } keys %match) . + ')'; + + my $extra_sql = $fuzopts{extra_sql}; + if ($extra_sql =~ /^\s*where /i or keys %{ $fuzopts{hashref} }) { + $extra_sql .= ' AND '; + } else { + $extra_sql .= 'WHERE '; + } + $extra_sql .= "$field $in_matches"; + + my $addl_from = $fuzopts{addl_from}; + if ( $field =~ /^cust_location/ ) { + $addl_from .= ' JOIN cust_location USING (custnum)'; } - my %fsaw = (); - push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust; + + push @cust_main, qsearch({ + %fuzopts, + 'addl_from' => $addl_from, + 'extra_sql' => $extra_sql, + }); } # we want the components of $fuzzy ANDed, not ORed, but still don't want dupes @@ -997,28 +1029,29 @@ sub rebuild_fuzzyfiles { foreach my $fuzzy ( @fuzzyfields ) { - open(LOCK,">>$dir/cust_main.$fuzzy") - or die "can't open $dir/cust_main.$fuzzy: $!"; - flock(LOCK,LOCK_EX) - or die "can't lock $dir/cust_main.$fuzzy: $!"; + my ($field, $table) = reverse split('\.', $fuzzy); + $table ||= 'cust_main'; - open (CACHE, '>:encoding(UTF-8)', "$dir/cust_main.$fuzzy.tmp") - or die "can't open $dir/cust_main.$fuzzy.tmp: $!"; + open(LOCK,">>$dir/$table.$field") + or die "can't open $dir/$table.$field: $!"; + flock(LOCK,LOCK_EX) + or die "can't lock $dir/$table.$field: $!"; - foreach my $field ( $fuzzy, "ship_$fuzzy" ) { - my $sth = dbh->prepare("SELECT $field FROM cust_main". - " WHERE $field != '' AND $field IS NOT NULL"); - $sth->execute or die $sth->errstr; + open (CACHE, '>:encoding(UTF-8)', "$dir/$table.$field.tmp") + or die "can't open $dir/$table.$field.tmp: $!"; - while ( my $row = $sth->fetchrow_arrayref ) { - print CACHE $row->[0]. "\n"; - } + my $sth = dbh->prepare( + "SELECT $field FROM $table WHERE $field IS NOT NULL AND $field != ''" + ); + $sth->execute or die $sth->errstr; - } + while ( my $row = $sth->fetchrow_arrayref ) { + print CACHE $row->[0]. "\n"; + } - close CACHE or die "can't close $dir/cust_main.$fuzzy.tmp: $!"; + close CACHE or die "can't close $dir/$table.$field.tmp: $!"; - rename "$dir/cust_main.$fuzzy.tmp", "$dir/cust_main.$fuzzy"; + rename "$dir/$table.$field.tmp", "$dir/$table.$field"; close LOCK; } @@ -1037,20 +1070,24 @@ sub append_fuzzyfiles { my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; - foreach my $field (@fuzzyfields) { + foreach my $fuzzy (@fuzzyfields) { + + my ($field, $table) = reverse split('\.', $fuzzy); + $table ||= 'cust_main'; + my $value = shift; if ( $value ) { - open(CACHE, '>>:encoding(UTF-8)', "$dir/cust_main.$field" ) - or die "can't open $dir/cust_main.$field: $!"; + open(CACHE, '>>:encoding(UTF-8)', "$dir/$table.$field" ) + or die "can't open $dir/$table.$field: $!"; flock(CACHE,LOCK_EX) - or die "can't lock $dir/cust_main.$field: $!"; + or die "can't lock $dir/$table.$field: $!"; print CACHE "$value\n"; flock(CACHE,LOCK_UN) - or die "can't unlock $dir/cust_main.$field: $!"; + or die "can't unlock $dir/$table.$field: $!"; close CACHE; } @@ -1064,10 +1101,13 @@ sub append_fuzzyfiles { =cut sub all_X { - my( $self, $field ) = @_; + my( $self, $fuzzy ) = @_; + my ($field, $table) = reverse split('\.', $fuzzy); + $table ||= 'cust_main'; + my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; - open(CACHE, '<:encoding(UTF-8)', "$dir/cust_main.$field") - or die "can't open $dir/cust_main.$field: $!"; + open(CACHE, '<:encoding(UTF-8)', "$dir/$table.$field") + or die "can't open $dir/$table.$field: $!"; my @array = map { chomp; $_; } <CACHE>; close CACHE; \@array; diff --git a/FS/FS/cust_main/_Marketgear.pm b/FS/FS/cust_main/_Marketgear.pm deleted file mode 100644 index 2d3c9270e..000000000 --- a/FS/FS/cust_main/_Marketgear.pm +++ /dev/null @@ -1,146 +0,0 @@ -package FS::cust_main::_Marketgear; - -use strict; -use vars qw( $DEBUG $me $conf ); - -$DEBUG = 0; -$me = '[FS::cust_main::_Marketgear]'; - -install_callback FS::UID sub { - $conf = new FS::Conf; -}; - -sub start_copy_skel { - my $self = shift; - - return '' unless $conf->config('cust_main-skeleton_tables') - && $conf->config('cust_main-skeleton_custnum'); - - warn " inserting skeleton records\n" - if $DEBUG > 1 || $cust_main::DEBUG > 1; - - #'mg_user_preference' => {}, - #'mg_user_indicator_profile.user_indicator_profile_id' => { 'mg_profile_indicator.profile_indicator_id' => { 'mg_profile_details.profile_detail_id' }, }, - #'mg_watchlist_header.watchlist_header_id' => { 'mg_watchlist_details.watchlist_details_id' }, - #'mg_user_grid_header.grid_header_id' => { 'mg_user_grid_details.user_grid_details_id' }, - #'mg_portfolio_header.portfolio_header_id' => { 'mg_portfolio_trades.portfolio_trades_id' => { 'mg_portfolio_trades_positions.portfolio_trades_positions_id' } }, - my @tables = eval(join('\n',$conf->config('cust_main-skeleton_tables'))); - die $@ if $@; - - _copy_skel( 'cust_main', #tablename - $conf->config('cust_main-skeleton_custnum'), #sourceid - $self->custnum, #destid - @tables, #child tables - ); -} - -#recursive subroutine, not a method -sub _copy_skel { - my( $table, $sourceid, $destid, %child_tables ) = @_; - - my $primary_key; - if ( $table =~ /^(\w+)\.(\w+)$/ ) { - ( $table, $primary_key ) = ( $1, $2 ); - } else { - my $dbdef_table = dbdef->table($table); - $primary_key = $dbdef_table->primary_key - or return "$table has no primary key". - " (or do you need to run dbdef-create?)"; - } - - warn " _copy_skel: $table.$primary_key $sourceid to $destid for ". - join (', ', keys %child_tables). "\n" - if $DEBUG > 2; - - foreach my $child_table_def ( keys %child_tables ) { - - my $child_table; - my $child_pkey = ''; - if ( $child_table_def =~ /^(\w+)\.(\w+)$/ ) { - ( $child_table, $child_pkey ) = ( $1, $2 ); - } else { - $child_table = $child_table_def; - - $child_pkey = dbdef->table($child_table)->primary_key; - # or return "$table has no primary key". - # " (or do you need to run dbdef-create?)\n"; - } - - my $sequence = ''; - if ( keys %{ $child_tables{$child_table_def} } ) { - - return "$child_table has no primary key". - " (run dbdef-create or try specifying it?)\n" - unless $child_pkey; - - #false laziness w/Record::insert and only works on Pg - #refactor the proper last-inserted-id stuff out of Record::insert if this - # ever gets use for anything besides a quick kludge for one customer - my $default = dbdef->table($child_table)->column($child_pkey)->default; - $default =~ /^nextval\(\(?'"?([\w\.]+)"?'/i - or return "can't parse $child_table.$child_pkey default value ". - " for sequence name: $default"; - $sequence = $1; - - } - - my @sel_columns = grep { $_ ne $primary_key } - dbdef->table($child_table)->columns; - my $sel_columns = join(', ', @sel_columns ); - - my @ins_columns = grep { $_ ne $child_pkey } @sel_columns; - my $ins_columns = ' ( '. join(', ', $primary_key, @ins_columns ). ' ) '; - my $placeholders = ' ( ?, '. join(', ', map '?', @ins_columns ). ' ) '; - - my $sel_st = "SELECT $sel_columns FROM $child_table". - " WHERE $primary_key = $sourceid"; - warn " $sel_st\n" - if $DEBUG > 2; - my $sel_sth = dbh->prepare( $sel_st ) - or return dbh->errstr; - - $sel_sth->execute or return $sel_sth->errstr; - - while ( my $row = $sel_sth->fetchrow_hashref ) { - - warn " selected row: ". - join(', ', map { "$_=".$row->{$_} } keys %$row ). "\n" - if $DEBUG > 2; - - my $statement = - "INSERT INTO $child_table $ins_columns VALUES $placeholders"; - my $ins_sth =dbh->prepare($statement) - or return dbh->errstr; - my @param = ( $destid, map $row->{$_}, @ins_columns ); - warn " $statement: [ ". join(', ', @param). " ]\n" - if $DEBUG > 2; - $ins_sth->execute( @param ) - or return $ins_sth->errstr; - - #next unless keys %{ $child_tables{$child_table} }; - next unless $sequence; - - #another section of that laziness - my $seq_sql = "SELECT currval('$sequence')"; - my $seq_sth = dbh->prepare($seq_sql) or return dbh->errstr; - $seq_sth->execute or return $seq_sth->errstr; - my $insertid = $seq_sth->fetchrow_arrayref->[0]; - - # don't drink soap! recurse! recurse! okay! - my $error = - _copy_skel( $child_table_def, - $row->{$child_pkey}, #sourceid - $insertid, #destid - %{ $child_tables{$child_table_def} }, - ); - return $error if $error; - - } - - } - - return ''; - -} - -1; diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm index 573359571..a61d67e11 100644 --- a/FS/FS/cust_main_county.pm +++ b/FS/FS/cust_main_county.pm @@ -137,33 +137,6 @@ sub check { } -sub taxname { - my $self = shift; - if ( $self->dbdef_table->column('taxname') ) { - return $self->setfield('taxname', $_[0]) if @_; - return $self->getfield('taxname'); - } - return ''; -} - -sub setuptax { - my $self = shift; - if ( $self->dbdef_table->column('setuptax') ) { - return $self->setfield('setuptax', $_[0]) if @_; - return $self->getfield('setuptax'); - } - return ''; -} - -sub recurtax { - my $self = shift; - if ( $self->dbdef_table->column('recurtax') ) { - return $self->setfield('recurtax', $_[0]) if @_; - return $self->getfield('recurtax'); - } - return ''; -} - =item label OPTIONS Returns a label looking like "Anytown, Alameda County, CA, US". @@ -174,13 +147,10 @@ If the taxname field is set, it will look like If the taxclass is set, then it will be "Anytown, Alameda County, CA, US (International)". -Currently it will not contain the district, even if the city+county+state -is not unique. - -OPTIONS may contain "no_taxclass" (hides taxclass) and/or "no_city" -(hides city). It may also contain "out", in which case, if this -region (district+city+county+state+country) contains no non-zero -taxes, the label will read "Out of taxable region(s)". +OPTIONS may contain "with_taxclass", "with_city", and "with_district" to show +those fields. It may also contain "out", in which case, if this region +(district+city+county+state+country) contains no non-zero taxes, the label +will read "Out of taxable region(s)". =cut @@ -202,12 +172,15 @@ sub label { my $label = $self->country; $label = $self->state.", $label" if $self->state; $label = $self->county." County, $label" if $self->county; - if (!$opt{no_city}) { + if ($opt{with_city}) { $label = $self->city.", $label" if $self->city; + if ($opt{with_district} and $self->district) { + $label = $self->district . ", $label"; + } } # ugly labels when taxclass and taxname are both non-null... # but this is how the tax report does it - if (!$opt{no_taxclass}) { + if ($opt{with_taxclass}) { $label = "$label (".$self->taxclass.')' if $self->taxclass; } $label = $self->taxname." ($label)" if $self->taxname; @@ -512,8 +485,10 @@ sub taxline { # now round and distribute my $extra_cents = sprintf('%.2f', $taxable_cents * $self->tax / 100) * 100 - $tax_cents; + # make sure we have an integer + $extra_cents = sprintf('%.0f', $extra_cents); if ( $extra_cents < 0 ) { - die "nonsense extra_cents value $extra_cents"; # because seriously, wtf + die "nonsense extra_cents value $extra_cents"; } $tax_cents += $extra_cents; my $i = 0; diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index 4535aadb2..0e9e8a716 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -1032,18 +1032,48 @@ sub _upgrade_data { #class method ### # not only cust_pay, but also voided and refunded payments - if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch')) { + if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) { + local $FS::Record::nowarn_classload=1; # really inefficient, but again, only has to run once foreach my $table (qw(cust_pay cust_pay_void cust_refund)) { + my $and_batchnum_is_null = + ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' ); foreach my $object ( qsearch({ table => $table, extra_sql => "WHERE payby IN('CARD','CHEK') ". - "AND paybatch IS NOT NULL", + "AND (paybatch IS NOT NULL ". + "OR (paybatch IS NULL AND auth IS NULL + $and_batchnum_is_null ) )", }) ) { + if ( $object->paybatch eq '' ) { + # repair for a previous upgrade that didn't save 'auth' + my $pkey = $object->primary_key; + # find the last history record that had a paybatch value + my $h = qsearchs({ + table => "h_$table", + hashref => { + $pkey => $object->$pkey, + paybatch => { op=>'!=', value=>''}, + history_action => 'replace_old', + }, + order_by => 'ORDER BY history_date DESC LIMIT 1', + }); + if (!$h) { + warn "couldn't find paybatch history record for $table ".$object->$pkey."\n"; + next; + } + # if the paybatch didn't have an auth string, then it's fine + $h->paybatch =~ /:(\w+):/ or next; + # set paybatch to what it was in that record + $object->set('paybatch', $h->paybatch) + # and then upgrade it like the old records + } + my $parsed = $object->_parse_paybatch; if (keys %$parsed) { $object->set($_ => $parsed->{$_}) foreach keys %$parsed; + $object->set('auth' => $parsed->{authorization}); $object->set('paybatch', ''); my $error = $object->replace; warn "error parsing CARD/CHEK paybatch fields on $object #". @@ -1052,7 +1082,7 @@ sub _upgrade_data { #class method } } #$object } #$table - FS::upgrade_journal->set_done('cust_pay__parse_paybatch'); + FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1'); } } diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm index 9f2e9ddfc..e1e32d3d4 100644 --- a/FS/FS/cust_pay_batch.pm +++ b/FS/FS/cust_pay_batch.pm @@ -9,7 +9,7 @@ use FS::payinfo_Mixin; use FS::cust_main; use FS::cust_bill; -@ISA = qw( FS::payinfo_Mixin FS::Record ); +@ISA = qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record ); # 1 is mostly method/subroutine entry and options # 2 traces progress of some operations @@ -80,7 +80,9 @@ following fields are currently supported: =item country -=item status +=item status - 'Approved' or 'Declined' + +=item error_message - the error returned by the gateway if any =back @@ -289,19 +291,21 @@ sub retriable { ''; } -=item approve PAYBATCH +=item approve OPTIONS Approve this payment. This will replace the existing record with the same paybatchnum, set its status to 'Approved', and generate a payment record (L<FS::cust_pay>). This should only be called from the batch import process. +OPTIONS may contain "gatewaynum", "processor", "auth", and "order_number". + =cut sub approve { # to break up the Big Wall of Code that is import_results my $new = shift; - my $paybatch = shift; + my %opt = @_; my $paybatchnum = $new->paybatchnum; my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum }) or return "paybatchnum $paybatchnum not found"; @@ -317,13 +321,17 @@ sub approve { my $cust_pay = new FS::cust_pay ( { 'custnum' => $new->custnum, 'payby' => $new->payby, - 'paybatch' => $paybatch, 'payinfo' => $new->payinfo || $old->payinfo, 'paid' => $new->paid, '_date' => $new->_date, 'usernum' => $new->usernum, 'batchnum' => $new->batchnum, + 'gatewaynum' => $opt{'gatewaynum'}, + 'processor' => $opt{'processor'}, + 'auth' => $opt{'auth'}, + 'order_number' => $opt{'order_number'} } ); + $error = $cust_pay->insert; if ( $error ) { return "error inserting payment for paybatchnum $paybatchnum: $error\n"; @@ -361,6 +369,12 @@ sub decline { # Void the payment my $cust_pay = qsearchs('cust_pay', { custnum => $new->custnum, + batchnum => $new->batchnum + }); + # these should all be migrated over, but if it's not found, look for + # batchnum in the 'paybatch' field also + $cust_pay ||= qsearchs('cust_pay', { + custnum => $new->custnum, paybatch => $new->batchnum }); if ( !$cust_pay ) { @@ -375,6 +389,7 @@ sub decline { } } # !$old->status $new->status('Declined'); + $new->error_message($reason); my $error = $new->replace($old); if ( $error ) { return "error updating status of paybatchnum $paybatchnum: $error\n"; diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 22a7b2c03..741d440fa 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -1,12 +1,13 @@ package FS::cust_pkg; use strict; -use base qw( FS::otaker_Mixin FS::cust_main_Mixin FS::location_Mixin +use base qw( FS::otaker_Mixin FS::cust_main_Mixin + FS::contact_Mixin FS::location_Mixin FS::m2m_Common FS::option_Common ); use vars qw($disable_agentcheck $DEBUG $me); use Carp qw(cluck); use Scalar::Util qw( blessed ); -use List::Util qw(max); +use List::Util qw(min max); use Tie::IxHash; use Time::Local qw( timelocal timelocal_nocheck ); use MIME::Entity; @@ -17,10 +18,13 @@ use FS::CurrentUser; use FS::cust_svc; use FS::part_pkg; use FS::cust_main; +use FS::contact; use FS::cust_location; use FS::pkg_svc; use FS::cust_bill_pkg; use FS::cust_pkg_detail; +use FS::cust_pkg_usage; +use FS::cdr_cust_pkg_usage; use FS::cust_event; use FS::h_cust_svc; use FS::reg_code; @@ -197,6 +201,15 @@ Previous locationnum =item waive_setup +=item main_pkgnum + +The pkgnum of the package that this package is supplemental to, if any. + +=item pkglinknum + +The package link (L<FS::part_pkg_link>) that defines this supplemental +package, if it is one. + =back Note: setup, last_bill, bill, adjourn, susp, expire, cancel and change_date @@ -214,7 +227,7 @@ Create a new billing item. To add the item to the database, see L<"insert">. =cut sub table { 'cust_pkg'; } -sub cust_linked { $_[0]->cust_main_custnum; } +sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum } sub cust_unlinked_msg { my $self = shift; "WARNING: can't find cust_main.custnum ". $self->custnum. @@ -256,6 +269,12 @@ a ticket will be added to this customer with this subject an optional queue name for ticket additions +=item allow_pkgpart + +Don't check the legality of the package definition. This should be used +when performing a package change that doesn't change the pkgpart (i.e. +a location change). + =back =cut @@ -263,7 +282,8 @@ an optional queue name for ticket additions sub insert { my( $self, %options ) = @_; - my $error = $self->check_pkgpart; + my $error; + $error = $self->check_pkgpart unless $options{'allow_pkgpart'}; return $error if $error; my $part_pkg = $self->part_pkg; @@ -594,13 +614,15 @@ replace methods. sub check { my $self = shift; - $self->locationnum('') if !$self->locationnum || $self->locationnum == -1; + if ( !$self->locationnum or $self->locationnum == -1 ) { + $self->set('locationnum', $self->cust_main->ship_locationnum); + } my $error = $self->ut_numbern('pkgnum') || $self->ut_foreign_key('custnum', 'cust_main', 'custnum') || $self->ut_numbern('pkgpart') - || $self->check_pkgpart + || $self->ut_foreign_keyn('contactnum', 'contact', 'contactnum' ) || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum') || $self->ut_numbern('start_date') || $self->ut_numbern('setup') @@ -616,6 +638,8 @@ sub check { || $self->ut_numbern('agent_pkgid') || $self->ut_enum('recur_show_zero', [ '', 'Y', 'N', ]) || $self->ut_enum('setup_show_zero', [ '', 'Y', 'N', ]) + || $self->ut_foreign_keyn('main_pkgnum', 'cust_pkg', 'pkgnum') + || $self->ut_foreign_keyn('pkglinknum', 'part_pkg_link', 'pkglinknum') ; return $error if $error; @@ -639,14 +663,19 @@ sub check { =item check_pkgpart +Check the pkgpart to make sure it's allowed with the reg_code and/or +promo_code of the package (if present) and with the customer's agent. +Called from C<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 } @@ -730,6 +759,11 @@ sub cancel { my( $self, %options ) = @_; my $error; + # pass all suspend/cancel actions to the main package + if ( $self->main_pkgnum and !$options{'from_main'} ) { + return $self->main_pkg->cancel(%options); + } + my $conf = new FS::Conf; warn "cust_pkg::cancel called with options". @@ -835,6 +869,22 @@ sub cancel { return $error; } + foreach my $supp_pkg ( $self->supplemental_pkgs ) { + $error = $supp_pkg->cancel(%options, 'from_main' => 1); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "canceling supplemental pkg#".$supp_pkg->pkgnum.": $error"; + } + } + + foreach my $usage ( $self->cust_pkg_usage ) { + $error = $usage->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "deleting usage pools: $error"; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; return '' if $date; #no errors @@ -894,6 +944,9 @@ svc_fatal: service provisioning errors are fatal svc_errors: pass an array reference, will be filled in with any provisioning errors +main_pkgnum: link the package as a supplemental package of this one. For +internal use only. + =cut sub uncancel { @@ -902,6 +955,10 @@ sub uncancel { #in case you try do do $uncancel-date = $cust_pkg->uncacel return '' unless $self->get('cancel'); + if ( $self->main_pkgnum and !$options{'main_pkgnum'} ) { + return $self->main_pkg->uncancel(%options); + } + ## # Transaction-alize ## @@ -926,6 +983,7 @@ sub uncancel { bill => ( $options{'bill'} || $self->get('bill') ), uncancel => time, uncancel_pkgnum => $self->pkgnum, + main_pkgnum => ($options{'main_pkgnum'} || ''), map { $_ => $self->get($_) } qw( custnum pkgpart locationnum setup @@ -937,6 +995,7 @@ sub uncancel { my $error = $cust_pkg->insert( 'change' => 1, #supresses any referral credit to a referring customer + 'allow_pkgpart' => 1, # allow this even if the package def is disabled ); if ($error) { $dbh->rollback if $oldAutoCommit; @@ -1023,6 +1082,20 @@ sub uncancel { } ## + # Uncancel any supplemental packages, and make them supplemental to the + # new one. + ## + + foreach my $supp_pkg ( $self->supplemental_pkgs ) { + my $new_pkg; + $error = $supp_pkg->uncancel(%options, 'main_pkgnum' => $cust_pkg->pkgnum); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "canceling supplemental pkg#".$supp_pkg->pkgnum.": $error"; + } + } + + ## # Finish ## @@ -1111,6 +1184,9 @@ of final invoices or unused-time credits unsuspended. This may be more convenient than calling C<unsuspend()> separately. +=item from_main - allows a supplemental package to be suspended, rather +than redirecting the method call to its main package. For internal use. + =back If there is an error, returns the error, otherwise returns false. @@ -1121,6 +1197,11 @@ sub suspend { my( $self, %options ) = @_; my $error; + # pass all suspend/cancel actions to the main package + if ( $self->main_pkgnum and !$options{'from_main'} ) { + return $self->main_pkg->suspend(%options); + } + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -1271,6 +1352,14 @@ sub suspend { } + foreach my $supp_pkg ( $self->supplemental_pkgs ) { + $error = $supp_pkg->suspend(%options, 'from_main' => 1); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "suspending supplemental pkg#".$supp_pkg->pkgnum.": $error"; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no errors @@ -1353,6 +1442,11 @@ sub unsuspend { my( $self, %opt ) = @_; my $error; + # pass all suspend/cancel actions to the main package + if ( $self->main_pkgnum and !$opt{'from_main'} ) { + return $self->main_pkg->unsuspend(%opt); + } + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -1511,6 +1605,14 @@ sub unsuspend { } + foreach my $supp_pkg ( $self->supplemental_pkgs ) { + $error = $supp_pkg->unsuspend(%opt, 'from_main' => 1); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "unsuspending supplemental pkg#".$supp_pkg->pkgnum.": $error"; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no errors @@ -1662,12 +1764,23 @@ sub change { if ( $opt->{'cust_location'} && ( ! $opt->{'locationnum'} || $opt->{'locationnum'} == -1 ) ) { - $error = $opt->{'cust_location'}->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "inserting cust_location (transaction rolled back): $error"; + + if ( ! $opt->{'cust_location'}->locationnum ) { + # not inserted yet + $error = $opt->{'cust_location'}->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "inserting cust_location (transaction rolled back): $error"; + } } $opt->{'locationnum'} = $opt->{'cust_location'}->locationnum; + + } + + # whether to override pkgpart checking on the new package + my $same_pkgpart = 1; + if ( $opt->{'pkgpart'} and ( $opt->{'pkgpart'} != $self->pkgpart ) ) { + $same_pkgpart = 0; } my $unused_credit = 0; @@ -1676,9 +1789,11 @@ sub change { # going to be credited for remaining time, don't keep setup, bill, # or last_bill dates, and DO pass the flag to cancel() to credit # the customer. - if ( $opt->{'pkgpart'} and $opt->{'pkgpart'} != $self->pkgpart ) { + if ( $opt->{'pkgpart'} + and $opt->{'pkgpart'} != $self->pkgpart + and $self->part_pkg->option('unused_credit_change', 1) ) { + $unused_credit = 1; $keep_dates = 0; - $unused_credit = 1 if $self->part_pkg->option('unused_credit_change', 1); $hash{$_} = '' foreach qw(setup bill last_bill); } @@ -1692,6 +1807,12 @@ sub change { # (i.e. customer default location) $opt->{'locationnum'} = $self->locationnum if !exists($opt->{'locationnum'}); + # usually this doesn't matter. the two cases where it does are: + # 1. unused_credit_change + pkgpart change + setup fee on the new package + # and + # 2. (more importantly) changing a package before it's billed + $hash{'waive_setup'} = $self->waive_setup; + # Create the new package. my $cust_pkg = new FS::cust_pkg { custnum => $self->custnum, @@ -1700,8 +1821,8 @@ sub change { locationnum => ( $opt->{'locationnum'} ), %hash, }; - - $error = $cust_pkg->insert( 'change' => 1 ); + $error = $cust_pkg->insert( 'change' => 1, + 'allow_pkgpart' => $same_pkgpart ); if ($error) { $dbh->rollback if $oldAutoCommit; return $error; @@ -1747,6 +1868,84 @@ sub change { $dbh->rollback if $oldAutoCommit; return "Error setting usage values: $error"; } + } else { + # if NOT changing pkgpart, transfer any usage pools over + foreach my $usage ($self->cust_pkg_usage) { + $usage->set('pkgnum', $cust_pkg->pkgnum); + $error = $usage->replace; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error transferring usage pools: $error"; + } + } + } + + # transfer discounts, if we're not changing pkgpart + if ( $same_pkgpart ) { + foreach my $old_discount ($self->cust_pkg_discount_active) { + # don't remove the old discount, we may still need to bill that package. + my $new_discount = new FS::cust_pkg_discount { + 'pkgnum' => $cust_pkg->pkgnum, + 'discountnum' => $old_discount->discountnum, + 'months_used' => $old_discount->months_used, + }; + $error = $new_discount->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error transferring discounts: $error"; + } + } + } + + # Order any supplemental packages. + my $part_pkg = $cust_pkg->part_pkg; + my @old_supp_pkgs = $self->supplemental_pkgs; + my @new_supp_pkgs; + foreach my $link ($part_pkg->supp_part_pkg_link) { + my $old; + foreach (@old_supp_pkgs) { + if ($_->pkgpart == $link->dst_pkgpart) { + $old = $_; + $_->pkgpart(0); # so that it can't match more than once + } + last if $old; + } + # false laziness with FS::cust_main::Packages::order_pkg + my $new = FS::cust_pkg->new({ + pkgpart => $link->dst_pkgpart, + pkglinknum => $link->pkglinknum, + custnum => $self->custnum, + main_pkgnum => $cust_pkg->pkgnum, + locationnum => $cust_pkg->locationnum, + start_date => $cust_pkg->start_date, + order_date => $cust_pkg->order_date, + expire => $cust_pkg->expire, + adjourn => $cust_pkg->adjourn, + contract_end => $cust_pkg->contract_end, + refnum => $cust_pkg->refnum, + discountnum => $cust_pkg->discountnum, + waive_setup => $cust_pkg->waive_setup, + }); + if ( $old and $opt->{'keep_dates'} ) { + foreach (qw(setup bill last_bill)) { + $new->set($_, $old->get($_)); + } + } + $error = $new->insert( allow_pkgpart => $same_pkgpart ); + # transfer services + if ( $old ) { + $error ||= $old->transfer($new); + } + if ( $error and $error > 0 ) { + # no reason why this should ever fail, but still... + $error = "Unable to transfer all services from supplemental package ". + $old->pkgnum; + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + push @new_supp_pkgs, $new; } #Good to go, cancel old package. Notify 'cancel' of whether to credit @@ -1754,6 +1953,7 @@ sub change { #Don't allow billing the package (preceding period packages and/or #outstanding usage) if we are keeping dates (i.e. location changing), #because the new package will be billed for the same date range. + #Supplemental packages are also canceled here. $error = $self->cancel( quiet => 1, unused_credit => $unused_credit, @@ -1766,7 +1966,9 @@ sub change { if ( $conf->exists('cust_pkg-change_pkgpart-bill_now') ) { #$self->cust_main - my $error = $cust_pkg->cust_main->bill( 'pkg_list' => [ $cust_pkg ] ); + my $error = $cust_pkg->cust_main->bill( + 'pkg_list' => [ $cust_pkg, @new_supp_pkgs ] + ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -1779,6 +1981,24 @@ sub change { } +=item set_quantity QUANTITY + +Change the package's quantity field. This is the one package property +that can safely be changed without canceling and reordering the package +(because it doesn't affect tax eligibility). Returns an error or an +empty string. + +=cut + +sub set_quantity { + my $self = shift; + $self = $self->replace_old; # just to make sure + my $qty = shift; + ($qty =~ /^\d+$/ and $qty > 0) or return "bad package quantity $qty"; + $self->set('quantity' => $qty); + $self->replace; +} + use Storable 'thaw'; use MIME::Base64; sub process_bulk_cust_pkg { @@ -2469,7 +2689,7 @@ sub statuscolor { =item pkg_label Returns a label for this package. (Currently "pkgnum: pkg - comment" or -"pkg-comment" depending on user preference). +"pkg - comment" depending on user preference). =cut @@ -2496,6 +2716,17 @@ sub pkg_label_long { $label; } +=item pkg_locale + +Returns a customer-localized label for this package. + +=cut + +sub pkg_locale { + my $self = shift; + $self->part_pkg->pkg_locale( $self->cust_main->locale ); +} + =item primary_cust_svc Returns a primary service (as FS::cust_svc object) if one can be identified. @@ -3137,6 +3368,207 @@ sub cust_pkg_discount_active { grep { $_->status eq 'active' } $self->cust_pkg_discount; } +=item cust_pkg_usage + +Returns a list of all voice usage counters attached to this package. + +=cut + +sub cust_pkg_usage { + my $self = shift; + qsearch('cust_pkg_usage', { pkgnum => $self->pkgnum }); +} + +=item apply_usage OPTIONS + +Takes the following options: +- cdr: a call detail record (L<FS::cdr>) +- rate_detail: the rate determined for this call (L<FS::rate_detail>) +- minutes: the maximum number of minutes to be charged + +Finds available usage minutes for a call of this class, and subtracts +up to that many minutes from the usage pool. If the usage pool is empty, +and the C<cdr-minutes_priority> global config option is set, minutes may +be taken from other calls as well. Either way, an allocation record will +be created (L<FS::cdr_cust_pkg_usage>) and this method will return the +number of minutes of usage applied to the call. + +=cut + +sub apply_usage { + my ($self, %opt) = @_; + my $cdr = $opt{cdr}; + my $rate_detail = $opt{rate_detail}; + my $minutes = $opt{minutes}; + my $classnum = $rate_detail->classnum; + my $pkgnum = $self->pkgnum; + my $custnum = $self->custnum; + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + my $order = FS::Conf->new->config('cdr-minutes_priority'); + + my $is_classnum; + if ( $classnum ) { + $is_classnum = ' part_pkg_usage_class.classnum = '.$classnum; + } else { + $is_classnum = ' part_pkg_usage_class.classnum IS NULL'; + } + my @usage_recs = qsearch({ + 'table' => 'cust_pkg_usage', + 'addl_from' => ' JOIN part_pkg_usage USING (pkgusagepart)'. + ' JOIN cust_pkg USING (pkgnum)'. + ' JOIN part_pkg_usage_class USING (pkgusagepart)', + 'select' => 'cust_pkg_usage.*', + 'extra_sql' => " WHERE ( cust_pkg.pkgnum = $pkgnum OR ". + " ( cust_pkg.custnum = $custnum AND ". + " part_pkg_usage.shared IS NOT NULL ) ) AND ". + $is_classnum . ' AND '. + " cust_pkg_usage.minutes > 0", + 'order_by' => " ORDER BY priority ASC", + }); + + my $orig_minutes = $minutes; + my $error; + while (!$error and $minutes > 0 and @usage_recs) { + my $cust_pkg_usage = shift @usage_recs; + $cust_pkg_usage->select_for_update; + my $cdr_cust_pkg_usage = FS::cdr_cust_pkg_usage->new({ + pkgusagenum => $cust_pkg_usage->pkgusagenum, + acctid => $cdr->acctid, + minutes => min($cust_pkg_usage->minutes, $minutes), + }); + $cust_pkg_usage->set('minutes', + sprintf('%.0f', $cust_pkg_usage->minutes - $cdr_cust_pkg_usage->minutes) + ); + $error = $cust_pkg_usage->replace || $cdr_cust_pkg_usage->insert; + $minutes -= $cdr_cust_pkg_usage->minutes; + } + if ( $order and $minutes > 0 and !$error ) { + # then try to steal minutes from another call + my %search = ( + 'table' => 'cdr_cust_pkg_usage', + 'addl_from' => ' JOIN cust_pkg_usage USING (pkgusagenum)'. + ' JOIN part_pkg_usage USING (pkgusagepart)'. + ' JOIN cust_pkg USING (pkgnum)'. + ' JOIN part_pkg_usage_class USING (pkgusagepart)'. + ' JOIN cdr USING (acctid)', + 'select' => 'cdr_cust_pkg_usage.*', + 'extra_sql' => " WHERE cdr.freesidestatus = 'rated' AND ". + " ( cust_pkg.pkgnum = $pkgnum OR ". + " ( cust_pkg.custnum = $custnum AND ". + " part_pkg_usage.shared IS NOT NULL ) ) AND ". + " part_pkg_usage_class.classnum = $classnum", + 'order_by' => ' ORDER BY part_pkg_usage.priority ASC', + ); + if ( $order eq 'time' ) { + # find CDRs that are using minutes, but have a later startdate + # than this call + my $startdate = $cdr->startdate; + if ($startdate !~ /^\d+$/) { + die "bad cdr startdate '$startdate'"; + } + $search{'extra_sql'} .= " AND cdr.startdate > $startdate"; + # minimize needless reshuffling + $search{'order_by'} .= ', cdr.startdate DESC'; + } else { + # XXX may not work correctly with rate_time schedules. Could + # fix this by storing ratedetailnum in cdr_cust_pkg_usage, I + # think... + $search{'addl_from'} .= + ' JOIN rate_detail'. + ' ON (cdr.rated_ratedetailnum = rate_detail.ratedetailnum)'; + if ( $order eq 'rate_high' ) { + $search{'extra_sql'} .= ' AND rate_detail.min_charge < '. + $rate_detail->min_charge; + $search{'order_by'} .= ', rate_detail.min_charge ASC'; + } elsif ( $order eq 'rate_low' ) { + $search{'extra_sql'} .= ' AND rate_detail.min_charge > '. + $rate_detail->min_charge; + $search{'order_by'} .= ', rate_detail.min_charge DESC'; + } else { + # this should really never happen + die "invalid cdr-minutes_priority value '$order'\n"; + } + } + my @cdr_usage_recs = qsearch(\%search); + my %reproc_cdrs; + while (!$error and @cdr_usage_recs and $minutes > 0) { + my $cdr_cust_pkg_usage = shift @cdr_usage_recs; + my $cust_pkg_usage = $cdr_cust_pkg_usage->cust_pkg_usage; + my $old_cdr = $cdr_cust_pkg_usage->cdr; + $reproc_cdrs{$old_cdr->acctid} = $old_cdr; + $cdr_cust_pkg_usage->select_for_update; + $old_cdr->select_for_update; + $cust_pkg_usage->select_for_update; + # in case someone else stole the usage from this CDR + # while waiting for the lock... + next if $old_cdr->acctid != $cdr_cust_pkg_usage->acctid; + # steal the usage allocation and flag the old CDR for reprocessing + $cdr_cust_pkg_usage->set('acctid', $cdr->acctid); + # if the allocation is more minutes than we need, adjust it... + my $delta = $cdr_cust_pkg_usage->minutes - $minutes; + if ( $delta > 0 ) { + $cdr_cust_pkg_usage->set('minutes', $minutes); + $cust_pkg_usage->set('minutes', $cust_pkg_usage->minutes + $delta); + $error = $cust_pkg_usage->replace; + } + #warn 'CDR '.$cdr->acctid . ' stealing allocation '.$cdr_cust_pkg_usage->cdrusagenum.' from CDR '.$old_cdr->acctid."\n"; + $error ||= $cdr_cust_pkg_usage->replace; + # deduct the stolen minutes + $minutes -= $cdr_cust_pkg_usage->minutes; + } + # after all minute-stealing is done, reset the affected CDRs + foreach (values %reproc_cdrs) { + $error ||= $_->set_status(''); + # XXX or should we just call $cdr->rate right here? + # it's not like we can create a loop this way, since the min_charge + # or call time has to go monotonically in one direction. + # we COULD get some very deep recursions going, though... + } + } # if $order and $minutes + if ( $error ) { + $dbh->rollback; + die "error applying included minutes\npkgnum ".$self->pkgnum.", class $classnum, acctid ".$cdr->acctid."\n$error\n" + } else { + $dbh->commit if $oldAutoCommit; + return $orig_minutes - $minutes; + } +} + +=item supplemental_pkgs + +Returns a list of all packages supplemental to this one. + +=cut + +sub supplemental_pkgs { + my $self = shift; + qsearch('cust_pkg', { 'main_pkgnum' => $self->pkgnum }); +} + +=item main_pkg + +Returns the package that this one is supplemental to, if any. + +=cut + +sub main_pkg { + my $self = shift; + if ( $self->main_pkgnum ) { + return FS::cust_pkg->by_key($self->main_pkgnum); + } + return; +} + =back =head1 CLASS METHODS @@ -3664,10 +4096,10 @@ sub search { my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : ''; - my $addl_from = 'LEFT JOIN cust_main USING ( custnum ) '. - 'LEFT JOIN part_pkg USING ( pkgpart ) '. + my $addl_from = 'LEFT JOIN part_pkg USING ( pkgpart ) '. 'LEFT JOIN pkg_class ON ( part_pkg.classnum = pkg_class.classnum ) '. - 'LEFT JOIN cust_location USING ( locationnum ) '; + 'LEFT JOIN cust_location USING ( locationnum ) '. + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); my $select; my $count_query; @@ -3951,11 +4383,25 @@ sub order { %hash, }; $error = $cust_pkg->insert( 'change' => $change ); + push @$return_cust_pkg, $cust_pkg; + + foreach my $link ($cust_pkg->part_pkg->supp_part_pkg_link) { + my $supp_pkg = FS::cust_pkg->new({ + custnum => $custnum, + pkgpart => $link->dst_pkgpart, + refnum => $refnum, + main_pkgnum => $cust_pkg->pkgnum, + %hash, + }); + $error ||= $supp_pkg->insert( 'change' => $change ); + push @$return_cust_pkg, $supp_pkg; + } + if ($error) { $dbh->rollback if $oldAutoCommit; return $error; } - push @$return_cust_pkg, $cust_pkg; + } # $return_cust_pkg now contains refs to all of the newly # created packages. diff --git a/FS/FS/cust_pkg_discount.pm b/FS/FS/cust_pkg_discount.pm index 5f4d0dccf..d82d94990 100644 --- a/FS/FS/cust_pkg_discount.pm +++ b/FS/FS/cust_pkg_discount.pm @@ -164,7 +164,7 @@ sub check { $self->ut_numbern('pkgdiscountnum') || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum') || $self->ut_foreign_key('discountnum', 'discount', 'discountnum' ) - || $self->ut_float('months_used') #actually decimal, but this will do + || $self->ut_sfloat('months_used') #actually decimal, but this will do || $self->ut_numbern('end_date') || $self->ut_alphan('otaker') || $self->ut_numbern('usernum') @@ -202,7 +202,7 @@ sub discount { qsearchs('discount', { 'discountnum' => $self->discountnum } ); } -=item increment_months_used +=item increment_months_used MONTHS Increments months_used by the given parameter @@ -216,6 +216,31 @@ sub increment_months_used { $self->replace(); } +=item decrement_months_used MONTHS + +Decrement months_used by the given parameter + +(Note: as in, extending the length of the discount. Typically only used to +stack/extend a discount when the customer package has one active already.) + +=cut + +sub decrement_months_used { + my( $self, $recharged ) = @_; + #UPDATE cust_pkg_discount SET months_used = months_used - ? + #leaves no history, and billing is mutexed per-customer + + #we're run from part_event/Action/referral_pkg_discount on behalf of a + # different customer, so we need to grab this customer's mutex. + # incidentally, that's some inelegant encapsulation breaking shit, and a + # great argument in favor of native-DB trigger history so we can trust + # in normal ACID like the SQL above instead of this + $self->cust_pkg->cust_main->select_for_update; + + $self->months_used( $self->months_used - $recharged ); + $self->replace(); +} + =item status =cut diff --git a/FS/FS/cust_pkg_usage.pm b/FS/FS/cust_pkg_usage.pm new file mode 100644 index 000000000..0eefd7480 --- /dev/null +++ b/FS/FS/cust_pkg_usage.pm @@ -0,0 +1,163 @@ +package FS::cust_pkg_usage; + +use strict; +use base qw( FS::Record ); +use FS::cust_pkg; +use FS::part_pkg_usage; +use FS::Record qw( qsearch qsearchs ); + +=head1 NAME + +FS::cust_pkg_usage - Object methods for cust_pkg_usage records + +=head1 SYNOPSIS + + use FS::cust_pkg_usage; + + $record = new FS::cust_pkg_usage \%hash; + $record = new FS::cust_pkg_usage { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_pkg_usage object represents a counter of remaining included +minutes on a voice-call package. FS::cust_pkg_usage inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item pkgusagenum - primary key + +=item pkgnum - the package (L<FS::cust_pkg>) containing the usage + +=item pkgusagepart - the usage stock definition (L<FS::part_pkg_usage>). +This record in turn links to the call usage classes that are eligible to +use these minutes. + +=item minutes - the remaining minutes + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +# the new method can be inherited from FS::Record, if a table method is defined + +=cut + +sub table { 'cust_pkg_usage'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# the insert method can be inherited from FS::Record + +=item delete + +Delete this record from the database. + +=cut + +sub delete { + my $self = shift; + my $error = $self->reset || $self->SUPER::delete; +} + +=item reset + +Remove all allocations of this usage to CDRs. + +=cut + +sub reset { + my $self = shift; + my $error = ''; + foreach (qsearch('cdr_cust_pkg_usage', { pkgusagenum => $self->pkgusagenum })) + { + $error ||= $_->delete; + } + $error; +} + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +# the replace method can be inherited from FS::Record + +=item check + +Checks all fields to make sure this is a valid example. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('pkgusagenum') + || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum') + || $self->ut_numbern('minutes') + || $self->ut_foreign_key('pkgusagepart', 'part_pkg_usage', 'pkgusagepart') + ; + return $error if $error; + + if ( $self->minutes eq '' ) { + $self->set(minutes => $self->part_pkg_usage->minutes); + } + + $self->SUPER::check; +} + +=item cust_pkg + +Return the L<FS::cust_pkg> linked to this record. + +=item part_pkg_usage + +Return the L<FS::part_pkg_usage> linked to this record. + +=cut + +sub cust_pkg { + my $self = shift; + FS::cust_pkg->by_key($self->pkgnum); +} + +sub part_pkg_usage { + my $self = shift; + FS::part_pkg_usage->by_key($self->pkgusagepart); +} + +=back + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index b608b2349..165384048 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -13,6 +13,7 @@ use FS::pkg_svc; use FS::domain_record; use FS::part_export; use FS::cdr; +use FS::UI::Web; #most FS::svc_ classes are autoloaded in svc_x emthod use FS::svc_acct; #this one is used in the cache stuff @@ -883,7 +884,7 @@ sub smart_search_param { my $extra_sql = ' WHERE '.join(' AND ', @extra_sql); #for agentnum my $addl_from = ' LEFT JOIN cust_pkg USING ( pkgnum )'. - ' LEFT JOIN cust_main USING ( custnum )'. + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'). ' LEFT JOIN part_svc USING ( svcpart )'; ( @@ -894,6 +895,48 @@ sub smart_search_param { ); } +sub _upgrade_data { + my $class = shift; + + # fix missing (deleted by mistake) svc_x records + warn "searching for missing svc_x records...\n"; + my %search = ( + 'table' => 'cust_svc', + 'select' => 'cust_svc.*', + 'addl_from' => ' LEFT JOIN ( ' . + join(' UNION ', + map { "SELECT svcnum FROM $_" } + FS::part_svc->svc_tables + ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum', + 'extra_sql' => ' WHERE svc_all.svcnum IS NULL', + ); + my @svcs = qsearch(\%search); + warn "found ".scalar(@svcs)."\n"; + + local $FS::Record::nowarn_classload = 1; # for h_svc_ + local $FS::svc_Common::noexport_hack = 1; # because we're inserting services + + my %h_search = ( + 'hashref' => { history_action => 'delete' }, + 'order_by' => ' ORDER BY history_date DESC LIMIT 1', + ); + foreach my $cust_svc (@svcs) { + my $svcnum = $cust_svc->svcnum; + my $svcdb = $cust_svc->part_svc->svcdb; + $h_search{'hashref'}{'svcnum'} = $svcnum; + $h_search{'table'} = "h_$svcdb"; + my $h_svc_x = qsearchs(\%h_search) + or next; + my $class = "FS::$svcdb"; + my $new_svc_x = $class->new({ $h_svc_x->hash }); + my $error = $new_svc_x->insert; + warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n" + if $error; + } + + ''; +} + =back =head1 BUGS diff --git a/FS/FS/export_svc.pm b/FS/FS/export_svc.pm index 0370f5f0b..b08f8f7c3 100644 --- a/FS/FS/export_svc.pm +++ b/FS/FS/export_svc.pm @@ -5,6 +5,7 @@ use vars qw( @ISA ); use FS::Record qw( qsearch qsearchs dbh ); use FS::part_export; use FS::part_svc; +use FS::svc_export_machine; @ISA = qw(FS::Record); @@ -209,6 +210,19 @@ sub insert { } #end of duplicate check, whew $error = $self->SUPER::insert; + + my $part_export = $self->part_export; + if ( !$error and $part_export->default_machine ) { + foreach my $cust_svc ( $self->part_svc->cust_svc ) { + my $svc_export_machine = FS::svc_export_machine->new({ + 'exportnum' => $self->exportnum, + 'svcnum' => $cust_svc->svcnum, + 'machinenum' => $part_export->default_machine, + }); + $error ||= $svc_export_machine->insert; + } + } + if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -251,7 +265,23 @@ Delete this record from the database. =cut -# the delete method can be inherited from FS::Record +sub delete { + my $self = shift; + my $dbh = dbh; + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + + my $error = $self->SUPER::delete; + foreach ($self->svc_export_machine) { + $error ||= $_->delete; + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; +} + =item replace OLD_RECORD @@ -307,6 +337,24 @@ sub part_svc { qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } ); } +=item svc_export_machine + +Returns all export hostname records (L<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 diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index e38346a66..2f5e4762a 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -3,7 +3,7 @@ package FS::msg_template; use strict; use base qw( FS::Record ); use Text::Template; -use FS::Misc qw( generate_email send_email ); +use FS::Misc qw( generate_email send_email do_print ); use FS::Conf; use FS::Record qw( qsearch qsearchs ); use FS::UID qw( dbh ); @@ -457,24 +457,13 @@ sub render { my %hash = $self->prepare(%opt); my $html = $hash{'html_body'}; - my $tmp = 'msg'.$self->msgnum.'-'.time2str('%Y%m%d', time).'-XXXXXXXX'; - my $dir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc"; - # Graphics/stylesheets should probably go in /var/www on the Freeside # machine. my $kit = PDF::WebKit->new(\$html); #%options # hack to use our wrapper script $kit->configure(sub { shift->wkhtmltopdf('freeside-wkhtmltopdf') }); - my $fh = File::Temp->new( - TEMPLATE => $tmp, - DIR => $dir, - UNLINK => 0, - SUFFIX => '.pdf' - ); - print $fh $kit->to_pdf; - close $fh; - return $fh->filename; + $kit->to_pdf; } =item print OPTIONS @@ -484,13 +473,10 @@ Render a PDF and send it to the printer. OPTIONS are as for 'render'. =cut sub print { - my $file = render(@_); - my @lpr = $conf->config('lpr'); - run ([@lpr, '-r'], '<', $file) - or die "lpr error:\n$?\n"; + my( $self, %opt ) = @_; + do_print( [ $self->render(%opt) ], agentnum=>$opt{cust_main}->agentnum ); } - # helper sub for package dates my $ymd = sub { $_[0] ? time2str('%Y-%m-%d', $_[0]) : '' }; diff --git a/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm b/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm index 73d32e0a7..33aeadd35 100644 --- a/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm +++ b/FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm @@ -2,6 +2,7 @@ package FS::part_event::Action::Mixin::credit_agent_pkg_class; use base qw( FS::part_event::Action::Mixin::credit_pkg ); use strict; +use FS::Record qw(qsearchs); sub option_fields { my $class = shift; diff --git a/FS/FS/part_event/Action/Mixin/credit_pkg.pm b/FS/FS/part_event/Action/Mixin/credit_pkg.pm index 9dcd701a9..a3c1d6efb 100644 --- a/FS/FS/part_event/Action/Mixin/credit_pkg.pm +++ b/FS/FS/part_event/Action/Mixin/credit_pkg.pm @@ -16,18 +16,24 @@ sub option_fields { 'type' => 'input-percentage', 'default' => '100', }, - 'what' => { 'label' => 'Of', - 'type' => 'select', - #add additional ways to specify in the package def - 'options' => [ qw( base_recur_permonth unit_setup recur_cost_permonth setup_cost ) ], - 'labels' => { 'base_recur_permonth' => 'Base monthly fee', - 'unit_setup' => 'Setup fee', - 'recur_cost_permonth' => 'Monthly cost', - 'setup_cost' => 'Setup cost', - }, - }, + 'what' => { + 'label' => 'Of', + 'type' => 'select', + #add additional ways to specify in the package def + 'options' => [qw( + base_recur_permonth cust_bill_pkg_recur recur_cost_permonth + unit_setup setup_cost + )], + 'labels' => { + 'base_recur_permonth' => 'Base monthly fee', + 'cust_bill_pkg_recur' => 'Actual invoiced amount of most recent'. + ' recurring charge', + 'recur_cost_permonth' => 'Monthly cost', + 'unit_setup' => 'Setup fee', + 'setup_cost' => 'Setup cost', + }, + }, ); - } #my %no_cust_pkg = ( 'setup_cost' => 1 ); diff --git a/FS/FS/part_event/Action/cust_bill_send_reminder.pm b/FS/FS/part_event/Action/cust_bill_send_reminder.pm index 2ba8136dd..073bb8fd3 100644 --- a/FS/FS/part_event/Action/cust_bill_send_reminder.pm +++ b/FS/FS/part_event/Action/cust_bill_send_reminder.pm @@ -11,9 +11,10 @@ sub eventtable_hashref { sub option_fields { ( - 'notice_name' => 'Reminder name', - #'notes' => { 'label' => 'Reminder notes' }, + 'notice_name' => 'Reminder name', + #'notes' => { 'label' => 'Reminder notes' }, #include standard notes? no/prepend/append + 'lpr' => 'Optional alternate print command', ); } @@ -25,7 +26,10 @@ sub do_action { #my $cust_main = $self->cust_main($cust_bill); #my $cust_main = $cust_bill->cust_main; - $cust_bill->send({ 'notice_name' => $self->option('notice_name') }); + $cust_bill->send({ + 'notice_name' => $self->option('notice_name'), + 'lpr' => $self->option('lpr'), + }); } 1; diff --git a/FS/FS/part_event/Action/referral_pkg_billdate.pm b/FS/FS/part_event/Action/referral_pkg_billdate.pm new file mode 100644 index 000000000..6b485e59b --- /dev/null +++ b/FS/FS/part_event/Action/referral_pkg_billdate.pm @@ -0,0 +1,59 @@ +package FS::part_event::Action::referral_pkg_billdate; + +use strict; +use base qw( FS::part_event::Action ); + +sub description { "Increment the referring customer's package's next bill date"; } + +#sub eventtable_hashref { +#} + +sub option_fields { + ( + 'if_pkgpart' => { 'label' => 'Only packages', + 'type' => 'select-part_pkg', + 'multiple' => 1, + }, + 'increment' => { 'label' => 'Increment by', + 'type' => 'freq', + 'value' => '1m', + }, + ); +} + +#false laziness w/referral_pkg_discount, probably should make +# Mixin/referral_pkg.pm if we need changes or anything else in this vein +sub do_action { + my( $self, $cust_object, $cust_event ) = @_; + + my $cust_main = $self->cust_main($cust_object); + + return 'No referring customer' unless $cust_main->referral_custnum; + + my $referring_cust_main = $cust_main->referring_cust_main; + #return 'Referring customer is cancelled' + # if $referring_cust_main->status eq 'cancelled'; + + my %if_pkgpart = map { $_=>1 } split(/\s*,\s*/, $self->option('if_pkgpart') ); + my @cust_pkg = grep $if_pkgpart{ $_->pkgpart }, + $referring_cust_main->billing_pkgs; + return 'No qualifying billing package definition' unless @cust_pkg; + + my $cust_pkg = $cust_pkg[0]; #only one + + #end of false laziness + + my $bill = $cust_pkg->bill || $cust_pkg->setup || time; + + $cust_pkg->bill( + $cust_pkg->part_pkg->add_freq( $bill, $self->option('increment') ) + ); + + my $error = $cust_pkg->replace; + die "Error incrementing next bill date: $error" if $error; + + ''; + +} + +1; diff --git a/FS/FS/part_event/Action/referral_pkg_discount.pm b/FS/FS/part_event/Action/referral_pkg_discount.pm new file mode 100644 index 000000000..2ff1b35fb --- /dev/null +++ b/FS/FS/part_event/Action/referral_pkg_discount.pm @@ -0,0 +1,101 @@ +package FS::part_event::Action::referral_pkg_discount; + +use strict; +use base qw( FS::part_event::Action ); + +sub description { "Discount the referring customer's package"; } + +#sub eventtable_hashref { +#} + +sub option_fields { + ( + 'if_pkgpart' => { 'label' => 'Only packages', + 'type' => 'select-part_pkg', + 'multiple' => 1, + }, + 'discountnum' => { 'label' => 'Discount', + 'type' => 'select-table', #we don't handle the select-discount create a discount case + 'table' => 'discount', + 'name_col' => 'description', #well, method + 'order_by' => 'ORDER BY discountnum', #requied because name_col is a method + 'hashref' => { 'disabled' => '', + 'months' => { op=>'!=', value=>'0' }, + }, + 'disable_empty' => 1, + }, + ); +} + +#false laziness w/referral_pkg_billdate, probably should make +# Mixin/referral_pkg.pm if we need changes or anything else in this vein +sub do_action { + my( $self, $cust_object, $cust_event ) = @_; + + my $cust_main = $self->cust_main($cust_object); + + return 'No referring customer' unless $cust_main->referral_custnum; + + my $referring_cust_main = $cust_main->referring_cust_main; + #return 'Referring customer is cancelled' + # if $referring_cust_main->status eq 'cancelled'; + + my %if_pkgpart = map { $_=>1 } split(/\s*,\s*/, $self->option('if_pkgpart') ); + my @cust_pkg = grep $if_pkgpart{ $_->pkgpart }, + $referring_cust_main->billing_pkgs; + return 'No qualifying billing package definition' unless @cust_pkg; + + my $cust_pkg = $cust_pkg[0]; #only one + + #end of false laziness + + my @cust_pkg_discount = $cust_pkg->cust_pkg_discount_active; + my @my_cust_pkg_discount = + grep { $_->discountnum == $self->option('discountnum') } @cust_pkg_discount; + + if ( @my_cust_pkg_discount ) { #increment the existing one instead + + die "guru meditation #and: multiple discounts" + if scalar(@my_cust_pkg_discount) > 1; + + my $cust_pkg_discount = $my_cust_pkg_discount[0]; + my $discount = $cust_pkg_discount->discount; + die "guru meditation #goob: can't extended non-expiring discount" + if $discount->months == 0; + + my $error = $cust_pkg_discount->decrement_months_used( $discount->months ); + die "Error extending discount: $error\n" if $error; + + } elsif ( @cust_pkg_discount ) { + + #"stacked" discount case not possible from UI, not handled, so prevent + # against creating one here. i guess we could try to find a different + # @cust_pkg above if this case needed to be handled better? + die "Can't discount an already discounted package"; + + } else { #normal case, create a new one + + my $cust_pkg_discount = new FS::cust_pkg_discount { + 'pkgnum' => $cust_pkg->pkgnum, + 'discountnum' => $self->option('discountnum'), + 'months_used' => 0, + #'end_date' => '', + #we dont handle the create a new discount case + #'_type' => scalar($cgi->param('discountnum__type')), + #'amount' => scalar($cgi->param('discountnum_amount')), + #'percent' => scalar($cgi->param('discountnum_percent')), + #'months' => scalar($cgi->param('discountnum_months')), + #'setup' => scalar($cgi->param('discountnum_setup')), + ##'linked' => scalar($cgi->param('discountnum_linked')), + ##'disabled' => $self->discountnum_disabled, + }; + my $error = $cust_pkg_discount->insert; + die "Error discounting package: $error\n" if $error; + + } + + ''; + +} + +1; diff --git a/FS/FS/part_event/Condition/cust_bill_owed_percent.pm b/FS/FS/part_event/Condition/cust_bill_owed_percent.pm new file mode 100644 index 000000000..e06b511ef --- /dev/null +++ b/FS/FS/part_event/Condition/cust_bill_owed_percent.pm @@ -0,0 +1,50 @@ +package FS::part_event::Condition::cust_bill_owed_percent; + +use strict; +use FS::cust_bill; + +use base qw( FS::part_event::Condition ); + +sub description { + 'Percentage owed on specific invoice'; +} + +sub eventtable_hashref { + { 'cust_main' => 0, + 'cust_bill' => 1, + 'cust_pkg' => 0, + }; +} + +sub option_fields { + ( + 'owed' => { 'label' => 'Percentage of invoice owed over', + 'type' => 'percentage', + 'value' => '0', #default + }, + ); +} + +sub condition { + #my($self, $cust_bill, %opt) = @_; + my($self, $cust_bill) = @_; + + my $percent = $self->option('owed') || 0; + my $over = sprintf('%.2f', + $cust_bill->charged * $percent / 100); + + $cust_bill->owed > $over; +} + +sub condition_sql { + my( $class, $table ) = @_; + + # forces the option to be an integer--do we care? + my $percent = $class->condition_sql_option_integer('owed'); + + my $owed_sql = FS::cust_bill->owed_sql; + + "$owed_sql > CAST( cust_bill.charged * $percent / 100 AS DECIMAL(10,2) )"; +} + +1; diff --git a/FS/FS/part_event/Condition/has_pkgpart.pm b/FS/FS/part_event/Condition/has_pkgpart.pm index c54b7e256..d85e1bd43 100644 --- a/FS/FS/part_event/Condition/has_pkgpart.pm +++ b/FS/FS/part_event/Condition/has_pkgpart.pm @@ -4,7 +4,7 @@ use strict; use base qw( FS::part_event::Condition ); -sub description { 'Customer has uncancelled package of specified definitions'; } +sub description { 'Customer has uncancelled specific package(s)'; } sub eventtable_hashref { { 'cust_main' => 1, @@ -27,7 +27,6 @@ sub condition { my $cust_main = $self->cust_main($object); - #XXX test my $if_pkgpart = $self->option('if_pkgpart') || {}; grep $if_pkgpart->{ $_->pkgpart }, $cust_main->ncancelled_pkgs; diff --git a/FS/FS/part_event/Condition/has_referral_custnum.pm b/FS/FS/part_event/Condition/has_referral_custnum.pm index dee240fec..c50579411 100644 --- a/FS/FS/part_event/Condition/has_referral_custnum.pm +++ b/FS/FS/part_event/Condition/has_referral_custnum.pm @@ -13,7 +13,7 @@ sub option_fields { 'type' => 'checkbox', 'value' => 'Y', }, - 'check_bal' => { 'label' => 'Check referring custoemr balance', + 'check_bal' => { 'label' => 'Check referring customer balance', 'type' => 'checkbox', 'value' => 'Y', }, diff --git a/FS/FS/part_event/Condition/message_email.pm b/FS/FS/part_event/Condition/message_email.pm new file mode 100644 index 000000000..7cceba697 --- /dev/null +++ b/FS/FS/part_event/Condition/message_email.pm @@ -0,0 +1,22 @@ +package FS::part_event::Condition::message_email; +use base qw( FS::part_event::Condition ); +use strict; + +sub description { + 'Customer allows email notices' +} + +sub condition { + my( $self, $object ) = @_; + my $cust_main = $self->cust_main($object); + + $cust_main->message_noemail ? 0 : 1; +} + +sub condition_sql { + my( $self, $table ) = @_; + + "cust_main.message_noemail IS NULL" +} + +1; diff --git a/FS/FS/part_event/Condition/once_percust.pm b/FS/FS/part_event/Condition/once_percust.pm index b8a8fbfb6..67767f91b 100644 --- a/FS/FS/part_event/Condition/once_percust.pm +++ b/FS/FS/part_event/Condition/once_percust.pm @@ -45,7 +45,6 @@ sub condition { } -#XXX test? sub condition_sql { my( $self, $table ) = @_; diff --git a/FS/FS/part_event/Condition/once_perinv.pm b/FS/FS/part_event/Condition/once_perinv.pm index f85a05665..1ee53b812 100644 --- a/FS/FS/part_event/Condition/once_perinv.pm +++ b/FS/FS/part_event/Condition/once_perinv.pm @@ -12,6 +12,15 @@ sub description { "Run only once for each time the package has been billed"; } # Run the event, at most, a number of times equal to the number of # distinct invoices that contain line items from this package. +sub option_fields { + ( + 'paid' => { 'label' => 'Only count paid bills', + 'type' => 'checkbox', + 'value' => 'Y', + }, + ) +} + sub eventtable_hashref { { 'cust_main' => 0, 'cust_bill' => 0, @@ -22,9 +31,15 @@ sub eventtable_hashref { sub condition { my($self, $cust_pkg, %opt) = @_; - my %invnum; - $invnum{$_->invnum} = 1 - foreach ( qsearch('cust_bill_pkg', { 'pkgnum' => $cust_pkg->pkgnum }) ); + my @cust_bill_pkg = qsearch('cust_bill_pkg', { pkgnum=>$cust_pkg->pkgnum }); + + @cust_bill_pkg = grep { ($_->owed_setup + $_->owed_recur) == 0 } + @cust_bill_pkg + if $self->option('paid'); + + my %invnum = (); + $invnum{$_->invnum} = 1 foreach @cust_bill_pkg; + my @events = qsearch( { 'table' => 'cust_event', 'hashref' => { 'eventpart' => $self->eventpart, @@ -40,6 +55,9 @@ sub condition { sub condition_sql { my( $self, $table ) = @_; + #paid flag not yet implemented here, but that's okay, a partial optimization + # is better than none + "( ( SELECT COUNT(distinct(invnum)) FROM cust_bill_pkg diff --git a/FS/FS/part_event/Condition/times_percust.pm b/FS/FS/part_event/Condition/times_percust.pm new file mode 100644 index 000000000..fc7064b7e --- /dev/null +++ b/FS/FS/part_event/Condition/times_percust.pm @@ -0,0 +1,76 @@ +package FS::part_event::Condition::times_percust; + +use strict; +use FS::Record qw( qsearch ); +use FS::part_event; +use FS::cust_event; + +use base qw( FS::part_event::Condition ); + +sub description { "Run this event the specified number of times per customer"; } + +sub option_fields { + ( + 'run_times' => { label=>'Number of times', type=>'text', value=>'1', }, + ); +} + +sub eventtable_hashref { + { 'cust_main' => 0, + 'cust_bill' => 1, + 'cust_pkg' => 1, + }; +} + +sub condition { + my($self, $object, %opt) = @_; + + my $obj_pkey = $object->primary_key; + my $obj_table = $object->table; + my $custnum = $object->custnum; + + my @where = ( + "tablenum IN ( SELECT $obj_pkey FROM $obj_table WHERE custnum = $custnum )" + ); + if ( $opt{'cust_event'}->eventnum =~ /^(\d+)$/ ) { + push @where, " eventnum != $1 "; + } + my $extra_sql = ' AND '. join(' AND ', @where); + + my @existing = qsearch( { + 'table' => 'cust_event', + 'hashref' => { + 'eventpart' => $self->eventpart, + #'tablenum' => $tablenum, + 'status' => { op=>'!=', value=>'failed' }, + }, + 'extra_sql' => $extra_sql, + } ); + + scalar(@existing) < $self->option('run_times'); + +} + +sub condition_sql { + my( $class, $table, %opt ) = @_; + + my %pkey = %{ FS::part_event->eventtable_pkey }; + + my $run_times = + $class->condition_sql_option_integer('run_times', $opt{'driver_name'}); + + my $pkey = $pkey{$table}; + + my $existing = "( SELECT COUNT(*) FROM cust_event + WHERE cust_event.eventpart = part_event.eventpart + AND cust_event.tablenum IN ( + SELECT $pkey FROM $table AS times_percust + WHERE times_percust.custnum = cust_main.custnum ) + AND status != 'failed' + )"; + + "$existing < $run_times"; + +} + +1; diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm index 5d650626e..28cb1419d 100644 --- a/FS/FS/part_export.pm +++ b/FS/FS/part_export.pm @@ -125,31 +125,14 @@ sub insert { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $error = $self->SUPER::insert(@_); + my $error = $self->SUPER::insert(@_) + || $self->replace; + # use replace to do all the part_export_machine and default_machine stuff if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; } - #kinda false laziness with process_m2name - my @machines = map { $_ =~ s/^\s+//; $_ =~ s/\s+$//; $_ } - grep /\S/, - split /[\n\r]{1,2}/, - $self->part_export_machine_textarea; - - foreach my $machine ( @machines ) { - - my $part_export_machine = new FS::part_export_machine { - 'exportnum' => $self->exportnum, - 'machine' => $machine, - }; - $error = $part_export_machine->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - } - $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } @@ -217,6 +200,7 @@ or modified. sub replace { my $self = shift; + my $old = $self->replace_old; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -228,12 +212,7 @@ sub replace { my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; - - my $error = $self->SUPER::replace(@_); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } + my $error; if ( $self->part_export_machine_textarea ) { @@ -258,6 +237,10 @@ sub replace { } } + if ( $self->default_machine_name eq $machine ) { + $self->default_machine( $part_export_machine{$machine}->machinenum ); + } + delete $part_export_machine{$machine}; #so we don't disable it below } else { @@ -272,11 +255,13 @@ sub replace { return $error; } + if ( $self->default_machine_name eq $machine ) { + $self->default_machine( $part_export_machine->machinenum ); + } } } - foreach my $part_export_machine ( values %part_export_machine ) { $part_export_machine->disabled('Y'); $error = $part_export_machine->replace; @@ -286,6 +271,48 @@ sub replace { } } + if ( $old->machine ne '_SVC_MACHINE' ) { + # then set up the default for any already-attached export_svcs + foreach my $export_svc ( $self->export_svc ) { + my @svcs = qsearch('cust_svc', { 'svcpart' => $export_svc->svcpart }); + foreach my $cust_svc ( @svcs ) { + my $svc_export_machine = FS::svc_export_machine->new({ + 'exportnum' => $self->exportnum, + 'svcnum' => $cust_svc->svcnum, + 'machinenum' => $self->default_machine, + }); + $error ||= $svc_export_machine->insert; + } + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } # if switching to selectable hosts + + } elsif ( $old->machine eq '_SVC_MACHINE' ) { + # then we're switching from selectable to non-selectable + foreach my $svc_export_machine ( + qsearch('svc_export_machine', { 'exportnum' => $self->exportnum }) + ) { + $error ||= $svc_export_machine->delete; + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + } + + $error = $self->SUPER::replace(@_); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + if ( $self->machine eq '_SVC_MACHINE' and ! $self->default_machine ) { + $dbh->rollback if $oldAutoCommit; + return "no default export host selected"; } $dbh->commit or die $dbh->errstr if $oldAutoCommit; @@ -308,6 +335,13 @@ sub check { || $self->ut_domainn('machine') || $self->ut_alpha('exporttype') ; + + if ( $self->machine eq '_SVC_MACHINE' ) { + $error ||= $self->ut_numbern('default_machine') + } else { + $self->set('default_machine', ''); + } + return $error if $error; $self->nodomain =~ /^(Y?)$/ or return "Illegal nodomain: ". $self->nodomain; @@ -471,7 +505,9 @@ sub _rebless { $self; } -=item svc_machine +=item svc_machine SVC_X + +Return the export hostname for SVC_X. =cut @@ -483,14 +519,33 @@ sub svc_machine { my $svc_export_machine = qsearchs('svc_export_machine', { 'svcnum' => $svc_x->svcnum, 'exportnum' => $self->exportnum, - }) - #would only happen if you add this export to existing services without a - #machine set then try to run exports without setting it... right? - or die "No hostname selected for ".($self->exportname || $self->exporttype); + }); + + if (!$svc_export_machine) { + warn "No hostname selected for ".($self->exportname || $self->exporttype); + return $self->default_export_machine->machine; + } return $svc_export_machine->part_export_machine->machine; } +=item default_export_machine + +Return the default export hostname for this export. + +=cut + +sub default_export_machine { + my $self = shift; + my $machinenum = $self->default_machine; + if ( $machinenum ) { + my $default_machine = FS::part_export_machine->by_key($machinenum); + return $default_machine->machine if $default_machine; + } + # this should not happen + die "no default export hostname for export ".$self->exportnum; +} + #these should probably all go away, just let the subclasses define em =item export_insert SVC_OBJECT @@ -601,6 +656,17 @@ DEFAULTSREF is a hashref with the same keys where true values indicate the setting is a default (and thus can be displayed in the UI with less emphasis, or hidden by default). +=item actions + +Adds one or more "action" links to the export's display in +browse/part_export.cgi. Should return pairs of values. The first is +the link label; the second is the Mason path to a document to load. +The document will show in a popup. + +=cut + +sub actions { } + =cut =item weight @@ -630,6 +696,10 @@ sub info { #default fallbacks... FS::part_export::DID_Common ? sub get_dids_can_tollfree { 0; } +sub get_dids_can_manual { 0; } +sub get_dids_can_edit { 0; } #don't use without can_manual, otherwise the + # DID selector provisions a new number from + # inventory each edit sub get_dids_npa_select { 1; } =back @@ -688,6 +758,55 @@ sub _upgrade_data { #class method $error = $opt->replace; die $error if $error; } + # for exports that have selectable hostnames, make sure all services + # have a hostname selected + foreach my $part_export ( + qsearch('part_export', { 'machine' => '_SVC_MACHINE' }) + ) { + + my $exportnum = $part_export->exportnum; + my $machinenum = $part_export->default_machine; + if (!$machinenum) { + my ($first) = $part_export->part_export_machine; + if (!$first) { + # user intervention really is required. + die "Export $exportnum has no hostname options defined.\n". + "You must correct this before upgrading.\n"; + } + # warn about this, because we might not choose the right one + warn "Export $exportnum (". $part_export->exporttype. + ") has no default hostname. Setting to ".$first->machine."\n"; + $machinenum = $first->machinenum; + $part_export->set('default_machine', $machinenum); + my $error = $part_export->replace; + die $error if $error; + } + + # the service belongs to a service def that uses this export + # and there is not a hostname selected for this export for that service + my $join = ' JOIN export_svc USING ( svcpart )'. + ' LEFT JOIN svc_export_machine'. + ' ON ( cust_svc.svcnum = svc_export_machine.svcnum'. + ' AND export_svc.exportnum = svc_export_machine.exportnum )'; + + my @svcs = qsearch( { + 'select' => 'cust_svc.*', + 'table' => 'cust_svc', + 'addl_from' => $join, + 'extra_sql' => ' WHERE svcexportmachinenum IS NULL'. + ' AND export_svc.exportnum = '.$part_export->exportnum, + } ); + foreach my $cust_svc (@svcs) { + my $svc_export_machine = FS::svc_export_machine->new({ + 'exportnum' => $exportnum, + 'machinenum' => $machinenum, + 'svcnum' => $cust_svc->svcnum, + }); + my $error = $svc_export_machine->insert; + die $error if $error; + } + } + # pass downstream my %exports_in_use; $exports_in_use{ref $_} = 1 foreach qsearch('part_export', {}); diff --git a/FS/FS/part_export/acct_xmlrpc.pm b/FS/FS/part_export/acct_xmlrpc.pm index a493f5206..acd7ffe5d 100644 --- a/FS/FS/part_export/acct_xmlrpc.pm +++ b/FS/FS/part_export/acct_xmlrpc.pm @@ -131,10 +131,10 @@ sub _export_command { sub _export_replace { my( $self, $new, $old ) = (shift, shift, shift); - my $method = $self->option($action.'_method'); + my $method = $self->option('replace_method'); return '' if $method =~ /^\s*$/; - my @params = split("\n", $self->option($action.'_params') ); + my @params = split("\n", $self->option('replace_params') ); my( @x_param ) = (); my( %x_struct ) = (); diff --git a/FS/FS/part_export/dma_radiusmanager.pm b/FS/FS/part_export/dma_radiusmanager.pm deleted file mode 100644 index d46a996ca..000000000 --- a/FS/FS/part_export/dma_radiusmanager.pm +++ /dev/null @@ -1,355 +0,0 @@ -package FS::part_export::dma_radiusmanager; - -use strict; -use vars qw($DEBUG %info %options); -use base 'FS::part_export'; -use FS::part_svc; -use FS::svc_acct; -use FS::radius_group; -use Tie::IxHash; -use Digest::MD5 'md5_hex'; - -use Locale::Country qw(code2country); -use Locale::SubCountry; -use Date::Format 'time2str'; - -tie %options, 'Tie::IxHash', - 'dbname' => { label=>'Database name', default=>'radius' }, - 'username' => { label=>'Database username' }, - 'password' => { label=>'Database password' }, - 'manager' => { label=>'Manager name' }, - 'template_name' => { label=>'Template service name' }, - 'service_prefix' => { label=>'Service name prefix' }, - 'debug' => { label=>'Enable debugging', type=>'checkbox' }, -; - -%info = ( - 'svc' => 'svc_acct', - 'desc' => 'Export to DMA Radius Manager', - 'options' => \%options, - 'nodomain' => 'Y', - 'notes' => '', #XXX -); - -$DEBUG = 0; - -sub connect { - my $self = shift; - my $datasrc = 'dbi:mysql:host='.$self->machine. - ':database='.$self->option('dbname'); - DBI->connect( - $datasrc, - $self->option('username'), - $self->option('password'), - { AutoCommit => 0 } - ) or die $DBI::errstr; -} - -sub export_insert { my $self = shift; $self->dma_rm_queue('insert', @_) } -sub export_delete { my $self = shift; $self->dma_rm_queue('delete', @_) } -sub export_replace { my $self = shift; $self->dma_rm_queue('replace', @_) } -sub export_suspend { my $self = shift; $self->dma_rm_queue('suspend', @_) } -sub export_unsuspend { my $self = shift; $self->dma_rm_queue('unsuspend', @_) } - -sub dma_rm_queue { - my ($self, $action, $svc_acct, $old) = @_; - - my $svcnum = $svc_acct->svcnum; - - my $cust_pkg = $svc_acct->cust_svc->cust_pkg; - my $cust_main = $cust_pkg->cust_main; - my $location = $cust_pkg->cust_location; - - my $address = $location->address1; - $address .= ' '.$location->address2 if $location->address2; - my $country = code2country($location->country); - my $lsc = Locale::SubCountry->new($location->country); - my $state = $lsc->full_name($location->state) if defined($lsc); - - my %params = ( - # for the remote side - username => $svc_acct->username, - password => md5_hex($svc_acct->_password), - groupid => $self->option('groupid'), - enableuser => 1, - firstname => $cust_main->first, - lastname => $cust_main->last, - company => $cust_main->company, - phone => ($cust_main->daytime || $cust_main->night), - mobile => $cust_main->mobile, - address => $location->address1, # address2? - city => $location->city, - state => $state, #full name - zip => $location->zip, - country => $country, #full name - gpslat => $location->latitude, - gpslong => $location->longitude, - comment => 'svcnum'.$svcnum, - createdby => $self->option('manager'), - owner => $self->option('manager'), - email => $cust_main->invoicing_list_emailonly_scalar, - - # used internally by the export - exportnum => $self->exportnum, - svcnum => $svcnum, - action => $action, - svcpart => $svc_acct->cust_svc->svcpart, - _password => $svc_acct->_password, - ); - if ( $action eq 'replace' ) { - $params{'old_username'} = $old->username; - $params{'old_password'} = $old->_password; - } - my $queue = FS::queue->new({ - 'svcnum' => $svcnum, - 'job' => "FS::part_export::dma_radiusmanager::dma_rm_action", - }); - $queue->insert(%params); -} - -sub dma_rm_action { - my %params = @_; - my $svcnum = delete $params{svcnum}; - my $action = delete $params{action}; - my $svcpart = delete $params{svcpart}; - my $exportnum = delete $params{exportnum}; - - my $username = $params{username}; - my $password = delete $params{_password}; - - my $self = FS::part_export->by_key($exportnum); - my $dbh = $self->connect; - local $DEBUG = 1 if $self->option('debug'); - - # export the part_svc if needed, and get its srvid - my $part_svc = FS::part_svc->by_key($svcpart); - my $srvid = $self->export_part_svc($part_svc, $dbh); # dies on error - $params{srvid} = $srvid; - - if ( $action eq 'insert' ) { - $params{'createdon'} = time2str('%Y-%m-%d', time); - $params{'expiration'} = time2str('%Y-%m-%d', time); - warn "rm_users: inserting svcnum$svcnum\n" if $DEBUG; - my $sth = $dbh->prepare( 'INSERT INTO rm_users ( '. - join(', ', keys(%params)). - ') VALUES ('. - join(', ', ('?') x keys(%params)). - ')' - ); - $sth->execute(values(%params)) or die $dbh->errstr; - - # minor false laziness w/ sqlradius_insert - warn "radcheck: inserting $username\n" if $DEBUG; - $sth = $dbh->prepare( 'INSERT INTO radcheck ( - username, attribute, op, value - ) VALUES (?, ?, ?, ?)' ); - $sth->execute( - $username, - 'Cleartext-Password', - ':=', # :=( - $password, - ) or die $dbh->errstr; - - $sth->execute( - $username, - 'Simultaneous-Use', - ':=', - 1, # should this be an option? - ) or die $dbh->errstr; - # also, we don't support exporting any other radius attrs... - # those should go in 'custattr' if we need them - } elsif ( $action eq 'replace' ) { - - my $old_username = delete $params{old_username}; - my $old_password = delete $params{old_password}; - # svcnum is invariant and on the remote side, so we don't need any - # of the old fields to do this - warn "rm_users: updating svcnum$svcnum\n" if $DEBUG; - my $sth = $dbh->prepare( 'UPDATE rm_users SET '. - join(', ', map { "$_ = ?" } keys(%params)). - ' WHERE comment = ?' - ); - $sth->execute(values(%params), $params{comment}) or die $dbh->errstr; - # except for username/password changes - if ( $old_password ne $password ) { - warn "radcheck: changing password for $old_username\n" if $DEBUG; - $sth = $dbh->prepare( 'UPDATE radcheck SET value = ? '. - 'WHERE username = ? and attribute = \'Cleartext-Password\'' - ); - $sth->execute($password, $old_username) or die $dbh->errstr; - } - if ( $old_username ne $username ) { - warn "radcheck: changing username $old_username to $username\n" - if $DEBUG; - $sth = $dbh->prepare( 'UPDATE radcheck SET username = ? '. - 'WHERE username = ?' - ); - $sth->execute($username, $old_username) or die $dbh->errstr; - } - - } elsif ( $action eq 'suspend' ) { - - # this is sufficient - warn "rm_users: disabling svcnum#$svcnum\n" if $DEBUG; - my $sth = $dbh->prepare( 'UPDATE rm_users SET enableuser = 0 '. - 'WHERE comment = ?' - ); - $sth->execute($params{comment}) or die $dbh->errstr; - - } elsif ( $action eq 'unsuspend' ) { - - warn "rm_users: enabling svcnum#$svcnum\n" if $DEBUG; - my $sth = $dbh->prepare( 'UPDATE rm_users SET enableuser = 1 '. - 'WHERE comment = ?' - ); - $sth->execute($params{comment}) or die $dbh->errstr; - - } elsif ( $action eq 'delete' ) { - - warn "rm_users: deleting svcnum#$svcnum\n" if $DEBUG; - my $sth = $dbh->prepare( 'DELETE FROM rm_users WHERE comment = ?' ); - $sth->execute($params{comment}) or die $dbh->errstr; - - warn "radcheck: deleting $username\n" if $DEBUG; - $sth = $dbh->prepare( 'DELETE FROM radcheck WHERE username = ?' ); - $sth->execute($username) or die $dbh->errstr; - - # if this were smarter it would also delete the rm_services record - # if it was no longer in use, but that's not really necessary - } - - $dbh->commit; - ''; -} - -=item export_part_svc PART_SVC DBH - -Query Radius Manager for a service definition matching the name of -PART_SVC (optionally with a prefix defined in the export options). -If there is one, update it to match the attributes of PART_SVC; if -not, create one. Then return its srvid. - -=cut - -sub export_part_svc { - my ($self, $part_svc, $dbh) = @_; - - # if $dbh exists, use the existing transaction - # otherwise create our own and commit when finished - my $commit = 0; - if (!$dbh) { - $dbh = $self->connect; - $commit = 1; - } - - my $name = $self->option('service_prefix').$part_svc->svc; - - my %params = ( - 'srvname' => $name, - 'enableservice' => 1, - 'nextsrvid' => -1, - 'dailynextsrvid' => -1, - # force price-related fields to zero - 'unitprice' => 0, - 'unitpriceadd' => 0, - 'unitpricetax' => 0, - 'unitpriceaddtax' => 0, - ); - my @fixed_groups; - # use speed settings from fixed usergroups configured on this part_svc - if ( my $psc = $part_svc->part_svc_column('usergroup') ) { - # each part_svc really should only have one fixed group with non-null - # speed settings, but go by priority order for consistency - @fixed_groups = - sort { $a->priority <=> $b->priority } - grep { $_ } - map { FS::radius_group->by_key($_) } - split(/\s*,\s*/, $psc->columnvalue); - } # otherwise there are no fixed groups, so leave speed empty - - foreach (qw(down up)) { - my $speed = "speed_$_"; - foreach my $group (@fixed_groups) { - if ( ($group->$speed || 0) > 0 ) { - $params{$_.'rate'} = $group->$speed; - last; - } - } - } - # anything else we need here? poolname, maybe? - - warn "rm_services: looking for '$name'\n" if $DEBUG; - my $sth = $dbh->prepare( - 'SELECT srvid FROM rm_services WHERE srvname = ? AND enableservice = 1' - ); - $sth->execute($name) or die $dbh->errstr; - if ( $sth->rows > 1 ) { - die "Multiple services with name '$name' found in Radius Manager.\n"; - - } elsif ( $sth->rows == 0 ) { - # leave this blank to disable creating new service defs - my $template_name = $self->option('template_name'); - - die "Can't create a new service profile--no template service specified.\n" - unless $template_name; - - warn "rm_services: fetching template '$template_name'\n" if $DEBUG; - $sth = $dbh->prepare('SELECT * FROM rm_services WHERE srvname = ? LIMIT 1'); - $sth->execute($template_name); - die "Can't create a new service profile--template service ". - "'$template_name' not found.\n" unless $sth->rows == 1; - my $template = $sth->fetchrow_hashref; - %params = (%$template, %params); - - # get the next available srvid - $sth = $dbh->prepare('SELECT MAX(srvid) FROM rm_services'); - $sth->execute or die $dbh->errstr; - my $srvid; - if ( $sth->rows ) { - $srvid = $sth->fetchrow_arrayref->[0] + 1; - } - $params{'srvid'} = $srvid; - - # create a new one based on the template - warn "rm_services: inserting '$name' as srvid#$srvid\n" if $DEBUG; - $sth = $dbh->prepare( - 'INSERT INTO rm_services ('.join(', ', keys %params). - ') VALUES ('.join(', ', map {'?'} keys %params).')' - ); - $sth->execute(values(%params)) or die $dbh->errstr; - # also link it to all the managers allowed on the template service - warn "rm_services: linking to manager\n" if $DEBUG; - $sth = $dbh->prepare( - 'INSERT INTO rm_allowedmanagers (srvid, managername) '. - 'SELECT ?, managername FROM rm_allowedmanagers WHERE srvid = ?' - ); - $sth->execute($srvid, $template->{srvid}) or die $dbh->errstr; - # and the same for NASes - warn "rm_services: linking to nas\n" if $DEBUG; - $sth = $dbh->prepare( - 'INSERT INTO rm_allowednases (srvid, nasid) '. - 'SELECT ?, nasid FROM rm_allowednases WHERE srvid = ?' - ); - $sth->execute($srvid, $template->{srvid}) or die $dbh->errstr; - - $dbh->commit if $commit; - return $srvid; - - } else { # $sth->rows == 1, it already exists - - my $row = $sth->fetchrow_arrayref; - my $srvid = $row->[0]; - warn "rm_services: updating srvid#$srvid\n" if $DEBUG; - $sth = $dbh->prepare( - 'UPDATE rm_services SET '.join(', ', map {"$_ = ?"} keys %params) . - ' WHERE srvid = ?' - ); - $sth->execute(values(%params), $srvid) or die $dbh->errstr; - - $dbh->commit if $commit; - return $srvid; - - } -} - -1; diff --git a/FS/FS/part_export/fibernetics_did.pm b/FS/FS/part_export/fibernetics_did.pm index fb0378550..a51457a03 100644 --- a/FS/FS/part_export/fibernetics_did.pm +++ b/FS/FS/part_export/fibernetics_did.pm @@ -28,6 +28,8 @@ tie my %options, 'Tie::IxHash', sub rebless { shift; } sub get_dids_can_tollfree { 0; }; +sub get_dids_can_manual { 1; }; +sub get_dids_can_edit { 1; }; sub get_dids_npa_select { 0; }; # i guess we could get em from the API, but since its returning states without diff --git a/FS/FS/part_export/http_status.pm b/FS/FS/part_export/http_status.pm index 6fbd3fbe6..80139e776 100644 --- a/FS/FS/part_export/http_status.pm +++ b/FS/FS/part_export/http_status.pm @@ -3,28 +3,53 @@ use base qw( FS::part_export ); use strict; use warnings; -use vars qw( %info ); +use vars qw( %info $DEBUG ); +use URI::Escape; use LWP::UserAgent; use HTTP::Request::Common; +use Email::Valid; tie my %options, 'Tie::IxHash', 'url' => { label => 'URL', }, + 'blacklist_add_url' => { label => 'Optional blacklist add URL', }, + 'blacklist_del_url' => { label => 'Optional blacklist delete URL', }, + 'whitelist_add_url' => { label => 'Optional whitelist add URL', }, + 'whitelist_del_url' => { label => 'Optional whitelist delete URL', }, + 'vacation_add_url' => { label => 'Optional vacation message add URL', }, + 'vacation_del_url' => { label => 'Optional vacation message delete URL', }, + #'user' => { label => 'Username', default=>'' }, #'password' => { label => 'Password', default => '' }, ; %info = ( - 'svc' => 'svc_dsl', + 'svc' => [ 'svc_acct', 'svc_dsl', ], 'desc' => 'Retrieve status information via HTTP or HTTPS', 'options' => \%options, 'no_machine' => 1, 'notes' => <<'END' Fields from the service can be substituted in the URL as $field. + +Optionally, spam black/whitelist addresees and a vacation message may be +modified via HTTP or HTTPS as well. END ); +$DEBUG = 1; + sub rebless { shift; } +our %addl_fields = ( + 'svc_acct' => [qw( email ) ], + 'svc_dsl' => [qw( gateway_access_or_phonenum ) ], +); + +#some NOPs for required subroutines, to avoid throwing the exceptions in the +# part_export.pm fallbacks +sub _export_insert { '' }; +sub _export_replace { '' }; +sub _export_delete { '' }; + sub export_getstatus { my( $self, $svc_x, $htmlref, $hashref ) = @_; @@ -34,10 +59,97 @@ sub export_getstatus { { no strict 'refs'; ${$_} = $svc_x->getfield($_) foreach $svc_x->fields; - if ( $svc_x->table eq 'svc_dsl' ) { - ${$_} = $svc_x->$_() foreach (qw( gateway_access_or_phonenum )); + ${$_} = $svc_x->$_() foreach @{ $addl_fields{ $svc_x->table } }; + $url = eval(qq("$urlopt")); + } + + my $req = HTTP::Request::Common::GET( $url ); + my $ua = LWP::UserAgent->new; + my $response = $ua->request($req); + + if ( $svc_x->table eq 'svc_dsl' ) { + + $$htmlref = $response->is_error ? $response->error_as_HTML + : $response->content; + + #hash data not yet implemented for svc_dsl + + } elsif ( $svc_x->table eq 'svc_acct' ) { + + #this whole section is rather specific to fibernetics and should be an + # option or callback or something + + # to,from,wb_value + + use Text::CSV_XS; + my $csv = Text::CSV_XS->new; + + my @lines = split("\n", $response->content); + pop @lines if $lines[-1] eq ''; + my $header = shift @lines; + $csv->parse($header) or return; + my @header = $csv->fields; + + while ( my $line = shift @lines ) { + $csv->parse($line) or next; + my @fields = $csv->fields; + my %hash = map { $_ => shift(@fields) } @header; + + if ( defined $hash{'wb_value'} ) { + if ( $hash{'wb_value'} =~ /^[WA]/i ) { #Whitelist/Allow + push @{ $hashref->{'whitelist'} }, $hash{'from'}; + } else { # if ( $hash{'wb_value'} =~ /^[BD]/i ) { #Blacklist/Deny + push @{ $hashref->{'blacklist'} }, $hash{'from'}; + } + } + + for (qw( created enddate )) { + $hash{$_} = '' if $hash{$_} =~ /^0000-/; + $hash{$_} = (split(' ', $hash{$_}))[0]; + } + + next unless $hash{'active'}; + $hashref->{"vacation_$_"} = $hash{$_} || '' + foreach qw( active subject body created enddate ); + } + } #else { die 'guru meditation #295'; } + +} + +sub export_setstatus_listadd { + my( $self, $svc_x, $hr ) = @_; + $self->export_setstatus_listX( $svc_x, 'add', $hr->{list}, $hr->{address} ); +} + +sub export_setstatus_listdel { + my( $self, $svc_x, $hr ) = @_; + $self->export_setstatus_listX( $svc_x, 'del', $hr->{list}, $hr->{address} ); +} + +sub export_setstatus_listX { + my( $self, $svc_x, $action, $list, $address ) = @_; + + my $option; + if ( $list =~ /^[WA]/i ) { #Whitelist/Allow + $option = 'whitelist_'; + } else { # if ( $hash{'wb_value'} =~ /^[BD]/i ) { #Blacklist/Deny + $option = 'blacklist_'; + } + $option .= $action. '_url'; + + $address = Email::Valid->address($address) + or die "address failed $Email::Valid::Details check.\n"; + + #some false laziness w/export_getstatus above + my $url; + my $urlopt = $self->option($option) or return; #DIFF + no strict 'vars'; + { + no strict 'refs'; + ${$_} = $svc_x->getfield($_) foreach $svc_x->fields; + ${$_} = $svc_x->$_() foreach @{ $addl_fields{ $svc_x->table } }; $url = eval(qq("$urlopt")); } @@ -45,11 +157,56 @@ sub export_getstatus { my $ua = LWP::UserAgent->new; my $response = $ua->request($req); - $$htmlref = $response->is_error ? $response->error_as_HTML - : $response->content; + die $response->code. ' '. $response->message if $response->is_error; - #hash data note yet implemented for this status export +} +sub export_setstatus_vacationadd { + my( $self, $svc_x, $hr ) = @_; + $self->export_setstatus_vacationX( $svc_x, 'add', $hr ); } +sub export_setstatus_vacationdel { + my( $self, $svc_x, $hr ) = @_; + $self->export_setstatus_vacationX( $svc_x, 'del', $hr ); +} + +sub export_setstatus_vacationX { + my( $self, $svc_x, $action, $hr ) = @_; + + my $option = 'vacation_'. $action. '_url'; + + my $subject = uri_escape($hr->{subject}); + my $body = uri_escape($hr->{body}); + for (qw( created enddate )) { + if ( $hr->{$_} =~ /^(\d{4}-\d{2}-\d{2})$/ ) { + $hr->{$_} = $1; + } else { + $hr->{$_} = ''; + } + } + my $created = $hr->{created}; + my $enddate = $hr->{enddate}; + + #some false laziness w/export_getstatus above + my $url; + my $urlopt = $self->option($option) or return; #DIFF + no strict 'vars'; + { + no strict 'refs'; + ${$_} = $svc_x->getfield($_) foreach $svc_x->fields; + ${$_} = $svc_x->$_() foreach @{ $addl_fields{ $svc_x->table } }; + $url = eval(qq("$urlopt")); + } + + my $req = HTTP::Request::Common::GET( $url ); + my $ua = LWP::UserAgent->new; + my $response = $ua->request($req); + + die $response->code. ' '. $response->message if $response->is_error; + +} + +1; + 1; diff --git a/FS/FS/part_export/huawei_hlr.pm b/FS/FS/part_export/huawei_hlr.pm new file mode 100644 index 000000000..007981880 --- /dev/null +++ b/FS/FS/part_export/huawei_hlr.pm @@ -0,0 +1,340 @@ +package FS::part_export::huawei_hlr; + +use vars qw(@ISA %info $DEBUG $CACHE); +use Tie::IxHash; +use FS::Record qw(qsearch qsearchs dbh); +use FS::part_export; +use FS::svc_phone; +use FS::inventory_class; +use FS::inventory_item; +use IO::Socket::INET; +use Data::Dumper; +use MIME::Base64 qw(decode_base64); +use Storable qw(thaw); + +use strict; + +$DEBUG = 0; +@ISA = qw(FS::part_export); + +tie my %options, 'Tie::IxHash', + 'opname' => { label=>'Operator login' }, + 'pwd' => { label=>'Operator password' }, + 'tplid' => { label=>'Template number' }, + 'hlrsn' => { label=>'HLR serial number' }, + 'k4sno' => { label=>'K4 serial number' }, + 'cardtype' => { label => 'Card type', + type => 'select', + options=> ['SIM', 'USIM'] + }, + 'alg' => { label => 'Authentication algorithm', + type => 'select', + options=> ['COMP128_1', + 'COMP128_2', + 'COMP128_3', + 'MILENAGE' ], + }, + 'opcvalue' => { label=>'OPC value (for MILENAGE only)' }, + 'opsno' => { label=>'OP serial number (for MILENAGE only)' }, + 'timeout' => { label=>'Timeout (seconds)', default => 120 }, + 'debug' => { label=>'Enable debugging', type=>'checkbox' }, +; + +%info = ( + 'svc' => 'svc_phone', + 'desc' => 'Provision mobile phone service to Huawei HLR9820', + 'options' => \%options, + 'notes' => <<'END' +Connects to a Huawei Subscriber Management Unit via TCP and configures mobile +phone services according to a template. The <i>sim_imsi</i> field must be +set on the service, and the template must exist. +END +); + +sub actions { + 'Import SIMs' => 'misc/part_export/huawei_hlr-import_sim.html' +} + +sub _export_insert { + my( $self, $svc_phone ) = (shift, shift); + # svc_phone::check should ensure phonenum and sim_imsi are numeric + my @command = ( + IMSI => '"'.$svc_phone->sim_imsi.'"', + ISDN => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"', + TPLID => $self->option('tplid'), + ); + unshift @command, 'HLRSN', $self->option('hlrsn') + if $self->option('hlrsn'); + unshift @command, 'ADD TPLSUB'; + my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command); + ref($err_or_queue) ? '' : $err_or_queue; +} + +sub _export_replace { + my( $self, $new, $old ) = @_; + my $depend_jobnum; + if ( $new->sim_imsi ne $old->sim_imsi ) { + my @command = ( + 'MOD IMSI', + ISDN => '"'.$old->countrycode.$old->phonenum.'"', + IMSI => '"'.$old->sim_imsi.'"', + NEWIMSI => '"'.$new->sim_imsi.'"', + ); + my $err_or_queue = $self->queue_command($new->svcnum, @command); + return $err_or_queue unless ref $err_or_queue; + $depend_jobnum = $err_or_queue->jobnum; + } + if ( $new->countrycode ne $old->countrycode or + $new->phonenum ne $old->phonenum ) { + my @command = ( + 'MOD ISDN', + ISDN => '"'.$old->countrycode.$old->phonenum.'"', + NEWISDN => '"'.$new->countrycode.$new->phonenum.'"', + ); + my $err_or_queue = $self->queue_command($new->svcnum, @command); + return $err_or_queue unless ref $err_or_queue; + if ( $depend_jobnum ) { + my $error = $err_or_queue->depend_insert($depend_jobnum); + return $error if $error; + } + } + # no other svc_phone changes need to be exported + ''; +} + +sub _export_suspend { + my( $self, $svc_phone ) = (shift, shift); + $self->_export_lock($svc_phone, 'TRUE'); +} + +sub _export_unsuspend { + my( $self, $svc_phone ) = (shift, shift); + $self->_export_lock($svc_phone, 'FALSE'); +} + +sub _export_lock { + my ($self, $svc_phone, $lockstate) = @_; + # XXX I'm not sure this actually suspends. Need to test it. + my @command = ( + 'MOD LCK', + IMSI => '"'.$svc_phone->sim_imsi.'"', + ISDN => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"', + IC => $lockstate, + OC => $lockstate, + GPRSLOCK=> $lockstate, + ); + my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command); + ref($err_or_queue) ? '' : $err_or_queue; +} + +sub _export_delete { + my( $self, $svc_phone ) = (shift, shift); + my @command = ( + 'RMV SUB', + #IMSI => '"'.$svc_phone->sim_imsi.'"', + ISDN => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"', + ); + my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command); + ref($err_or_queue) ? '' : $err_or_queue; +} + +sub queue_command { + my ($self, $svcnum, @command) = @_; + my $queue = FS::queue->new({ + svcnum => $svcnum, + job => 'FS::part_export::huawei_hlr::run_command', + }); + $queue->insert($self->exportnum, @command) || $queue; +} + +sub run_command { + my ($exportnum, @command) = @_; + my $self = FS::part_export->by_key($exportnum); + my $socket = $self->login; + my $result = $self->command($socket, @command); + $self->logout($socket); + $socket->close; + die $result->{error} if $result->{error}; + ''; +} + +sub login { + my $self = shift; + local $DEBUG = $self->option('debug') || 0; + # Send a command to the SMU. + # The caller is responsible for quoting string parameters. + my %socket_param = ( + PeerAddr => $self->machine, + PeerPort => 7777, + Proto => 'tcp', + Timeout => ($self->option('timeout') || 30), + ); + warn "Connecting to ".$self->machine."...\n" if $DEBUG; + warn Dumper(\%socket_param) if $DEBUG; + my $socket = IO::Socket::INET->new(%socket_param) + or die "Failed to connect: $!\n"; + + warn 'Logging in as "'.$self->option('opname').".\"\n" if $DEBUG; + my @login_param = ( + OPNAME => '"'.$self->option('opname').'"', + PWD => '"'.$self->option('pwd').'"', + ); + if ($self->option('HLRSN')) { + unshift @login_param, 'HLRSN', $self->option('HLRSN'); + } + my $login_result = $self->command($socket, 'LGI', @login_param); + die $login_result->{error} if $login_result->{error}; + return $socket; +} + +sub logout { + warn "Logging out.\n" if $DEBUG; + my $self = shift; + my ($socket) = @_; + $self->command($socket, 'LGO'); + $socket->close; +} + +sub command { + my $self = shift; + my ($socket, $command, @param) = @_; + my $string = $command . ':'; + while (@param) { + $string .= shift(@param) . '=' . shift(@param); + $string .= ',' if @param; + } + $string .= "\n;"; + my @result; + eval { # timeout + local $SIG{ALRM} = sub { die "timeout\n" }; + alarm ($self->option('timeout') || 120); + warn "Sending to server:\n$string\n\n" if $DEBUG; + $socket->print($string); + warn "Received:\n"; + my $line; + local $/ = "\r\n"; + do { + $line = $socket->getline(); + warn $line if $DEBUG; + chomp $line; + push @result, $line if length($line); + } until ( $line =~ /^---\s*END$/ or $socket->eof ); + alarm 0; + }; + my %return; + if ( $@ eq "timeout\n" ) { + return { error => 'request timed out' }; + } elsif ( $@ ) { + return { error => $@ }; + } else { + #+++ HLR9820 <date> <time>\n + my $header = shift(@result); + $header =~ /(\+\+\+.*)/ + or return { error => 'malformed response: '.$header }; + $return{header} = $1; + #SMU #<serial number>\n + $return{smu} = shift(@result); + #%%<command string>%%\n + $return{echo} = shift(@result); # should match the input + #<message code>: <message description>\n + my $message = shift(@result); + if ($message =~ /^SUCCESS/) { + $return{success} = $message; + } else { #/^ERR/ + $return{error} = $message; + } + $return{trailer} = pop(@result); + $return{details} = join("\n",@result,''); + } + \%return; +} + +sub process_import_sim { + my $job = shift; + my $param = thaw(decode_base64(shift)); + $param->{'job'} = $job; + my $exportnum = delete $param->{'exportnum'}; + my $export = __PACKAGE__->by_key($exportnum); + my $file = delete $param->{'uploaded_files'}; + $file =~ s/^file://; + my $dir = $FS::UID::cache_dir .'/cache.'. $FS::UID::datasrc; + open( $param->{'filehandle'}, '<', "$dir/$file" ) + or die "unable to open '$file'.\n"; + my $error = $export->import_sim($param); +} + +sub import_sim { + # import a SIM list + local $FS::UID::AutoCommit = 1; # yes, 1 + my $self = shift; + my $param = shift; + my $job = $param->{'job'}; + my $fh = $param->{'filehandle'}; + my @lines = $fh->getlines; + + my @command = 'ADD KI'; + push @command, ('HLRSN', $self->option('hlrsn')) if $self->option('hlrsn'); + + my @args = ('OPERTYPE', 'ADD'); + push @args, ('K4SNO', $self->option('k4sno')) if $self->option('k4sno'); + push @args, ('CARDTYPE', $self->option('cardtype'), + 'ALG', $self->option('alg')); + push @args, ('OPCVALUE', $self->option('opcvalue'), + 'OPSNO', $self->option('opsno')) + if $self->option('alg') eq 'MILENAGE'; + + my $agentnum = $param->{'agentnum'}; + my $classnum = $param->{'classnum'}; + my $class = FS::inventory_class->by_key($classnum) + or die "bad inventory class $classnum\n"; + my %existing = map { $_->item, 1 } + qsearch('inventory_item', { 'classnum' => $classnum }); + + my $socket = $self->login; + my $num=0; + my $total = scalar(@lines); + foreach my $line (@lines) { + $num++; + $job->update_statustext(int(100*$num/$total).',Provisioning IMSIs...') + if $job; + + chomp $line; + my ($imsi, $iccid, $pin1, $puk1, $pin2, $puk2, $acc, $ki) = + split(' ', $line); + # the only fields we really care about are the IMSI and KI. + if ($imsi !~ /^\d{15}$/ or $ki !~ /^[0-9A-Z]{32}$/) { + warn "misspelled line in SIM file: $line\n"; + next; + } + if ($existing{$imsi}) { + warn "IMSI $imsi already in inventory, skipped\n"; + next; + } + + # push IMSI/KI to the HLR + my $return = $self->command($socket, + @command, + 'IMSI', $imsi, + 'KIVALUE', $ki, + @args + ); + if ( $return->{success} ) { + # add to inventory + my $item = FS::inventory_item->new({ + 'classnum' => $classnum, + 'agentnum' => $agentnum, + 'item' => $imsi, + }); + my $error = $item->insert; + if ( $error ) { + die "IMSI $imsi added to HLR, but not to inventory:\n$error\n"; + } + } else { + die "IMSI $imsi could not be added to HLR:\n".$return->{error}."\n"; + } + } #foreach $line + $self->logout($socket); + return; +} + +1; diff --git a/FS/FS/part_export/netsapiens.pm b/FS/FS/part_export/netsapiens.pm index 2e37d04b6..c72093d00 100644 --- a/FS/FS/part_export/netsapiens.pm +++ b/FS/FS/part_export/netsapiens.pm @@ -72,7 +72,7 @@ tie my %options, 'Tie::IxHash', ; %info = ( - 'svc' => [ 'svc_phone', ], # 'part_device', + 'svc' => [qw( svc_phone part_device )], 'desc' => 'Provision phone numbers to NetSapiens', 'options' => \%options, 'no_machine' => 1, diff --git a/FS/FS/part_export/phone_shellcommands.pm b/FS/FS/part_export/phone_shellcommands.pm index 9ace21355..161ffe0f5 100644 --- a/FS/FS/part_export/phone_shellcommands.pm +++ b/FS/FS/part_export/phone_shellcommands.pm @@ -13,16 +13,18 @@ use FS::part_export; #- suspension/unsuspension tie my %options, 'Tie::IxHash', - 'user' => { label=>'Remote username', default=>'root', }, - 'useradd' => { label=>'Insert command', }, - 'userdel' => { label=>'Delete command', }, - 'usermod' => { label=>'Modify command', }, - 'suspend' => { label=>'Suspension command', }, - 'unsuspend' => { label=>'Unsuspension command', }, + 'user' => { label=>'Remote username', default=>'root', }, + 'useradd' => { label=>'Insert command', }, + 'userdel' => { label=>'Delete command', }, + 'usermod' => { label=>'Modify command', }, + 'suspend' => { label=>'Suspension command', }, + 'unsuspend' => { label=>'Unsuspension command', }, + 'mac_insert' => { label=>'Device MAC address insert command', }, + 'mac_delete' => { label=>'Device MAC address delete command', }, ; %info = ( - 'svc' => 'svc_phone', + 'svc' => [qw( svc_phone part_device )], 'desc' => 'Run remote commands via SSH, for phone numbers', 'options' => \%options, 'notes' => <<'END' @@ -50,6 +52,7 @@ old_ for replace operations): <LI><code>$pin</code> - Personal identification number <LI><code>$cust_name</code> - Customer name (quoted for the shell) <LI><code>$pkgnum</code> - Internal package number + <LI><code>$mac_addr</code> - MAC address (Device MAC address insert and delete commands only) </UL> END ); @@ -57,27 +60,41 @@ END sub rebless { shift; } sub _export_insert { - my($self) = shift; + my $self = shift; $self->_export_command('useradd', @_); } sub _export_delete { - my($self) = shift; + my $self = shift; $self->_export_command('userdel', @_); } sub _export_suspend { - my($self) = shift; + my $self = shift; $self->_export_command('suspend', @_); } sub _export_unsuspend { - my($self) = shift; + my $self = shift; $self->_export_command('unsuspend', @_); } +sub export_device_insert { + my( $self, $svc_phone, $phone_device ) = @_; + $self->_export_command('mac_insert', $svc_phone, + 'mac_addr'=>$phone_device->mac_addr + ); +} + +sub export_device_delete { + my( $self, $svc_phone, $phone_device ) = @_; + $self->_export_command('mac_delete', $svc_phone, + 'mac_addr'=>$phone_device->mac_addr + ); +} + sub _export_command { - my ( $self, $action, $svc_phone) = (shift, shift, shift); + my ( $self, $action, $svc_phone, %addl_vars) = @_; my $command = $self->option($action); return '' if $command =~ /^\s*$/; @@ -86,6 +103,7 @@ sub _export_command { { no strict 'refs'; ${$_} = $svc_phone->getfield($_) foreach $svc_phone->fields; + ${$_} = $addl_vars{$_} foreach keys %addl_vars; } my $cust_pkg = $svc_phone->cust_svc->cust_pkg; my $pkgnum = $cust_pkg ? $cust_pkg->pkgnum : ''; diff --git a/FS/FS/part_export/shellcommands.pm b/FS/FS/part_export/shellcommands.pm index f964af31c..9408d1454 100644 --- a/FS/FS/part_export/shellcommands.pm +++ b/FS/FS/part_export/shellcommands.pm @@ -243,12 +243,12 @@ sub _export_command { ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields; # snarfs are unused at this point? - my $count = 1; - foreach my $acct_snarf ( $svc_acct->acct_snarf ) { - ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) ) - foreach qw( machine username _password ); - $count++; - } + # my $count = 1; + # foreach my $acct_snarf ( $svc_acct->acct_snarf ) { + # ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) ) + # foreach qw( machine username _password ); + # $count++; + # } } my $cust_pkg = $svc_acct->cust_svc->cust_pkg; diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm index 58cc5be95..833dd9a1d 100644 --- a/FS/FS/part_export/sqlradius.pm +++ b/FS/FS/part_export/sqlradius.pm @@ -597,7 +597,8 @@ New-style: pass a hashref with the following keys: =item stoptime_end - Upper bound for AcctStopTime, as a UNIX timestamp -=item open_sessions - Only show records with no AcctStopTime (typically used without stoptime_* options and with starttime_* options instead) +=item session_status - 'closed' to only show records with AcctStopTime, +'open' to only show records I<without> AcctStopTime, empty to show both. =item starttime_start - Lower bound for AcctStartTime, as a UNIX timestamp @@ -727,17 +728,27 @@ sub usage_sessions { push @where, " CalledStationID LIKE 'sip:$prefix\%'"; } - if ( $start ) { - push @where, "$str2time AcctStopTime ) >= ?"; - push @param, $start; - } - if ( $end ) { - push @where, "$str2time AcctStopTime ) <= ?"; - push @param, $end; + my $acctstoptime = ''; + if ( $opt->{session_status} ne 'open' ) { + if ( $start ) { + $acctstoptime .= "$str2time AcctStopTime ) >= ?"; + push @param, $start; + $acctstoptime .= ' AND ' if $end; + } + if ( $end ) { + $acctstoptime .= "$str2time AcctStopTime ) <= ?"; + push @param, $end; + } } - if ( $opt->{open_sessions} ) { - push @where, 'AcctStopTime IS NULL'; + if ( $opt->{session_status} ne 'closed' ) { + if ( $acctstoptime ) { + $acctstoptime = "( ( $acctstoptime ) OR AcctStopTime IS NULL )"; + } else { + $acctstoptime = 'AcctStopTime IS NULL'; + } } + push @where, $acctstoptime; + if ( $opt->{starttime_start} ) { push @where, "$str2time AcctStartTime ) >= ?"; push @param, $opt->{starttime_start}; @@ -756,10 +767,14 @@ sub usage_sessions { my $orderby = 'ORDER BY AcctStartTime DESC'; $orderby = '' if $summarize; - my $sth = $dbh->prepare('SELECT '. join(', ', @fields). - " FROM radacct $where $groupby $orderby - ") or die $dbh->errstr; - $sth->execute(@param) or die $sth->errstr; + my $sql = 'SELECT '. join(', ', @fields). + " FROM radacct $where $groupby $orderby"; + if ( $DEBUG ) { + warn $sql; + warn join(',', @param); + } + my $sth = $dbh->prepare($sql) or die $dbh->errstr; + $sth->execute(@param) or die $sth->errstr; [ map { { %$_ } } @{ $sth->fetchall_arrayref({}) } ]; diff --git a/FS/FS/part_export/status_shellcommands.pm b/FS/FS/part_export/status_shellcommands.pm index 53d2b3754..c5200ec50 100644 --- a/FS/FS/part_export/status_shellcommands.pm +++ b/FS/FS/part_export/status_shellcommands.pm @@ -43,6 +43,10 @@ sub _export_unsuspend {} sub export_setstatus { my($self, $svc_acct, $hashref) = @_; + for (qw( spam_tag2_level spam_kill_level )) { + $hashref->{$_} =~ /^\d+(\.\d+)?$/ or return "illegal $_"; + } + my @shellargs = ( $svc_acct->svcnum, user => $self->option('user') || 'root', diff --git a/FS/FS/part_export/test.pm b/FS/FS/part_export/test.pm new file mode 100644 index 000000000..126897c0b --- /dev/null +++ b/FS/FS/part_export/test.pm @@ -0,0 +1,75 @@ +package FS::part_export::test; + +use strict; +use vars qw(%options %info); +use Tie::IxHash; +use base qw(FS::part_export); + +tie %options, 'Tie::IxHash', + 'result' => { label => 'Result', + type => 'select', + options => [ 'success', 'failure', 'exception' ], + default => 'success', + }, + 'errormsg'=> { label => 'Error message', + default => 'Test export' }, + 'insert' => { label => 'Insert', type => 'checkbox', default => 1, }, + 'delete' => { label => 'Delete', type => 'checkbox', default => 1, }, + 'replace' => { label => 'Replace',type => 'checkbox', default => 1, }, + 'suspend' => { label => 'Suspend',type => 'checkbox', default => 1, }, + 'unsuspend'=>{ label => 'Unsuspend', type => 'checkbox', default => 1, }, +; + +%info = ( + 'svc' => [ qw(svc_acct svc_broadband svc_phone svc_domain) ], + 'desc' => 'Test export for development', + 'options' => \%options, + 'notes' => <<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; diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index 6e7f8f87e..e788269f7 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -1,7 +1,8 @@ package FS::part_pkg; +use base qw( FS::m2m_Common FS::o2m_Common FS::option_Common ); use strict; -use vars qw( @ISA %plans $DEBUG $setup_hack $skip_pkg_svc_hack ); +use vars qw( %plans $DEBUG $setup_hack $skip_pkg_svc_hack ); use Carp qw(carp cluck confess); use Scalar::Util qw( blessed ); use Time::Local qw( timelocal_nocheck ); @@ -16,14 +17,15 @@ use FS::type_pkgs; use FS::part_pkg_option; use FS::pkg_class; use FS::agent; +use FS::part_pkg_msgcat; use FS::part_pkg_taxrate; use FS::part_pkg_taxoverride; use FS::part_pkg_taxproduct; use FS::part_pkg_link; use FS::part_pkg_discount; +use FS::part_pkg_usage; use FS::part_pkg_vendor; -@ISA = qw( FS::m2m_Common FS::option_Common ); $DEBUG = 0; $setup_hack = 0; $skip_pkg_svc_hack = 0; @@ -364,7 +366,7 @@ sub replace { ? shift : { @_ }; - $options->{options} = {} unless defined($options->{options}); + $options->{options} = { $old->options } unless defined($options->{options}); warn "FS::part_pkg::replace called on $new to replace $old with options". join(', ', map "$_ => ". $options->{$_}, keys %$options) @@ -446,53 +448,55 @@ sub replace { } warn " replacing pkg_svc records" if $DEBUG; - my $pkg_svc = $options->{'pkg_svc'} || {}; + my $pkg_svc = $options->{'pkg_svc'}; my $hidden_svc = $options->{'hidden_svc'} || {}; - foreach my $part_svc ( qsearch('part_svc', {} ) ) { - my $quantity = $pkg_svc->{$part_svc->svcpart} || 0; - my $hidden = $hidden_svc->{$part_svc->svcpart} || ''; - my $primary_svc = - ( defined($options->{'primary_svc'}) && $options->{'primary_svc'} - && $options->{'primary_svc'} == $part_svc->svcpart - ) - ? 'Y' - : ''; + if ( $pkg_svc ) { # if it wasn't passed, don't change existing pkg_svcs + foreach my $part_svc ( qsearch('part_svc', {} ) ) { + my $quantity = $pkg_svc->{$part_svc->svcpart} || 0; + my $hidden = $hidden_svc->{$part_svc->svcpart} || ''; + my $primary_svc = + ( defined($options->{'primary_svc'}) && $options->{'primary_svc'} + && $options->{'primary_svc'} == $part_svc->svcpart + ) + ? 'Y' + : ''; - my $old_pkg_svc = qsearchs('pkg_svc', { - 'pkgpart' => $old->pkgpart, - 'svcpart' => $part_svc->svcpart, + my $old_pkg_svc = qsearchs('pkg_svc', { + 'pkgpart' => $old->pkgpart, + 'svcpart' => $part_svc->svcpart, + } + ); + my $old_quantity = 0; + my $old_primary_svc = ''; + my $old_hidden = ''; + if ( $old_pkg_svc ) { + $old_quantity = $old_pkg_svc->quantity; + $old_primary_svc = $old_pkg_svc->primary_svc + if $old_pkg_svc->dbdef_table->column('primary_svc'); # is this needed? + $old_hidden = $old_pkg_svc->hidden; } - ); - my $old_quantity = 0; - my $old_primary_svc = ''; - my $old_hidden = ''; - if ( $old_pkg_svc ) { - $old_quantity = $old_pkg_svc->quantity; - $old_primary_svc = $old_pkg_svc->primary_svc - if $old_pkg_svc->dbdef_table->column('primary_svc'); # is this needed? - $old_hidden = $old_pkg_svc->hidden; - } - - next unless $old_quantity != $quantity || - $old_primary_svc ne $primary_svc || - $old_hidden ne $hidden; - - my $new_pkg_svc = new FS::pkg_svc( { - 'pkgsvcnum' => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ), - 'pkgpart' => $new->pkgpart, - 'svcpart' => $part_svc->svcpart, - 'quantity' => $quantity, - 'primary_svc' => $primary_svc, - 'hidden' => $hidden, - } ); - my $error = $old_pkg_svc - ? $new_pkg_svc->replace($old_pkg_svc) - : $new_pkg_svc->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - } + + next unless $old_quantity != $quantity || + $old_primary_svc ne $primary_svc || + $old_hidden ne $hidden; + + my $new_pkg_svc = new FS::pkg_svc( { + 'pkgsvcnum' => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ), + 'pkgpart' => $new->pkgpart, + 'svcpart' => $part_svc->svcpart, + 'quantity' => $quantity, + 'primary_svc' => $primary_svc, + 'hidden' => $hidden, + } ); + my $error = $old_pkg_svc + ? $new_pkg_svc->replace($old_pkg_svc) + : $new_pkg_svc->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } #foreach $part_svc + } #if $options->{pkg_svc} my @part_pkg_vendor = $old->part_pkg_vendor; my @current_exportnum = (); @@ -712,6 +716,35 @@ sub propagate { join("\n", @error); } +=item pkg_locale LOCALE + +Returns a customer-viewable string representing this package for the given +locale, from the part_pkg_msgcat table. If the given locale is empty or no +localized string is found, returns the base pkg field. + +=cut + +sub pkg_locale { + my( $self, $locale ) = @_; + return $self->pkg unless $locale; + my $part_pkg_msgcat = $self->part_pkg_msgcat($locale) or return $self->pkg; + $part_pkg_msgcat->pkg; +} + +=item part_pkg_msgcat LOCALE + +Like pkg_locale, but returns the FS::part_pkg_msgcat object itself. + +=cut + +sub part_pkg_msgcat { + my( $self, $locale ) = @_; + qsearchs( 'part_pkg_msgcat', { + pkgpart => $self->pkgpart, + locale => $locale, + }); +} + =item pkg_comment [ OPTION => VALUE... ] Returns an (internal) string representing this package. Currently, @@ -1175,6 +1208,17 @@ sub svc_part_pkg_link { shift->_part_pkg_link('svc', @_); } +=item supp_part_pkg_link + +Returns the associated part_pkg_link records of type 'supp' (supplemental +packages). + +=cut + +sub supp_part_pkg_link { + shift->_part_pkg_link('supp', @_); +} + sub _part_pkg_link { my( $self, $type ) = @_; qsearch({ table => 'part_pkg_link', @@ -1384,6 +1428,18 @@ sub part_pkg_discount { qsearch('part_pkg_discount', { 'pkgpart' => $self->pkgpart }); } +=item part_pkg_usage + +Returns the voice usage pools (see L<FS::part_pkg_usage>) defined for +this package. + +=cut + +sub part_pkg_usage { + my $self = shift; + qsearch('part_pkg_usage', { 'pkgpart' => $self->pkgpart }); +} + =item _rebless Reblesses the object into the FS::part_pkg::PLAN class (if available), where @@ -1439,6 +1495,29 @@ sub recur_cost_permonth { sprintf('%.2f', $self->recur_cost / $self->freq ); } +=item cust_bill_pkg_recur CUST_PKG + +Actual recurring charge for the specified customer package from customer's most +recent invoice + +=cut + +sub cust_bill_pkg_recur { + my($self, $cust_pkg) = @_; + my $cust_bill_pkg = qsearchs({ + 'table' => 'cust_bill_pkg', + 'addl_from' => 'LEFT JOIN cust_bill USING ( invnum )', + 'hashref' => { 'pkgnum' => $cust_pkg->pkgnum, + 'recur' => { op=>'>', value=>'0' }, + }, + 'order_by' => 'ORDER BY cust_bill._date DESC, + cust_bill_pkg.sdate DESC + LIMIT 1 + ', + }) or return 0; #die "use cust_bill_pkg_recur credits with once_perinv condition"; + $cust_bill_pkg->recur; +} + =item format OPTION DATA Returns data formatted according to the function 'format' described diff --git a/FS/FS/part_pkg/base_delayed.pm b/FS/FS/part_pkg/base_delayed.pm deleted file mode 100644 index c6864a692..000000000 --- a/FS/FS/part_pkg/base_delayed.pm +++ /dev/null @@ -1,42 +0,0 @@ -package FS::part_pkg::base_delayed; - -use strict; -use vars qw(@ISA %info); -#use FS::Record qw(qsearch qsearchs); -use FS::part_pkg::base_rate; - -@ISA = qw(FS::part_pkg::base_rate); - -%info = ( - 'name' => 'Free (or setup fee) for X days, then base rate'. - ' (anniversary billing)', - 'shortname' => 'Bulk (manual from "units" option), w/intro period', - 'inherit_fields' => [ 'global_Mixin' ], - 'fields' => { - 'free_days' => { 'name' => 'Initial free days', - 'default' => 0, - }, - 'recur_notify' => { 'name' => 'Number of days before recurring billing'. - ' commences to notify customer. (0 means'. - ' no warning)', - 'default' => 0, - }, - }, - 'fieldorder' => [ 'free_days', 'recur_notify', - ], - #'setup' => '\'my $d = $cust_pkg->bill || $time; $d += 86400 * \' + what.free_days.value + \'; $cust_pkg->bill($d); $cust_pkg_mod_flag=1; \' + what.setup_fee.value', - #'recur' => 'what.recur_fee.value', - 'weight' => 54, #&g! -); - -sub calc_setup { - my($self, $cust_pkg, $time ) = @_; - - my $d = $cust_pkg->bill || $time; - $d += 86400 * $self->option('free_days'); - $cust_pkg->bill($d); - - $self->option('setup_fee'); -} - -1; diff --git a/FS/FS/part_pkg/base_rate.pm b/FS/FS/part_pkg/base_rate.pm deleted file mode 100644 index 43a050610..000000000 --- a/FS/FS/part_pkg/base_rate.pm +++ /dev/null @@ -1,97 +0,0 @@ -package FS::part_pkg::base_rate; - -use strict; -use vars qw(@ISA %info); -#use FS::Record qw(qsearch); -use FS::part_pkg; - -@ISA = qw(FS::part_pkg); - -%info = ( - 'name' => 'Base rate (anniversary billing, Times units ordered)', - # XXX it multiplies recurring fee by cust_pkg option "units", how to - # express that - 'shortname' => 'Bulk (manual from "units" option)', - 'inherit_fields' => [ 'global_Mixin' ], - 'fields' => { - 'externalid' => { 'name' => 'Optional External ID', - 'default' => '', - }, - }, - 'fieldorder' => [ qw( externalid ) ], - 'weight' => 52, -); - -sub price_info { - my $self = shift; - my $conf = new FS::Conf; - my $money_char = $conf->config('money_char') || '$'; - my $setup = $self->option('setup_fee') || 0; - my $recur = $self->option('recur_fee', 1) || 0; - my $str = ''; - $str = $money_char . $setup . ' one-time' if $setup; - $str .= ', ' if ($setup && $recur); - $str .= $money_char . $recur . ' recurring per unit ' if $recur; - $str; -} - - -sub calc_setup { - my($self, $cust_pkg, $sdate, $details ) = @_; - - my $i = 0; - my $count = $self->option( 'additional_count', 'quiet' ) || 0; - while ($i < $count) { - push @$details, $self->option( 'additional_info' . $i++ ); - } - - $self->option('setup_fee'); -} - -sub calc_recur { - my($self, $cust_pkg) = @_; - $self->base_recur($cust_pkg); -} - -sub base_recur { - my($self, $cust_pkg) = @_; - my $units = $cust_pkg->option('units') ? $cust_pkg->option('units') : 1 ; - # default to 1 if not found - sprintf("%.2f", - ($self->option('recur_fee') * $units ) - ); -} - -sub calc_remain { - my ($self, $cust_pkg, %options) = @_; - my $time = $options{'time'} || time; - my $next_bill = $cust_pkg->getfield('bill') || 0; - return 0 if ! $self->base_recur($cust_pkg) - || ! $next_bill - || $next_bill < $time; - - my %sec = ( - 'h' => 3600, # 60 * 60 - 'd' => 86400, # 60 * 60 * 24 - 'w' => 604800, # 60 * 60 * 24 * 7 - 'm' => 2629744, # 60 * 60 * 24 * 365.2422 / 12 - ); - - $self->freq =~ /^(\d+)([hdwm]?)$/ - or die 'unparsable frequency: '. $self->freq; - my $freq_sec = $1 * $sec{$2||'m'}; - return 0 unless $freq_sec; - - sprintf("%.2f", $self->base_recur($cust_pkg) * ( $next_bill - $time ) / $freq_sec ); - -} - -sub is_free_options { - qw( setup_fee recur_fee ); -} - -sub is_prepaid { - 0; #no, we're postpaid -} - -1; diff --git a/FS/FS/part_pkg/delayed_Mixin.pm b/FS/FS/part_pkg/delayed_Mixin.pm index 83e543a4f..ab53bda06 100644 --- a/FS/FS/part_pkg/delayed_Mixin.pm +++ b/FS/FS/part_pkg/delayed_Mixin.pm @@ -23,7 +23,8 @@ use NEXT; ); sub calc_setup { - my($self, $cust_pkg, $time ) = @_; + my $self = shift; + my( $cust_pkg, $time ) = @_; unless ( $self->option('delay_setup', 1) ) { my $d = $cust_pkg->bill || $time; @@ -31,7 +32,7 @@ sub calc_setup { $cust_pkg->bill($d); } - $self->option('setup_fee'); + $self->NEXT::calc_setup(@_); } sub calc_remain { diff --git a/FS/FS/part_pkg/flat_introrate.pm b/FS/FS/part_pkg/flat_introrate.pm index 10c205609..733760276 100644 --- a/FS/FS/part_pkg/flat_introrate.pm +++ b/FS/FS/part_pkg/flat_introrate.pm @@ -1,12 +1,8 @@ package FS::part_pkg::flat_introrate; +use base qw( FS::part_pkg::flat ); use strict; -use vars qw(@ISA %info $DEBUG $me); -use FS::part_pkg::flat; - -@ISA = qw(FS::part_pkg::flat); -$me = '[' . __PACKAGE__ . ']'; -$DEBUG = 0; +use vars qw( %info ); %info = ( 'name' => 'Introductory price for X months, then flat rate,'. diff --git a/FS/FS/part_pkg/prorate_Mixin.pm b/FS/FS/part_pkg/prorate_Mixin.pm index d148c963d..9efc7e89d 100644 --- a/FS/FS/part_pkg/prorate_Mixin.pm +++ b/FS/FS/part_pkg/prorate_Mixin.pm @@ -67,11 +67,11 @@ the base price per billing cycle. Options: - add_full_period: Bill for the time up to the prorate day plus one full -billing period after that. + billing period after that. - prorate_round_day: Round the current time to the nearest full day, -instead of using the exact time. + instead of using the exact time. - prorate_defer_bill: Don't bill the prorate interval until the prorate -day arrives. + day arrives. - prorate_verbose: Generate details to explain the prorate calculations. =cut @@ -104,7 +104,7 @@ sub calc_prorate { $add_period = 1; } - # if the customer alreqady has a billing day-of-month established, + # if the customer already has a billing day-of-month established, # and it's a valid cutoff day, try to respect it my $next_bill_day; if ( my $next_bill = $cust_pkg->cust_main->next_bill_date ) { @@ -123,38 +123,56 @@ sub calc_prorate { my $permonth = $charge / $self->freq; my $months = ( ( $self->freq - 1 ) + ($mend-$mnow) / ($mend-$mstart) ); - - if ( $self->option('prorate_verbose',1) - and $months > 0 and $months < $self->freq ) { - push @$details, - 'Prorated (' . time2str('%b %d', $mnow) . - ' - ' . time2str('%b %d', $mend) . '): ' . $money_char . - sprintf('%.2f', $permonth * $months + 0.00000001 ); - } + # after this, $self->freq - 1 < $months <= $self->freq # add a full period if currently billing for a partial period # or periods up to freq_override if billing for an override interval if ( ($param->{'freq_override'} || 0) > 1 ) { $months += $param->{'freq_override'} - 1; - } - elsif ( $add_period && $months < $self->freq) { + # freq_override - 1 correct here? + # (probably only if freq == 1, yes?) + } elsif ( $add_period && $months < $self->freq ) { - if ( $self->option('prorate_verbose',1) ) { - # calculate the prorated and add'l period charges + # 'add_period' is a misnomer. + # we add enough to make the total at least a full period + $months++; + $$sdate = $self->add_freq($mstart, 1); + # now $self->freq <= $months <= $self->freq + 1 + # (note that this only happens if $months < $self->freq to begin with) + + } + + if ( $self->option('prorate_verbose',1) and $months > 0 ) { + if ( $months < $self->freq ) { + # we are billing a fractional period only + # # (though maybe not a fractional month) + my $period_end = $self->add_freq($mstart); + push @$details, + 'Prorated (' . time2str('%b %d', $mnow) . + ' - ' . time2str('%b %d', $period_end) . '): ' . $money_char . + sprintf('%.2f', $permonth * $months + 0.00000001 ); + + } elsif ( $months > $self->freq ) { + # we are billing MORE than a full period push @$details, - 'First full month: ' . $money_char . - sprintf('%.2f', $permonth); - } - $months += $self->freq; - $$sdate = $self->add_freq($mstart); + 'Prorated (' . time2str('%b %d', $mnow) . + ' - ' . time2str('%b %d', $mend) . '): ' . $money_char . + sprintf('%.2f', $permonth * ($months - $self->freq + 0.0000001)), + + 'First full period: ' . $money_char . + sprintf('%.2f', $permonth * $self->freq); + } # else $months == $self->freq, and no prorating has happened } $param->{'months'} = $months; #so 1.005 rounds to 1.01 $charge = sprintf('%.2f', $permonth * $months + 0.00000001 ); - return $charge; + my $quantity = $cust_pkg->quantity || 1; + $charge *= $quantity; + + return sprintf('%.2f', $charge); } =item prorate_setup CUST_PKG SDATE diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm index aae51e96c..21c6a8a2c 100644 --- a/FS/FS/part_pkg/voip_cdr.pm +++ b/FS/FS/part_pkg/voip_cdr.pm @@ -51,6 +51,11 @@ tie my %unrateable_opts, 'Tie::IxHash', 2 => 'Flag for later review', ; +tie my %detail_formats, 'Tie::IxHash', + '' => '', + FS::cdr::invoice_formats() +; + %info = ( 'name' => 'VoIP rating by plan of CDR records in an internal (or external) SQL table', 'shortname' => 'VoIP/telco CDR rating (standard)', @@ -152,10 +157,16 @@ tie my %unrateable_opts, 'Tie::IxHash', 'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: ', }, - 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to: ', + 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to this cdrtypenum: ', + }, + + 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to this cdrtypenum: ', + }, + + 'use_calltypenum' => { 'name' => 'Only charge for CDRs where the CDR Call Type is set to this calltypenum: ', }, - 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to: ', + 'ignore_calltypenum' => { 'name' => 'Do not charge for CDRs where the CDR Call Type is set to this calltypenum: ', }, 'ignore_disposition' => { 'name' => 'Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: ', @@ -203,6 +214,11 @@ tie my %unrateable_opts, 'Tie::IxHash', 'skip_max_callers' => { 'name' => 'Do not charge for CDRs where max_callers is less than or equal to this value: ', }, + 'skip_same_customer' => { + 'name' => 'Do not charge for calls between numbers belonging to the same customer', + 'type' => 'checkbox', + }, + 'use_duration' => { 'name' => 'Calculate usage based on the duration field instead of the billsec field', 'type' => 'checkbox', }, @@ -211,12 +227,25 @@ tie my %unrateable_opts, 'Tie::IxHash', }, #false laziness w/cdr_termination.pm - 'output_format' => { 'name' => 'CDR invoice display format', + 'output_format' => { 'name' => 'CDR display format for invoices', 'type' => 'select', - 'select_options' => { FS::cdr::invoice_formats() }, + 'select_options' => \%detail_formats, 'default' => 'default', #XXX test }, + 'selfservice_format' => + { 'name' => 'CDR display format for selfservice', + 'type' => 'select', + 'select_options' => \%detail_formats, + 'default' => 'default' + }, + 'selfservice_inbound_format' => + { 'name' => 'Inbound CDR display format for selfservice', + 'type' => 'select', + 'select_options' => \%detail_formats, + 'default' => '' + }, + 'usage_section' => { 'name' => 'Section in which to place usage charges (whether separated or not): ', }, @@ -286,6 +315,7 @@ tie my %unrateable_opts, 'Tie::IxHash', use_amaflags use_carrierid use_cdrtypenum ignore_cdrtypenum + use_calltypenum ignore_calltypenum ignore_disposition disposition_in skip_dcontext skip_dst_prefix skip_dstchannel_prefix skip_src_length_more @@ -295,9 +325,12 @@ tie my %unrateable_opts, 'Tie::IxHash', noskip_dst_length_accountcode_tollfree skip_lastapp skip_max_callers + skip_same_customer use_duration 411_rewrite - output_format usage_mandate summarize_usage usage_section + output_format + selfservice_format selfservice_inbound_format + usage_mandate summarize_usage usage_section bill_every_call bill_inactive_svcs count_available_phones suspend_bill ) @@ -394,6 +427,7 @@ sub calc_usage { 'disable_src' => $self->option('disable_src'), 'default_prefix' => $self->option('default_prefix'), 'cdrtypenum' => $self->option('use_cdrtypenum'), + 'calltypenum' => $self->option('use_calltypenum'), 'status' => '', 'for_update' => 1, ); # $last_bill, $$sdate ) @@ -414,6 +448,7 @@ sub calc_usage { my $error = $cdr->rate( 'part_pkg' => $self, + 'cust_pkg' => $cust_pkg, 'svcnum' => $svc_x->svcnum, 'single_price_included_min' => \$included_min, 'region_group_included_min' => \$region_group_included_min, @@ -460,6 +495,7 @@ sub calc_usage { } #returns a reason why not to rate this CDR, or false if the CDR is chargeable +# lots of false laziness w/voip_inbound sub check_chargable { my( $self, $cdr, %flags ) = @_; @@ -493,6 +529,15 @@ sub check_chargable { if length($self->option_cacheable('ignore_cdrtypenum')) && $cdr->cdrtypenum eq $self->option_cacheable('ignore_cdrtypenum'); #eq otherwise 0 matches '' + # unlike everything else, use_calltypenum is applied in FS::svc_x::get_cdrs. + return "calltypenum != ". $self->option_cacheable('use_calltypenum') + if length($self->option_cacheable('use_calltypenum')) + && $cdr->calltypenum ne $self->option_cacheable('use_calltypenum'); #ne otherwise 0 matches '' + + return "calltypenum == ". $self->option_cacheable('ignore_calltypenum') + if length($self->option_cacheable('ignore_calltypenum')) + && $cdr->calltypenum eq $self->option_cacheable('ignore_calltypenum'); #eq otherwise 0 matches '' + return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )" if $self->option_cacheable('skip_dcontext') =~ /\S/ && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext')); @@ -561,6 +606,41 @@ sub calc_units { $count; } +sub reset_usage { + my ($self, $cust_pkg, %opt) = @_; + my @part_pkg_usage = $self->part_pkg_usage or return ''; + warn " resetting usage minutes\n" if $opt{debug}; + my %cust_pkg_usage = map { $_->pkgusagepart, $_ } $cust_pkg->cust_pkg_usage; + foreach my $part_pkg_usage (@part_pkg_usage) { + my $part = $part_pkg_usage->pkgusagepart; + my $usage = $cust_pkg_usage{$part} || + FS::cust_pkg_usage->new({ + 'pkgnum' => $cust_pkg->pkgnum, + 'pkgusagepart' => $part, + 'minutes' => $part_pkg_usage->minutes, + }); + foreach my $cdr_usage ( + qsearch('cdr_cust_pkg_usage', {'cdrusagenum' => $usage->cdrusagenum}) + ) { + my $error = $cdr_usage->delete; + warn " error resetting CDR usage: $error\n"; + } + + if ( $usage->pkgusagenum ) { + if ( $part_pkg_usage->rollover ) { + $usage->set('minutes', $part_pkg_usage->minutes + $usage->minutes); + } else { + $usage->set('minutes', $part_pkg_usage->minutes); + } + my $error = $usage->replace; + warn " error resetting usage minutes: $error\n" if $error; + } else { + my $error = $usage->insert; + warn " error resetting usage minutes: $error\n" if $error; + } + } #foreach $part_pkg_usage +} + # tells whether cust_bill_pkg_detail should return a single line for # each phonenum sub sum_usage { diff --git a/FS/FS/part_pkg/voip_inbound.pm b/FS/FS/part_pkg/voip_inbound.pm index 9054f7b99..525db804d 100644 --- a/FS/FS/part_pkg/voip_inbound.pm +++ b/FS/FS/part_pkg/voip_inbound.pm @@ -60,15 +60,21 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities(); 'type' => 'checkbox', }, - 'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to: ', + 'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: ', }, - 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to: ', + 'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to this cdrtypenum: ', }, - 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to: ', + 'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to this cdrtypenum: ', }, + 'use_calltypenum' => { 'name' => 'Only charge for CDRs where the CDR Call Type is set to this cdrtypenum: ', + }, + + 'ignore_calltypenum' => { 'name' => 'Do not charge for CDRs where the CDR Call Type is set to this cdrtypenum: ', + }, + 'ignore_disposition' => { 'name' => 'Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: ', }, @@ -147,6 +153,7 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities(); use_amaflags use_carrierid use_cdrtypenum ignore_cdrtypenum + use_calltypenum ignore_calltypenum ignore_disposition disposition_in skip_dcontext skip_dstchannel_prefix skip_dst_length_less skip_lastapp @@ -329,67 +336,58 @@ sub calc_usage { } #returns a reason why not to rate this CDR, or false if the CDR is chargeable +# lots of false laziness w/voip_cdr... sub check_chargable { my( $self, $cdr, %flags ) = @_; - #should have some better way of checking these options from a hash - #or something - - my @opt = qw( - use_amaflags - use_carrierid - use_cdrtypenum - ignore_cdrtypenum - disposition_in - ignore_disposition - skip_dcontext - skip_dstchannel_prefix - skip_dst_length_less - skip_lastapp - ); - foreach my $opt (grep !exists($flags{option_cache}->{$_}), @opt ) { - $flags{option_cache}->{$opt} = $self->option($opt, 1); - } - my %opt = %{ $flags{option_cache} }; - return 'amaflags != 2' - if $opt{'use_amaflags'} && $cdr->amaflags != 2; - - return "disposition NOT IN ( $opt{'disposition_in'} )" - if $opt{'disposition_in'} =~ /\S/ - && !grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $opt{'disposition_in'}); - - return "disposition IN ( $opt{'ignore_disposition'} )" - if $opt{'ignore_disposition'} =~ /\S/ - && grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $opt{'ignore_disposition'}); - - return "carrierid != $opt{'use_carrierid'}" - if length($opt{'use_carrierid'}) - && $cdr->carrierid ne $opt{'use_carrierid'}; #ne otherwise 0 matches '' + if $self->option_cacheable('use_amaflags') && $cdr->amaflags != 2; - return "cdrtypenum != $opt{'use_cdrtypenum'}" - if length($opt{'use_cdrtypenum'}) - && $cdr->cdrtypenum ne $opt{'use_cdrtypenum'}; #ne otherwise 0 matches '' - - return "cdrtypenum == $opt{'ignore_cdrtypenum'}" - if length($opt{'ignore_cdrtypenum'}) - && $cdr->cdrtypenum eq $opt{'ignore_cdrtypenum'}; #eq otherwise 0 matches '' + return "disposition NOT IN ( ". $self->option_cacheable('disposition_in')." )" + if $self->option_cacheable('disposition_in') =~ /\S/ + && !grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('disposition_in')); + + return "disposition IN ( ". $self->option_cacheable('ignore_disposition')." )" + if $self->option_cacheable('ignore_disposition') =~ /\S/ + && grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('ignore_disposition')); + + return "carrierid NOT IN ( ". $self->option_cacheable('use_carrierid'). " )" + if $self->option_cacheable('use_carrierid') =~ /\S/ + && !grep { $cdr->carrierid eq $_ } split(/\s*,\s*/, $self->option_cacheable('use_carrierid')); #eq otherwise 0 matches '' + + # unlike everything else, use_cdrtypenum is applied in FS::svc_x::get_cdrs. + return "cdrtypenum != ". $self->option_cacheable('use_cdrtypenum') + if length($self->option_cacheable('use_cdrtypenum')) + && $cdr->cdrtypenum ne $self->option_cacheable('use_cdrtypenum'); #ne otherwise 0 matches '' + + return "cdrtypenum == ". $self->option_cacheable('ignore_cdrtypenum') + if length($self->option_cacheable('ignore_cdrtypenum')) + && $cdr->cdrtypenum eq $self->option_cacheable('ignore_cdrtypenum'); #eq otherwise 0 matches '' + + # unlike everything else, use_calltypenum is applied in FS::svc_x::get_cdrs. + return "calltypenum != ". $self->option_cacheable('use_calltypenum') + if length($self->option_cacheable('use_calltypenum')) + && $cdr->calltypenum ne $self->option_cacheable('use_calltypenum'); #ne otherwise 0 matches '' + + return "calltypenum == ". $self->option_cacheable('ignore_calltypenum') + if length($self->option_cacheable('ignore_calltypenum')) + && $cdr->calltypenum eq $self->option_cacheable('ignore_calltypenum'); #eq otherwise 0 matches '' - return "dcontext IN ( $opt{'skip_dcontext'} )" - if $opt{'skip_dcontext'} =~ /\S/ - && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $opt{'skip_dcontext'}); + return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )" + if $self->option_cacheable('skip_dcontext') =~ /\S/ + && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext')); - my $len_prefix = length($opt{'skip_dstchannel_prefix'}); - return "dstchannel starts with $opt{'skip_dstchannel_prefix'}" + my $len_prefix = length($self->option_cacheable('skip_dstchannel_prefix')); + return "dstchannel starts with ". $self->option_cacheable('skip_dstchannel_prefix') if $len_prefix - && substr($cdr->dstchannel,0,$len_prefix) eq $opt{'skip_dstchannel_prefix'}; + && substr($cdr->dstchannel,0,$len_prefix) eq $self->option_cacheable('skip_dstchannel_prefix'); - my $dst_length = $opt{'skip_dst_length_less'}; + my $dst_length = $self->option_cacheable('skip_dst_length_less'); return "destination less than $dst_length digits" if $dst_length && length($cdr->dst) < $dst_length; - return "lastapp is $opt{'skip_lastapp'}" - if length($opt{'skip_lastapp'}) && $cdr->lastapp eq $opt{'skip_lastapp'}; + return "lastapp is ". $self->option_cacheable('skip_lastapp') + if length($self->option_cacheable('skip_lastapp')) && $cdr->lastapp eq $self->option_cacheable('skip_lastapp'); #all right then, rate it ''; diff --git a/FS/FS/part_pkg_link.pm b/FS/FS/part_pkg_link.pm index fb7a8d387..9ce8e6a76 100644 --- a/FS/FS/part_pkg_link.pm +++ b/FS/FS/part_pkg_link.pm @@ -49,12 +49,13 @@ Destination package (see L<FS::part_pkg>) =item link_type Link type - currently, "bill" (source package bills a line item from target -package), or "svc" (source package includes services from target package). +package), or "svc" (source package includes services from target package), +or "supp" (ordering source package creates a target package). =item hidden Flag indicating that this subpackage should be felt, but not seen as an invoice -line item when set to 'Y' +line item when set to 'Y'. Not allowed for "supp" links. =back @@ -119,11 +120,26 @@ sub check { $self->ut_numbern('pkglinknum') || $self->ut_foreign_key('src_pkgpart', 'part_pkg', 'pkgpart') || $self->ut_foreign_key('dst_pkgpart', 'part_pkg', 'pkgpart') - || $self->ut_enum('link_type', [ 'bill', 'svc' ] ) + || $self->ut_enum('link_type', [ 'bill', 'svc', 'supp' ] ) || $self->ut_enum('hidden', [ '', 'Y' ] ) ; return $error if $error; + if ( $self->link_type eq 'supp' ) { + # some sanity checking + my $src_pkg = $self->src_pkg; + my $dst_pkg = $self->dst_pkg; + if ( $src_pkg->freq eq '0' and $dst_pkg->freq ne '0' ) { + return "One-time charges can't have supplemental packages." + } elsif ( $dst_pkg->freq ne '0' ) { + my $ratio = $dst_pkg->freq / $src_pkg->freq; + if ($ratio != int($ratio)) { + return "Supplemental package period (pkgpart ".$dst_pkg->pkgpart. + ") must be an integer multiple of main package period."; + } + } + } + $self->SUPER::check; } diff --git a/FS/FS/part_pkg_msgcat.pm b/FS/FS/part_pkg_msgcat.pm new file mode 100644 index 000000000..7c00c26ac --- /dev/null +++ b/FS/FS/part_pkg_msgcat.pm @@ -0,0 +1,138 @@ +package FS::part_pkg_msgcat; + +use strict; +use base qw( FS::Record ); +use FS::Locales; +#use FS::Record qw( qsearch qsearchs ); +use FS::part_pkg; + +=head1 NAME + +FS::part_pkg_msgcat - Object methods for part_pkg_msgcat records + +=head1 SYNOPSIS + + use FS::part_pkg_msgcat; + + $record = new FS::part_pkg_msgcat \%hash; + $record = new FS::part_pkg_msgcat { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::part_pkg_msgcat object represents localized labels of a package +definition. FS::part_pkg_msgcat inherits from FS::Record. The following +fields are currently supported: + +=over 4 + +=item pkgpartmsgnum + +primary key + +=item pkgpart + +Package definition + +=item locale + +locale + +=item pkg + +Localized package name (customer-viewable) + +=item comment + +Localized package comment (non-customer-viewable), optional + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record. To add the record to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I<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; + diff --git a/FS/FS/part_pkg_usage.pm b/FS/FS/part_pkg_usage.pm new file mode 100644 index 000000000..99014d398 --- /dev/null +++ b/FS/FS/part_pkg_usage.pm @@ -0,0 +1,159 @@ +package FS::part_pkg_usage; + +use strict; +use base qw( FS::m2m_Common FS::Record ); +use FS::Record qw( qsearch qsearchs ); +use Scalar::Util qw(blessed); + +=head1 NAME + +FS::part_pkg_usage - Object methods for part_pkg_usage records + +=head1 SYNOPSIS + + use FS::part_pkg_usage; + + $record = new FS::part_pkg_usage \%hash; + $record = new FS::part_pkg_usage { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::part_pkg_usage object represents a stock of usage minutes (generally +for voice services) included in a package definition. FS::part_pkg_usage +inherits from FS::Record. The following fields are currently supported: + +=over 4 + +=item pkgusagepart - primary key + +=item pkgpart - the package definition (L<FS::part_pkg>) + +=item minutes - the number of minutes included per billing cycle + +=item priority - the relative order in which to use this stock of minutes. + +=item shared - 'Y' to allow these minutes to be shared with other packages +belonging to the same customer. Otherwise, only usage allocated to this +package will use this stock of minutes. + +=item rollover - 'Y' to allow unused minutes to carry over between billing +cycles. Otherwise, the available minutes will reset to the value of the +"minutes" field upon billing. + +=item description - a text description of this stock of minutes + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +=item insert CLASSES + +=item replace CLASSES + +CLASSES can be an array or hash of usage classnums (see L<FS::usage_class>) +to link to this record. + +=item delete + +=cut + +sub table { 'part_pkg_usage'; } + +sub insert { + my $self = shift; + my $opt = ref($_[0]) eq 'HASH' ? shift : { @_ }; + + $self->SUPER::insert + || $self->process_m2m( 'link_table' => 'part_pkg_usage_class', + 'target_table' => 'usage_class', + 'params' => $opt, + ); +} + +sub replace { + my $self = shift; + my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') ) + ? shift + : $self->replace_old; + my $opt = ref($_[0]) eq 'HASH' ? $_[0] : { @_ }; + $self->SUPER::replace($old) + || $self->process_m2m( 'link_table' => 'part_pkg_usage_class', + 'target_table' => 'usage_class', + 'params' => $opt, + ); +} + +sub delete { + my $self = shift; + $self->process_m2m( 'link_table' => 'part_pkg_usage_class', + 'target_table' => 'usage_class', + 'params' => {}, + ) || $self->SUPER::delete; +} + +=item check + +Checks all fields to make sure this is a valid example. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('pkgusagepart') + || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart') + || $self->ut_number('minutes') + || $self->ut_numbern('priority') + || $self->ut_flag('shared') + || $self->ut_flag('rollover') + || $self->ut_textn('description') + ; + return $error if $error; + + $self->SUPER::check; +} + +=item classnums + +Returns the usage class numbers that are allowed to use minutes from this +pool. + +=cut + +sub classnums { + my $self = shift; + if (!$self->get('classnums')) { + my $classnums = [ + map { $_->classnum } + qsearch('part_pkg_usage_class', { 'pkgusagepart' => $self->pkgusagepart }) + ]; + $self->set('classnums', $classnums); + } + @{ $self->get('classnums') }; +} + +=back + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/part_pkg_usage_class.pm b/FS/FS/part_pkg_usage_class.pm new file mode 100644 index 000000000..9a99783af --- /dev/null +++ b/FS/FS/part_pkg_usage_class.pm @@ -0,0 +1,125 @@ +package FS::part_pkg_usage_class; + +use strict; +use base qw( FS::Record ); +use FS::Record qw( qsearch qsearchs ); + +=head1 NAME + +FS::part_pkg_usage_class - Object methods for part_pkg_usage_class records + +=head1 SYNOPSIS + + use FS::part_pkg_usage_class; + + $record = new FS::part_pkg_usage_class \%hash; + $record = new FS::part_pkg_usage_class { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::part_pkg_usage_class object is a link between a package usage stock +(L<FS::part_pkg_usage>) and a voice usage class (L<FS::usage_class)>. +FS::part_pkg_usage_class inherits from FS::Record. The following fields +are currently supported: + +=over 4 + +=item num - primary key + +=item pkgusagepart - L<FS::part_pkg_usage> key + +=item classnum - L<FS::usage_class> key. Set to null to allow this stock +to be used for calls that have no usage class. To avoid confusion, you +should only do this if you don't use usage classes on your system. + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new example. To add the example to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I<hash> method. + +=cut + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'part_pkg_usage_class'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# the insert method can be inherited from FS::Record + +=item delete + +Delete this record from the database. + +=cut + +# the delete method can be inherited from FS::Record + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +# the replace method can be inherited from FS::Record + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('num') + || $self->ut_foreign_key('pkgusagepart', 'part_pkg_usage', 'pkgusagepart') + || $self->ut_foreign_keyn('classnum', 'usage_class', 'classnum') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +The author forgot to customize this manpage. + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm index c47177171..da794dd4c 100644 --- a/FS/FS/part_svc.pm +++ b/FS/FS/part_svc.pm @@ -58,6 +58,13 @@ L<FS::svc_domain>, and L<FS::svc_forward>, among others. =item preserve - Preserve after cancellation, empty or 'Y' +=item selfservice_access - Access allowed to the service via self-service: +empty for full access, "readonly" for read-only, "hidden" to hide it entirely + +=item restrict_edit_password - Require the "Provision customer service" access +right to change the password field, rather than just "Edit password". Only +relevant to svc_acct for now. + =back =head1 METHODS @@ -391,7 +398,8 @@ sub check { || $self->ut_enum('preserve', [ '', 'Y' ] ) || $self->ut_enum('selfservice_access', [ '', 'hidden', 'readonly' ] ) || $self->ut_foreign_keyn('classnum', 'part_svc_class', 'classnum' ) - ; + || $self->ut_enum('restrict_edit_password', [ '', 'Y' ] ) +; return $error if $error; my @fields = eval { fields( $self->svcdb ) }; #might die @@ -749,11 +757,9 @@ sub process { if ( $flag =~ /^[MAH]$/ ) { $param->{ $f } = delete( $param->{ $f.'_classnum' } ); } - if ( $flag =~ /^S$/ - or $_ eq 'usergroup' ) { - $param->{ $f } = ref($param->{ $f }) - ? join(',', @{$param->{ $f }} ) - : $param->{ $f }; + if ( ( $flag =~ /^[MAHS]$/ or $_ eq 'usergroup' ) + and ref($param->{ $f }) ) { + $param->{ $f } = join(',', @{ $param->{ $f } }); } ( $f, $f.'_flag', $f.'_label' ); } diff --git a/FS/FS/part_svc_column.pm b/FS/FS/part_svc_column.pm index d467516ed..38ce1fa80 100644 --- a/FS/FS/part_svc_column.pm +++ b/FS/FS/part_svc_column.pm @@ -99,8 +99,14 @@ sub check { $self->columnflag(uc($1)); if ( $self->columnflag =~ /^[MA]$/ ) { - $error = - $self->ut_foreign_key( 'columnvalue', 'inventory_class', 'classnum' ); + # split, check all values independently, and normalize + my @classnums = split(/\s*,\s*/, $self->columnvalue); + foreach (@classnums) { + $self->set('columnvalue', $_); + $error = $self->ut_foreign_key( 'columnvalue', 'inventory_class', 'classnum' ); + return $error if $error; + } + $self->set('columnvalue', join(',', @classnums)); } if ( $self->columnflag eq 'H' ) { $error = diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm index b8da9b49b..2a048a115 100644 --- a/FS/FS/pay_batch.pm +++ b/FS/FS/pay_batch.pm @@ -201,7 +201,7 @@ foreach my $INC (@INC) { \\%FS::pay_batch::$mod\::export_info, \$FS::pay_batch::$mod\::name)"; $name ||= $mod; # in case it's not defined - if( $@) { + if ($@) { # in FS::cdr this is a die, not a warn. That's probably a bug. warn "error using FS::pay_batch::$mod (skipping): $@\n"; next; @@ -401,12 +401,12 @@ sub import_results { foreach ('paid', '_date', 'payinfo') { $new_cust_pay_batch->$_($hash{$_}) if $hash{$_}; } - $error = $new_cust_pay_batch->approve($hash{'paybatch'} || $self->batchnum); + $error = $new_cust_pay_batch->approve(%hash); $total += $hash{'paid'}; } elsif ( &{$declined_condition}(\%hash) ) { - $error = $new_cust_pay_batch->decline; + $error = $new_cust_pay_batch->decline($hash{'error_message'});; } @@ -572,8 +572,6 @@ sub import_from_gateway { my $payby; # CARD or CHEK my $error; - # follow realtime gateway practice here - # though eventually this stuff should go into separate fields... my $paybatch = $gateway->gatewaynum . '-' . $gateway->gateway_module . ':' . $item->authorization . ':' . $item->order_number; @@ -644,8 +642,11 @@ sub import_from_gateway { payby => $payby, invnum => $item->invoice_number, batchnum => $pay_batch->batchnum, - paybatch => $paybatch, payinfo => $payinfo, + gatewaynum => $gateway->gatewaynum, + processor => $gateway->gateway_module, + auth => $item->authorization, + order_number => $item->order_number, } ); $error ||= $cust_pay->insert; @@ -725,7 +726,12 @@ sub import_from_gateway { # approval status if ( $item->approved ) { # follow Billing_Realtime format for paybatch - $error = $cust_pay_batch->approve($paybatch); + $error = $cust_pay_batch->approve( + 'gatewaynum' => $gateway->gatewaynum, + 'processor' => $gateway->gateway_module, + 'auth' => $item->authorization, + 'order_number' => $item->order_number, + ); $total += $cust_pay_batch->paid; } else { @@ -829,6 +835,9 @@ sub try_to_resolve { } return $error if $error; } + } elsif ( @unresolved ) { + # auto resolve is not enabled, and we're not ready to resolve + return; } $self->set_status('R'); @@ -1028,7 +1037,6 @@ sub manual_approve { my $self = shift; my $date = time; my %opt = @_; - my $paybatch = $opt{'paybatch'} || $self->batchnum; my $usernum = $opt{'usernum'} || die "manual approval requires a usernum"; my $conf = FS::Conf->new; return 'manual batch approval disabled' @@ -1058,7 +1066,9 @@ sub manual_approve { '_date' => $date, 'usernum' => $usernum, }; - my $error = $new_cust_pay_batch->approve($paybatch); + my $error = $new_cust_pay_batch->approve(); + # there are no approval options here (authorization, order_number, etc.) + # because the transaction wasn't really approved if ( $error ) { $dbh->rollback; return 'paybatchnum '.$cust_pay_batch->paybatchnum.": $error"; diff --git a/FS/FS/pay_batch/BoM.pm b/FS/FS/pay_batch/BoM.pm index 719b504e5..a3708d477 100644 --- a/FS/FS/pay_batch/BoM.pm +++ b/FS/FS/pay_batch/BoM.pm @@ -31,13 +31,13 @@ $name = 'BoM'; }, header => sub { my $pay_batch = shift; - sprintf( "A%10s%04u%06u%05u%54s\n", #80 + sprintf( "A%10s%04u%06u%05u%53s\n", #80 $origid, $pay_batch->batchnum, jdate($pay_batch->download), $datacenter, "") . - sprintf( "XD%03u%06u%-15s%-30s%09u%-12s \n", #80 + sprintf( "XD%03u%06u%-15s%-30s%09u%-12s ", #80 $typecode, jdate($pay_batch->download), $shortname, @@ -48,7 +48,7 @@ $name = 'BoM'; row => sub { my ($cust_pay_batch, $pay_batch) = @_; my ($account, $aba) = split('@', $cust_pay_batch->payinfo); - sprintf( "D%010.0f%09u%-12s%-29s%-19s\n", #80 + sprintf( "D%010.0f%09u%-12s%-29s%-18s ", #80 $cust_pay_batch->amount * 100, $aba, $account, @@ -58,8 +58,8 @@ $name = 'BoM'; }, footer => sub { my ($pay_batch, $batchcount, $batchtotal) = @_; - sprintf( "YD%08u%014.0f%56s\n", $batchcount, $batchtotal*100, ""). #80 - sprintf( "Z%014u%04u%014u%05u%42s\n", #80 now + sprintf( "YD%08u%014.0f%55s\n", $batchcount, $batchtotal*100, ""). #80 + sprintf( "Z%014u%05u%014u%05u%40s", #80 now $batchtotal*100, $batchcount, "0", "0", ""); }, ); diff --git a/FS/FS/pay_batch/eft_canada.pm b/FS/FS/pay_batch/eft_canada.pm index 220fecb3d..b24c9c3a4 100644 --- a/FS/FS/pay_batch/eft_canada.pm +++ b/FS/FS/pay_batch/eft_canada.pm @@ -25,12 +25,6 @@ my %holiday_yearly = ( 12 => { map {$_=>1} 26 }, #boxing day ); my %holiday = ( - 2012 => { - 7 => { map {$_=>1} 2 }, #canada day - 8 => { map {$_=>1} 6 }, #First Monday of August Civic Holiday - 9 => { map {$_=>1} 3 }, #labour day - 10 => { map {$_=>1} 8 }, #thanksgiving - }, 2013 => { 2 => { map {$_=>1} 18 }, #family day 3 => { map {$_=>1} 29 }, #good friday 4 => { map {$_=>1} 1 }, #easter monday diff --git a/FS/FS/pay_batch/nacha.pm b/FS/FS/pay_batch/nacha.pm new file mode 100644 index 000000000..c069082c7 --- /dev/null +++ b/FS/FS/pay_batch/nacha.pm @@ -0,0 +1,208 @@ +package FS::pay_batch::nacha; + +use strict; +use vars qw( %import_info %export_info $name $conf $entry_hash $DEBUG ); +use Date::Format; +#use Time::Local 'timelocal'; +#use FS::Conf; + +$name = 'NACHA'; + +$DEBUG = 0; + +%import_info = ( + #XXX stub finish me + 'filetype' => 'CSV', + 'fields' => [ + ], + 'hook' => sub { + my $hash = shift; + }, + 'approved' => sub { 1 }, + 'declined' => sub { 0 }, +); + +%export_info = ( + + #optional + init => sub { + $conf = shift; + }, + + delimiter => '', + + + header => sub { + my( $pay_batch, $cust_pay_batch_arrayref ) = @_; + + $conf->config('batchconfig-nacha-destination') =~ /^\s*(\d{9})\s*$/ + or die 'illegal NACHA Destination'; + my $dest = $1; + + my $dest_name = $conf->config('batchconfig-nacha-destination_name'); + $dest_name = substr( $dest_name. (' 'x23), 0, 23); + + $conf->config('batchconfig-nacha-origin') =~ /^\s*(\d{10})\s*$/ + or die 'illegal NACHA Origin'; + my $origin = $1; + + my $company = $conf->config('company_name', $pay_batch->agentnum); + $company = substr(uc($company). (' 'x23), 0, 23); + + my $now = time; + + #haha don't want to break after a quarter million years of a batch a day + #or 54 years for 5000 agent-virtualized hosted companies batching daily + my $refcode = substr( (' 'x8). $pay_batch->batchnum, -8); + + #or only 25,000 years or 5.4 for 5000 companies :) + #though they would probably want them numbered per company + my $batchnum = substr( ('0'x7). $pay_batch->batchnum, -7); + + $entry_hash = 0; + + warn "building File & Batch Header Records\n" if $DEBUG; + + ## + # File Header Record + ## + + '1'. #Record Type Code + '01'. #Priority Code + ' '. $dest. #Immediate Destination / 9-digit transit routing # + $origin. #Immediate Origin / 10 digit company number + time2str('%y%m%d', $now). #File Creation Date + time2str('%H%M', $now). #File Creation Time + 'A'. #XXX file ID modifier, mult. files in transit? [A-Z0-9] + '094'. #94 character records + '10'. #Blocking Factor + '1'. #Format code + $dest_name. #Immediate Destination Name / 23 char bank name + $company. #Immediate Origin Name / 23 char company name + $refcode. #Reference Code (internal/optional) + + ### + # Batch Header Record + ### + + '5'. #Record Type Code + '225'. #Service Class Code (220 credits only, + # 200 mixed debits & credits) + substr($company, 0, 16). #on cust. statements + (' 'x20 ). #20 char "company internal use if desired" + $origin. #Company Identification (Immediate Origin) + 'PPD'. #others? + #PPD "Prearranged Payments and Deposit entries" for consumer items + #CCD (Cash Concentration and Disbursement) + #CTX (Corporate Trade Exchange) + #TEL (Telephone initiated entires) + #WEB (Authorization received via the Internet) + 'InterntSvc'. #XXX from conf 10 char txn desc, printed on cust. statements + + #6 char "Descriptive date" printed on customer statements + #XXX now? or use a separate post date? + time2str('%y%m%d', $now). + + #6 char date transactions are to be posted + #XXX now? or do we need a future banking day date like eft_canada trainwreck + time2str('%y%m%d', $now). + + (' 'x3). #Settlement Date / Reserved + '1'. #Originator Status Code + substr($dest, 0, 8). #Originating Financial Institution + $batchnum #Batch Number ("number batches sequentially") + + }, + + 'row' => sub { + my( $cust_pay_batch, $pay_batch, $batchcount, $batchtotal ) = @_; + + my ($account, $aba) = split('@', $cust_pay_batch->payinfo); + + # "Total of all positions 4-11 on each 6 record" + $entry_hash += substr($aba,0,8); + + my $cust_main = $cust_pay_batch->cust_main; + my $cust_identifier = substr($cust_main->display_custnum. (' 'x15), 0, 15); + + #XXX paytype should actually be in the batch, but this will do for now + #27 checking debit, 37 savings debit + my $transaction_code = ( $cust_main->paytype =~ /savings/i ? '37' : '27' ); + + my $cust_name = substr($cust_main->name. (' 'x22), 0, 22); + + #non-PPD transactions? future + + warn "building PPD Record\n" if $DEBUG; + + ### + # PPD Entry Detail Record + ### + + '6'. #Record Type Code + $transaction_code. #Transaction Code + $aba. #Receiving DFI Identification, check digit + substr($account.(' 'x17), 0, 17). #DFI Account number (Left justify) + sprintf('%010d', $cust_pay_batch->amount * 100). #Amount + $cust_identifier. #Individual Identification Number, 15 char + $cust_name. #Individual name (22-char) + ' '. #2 char "company internal use if desired" + '0'. #Addenda Record Indicator + (' 'x15) #15 digit "bank will assign trace number" + # (00000?) + }, + + 'footer' => sub { + my( $pay_batch, $batchcount, $batchtotal ) = @_; + + #Only use the final 10 positions in the entry + $entry_hash = substr( '00'.$entry_hash, -10); + + $conf->config('batchconfig-nacha-destination') =~ /^\s*(\d{9})\s*$/ + or die 'illegal NACHA Destination'; + my $dest = $1; + + $conf->config('batchconfig-nacha-origin') =~ /^\s*(\d{10})\s*$/ + or die 'illegal NACHA Origin'; + my $origin = $1; + + my $batchnum = substr( ('0'x7). $pay_batch->batchnum, -7); + + warn "building Batch & File Control Records\n" if $DEBUG; + + ### + # Batch Control Record + ### + + '8'. #Record Type Code + '225'. #Service Class Code (220 credits only, + # 200 mixed debits&credits) + sprintf('%06d', $batchcount). #Entry / Addenda Count + $entry_hash. + sprintf('%012d', $batchtotal * 100). #Debit total + '000000000000'. #Credit total + $origin. #Company Identification (Immediate Origin) + (' 'x19). #Message Authentication Code (19 char blank) + (' 'x6). #Federal Reserve Use (6 char blank) + substr($dest, 0, 8). #Originating Financial Institution + $batchnum. #Batch Number ("number batches sequentially") + + ### + # File Control Record + ### + + '9'. #Record Type Code + '000001'. #Batch Counter (# of batch header recs) + sprintf('%06d', $batchcount + 4). #num of physical blocks on the file..? + sprintf('%08d', $batchcount). #total # of entry detail and addenda + $entry_hash. + sprintf('%012d', $batchtotal * 100). #Debit total + '000000000000'. #Credit total + ( ' 'x39 ) #Reserved / blank + + }, + +); + +1; + diff --git a/FS/FS/pay_batch/paymentech.pm b/FS/FS/pay_batch/paymentech.pm index c687cc8e2..1ecf35afd 100644 --- a/FS/FS/pay_batch/paymentech.pm +++ b/FS/FS/pay_batch/paymentech.pm @@ -23,7 +23,10 @@ my $gateway; '_date', 'approvalStatus', 'order_number', - 'authorization', + 'auth', + 'procStatus', + 'procStatusMessage', + 'respCodeMessage', ], xmlkeys => [ 'orderID', @@ -31,6 +34,9 @@ my $gateway; 'approvalStatus', 'txRefNum', 'authorizationCode', + 'procStatus', + 'procStatusMessage', + 'respCodeMessage', ], 'hook' => sub { if ( !$gateway ) { @@ -38,7 +44,7 @@ my $gateway; # as the batch config, if there is one. If not, leave # gateway out entirely. my $merchant = (FS::Conf->new->config('batchconfig-paymentech'))[2]; - my $g = qsearchs({ + $gateway = qsearchs({ 'table' => 'payment_gateway', 'addl_from' => ' JOIN payment_gateway_option USING (gatewaynum) ', 'hashref' => { disabled => '', @@ -46,18 +52,19 @@ my $gateway; optionvalue => $merchant, }, }); - $gateway = ($g ? $g->gatewaynum . '-' : '') . 'PaymenTech'; } my ($hash, $oldhash) = @_; + $hash->{'gatewaynum'} = $gateway->gatewaynum if $gateway; + $hash->{'processor'} = 'PaymenTech'; my ($mon, $day, $year, $hour, $min, $sec) = $hash->{'_date'} =~ /^(..)(..)(....)(..)(..)(..)$/; $hash->{'_date'} = timelocal($sec, $min, $hour, $day, $mon-1, $year); $hash->{'paid'} = $oldhash->{'amount'}; - $hash->{'paybatch'} = join(':', - $gateway, - $hash->{'authorization'}, - $hash->{'order_number'}, - ); + if ( $hash->{'procStatus'} == 0 ) { + $hash->{'error_message'} = $hash->{'respCodeMessage'}; + } else { + $hash->{'error_message'} = $hash->{'procStatusMessage'}; + } }, 'approved' => sub { my $hash = shift; $hash->{'approvalStatus'} @@ -103,32 +110,32 @@ my %paymentech_countries = map { $_ => 1 } qw( US CA GB UK ); $xml->startTag('newOrder', BatchRequestNo => $count++); my $status = $_->cust_main->status; tie my %order, 'Tie::IxHash', ( - industryType => 'EC', - transType => 'AC', - bin => $bin, - merchantID => $merchantID, - terminalID => $terminalID, + industryType => 'EC', + transType => 'AC', + bin => $bin, + merchantID => $merchantID, + terminalID => $terminalID, ($_->payby eq 'CARD') ? ( - ccAccountNum => $_->payinfo, - ccExp => $_->expmmyy, + ccAccountNum => $_->payinfo, + ccExp => $_->expmmyy, ) : ( ecpCheckRT => ($_->payinfo =~ /@(\d+)/), ecpCheckDDA => ($_->payinfo =~ /(\d+)@/), ecpBankAcctType => $paytype{lc($_->cust_main->paytype)}, ecpDelvMethod => 'A', ), - avsZip => substr($_->zip, 0, 10), + avsZip => substr($_->zip, 0, 10), avsAddress1 => substr($_->address1, 0, 30), avsAddress2 => substr($_->address2, 0, 30), - avsCity => substr($_->city, 0, 20), - avsState => $_->state, - avsName => substr($_->first . ' ' . $_->last, 0, 30), - avsCountryCode => ( $paymentech_countries{ $_->country } - ? $_->country - : '' - ), - orderID => $_->paybatchnum, - amount => $_->amount * 100, + avsCity => substr($_->city, 0, 20), + avsState => substr($_->state, 0, 2), + avsName => substr($_->first. ' '. $_->last, 0, 30), + ( $paymentech_countries{ $_->country } + ? ( avsCountryCode => $_->country ) + : () + ), + orderID => $_->paybatchnum, + amount => $_->amount * 100, ); # only do this if recurringInd is enabled in config, # and the customer has at least one non-canceled recurring package diff --git a/FS/FS/payinfo_transaction_Mixin.pm b/FS/FS/payinfo_transaction_Mixin.pm index 093891e93..50659ac1e 100644 --- a/FS/FS/payinfo_transaction_Mixin.pm +++ b/FS/FS/payinfo_transaction_Mixin.pm @@ -73,10 +73,7 @@ sub _parse_paybatch { my $payment_gateway = qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } ); - die "payment gateway $gatewaynum not found" #? - unless $payment_gateway; - - $processor = $payment_gateway->gateway_module; + $processor = $payment_gateway->gateway_module if $payment_gateway; } diff --git a/FS/FS/prospect_main.pm b/FS/FS/prospect_main.pm index b5d51d333..a18c8ff67 100644 --- a/FS/FS/prospect_main.pm +++ b/FS/FS/prospect_main.pm @@ -2,7 +2,7 @@ package FS::prospect_main; use strict; use base qw( FS::Quotable_Mixin FS::o2m_Common FS::Record ); -use vars qw( $DEBUG ); +use vars qw( $DEBUG @location_fields ); use Scalar::Util qw( blessed ); use FS::Record qw( dbh qsearch qsearchs ); use FS::agent; @@ -12,6 +12,43 @@ use FS::qual; $DEBUG = 0; +#started as false laziness w/cust_main/Location.pm + +use Carp qw(carp); + +my $init = 0; +BEGIN { + # set up accessors for location fields + if (!$init) { + no strict 'refs'; + @location_fields = + qw( address1 address2 city county state zip country district + latitude longitude coord_auto censustract censusyear geocode + addr_clean ); + + foreach my $f (@location_fields) { + *{"FS::prospect_main::$f"} = sub { + carp "WARNING: tried to set cust_main.$f with accessor" if (@_ > 1); + my @cust_location = shift->cust_location or return ''; + #arbitrarily picking the first because the UI only lets you add one + $cust_location[0]->$f + }; + } + $init++; + } +} + +#debugging shim--probably a performance hit, so remove this at some point +sub get { + my $self = shift; + my $field = shift; + if ( $DEBUG and grep { $_ eq $field } @location_fields ) { + carp "WARNING: tried to get() location field $field"; + $self->$field; + } + $self->FS::Record::get($field); +} + =head1 NAME FS::prospect_main - Object methods for prospect_main records @@ -208,6 +245,12 @@ sub check { ; return $error if $error; + my $company = $self->company; + $company =~ s/^\s+//; + $company =~ s/\s+$//; + $company =~ s/\s+/ /g; + $self->company($company); + $self->SUPER::check; } diff --git a/FS/FS/quotation.pm b/FS/FS/quotation.pm index bf2711b0a..47f13e6dc 100644 --- a/FS/FS/quotation.pm +++ b/FS/FS/quotation.pm @@ -176,6 +176,36 @@ sub _total { } +#prevent things from falsely showing up as taxes, at least until we support +# quoting tax amounts.. +sub _items_tax { + return (); +} +sub _items_nontax { + shift->cust_bill_pkg; +} + +sub _items_total { + my( $self, $total_items ) = @_; + + if ( $self->total_setup > 0 ) { + push @$total_items, { + 'total_item' => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ), + 'total_amount' => $self->total_setup, + }; + } + + #could/should add up the different recurring frequencies on lines of their own + # but this will cover the 95% cases for now + if ( $self->total_recur > 0 ) { + push @$total_items, { + 'total_item' => $self->mt('Total Recurring'), + 'total_amount' => $self->total_recur, + }; + } + +} + =item enable_previous =cut diff --git a/FS/FS/quotation_pkg.pm b/FS/FS/quotation_pkg.pm index 3d40bb03a..efff9683f 100644 --- a/FS/FS/quotation_pkg.pm +++ b/FS/FS/quotation_pkg.pm @@ -1,10 +1,12 @@ package FS::quotation_pkg; use strict; -use base qw( FS::Record ); +use base qw( FS::TemplateItem_Mixin FS::Record ); use FS::Record qw( qsearchs ); #qsearch use FS::part_pkg; use FS::cust_location; +use FS::quotation; +use FS::quotation_pkg_discount; #so its loaded when TemplateItem_Mixin needs it =head1 NAME @@ -80,6 +82,14 @@ points to. You can ask the object for a copy with the I<hash> method. sub table { 'quotation_pkg'; } +sub display_table { 'quotation_pkg'; } + +#forget it, just overriding cust_bill_pkg_display entirely +#sub display_table_orderby { 'quotationpkgnum'; } # something else? +# # (for invoice display order) + +sub discount_table { 'quotation_pkg_discount'; } + =item insert Adds this record to the database. If there is an error, returns the error, @@ -107,8 +117,9 @@ sub check { my $error = $self->ut_numbern('quotationpkgnum') - || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart' ) - || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum' ) + || $self->ut_foreign_key( 'quotationnum', 'quotation', 'quotationnum' ) + || $self->ut_foreign_key( 'pkgpart', 'part_pkg', 'pkgpart' ) + || $self->ut_foreign_keyn( 'locationnum', 'cust_location', 'locationnum' ) || $self->ut_numbern('start_date') || $self->ut_numbern('contract_end') || $self->ut_numbern('quantity') @@ -131,7 +142,7 @@ sub desc { sub setup { my $self = shift; - return '0.00' if $self->waive_setup eq 'Y'; + return '0.00' if $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'}; my $part_pkg = $self->part_pkg; #my $setup = $part_pkg->can('base_setup') ? $part_pkg->base_setup # : $part_pkg->option('setup_fee'); @@ -144,6 +155,7 @@ sub setup { sub recur { my $self = shift; + return '0.00' if $self->{'_NO_RECUR_KLUDGE'}; my $part_pkg = $self->part_pkg; my $recur = $part_pkg->can('base_recur') ? $part_pkg->base_recur : $part_pkg->option('recur_fee'); @@ -152,6 +164,43 @@ sub recur { sprintf('%.2f', $recur); } +=item cust_bill_pkg_display [ type => TYPE ] + +=cut + +sub cust_bill_pkg_display { + my ( $self, %opt ) = @_; + + my $type = $opt{type} if exists $opt{type}; + return () if $type eq 'U'; #quotations don't have usage + + if ( $self->get('display') ) { + return ( grep { defined($type) ? ($type eq $_->type) : 1 } + @{ $self->get('display') } + ); + } else { + + #?? + my $setup = $self->new($self->hashref); + $setup->{'_NO_RECUR_KLUDGE'} = 1; + $setup->{'type'} = 'S'; + my $recur = $self->new($self->hashref); + $recur->{'_NO_SETUP_KLUDGE'} = 1; + $recur->{'type'} = 'R'; + + if ( $type eq 'S' ) { + return ($setup); + } elsif ( $type eq 'R' ) { + return ($recur); + } else { + #return ($setup, $recur); + return ($self); + } + + } + +} + =back =head1 BUGS diff --git a/FS/FS/rate.pm b/FS/FS/rate.pm index a2511cf99..49ac938fd 100644 --- a/FS/FS/rate.pm +++ b/FS/FS/rate.pm @@ -308,17 +308,28 @@ sub dest_detail { #find a rate prefix, first look at most specific, then fewer digits, # finally trying the country code only my $rate_prefix = ''; - for my $len ( reverse(1..10) ) { - $rate_prefix = qsearchs('rate_prefix', { + $rate_prefix = qsearchs({ + 'table' => 'rate_prefix', + 'addl_from' => ' JOIN rate_region USING (regionnum)', + 'hashref' => { + 'countrycode' => $countrycode, + 'npa' => $phonenum, + }, + 'extra_sql' => ' AND exact_match = \'Y\'' + }); + if (!$rate_prefix) { + for my $len ( reverse(1..10) ) { + $rate_prefix = qsearchs('rate_prefix', { + 'countrycode' => $countrycode, + #'npa' => { op=> 'LIKE', value=> substr($number, 0, $len) } + 'npa' => substr($phonenum, 0, $len), + } ) and last; + } + $rate_prefix ||= qsearchs('rate_prefix', { 'countrycode' => $countrycode, - #'npa' => { op=> 'LIKE', value=> substr($number, 0, $len) } - 'npa' => substr($phonenum, 0, $len), - } ) and last; + 'npa' => '', + }); } - $rate_prefix ||= qsearchs('rate_prefix', { - 'countrycode' => $countrycode, - 'npa' => '', - }); return '' unless $rate_prefix; diff --git a/FS/FS/rate_region.pm b/FS/FS/rate_region.pm index f4a0ab196..d42fdb41e 100644 --- a/FS/FS/rate_region.pm +++ b/FS/FS/rate_region.pm @@ -36,7 +36,10 @@ inherits from FS::Record. The following fields are currently supported: =item regionnum - primary key -=item regionname +=item regionname - name of the region + +=item exact_match - 'Y' if "prefixes" in this region really represent +complete phone numbers. Null if they represent prefixes (the usual case). =back @@ -233,6 +236,7 @@ sub check { my $error = $self->ut_numbern('regionnum') || $self->ut_text('regionname') + || $self->ut_flag('exact_match') ; return $error if $error; diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm index 7aede54a6..0aea4559b 100644 --- a/FS/FS/svc_Common.pm +++ b/FS/FS/svc_Common.pm @@ -43,27 +43,6 @@ inherit from, i.e. FS::svc_acct. FS::svc_Common inherits from FS::Record. =over 4 -=item search_sql_field FIELD STRING - -Class method which returns an SQL fragment to search for STRING in FIELD. - -It is now case-insensitive by default. - -=cut - -sub search_sql_field { - my( $class, $field, $string ) = @_; - my $table = $class->table; - my $q_string = dbh->quote($string); - "LOWER($table.$field) = LOWER($q_string)"; -} - -#fallback for services that don't provide a search... -sub search_sql { - #my( $class, $string ) = @_; - '1 = 0'; #false -} - =item new =cut @@ -863,13 +842,20 @@ sub set_auto_inventory { next if $columnflag eq 'A' && $self->$field() ne ''; my $classnum = $part_svc_column->columnvalue; - my %hash = ( 'classnum' => $classnum ); + my %hash; if ( $columnflag eq 'A' && $self->$field() eq '' ) { $hash{'svcnum'} = ''; } elsif ( $columnflag eq 'M' ) { return "Select inventory item for $field" unless $self->getfield($field); $hash{'item'} = $self->getfield($field); + my $chosen_classnum = $self->getfield($field.'_classnum'); + if ( grep {$_ == $chosen_classnum} split(',', $classnum) ) { + $classnum = $chosen_classnum; + } + # otherwise the chosen classnum is either (all), or somehow not on + # the list, so ignore it and choose the first item that's in any + # class on the list } my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql( @@ -880,18 +866,30 @@ sub set_auto_inventory { my $inventory_item = qsearchs({ 'table' => 'inventory_item', 'hashref' => \%hash, - 'extra_sql' => "AND $agentnums_sql", + 'extra_sql' => "AND classnum IN ($classnum) AND $agentnums_sql", 'order_by' => 'ORDER BY ( agentnum IS NULL ) '. #agent inventory first ' LIMIT 1 FOR UPDATE', }); unless ( $inventory_item ) { + # should really only be shown if columnflag eq 'A'... $dbh->rollback if $oldAutoCommit; - my $inventory_class = - qsearchs('inventory_class', { 'classnum' => $classnum } ); - return "Can't find inventory_class.classnum $classnum" - unless $inventory_class; - return "Out of ". PL_N($inventory_class->classname); + my $message = 'Out of '; + my @classnums = split(',', $classnum); + foreach ( @classnums ) { + my $class = FS::inventory_class->by_key($_) + or return "Can't find inventory_class.classnum $_"; + $message .= PL_N($class->classname); + if ( scalar(@classnums) > 2 ) { # english is hard + if ( $_ != $classnums[-1] ) { + $message .= ', '; + } + } + if ( scalar(@classnums) > 1 and $_ == $classnums[-2] ) { + $message .= 'and '; + } + } + return $message; } next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum; @@ -899,13 +897,14 @@ sub set_auto_inventory { $self->setfield( $field, $inventory_item->item ); #if $columnflag eq 'A' && $self->$field() eq ''; + # release the old inventory item, if there was one if ( $old && $old->$field() && $old->$field() ne $self->$field() ) { my $old_inv = qsearchs({ 'table' => 'inventory_item', - 'hashref' => { 'classnum' => $classnum, + 'hashref' => { 'svcnum' => $old->svcnum, }, - 'extra_sql' => ' AND '. + 'extra_sql' => "AND classnum IN ($classnum) AND ". '( ( svc_field IS NOT NULL AND svc_field = '.$dbh->quote($field).' )'. ' OR ( svc_field IS NULL AND item = '. dbh->quote($old->$field).' )'. ')', @@ -941,6 +940,9 @@ sub set_auto_inventory { =item return_inventory +Release all inventory items attached to this service's fields. Call +when unprovisioning the service. + =cut sub return_inventory { @@ -1082,17 +1084,22 @@ otherwise returns false. =cut -sub export_setstatus { - my( $self, @args ) = @_; - my $error = $self->export('setstatus', @args); +sub export_setstatus { shift->_export_setstatus_X('setstatus', @_) } +sub export_setstatus_listadd { shift->_export_setstatus_X('setstatus_listadd', @_) } +sub export_setstatus_listdel { shift->_export_setstatus_X('setstatus_listdel', @_) } +sub export_setstatus_vacationadd { shift->_export_setstatus_X('setstatus_vacationadd', @_) } +sub export_setstatus_vacationdel { shift->_export_setstatus_X('setstatus_vacationdel', @_) } + +sub _export_setstatus_X { + my( $self, $method, @args ) = @_; + my $error = $self->export($method, @args); if ( $error ) { - warn "error running export_setstatus: $error"; + warn "error running export_$method: $error"; return $error; } ''; } - =item export HOOK [ EXPORT_ARGS ] Runs the provided export hook (i.e. "suspend", "unsuspend") for this service. @@ -1277,6 +1284,221 @@ sub nms_ip_delete { #XXX not yet implemented } +=item search_sql_field FIELD STRING + +Class method which returns an SQL fragment to search for STRING in FIELD. + +It is now case-insensitive by default. + +=cut + +sub search_sql_field { + my( $class, $field, $string ) = @_; + my $table = $class->table; + my $q_string = dbh->quote($string); + "LOWER($table.$field) = LOWER($q_string)"; +} + +#fallback for services that don't provide a search... +sub search_sql { + #my( $class, $string ) = @_; + '1 = 0'; #false +} + +=item search HASHREF + +Class method which returns a qsearch hash expression to search for parameters +specified in HASHREF. + +Parameters: + +=over 4 + +=item unlinked - set to search for all unlinked services. Overrides all other options. + +=item agentnum + +=item custnum + +=item svcpart + +=item ip_addr + +=item pkgpart - arrayref + +=item routernum - arrayref + +=item sectornum - arrayref + +=item towernum - arrayref + +=item order_by + +=back + +=cut + +# svc_broadband::search should eventually use this instead +sub search { + my ($class, $params) = @_; + + my @from = ( + 'LEFT JOIN cust_svc USING ( svcnum )', + 'LEFT JOIN part_svc USING ( svcpart )', + 'LEFT JOIN cust_pkg USING ( pkgnum )', + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'), + ); + + my @where = (); + + $class->_search_svc($params, \@from, \@where) if $class->can('_search_svc'); + +# # domain +# if ( $params->{'domain'} ) { +# my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } ); +# #preserve previous behavior & bubble up an error if $svc_domain not found? +# push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain; +# } +# +# # domsvc +# if ( $params->{'domsvc'} =~ /^(\d+)$/ ) { +# push @where, "domsvc = $1"; +# } + + #unlinked + push @where, 'pkgnum IS NULL' if $params->{'unlinked'}; + + #agentnum + if ( $params->{'agentnum'} =~ /^(\d+)$/ && $1 ) { + push @where, "cust_main.agentnum = $1"; + } + + #custnum + if ( $params->{'custnum'} =~ /^(\d+)$/ && $1 ) { + push @where, "custnum = $1"; + } + + #customer status + if ( $params->{'cust_status'} =~ /^([a-z]+)$/ ) { + push @where, FS::cust_main->cust_status_sql . " = '$1'"; + } + + #customer balance + if ( $params->{'balance'} =~ /^\s*(\-?\d*(\.\d{1,2})?)\s*$/ && length($1) ) { + my $balance = $1; + + my $age = ''; + if ( $params->{'balance_days'} =~ /^\s*(\d*(\.\d{1,3})?)\s*$/ && length($1) ) { + $age = time - 86400 * $1; + } + push @where, FS::cust_main->balance_date_sql($age) . " > $balance"; + } + + #payby + if ( $params->{'payby'} && scalar(@{ $params->{'payby'} }) ) { + my @payby = map "'$_'", grep /^(\w+)$/, @{ $params->{'payby'} }; + push @where, 'payby IN ('. join(',', @payby ). ')'; + } + + #pkgpart + ##pkgpart, now properly untainted, can be arrayref + #for my $pkgpart ( $params->{'pkgpart'} ) { + # if ( ref $pkgpart ) { + # my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart ); + # push @where, "cust_pkg.pkgpart IN ($where)" if $where; + # } + # elsif ( $pkgpart =~ /^(\d+)$/ ) { + # push @where, "cust_pkg.pkgpart = $1"; + # } + #} + if ( $params->{'pkgpart'} ) { + my @pkgpart = ref( $params->{'pkgpart'} ) + ? @{ $params->{'pkgpart'} } + : $params->{'pkgpart'} + ? ( $params->{'pkgpart'} ) + : (); + @pkgpart = grep /^(\d+)$/, @pkgpart; + push @where, 'cust_pkg.pkgpart IN ('. join(',', @pkgpart ). ')' if @pkgpart; + } + + #svcnum + if ( $params->{'svcnum'} =~ /^(\d+)$/ ) { + push @where, "svcnum = $1"; + } + + # svcpart + if ( $params->{'svcpart'} ) { + my @svcpart = ref( $params->{'svcpart'} ) + ? @{ $params->{'svcpart'} } + : $params->{'svcpart'} + ? ( $params->{'svcpart'} ) + : (); + @svcpart = grep /^(\d+)$/, @svcpart; + push @where, 'svcpart IN ('. join(',', @svcpart ). ')' if @svcpart; + } + + if ( $params->{'exportnum'} =~ /^(\d+)$/ ) { + push @from, ' LEFT JOIN export_svc USING ( svcpart )'; + push @where, "exportnum = $1"; + } + +# # sector and tower +# my @where_sector = $class->tower_sector_sql($params); +# if ( @where_sector ) { +# push @where, @where_sector; +# push @from, ' LEFT JOIN tower_sector USING ( sectornum )'; +# } + + # here is the agent virtualization + #if ($params->{CurrentUser}) { + # my $access_user = + # qsearchs('access_user', { username => $params->{CurrentUser} }); + # + # if ($access_user) { + # push @where, $access_user->agentnums_sql('table'=>'cust_main'); + # }else{ + # push @where, "1=0"; + # } + #} else { + push @where, $FS::CurrentUser::CurrentUser->agentnums_sql( + 'table' => 'cust_main', + 'null_right' => 'View/link unlinked services', + ); + #} + + push @where, @{ $params->{'where'} } if $params->{'where'}; + + my $addl_from = join(' ', @from); + my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : ''; + + my $table = $class->table; + + my $count_query = "SELECT COUNT(*) FROM $table $addl_from $extra_sql"; + #if ( keys %svc_X ) { + # $count_query .= ' WHERE '. + # join(' AND ', map "$_ = ". dbh->quote($svc_X{$_}), + # keys %svc_X + # ); + #} + + { + 'table' => $table, + 'hashref' => {}, + 'select' => join(', ', + "$table.*", + 'part_svc.svc', + 'cust_main.custnum', + @{ $params->{'addl_select'} || [] }, + FS::UI::Web::cust_sql_fields($params->{'cust_fields'}), + ), + 'addl_from' => $addl_from, + 'extra_sql' => $extra_sql, + 'order_by' => $params->{'order_by'}, + 'count_query' => $count_query, + }; + +} + =back =head1 BUGS diff --git a/FS/FS/svc_Tower_Mixin.pm b/FS/FS/svc_Tower_Mixin.pm index 6adbc6f5e..3da07c1cd 100644 --- a/FS/FS/svc_Tower_Mixin.pm +++ b/FS/FS/svc_Tower_Mixin.pm @@ -27,12 +27,10 @@ towernum or sectornum can also contain 'none' to allow null values. =cut sub tower_sector_sql { - my $class = shift; - my $params = shift; - return '' unless keys %$params; - my $where = ''; + my( $class, $params ) = @_; + return () unless keys %$params; - my @where; + my @where = (); for my $field (qw(towernum sectornum)) { my $value = $params->{$field} or next; if ( ref $value and grep { $_ } @$value ) { diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm index 8e71d829d..26d6e5b72 100644 --- a/FS/FS/svc_acct.pm +++ b/FS/FS/svc_acct.pm @@ -15,6 +15,7 @@ use vars qw( $DEBUG $me $conf $skip_fuzzyfiles $username_noperiod $username_nounderscore $username_nodash $username_uppercase $username_percent $username_colon $username_slash $username_equals $username_pound + $username_exclamation $password_noampersand $password_noexclamation $warning_template $warning_from $warning_subject $warning_mimetype $warning_cc @@ -85,6 +86,7 @@ FS::UID->install_callback( sub { $username_slash = $conf->exists('username-slash'); $username_equals = $conf->exists('username-equals'); $username_pound = $conf->exists('username-pound'); + $username_exclamation = $conf->exists('username-exclamation'); $password_noampersand = $conf->exists('password-noexclamation'); $password_noexclamation = $conf->exists('password-noexclamation'); $dirhash = $conf->config('dirhash') || 0; @@ -1193,7 +1195,7 @@ sub check { my $ulen = $usernamemax || $self->dbdef_table->column('username')->length; - $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:\/\=\#]{$usernamemin,$ulen})$/i + $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:\/\=\#\!]{$usernamemin,$ulen})$/i or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username}; $recref->{username} = $1; @@ -1234,6 +1236,9 @@ sub check { unless ( $username_pound ) { $recref->{username} =~ /\#/ and return $uerror; } + unless ( $username_exclamation ) { + $recref->{username} =~ /\!/ and return $uerror; + } $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum}; @@ -1890,12 +1895,14 @@ sub email { $self->username. '@'. $self->domain(@_); } + =item acct_snarf Returns an array of FS::acct_snarf records associated with the account. =cut +# unused as originally intended, but now by Communigate Pro "RPOP" sub acct_snarf { my $self = shift; qsearch({ @@ -2817,116 +2824,39 @@ Arrayref of additional WHERE clauses, will be ANDed together. =cut -sub search { - my ($class, $params) = @_; - - my @from = ( - ' LEFT JOIN cust_svc USING ( svcnum ) ', - ' LEFT JOIN part_svc USING ( svcpart ) ', - ' LEFT JOIN cust_pkg USING ( pkgnum ) ', - ' LEFT JOIN cust_main USING ( custnum ) ', - ); +sub _search_svc { + my( $class, $params, $from, $where ) = @_; - my @where = (); + #these two should probably move to svc_Domain_Mixin ? # domain if ( $params->{'domain'} ) { my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } ); #preserve previous behavior & bubble up an error if $svc_domain not found? - push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain; + push @$where, 'domsvc = '. $svc_domain->svcnum if $svc_domain; } # domsvc if ( $params->{'domsvc'} =~ /^(\d+)$/ ) { - push @where, "domsvc = $1"; + push @$where, "domsvc = $1"; } - #unlinked - push @where, 'pkgnum IS NULL' if $params->{'unlinked'}; - - #agentnum - if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) { - push @where, "cust_main.agentnum = $1"; - } - - #custnum - if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) { - push @where, "custnum = $1"; - } - - #pkgpart - if ( $params->{'pkgpart'} && scalar(@{ $params->{'pkgpart'} }) ) { - #XXX untaint or sql quote - push @where, - 'cust_pkg.pkgpart IN ('. join(',', @{ $params->{'pkgpart'} } ). ')'; - } # popnum if ( $params->{'popnum'} =~ /^(\d+)$/ ) { - push @where, "popnum = $1"; + push @$where, "popnum = $1"; } - # svcpart - if ( $params->{'svcpart'} =~ /^(\d+)$/ ) { - push @where, "svcpart = $1"; - } - if ( $params->{'exportnum'} =~ /^(\d+)$/ ) { - push @from, ' LEFT JOIN export_svc USING ( svcpart )'; - push @where, "exportnum = $1"; - } + #and these in svc_Tower_Mixin, or maybe we never should have done svc_acct + # towers (or, as mark thought, never should have done svc_broadband) # sector and tower my @where_sector = $class->tower_sector_sql($params); if ( @where_sector ) { - push @where, @where_sector; - push @from, ' LEFT JOIN tower_sector USING ( sectornum )'; - } - - # here is the agent virtualization - #if ($params->{CurrentUser}) { - # my $access_user = - # qsearchs('access_user', { username => $params->{CurrentUser} }); - # - # if ($access_user) { - # push @where, $access_user->agentnums_sql('table'=>'cust_main'); - # }else{ - # push @where, "1=0"; - # } - #} else { - push @where, $FS::CurrentUser::CurrentUser->agentnums_sql( - 'table' => 'cust_main', - 'null_right' => 'View/link unlinked services', - ); - #} - - push @where, @{ $params->{'where'} } if $params->{'where'}; - - my $addl_from = join(' ', @from); - my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : ''; - - my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql"; - #if ( keys %svc_acct ) { - # $count_query .= ' WHERE '. - # join(' AND ', map "$_ = ". dbh->quote($svc_acct{$_}), - # keys %svc_acct - # ); - #} - - my $sql_query = { - 'table' => 'svc_acct', - 'hashref' => {}, # \%svc_acct, - 'select' => join(', ', - 'svc_acct.*', - 'part_svc.svc', - 'cust_main.custnum', - FS::UI::Web::cust_sql_fields($params->{'cust_fields'}), - ), - 'addl_from' => $addl_from, - 'extra_sql' => $extra_sql, - 'order_by' => $params->{'order_by'}, - 'count_query' => $count_query, - }; + push @$where, @where_sector; + push @$from, ' LEFT JOIN tower_sector USING ( sectornum )'; + } } diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm index af8135304..002aa55ce 100755 --- a/FS/FS/svc_broadband.pm +++ b/FS/FS/svc_broadband.pm @@ -103,10 +103,10 @@ sub table_info { 'ip_field' => 'ip_addr', 'fields' => { 'svcnum' => 'Service', - 'description' => 'Descriptive label for this particular device', - 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.', - 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.', - 'ip_addr' => 'IP address. Leave blank for automatic assignment.', + 'description' => 'Descriptive label', + 'speed_down' => 'Download speed (Kbps)', + 'speed_up' => 'Upload speed (Kbps)', + 'ip_addr' => 'IP address', 'blocknum' => { 'label' => 'Address block', 'type' => 'select', @@ -134,6 +134,15 @@ sub table_info { disable_inventory => 1, multiple => 1, }, + 'radio_serialnum' => 'Radio Serial Number', + 'radio_location' => 'Radio Location', + 'poe_location' => 'POE Location', + 'rssi' => 'RSSI', + 'suid' => 'SUID', + 'shared_svcnum' => { label => 'Shared Service', + type => 'search-svc_broadband', + disable_inventory => 1, + }, }, }; } @@ -175,115 +184,44 @@ Parameters: =cut -sub search { - my ($class, $params) = @_; - my @where = (); - my @from = ( - 'LEFT JOIN cust_svc USING ( svcnum )', - 'LEFT JOIN part_svc USING ( svcpart )', - 'LEFT JOIN cust_pkg USING ( pkgnum )', - 'LEFT JOIN cust_main USING ( custnum )', - ); - - # based on FS::svc_acct::search, probably the most mature of the bunch - #unlinked - push @where, 'pkgnum IS NULL' if $params->{'unlinked'}; - - #agentnum - if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) { - push @where, "cust_main.agentnum = $1"; - } - push @where, $FS::CurrentUser::CurrentUser->agentnums_sql( - 'null_right' => 'View/link unlinked services', - 'table' => 'cust_main' - ); - - #custnum - if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) { - push @where, "custnum = $1"; - } - - #pkgpart, now properly untainted, can be arrayref - for my $pkgpart ( $params->{'pkgpart'} ) { - if ( ref $pkgpart ) { - my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart ); - push @where, "cust_pkg.pkgpart IN ($where)" if $where; - } - elsif ( $pkgpart =~ /^(\d+)$/ ) { - push @where, "cust_pkg.pkgpart = $1"; - } - } +sub _search_svc { + my( $class, $params, $from, $where ) = @_; #routernum, can be arrayref for my $routernum ( $params->{'routernum'} ) { # this no longer uses addr_block if ( ref $routernum and grep { $_ } @$routernum ) { my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum ); - my @orwhere; + my @orwhere = (); push @orwhere, "svc_broadband.routernum IN ($in)" if $in; push @orwhere, "svc_broadband.routernum IS NULL" if grep /^none$/, @$routernum; - push @where, '( '.join(' OR ', @orwhere).' )'; + push @$where, '( '.join(' OR ', @orwhere).' )'; } elsif ( $routernum =~ /^(\d+)$/ ) { - push @where, "svc_broadband.routernum = $1"; + push @$where, "svc_broadband.routernum = $1"; } elsif ( $routernum eq 'none' ) { - push @where, "svc_broadband.routernum IS NULL"; + push @$where, "svc_broadband.routernum IS NULL"; } } + #this should probably move to svc_Tower_Mixin, or maybe we never should have + # done svc_acct # towers (or, as mark thought, never should have done + # svc_broadband) + #sector and tower, as above my @where_sector = $class->tower_sector_sql($params); if ( @where_sector ) { - push @where, @where_sector; - push @from, 'LEFT JOIN tower_sector USING ( sectornum )'; + push @$where, @where_sector; + push @$from, 'LEFT JOIN tower_sector USING ( sectornum )'; } - #svcnum - if ( $params->{'svcnum'} =~ /^(\d+)$/ ) { - push @where, "svcnum = $1"; - } - - #svcpart - if ( $params->{'svcpart'} =~ /^(\d+)$/ ) { - push @where, "svcpart = $1"; - } - - #exportnum - if ( $params->{'exportnum'} =~ /^(\d+)$/ ) { - push @from, 'LEFT JOIN export_svc USING ( svcpart )'; - push @where, "exportnum = $1"; - } - #ip_addr if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) { - push @where, "ip_addr = '$1'"; + push @$where, "ip_addr = '$1'"; } - #custnum - if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) { - push @where, "custnum = $1"; - } - - my $addl_from = join(' ', @from); - my $extra_sql = ''; - $extra_sql = 'WHERE '.join(' AND ', @where) if @where; - my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql"; - return( { - 'table' => 'svc_broadband', - 'hashref' => {}, - 'select' => join(', ', - 'svc_broadband.*', - 'part_svc.svc', - 'cust_main.custnum', - FS::UI::Web::cust_sql_fields($params->{'cust_fields'}), - ), - 'extra_sql' => $extra_sql, - 'addl_from' => $addl_from, - 'order_by' => "ORDER BY ".($params->{'order_by'} || 'svcnum'), - 'count_query' => $count_query, - } ); } =item search_sql STRING @@ -296,15 +234,31 @@ sub search_sql { my( $class, $string ) = @_; if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) { $class->search_sql_field('ip_addr', $string ); - }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) { + } elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) { $class->search_sql_field('mac_addr', uc($string)); - }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) { + } elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) { $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") ); + } elsif ( $string =~ /^(\d+)$/ ) { + my $table = $class->table; + "$table.svcnum = $1"; } else { '1 = 0'; #false } } +=item smart_search STRING + +=cut + +sub smart_search { + my( $class, $string ) = @_; + qsearch({ + 'table' => $class->table, #'svc_broadband', + 'hashref' => {}, + 'extra_sql' => 'WHERE '. $class->search_sql($string), + }); +} + =item label Returns the IP address. @@ -313,7 +267,12 @@ Returns the IP address. sub label { my $self = shift; - $self->ip_addr; + my $label = 'IP:'. ($self->ip_addr || 'Unknown'); + $label .= ', MAC:'. $self->mac_addr + if $self->mac_addr; + $label .= ' ('. $self->description. ')' + if $self->description; + return $label; } =item insert [ , OPTION => VALUE ... ] @@ -377,7 +336,7 @@ sub check { # remove delimiters my $mac_addr = uc($self->get('mac_addr')); - $mac_addr =~ s/[-: ]//g; + $mac_addr =~ s/[\W_]//g; $self->set('mac_addr', $mac_addr); my $error = @@ -396,6 +355,12 @@ sub check { || $self->ut_sfloatn('altitude') || $self->ut_textn('vlan_profile') || $self->ut_textn('plan_id') + || $self->ut_alphan('radio_serialnum') + || $self->ut_textn('radio_location') + || $self->ut_textn('poe_location') + || $self->ut_snumbern('rssi') + || $self->ut_numbern('suid') + || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum') ; return $error if $error; diff --git a/FS/FS/svc_export_machine.pm b/FS/FS/svc_export_machine.pm index 10f7b6821..7ca20ccb6 100644 --- a/FS/FS/svc_export_machine.pm +++ b/FS/FS/svc_export_machine.pm @@ -40,6 +40,10 @@ fields are currently supported: primary key +=item exportnum + +Export definition, see L<FS::part_export> + =item svcnum Customer service, see L<FS::cust_svc> diff --git a/FS/FS/svc_hardware.pm b/FS/FS/svc_hardware.pm index af6865f12..b28cc9ef5 100644 --- a/FS/FS/svc_hardware.pm +++ b/FS/FS/svc_hardware.pm @@ -105,9 +105,13 @@ sub search_sql { my ($class, $string) = @_; my @where = (); - my $ip = NetAddr::IP->new($string); - if ( $ip ) { - push @where, $class->search_sql_field('ip_addr', $ip->addr); + if ( $string =~ /^[\d\.:]+$/ ) { + # if the string isn't an IP address, this will waste several seconds + # attempting a DNS lookup. so try to filter those out. + my $ip = NetAddr::IP->new($string); + if ( $ip ) { + push @where, $class->search_sql_field('ip_addr', $ip->addr); + } } if ( $string =~ /^(\w+)$/ ) { @@ -164,7 +168,7 @@ sub check { return $x unless ref $x; my $hw_addr = $self->getfield('hw_addr'); - $hw_addr = join('', split(/\W/, $hw_addr)); + $hw_addr = join('', split(/[_\W]/, $hw_addr)); if ( $conf->exists('svc_hardware-check_mac_addr') ) { $hw_addr = uc($hw_addr); $hw_addr =~ /^[0-9A-F]{12}$/ diff --git a/FS/FS/svc_pbx.pm b/FS/FS/svc_pbx.pm index 4182a1315..66e51da71 100644 --- a/FS/FS/svc_pbx.pm +++ b/FS/FS/svc_pbx.pm @@ -292,7 +292,9 @@ to allow title to indicate a range of IP addresses. =item begin, end: Start and end of date range, as unix timestamp. -=item cdrtypenum: Only return CDRs with this type number. +=item cdrtypenum: Only return CDRs with this type. + +=item calltypenum: Only return CDRs with this call type. =back @@ -310,6 +312,9 @@ sub psearch_cdrs { if ($options{'cdrtypenum'}) { $hash{'cdrtypenum'} = $options{'cdrtypenum'}; } + if ($options{'calltypenum'}) { + $hash{'calltypenum'} = $options{'calltypenum'}; + } my $for_update = $options{'for_update'} ? 'FOR UPDATE' : ''; diff --git a/FS/FS/svc_phone.pm b/FS/FS/svc_phone.pm index 1296c1e85..3cc1adc66 100644 --- a/FS/FS/svc_phone.pm +++ b/FS/FS/svc_phone.pm @@ -23,10 +23,11 @@ $DEBUG = 0; @pw_set = ( 'a'..'k', 'm','n', 'p-z', 'A'..'N', 'P'..'Z' , '2'..'9' ); #ask FS::UID to run this stuff for us later -$FS::UID::callback{'FS::svc_acct'} = sub { +FS::UID->install_callback( sub { $conf = new FS::Conf; $phone_name_max = $conf->config('svc_phone-phone_name-max_length'); -}; +} +); =head1 NAME @@ -68,6 +69,10 @@ primary key =item phonenum +=item sim_imsi + +SIM IMSI (http://en.wikipedia.org/wiki/International_mobile_subscriber_identity) + =item sip_password =item pin @@ -147,6 +152,7 @@ sub table_info { disable_select => 1, }, 'phonenum' => 'Phone number', + 'sim_imsi' => 'IMSI', #http://en.wikipedia.org/wiki/International_mobile_subscriber_identity 'pin' => { label => 'Voicemail PIN', #'Personal Identification Number', type => 'text', disable_inventory => 1, @@ -466,6 +472,7 @@ sub check { $self->ut_numbern('svcnum') || $self->ut_numbern('countrycode') || $self->$phonenum_check_method('phonenum') + || $self->ut_numbern('sim_imsi') || $self->ut_anything('sip_password') || $self->ut_numbern('pin') || $self->ut_textn('phone_name') @@ -486,6 +493,10 @@ sub check { ; return $error if $error; + return 'Illegal IMSI (not 14-15 digits)' #shorter? + if length($self->sim_imsi) + && ( length($self->sim_imsi) < 14 || length($self->sim_imsi) > 15 ); + # LNP data validation return 'Cannot set LNP fields: no LNP in progress' if ( ($self->lnp_desired_due_date || $self->lnp_due_date @@ -673,10 +684,14 @@ with the chosen prefix. =item begin, end: Start and end of a date range, as unix timestamp. -=item cdrtypenum: Only return CDRs with this type number. +=item cdrtypenum: Only return CDRs with this type. + +=item calltypenum: Only return CDRs with this call type. =item disable_src => 1: Only match on "charged_party", not "src". +=item nonzero: Only return CDRs where duration > 0. + =item by_svcnum: not supported for svc_phone =item billsec_sum: Instead of returning all of the CDRs, return a single @@ -722,6 +737,9 @@ sub psearch_cdrs { if ($options{'cdrtypenum'}) { $hash{'cdrtypenum'} = $options{'cdrtypenum'}; } + if ($options{'calltypenum'}) { + $hash{'calltypenum'} = $options{'calltypenum'}; + } my $for_update = $options{'for_update'} ? 'FOR UPDATE' : ''; @@ -744,6 +762,9 @@ sub psearch_cdrs { if ( $options{'end'} ) { push @where, 'startdate < '. $options{'end'}; } + if ( $options{'nonzero'} ) { + push @where, 'duration > 0'; + } my $extra_sql = ( keys(%hash) ? ' AND ' : ' WHERE ' ). join(' AND ', @where ); @@ -770,6 +791,30 @@ sub get_cdrs { qsearch ( $psearch->{query} ) } +=item sum_cdrs + +Takes the same options as psearch_cdrs, but returns a single row containing +"count" (the number of CDRs) and the sums of the following fields: duration, +billsec, rated_price, rated_seconds, rated_minutes. + +Note that if any calls are not rated, their rated_* fields will be null. +If you want to use those fields, pass the 'status' option to limit to +calls that have been rated. This is intentional; please don't "fix" it. + +=cut + +sub sum_cdrs { + my $self = shift; + my $psearch = $self->psearch_cdrs(@_); + $psearch->{query}->{'select'} = join(',', + 'COUNT(*) AS count', + map { "SUM($_) AS $_" } + qw(duration billsec rated_price rated_seconds rated_minutes) + ); + # hack + $psearch->{query}->{'extra_sql'} =~ s/ ORDER BY.*$//; + qsearchs ( $psearch->{query} ); +} =back diff --git a/FS/MANIFEST b/FS/MANIFEST index f954fe8dd..94232905e 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -13,6 +13,7 @@ bin/freeside-deloutsource bin/freeside-deloutsourceuser bin/freeside-deluser bin/freeside-email +bin/freeside-phonenum_list bin/freeside-queued bin/freeside-radgroup bin/freeside-reexport @@ -33,7 +34,6 @@ FS/ClientAPI_SessionCache.pm FS/ClientAPI_XMLRPC.pm FS/ClientAPI/passwd.pm FS/ClientAPI/Agent.pm -FS/ClientAPI/Bulk.pm FS/ClientAPI/MasonComponent.pm FS/ClientAPI/MyAccount.pm FS/ClientAPI/PrepaidPhone.pm @@ -74,7 +74,6 @@ FS/cust_main/Billing_Realtime.pm FS/cust_main/Import.pm FS/cust_main/Packages.pm FS/cust_main/Search.pm -FS/cust_main/_Marketgear.pm FS/cust_main_Mixin.pm FS/cust_main_county.pm FS/cust_main_invoice.pm @@ -149,8 +148,6 @@ FS/part_pkg/sqlradacct_hour.pm FS/part_pkg/subscription.pm FS/part_pkg/voip_sqlradacct.pm FS/part_pkg/voip_cdr.pm -FS/part_pkg/base_rate.pm -FS/part_pkg/base_delayed.pm FS/part_pop_local.pm FS/part_referral.pm FS/part_svc.pm @@ -493,6 +490,8 @@ FS/phone_type.pm t/phone_type.t FS/contact_email.pm t/contact_email.t +FS/contact_Mixin.pm +t/contact_Mixin.t FS/prospect_main.pm t/prospect_main.t FS/o2m_Common.pm @@ -679,3 +678,15 @@ FS/log.pm t/log.t FS/log_context.pm t/log_context.t +FS/part_pkg_usage_class.pm +t/part_pkg_usage_class.t +FS/cust_pkg_usage.pm +t/cust_pkg_usage.t +FS/part_pkg_usage_class.pm +t/part_pkg_usage_class.t +FS/part_pkg_usage.pm +t/part_pkg_usage.t +FS/cdr_cust_pkg_usage.pm +t/cdr_cust_pkg_usage.t +FS/part_pkg_msgcat.pm +t/part_pkg_msgcat.t diff --git a/FS/bin/freeside-cdr-sftp_and_import b/FS/bin/freeside-cdr-sftp_and_import index 7f2693fcb..c37ff11d6 100755 --- a/FS/bin/freeside-cdr-sftp_and_import +++ b/FS/bin/freeside-cdr-sftp_and_import @@ -218,6 +218,8 @@ or FTP and then import them into the database. -c: cdrtypenum to set, defaults to none +-g: File is gzipped + user: freeside username format: CDR format name diff --git a/FS/bin/freeside-cdrrated b/FS/bin/freeside-cdrrated index 131b56a7e..99ea67594 100644 --- a/FS/bin/freeside-cdrrated +++ b/FS/bin/freeside-cdrrated @@ -33,9 +33,11 @@ if ( @cdrtypenums ) { $extra_sql .= ' AND cdrtypenum IN ('. join(',', @cdrtypenums ). ')'; } -our %svcnum = (); -our %pkgpart = (); -our %part_pkg = (); +our %svcnum = (); # phonenum => svcnum +our %pkgnum = (); # phonenum => pkgnum +our %cust_pkg = (); # pkgnum => cust_pkg (NOT phonenum => cust_pkg!) +our %pkgpart = (); # phonenum => pkgpart +our %part_pkg = (); # phonenum => part_pkg #some false laziness w/freeside-cdrrewrited @@ -91,6 +93,9 @@ while (1) { next; } + $pkgnum{$number} = $cust_pkg->pkgnum; + $cust_pkg{$cust_pkg->pkgnum} ||= $cust_pkg; + #get the package, search through the part_pkg and linked for a voip_cdr def w/matching cdrtypenum (or no use_cdrtypenum) my @part_pkg = grep { $_->plan eq 'voip_cdr' @@ -126,10 +131,11 @@ while (1) { #} #XXX if $part_pkg->option('min_included') then we can't prerate this CDR - + my $error = $cdr->rate( 'part_pkg' => $part_pkg{ $pkgpart{$number} }, - 'svcnum' => $svcnum{ $number }, + 'cust_pkg' => $cust_pkg{ $pkgnum{$number} }, + 'svcnum' => $svcnum{$number}, ); if ( $error ) { #XXX ??? diff --git a/FS/bin/freeside-cdrrewrited b/FS/bin/freeside-cdrrewrited index f2c3926fb..16f931fbf 100644 --- a/FS/bin/freeside-cdrrewrited +++ b/FS/bin/freeside-cdrrewrited @@ -30,9 +30,9 @@ die "not running; cdr-asterisk_forward_rewrite, cdr-charged_party_rewrite ". #-- -my %accountcode_unmatch = (); -my $accountcode_retry = 4 * 60 * 60; # 4 hours -my $accountcode_giveup = 4 * 24 * 60 * 60; # 4 days +my %sessionnum_unmatch = (); +my $sessionnum_retry = 4 * 60 * 60; # 4 hours +my $sessionnum_giveup = 4 * 24 * 60 * 60; # 4 days my %cdr_type = map { lc($_->cdrtypename) => $_->cdrtypenum } qsearch('cdr_type',{}); @@ -45,8 +45,8 @@ while (1) { # instead of just doing this search like normal CDRs #hmm :/ - my @recent = grep { ($accountcode_unmatch{$_} + $accountcode_retry) > time } - keys %accountcode_unmatch; + my @recent = grep { ($sessionnum_unmatch{$_} + $sessionnum_retry) > time } + keys %sessionnum_unmatch; my $extra_sql = scalar(@recent) ? ' AND acctid NOT IN ('. join(',', @recent). ') ' : ''; @@ -136,45 +136,62 @@ while (1) { } - if ( $conf->exists('cdr-taqua-accountcode_rewrite') - && $cdr->lastapp eq 'acctcode' && $cdr->cdrtypenum == 1 + if ( $cdr->cdrtypenum == 1 + and $cdr->lastapp + and ( + $conf->exists('cdr-taqua-accountcode_rewrite') or + $conf->exists('cdr-taqua-callerid_rewrite') ) ) { #find the matching CDR - my $primary = qsearchs('cdr', { - 'sessionnum' => $cdr->sessionnum, - 'src' => $cdr->subscriber, - #'accountcode' => '', - }); + my %search = ( 'sessionnum' => $cdr->sessionnum ); + if ( $cdr->lastapp eq 'acctcode' ) { + $search{'src'} = $cdr->subscriber; + } elsif ( $cdr->lastapp eq 'CallerId' ) { + $search{'dst'} = $cdr->subscriber; + } + my $primary = qsearchs('cdr', \%search); unless ( $primary ) { my $cantfind = "can't find primary CDR with session ". $cdr->sessionnum. ", src ". $cdr->subscriber; - if ( $cdr->calldate_unix + $accountcode_giveup < time ) { + if ( $cdr->calldate_unix + $sessionnum_giveup < time ) { warn "ERROR: $cantfind; giving up\n"; - push @status, 'taqua-accountcode-NOTFOUND'; + push @status, 'taqua-sessionnum-NOTFOUND'; $cdr->status('done'); #so it doesn't try to rate - delete $accountcode_unmatch{$cdr->acctid}; #so it doesn't suck mem + delete $sessionnum_unmatch{$cdr->acctid}; #so it doesn't suck mem } else { warn "WARNING: $cantfind; will keep trying\n"; - $accountcode_unmatch{$cdr->acctid} = time; + $sessionnum_unmatch{$cdr->acctid} = time; next; } } else { - $primary->accountcode( $cdr->lastdata ); + if ( $cdr->lastapp eq 'acctcode' ) { + # lastdata contains the dialed account code + $primary->accountcode( $cdr->lastdata ); + push @status, 'taqua-accountcode'; + } elsif ( $cdr->lastapp eq 'CallerId' ) { + # lastdata contains "allowed" or "restricted" + # or case variants thereof + if ( lc($cdr->lastdata) eq 'restricted' ) { + $primary->clid( 'PRIVATE' ); + } + push @status, 'taqua-callerid'; + } else { + warn "unknown Taqua service name: ".$cdr->lastapp."\n"; + } #$primary->freesiderewritestatus( 'taqua-accountcode-primary' ); - my $error = $primary->replace; + my $error = $primary->replace if $primary->modified; if ( $error ) { warn "WARNING: error rewriting primary CDR (will retry): $error\n"; next; } $skip{$primary->acctid} = 1; - push @status, 'taqua-accountcode'; $cdr->status('done'); #so it doesn't try to rate } @@ -214,7 +231,10 @@ sub _shouldrun { $conf->exists('cdr-asterisk_forward_rewrite') || $conf->exists('cdr-asterisk_australia_rewrite') || $conf->exists('cdr-charged_party_rewrite') - || $conf->exists('cdr-taqua-accountcode_rewrite'); + || $conf->exists('cdr-taqua-accountcode_rewrite') + || $conf->exists('cdr-taqua-callerid_rewrite') + || 0 + ; } sub usage { diff --git a/FS/bin/freeside-ipifony-download b/FS/bin/freeside-ipifony-download index 837cc3329..9df4db08a 100644 --- a/FS/bin/freeside-ipifony-download +++ b/FS/bin/freeside-ipifony-download @@ -9,6 +9,7 @@ use FS::UID qw(adminsuidsetup); use FS::Record qw(qsearch qsearchs); use FS::cust_main; use FS::Conf; +use File::Copy qw(copy); use Text::CSV; my %opt; @@ -116,7 +117,7 @@ die "failed to connect to '$sftpuser\@$host'\n(".$sftp->error.")\n" $sftp->setcwd($path) if $path; -my $files = $sftp->ls('.', wanted => qr/\.csv$/, names_only => 1); +my $files = $sftp->ls('ready', wanted => qr/\.csv$/, names_only => 1); if (!@$files) { print STDERR "No charge files found.\n" if $opt{v}; exit(-1); @@ -129,23 +130,23 @@ my %is_e911 = map {$_ => 1} @E911_CODES; FILE: foreach my $filename (@$files) { print STDERR "Retrieving $filename\n" if $opt{v}; - $sftp->get("$filename", "$tmpdir/$filename"); + $sftp->get("ready/$filename", "$tmpdir/$filename"); if($sftp->error) { warn "failed to download $filename\n"; next FILE; } # make sure server archive dir exists - if ( !$sftp->stat('Archive') ) { - print STDERR "Creating $path/Archive\n" if $opt{v}; - $sftp->mkdir('Archive'); + if ( !$sftp->stat('done') ) { + print STDERR "Creating $path/done\n" if $opt{v}; + $sftp->mkdir('done'); if($sftp->error) { # something is seriously wrong die "failed to create archive directory on server:\n".$sftp->error."\n"; } } #move to server archive dir - $sftp->rename("$filename", "Archive/$filename"); + $sftp->rename("ready/$filename", "done/$filename"); if($sftp->error) { warn "failed to archive $filename on server:\n".$sftp->error."\n"; } # process it anyway, I guess/ @@ -159,11 +160,6 @@ FILE: foreach my $filename (@$files) { } open my $fh, "<$tmpdir/$filename"; - my $header = <$fh>; - if ($header !~ /^"cust_id"/) { - warn "warning: $filename has incorrect header row:\n$header\n"; - # but try anyway - } my $csv = Text::CSV->new; # orthodox CSV my %hash; while (my $line = <$fh>) { @@ -172,6 +168,11 @@ FILE: foreach my $filename (@$files) { next FILE; }; @hash{@fields} = $csv->fields(); + if ( $hash{custnum} =~ /^cust/ ) { + # there appears to be a header row + print STDERR "skipping header row\n" if $opt{v}; + next; + } my $cust_main = $cust_main{$hash{custnum}} ||= FS::cust_main->by_key($hash{custnum}); if (!$cust_main) { @@ -184,10 +185,11 @@ FILE: foreach my $filename (@$files) { my $amount = sprintf('%.2f',$hash{quantity} * $hash{unit_price}); # construct arguments for $cust_main->charge my %charge_opt = ( - amount => $amount, + amount => $hash{unit_price}, quantity => $hash{quantity}, start_date => $cust_main->next_bill_date, - pkg => $hash{date_desc}, + pkg => $hash{date_desc} . + ' (' . $hash{quantity} . ' @ $' . $hash{unit_price} . ' ea)', taxclass => $TAXCLASSES{ $hash{taxclass} }, ); if (my $classname = $hash{classname}) { @@ -221,7 +223,7 @@ FILE: foreach my $filename (@$files) { $num_errors++; } else { $num_charges++; - $sum_charges += $hash{amount}; + $sum_charges += $amount; } if ( $opt{e} and $is_e911{$hash{classname}} ) { diff --git a/FS/bin/freeside-phonenum_list b/FS/bin/freeside-phonenum_list new file mode 100755 index 000000000..19b564dee --- /dev/null +++ b/FS/bin/freeside-phonenum_list @@ -0,0 +1,86 @@ +#!/usr/bin/perl + +use strict; +use vars qw( $opt_c $opt_o $opt_l $opt_p $opt_b $opt_d $opt_s $opt_t ); +use Getopt::Std; +use FS::UID qw(adminsuidsetup); +use FS::Conf; +use FS::Record qw(qsearch); +use FS::svc_phone; + +getopts('colp:b:d:s:t:'); + +my $user = shift or &usage; +adminsuidsetup $user; + +my $conf = new FS::Conf; +my $default_locale = $conf->config('locale') || 'en_US'; + +my %search = (); + +$search{payby} = [ split(/\s*,\s*/, $opt_p) ] if $opt_p; +$search{balance} = $opt_b if $opt_b; +$search{balance_days} = $opt_d if $opt_d; +$search{svcpart} = [ split(/\s*,\s*/, $opt_s) ] if $opt_s; +$search{cust_status} = lc($opt_t) if $opt_t; + +my @svc_phone = qsearch( FS::svc_phone->search(\%search) ); + +foreach my $svc_phone (@svc_phone) { + print $svc_phone->countrycode if $opt_c; + print $svc_phone->phonenum; + print '@'. $svc_phone->domain if $opt_o; + if ( $opt_l ) { + my $cust_pkg = $svc_phone->cust_svc->cust_pkg; + print ','. ($cust_pkg && $cust_pkg->cust_main->locale || $default_locale); + } + print "\n"; +} + +sub usage { + die "usage: freeside-phonenum_list [ -c ] [ -o ] [ -l ] [ -p payby,payby... ] [ -b balance [ -d balance_days ] ] [ -s svcpart,svcpart... ] username \n"; +} + +=head1 NAME + +freeside-phonenum_list + +=head1 SYNOPSIS + freeside-phonenum_list [ -c ] [ -o ] [ -l ] [ -p payby,payby... ] [ -b balance [ -d balance_days ] ] [ -s svcpart,svcpart... ] username + +=head1 DESCRIPTION + +Command-line tool to list phone numbers. + +Display options: + +-c: Include country code + +-o: Include domain + +-l: Include customer locale + +Selection options: + +-p: Customer payby (CARD, BILL, etc.). Separate multiple values with commas. + +-b: Customer balance over (or equal to) this amount + +-d: Customer balance age over this many days + +-s: Service definition (svcpart). Separate multiple values with commas. + +-t: Customer status: prospect, active, ordered, inactive, suspended or cancelled + +username: Employee username + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::svc_phone>, L<FS::cust_main> + +=cut + +1; + diff --git a/FS/bin/freeside-queued b/FS/bin/freeside-queued index 2fd80255e..dcc6ac4ba 100644 --- a/FS/bin/freeside-queued +++ b/FS/bin/freeside-queued @@ -212,8 +212,10 @@ while (1) { # don't put @args in the log, may expose passwords $log->info('starting job ('.$ljob->job.')'); warn 'running "&'. $ljob->job. '('. join(', ', @args). ")\n" if $DEBUG; + local $FS::UID::AutoCommit = 0; # so that we can clean up failures eval $eval; #throw away return value? suppose so if ( $@ ) { + dbh->rollback; my %hash = $ljob->hash; $hash{'statustext'} = $@; if ( $hash{'statustext'} =~ /\/misc\/queued_report/ ) { #use return? @@ -225,8 +227,10 @@ while (1) { my $fjob = new FS::queue( \%hash ); my $error = $fjob->replace($ljob); die $error if $error; + dbh->commit; # for the status change only } else { $ljob->delete; + dbh->commit; # for the job itself } if ( UNIVERSAL::can(dbh, 'sprintProfile') ) { diff --git a/FS/bin/freeside-selfservice-server b/FS/bin/freeside-selfservice-server index c10623c96..9df313fec 100644 --- a/FS/bin/freeside-selfservice-server +++ b/FS/bin/freeside-selfservice-server @@ -108,31 +108,7 @@ while (1) { if ( $keepalives && $keepalive_count++ > 10 ) { $keepalive_count = 0; lock_write; - nstore_fd( { _token => '_keepalive' }, $writer ); - -#commenting izoom stuff out until we can move it to a branch (or just remove) -# foreach my $agent ( qsearch( 'agent', { disabled => '' } ) ) { -# my $config = qsearchs( 'conf', { name => 'selfservice-bulk_ftp_dir', -# agentnum => $agent->agentnum, -# } ) -# or next; -# -# my $session = -# FS::ClientAPI->dispatch( 'Agent/agent_login', -# { username => $agent->username, -# password => $agent->_password, -# } -# ); -# -# nstore_fd( { _token => '_ftp_scan', -# dir => $config->value, -# session_id => $session->{session_id}, -# }, -# $writer -# ); -# } - unlock_write; } next; diff --git a/FS/bin/freeside-upgrade b/FS/bin/freeside-upgrade index b08a8401f..3d1c2e072 100755 --- a/FS/bin/freeside-upgrade +++ b/FS/bin/freeside-upgrade @@ -123,6 +123,8 @@ my $cf; while ( $cf = $cfsth->fetchrow_hashref ) { my $tbl = $cf->{'dbtable'}; my $name = $cf->{'name'}; + $name = lc($name) unless driver_name =~ /^mysql/i; + @statements = grep { $_ !~ /^\s*ALTER\s+TABLE\s+(h_|)$tbl\s+DROP\s+COLUMN\s+cf_$name\s*$/i } @statements; push @statements, diff --git a/FS/bin/freeside-username_list b/FS/bin/freeside-username_list new file mode 100755 index 000000000..5352f02eb --- /dev/null +++ b/FS/bin/freeside-username_list @@ -0,0 +1,84 @@ +#!/usr/bin/perl + +use strict; +use vars qw( $opt_o $opt_l $opt_p $opt_b $opt_d $opt_s $opt_t ); +use Getopt::Std; +use FS::UID qw(adminsuidsetup); +use FS::Conf; +use FS::Record qw(qsearch); +use FS::svc_acct; + +getopts('olp:b:d:s:t:'); + +my $user = shift or &usage; +adminsuidsetup $user; + +my $conf = new FS::Conf; +my $default_locale = $conf->config('locale') || 'en_US'; + +my %search = (); + +$search{payby} = [ split(/\s*,\s*/, $opt_p) ] if $opt_p; +$search{balance} = $opt_b if $opt_b; +$search{balance_days} = $opt_d if $opt_d; +$search{svcpart} = [ split(/\s*,\s*/, $opt_s) ] if $opt_s; +$search{cust_status} = lc($opt_t) if $opt_t; + +my @svc_acct = qsearch( FS::svc_acct->search(\%search) ); + +foreach my $svc_acct (@svc_acct) { + print $svc_acct->username; + print '@'. $svc_acct->domain if $opt_o; + if ( $opt_l ) { + my $cust_pkg = $svc_acct->cust_svc->cust_pkg; + print ','. ($cust_pkg && $cust_pkg->cust_main->locale || $default_locale); + } + print "\n"; +} + +sub usage { + die "usage: freeside-username_list [ -c ] [ -l ] [ -p payby,payby... ] [ -b balance [ -d balance_days ] ] [ -s svcpart,svcpart... ] username \n"; +} + +=head1 NAME + +freeside-username_list + +=head1 SYNOPSIS + + freeside-username_list [ -c ] [ -l ] [ -p payby,payby... ] [ -b balance [ -d balance_days ] ] [ -s svcpart,svcpart... ] username + +=head1 DESCRIPTION + +Command-line tool to list usernames. + +Display options: + +-o: Include domain + +-l: Include customer locale + +Selection options: + +-p: Customer payby (CARD, BILL, etc.). Separate multiple values with commas. + +-b: Customer balance over (or equal to) this amount + +-d: Customer balance age over this many days + +-s: Service definition (svcpart). Separate multiple values with commas. + +-t: Customer status: prospect, active, ordered, inactive, suspended or cancelled + +username: Employee username + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::svc_acct>, L<FS::cust_main> + +=cut + +1; + diff --git a/FS/bin/freeside-wkhtmltopdf b/FS/bin/freeside-wkhtmltopdf index c6c5531a5..f0c53e6da 100755 --- a/FS/bin/freeside-wkhtmltopdf +++ b/FS/bin/freeside-wkhtmltopdf @@ -1,7 +1,7 @@ #!/bin/sh -if [ $DISPLAY ] ; then - wkhtmltopdf $@ -else +#if [ $DISPLAY ] ; then +# wkhtmltopdf $@ +#else xvfb-run -- wkhtmltopdf $@ -fi +#fi diff --git a/FS/t/cdr_cust_pkg_usage.t b/FS/t/cdr_cust_pkg_usage.t new file mode 100644 index 000000000..1e2060e96 --- /dev/null +++ b/FS/t/cdr_cust_pkg_usage.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cdr_cust_pkg_usage; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/contact_Mixin.t b/FS/t/contact_Mixin.t new file mode 100644 index 000000000..89dcc37c5 --- /dev/null +++ b/FS/t/contact_Mixin.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::contact_Mixin; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/cust_pkg_usage.t b/FS/t/cust_pkg_usage.t new file mode 100644 index 000000000..23a7b299e --- /dev/null +++ b/FS/t/cust_pkg_usage.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_pkg_usage; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/part_pkg_msgcat.t b/FS/t/part_pkg_msgcat.t new file mode 100644 index 000000000..541c16799 --- /dev/null +++ b/FS/t/part_pkg_msgcat.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_pkg_msgcat; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/part_pkg_usage.t b/FS/t/part_pkg_usage.t new file mode 100644 index 000000000..ba5ccb6c8 --- /dev/null +++ b/FS/t/part_pkg_usage.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_pkg_usage; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/part_pkg_usage_class.t b/FS/t/part_pkg_usage_class.t new file mode 100644 index 000000000..e46ff0648 --- /dev/null +++ b/FS/t/part_pkg_usage_class.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_pkg_usage_class; +$loaded=1; +print "ok 1\n"; diff --git a/INSTALL b/INSTALL deleted file mode 100644 index 4ea167893..000000000 --- a/INSTALL +++ /dev/null @@ -1,3 +0,0 @@ -See: - -http://www.freeside.biz/mediawiki/index.php/Freeside:1.7:Documentation#Installation_and_upgrades @@ -164,11 +164,15 @@ wikiman: chmod a+rx ./bin/pod2x ./bin/pod2x -install-docs: check-conflicts docs - [ -e ${FREESIDE_DOCUMENT_ROOT} ] && mv ${FREESIDE_DOCUMENT_ROOT} ${FREESIDE_DOCUMENT_ROOT}.`date +%Y%m%d%H%M%S` || true - cp -r masondocs ${FREESIDE_DOCUMENT_ROOT} +install-docs: docs + #ancient attempt to avoid overwriting customer modifications directly to production web files that's overlived its usefulness + #[ -e ${FREESIDE_DOCUMENT_ROOT} ] && mv ${FREESIDE_DOCUMENT_ROOT} ${FREESIDE_DOCUMENT_ROOT}.`date +%Y%m%d%H%M%S` || true + #cp -r masondocs ${FREESIDE_DOCUMENT_ROOT} + [ -h ${FREESIDE_DOCUMENT_ROOT} ] && rm ${FREESIDE_DOCUMENT_ROOT} || true + mkdir -p ${FREESIDE_DOCUMENT_ROOT} + cp -r masondocs/* masondocs/.htaccess ${FREESIDE_DOCUMENT_ROOT} chown -R freeside:freeside ${FREESIDE_DOCUMENT_ROOT} - cp htetc/handler.pl ${MASON_HANDLER} + install -D htetc/handler.pl ${MASON_HANDLER} perl -p -i -e "\ s|%%%FREESIDE_EXPORT%%%|${FREESIDE_EXPORT}|g;\ s'%%%RT_ENABLED%%%'${RT_ENABLED}'g; \ @@ -225,7 +229,7 @@ perl-modules: s|%%%DIST_CONF%%%|${DIST_CONF}|g;\ " blib/script/* -install-perl-modules: check-conflicts perl-modules install-rt-initialdata +install-perl-modules: perl-modules install-rt-initialdata [ -L ${PERL_INC_DEV_KLUDGE}/FS ] \ && rm ${PERL_INC_DEV_KLUDGE}/FS \ && mv ${PERL_INC_DEV_KLUDGE}/FS.old ${PERL_INC_DEV_KLUDGE}/FS \ @@ -372,7 +376,7 @@ create-rt: configure-rt --datafile ${RT_PATH}/etc/initialdata \ || true -install-rt: check-conflicts +install-rt: if [ ${RT_ENABLED} -eq 1 ]; then ( cd rt; make install ); fi if [ ${RT_ENABLED} -eq 1 ]; then perl -p -i -e "\ s'%%%RT_DOMAIN%%%'${RT_DOMAIN}'g;\ @@ -412,9 +416,6 @@ clean: -cd fs_selfservice/FS-SelfService; \ make clean -check-conflicts: - ! grep -r --exclude='*config.log*' '--exclude=*config.status*' --exclude=gnupg_details_on_output_formats '--exclude=*mason_handler*' '^=======$$' . - #these are probably only useful if you're me... #release: upload-docs diff --git a/bin/23diff b/bin/23diff index d38c84834..1dc1659d2 100755 --- a/bin/23diff +++ b/bin/23diff @@ -7,7 +7,7 @@ $dir =~ s/freeside(\/?)/freeside2.3$1/; warn $dir; #$cmd = "diff -u $file $dir/$file"; -$cmd = "diff -u $dir/$file $file"; +$cmd = "diff -ubBw $dir/$file $file"; print "$cmd\n"; system($cmd); diff --git a/bin/cdr-netsapiens.import b/bin/cdr-netsapiens.import index 8aa4ac0b7..1cce461e2 100755 --- a/bin/cdr-netsapiens.import +++ b/bin/cdr-netsapiens.import @@ -37,6 +37,7 @@ do { my $ns = $part_export->ns_command( 'GET', '/cdr/', 'time_release' => "$time_release,", '_sort' => '+time_release', + '_limit' => '500', ); #loop over them, double check duplicates, insert the rest diff --git a/bin/cust_main-bulk_change b/bin/cust_main-bulk_change index fdf53d999..32a6d7bd6 100755 --- a/bin/cust_main-bulk_change +++ b/bin/cust_main-bulk_change @@ -1,13 +1,15 @@ #!/usr/bin/perl use strict; -use vars qw( $opt_p ); +use vars qw( $opt_a $opt_p $opt_t $opt_k ); use Getopt::Std; use FS::UID qw(adminsuidsetup); -use FS::Record qw(qsearchs); +use FS::Record qw(qsearch qsearchs); use FS::cust_main; +use FS::cust_tag; +use FS::cust_pkg; -getopts('p:'); +getopts('a:p:t:k:'); my $user = shift or &usage; adminsuidsetup $user; @@ -31,17 +33,41 @@ while (<STDIN>) { next; } - if ( $opt_p ) { - $cust_main->payby($opt_p); + my %cust_tag = ( custnum=>$custnum, tagnum=>$opt_t ); + if ( $opt_t && ! qsearchs('cust_tag', \%cust_tag) ) { + my $cust_tag = new FS::cust_tag \%cust_tag; + my $error = $cust_tag->insert; + die "$error\n" if $error; } - my $error = $cust_main->replace; - die "$error\n" if $error; + if ( $opt_p || $opt_a ) { + $cust_main->agentnum($opt_a) if $opt_a; + $cust_main->payby($opt_p) if $opt_p; + + my $error = $cust_main->replace; + die "$error\n" if $error; + } + + if ( $opt_k ) { + foreach my $k (split(/\s*,\s*/, $opt_k)) { + my($old, $new) = split(/\s*:\s*/, $k); + foreach my $cust_pkg ( qsearch('cust_pkg', { + 'custnum' => $cust_main->custnum, + 'pkgpart' => $old, + }) + ) + { + $cust_pkg->pkgpart($new); + my $error = $cust_pkg->replace; + die "$error\n" if $error; + } + } + } } sub usage { - die "usage: cust_main-bulk_change -p NEW_PAYBY employee_username <custnums.txt\n"; + die "usage: cust_main-bulk_change [ -a agentnum ] [ -p NEW_PAYBY ] [ -t tagnum ] [ -k old_pkgpart:new_pkgpart,... ] employee_username <custnums.txt\n"; } =head1 NAME @@ -50,13 +76,19 @@ cust_main-bulk_change =head1 SYNOPSIS - cust_main-bulk_change -p NEW_PAYBY username <custnums.txt + cust_main-bulk_change [ -a agentnum ] [ -p NEW_PAYBY ] [ -t tagnum ] [ -k old_pkgpart:new_pkgpart,... ] username <custnums.txt =head1 DESCRIPTION -Command-line tool to change the payby field for a group of customers. +Command-line tool to make bulk changes to a group of customers. + +-a: new agentnum + +-p: new payby, for example, I<CARD> or I<DCRD> + +-t: tagnum to add if not present --p: new payby, for example, I<CARD> or I<DCRD>. +-k: old_pkgpart:new_pkgpart, for example, I<5:4>. Multiple entries can be comma-separated. user: Employee username diff --git a/bin/fs-migrate-supplemental b/bin/fs-migrate-supplemental new file mode 100755 index 000000000..dbef95fc1 --- /dev/null +++ b/bin/fs-migrate-supplemental @@ -0,0 +1,151 @@ +#!/usr/bin/perl + +use strict; +use FS::UID qw(adminsuidsetup); +use FS::Record qw(qsearch qsearchs); +use FS::cust_pkg; +use FS::part_pkg; + +my $user = shift or die &usage; +my @pkgparts = @ARGV or die &usage; +my $dbh = adminsuidsetup $user; + +$FS::UID::AutoCommit = 0; + +my %stats = ( + mainpkgs => 0, + created => 0, + linked => 0, + errors => 0, +); + +my %pkg_freq; # cache +foreach my $pkgpart (@pkgparts) { + my $part_pkg = FS::part_pkg->by_key($pkgpart) + or die "pkgpart $pkgpart not found.\n"; + $pkg_freq{$pkgpart} = $part_pkg->freq; + my @links = $part_pkg->supp_part_pkg_link + or die "pkgpart $pkgpart has no supplemental packages.\n"; + CUST_PKG: foreach my $cust_pkg ( + qsearch('cust_pkg', { + 'pkgpart' => $pkgpart, + 'cancel' => '', + }) + ) { + my $cust_main = $cust_pkg->cust_main; + my @existing = $cust_pkg->supplemental_pkgs; + my @active = grep { !$_->main_pkgnum } $cust_main->ncancelled_pkgs; + LINK: foreach my $link (@links) { + # yeah, it's expensive + # see if there's an existing package with this link identity + foreach (@existing) { + if ($_->pkglinknum == $link->pkglinknum) { + next LINK; + } + } + # no? then is there one with this pkgpart? + my $i = 0; + foreach (@active) { + if ( $_->pkgpart == $link->dst_pkgpart ) { + set_link($cust_pkg, $link, $_); + splice(@active, $i, 1); # delete it so we don't reuse it + next LINK; + } + } + # no? then create one + create_linked($cust_pkg, $link); + } #foreach $link + $stats{mainpkgs}++; + } #foreach $cust_pkg +} #foreach $pkgpart + +print " +Main packages: $stats{mainpkgs} +Supplemental packages linked: $stats{linked} +Supplemental packages ordered: $stats{created} +Errors: $stats{errors} +"; + +$dbh->commit or die $dbh->errstr; + +sub set_link { + my ($main_pkg, $part_pkg_link, $supp_pkg) = @_; + my $task = "linking package ".$supp_pkg->pkgnum. + " to package ".$main_pkg->pkgnum; + $supp_pkg->set('main_pkgnum', $main_pkg->pkgnum); + $supp_pkg->set('pkglinknum', $part_pkg_link->pkglinknum); + # Set the next bill date of the supplemental package to the nearest one in + # the future that lines up with the main package. If the main package + # hasn't started billing yet, use its future start date. + my $new_bill = $main_pkg->get('bill') || $main_pkg->get('start_date'); + if ( $new_bill ) { + my $old_bill = $supp_pkg->get('bill'); + my $diff = $new_bill - $old_bill; + my $main_freq = $pkg_freq{$main_pkg->pkgpart}; + my $prev_bill = 0; + while ($diff < 0) { + # this will exit once $new_bill has overtaken the existing bill date. + # if there is no existing bill date, then this will exit right away + # and set bill to the bill date of the main package, which is correct. + $prev_bill = $new_bill; + $new_bill = FS::part_pkg->add_freq($new_bill, $main_freq); + $diff = $new_bill - $old_bill; + } + # then, of $new_bill and $prev_bill, pick the one that's closer to $old_bill + if ( $prev_bill > 0 and + $new_bill - $old_bill > $old_bill - $prev_bill ) { + $supp_pkg->set('bill', $prev_bill); + } else { + $supp_pkg->set('bill', $new_bill); + } + } else { + # otherwise the main package hasn't been billed yet and has no + # start date, so we can't sync the supplemental to it yet. + # but we can still link them. + warn "$task: main package has no next bill date.\n"; + } + my $error = $supp_pkg->replace; + if ( $error ) { + warn "$task:\n $error\n"; + $stats{errors}++; + } else { + $stats{linked}++; + } + return; +} + +sub create_linked { + my ($main_pkg, $part_pkg_link) = @_; + my $task = "creating pkgpart ".$part_pkg_link->dst_pkgpart. + " supplemental to package ".$main_pkg->pkgnum; + my $supp_pkg = FS::cust_pkg->new({ + 'pkgpart' => $part_pkg_link->dst_pkgpart, + 'pkglinknum' => $part_pkg_link->pkglinknum, + 'custnum' => $main_pkg->custnum, + 'main_pkgnum' => $main_pkg->pkgnum, + 'locationnum' => $main_pkg->locationnum, + 'start_date' => $main_pkg->start_date, + 'order_date' => $main_pkg->order_date, + 'expire' => $main_pkg->expire, + 'adjourn' => $main_pkg->adjourn, + 'contract_end' => $main_pkg->contract_end, + 'susp' => $main_pkg->susp, + 'bill' => $main_pkg->bill, + 'refnum' => $main_pkg->refnum, + 'discountnum' => $main_pkg->discountnum, + 'waive_setup' => $main_pkg->waive_setup, + }); + my $error = $supp_pkg->insert; + if ( $error ) { + warn "$task:\n $error\n"; + $stats{errors}++; + } else { + $stats{created}++; + } + return; +} + +sub usage { + die "Usage:\n fs-migrate-supplemental user main_pkgpart\n"; +} + diff --git a/etc/megapop.pl b/bin/megapop.pl index e2930fb55..e2930fb55 100755 --- a/etc/megapop.pl +++ b/bin/megapop.pl diff --git a/conf/invoice_html b/conf/invoice_html index 567385b06..cd348274f 100644 --- a/conf/invoice_html +++ b/conf/invoice_html @@ -132,8 +132,8 @@ $OUT .= '<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>'; } @@ -158,8 +158,8 @@ ( $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>' : '' ). diff --git a/conf/invoice_htmlsummary b/conf/invoice_htmlsummary index a06c8ffea..a6ea1e9e3 100644 --- a/conf/invoice_htmlsummary +++ b/conf/invoice_htmlsummary @@ -34,7 +34,16 @@ <%= my ($last) = grep { $_->{tax_section} || !$_->{summarized} and !($finance_section && $_->{'description'} eq $finance_section) and $_->{'description'} !~ /^\d+ $/ } reverse @sections; - foreach my $section ( grep { $_->{tax_section} || !$_->{summarized} and !($finance_section && $_->{'description'} eq $finance_section) and $_->{'description'} !~ /^\d+ $/ } @sections ) { + #false laziness w/invoice_latexsummary + foreach my $section ( + grep { + $_->{tax_section} || !$_->{summarized} + and ! $_->{adjust_section} + and !($finance_section && $_->{'description'} eq $finance_section) + and $_->{'description'} !~ /^\d+ $/ + } + @sections + ) { $OUT .= '<tr><td><b>'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '</b></td>'; my $celltype = ($last == $section) ? 'th' : 'td'; $OUT .= qq(<$celltype align="right"><b>). $section->{'subtotal'}. "</b></$celltype></tr>"; @@ -63,9 +72,19 @@ <td><b>New Charges</b></td> <th align="right"><b><%= $dollar.$current_less_finance %></b></th> </tr> + + <%= + + #false laziness w/invoice_latexsummary and above + foreach my $section ( grep $_->{adjust_section}, @sections) { + $OUT .= '<tr><td><b>'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '</b></td>'; + $OUT .= qq(<th align="right"><b>). $section->{'subtotal'}. "</b></th></tr>"; + } + %> + <tr> <td><b>Total Amount Due</b></td> - <td align="right"><b><%= $dollar.sprintf('%.2f', $true_previous_balance + $current_charges - $balance_adjustments) %></b></td> + <td align="right"><b><%= $dollar.sprintf('%.2f', $balance) %></b></td> </tr> <tr><th colspan=2><br></th></tr> </table> diff --git a/conf/invoice_latex b/conf/invoice_latex index d56a7fbdc..533e8340d 100644 --- a/conf/invoice_latex +++ b/conf/invoice_latex @@ -164,8 +164,9 @@ \newcommand{\FSdescriptionlength} { [@-- $unitprices ? '8.2cm' : '12.8cm' --@] }
\newcommand{\FSdescriptioncolumncount} { [@-- $unitprices ? '4' : '6' --@] }
\newcommand{\FSunitcolumns}{ [@--
- $unitprices
- ? '\makebox[2.5cm][l]{\textbf{~~'.emt('Unit Price').'}}&\makebox[1.4cm]{\textbf{~'.emt('Quantity').'}}&'
+ $unitprices
+ ? '\makebox[2.5cm][r]{\textbf{~~' . emt('Unit Price') . '}} &' .
+ '\makebox[1.4cm]{\textbf{~' . emt('Quantity') . '}} & '
: '' --@] }
\newcommand{\FShead}{
@@ -182,7 +183,7 @@ \newcommand{\FSdesc}[5]{
\multicolumn{1}{c}{\rule{0pt}{2.5ex}\textbf{#1}} &
\multicolumn{[@-- $unitprices ? '4' : '6' --@]}{l}{\textbf{#2}} &
-[@-- $unitprices ? ' \multicolumn{1}{l}{\textbf{#3}} &'."\n".
+[@-- $unitprices ? ' \multicolumn{1}{r}{\textbf{\dollar #3}} &'."\n".
' \multicolumn{1}{r}{\textbf{#4}} &'."\n"
: ''
--@]
diff --git a/conf/invoice_latexsummary b/conf/invoice_latexsummary index 4e4f62bf8..a68e5d381 100644 --- a/conf/invoice_latexsummary +++ b/conf/invoice_latexsummary @@ -21,7 +21,16 @@ \textbf{\underline{Summary of New Charges}} & \\ &\\ [@-- - foreach my $section ( grep { $_->{tax_section} || !$_->{summarized} and !($finance_section && $_->{'description'} eq $finance_section) and $_->{'description'} !~ /^\d+ $/ } @sections ) { + #false laziness w/invoice_htmlsummary + foreach my $section ( + grep { + $_->{tax_section} || !$_->{summarized} + and ! $_->{adjust_section} + and !($finance_section && $_->{'description'} eq $finance_section) + and $_->{'description'} !~ /^\d+ $/ + } + @sections + ) { $OUT .= '\textbf{'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '}'; $OUT .= '&\textbf{'. $section->{'subtotal'}. '}\\\\'; } @@ -36,8 +45,17 @@ \textbf{Previous Past Due Charges}&\textbf{\dollar[@-- sprintf('%.2f', $true_previous_balance - $balance_adjustments) --@]}\\ \textbf{Finance charges on overdue amount}&\textbf{\dollar[@-- $finance_amount --@]}\\ \textbf{New Charges}&\textbf{\dollar[@-- $current_less_finance --@]}\\ + +[@-- + #false laziness w/invoice_htmlsummary and above + foreach my $section ( grep $_->{adjust_section}, @sections ) { + $OUT .= '\textbf{'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '}'; + $OUT .= '&\textbf{'. $section->{'subtotal'}. '}\\\\'; + } +--@] + \cline{2-2} -\textbf{Total Amount Due}&\textbf{\dollar[@-- sprintf('%.2f', $true_previous_balance + $current_charges - $balance_adjustments) --@]}\\ +\textbf{Total Amount Due}&\textbf{\dollar[@-- sprintf('%.2f', $balance) --@]}\\ &\\ \hline \end{tabular} diff --git a/debian/config b/debian/OLD/config index 4ffa236f3..4ffa236f3 100644 --- a/debian/config +++ b/debian/OLD/config diff --git a/debian/cron.d b/debian/OLD/cron.d index f86db1b76..f86db1b76 100644 --- a/debian/cron.d +++ b/debian/OLD/cron.d diff --git a/debian/dbconfig-common.install b/debian/OLD/dbconfig-common.install index 31b5d1439..31b5d1439 100644 --- a/debian/dbconfig-common.install +++ b/debian/OLD/dbconfig-common.install diff --git a/debian/dbconfig-common.upgrade b/debian/OLD/dbconfig-common.upgrade index cae9adbfe..cae9adbfe 100644 --- a/debian/dbconfig-common.upgrade +++ b/debian/OLD/dbconfig-common.upgrade diff --git a/debian/freeside.apache-alias.conf b/debian/OLD/freeside.apache-alias.conf index fdd4340e9..fdd4340e9 100644 --- a/debian/freeside.apache-alias.conf +++ b/debian/OLD/freeside.apache-alias.conf diff --git a/debian/postinst b/debian/OLD/postinst index 5d045508a..5d045508a 100644 --- a/debian/postinst +++ b/debian/OLD/postinst diff --git a/debian/postrm b/debian/OLD/postrm index c00844543..c00844543 100644 --- a/debian/postrm +++ b/debian/OLD/postrm diff --git a/debian/prerm b/debian/OLD/prerm index 4c1748936..4c1748936 100644 --- a/debian/prerm +++ b/debian/OLD/prerm diff --git a/debian/TODO b/debian/TODO index 15fed6914..d2928e629 100644 --- a/debian/TODO +++ b/debian/TODO @@ -1,20 +1,57 @@ +--- High --- + +web stuff going to /var/www/freeside/masondocs oops + +apache configs going to +./etc/freeside/apache2/freeside-rt.conf +?? oh there's links etc. check + +file +./and..?/ +in freeside-lib? oops wtf +also +./default_conf/ +and +/#for/ + +test actually installing! +- FS files +- /var/www/ files +- what else should package install? + - init script + - apache config + - /usr/local/etc/freeside/default_conf for new installs + +test RT was missing, but we're cheating more now by ignoring a huge remap +to deb policy-comliant paths. get it working + +init.d/freeside-init +htetc/handler.pl + +#copied to /usr/local/etc/freeside by make install-docs +htetc/htpasswd.logout + +init.d/insserv-override-apache2 + +etc/longtable.sty + +--- Medium --- test) freeside-webui /etc/apache/conf.d/freeside.conf AuthUserFile is wrong (just fucked) -test its working) somes sort of Alias /freeside /usr/share/freeside/www is needed - test in postinst) freeside package var/cache/freeside/cache.<datasrc is missing> -test RT is missing. doh. get it working. - -test actually installing! +--- Low --- ---- rc2... right? --- +bin/* ? Anything here needed in a live customer install should be moved to FS/bin so it installs as part of the packaging. freeside-selfservice-client doesn't install at all -start freeside-sqlradius-radacctd from /etc/default/freeside too +--- Debian --- + +redo & test its working) somes sort of Alias /freeside /usr/share/freeside/www is needed +/var/www/freeside -> /usr/lib/freeside and Alias in apache Added to README.Debian... do something else? Ensure apache is set to run as User freeside. @@ -24,15 +61,9 @@ init.d.ex or init.d.lsb.ex finish -RT install locations (or for now: disable for unstable, enable for -experiemental. but try to get it finished off in time for lenny) +RT install locations (? maybe our RT libraries shouldn't conflict with +upstream ones?) debian/copyright administrivia -AGPL drama - upload - -AGPL drama or silent waiting for days or years - -profit! err diff --git a/debian/changelog b/debian/changelog index d070c46c9..0aadb48ff 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +freeside (3.0~20130205-1) UNRELEASED; urgency=low + + * Another stab at packaging. + + -- Ivan Kohler <ivan-debian@420.am> Tue, 05 Feb 2013 17:00:36 -0800 + freeside (2.1.1-1) UNRELEASED; urgency=low * New upstream release diff --git a/debian/compat b/debian/compat index 7ed6ff82d..45a4fb75d 100644 --- a/debian/compat +++ b/debian/compat @@ -1 +1 @@ -5 +8 diff --git a/debian/control b/debian/control index 4ea4815d2..157240659 100644 --- a/debian/control +++ b/debian/control @@ -5,20 +5,22 @@ Maintainer: Ivan Kohler <ivan-debian@420.am> Build-Depends: debhelper (>= 5), perl (>= 5.8) Standards-Version: 3.7.2 Homepage: http://www.freeside.biz/freeside -Vcs-Browser: http://www.freeside.biz/cgi-bin/viewvc.cgi/freeside/ -Vcs-Cvs: :pserver:anonymous:anonymous@cvs.420.am:/home/cvs/cvsroot freeside +#Vcs-Browser: http://www.freeside.biz/cgi-bin/viewvc.cgi/freeside/ +#Vcs-Cvs: :pserver:anonymous:anonymous@cvs.420.am:/home/cvs/cvsroot freeside Package: freeside Architecture: all -Pre-Depends: freeside-lib, dbconfig-common +Pre-Depends: freeside-lib +# dbconfig-common Depends: ${perl:Depends}, ${shlibs:Depends}, ${misc:Depends}, freeside-webui, debconf, adduser (>= 3.11) Recommends: cron Suggests: gnupg Description: Billing and trouble ticketing for service providers - Freeside is a web-based billing and trouble ticketing application. It - includes features for ISPs, hosting providers, and VoIP providers, but can - also be used as a generic customer database, invoicing and membership - application. If you like buzzwords, call it an "BSS/OSS and CRM solution". + Freeside is a web-based billing, trouble ticketing and network monitoring + application. It includes features for ISPs and WISPs, hosting providers and + VoIP providers, but can also be used as a generic customer database, invoicing + and membership application. If you like buzzwords, you can call it a + "BSS/OSS and CRM solution". Package: freeside-lib Architecture: all @@ -28,7 +30,9 @@ Suggests: libbusiness-onlinepayment-perl Description: Libraries for Freeside billing and trouble ticketing Freeside is a web-based billing and trouble ticketing application. . - This package provides the perl libraries and command line utilities. + This package provides the perl libraries and command line utilities. Also, + the init script and daemons used by the system are currently provided by this + package. #Package: freeside-bin #Architecture: all diff --git a/debian/copyright b/debian/copyright index c409cb99e..e521a701a 100644 --- a/debian/copyright +++ b/debian/copyright @@ -9,7 +9,7 @@ Upstream Author(s): Copyright: -Copyright (C) 2005-2008 Freeside Internet Services, Inc. +Copyright (C) 2005-2013 Freeside Internet Services, Inc. Copyright (C) 2000-2005 Ivan Kohler Copyright (C) 1999 Silicon Interactive Software Design All rights reserved diff --git a/debian/freeside.docs b/debian/freeside.docs index e845566c0..f4a511b64 100644 --- a/debian/freeside.docs +++ b/debian/freeside.docs @@ -1 +1,2 @@ README +AGPL diff --git a/debian/init.d.ex b/debian/init.d.ex deleted file mode 100644 index 2480f515d..000000000 --- a/debian/init.d.ex +++ /dev/null @@ -1,157 +0,0 @@ -#! /bin/sh -# -# skeleton example file to build /etc/init.d/ scripts. -# This file should be used to construct scripts for /etc/init.d. -# -# Written by Miquel van Smoorenburg <miquels@cistron.nl>. -# Modified for Debian -# by Ian Murdock <imurdock@gnu.ai.mit.edu>. -# Further changes by Javier Fernandez-Sanguino <jfs@debian.org> -# -# Version: @(#)skeleton 1.9 26-Feb-2001 miquels@cistron.nl -# - -PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin -DAEMON=/usr/sbin/freeside -NAME=freeside -DESC=freeside - -test -x $DAEMON || exit 0 - -LOGDIR=/var/log/freeside -PIDFILE=/var/run/$NAME.pid -DODTIME=1 # Time to wait for the server to die, in seconds - # If this value is set too low you might not - # let some servers to die gracefully and - # 'restart' will not work - -# Include freeside defaults if available -if [ -f /etc/default/freeside ] ; then - . /etc/default/freeside -fi - -set -e - -running_pid() -{ - # Check if a given process pid's cmdline matches a given name - pid=$1 - name=$2 - [ -z "$pid" ] && return 1 - [ ! -d /proc/$pid ] && return 1 - cmd=`cat /proc/$pid/cmdline | tr "\000" "\n"|head -n 1 |cut -d : -f 1` - # Is this the expected child? - [ "$cmd" != "$name" ] && return 1 - return 0 -} - -running() -{ -# Check if the process is running looking at /proc -# (works for all users) - - # No pidfile, probably no daemon present - [ ! -f "$PIDFILE" ] && return 1 - # Obtain the pid and check it against the binary name - pid=`cat $PIDFILE` - running_pid $pid $NAME || return 1 - return 0 -} - -force_stop() { -# Forcefully kill the process - [ ! -f "$PIDFILE" ] && return - if running ; then - kill -15 $pid - # Is it really dead? - [ -n "$DODTIME" ] && sleep "$DODTIME"s - if running ; then - kill -9 $pid - [ -n "$DODTIME" ] && sleep "$DODTIME"s - if running ; then - echo "Cannot kill $LABEL (pid=$pid)!" - exit 1 - fi - fi - fi - rm -f $PIDFILE - return 0 -} - -case "$1" in - start) - echo -n "Starting $DESC: " - start-stop-daemon --start --quiet --pidfile $PIDFILE \ - --exec $DAEMON -- $DAEMON_OPTS - if running then - echo "$NAME." - else - echo " ERROR." - fi - ;; - stop) - echo -n "Stopping $DESC: " - start-stop-daemon --stop --quiet --pidfile $PIDFILE \ - --exec $DAEMON - echo "$NAME." - ;; - force-stop) - echo -n "Forcefully stopping $DESC: " - force_stop - if ! running then - echo "$NAME." - else - echo " ERROR." - fi - ;; - #reload) - # - # If the daemon can reload its config files on the fly - # for example by sending it SIGHUP, do it here. - # - # If the daemon responds to changes in its config file - # directly anyway, make this a do-nothing entry. - # - # echo "Reloading $DESC configuration files." - # start-stop-daemon --stop --signal 1 --quiet --pidfile \ - # /var/run/$NAME.pid --exec $DAEMON - #;; - force-reload) - # - # If the "reload" option is implemented, move the "force-reload" - # option to the "reload" entry above. If not, "force-reload" is - # just the same as "restart" except that it does nothing if the - # daemon isn't already running. - # check wether $DAEMON is running. If so, restart - start-stop-daemon --stop --test --quiet --pidfile \ - /var/run/$NAME.pid --exec $DAEMON \ - && $0 restart \ - || exit 0 - ;; - restart) - echo -n "Restarting $DESC: " - start-stop-daemon --stop --quiet --pidfile \ - /var/run/$NAME.pid --exec $DAEMON - [ -n "$DODTIME" ] && sleep $DODTIME - start-stop-daemon --start --quiet --pidfile \ - /var/run/$NAME.pid --exec $DAEMON -- $DAEMON_OPTS - echo "$NAME." - ;; - status) - echo -n "$LABEL is " - if running ; then - echo "running" - else - echo " not running." - exit 1 - fi - ;; - *) - N=/etc/init.d/$NAME - # echo "Usage: $N {start|stop|restart|reload|force-reload}" >&2 - echo "Usage: $N {start|stop|restart|force-reload|status|force-stop}" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/debian/init.d.lsb.ex b/debian/init.d.lsb.ex deleted file mode 100644 index 12231294e..000000000 --- a/debian/init.d.lsb.ex +++ /dev/null @@ -1,281 +0,0 @@ -#!/bin/sh -# -# Example init.d script with LSB support. -# -# Please read this init.d carefully and modify the sections to -# adjust it to the program you want to run. -# -# Copyright (c) 2007 Javier Fernandez-Sanguino <jfs@debian.org> -# -# This is free software; you may redistribute it and/or modify -# it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2, -# or (at your option) any later version. -# -# This is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License with -# the Debian operating system, in /usr/share/common-licenses/GPL; if -# not, write to the Free Software Foundation, Inc., 59 Temple Place, -# Suite 330, Boston, MA 02111-1307 USA -# -### BEGIN INIT INFO -# Provides: freeside -# Required-Start: $network $local_fs -# Required-Stop: -# Should-Start: $named -# Should-Stop: -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: <Enter a short description of the sortware> -# Description: <Enter a long description of the software> -# <...> -# <...> -### END INIT INFO - -PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin - -DAEMON=/usr/sbin/freeside # Introduce the server's location here -NAME=#PACKAGE # Introduce the short server's name here -DESC=#PACKAGE # Introduce a short description here -LOGDIR=/var/log/freeside # Log directory to use - -PIDFILE=/var/run/$NAME.pid - -test -x $DAEMON || exit 0 -test -x $DAEMON_WRAPPER || exit 0 - -. /lib/lsb/init-functions - -# Default options, these can be overriden by the information -# at /etc/default/$NAME -DAEMON_OPTS="" # Additional options given to the server - -DODTIME=10 # Time to wait for the server to die, in seconds - # If this value is set too low you might not - # let some servers to die gracefully and - # 'restart' will not work - -LOGFILE=$LOGDIR/$NAME.log # Server logfile -#DAEMONUSER=freeside # Users to run the daemons as. If this value - # is set start-stop-daemon will chuid the server - -# Include defaults if available -if [ -f /etc/default/$NAME ] ; then - . /etc/default/$NAME -fi - -# Use this if you want the user to explicitly set 'RUN' in -# /etc/default/ -#if [ "x$RUN" != "xyes" ] ; then -# log_failure_msg "$NAME disabled, please adjust the configuration to your needs " -# log_failure_msg "and then set RUN to 'yes' in /etc/default/$NAME to enable it." -# exit 1 -#fi - -# Check that the user exists (if we set a user) -# Does the user exist? -if [ -n "$DAEMONUSER" ] ; then - if getent passwd | grep -q "^$DAEMONUSER:"; then - # Obtain the uid and gid - DAEMONUID=`getent passwd |grep "^$DAEMONUSER:" | awk -F : '{print $3}'` - DAEMONGID=`getent passwd |grep "^$DAEMONUSER:" | awk -F : '{print $4}'` - else - log_failure_msg "The user $DAEMONUSER, required to run $NAME does not exist." - exit 1 - fi -fi - - -set -e - -running_pid() { -# Check if a given process pid's cmdline matches a given name - pid=$1 - name=$2 - [ -z "$pid" ] && return 1 - [ ! -d /proc/$pid ] && return 1 - cmd=`cat /proc/$pid/cmdline | tr "\000" "\n"|head -n 1 |cut -d : -f 1` - # Is this the expected server - [ "$cmd" != "$name" ] && return 1 - return 0 -} - -running() { -# Check if the process is running looking at /proc -# (works for all users) - - # No pidfile, probably no daemon present - [ ! -f "$PIDFILE" ] && return 1 - pid=`cat $PIDFILE` - running_pid $pid $DAEMON_WRAPPER || return 1 - return 0 -} - -start_server() { -# Start the process using the wrapper - if [ -z "$DAEMONUSER" ] ; then - start-stop-daemon --start --quiet --pidfile $PIDFILE \ - --exec $DAEMON -- $DAEMON_OPTS - errcode=$? - else -# if we are using a daemonuser then change the user id - start-stop-daemon --start --quiet --pidfile $PIDFILE \ - --chuid $DAEMONUSER \ - --exec $DAEMON -- $DAEMON_OPTS - errcode=$? - fi - return $errcode -} - -stop_server() { -# Stop the process using the wrapper - if [ -z "$DAEMONUSER" ] ; then - start-stop-daemon --stop --quiet --pidfile $PIDFILE \ - --exec $DAEMON - errcode=$ - else -# if we are using a daemonuser then look for process that match - start-stop-daemon --stop --quiet --pidfile $PIDFILE \ - --user $DAEMONUSER \ - --exec $DAEMON - errcode=$ - fi - - return $errcode -} - -reload_server() { - [ ! -f "$PIDFILE" ] && return 1 - pid=`cat $PIDFILE` # This is the daemon's pid - # Send a SIGHUP - kill -1 $pid - return $? -} - -force_stop() { -# Force the process to die killing it manually - [ ! -e "$PIDFILE" ] && return - if running ; then - kill -15 $pid - # Is it really dead? - sleep "$DIETIME"s - if running ; then - kill -9 $pid - sleep "$DIETIME"s - if running ; then - echo "Cannot kill $NAME (pid=$pid)!" - exit 1 - fi - fi - fi - rm -f $PIDFILE -} - - -case "$1" in - start) - log_daemon_msg "Starting $DESC " "$NAME" - # Check if it's running first - if running ; then - log_progress_msg "apparently already running" - log_end_msg 0 - exit 0 - fi - if start_server && running ; then - # It's ok, the server started and is running - log_end_msg 0 - else - # Either we could not start it or it is not running - # after we did - # NOTE: Some servers might die some time after they start, - # this code does not try to detect this and might give - # a false positive (use 'status' for that) - log_end_msg 1 - fi - ;; - stop) - log_daemon_msg "Stopping $DESC" "$NAME" - if running ; then - # Only stop the server if we see it running - stop_server - log_end_msg $? - else - # If it's not running don't do anything - log_progress_msg "apparently not running" - log_end_msg 0 - exit 0 - fi - ;; - force-stop) - # First try to stop gracefully the program - $0 stop - if running; then - # If it's still running try to kill it more forcefully - log_daemon_msg "Stopping (force) $DESC" "$NAME" - force_stop - log_end_msg $? - fi - ;; - restart|force-reload) - log_daemon_msg "Restarting $DESC" "$NAME" - stop_server - # Wait some sensible amount, some server need this - [ -n "$DIETIME" ] && sleep $DIETIME - start_server - running - log_end_msg $? - ;; - status) - - log_daemon_msg "Checking status of $DESC" "$NAME" - if running ; then - log_progress_msg "running" - log_end_msg 0 - else - log_progress_msg "apparently not running" - log_end_msg 1 - exit 1 - fi - ;; - # Use this if the daemon cannot reload - reload) - log_warning_msg "Reloading $NAME daemon: not implemented, as the daemon" - log_warning_msg "cannot re-read the config file (use restart)." - ;; - # And this if it cann - #reload) - # - # If the daemon can reload its config files on the fly - # for example by sending it SIGHUP, do it here. - # - # If the daemon responds to changes in its config file - # directly anyway, make this a do-nothing entry. - # - # log_daemon_msg "Reloading $DESC configuration files" "$NAME" - # if running ; then - # reload_server - # if ! running ; then - # Process died after we tried to reload - # log_progress_msg "died on reload" - # log_end_msg 1 - # exit 1 - # fi - # else - # log_progress_msg "server is not running" - # log_end_msg 1 - # exit 1 - # fi - #;; - - *) - N=/etc/init.d/$NAME - echo "Usage: $N {start|stop|force-stop|restart|force-reload|status}" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/debian/rules b/debian/rules index d37dfd1c5..a8835e5fe 100755 --- a/debian/rules +++ b/debian/rules @@ -12,30 +12,37 @@ PERL ?= /usr/bin/perl #PACKAGE = $(shell dh_listpackages) PACKAGE = freeside TMP = $(CURDIR)/debian/$(PACKAGE) -DBC_SCRIPTS = $(TMP)/usr/share/dbconfig-common/scripts/freeside +#DBC_SCRIPTS = $(TMP)/usr/share/dbconfig-common/scripts/freeside -#this is gotten from dbconfig-common -DB_TYPE = db_type_is_configured_during_pkg_install_by_dbconfig-common_not_at_build_time +##this is gotten from dbconfig-common +#DB_TYPE = db_type_is_configured_during_pkg_install_by_dbconfig-common_not_at_build_time #no chance, it doesn't get backslash-interpolted now... -#DEBVERSION = `head -1 debian/changelog | cut -d')' -f1 | cut -c11-` -DEBVERSION = 1.7.3~rc2-1 -export VERSION = $(DEBVERSION) (Debian) +##DEBVERSION = `head -1 debian/changelog | cut -d')' -f1 | cut -c11-` +#DEBVERSION = 1.7.3~rc2-1 +#export VERSION = $(DEBVERSION) (Debian) -export FREESIDE_CONF = /etc/freeside -export FREESIDE_LOG = /var/log/freeside -export FREESIDE_LOCK = /var/lock/freeside -export FREESIDE_CACHE = $(TMP)/var/cache/freeside -FREESIDE_CACHE = $(TMP)/var/cache/freeside +#export FREESIDE_CONF = /etc/freeside +#export FREESIDE_LOG = /var/log/freeside +#export FREESIDE_LOCK = /var/lock/freeside +#export FREESIDE_CACHE = $(TMP)/var/cache/freeside +#FREESIDE_CACHE = $(TMP)/var/cache/freeside #XXX huh? -export FREESIDE_EXPORT = /var/spool/freeside +#export FREESIDE_EXPORT = /var/spool/freeside + +export FREESIDE_CONF = $(TMP)/usr/local/etc/freeside +export FREESIDE_LOG = $(TMP)/usr/local/etc/freeside +export FREESIDE_LOCK = $(TMP)/usr/local/etc/freeside +export FREESIDE_CACHE = $(TMP)/usr/local/etc/freeside +export FREESIDE_EXPORT = $(TMP)/usr/local/etc/freeside #XXX own subdir? -export MASON_HANDLER = $(TMP)-webui/usr/share/freeside/handler.pl +#export MASON_HANDLER = $(TMP)-webui/usr/share/freeside/handler.pl +export MASON_HANDLER=$(TMP)-webui/usr/local/etc/freeside/handler.pl -export APACHE_VERSION = 2 -export FREESIDE_DOCUMENT_ROOT = $(TMP)-webui/usr/share/freeside/www +#export FREESIDE_DOCUMENT_ROOT = $(TMP)-webui/usr/share/freeside/www +export FREESIDE_DOCUMENT_ROOT = $(TMP)-webui/var/www/freeside export INIT_FILE = $(TMP).init export INIT_INSTALL = /bin/true export HTTPD_RESTART = /bin/true @@ -49,22 +56,22 @@ export INSTALLGROUP = adm export SELFSERVICE_MACHINES = #prompt ? XXX these are runtime, not buildtime :/ -export RT_DOMAIN = `dnsdomainname` -export RT_TIMEZONE = `cat /etc/timezone` +#export RT_DOMAIN = `dnsdomainname` +#export RT_TIMEZONE = `cat /etc/timezone` -export HOSTNAME = `hostname -f` -export FREESIDE_URL = http://$(HOSTNAME)/freeside/ +#export HOSTNAME = `hostname -f` +#export FREESIDE_URL = http://$(HOSTNAME)/freeside/ #specific to deb pkg, for purposes of saving off a permanent copy of default #config for postinst and that sort of thing -export DIST_CONF = $(TMP)/usr/share/freeside/default_conf +#export DIST_CONF = $(TMP)/usr/share/freeside/default_conf #XXX yuck. proper RT layout is entirely necessary #this seems to infect way to much of RT with the build location, requiring # a kludge to hack it out afterwords. look into using fakeroot (didn't # realize it would need to be explicit argh) # (but leaving it for now, otherwise can't get RT to put files where we need em) -export RT_PATH = $(TMP)/var/opt/freeside/rt +#export RT_PATH = $(TMP)/var/opt/freeside/rt # This has to be exported to make some magic below work. export DH_OPTIONS @@ -114,14 +121,14 @@ install-stamp: build-stamp #false laziness w/install-perl-modules now #install this for postinst later (no create-config) - install -d $(DIST_CONF) + ##install -d $(DIST_CONF) #install conf/[a-z]* $(DEFAULT_CONF) #CVS is not [a-z] - install `ls -d conf/[a-z]* | grep -v CVS` $(DIST_CONF) + ##install `ls -d conf/[a-z]* | grep -v CVS` $(DIST_CONF) install -d $(FREESIDE_DOCUMENT_ROOT) install -d $(FREESIDE_CACHE)/masondata #MASONDATA - $(MAKE) -e install-docs + $(MAKE) -e DESTDIR=$(TMP)-webui install-docs #hack the build dir out of Freeside too. oh yeah, sucky. perl -p -i -e "\ @@ -131,71 +138,75 @@ install-stamp: build-stamp ${TMP}/usr/share/perl5/FS/*/* \ ${TMP}/usr/bin/* - rm -r $(FREESIDE_DOCUMENT_ROOT).* + #rm -r $(FREESIDE_DOCUMENT_ROOT).* install -d $(APACHE_CONF) - install debian/freeside.apache-alias.conf $(APACHE_CONF)/freeside-alias.conf - FREESIDE_DOCUMENT_ROOT=/usr/share/freeside/www MASON_HANDLER=/usr/share/freeside/handler.pl FREESIDE_CONF=/etc/freeside $(MAKE) -e install-apache + #install debian/freeside.apache-alias.conf $(APACHE_CONF)/freeside-alias.conf + #FREESIDE_DOCUMENT_ROOT=/usr/share/freeside/www MASON_HANDLER=/usr/share/freeside/handler.pl FREESIDE_CONF=/etc/freeside $(MAKE) -e install-apache + $(MAKE) -e install-apache $(MAKE) -e install-init #RT #(configure-rt) - - # XXX need to adjust db-type, db-database, db-rt-user, db-rt-pass - # based on info from dbc - ( cd rt; \ - cp config.layout.in config.layout; \ - perl -p -i -e "\ - s'%%%FREESIDE_DOCUMENT_ROOT%%%'${FREESIDE_DOCUMENT_ROOT}'g;\ - s'%%%MASONDATA%%%'${FREESIDE_CACHE}/masondata'g;\ - " config.layout; \ - ./configure --prefix=${RT_PATH} \ - --enable-layout=Freeside \ - --with-db-type=Pg \ - --with-db-dba=freeside \ - --with-db-database=_DBC_DBNAME_ \ - --with-db-rt-user=_DBC_DBUSER_ \ - --with-db-rt-pass=_DBC_DBPASS_ \ - --with-web-user=freeside \ - --with-web-group=freeside \ - --with-rt-group=freeside \ - ) - - #(create-rt) - install -d $(RT_PATH) - ( cd rt; make install ) - #hack the build dir out of RT. yeah, sucky. - perl -p -i -e "\ - s'${TMP}''g;\ - " ${RT_PATH}/etc/RT_Config.pm \ - ${RT_PATH}/lib/RT.pm \ - ${RT_PATH}/bin/mason_handler.fcgi \ - ${RT_PATH}/bin/mason_handler.scgi \ - ${RT_PATH}/bin/standalone_httpd \ - ${RT_PATH}/bin/webmux.pl \ - ${RT_PATH}/bin/rt-crontool \ - ${RT_PATH}/sbin/rt-dump-database \ - ${RT_PATH}/sbin/rt-setup-database - - #hack @INC dir out of RT (well, handler.pl) too. - perl -p -i -e "\ - s'/opt/rt3/'/var/opt/freeside/rt/'g;\ - " ${TMP}-webui/usr/share/freeside/handler.pl - - mv ${RT_PATH}/etc/RT_Config.pm ${RT_PATH}/etc/RT_Config.pm.dbc - - perl -p -i -e "\ - s'%%%RT_DOMAIN%%%'${RT_DOMAIN}'g;\ - s'%%%RT_TIMEZONE%%%'${RT_TIMEZONE}'g;\ - s'%%%FREESIDE_URL%%%'${FREESIDE_URL}'g;\ - " ${RT_PATH}/etc/RT_SiteConfig.pm - - install -D debian/dbconfig-common.install $(DBC_SCRIPTS)/install/pgsql - install -D debian/dbconfig-common.install $(DBC_SCRIPTS)/install/mysql + $(MAKE) -e configure-rt + + ## XXX need to adjust db-type, db-database, db-rt-user, db-rt-pass + ## based on info from dbc + #( cd rt; \ + # cp config.layout.in config.layout; \ + # perl -p -i -e "\ + # s'%%%FREESIDE_DOCUMENT_ROOT%%%'${FREESIDE_DOCUMENT_ROOT}'g;\ + # s'%%%MASONDATA%%%'${FREESIDE_CACHE}/masondata'g;\ + # " config.layout; \ + # ./configure --prefix=${RT_PATH} \ + # --enable-layout=Freeside \ + # --with-db-type=Pg \ + # --with-db-dba=freeside \ + # --with-db-database=_DBC_DBNAME_ \ + # --with-db-rt-user=_DBC_DBUSER_ \ + # --with-db-rt-pass=_DBC_DBPASS_ \ + # --with-web-user=freeside \ + # --with-web-group=freeside \ + # --with-rt-group=freeside \ + #) + + ##(create-rt) + #$(MAKE) -e create-rt + + #install -d $(RT_PATH) + #( cd rt; make install ) + ##hack the build dir out of RT. yeah, sucky. + #perl -p -i -e "\ + # s'${TMP}''g;\ + #" ${RT_PATH}/etc/RT_Config.pm \ + # ${RT_PATH}/lib/RT.pm \ + # ${RT_PATH}/bin/mason_handler.fcgi \ + # ${RT_PATH}/bin/mason_handler.scgi \ + # ${RT_PATH}/bin/standalone_httpd \ + # ${RT_PATH}/bin/webmux.pl \ + # ${RT_PATH}/bin/rt-crontool \ + # ${RT_PATH}/sbin/rt-dump-database \ + # ${RT_PATH}/sbin/rt-setup-database + # + ##hack @INC dir out of RT (well, handler.pl) too. + #perl -p -i -e "\ + # s'/opt/rt3/'/var/opt/freeside/rt/'g;\ + #" ${TMP}-webui/usr/share/freeside/handler.pl + + #mv ${RT_PATH}/etc/RT_Config.pm ${RT_PATH}/etc/RT_Config.pm.dbc + + #perl -p -i -e "\ + # s'%%%RT_DOMAIN%%%'${RT_DOMAIN}'g;\ + # s'%%%RT_TIMEZONE%%%'${RT_TIMEZONE}'g;\ + # s'%%%FREESIDE_URL%%%'${FREESIDE_URL}'g;\ + #" ${RT_PATH}/etc/RT_SiteConfig.pm + + #install -D debian/dbconfig-common.install $(DBC_SCRIPTS)/install/pgsql + #install -D debian/dbconfig-common.install $(DBC_SCRIPTS)/install/mysql - install -D debian/dbconfig-common.upgrade $(DBC_SCRIPTS)/upgrade/pgsql/$(DEBVERSION) - install -D debian/dbconfig-common.upgrade $(DBC_SCRIPTS)/upgrade/mysql/$(DEBVERSION) + #install -D debian/dbconfig-common.upgrade $(DBC_SCRIPTS)/upgrade/pgsql/$(DEBVERSION) + #install -D debian/dbconfig-common.upgrade $(DBC_SCRIPTS)/upgrade/mysql/$(DEBVERSION) dh_install @@ -207,7 +218,6 @@ binary-arch: binary-indep: build install dh_testdir dh_testroot - dh_installchangelogs ChangeLog dh_installdocs #freeside.docs README AGPL dh_installexamples eg/* # dh_installmenu diff --git a/debian/templates b/debian/templates deleted file mode 100644 index e69de29bb..000000000 --- a/debian/templates +++ /dev/null diff --git a/fs_selfservice/FS-SelfService/SelfService.pm b/fs_selfservice/FS-SelfService/SelfService.pm index c22e4269e..651a8f5cf 100644 --- a/fs_selfservice/FS-SelfService/SelfService.pm +++ b/fs_selfservice/FS-SelfService/SelfService.pm @@ -57,6 +57,10 @@ $socket .= '.'.$tag if defined $tag && length($tag); 'svc_status_html' => 'MyAccount/svc_status_html', 'svc_status_hash' => 'MyAccount/svc_status_hash', 'set_svc_status_hash' => 'MyAccount/set_svc_status_hash', + 'set_svc_status_listadd' => 'MyAccount/set_svc_status_listadd', + 'set_svc_status_listdel' => 'MyAccount/set_svc_status_listdel', + 'set_svc_status_vacationadd'=> 'MyAccount/set_svc_status_vacationadd', + 'set_svc_status_vacationdel'=> 'MyAccount/set_svc_status_vacationdel', 'acct_forward_info' => 'MyAccount/acct_forward_info', 'process_acct_forward' => 'MyAccount/process_acct_forward', 'list_dsl_devices' => 'MyAccount/list_dsl_devices', diff --git a/fs_selfservice/FS-SelfService/cgi/myaccount_menu.html b/fs_selfservice/FS-SelfService/cgi/myaccount_menu.html index c7d2bb2aa..4a31b1258 100644 --- a/fs_selfservice/FS-SelfService/cgi/myaccount_menu.html +++ b/fs_selfservice/FS-SelfService/cgi/myaccount_menu.html @@ -88,11 +88,13 @@ push @menu, { title=>'Logout', url=>'logout', size=>'+1', }, ; +my %menu_disable = map { $_=>1 } @menu_disable; foreach my $item ( @menu ) { - next if $menu_skipblanks && $item->{'title'} =~ /^\s*$/; - next if $menu_skipheadings && ! $item->{'url'}; - + next if ( $menu_skipblanks && $item->{'title'} =~ /^\s*$/ ) + || ( $menu_skipheadings && ! $item->{'url'} ) + || $menu_disable{$item->{'title'}}; + $OUT .= '<TR><TD'; if ( $menu_body_image ) { if ( exists $item->{'url'} && $action eq $item->{'url'} ) { diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi index de0ab1a76..f7fe308cf 100755 --- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi +++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi @@ -873,6 +873,7 @@ sub view_cdr_details { 'svcnum' => $cgi->param('svcnum'), 'beginning' => $cgi->param('beginning') || '', 'ending' => $cgi->param('ending') || '', + 'inbound' => $cgi->param('inbound') || 0, ); } diff --git a/fs_selfservice/FS-SelfService/cgi/signup.html b/fs_selfservice/FS-SelfService/cgi/signup.html index a3db74cfd..447f39af0 100755 --- a/fs_selfservice/FS-SelfService/cgi/signup.html +++ b/fs_selfservice/FS-SelfService/cgi/signup.html @@ -30,10 +30,10 @@ ' Signup form</FONT><BR><BR>'; %> -<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT> +<FONT SIZE="+1" COLOR="#ff0000"><%= encode_entities($error) %></FONT> <FORM NAME="OneTrueForm" ACTION="<%= $self_url %>" METHOD=POST onSubmit="document.OneTrueForm.signup.disabled=true"> -<INPUT TYPE="hidden" NAME="prepaid_shortform" VALUE="<%= $prepaid_shortform %>"> +<INPUT TYPE="hidden" NAME="prepaid_shortform" VALUE="<%= encode_entities($prepaid_shortform) %>"> <INPUT TYPE="hidden" NAME="session" VALUE="<%= $session_id %>"> <INPUT TYPE="hidden" NAME="action" VALUE="process_signup"> <INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>"> @@ -149,6 +149,7 @@ $OUT .= qq! else { @payby = ('PREPAY'); } +''; %> <BR>Billing information<TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=0 WIDTH="100%"> diff --git a/fs_selfservice/FS-SelfService/cgi/small_custview.html b/fs_selfservice/FS-SelfService/cgi/small_custview.html index 8d6e07368..470fe7151 100644 --- a/fs_selfservice/FS-SelfService/cgi/small_custview.html +++ b/fs_selfservice/FS-SelfService/cgi/small_custview.html @@ -10,10 +10,10 @@ Customer #<B><%= $custnum %></B> ? '<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>' diff --git a/fs_selfservice/FS-SelfService/cgi/view_cdr_details.html b/fs_selfservice/FS-SelfService/cgi/view_cdr_details.html index b0205ec89..0ee8e9635 100644 --- a/fs_selfservice/FS-SelfService/cgi/view_cdr_details.html +++ b/fs_selfservice/FS-SelfService/cgi/view_cdr_details.html @@ -1,5 +1,6 @@ <%= $url = "$selfurl?session=$session_id;action="; ''; %> -<%= include('header', 'Call usage for '. +<%= include('header', ($inbound ? 'Received calls' : 'Dialed calls' ) . + ' for '. Date::Format::time2str('%b %o %Y', $beginning). ' - '. Date::Format::time2str('%b %o %Y', $ending) diff --git a/fs_selfservice/FS-SelfService/cgi/view_usage.html b/fs_selfservice/FS-SelfService/cgi/view_usage.html index fd5426a75..35d128998 100644 --- a/fs_selfservice/FS-SelfService/cgi/view_usage.html +++ b/fs_selfservice/FS-SelfService/cgi/view_usage.html @@ -1,7 +1,20 @@ <%= $url = "$selfurl?session=$session_id;action="; - @svc_acct = grep { $_->{svcdb} eq 'svc_acct' } @svcs; - @svc_phone = grep { $_->{svcdb} eq 'svc_phone' } @svcs; - @svc_port = grep { $_->{svcdb} eq 'svc_port' } @svcs; + %by_pkg_label = (); # not used yet, but I'm sure it will be... + @svc_acct = (); + @svc_phone = (); + @svc_port = (); + + foreach (@svcs) { + $by_pkg_label{ $_->{pkg_label} } ||= []; + push @{ $by_pkg_label{ $_->{pkg_label} } }, $_; + if ( $_->{svcdb} eq 'svc_acct' ) { + push @svc_acct, $_; + } elsif ( $_->{svcdb} eq 'svc_phone' ) { + push @svc_phone, $_; + } elsif ( $_->{svcdb} eq 'svc_port' ) { + push @svc_port, $_; + } + } ''; %> <%= include('header', 'Account usage') %> @@ -62,11 +75,22 @@ <%= scalar(@svc_acct) ? '</TABLE><BR><BR>' : '' %> <%= if ( @svc_phone ) { + %any = (); + for my $dir (qw(outbound inbound)) { + $any{$dir} = grep { $_->{$dir} } @svc_phone; + } $OUT.= '<FONT SIZE="4">Call usage</FONT><BR><BR> - <TABLE BGCOLOR="#cccccc"> + <TABLE BGCOLOR="#cccccc" STYLE="display:inline-block"> <TR> - <TH ALIGN="left">Number</TH>'; #"Account" ? - #what else? + <TH ALIGN="left">Number</TH>'; + if ( $any{outbound} ) { + $OUT .= ' + <TH>Dialed</TH>'; + } + if ( $any{inbound} ) { + $OUT .= ' + <TH>Received</TH>'; + } $OUT .= '</TR>'; } else { $OUT .= ''; @@ -76,13 +100,65 @@ <%= foreach my $svc_phone ( @svc_phone ) { my $link = "${url}view_cdr_details;". "svcnum=$svc_phone->{'svcnum'};beginning=0;ending=0"; - $OUT .= '<TR><TD>'; - $OUT .= qq!<A HREF="$link">!. $svc_phone->{'label'}. ': '. $svc_phone->{'value'}.'</A>'; - $OUT .= '</TD></TR>'; + $OUT .= '<TR><TD>'. $svc_phone->{'label'}. ': '. $svc_phone->{'value'}; + $OUT .= '</TD>'; + # usage summary w/ links + for my $dir (qw(outbound inbound)) { + if ( $dir eq 'inbound' ) { + $link .= ';inbound=1'; + } + if ( $svc_phone->{$dir} ) { + $OUT .= '<TD ALIGN="right">'.qq!<A HREF="$link">! . + sprintf('%d calls (%.0f minutes)', + $svc_phone->{$dir}->{'count'}, + $svc_phone->{$dir}->{'duration'} / 60 + ) . + '</A></TD>'; + } elsif ( $any{$dir} ) { + $OUT .= '<TD></TD>'; + } } + $OUT .= '</TR>'; +} +''; %> -<%= scalar(@svc_phone) ? '</TABLE><BR><BR>' : '' %> +<%= if ( @usage_pools ) { + $OUT .= '</TABLE> + <TABLE BGCOLOR="#cccccc" STYLE="display: inline-block"> + <TR><TH COLSPAN=4>Remaining minutes</TH></TR> + '; + my $any_shared = 0; + foreach my $usage (@usage_pools) { + # false laziness with the back office side + my ($description, $remain, $total, $shared) = @$usage; + if ( $shared ) { + $any_shared = 1; + $description .= '*'; + } + my $ratio = 255 * ($remain/$total); + $ratio = 255 if $color > 255; + my $color = + sprintf('STYLE="font-weight: bold; color: #%02x%02x00"', + 255 - $ratio, $ratio); + $OUT .= + qq!<TR> + <TD ALIGN="right">$description</TD> + <TD $color ALIGN="right">$remain</TD> + <TD $color> / </TD> + <TD $color>$total</TD> + </TR>!; + } + if ( $any_shared ) { + $OUT .= '<TR STYLE="font-size: 80%; font-style: italic">'. + '<TD COLSPAN=4>* shared among all your phone plans</TD></TR>'; + } +} +if ( scalar(@svc_phone) or scalar(@usage_pools) ) { + $OUT .= '</TABLE><BR><BR>'; +} +''; +%> <%= if ( @svc_port ) { $OUT.= '<FONT SIZE="4">Bandwidth Graphs</FONT><BR><BR> diff --git a/htetc/freeside-rt.conf b/htetc/freeside-rt.conf index 5586e1229..71ebfbd0b 100644 --- a/htetc/freeside-rt.conf +++ b/htetc/freeside-rt.conf @@ -77,3 +77,10 @@ PerlHandler HTML::Mason 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> diff --git a/httemplate/browse/msgcat.html b/httemplate/browse/msgcat.html index ac8a3a437..7509cf7d4 100644 --- a/httemplate/browse/msgcat.html +++ b/httemplate/browse/msgcat.html @@ -1,5 +1,5 @@ <& elements/browse.html, - title => mt('Message catalog'), + title => mt('Translation strings'), name_singular => 'string', #mt? no, we need to do it through the quant/PL stuff query => { 'table' => 'msgcat', 'hashref' => { 'locale' => $locale, }, diff --git a/httemplate/browse/part_export.cgi b/httemplate/browse/part_export.cgi index 91238a0fd..876633afc 100755 --- a/httemplate/browse/part_export.cgi +++ b/httemplate/browse/part_export.cgi @@ -38,6 +38,21 @@ function part_export_areyousure(href) { <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"> <% $part_export->label_html %> (<A HREF="<% $p %>edit/part_export.cgi?<% $part_export->exportnum %>">edit</A> | <A HREF="javascript:part_export_areyousure('<% $p %>misc/delete-part_export.cgi?<% $part_export->exportnum %>')">delete</A>) +% if ( my @actions = $part_export->actions ) { + <P STYLE="position: absolute"> + Management: +% while (@actions) { +% my $label = shift @actions; +% my $path = shift @actions; + <& /elements/popup_link.html, + 'label' => $label, + 'action' => $fsurl.$path.'?'.$part_export->exportnum, + 'actionlabel' => $label, + &><% @actions ? ' | ' : '' %> +% } + </P> +% } #if @actions + </TD> <TD CLASS="inv" BGCOLOR="<% $bgcolor %>"> diff --git a/httemplate/browse/part_pkg.cgi b/httemplate/browse/part_pkg.cgi index 57a429747..bb5bc5215 100755 --- a/httemplate/browse/part_pkg.cgi +++ b/httemplate/browse/part_pkg.cgi @@ -1,6 +1,8 @@ <% include( 'elements/browse.html', 'title' => 'Package Definitions', + 'menubar' => \@menubar, 'html_init' => $html_init, + 'html_form' => $html_form, 'html_posttotal' => $html_posttotal, 'name' => 'package definitions', 'disableable' => 1, @@ -20,6 +22,9 @@ 'fields' => \@fields, 'links' => \@links, 'align' => $align, + 'link_field' => 'pkgpart', + 'html_init' => $html_init, + 'html_foot' => $html_foot, ) %> <%init> @@ -33,6 +38,7 @@ my $acl_edit_global = $curuser->access_right($edit_global); my $acl_config = $curuser->access_right('Configuration'); #to edit services #and agent types #and bulk change +my $acl_edit_bulk = $curuser->access_right('Bulk edit package definitions'); die "access denied" unless $acl_edit || $acl_edit_global; @@ -130,9 +136,7 @@ $select = " "; -my $html_init; -#unless ( $cgi->param('active') ) { - $html_init = qq! +my $html_init = qq! One or more service definitions are grouped together into a package definition and given pricing information. Customers purchase packages rather than purchase services directly.<BR><BR> @@ -144,7 +148,6 @@ my $html_init; </FORM> <BR><BR> !; -#} $cgi->param('dummy', 1); @@ -274,6 +277,18 @@ push @fields, sub { : () ), ], + ( map { my $dst_pkg = $_->dst_pkg; + [ + { data => 'Supplemental: '. + '<A HREF="#'. $dst_pkg->pkgpart . '">' . + $dst_pkg->pkg . '</A>', + align=> 'center', + colspan => 2, + } + ] + } + $part_pkg->supp_part_pkg_link + ), ( map { my $dst_pkg = $_->dst_pkg; [ @@ -423,6 +438,10 @@ if ( $taxclasses ) { $align .= 'l'; } +# make a table of report class optionnames => the actual +my %report_optionname_name = map { 'report_option_'.$_->num, $_->name } + qsearch('part_pkg_report_option', { disabled => '' }); + push @header, 'Plan options', 'Services'; #'Service', 'Quan', 'Primary'; @@ -433,8 +452,18 @@ push @fields, if ( $part_pkg->plan ) { my %options = $part_pkg->options; - - [ map { + # gather any options that are really report options, + # convert them to their user-friendly names, + # and sort them (I think?) + my @report_options = + sort { $a cmp $b } + map { $report_optionname_name{$_} } + grep { $options{$_} + and exists($report_optionname_name{$_}) } + keys %options; + + my @rows = ( + map { [ { 'data' => "$_: ", 'align' => 'right', @@ -445,11 +474,30 @@ push @fields, ]; } grep { $options{$_} =~ /\S/ } - grep { $_ !~ /^(setup|recur)_fee$/ } + grep { $_ !~ /^(setup|recur)_fee$/ + and $_ !~ /^report_option_\d+$/ } keys %options - ]; + ); + if ( @report_options ) { + push @rows, + [ { 'data' => 'Report classes', + 'align' => 'center', + 'style' => 'font-weight: bold', + 'colspan' => 2 + } ]; + foreach (@report_options) { + push @rows, [ + { 'data' => $_, + 'align' => 'center', + 'colspan' => 2 + } + ]; + } # foreach @report_options + } # if @report_options + + return \@rows; - } else { + } else { # should never happen... [ map { [ { 'data' => uc($_), @@ -470,6 +518,8 @@ push @fields, sub { my $part_pkg = shift; + my @part_pkg_usage = sort { $a->priority <=> $b->priority } + $part_pkg->part_pkg_usage; [ (map { @@ -512,7 +562,27 @@ push @fields, ] } $part_pkg->svc_part_pkg_link - ) + ), + ( scalar(@part_pkg_usage) ? + [ { data => 'Usage minutes', + align => 'center', + colspan => 2, + data_style => 'b', + link => $p.'browse/part_pkg_usage.html#pkgpart'. + $part_pkg->pkgpart + } ] + : () + ), + ( map { + [ { data => $_->minutes, + align => 'right' + }, + { data => $_->description, + align => 'left' + }, + ] + } @part_pkg_usage + ), ]; }; @@ -527,4 +597,25 @@ $extra_count = ( $count_extra_sql ? ' AND ' : ' WHERE ' ). $extra_count if $extra_count; my $count_query = "SELECT COUNT(*) FROM part_pkg $count_extra_sql $extra_count"; +my $html_form = ''; +my $html_foot = ''; +if ( $acl_edit_bulk ) { + # insert a checkbox column + push @header, ''; + push @fields, sub { + '<INPUT TYPE="checkbox" NAME="pkgpart" VALUE=' . $_[0]->pkgpart .'>'; + }; + push @links, ''; + $align .= 'c'; + $html_form = qq!<FORM ACTION="${p}edit/bulk-part_pkg.html" METHOD="POST">!; + $html_foot = include('/search/elements/checkbox-foot.html', + submit => 'edit report classes', # for now it's only report classes + ) . '</FORM>'; +} + +my @menubar; +# show this if there are any voip_cdr packages defined +if ( FS::part_pkg->count("plan = 'voip_cdr'") ) { + push @menubar, 'Per-package usage minutes' => $p.'browse/part_pkg_usage.html'; +} </%init> diff --git a/httemplate/browse/part_pkg_usage.html b/httemplate/browse/part_pkg_usage.html new file mode 100644 index 000000000..209fd3a01 --- /dev/null +++ b/httemplate/browse/part_pkg_usage.html @@ -0,0 +1,112 @@ +<& /elements/header.html, 'Package usage minutes' &> +<& /elements/menubar.html, 'Package definitions', $p.'browse/part_pkg.cgi' &> +<STYLE TYPE="text/css"> +.pkg_head { + background-color: #dddddd; + font-style: italic; +} +.pkg_head > td { + border-style: solid; + border-radius: 3px; + border-color: #555555; + border-width: 1px; +} +.usage > td { + text-align: center; +} +.error { + color: #ff0000; +} +</STYLE> +<FORM METHOD="POST" ACTION="<%$fsurl%>edit/process/part_pkg_usage.html"> + <TABLE STYLE="margin-top: 1em"> + <TR> + <TH>Minutes</TH> + <TH>Shared</TH> + <TH>Rollover</TH> + <TH>Description</TH> + <TH>Priority</TH> +% foreach my $class (@usage_class) { + <TH><% $class->classname %></TH> +% } + </TR> + +% my $error = $cgi->param('error'); +% foreach my $part_pkg (@part_pkg) { +% my $pkgpart = $part_pkg->pkgpart; +% my @part_pkg_usage; +% if ( $error ) { +% @part_pkg_usage = @{ $error->{$pkgpart} }; +% } else { +% @part_pkg_usage = $part_pkg->part_pkg_usage; +% foreach my $usage (@part_pkg_usage) { +% foreach ($usage->classnums) { +% $usage->set("class$_".'_', 'Y'); +% } +% } +% } + <TR CLASS="pkg_head" ID="pkgpart<%$pkgpart%>"> + <TD COLSPAN=<%$n_cols%>><% $part_pkg->pkg_comment %></TD> +% # make it easy to enumerate the pkgparts later + <INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $pkgpart %>"> + </TR> +% # template row + <TR id="pkgpart<%$pkgpart%>_template" CLASS="usage"> + <TD> + <INPUT TYPE="hidden" NAME="pkgusagepart"> + <INPUT TYPE="text" NAME="minutes" ID="minutes" SIZE=7> + </TD> +% foreach (qw(shared rollover)) { + <TD> + <INPUT TYPE="checkbox" NAME="<% $_ %>" ID="<% $_ %>" VALUE="Y"> + </TD> +% } + <TD> + <INPUT TYPE="text" NAME="description" ID="description" SIZE=20> + </TD> + <TD> + <INPUT TYPE="text" NAME="priority" ID="priority" SIZE=3> + </TD> +% foreach (@usage_class) { +% my $classnum = 'class' . $_->classnum . '_'; + <TD> + <INPUT TYPE="checkbox" NAME="<% $classnum %>" ID="<% $classnum %>" VALUE="Y"> + </TD> +% } + </TR> + <& /elements/auto-table.html, + table => "pkgpart$pkgpart", + template_row => "pkgpart$pkgpart".'_template', + data => \@part_pkg_usage, + &> +% } + </TABLE> + <BR> + <INPUT TYPE="submit"> +</FORM> +<& /elements/footer.html &> +<%init> +my $curuser = $FS::CurrentUser::CurrentUser; +die "access denied" + unless $curuser->access_right( + ['Edit package definitions', 'Edit global package definitions'] + ); + +my @where = ("(plan = 'voip_cdr' OR plan = 'voip_inbound')", + "freq != '0'", + "disabled IS NULL"); +push @where, FS::part_pkg->curuser_pkgs_sql + unless $curuser->access_right('Edit global package definitions'); +my $extra_sql = ' WHERE '.join(' AND ', @where); +my @part_pkg = qsearch({ + 'table' => 'part_pkg', + 'extra_sql' => $extra_sql, + 'order_by' => ' ORDER BY pkgpart', +}); + +my @usage_class = sort { $a->weight <=> $b->weight } + qsearch('usage_class', { disabled => '' }); + +my $n_usage_classes = scalar(@usage_class); +my $n_cols = $n_usage_classes + 5; # minutes, shared, rollover, desc, prio +</%init> diff --git a/httemplate/browse/part_svc.cgi b/httemplate/browse/part_svc.cgi index a8f4a7c84..0d3685355 100755 --- a/httemplate/browse/part_svc.cgi +++ b/httemplate/browse/part_svc.cgi @@ -82,6 +82,7 @@ function part_export_areyousure(href) { % } % @dfields ; % my $rowspan = scalar(@fields) || 1; +% $rowspan++ if $part_svc->restrict_edit_password; % my $url = "${p}edit/part_svc.cgi?". $part_svc->svcpart; % % if ( $bgcolor eq $bgcolor1 ) { @@ -174,24 +175,32 @@ function part_export_areyousure(href) { % my $value = &$formatter($part_svc->part_svc_column($field)->columnvalue); % if ( $flag =~ /^[MAH]$/ ) { % my $select_table = ($flag eq 'H') ? 'hardware_class' : 'inventory_class'; -% $select_class{$value} ||= -% qsearchs($select_table, { 'classnum' => $value } ); +% foreach my $classnum ( split(',', $value) ) { +% $select_class{$classnum} = +% qsearchs($select_table, { 'classnum' => $classnum } ); % - <% $select_class{$value} - ? $select_class{$value}->classname - : "WARNING: $select_table.classnum $value not found" %> + <% $select_class{$classnum} + ? $select_class{$classnum}->classname + : "WARNING: $select_table.classnum $classnum not found" %><BR> +% } % } else { <% $value %> -% } +% } </TD> % $n1="</TR><TR>"; -% } -% +% } #foreach $field +% if ( $part_svc->restrict_edit_password ) { + <TR> + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" COLSPAN=4 ALIGN="left"> + <B><% emt('Password editing restricted.') %></B> + </TD> + </TR> +% } </TR> -% } +% } #foreach $part_svc </TABLE> </BODY> diff --git a/httemplate/browse/rate_region.html b/httemplate/browse/rate_region.html index b958894cb..b0ce467c0 100644 --- a/httemplate/browse/rate_region.html +++ b/httemplate/browse/rate_region.html @@ -62,8 +62,14 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities(); die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); +my $sub_prefixes = sub { + my $region = shift; + $region->prefixes . + ($region->exact_match ? ' <I>(exact match only)</I>' : ''); +}; + my @header = ( '#', 'Region', 'Country code', 'Prefixes' ); -my @fields = ( 'regionnum', 'regionname', 'ccode', 'prefixes' ); +my @fields = ( 'regionnum', 'regionname', 'ccode', $sub_prefixes ); my @links = ( ($link) x 4 ); my @align = ( 'right', 'left', 'right', 'left' ); my @xls_format = ( ({ locked=>1, bg_color=>22 }) x 4 ); diff --git a/httemplate/docs/license.html b/httemplate/docs/license.html index e40b2436b..05a89c1f0 100644 --- a/httemplate/docs/license.html +++ b/httemplate/docs/license.html @@ -6,7 +6,7 @@ <P> -Copyright © 2005-2012 Freeside Internet Services, Inc.<BR> +Copyright © 2005-2013 Freeside Internet Services, Inc.<BR> Copyright © 2000-2005 Ivan Kohler<BR> Copyright © 1999 Silicon Interactive Software Design<BR> All rights reserved<BR> diff --git a/httemplate/edit/REAL_cust_pkg.cgi b/httemplate/edit/REAL_cust_pkg.cgi index 166a3b7ea..99e911ae5 100755 --- a/httemplate/edit/REAL_cust_pkg.cgi +++ b/httemplate/edit/REAL_cust_pkg.cgi @@ -9,6 +9,29 @@ <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT> <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT> +<SCRIPT TYPE="text/javascript"> +var submit_fields = []; +function confirm_changes() { + var i; + var querystring = 'pkgnum=<%$pkgnum%>'; + var f = document.forms.formname; + for(i = 0; i < submit_fields.length; i++) { + querystring += ';' + + submit_fields[i] + + '=' + + encodeURIComponent(f.elements[submit_fields[i] + '_text'].value); + } + overlib( + OLiframeContent( + '<%$p%>/misc/confirm-cust_pkg-edit_dates.html?' + querystring, + 576, 576, 'confirm_popup' + ), + CAPTION, 'Package date changes', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', + MIDX, 0, MIDY, 0, DRAGGABLE, BGCOLOR, '#333399', CGCOLOR, '#333399', + TEXTSIZE, 3 + ); +} +</SCRIPT> <FORM NAME="formname" ACTION="process/REAL_cust_pkg.cgi" METHOD="POST"> <INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>"> @@ -31,6 +54,15 @@ <TD BGCOLOR="#ffffff"><% $part_pkg->pkg %></TD> </TR> +% if ( $cust_pkg->main_pkgnum ) { +% my $main_pkg = $cust_pkg->main_pkg; + <TR> + <TD ALIGN="right">Supplemental to</TD> + <TD BGCOLOR="#ffffff">Package #<% $cust_pkg->main_pkgnum%>: \ + <% $main_pkg->part_pkg->pkg %></TD> + </TR> + +% } <TR> <TD ALIGN="right">Custom</TD> <TD BGCOLOR="#ffffff"><% $part_pkg->custom %></TD> @@ -38,7 +70,7 @@ <TR> <TD ALIGN="right">Comment</TD> - <TD BGCOLOR="#ffffff"><% $part_pkg->comment %></TD> + <TD BGCOLOR="#ffffff"><% $part_pkg->comment |h %></TD> </TR> <TR> @@ -50,14 +82,14 @@ % if ( $cust_pkg->setup && ! $cust_pkg->start_date ) { <& .row_display, cust_pkg=>$cust_pkg, column=>'start_date', label=>'Start' &> % } else { - <& .row_edit, cust_pkg=>$cust_pkg, column=>'start_date', label=>'Start' &> + <& .row_edit, cust_pkg=>$cust_pkg, column=>'start_date', label=>'Start', if_primary=>1 &> % } - <& .row_edit, cust_pkg=>$cust_pkg, column=>'setup', label=>'Setup' &> + <& .row_edit, cust_pkg=>$cust_pkg, column=>'setup', label=>'Setup', if_primary=>1 &> <& .row_edit, cust_pkg=>$cust_pkg, column=>'last_bill', label=>$last_bill_or_renewed &> <& .row_edit, cust_pkg=>$cust_pkg, column=>'bill', label=>$next_bill_or_prepaid_until &> %#if ( $cust_pkg->contract_end or $part_pkg->option('contract_end_months',1) ) { - <& .row_edit, cust_pkg=>$cust_pkg, column=>'contract_end',label=>'Contract end' &> + <& .row_edit, cust_pkg=>$cust_pkg, column=>'contract_end',label=>'Contract end', if_primary=>1 &> %#} <& .row_display, cust_pkg=>$cust_pkg, column=>'adjourn', label=>'Adjournment', note=>'(will <b>suspend</b> this package when the date is reached)' &> <& .row_display, cust_pkg=>$cust_pkg, column=>'susp', label=>'Suspension' &> @@ -73,10 +105,17 @@ $column $label $note => '' + $if_primary => 0 </%args> % my $value = $cust_pkg->get($column); % $value = $value ? time2str($format, $value) : ""; - +% +% # if_primary for the dates that can't be edited on supplemental packages +% if ($if_primary and $cust_pkg->main_pkgnum) { + <INPUT TYPE="hidden" ID="<%$column%>_text" VALUE="<% $cust_pkg->get($column) %>"> + <SCRIPT>submit_fields.push('<%$column%>');</SCRIPT> + <& .row_display, %ARGS &> +% } else { <TR> <TD ALIGN="right"><% $label %> date</TD> <TD> @@ -104,8 +143,11 @@ button: "<% $column %>_button", align: "BR" }); - </SCRIPT> + submit_fields.push('<%$column%>'); + + </SCRIPT> +% } </%def> <%def .row_display> @@ -114,6 +156,7 @@ $column $label $note => '' + $is_primary => 0 #ignored </%args> % if ( $cust_pkg->get($column) ) { <TR> @@ -130,7 +173,7 @@ </TABLE> <BR> -<INPUT TYPE="submit" VALUE="<% mt('Apply changes') |h %>"> +<INPUT TYPE="button" VALUE="<% mt('Apply changes') |h %>" onclick="confirm_changes()"> </FORM> <% include('/elements/footer.html') %> @@ -160,38 +203,6 @@ if ( $cgi->param('error') ) { my @errors = (); my %errors = map { $_=>1 } split(',', $cgi->param('error')); $cgi->param('error', ''); - - if ( $errors{'_bill_areyousure'} ) { - if ( $cgi->param('bill') =~ /^([\s\d\/\:\-\(\w\)]*)$/ ) { - my $bill = $1; - push @errors, - "You are attempting to set the next bill date to $bill, which is - in the past. This will charge the customer for the interval - from $bill until now. Are you sure you want to do this? ". - '<INPUT TYPE="checkbox" NAME="bill_areyousure" VALUE="1">'; - } - } - - if ( $errors{'_setup_areyousure'} ) { - push @errors, - "You are attempting to remove the setup date. This will re-charge the - customer for the setup fee. Are you sure you want to do this? ". - '<INPUT TYPE="checkbox" NAME="setup_areyousure" VALUE="1">'; - } - - if ( $errors{'_setupadd_areyousure'} ) { - push @errors, - "You are attempting to add a setup date. This will prevent charging the - customer for the setup fee. Are you sure you want to do this? ". - '<INPUT TYPE="checkbox" NAME="setupadd_areyousure" VALUE="1">'; - } - - if ( $errors{'_start'} ) { - push @errors, - "You are attempting to add a start date to a package that has already - started billing."; - } - $error = join('<BR><BR>', @errors ); } diff --git a/httemplate/edit/bulk-part_pkg.html b/httemplate/edit/bulk-part_pkg.html new file mode 100644 index 000000000..a1c6f0c9b --- /dev/null +++ b/httemplate/edit/bulk-part_pkg.html @@ -0,0 +1,74 @@ +<& /elements/header.html, 'Edit package report classes' &> +%# change that title if we add any other editing controls + +%# this should be centralized somewhere +<STYLE TYPE="text/css"> +.row0 { background-color: #eeeeee; } +.row1 { background-color: #ffffff; } +</STYLE> + +<FORM ACTION="process/bulk-part_pkg.html" METHOD="POST"> +<DIV> +The following packages will be changed:<BR> +% foreach my $pkgpart (sort keys(%part_pkg)) { +<INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $pkgpart %>"> +<% $part_pkg{$pkgpart}->pkg_comment |h %><BR> +% } +</DIV> +<BR> +<& /elements/table-grid.html &>\ +<& /elements/tr-justtitle.html, value => mt('Report classes') &> +% my $row = 0; +% foreach my $num (sort keys %report_class) { + <TR CLASS="row<%$row % 2%>"> + <TD> +% if ( defined $initial_state{$num} ) { + <& /elements/checkbox.html, + field => 'report_option_'.$num, + value => 1, + curr_value => $initial_state{$num} + &> +% } else { +% # needs to be a tristate so that you can say "don't change it" + <& /elements/checkbox-tristate.html, field => 'report_option_'.$num &> +% } + </TD> + <TD><% $report_class{$num}->name %></TD> + </TR> +% $row++; +% } +</TABLE> +<BR> +<INPUT TYPE="submit"> +</FORM> +<& /elements/footer.html &> +<%init> +die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Bulk edit package definitions'); +my @pkgparts = $cgi->param('pkgpart') + or die "no package definitions selected"; + +my %part_pkg = map { $_ => FS::part_pkg->by_key($_) } @pkgparts; +my %part_pkg_option = map { $_ => { $part_pkg{$_}->options } } @pkgparts; +my %report_class = map { $_->num => $_ } + qsearch('part_pkg_report_option', { disabled => '' }); + +my %initial_state; +foreach my $num (keys %report_class) { + my $yes = 0; + my $no = 0; + foreach my $option (values %part_pkg_option) { + if ( $option->{"report_option_$num"} ) { + $yes = 1; + } else { + $no = 1; + } + } + if ( $yes and $no ) { + $initial_state{$num} = undef; + } elsif ( $yes ) { + $initial_state{$num} = 1; + } elsif ( $no ) { + $initial_state{$num} = 0; + } # else, uh, you didn't provide any pkgparts +} +</%init> diff --git a/httemplate/edit/credit-cust_bill_pkg.html b/httemplate/edit/credit-cust_bill_pkg.html index 3d1cf2438..a5ecb69e3 100644 --- a/httemplate/edit/credit-cust_bill_pkg.html +++ b/httemplate/edit/credit-cust_bill_pkg.html @@ -70,10 +70,11 @@ <TH ALIGN="right" COLSPAN=2>Total credit amount: </TD> <TH ALIGN="right" ID="total_td"><% $money_char %><% sprintf('%.2f', 0) %></TD> </TR> -<INPUT TYPE="hidden" NAME="amount" ID="total_el" VALUE="0.00"> </table> +<INPUT TYPE="hidden" NAME="amount" ID="total_el" VALUE="0.00"> + <table> <& /elements/tr-select-reason.html, @@ -244,7 +245,7 @@ function calc_total(what) { <%init> my $curuser = $FS::CurrentUser::CurrentUser; -die "access denied" unless $curuser->access_right('Post credit'); +die "access denied" unless $curuser->access_right('Credit line items'); #a tiny bit of false laziness w/search/cust_bill_pkg.cgi, but we're pretty # specialized and a piece of UI, not a report diff --git a/httemplate/edit/cust_location.cgi b/httemplate/edit/cust_location.cgi index 80b27c2b3..b90ba66b8 100755 --- a/httemplate/edit/cust_location.cgi +++ b/httemplate/edit/cust_location.cgi @@ -7,20 +7,32 @@ ACTION="<% $p %>edit/process/cust_location.cgi" METHOD=POST> <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> diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi index be00213e2..2908848c6 100755 --- a/httemplate/edit/cust_main.cgi +++ b/httemplate/edit/cust_main.cgi @@ -48,7 +48,7 @@ <TD STYLE="width:650px"> %#; padding-right:2px; vertical-align:top"> <FONT CLASS="fsinnerbox-title"><% mt('Billing address') |h %></FONT> - <TABLE CLASS="fsinnerbox"> + <TABLE CLASS="fsinnerbox" WIDTH="100%"> <& cust_main/before_bill_location.html, $cust_main &> <& /elements/location.html, object => $cust_main->bill_location, @@ -62,7 +62,6 @@ <TR><TD STYLE="height:40px"></TD></TR> <TR> <TD STYLE="width:650px"> -%#; padding-left:2px; vertical-align:top"> <FONT CLASS="fsinnerbox-title"><% mt('Service address') |h %></FONT> <INPUT TYPE="checkbox" NAME="same" @@ -72,19 +71,17 @@ VALUE="Y" <% $has_ship_address ? '' : 'CHECKED' %> ><% mt('same as billing address') |h %> - <TABLE CLASS="fsinnerbox" ID="table_ship_location"> - <& /elements/location.html, - object => $cust_main->ship_location, - prefix => 'ship_', - enable_censustract => 1, - enable_district => 1, - enable_coords => 1, - &> - </TABLE> - <TABLE CLASS="fsinnerbox" ID="table_ship_location_blank" - STYLE="display:none"> - <TR><TD></TD></TR> - </TABLE> + <DIV CLASS="fsinnerbox"> + <TABLE ID="table_ship_location" WIDTH="100%"> + <& /elements/location.html, + object => $cust_main->ship_location, + prefix => 'ship_', + enable_censustract => 1, + enable_district => 1, + enable_coords => 1, + &> + </TABLE> + </DIV> </TD> </TR></TABLE> @@ -94,19 +91,14 @@ function samechanged(what) { %# document.getElementById('table_ship_location').style.visibility = %# what.checked ? 'hidden' : 'visible'; var t1 = document.getElementById('table_ship_location'); - var t2 = document.getElementById('table_ship_location_blank'); if ( what.checked ) { - t2.style.width = t1.clientWidth + 'px'; - t2.style.height = t1.clientHeight + 'px'; - t1.style.display = 'none'; - t2.style.display = ''; + t1.style.visibility = 'hidden'; } else { - t2.style.display = 'none'; - t1.style.display = ''; + t1.style.visibility = 'visible' } } -samechanged(document.getElementById('same')); +//samechanged(document.getElementById('same')); </SCRIPT> <BR> @@ -285,7 +277,8 @@ if ( $cgi->param('error') ) { my( $query ) = $cgi->keywords; $query =~ /^(\d+)$/; $custnum=$1; - $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ); + $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or die "custnum $custnum not found"; if ( $cust_main->dbdef_table->column('paycvv') && length($cust_main->paycvv) ) { my $paycvv = $cust_main->paycvv; diff --git a/httemplate/edit/cust_main/billing.html b/httemplate/edit/cust_main/billing.html index 2925ca87c..5a66f0a60 100644 --- a/httemplate/edit/cust_main/billing.html +++ b/httemplate/edit/cust_main/billing.html @@ -444,10 +444,11 @@ <TR><TD> </TD></TR> +% my $curuser = $FS::CurrentUser::CurrentUser; % my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups'); - % if ( $conf->exists('cust_class-tax_exempt') % || $conf->exists('tax-cust_exempt-groups-require_individual_nums') +% || ! $curuser->access_right('Edit customer tax exemptions') % ) % { @@ -461,14 +462,16 @@ % } -% foreach my $exempt_group ( @exempt_groups ) { -% my $cust_main_exemption = $cust_main->tax_exemption($exempt_group); -% #escape $exempt_group for NAME etc. -% my $checked = ($cust_main_exemption || $cgi->param("tax_$exempt_group")); - <TR> - <TD> <INPUT TYPE="checkbox" NAME="tax_<% $exempt_group %>" ID="tax_<% $exempt_group %>" VALUE="Y" <% $checked ? 'CHECKED' : '' %> onChange="tax_changed(this)"> Tax Exempt (<% $exempt_group %> taxes)</TD> - <TD> - Exemption number <INPUT TYPE="text" NAME="tax_<% $exempt_group %>_num" ID="tax_<% $exempt_group %>_num" VALUE="<% $cgi->param("tax_$exempt_group".'_num') || ( $cust_main_exemption ? $cust_main_exemption->exempt_number : '' ) |h %>" <% $checked ? '' : 'DISABLED' %>></TD> - </TR> +% if ( $curuser->access_right('Edit customer tax exemptions') ) { +% foreach my $exempt_group ( @exempt_groups ) { +% my $cust_main_exemption = $cust_main->tax_exemption($exempt_group); +% #escape $exempt_group for NAME etc. +% my $checked = ($cust_main_exemption || $cgi->param("tax_$exempt_group")); + <TR> + <TD> <INPUT TYPE="checkbox" NAME="tax_<% $exempt_group %>" ID="tax_<% $exempt_group %>" VALUE="Y" <% $checked ? 'CHECKED' : '' %> onChange="tax_changed(this)"> Tax Exempt (<% $exempt_group %> taxes)</TD> + <TD> - Exemption number <INPUT TYPE="text" NAME="tax_<% $exempt_group %>_num" ID="tax_<% $exempt_group %>_num" VALUE="<% $cgi->param("tax_$exempt_group".'_num') || ( $cust_main_exemption ? $cust_main_exemption->exempt_number : '' ) |h %>" <% $checked ? '' : 'DISABLED' %>></TD> + </TR> +% } % } % unless ( $conf->exists('emailinvoiceonly') ) { @@ -518,7 +521,13 @@ <% $conf->exists('cust_main-require_invoicing_list_email', $agentnum) ? $r : '' %>Email address(es) </TD> - <TD WIDTH="408"><INPUT TYPE="text" NAME="invoicing_list" VALUE="<% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) %>"></TD> + <TD WIDTH="408"><INPUT TYPE="text" NAME="invoicing_list" VALUE="<% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) %>"> + <INPUT TYPE="checkbox" NAME="message_noemail" VALUE="Y" <% + ( $cust_main->message_noemail eq 'Y' ) + ? 'CHECKED' + : '' + %>> <% emt('Do not send notices') %> + </TD> </TR> % } diff --git a/httemplate/edit/cust_main/bottomfixup.js b/httemplate/edit/cust_main/bottomfixup.js index 1cfa52d8f..0de6d9dab 100644 --- a/httemplate/edit/cust_main/bottomfixup.js +++ b/httemplate/edit/cust_main/bottomfixup.js @@ -70,8 +70,8 @@ function copy_payby_fields() { <& /elements/standardize_locations.js, 'callback' => 'submit_continue();', - 'main_prefix' => 'bill_', - 'no_company' => 1, + 'billship' => 1, + 'with_census' => 1, # no with_firm, apparently &> function copyelement(from, to) { diff --git a/httemplate/edit/cust_main/choose_tax_location.html b/httemplate/edit/cust_main/choose_tax_location.html deleted file mode 100644 index ac475c54b..000000000 --- a/httemplate/edit/cust_main/choose_tax_location.html +++ /dev/null @@ -1,87 +0,0 @@ -<FORM NAME="choosegeocodeform"> -<CENTER><BR><B>Choose tax location</B><BR><BR> -<P>the geocode is:<% $header %></P> -<P STYLE="<% $style %>"><% $header %></P> - -<SELECT NAME='geocodes' ID='geocodes' STYLE="<% $style %>"> -% foreach my $location (@cust_tax_location) { -% my %value = ( zip => $zip5, -% map { $_ => $location->$_ } -% qw ( city state geocode ) -% ); -% map { $value{$_} = $location{$_} } qw ( city state ) -% if $location{country} eq 'CA'; -% -% my $value = encode_entities(objToJson({ %value }) -% ); -% my $content = ''; -% $content .= $location->$_. ' ' x ( $max{$_} - length($location->$_) ) -% foreach qw( city county state ); -% $content .= $location->cityflag eq 'I' ? 'Y' : 'N' ; -% my $selected = '' ; -% if ($geocode && $location->geocode eq $geocode) { -% $selected = 'SELECTED'; -% } - <OPTION VALUE="<% $value %>" STYLE="<% $style %>" <% $selected %>><% $content %> -% } -</SELECT><BR><BR> - -<TABLE><TR> - <TD> <BUTTON TYPE="button" onClick="set_geocode(document.getElementById('geocodes'));"><IMG SRC="<%$p%>images/tick.png" ALT=""> Set location </BUTTON></TD> - <TD><BUTTON TYPE="button" onClick="document.CustomerForm.submitButton.disabled=false; parent.cClick();"><IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel submission </BUTTON></TD> -</TR> -</TABLE> - -</CENTER> -</FORM> -<%init> - -my $conf = new FS::Conf; - -my %location = (); - -($location{data_vendor}) = $cgi->param('data_vendor') =~ /^([-\w]+)$/; -($location{city}) = $cgi->param('city') =~ /^([\w ]+)$/; -($location{state}) = $cgi->param('state') =~ /^(\w+)$/; -($location{zip}) = $cgi->param('zip') =~ /^([-\w ]+)$/; -($location{country}) = $cgi->param('country') =~ /^([\w ]+)$/; - -my($geocode) = $cgi->param('geocode') =~ /^([\w]+)$/; - -my($zip5, $zip4) = split('-', $location{zip}); - -#only support US & CA -my $hashref = { 'data_vendor' => $location{data_vendor} }; -$hashref->{zip} = $location{country} eq 'CA' ? substr($zip5,0,1) : $zip5, - -my @keys = keys(%$hashref); -my @cust_tax_location = (); -until ( @cust_tax_location ) { - @cust_tax_location = qsearch({ table => 'cust_tax_location', - hashref => $hashref, - order_by => 'LIMIT 50', - }); - last unless scalar(@keys); - delete $hashref->{ shift @keys }; -} - -my %max = ( city => 4, county => 6, state => 5); -foreach my $location (@cust_tax_location) { - foreach ( qw( city county state ) ) { - my $length = length($location->$_); - $max{$_} = ($length > $max{$_}) ? $length : $max{$_}; - } -} -foreach ( qw( city county state ) ) { - $max{$_} = $location{$_} if $location{$_} > $max{$_}; - $max{$_}++; -} - -my $header = ' '; -$header .= $_. ' ' x ( $max{lc($_)} - length($_) ) - foreach qw( City County State ); -$header .= "In city?"; - -my $style = "font-family:monospace;"; - -</%init> diff --git a/httemplate/edit/cust_main/top_misc.html b/httemplate/edit/cust_main/top_misc.html index cfed8e4f6..b7e86ba78 100644 --- a/httemplate/edit/cust_main/top_misc.html +++ b/httemplate/edit/cust_main/top_misc.html @@ -32,6 +32,44 @@ document.getElementById('contacts_div').style.display = 'none'; } } + + var ship_locked_agents = <% encode_json(\%ship_locked_agents) %>; + var ship_fields = ['address1', 'city', 'state', 'zip', 'country', + 'latitude', 'longitude', 'district']; + function agent_changed(what) { + var agentnum = what.value; + var f = what.form; + if ( ship_locked_agents[agentnum] ) { +% # For this agent, the service location (except address2) +% # should be locked to the agent's location. +% # Set the ship_ fields to those values (just for display) and +% # then disable them. + for(var x in ship_locked_agents[agentnum]) { + f['ship_'+x].value = ship_locked_agents[agentnum][x]; + f['ship_'+x].disabled = true; + } + f['same'].checked = false; + f['same'].disabled = true; + } else { +% # Unlock the ship_ location fields. If they were previously +% # disabled, then they contain some agent's address, which is +% # no longer meaningful. So set them back to the customer's +% # current location. + for(var i=0; i<ship_fields.length; i++) { + x = ship_fields[i]; + if ( f['ship_'+x].disabled ) { + f['ship_'+x].value = f['old_ship_'+x].value; + } + f['ship_'+x].disabled = false; + } + f['same'].disabled = false; + } + samechanged(f['same']); + } + window.onload = function() { + agent_changed(document.getElementById('agentnum')); + } + </SCRIPT> % foreach my $field ($cust_main->virtual_fields) { @@ -51,12 +89,13 @@ % $cust_main->agentnum($agentnum); <INPUT TYPE="hidden" NAME="lock_agentnum" VALUE="<% $agentnum %>"> - <INPUT TYPE="hidden" NAME="agentnum" VALUE="<% $agentnum %>"> + <INPUT TYPE="hidden" NAME="agentnum" ID="agentnum" + VALUE="<% $agentnum %>"> <TR> <TD ALIGN="right"><% mt('Agent') |h %></TD> <TD CLASS="fsdisabled"><% $cust_main->agent->agent |h %></TD> </TR> - + % } else { <& /elements/tr-select-agent.html, @@ -65,6 +104,7 @@ 'empty_label' => emt('Select agent'), 'disable_empty' => ( $cust_main->agentnum ? 1 : 0 ), 'viewall_right' => emt('None'), + 'onchange' => 'agent_changed(this)', &> % } @@ -201,4 +241,17 @@ my $curuser = $FS::CurrentUser::CurrentUser; my $r = qq!<font color="#ff0000">*</font> !; +# which agents lock the service address, if any +my %ship_locked_agents; +foreach (qsearch('agent',{})) { + my $agentnum = $_->agentnum; + next unless $conf->exists('agent-ship_address', $_->agentnum); + my $cust_main = $_->agent_cust_main or next; + my $agent_ship_location = $cust_main->ship_location; + $ship_locked_agents{$agentnum} = +{ + map { $_ => $agent_ship_location->$_ } + qw(address1 city state zip country latitude longitude district) + }; +} + </%init> diff --git a/httemplate/edit/cust_pkg.cgi b/httemplate/edit/cust_pkg.cgi index dd1ed335f..88e925460 100755 --- a/httemplate/edit/cust_pkg.cgi +++ b/httemplate/edit/cust_pkg.cgi @@ -7,7 +7,6 @@ <INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>"> %#current packages -%my @cust_pkg = qsearch('cust_pkg', { 'custnum' => $custnum, 'cancel' => '' } ); %if (@cust_pkg) { Current packages - select to remove (services are moved to a new package below) @@ -18,13 +17,7 @@ </TR> <BR><BR> % -% -% foreach ( sort { $all_pkg{ $a->getfield('pkgpart') } -% cmp $all_pkg{ $b->getfield('pkgpart') } -% } -% @cust_pkg -% ) -% { +% foreach ( @main_pkgs ) { % my($pkgnum,$pkgpart)=( $_->getfield('pkgnum'), $_->getfield('pkgpart') ); % my $checked = $remove_pkg{$pkgnum} ? ' CHECKED' : ''; % @@ -36,6 +29,13 @@ <TD ALIGN="right"><% $pkgnum %>:</TD> <TD><% $all_pkg{$pkgpart} %> - <% $all_comment{$pkgpart} %></TD> </TR> +% foreach my $supp_pkg ( @{ $supp_pkgs_of{$pkgnum} } ) { + <TR> + <TD></TD> + <TD></TD> + <TD>+ <% $all_pkg{$supp_pkg->pkgpart} %> - <% $all_comment{$supp_pkg->pkgpart} %></TD> + </TR> +% } % } @@ -147,4 +147,24 @@ if ( $cgi->param('error') ) { my $p1 = popurl(1); +my @cust_pkg = qsearch('cust_pkg', { 'custnum' => $custnum, 'cancel' => '' } ); +my @main_pkgs; +my %supp_pkgs_of; # main pkgnum => arrayref of cust_pkgs + + +foreach my $cust_pkg + ( sort { $all_pkg{ $a->pkgpart } cmp $all_pkg{ $b->getfield('pkgpart') } } + @cust_pkg + ) + # XXX does not properly handle recursive supplemental links +{ + if ( my $main_pkgnum = $cust_pkg->main_pkgnum ) { + $supp_pkgs_of{$main_pkgnum} ||= []; + push @{ $supp_pkgs_of{$main_pkgnum} }, $cust_pkg; + } else { + push @main_pkgs, $cust_pkg; + $supp_pkgs_of{$cust_pkg->pkgnum} ||= []; + } +} + </%init> diff --git a/httemplate/edit/cust_pkg_detail.html b/httemplate/edit/cust_pkg_detail.html index 009ed5c6e..5e107066d 100644 --- a/httemplate/edit/cust_pkg_detail.html +++ b/httemplate/edit/cust_pkg_detail.html @@ -28,7 +28,7 @@ <TR> <TD ALIGN="right">Comment</TD> - <TD BGCOLOR="#ffffff"><% $part_pkg->comment %></TD> + <TD BGCOLOR="#ffffff"><% $part_pkg->comment |h %></TD> </TR> <TR> diff --git a/httemplate/edit/cust_pkg_quantity.html b/httemplate/edit/cust_pkg_quantity.html new file mode 100755 index 000000000..ec47ed6cb --- /dev/null +++ b/httemplate/edit/cust_pkg_quantity.html @@ -0,0 +1,49 @@ +<& /elements/header-popup.html, "Change Quantity" &> +<& /elements/error.html &> + +<FORM ACTION="<% $p %>edit/process/cust_pkg_quantity.html" METHOD=POST> +<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>"> +<& /elements/table-grid.html, 'bgcolor' => '#cccccc', 'cellpadding' => 2 &> + + <TR> + <TH ALIGN="right">Current package </TH> + <TD CLASS="grid"> + <% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B> - <% $part_pkg->comment |h %> + </TD> + </TR> + +<& /elements/tr-input-text.html, + 'field' => 'quantity', + 'curr_value' => $cust_pkg->quantity, + 'label' => emt('Quantity') +&> + +</TABLE> + +<BR> +<INPUT NAME="submit" TYPE="submit" VALUE="Change"> + +</FORM> +</BODY> +</HTML> + +<%init> + +#some false laziness w/misc/change_pkg.cgi + +my $conf = new FS::Conf; + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('Change customer package'); + +my $pkgnum = scalar($cgi->param('pkgnum')); +$pkgnum =~ /^(\d+)$/ or die "illegal pkgnum $pkgnum"; +$pkgnum = $1; + +my $cust_pkg = FS::cust_pkg->by_key($pkgnum) or die "unknown pkgnum $pkgnum"; + +my $part_pkg = $cust_pkg->part_pkg; + +</%init> diff --git a/httemplate/edit/cust_refund.cgi b/httemplate/edit/cust_refund.cgi index 656d5ebb5..df42e63ae 100755 --- a/httemplate/edit/cust_refund.cgi +++ b/httemplate/edit/cust_refund.cgi @@ -59,12 +59,12 @@ </TD> </TR> % } - +% if ( $cust_pay->processor ) { <TR> <TD ALIGN="right">Processor</TD> <TD BGCOLOR="#ffffff"><% $cust_pay->processor %></TD> </TR> -% if ( length($auth) ) { +% if ( length($cust_pay->auth) ) { <TR> <TD ALIGN="right">Authorization</TD> @@ -78,10 +78,10 @@ <TD BGCOLOR="#ffffff"><% $cust_pay->order_number %></TD> </TR> % } -% } #if $cust_pay +% } # if ($cust_pay->processor) </TABLE> -% } +% } #if $cust_pay <BR>Refund diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html index a24f23805..3e6bd5bec 100644 --- a/httemplate/edit/elements/edit.html +++ b/httemplate/edit/elements/edit.html @@ -329,6 +329,7 @@ Example: % qw( country ), #select-country % qw( width height ), #htmlarea % qw( alt_format ), #select-cust_location +% qw( classnum ), # select-inventory_item % ; % % #select-table diff --git a/httemplate/edit/elements/part_svc_column.html b/httemplate/edit/elements/part_svc_column.html new file mode 100644 index 000000000..d03c49d2f --- /dev/null +++ b/httemplate/edit/elements/part_svc_column.html @@ -0,0 +1,303 @@ +<%doc> +To be called from part_svc.cgi. +<& elements/part_svc_column.html, + 'svc_acct', + # options... + 'part_svc' => $part_svc, # the existing part_svc to edit + 'clone' => 0, # or a svcpart to clone from +&> + +</%doc> +<%once> +# the semantics of this could be better + +# all of these conditions are when NOT to allow that flag choice +# don't allow the 'inventory' flags (M, A) to be chosen for +# fields that aren't free-text +my $inv_sub = sub { $_[0]->{disable_inventory} || $_[0]->{type} ne 'text' }; +tie my %flag, 'Tie::IxHash', + '' => { 'desc' => 'No default', 'condition' => sub { 0 } }, + 'D' => { 'desc' => 'Default', + 'condition' => + sub { $_[0]->{disable_default } } + }, + 'F' => { 'desc' => 'Fixed (unchangeable)', + 'condition' => + sub { $_[0]->{disable_fixed} }, + }, + 'S' => { 'desc' => 'Selectable Choice', + 'condition' => + sub { $_[0]->{disable_select} }, + }, + 'M' => { 'desc' => 'Manual selection from inventory', + 'condition' => $inv_sub, + }, + 'A' => { 'desc' => 'Automatically fill in from inventory', + 'condition' => $inv_sub, + }, + 'H' => { 'desc' => 'Select from hardware class', + 'condition' => sub { $_[0]->{type} ne 'select-hardware' }, + }, + 'X' => { 'desc' => 'Excluded', + 'condition' => sub { 1 }, # obsolete + }, +; + +# the semantics of this could be much better +sub flag_condition { + my $f = shift; + not &{ $flag{$f}->{'condition'} }(@_); +} + +my %communigate_fields = ( + 'svc_acct' => { map { $_=>1 } + qw( file_quota file_maxnum file_maxsize + password_selfchange password_recover + ), + grep /^cgp_/, fields('svc_acct') + }, + 'svc_domain' => { map { $_=>1 } + qw( max_accounts trailer parent_svcnum ), + grep /^(cgp|acct_def)_/, fields('svc_domain') + }, +); +</%once> +<INPUT TYPE="hidden" NAME="svcdb" VALUE="<% $svcdb %>"> +<BR><BR> +<& /elements/table.html &> + <TR><TH COLSPAN=<% $columns %>>Exports</TH></TR> + <TR> +% # exports +% foreach my $part_export (@part_export) { + <TD> + <INPUT TYPE="checkbox" \ + NAME="exportnum<% $part_export->exportnum %>" \ + VALUE=1 \ + <% $has_export_svc{$part_export->exportnum} ? 'CHECKED' : '' %>> + <% $part_export->label_html %> + </TD> +% $count++; +% if ( $count % $columns == 0 ) { + </TR> + <TR> +% } +% } + </TR> +</TABLE><BR><BR> +For the selected table, you can give fields default or fixed (unchangeable) +values, or select an inventory class to manually or automatically fill in +that field. +<& /elements/table-grid.html, cellpadding => 4 &> + <TR> + <TH BGCOLOR="#cccccc">Field</TH> + <TH BGCOLOR="#cccccc">Label</TH> + <TH BGCOLOR="#cccccc" COLSPAN=2>Modifier</TH> + </TR> +% $part_svc->set('svcpart' => $opt{'clone'}) if $opt{'clone'}; # for now +% my $i = 0; +% foreach my $field (@fields) { +% my $def = shift @defs; +% my $part_svc_column = $part_svc->part_svc_column($field); +% my $flag = $part_svc_column->columnflag; +% my $formatter = $def->{'format'} || sub { shift }; +% my $value = &{$formatter}($part_svc_column->columnvalue); + <TR CLASS="row<%$i%>"> + <TD ROWSPAN=2 CLASS="grid" ALIGN="right"> + <% $def->{'label'} || $field %> + </TD> + <TD ROWSPAN=2 CLASS="grid"> + <INPUT NAME="<% $svcdb %>__<% $field %>_label" + STYLE="text-align: right" + VALUE="<% $part_svc_column->columnlabel || $def->{'label'} |h %>"> + </TD> + + <TD ROWSPAN=1 CLASS="grid"> +% # flag selection +% if ( $def->{'type'} eq 'disabled' ) { +% $flag = ''; + No default +% } else { +% my $name = $svcdb.'__'.$field.'_flag'; + <SELECT NAME="<%$name%>" + ID="<%$name%>" + STYLE="width:100%" + onchange="flag_changed(this)"> +% foreach my $f (keys %flag) { +% if ( flag_condition($f, $def, $svcdb, $field) ) { + <OPTION VALUE="<%$f%>"<% $flag eq $f ? ' SELECTED' : ''%>> + <% $flag{$f}->{desc} %> + </OPTION> +% } +% } + </SELECT> +% } # if $def->{'type'} eq 'disabled' + </TD> + <TD CLASS="grid"> +% # value entry/selection +% my $name = $svcdb.'__'.$field; +% # These are all MANDATORY SELECT types. Regardless of the flag value, +% # there will never be a text input (either in svc_* or in part_svc) for +% # these fields. +% if ( $def->{'type'} eq 'checkbox' ) { + <& /elements/checkbox.html, + 'field' => $name, + 'curr_value' => $value, + 'value' => 'Y' &> +% +% } elsif ( $def->{'type'} eq 'select' ) { +% +% if ( $def->{'select_table'} ) { + <& /elements/select-table.html, + 'field' => $name, + 'id' => $name.'_select', + 'table' => $def->{'select_table'}, + 'name_col' => $def->{'select_label'}, + 'value_col' => $def->{'select_key'}, + 'order_by' => dbdef->table($def->{'select_table'})->primary_key, + 'multiple' => $def->{'multiple'}, + 'disable_empty' => 1, + 'curr_value' => $value, + &> +% } else { +% my (@options, %labels); +% if ( $def->{'select_list'} ) { +% @options = @{ $def->{'select_list'} }; +% @labels{@options} = @options; +% } elsif ( $def->{'select_hash'} ) { +% if ( ref($def->{'select_hash'}) eq 'ARRAY' ) { +% tie my %hash, 'Tie::IxHash', @{ $def->{'select_hash'} }; +% $def->{'select_hash'} = \%hash; +% } +% @options = keys( %{ $def->{'select_hash'} } ); +% %labels = %{ $def->{'select_hash'} }; +% } + <& /elements/select.html, + 'field' => $name, + 'id' => $name.'_select', + 'options' => \@options, + 'labels' => \%labels, + 'multiple' => $def->{'multiple'}, + 'curr_value' => $value, + &> +% } +% } elsif ( $def->{'type'} =~ /select-(.*?).html/ ) { + <& '/elements/'.$def->{'type'}, + 'field' => $name, + 'id' => $name.'_select', + 'multiple' => $def->{'multiple'}, + 'curr_value' => $value, + &> +% } elsif ( $def->{'type'} eq 'communigate_pro-accessmodes' ) { + <& /elements/communigate_pro-accessmodes.html, + 'element_name_prefix' => $name.'_', + 'curr_value' => $value, + &> +% } elsif ( $def->{'type'} eq 'textarea' ) { +% # special cases + <TEXTAREA NAME="<%$name%>"><% $value |h %></TEXTAREA> +% } elsif ( $def->{'type'} eq 'disabled' ) { + <INPUT TYPE="hidden" NAME="<%$name%>" VALUE=""> +% } else { +% # the normal case: a text input, and a _select which is an inventory +% # or hardware class + <INPUT TYPE="text" + NAME="<%$name%>" + ID="<%$name%>" + VALUE="<%$value%>"> +% # inventory class selection + <& /elements/select-table.html, + 'field' => $name.'_classnum', + 'id' => $name.'_select', + 'table' => 'inventory_class', + 'name_col' => 'classname', + 'curr_value' => $value, + 'empty_label' => 'Select inventory class', + 'multiple' => 1, + &> +% } + </TD> + </TR> + <TR CLASS="row<%$i%>"> + <TD COLSPAN=2 CLASS="def_info"> +% if ( $def->{def_info} ) { + (<% $def->{def_info} %>) + </TD> + </TR> +% } +% $i = 1-$i; +% } # foreach my $field +% +% # special case: svc_acct password edit ACL +% if ( $svcdb eq 'svc_acct' ) { +% push @fields, 'restrict_edit_password'; + <TR> + <TD COLSPAN=3 ALIGN="right"> + <% emt('Require "Provision" access right to edit password') %> + </TD> + <TD> + <INPUT TYPE="checkbox" NAME="restrict_edit_password" VALUE="Y" \ + <% $part_svc->restrict_edit_password ? 'CHECKED' : '' %>> + </TD> + </TR> +% } +</TABLE> +<& /elements/progress-init.html, + $svcdb, #form name + [ # form fields to send + qw(svc svcpart classnum selfservice_access disabled preserve exportnum), + @fields + ], + 'process/part_svc.cgi', # target + $p.'browse/part_svc.cgi', # redirect landing + $svcdb, #key +&> +% $svcpart = '' if $opt{clone}; +<BR> +<INPUT NAME="submit" + TYPE="button" + VALUE="<% emt($svcpart ? 'Apply changes' : 'Add service') %>" + onclick="fixup_submit('<%$svcdb%>')" +> +<%init> +my $svcdb = shift; +my %opt = @_; +my $columns = 3; +my $count = 0; +my $communigate = 0; +my $conf = FS::Conf->new; + +my $part_svc = $opt{'part_svc'} || FS::part_svc->new; + +my @part_export; +my $export_info = FS::part_export::export_info($svcdb); +foreach (keys %{ $export_info }) { + push @part_export, qsearch('part_export', { exporttype => $_ }); +} +$communigate = scalar(grep {$_->exporttype =~ /^communigate/} @part_export); + +my $svcpart = $opt{'clone'} || $part_svc->svcpart; +my %has_export_svc; +if ( $svcpart ) { + foreach (qsearch('export_svc', { svcpart => $svcpart })) { + $has_export_svc{$_->exportnum} = 1; + } +} + +my @fields; +if ( defined( dbdef->table($svcdb) ) ) { # when is it ever not defined? + @fields = grep { + $_ ne 'svcnum' + and ( $communigate || ! $communigate_fields{$svcdb}->{$_} ) + and ( !FS::part_svc->svc_table_fields($svcdb)->{$_}->{disable_part_svc_column} + || $part_svc->part_svc_column($_)->columnflag ) + } fields($svcdb); +} +if ( $svcdb eq 'svc_acct' + or ( $svcdb eq 'svc_broadband' and $conf->exists('svc_broadband-radius') ) + ) +{ + push @fields, 'usergroup'; +} + +my @defs = map { FS::part_svc->svc_table_fields($svcdb)->{$_} } @fields; +</%init> diff --git a/httemplate/edit/elements/svc_Common.html b/httemplate/edit/elements/svc_Common.html index 0d9d36c07..d46d1cb42 100644 --- a/httemplate/edit/elements/svc_Common.html +++ b/httemplate/edit/elements/svc_Common.html @@ -88,30 +88,13 @@ } elsif ( $flag eq 'A' ) { $f->{'type'} = 'hidden'; } elsif ( $flag eq 'M' ) { + $f->{'type'} = 'select-inventory_item'; $f->{'empty_label'} = 'Select inventory item'; - $f->{'type'} = 'select-table'; - $f->{'table'} = 'inventory_item'; - $f->{'name_col'} = 'item'; - $f->{'value_col'} = 'item'; - $f->{'agent_virt'} = 1; - $f->{'agent_null'} = 1; - $f->{'hashref'} = { - 'classnum'=>$columndef->columnvalue, - #'svcnum' => '', - }; - $f->{'extra_sql'} = 'AND ( svcnum IS NULL '; - $f->{'extra_sql'} .= ' OR svcnum = '. $object->svcnum - if $object->svcnum; - $f->{'extra_sql'} .= ' ) '; + $f->{'extra_sql'} = 'WHERE ( svcnum IS NULL ' . + ($object->svcnum && ' OR svcnum = '.$object->svcnum) . + ')'; + $f->{'classnum'} = $columndef->columnvalue; $f->{'disable_empty'} = $object->svcnum ? 1 : 0; - if ( $f->{'field'} eq 'mac_addr' ) { - $f->{'compare_sub'} = sub { - my($a, $b) = @_; - $a =~ s/[-: ]//g; - $b =~ s/[-: ]//g; - lc($a) eq lc($b); - }; - } } elsif ( $flag eq 'H' ) { $f->{'type'} = 'select-hardware_type'; $f->{'hashref'} = { diff --git a/httemplate/edit/part_export.cgi b/httemplate/edit/part_export.cgi index 4dd253be8..2897cf39d 100644 --- a/httemplate/edit/part_export.cgi +++ b/httemplate/edit/part_export.cgi @@ -2,6 +2,34 @@ <% 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 %>"> @@ -58,7 +86,6 @@ my $widget = new HTML::Widgets::SelectLayers( '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; @@ -87,7 +114,8 @@ my $widget = new HTML::Widgets::SelectLayers( 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' ) @@ -97,38 +125,43 @@ my $widget = new HTML::Widgets::SelectLayers( $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">'; diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi index c3f4f88b6..fadde354e 100755 --- a/httemplate/edit/part_pkg.cgi +++ b/httemplate/edit/part_pkg.cgi @@ -28,7 +28,8 @@ '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', @@ -53,6 +54,7 @@ 'discountnum' => 'Offer discounts for longer terms', 'bill_dst_pkgpart' => 'Include line item(s) from package', 'svc_dst_pkgpart' => 'Include services of package', + 'supp_dst_pkgpart' => 'Include complete package', 'report_option' => 'Report classes', 'fcc_ds0s' => 'Voice-grade equivalents', 'fcc_voip_class' => 'Category', @@ -79,6 +81,7 @@ size => 40, #32 maxlength => 50, }, + #@locale_fields, {field=>'comment', type=>'text', size=>40 }, #32 { field => 'agentnum', type => 'select-agent', @@ -239,6 +242,19 @@ }, { 'type' => 'tablebreak-tr-title', + 'value' => 'Supplemental packages', + 'colspan' => '4', + }, + { 'field' => 'supp_dst_pkgpart', + 'type' => 'select-part_pkg', + 'm2_label' => 'Include complete package', + 'm2m_method' => 'supp_part_pkg_link', + 'm2m_dstcol' => 'dst_pkgpart', + 'm2_error_callback' => + &{$m2_error_callback_maker}('supp'), + }, + + { 'type' => 'tablebreak-tr-title', 'value' => 'Pricing add-ons', 'colspan' => 4, }, @@ -323,6 +339,22 @@ my $agent_clone_extra_sql = 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; @@ -354,6 +386,42 @@ my $recur_show_zero_disabled = 1; 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 ) = @_; @@ -394,6 +462,16 @@ my $error_callback = sub { $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' }; }; @@ -459,6 +537,20 @@ my $edit_callback = sub { $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 { @@ -473,6 +565,8 @@ my $new_callback = sub { $options{'suspend_bill'}=1 if $conf->exists('part_pkg-default_suspend_bill'); + &$splice_locale_fields($fields, '', ''); + }; my $clone_callback = sub { @@ -506,6 +600,16 @@ 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 { diff --git a/httemplate/edit/part_svc.cgi b/httemplate/edit/part_svc.cgi index 007c24629..58c237efd 100755 --- a/httemplate/edit/part_svc.cgi +++ b/httemplate/edit/part_svc.cgi @@ -1,11 +1,111 @@ -<& /elements/header.html, "$action Service Definition", - menubar('View all service definitions' => "${p}browse/part_svc.cgi"), +<& /elements/header.html, "$action Service Definition" &> +<& /elements/menubar.html, + 'View all service definitions' => "${p}browse/part_svc.cgi" #" onLoad=\"visualize()\"" &> <& /elements/init_overlib.html &> -<BR> +<BR><BR> + +<STYLE TYPE="text/css"> +.disabled { + background-color: #dddddd; +} +.hidden { + display: none; +} +.enabled { + background-color: #ffffff; +} +.row0 TD { + background-color: #eeeeee; +} +.row1 TD { + background-color: #ffffff; +} +.def_info { + text-align: center; + padding: 0px; + border-top: none; + font-size: smaller; + font-style: italic; +} +</STYLE> +<SCRIPT TYPE="text/javascript"> +function fixup_submit(layer) { + document.forms[layer].submit.disabled = true; + fixup(document.forms[layer]); + window[layer+'process'].call(); +} + +function flag_changed(obj) { + var newflag = obj.value; + var a = obj.name.match(/(.*)__(.*)_flag/); + var layer = a[1]; + var field = a[2]; + var input = document.getElementById(layer + '__' + field); + // for fields that have both 'input' and 'select', 'select' is 'select from + // inventory class'. + var select = document.getElementById(layer + '__' + field + '_select'); + if (newflag == "" || newflag == "X") { // disable + if ( input ) { + input.disabled = true; + input.className = 'disabled'; + } + if ( select ) { + select.disabled = true; + select.className = 'hidden'; + } + } else if ( newflag == 'D' || newflag == 'F' || newflag == 'S' ) { + if ( input ) { + // enable text box, disable inventory select + input.disabled = false; + input.className = 'enabled'; + if ( select ) { + select.disabled = false; + select.className = 'hidden'; + } + } else if ( select ) { + // enable select + select.disabled = false; + select.className = 'enabled'; + if ( newflag == 'S' || select.getAttribute('should_be_multiple') ) { + select.multiple = true; + } else { + select.multiple = false; + } + } + } else if ( newflag == 'M' || newflag == 'A' || newflag == 'H' ) { + // these all require a class selection + if ( select ) { + select.disabled = false; + select.className = 'enabled'; + if ( input ) { + input.disabled = false; + input.className = 'hidden'; + } + } + } +} + +window.onload = function() { + var selects = document.getElementsByTagName('SELECT'); + for(i = 0; i < selects.length; i++) { + var obj = selects[i]; + if ( obj.multiple ) { + obj.setAttribute('should_be_multiple', true); + } + } + for(i = 0; i < selects.length; i++) { + var obj = selects[i]; + if ( obj.name.match(/_flag$/) ) { + flag_changed(obj); + } + } +}; + +</SCRIPT> <FORM NAME="dummy"> @@ -53,386 +153,6 @@ <BR> -% my %vfields; -% #code duplication w/ edit/part_svc.cgi, should move this hash to part_svc.pm -% # and generalize the subs -% # condition sub is tested to see whether to disable display of this choice -% # params: ( $def, $layer, $field ) (see SUB below) -% my $inv_sub = sub { -% $_[0]->{disable_inventory} -% || $_[0]->{'type'} ne 'text' -% }; -% tie my %flag, 'Tie::IxHash', -% '' => { 'desc' => 'No default', }, -% 'D' => { 'desc' => 'Default', -% 'condition' => -% sub { $_[0]->{disable_default} }, -% }, -% 'F' => { 'desc' => 'Fixed (unchangeable)', -% 'condition' => -% sub { $_[0]->{disable_fixed} }, -% }, -% 'S' => { 'desc' => 'Selectable Choice', -% 'condition' => -% sub { !ref($_[0]) || $_[0]->{disable_select} }, -% }, -% 'M' => { 'desc' => 'Manual selection from inventory', -% 'condition' => $inv_sub, -% }, -% 'A' => { 'desc' => 'Automatically fill in from inventory', -% 'condition' => $inv_sub, -% }, -% 'H' => { 'desc' => 'Select from hardware class', -% 'condition' => sub { $_[0]->{type} ne 'select-hardware' }, -% }, -% 'X' => { 'desc' => 'Excluded', -% 'condition' => -% sub { ! $vfields{$_[1]}->{$_[2]} }, -% -% }, -% ; -% -% my @dbs = $hashref->{svcdb} -% ? ( $hashref->{svcdb} ) -% : FS::part_svc->svc_tables(); -% -% my $help = ''; -% unless ( $hashref->{svcpart} ) { -% $help = ' '. -% include('/elements/popup_link.html', -% 'action' => $p.'docs/part_svc-table.html', -% 'label' => 'help', -% 'actionlabel' => 'Service table help', -% 'width' => 763, -% #'height' => 400, -% ); -% } -% -% tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } grep dbdef->table($_), @dbs; -% my $widget = new HTML::Widgets::SelectLayers( -% #'selected_layer' => $p_svcdb, -% 'selected_layer' => $hashref->{svcdb} || 'svc_acct', -% 'options' => \%svcdb, -% 'form_name' => 'dummy', -% #'form_action' => 'process/part_svc.cgi', -% 'form_action' => 'part_svc.cgi', #self -% 'form_elements' => [qw( svc svcpart classnum selfservice_access -% disabled preserve -% )], -% 'html_between' => $help, -% 'layer_callback' => sub { -% my $layer = shift; -% -% my $html = qq!<INPUT TYPE="hidden" NAME="svcdb" VALUE="$layer">!; -% -% #$html .= $svcdb_info; -% -% my $columns = 3; -% my $count = 0; -% my $communigate = 0; -% my @part_export = -% map { qsearch( 'part_export', {exporttype => $_ } ) } -% keys %{FS::part_export::export_info($layer)}; -% $html .= '<BR><BR>'. include('/elements/table.html') . -% "<TR><TH COLSPAN=$columns>Exports</TH></TR><TR>"; -% foreach my $part_export ( @part_export ) { -% $communigate++ if $part_export->exporttype =~ /^communigate/; -% $html .= '<TD><INPUT TYPE="checkbox"'. -% ' NAME="exportnum'. $part_export->exportnum. '" VALUE="1" '; -% $html .= 'CHECKED' -% if ( $clone || $part_svc->svcpart ) #null svcpart search causing error -% && qsearchs( 'export_svc', { -% exportnum => $part_export->exportnum, -% svcpart => $clone || $part_svc->svcpart }); -% $html .= '>'. $part_export->label_html. '</TD>'; -% $count++; -% $html .= '</TR><TR>' unless $count % $columns; -% } -% $html .= '</TR></TABLE><BR><BR>'. $mod_info; -% -% $html .= include('/elements/table-grid.html', 'cellpadding' => 4 ). -% '<TR>'. -% '<TH CLASS="grid" BGCOLOR="#cccccc">Field</TH>'. -% '<TH CLASS="grid" BGCOLOR="#cccccc">Label</TH>'. -% '<TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=2>Modifier</TH>'. -% '</TR>'; -% -% my $bgcolor1 = '#eeeeee'; -% my $bgcolor2 = '#ffffff'; -% my $bgcolor; -% -% #yucky kludge -% my @fields = (); -% if ( defined( dbdef->table($layer) ) ) { -% @fields = grep { -% $_ ne 'svcnum' -% && ( $communigate || !$communigate_fields{$layer}->{$_} ) -% && ( !FS::part_svc->svc_table_fields($layer) -% ->{$_}->{disable_part_svc_column} -% || $part_svc->part_svc_column($_)->columnflag -% ) -% } fields($layer); -% } -% push @fields, 'usergroup' -% if $layer eq 'svc_acct' -% or ( $layer eq 'svc_broadband' and -% $conf->exists('svc_broadband-radius') ); # double kludge -% # (but we do want to check the config, right?) -% $part_svc->svcpart($clone) if $clone; #haha, undone below -% -% -% foreach my $field (@fields) { -% -% #a few lines of false laziness w/browse/part_svc.cgi -% my $def = FS::part_svc->svc_table_fields($layer)->{$field}; -% my $def_info = $def->{'def_info'}; -% my $formatter = $def->{'format'} || sub { shift }; -% -% my $part_svc_column = $part_svc->part_svc_column($field); -% my $label = $part_svc_column->columnlabel || $def->{'label'}; -% my $value = &$formatter($part_svc_column->columnvalue); -% my $flag = $part_svc_column->columnflag; -% -% if ( $bgcolor eq $bgcolor1 ) { -% $bgcolor = $bgcolor2; -% } else { -% $bgcolor = $bgcolor1; -% } -% -% $html .= qq!<TR><TD ROWSPAN=2 CLASS="grid" BGCOLOR="$bgcolor" ALIGN="right">!. -% ( $def->{'label'} || $field ). -% "</TD>"; -% -% $html .= qq!<TD ROWSPAN=2 CLASS="grid" BGCOLOR="$bgcolor"><INPUT NAME="${layer}__${field}_label" VALUE="!. encode_entities($label). '" STYLE="text-align:right"></TD>'; -% -% $flag = '' if $def->{type} eq 'disabled'; -% -% $html .= qq!<TD CLASS="grid" BGCOLOR="$bgcolor">!; -% -% if ( $def->{type} eq 'disabled' ) { -% -% $html .= 'No default'; -% -% } else { -% -% $html .= qq!<SELECT NAME="${layer}__${field}_flag"!. -% qq! onChange="${layer}__${field}_flag_changed(this)">!; -% -% foreach my $f ( keys %flag ) { -% -% # need to template-ize more httemplate/edit/svc_* first -% next if $f eq 'M' and $layer !~ /^svc_(broadband|external|phone|dish)$/; -% -% #here is where the SUB from above is called, to skip some choices -% next if $flag{$f}->{condition} -% && &{ $flag{$f}->{condition} }( $def, $layer, $field ); -% -% $html .= qq!<OPTION VALUE="$f"!. -% ' SELECTED'x($flag eq $f ). -% '>'. $flag{$f}->{desc}; -% -% } -% -% $html .= '</SELECT>'; -% -% $html .= join("\n", -% '<SCRIPT>', -% " function ${layer}__${field}_flag_changed(what) {", -% ' var f = what.options[what.selectedIndex].value;', -% ' if ( f == "" || f == "X" ) { //disable', -% " what.form.${layer}__${field}.disabled = true;". -% " what.form.${layer}__${field}.style.backgroundColor = '#dddddd';". -% " if ( what.form.${layer}__${field}_classnum ) {". -% " what.form.${layer}__${field}_classnum.disabled = true;". -% " what.form.${layer}__${field}_classnum.style.backgroundColor = '#dddddd';". -% " }". -% ' } else if ( f == "D" || f == "F" || f =="S" ) { //enable, text box', -% " what.form.${layer}__${field}.disabled = false;". -% " what.form.${layer}__${field}.style.backgroundColor = '#ffffff';". -% " if ( f == 'S' || '${field}' == 'usergroup' ) {". # kludge -% " what.form.${layer}__${field}.multiple = true;". -% " } else {". -% " what.form.${layer}__${field}.multiple = false;". -% " }". -% " what.form.${layer}__${field}.style.display = '';". -% " if ( what.form.${layer}__${field}_classnum ) {". -% " what.form.${layer}__${field}_classnum.disabled = false;". -% " what.form.${layer}__${field}_classnum.style.backgroundColor = '#ffffff';". -% " what.form.${layer}__${field}_classnum.style.display = 'none';". -% " }". -% ' } else if ( f == "M" || f == "A" || f == "H" ) { '. -% '//enable, inventory', -% " what.form.${layer}__${field}.disabled = false;". -% " what.form.${layer}__${field}.style.backgroundColor = '#ffffff';". -% " what.form.${layer}__${field}.style.display = 'none';". -% " if ( what.form.${layer}__${field}_classnum ) {". -% " what.form.${layer}__${field}_classnum.disabled = false;". -% " what.form.${layer}__${field}_classnum.style.backgroundColor = '#ffffff';". -% " what.form.${layer}__${field}_classnum.style.display = '';". -% " }". -% ' }', -% ' }', -% '</SCRIPT>', -% ); -% -% } -% -% $html .= qq!</TD><TD CLASS="grid" BGCOLOR="$bgcolor">!; -% -% my $disabled = $flag ? '' -% : 'DISABLED STYLE="background-color: #dddddd"'; -% my $nodisplay = ' STYLE="display:none"'; -% -% if ( !$def->{type} || $def->{type} eq 'text' ) { -% -% my $is_inv = ( $flag =~ /^[MA]$/ ); -% -% $html .= -% qq!<INPUT TYPE="text" NAME="${layer}__${field}" VALUE="$value" !. -% $disabled. -% ( $is_inv ? $nodisplay : $disabled ). -% '>'; -% -% $html .= include('/elements/select-table.html', -% 'element_name' => "${layer}__${field}_classnum", -% 'id' => "${layer}__${field}_classnum", -% 'element_etc' => ( $is_inv -% ? $disabled -% : $nodisplay -% ), -% 'table' => 'inventory_class', -% 'name_col' => 'classname', -% 'value' => $value, -% 'empty_label' => 'Select inventory class', -% ); -% -% } elsif ( $def->{type} eq 'checkbox' ) { -% -% $html .= include('/elements/checkbox.html', -% 'field' => $layer.'__'.$field, -% 'curr_value' => $value, -% 'value' => 'Y', -% ); -% -% } elsif ( $def->{type} eq 'select' ) { -% -% $html .= qq!<SELECT NAME="${layer}__${field}" $disabled!; -% $html .= ' MULTIPLE' if $flag eq 'S'; -% $html .= '>'; -% $html .= '<OPTION> </OPTION>' unless $value; -% if ( $def->{select_table} ) { -% foreach my $record ( qsearch( $def->{select_table}, {} ) ) { -% my $rvalue = $record->getfield($def->{select_key}); -% my $select_label = $def->{select_label}; -% $html .= qq!<OPTION VALUE="$rvalue"!. -% (grep(/^$rvalue$/, split(',',$value)) ? ' SELECTED>' : '>' ). -% $record->$select_label(). '</OPTION>'; -% } #next $record -% } elsif ( $def->{select_list} ) { -% foreach my $item ( @{$def->{select_list}} ) { -% $html .= qq!<OPTION VALUE="$item"!. -% (grep(/^$item$/, split(',',$value)) ? ' SELECTED>' : '>' ). -% $item. '</OPTION>'; -% } #next $item -% } elsif ( $def->{select_hash} ) { -% if ( ref($def->{select_hash}) eq 'ARRAY' ) { -% tie my %hash, 'Tie::IxHash', @{ $def->{select_hash} }; -% $def->{select_hash} = \%hash; -% } -% foreach my $key ( keys %{$def->{select_hash}} ) { -% $html .= qq!<OPTION VALUE="$key"!. -% (grep(/^$key$/, split(',',$value)) ? ' SELECTED>' : '>' ). -% $def->{select_hash}{$key}. '</OPTION>'; -% } #next $key -% } #endif -% $html .= '</SELECT>'; -% -% } elsif ( $def->{type} eq 'textarea' ) { -% -% $html .= -% qq!<TEXTAREA NAME="${layer}__${field}">!. encode_entities($value). -% '</TEXTAREA>'; -% -% } elsif ( $def->{type} =~ /select-(.*?).html/ ) { -% -% $html .= include("/elements/".$def->{type}, -% 'curr_value' => $value, -% 'element_name' => "${layer}__${field}", -% 'element_etc' => $disabled, -% 'multiple' => ($def->{multiple} || -% $flag eq 'S'), -% # allow the table def to force 'multiple' -% ); -% -% } elsif ( $def->{type} eq 'communigate_pro-accessmodes' ) { -% -% $html .= include('/elements/communigate_pro-accessmodes.html', -% 'element_name_prefix' => "${layer}__${field}_", -% 'curr_value' => $value, -% #doesn't work#'element_etc' => $disabled, -% ); -% -% } elsif ( $def->{type} eq 'select-hardware' ) { -% -% $html .= qq!<INPUT TYPE="text" NAME="${layer}__${field}" $disabled>!; -% $html .= include('/elements/select-hardware_class.html', -% 'curr_value' => $value, -% 'element_name' => "${layer}__${field}_classnum", -% 'id' => "${layer}__${field}_classnum", -% 'element_etc' => $flag ne 'H' && $nodisplay, -% 'empty_label' => 'Select hardware class', -% ); -% -% } elsif ( $def->{type} eq 'disabled' ) { -% -% $html .= -% qq!<INPUT TYPE="hidden" NAME="${layer}__${field}" VALUE="">!; -% -% } else { -% -% $html .= '<font color="#ff0000">unknown type '. $def->{type}; -% -% } -% -% $html .= "</TD></TR>\n"; - -% $def_info = "($def_info)" if $def_info; -% $html .= -% qq!<TR>!. -% qq! <TD COLSPAN=2 BGCOLOR="$bgcolor" ALIGN="center" !. -% qq! STYLE="padding:0; border-top: none">!. -% qq! <FONT SIZE="-1"><I>$def_info</I></FONT>!. -% qq! </TD>!. -% qq!</TR>\n!; -% -% } #foreach my $field (@fields) { -% -% $part_svc->svcpart('') if $clone; #undone -% $html .= "</TABLE>"; -% -% $html .= include('/elements/progress-init.html', -% $layer, #form name -% [ qw(svc svcpart classnum selfservice_access -% disabled preserve -% exportnum), -% @fields ], -% 'process/part_svc.cgi', -% $p.'browse/part_svc.cgi', -% $layer, -% ); -% $html .= '<BR><INPUT NAME="submit" TYPE="button" VALUE="'. -% ($hashref->{svcpart} ? 'Apply changes' : 'Add service'). '" '. -% ' onClick="document.'. "$layer.submit.disabled=true; ". -% "fixup(document.$layer); $layer". 'process();">'; -% -% #$html .= '<BR><INPUT TYPE="submit" VALUE="'. -% # ($hashref->{svcpart} ? 'Apply changes' : 'Add service'). '">'; -% -% $html; -% -% }, -% ); - <BR> Table <% $widget->html %> @@ -465,28 +185,43 @@ my $action = $part_svc->svcpart ? 'Edit' : 'Add'; my $hashref = $part_svc->hashref; # my $p_svcdb = $part_svc->svcdb || 'svc_acct'; -my %communigate_fields = ( - 'svc_acct' => { map { $_=>1 } - qw( file_quota file_maxnum file_maxsize - password_selfchange password_recover - ), - grep /^cgp_/, fields('svc_acct') - }, - 'svc_domain' => { map { $_=>1 } - qw( max_accounts trailer parent_svcnum ), - grep /^(cgp|acct_def)_/, fields('svc_domain') - }, - #'svc_forward' => { map { $_=>1 } qw( ) }, - #'svc_mailinglist' => { map { $_=>1 } qw( ) }, - #'svc_cert' => { map { $_=>1 } qw( ) }, -); -my $mod_info = ' -For the selected table, you can give fields default or fixed (unchangable) -values, or select an inventory class to manually or automatically fill in -that field. -'; +my @dbs = $hashref->{svcdb} + ? ( $hashref->{svcdb} ) + : FS::part_svc->svc_tables(); + +my $help = ''; +unless ( $hashref->{svcpart} ) { + $help = ' '. + include('/elements/popup_link.html', + 'action' => $p.'docs/part_svc-table.html', + 'label' => 'help', + 'actionlabel' => 'Service table help', + 'width' => 763, + #'height' => 400, + ); +} +tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } grep dbdef->table($_), @dbs; +my $widget = new HTML::Widgets::SelectLayers( + #'selected_layer' => $p_svcdb, + 'selected_layer' => $hashref->{svcdb} || 'svc_acct', + 'options' => \%svcdb, + 'form_name' => 'dummy', + #'form_action' => 'process/part_svc.cgi', + 'form_action' => 'part_svc.cgi', #self + 'form_elements' => [qw( svc svcpart classnum selfservice_access + disabled preserve + )], + 'html_between' => $help, + 'layer_callback' => sub { + include('elements/part_svc_column.html', + shift, + 'part_svc' => $part_svc, + 'clone' => $clone + ) + } +); </%init> diff --git a/httemplate/edit/part_tag.html b/httemplate/edit/part_tag.html index 5712560c1..2cf34c6e8 100644 --- a/httemplate/edit/part_tag.html +++ b/httemplate/edit/part_tag.html @@ -8,7 +8,7 @@ { field=>'by_default', type=>'checkbox', value=>'Y' }, $tagcolor, ], - 'labels' => { 'tagnum' => 'Tag #', + 'labels' => { 'tagnum' => 'Tag', 'tagname' => 'Tag', 'tagdesc' => 'Message', 'tagcolor' => 'Highlight Color', diff --git a/httemplate/edit/payment_gateway.html b/httemplate/edit/payment_gateway.html index dfe52f109..a469beb7f 100644 --- a/httemplate/edit/payment_gateway.html +++ b/httemplate/edit/payment_gateway.html @@ -19,7 +19,7 @@ <SCRIPT TYPE="text/javascript"> - var modulesForNamespace = <% to_json(\%modules_for_namespace, {canonical=>1}) %>; + var modulesForNamespace = <% encode_json(\%modules_for_namespace, {canonical=>1}) %>; function changeNamespace(what) { var ns = what.value; var select_module = document.getElementById('gateway_module'); diff --git a/httemplate/edit/phone_device.html b/httemplate/edit/phone_device.html index 4aec63e5a..7bc88a8c7 100644 --- a/httemplate/edit/phone_device.html +++ b/httemplate/edit/phone_device.html @@ -32,12 +32,11 @@ %> <%init> -my @deviceparts_with_inventory; -my @part_device = qsearch('part_device', {} ); -foreach my $part_device ( @part_device ) { - push @deviceparts_with_inventory, $part_device->devicepart - if $part_device->inventory_classnum; -} +my @deviceparts_with_inventory = + map $_->devicepart, + qsearch({ 'table' => 'part_device', + 'extra_sql' => 'WHERE inventory_classnum IS NOT NULL', + }); my $html_foot = sub { my $js = " @@ -72,9 +71,9 @@ my $html_foot = sub { var devicepart = what.options[what.selectedIndex].value; - var deviceparts_with_inventory = new Array(\""; -$js .= join("\",\"",@deviceparts_with_inventory); -$js .= "\"); + var deviceparts_with_inventory = new Array("; +$js .= join(',', map qq("$_"), @deviceparts_with_inventory); +$js .= "); var hasInventory = false; for ( i = 0; i < deviceparts_with_inventory.length; i++ ) { diff --git a/httemplate/edit/process/REAL_cust_pkg.cgi b/httemplate/edit/process/REAL_cust_pkg.cgi index 3e0ef59c1..fd2893487 100755 --- a/httemplate/edit/process/REAL_cust_pkg.cgi +++ b/httemplate/edit/process/REAL_cust_pkg.cgi @@ -19,36 +19,41 @@ die "access denied" my $pkgnum = $cgi->param('pkgnum') or die; my $old = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum}); my %hash = $old->hash; -$hash{$_}= $cgi->param($_) ? parse_datetime($cgi->param($_)) : '' - foreach qw( start_date setup bill last_bill contract_end ); +foreach ( qw( start_date setup bill last_bill contract_end ) ) { + if ( $cgi->param($_) =~ /^(\d+)$/ ) { + $hash{$_} = $1; + } else { + $hash{$_} = ''; + } # adjourn, expire, resume not editable this way - -my @errors = (); - -push @errors, '_bill_areyousure' - if $hash{'bill'} != $old->bill # if the next bill date was changed - && $hash{'bill'} < time # to a date in the past - && ! $cgi->param('bill_areyousure'); # and it wasn't confirmed - -push @errors, '_setup_areyousure' - if ! $hash{'setup'} && $old->setup # if the setup date was removed - && ! $cgi->param('setup_areyousure'); # and it wasn't confirmed - -push @errors, '_setupadd_areyousure' - if $hash{'setup'} && ! $old->setup # if the setup date was added - && ! $cgi->param('setupadd_areyousure'); # and it wasn't confirmed - -push @errors, '_start' - if $hash{'start_date'} && !$old->start_date # if a start date was added - && $hash{'setup'}; # but there's a setup date +} my $new; my $error; -if ( @errors ) { - $error = join(',', @errors); -} else { - $new = new FS::cust_pkg \%hash; - $error = $new->replace($old); +$new = new FS::cust_pkg \%hash; +$error = $new->replace($old); + +if (!$error) { + my @supp_pkgs = $old->supplemental_pkgs; + foreach $new (@supp_pkgs) { + foreach ( qw( start_date setup contract_end ) ) { + # propagate these to supplementals + $new->set($_, $hash{$_}); + } + if ( $hash{'bill'} ne $old->get('bill') ) { + if ( $hash{'bill'} and $old->get('bill') ) { + # adjust by the same interval + my $diff = $hash{'bill'} - $old->get('bill'); + $new->set('bill', $new->get('bill') + $diff); + } else { + # absolute date + $new->set('bill', $hash{'bill'}); + } + } + $error = $new->replace; + $error .= ' (supplemental package '.$new->pkgnum.')' if $error; + last if $error; + } } </%init> diff --git a/httemplate/edit/process/bulk-part_pkg.html b/httemplate/edit/process/bulk-part_pkg.html new file mode 100644 index 000000000..4775a9334 --- /dev/null +++ b/httemplate/edit/process/bulk-part_pkg.html @@ -0,0 +1,30 @@ +% if ( $error ) { +% $cgi->param('error', $error); +<% $cgi->redirect(popurl(3).'/edit/bulk-part_pkg.cgi?', $cgi->query_string) %> +% } else { +<% $cgi->redirect(popurl(3).'/browse/part_pkg.cgi') %> +% } +<%init> +die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Bulk edit package definitions'); + +my @pkgparts = $cgi->param('pkgpart') + or die "no package definitions selected"; + +my %changes; +foreach my $param (grep { /^report_option_\d+$/ } $cgi->param) { + if ( length($cgi->param($param)) ) { + if ( $cgi->param($param) == 1 ) { + $changes{$param} = 1; + } else { + $changes{$param} = ''; + } + } +} + +my $error; +foreach my $pkgpart (@pkgparts) { + my $part_pkg = FS::part_pkg->by_key($pkgpart); + my %options = ( $part_pkg->options, %changes ); + $error ||= $part_pkg->replace( options => \%options ); +} +</%init> diff --git a/httemplate/edit/process/change-cust_pkg.html b/httemplate/edit/process/change-cust_pkg.html index 2770f3283..77f261d56 100644 --- a/httemplate/edit/process/change-cust_pkg.html +++ b/httemplate/edit/process/change-cust_pkg.html @@ -32,11 +32,11 @@ my %change = map { $_ => scalar($cgi->param($_)) } $change{'keep_dates'} = 1; if ( $cgi->param('locationnum') == -1 ) { - my $cust_location = new FS::cust_location { + my $cust_location = FS::cust_location->new_or_existing({ 'custnum' => $cust_pkg->custnum, map { $_ => scalar($cgi->param($_)) } qw( address1 address2 city county state zip country ) - }; + }); $change{'cust_location'} = $cust_location; } diff --git a/httemplate/edit/process/credit-cust_bill_pkg.html b/httemplate/edit/process/credit-cust_bill_pkg.html index cbcf619ca..8e66368d4 100644 --- a/httemplate/edit/process/credit-cust_bill_pkg.html +++ b/httemplate/edit/process/credit-cust_bill_pkg.html @@ -10,7 +10,7 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Post credit'); + unless $FS::CurrentUser::CurrentUser->access_right('Credit line items'); my @billpkgnum_setuprecurs = map { $_ =~ /^billpkgnum(\d+\-\w*)$/ or die 'gm#23'; $1; } diff --git a/httemplate/edit/process/cust_location.cgi b/httemplate/edit/process/cust_location.cgi index b9f93db8b..56c3968f6 100644 --- a/httemplate/edit/process/cust_location.cgi +++ b/httemplate/edit/process/cust_location.cgi @@ -28,11 +28,10 @@ my $cust_location = qsearchs({ }); die "unknown locationnum $locationnum" unless $cust_location; -my $new = FS::cust_location->new({ +my $new = FS::cust_location->new_or_existing({ custnum => $cust_location->custnum, prospectnum => $cust_location->prospectnum, - map { $_ => scalar($cgi->param($_)) } - qw( address1 address2 city county state zip country ) + map { $_ => scalar($cgi->param($_)) } FS::cust_main->location_fields }); my $error = $cust_location->move_to($new); diff --git a/httemplate/edit/process/cust_main.cgi b/httemplate/edit/process/cust_main.cgi index 31ec4ab12..c1f815550 100755 --- a/httemplate/edit/process/cust_main.cgi +++ b/httemplate/edit/process/cust_main.cgi @@ -16,8 +16,8 @@ my $DEBUG = 0; </%once> <%init> -die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Edit customer'); +my $curuser = $FS::CurrentUser::CurrentUser; +die "access denied" unless $curuser->access_right('Edit customer'); my $conf = new FS::Conf; @@ -62,6 +62,18 @@ $cgi->param('invoicing_list', join(',', @invoicing_list) ); $cgi->param('duplicate_of_custnum') =~ /^(\d+)$/; my $duplicate_of = $1; +# if this is enabled, enforce it +if ( $conf->exists('agent-ship_address', $cgi->param('agentnum')) ) { + my $agent = FS::agent->by_key($cgi->param('agentnum')); + my $agent_cust_main = $agent->agent_cust_main; + if ( $agent_cust_main ) { + my $agent_location = $agent_cust_main->ship_location; + foreach (qw(address1 city state zip country latitude longitude district)) { + $cgi->param("ship_$_", $agent_location->get($_)); + } + } +} + my %locations; for my $pre (qw(bill ship)) { @@ -71,10 +83,7 @@ for my $pre (qw(bill ship)) { } $hash{'custnum'} = $cgi->param('custnum'); warn Dumper \%hash if $DEBUG; - # if we can qsearchs it, then it's unchanged, so use that - $locations{$pre} = qsearchs('cust_location', \%hash) - || FS::cust_location->new( \%hash ); - + $locations{$pre} = FS::cust_location->new_or_existing(\%hash); } if ( ($cgi->param('same') || '') eq 'Y' ) { @@ -156,9 +165,14 @@ foreach my $dfield (qw( $new->setfield('paid', $cgi->param('paid') ) if $cgi->param('paid'); -my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups'); -my @tax_exempt = grep { $cgi->param("tax_$_") eq 'Y' } @exempt_groups; -my %tax_exempt = map { $_ => scalar($cgi->param("tax_$_".'_num')) } @tax_exempt; +my %options = (); +if ( $curuser->access_right('Edit customer tax exemptions') ) { + my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups'); + my @tax_exempt = grep { $cgi->param("tax_$_") eq 'Y' } @exempt_groups; + $options{'tax_exemption'} = { + map { $_ => scalar($cgi->param("tax_$_".'_num')) } @tax_exempt + }; +} #perhaps this stuff should go to cust_main.pm if ( $new->custnum eq '' or $duplicate_of ) { @@ -266,8 +280,8 @@ if ( $new->custnum eq '' or $duplicate_of ) { else { # create the customer $error ||= $new->insert( \%hash, \@invoicing_list, - 'tax_exemption'=> \%tax_exempt, - 'prospectnum' => scalar($cgi->param('prospectnum')), + %options, + prospectnum => scalar($cgi->param('prospectnum')), ); my $conf = new FS::Conf; @@ -328,7 +342,7 @@ if ( $new->custnum eq '' or $duplicate_of ) { warn Dumper({ new => $new, old => $old }) if $DEBUG; $error ||= $new->replace( $old, \@invoicing_list, - 'tax_exemption' => \%tax_exempt, + %options, ); warn "$me returned from replace" if $DEBUG; diff --git a/httemplate/edit/process/cust_pkg_quantity.html b/httemplate/edit/process/cust_pkg_quantity.html new file mode 100644 index 000000000..fb2657252 --- /dev/null +++ b/httemplate/edit/process/cust_pkg_quantity.html @@ -0,0 +1,33 @@ +% if ($error) { +% $cgi->param('error', $error); +% $cgi->redirect(popurl(3). 'edit/cust_pkg_quantity.html?'. $cgi->query_string ); +% } else { + + <& /elements/header-popup.html, "Quantity changed" &> + <SCRIPT TYPE="text/javascript"> + window.top.location.reload(); + </SCRIPT> + </BODY> + </HTML> + +% } +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('Change customer package'); + +my $cust_pkg = qsearchs({ + 'table' => 'cust_pkg', + 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )', + 'hashref' => { 'pkgnum' => scalar($cgi->param('pkgnum')), }, + 'extra_sql' => ' AND '. $curuser->agentnums_sql, +}); +die 'unknown pkgnum' unless $cust_pkg; + +$cgi->param('quantity') =~ /^(\d+)$/; +my $quantity = $1; +my $error = $cust_pkg->set_quantity($1); + +</%init> diff --git a/httemplate/edit/process/cust_svc.cgi b/httemplate/edit/process/cust_svc.cgi index e22cbb201..7cb1d6d8f 100644 --- a/httemplate/edit/process/cust_svc.cgi +++ b/httemplate/edit/process/cust_svc.cgi @@ -6,7 +6,7 @@ %} <%init> -die 'access deined' +die 'access denied' unless $FS::CurrentUser::CurrentUser->access_right('Change customer service'); my $svcnum = $cgi->param('svcnum'); diff --git a/httemplate/edit/process/elements/process.html b/httemplate/edit/process/elements/process.html index 2d39e9dce..fb1ee7a27 100644 --- a/httemplate/edit/process/elements/process.html +++ b/httemplate/edit/process/elements/process.html @@ -263,6 +263,9 @@ foreach my $value ( @values ) { if ( !$error ) { if ( $old_pkey ) { + + &{ $opt{'edit_callback'} }( $new, $old ) if $opt{'edit_callback'}; + $error = $new->replace($old, @args); } else { $error = $new->insert(@args); diff --git a/httemplate/edit/process/elements/svc_Common.html b/httemplate/edit/process/elements/svc_Common.html index 5a8afbd6c..06f4c00b1 100644 --- a/httemplate/edit/process/elements/svc_Common.html +++ b/httemplate/edit/process/elements/svc_Common.html @@ -10,5 +10,10 @@ my %opt = @_; my $table = $opt{'table'}; $opt{'fields'} ||= [ fields($table) ]; push @{ $opt{'fields'} }, qw( pkgnum svcpart ); +foreach (fields($table)) { + if ( $cgi->param($_.'_classnum') ) { + push @{ $opt{'fields'} }, $_.'_classnum'; + } +} </%init> diff --git a/httemplate/edit/process/part_export.cgi b/httemplate/edit/process/part_export.cgi index bcb9c0df1..e0c470675 100644 --- a/httemplate/edit/process/part_export.cgi +++ b/httemplate/edit/process/part_export.cgi @@ -56,6 +56,7 @@ my $new = new FS::part_export ( { 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; diff --git a/httemplate/edit/process/part_pkg.cgi b/httemplate/edit/process/part_pkg.cgi index c388676df..932e33b1d 100755 --- a/httemplate/edit/process/part_pkg.cgi +++ b/httemplate/edit/process/part_pkg.cgi @@ -10,6 +10,7 @@ 'precheck_callback' => $precheck_callback, 'args_callback' => $args_callback, 'process_m2m' => \@process_m2m, + 'process_o2m' => \@process_o2m, ) %> <%init> @@ -185,6 +186,15 @@ my @process_m2m = ( grep /^svc_dst_pkgpart/, $cgi->param ], }, + { 'link_table' => 'part_pkg_link', + 'target_table' => 'part_pkg', + 'base_field' => 'src_pkgpart', + 'target_field' => 'dst_pkgpart', + 'hashref' => { 'link_type' => 'supp', 'hidden' => '' }, + 'params' => [ map $cgi->param($_), + grep /^supp_dst_pkgpart/, $cgi->param + ], + }, map { my $hidden = $_; { 'link_table' => 'part_pkg_link', @@ -235,4 +245,11 @@ if ( $cgi->param('pkgpart') || ! $conf->exists('agent_defaultpkg') ) { }; } +my @process_o2m = ( + { + 'table' => 'part_pkg_msgcat', + 'fields' => [qw( locale pkg )], + }, +); + </%init> diff --git a/httemplate/edit/process/part_pkg_usage.html b/httemplate/edit/process/part_pkg_usage.html new file mode 100644 index 000000000..eb6c37b82 --- /dev/null +++ b/httemplate/edit/process/part_pkg_usage.html @@ -0,0 +1,67 @@ +% if ( $is_error ) { +% $cgi->param('error' => \%part_pkg_usage); +% # internal redirect, because it's a lot of state to pass through +<& /browse/part_pkg_usage.html &> +% } else { +% # uh, not quite sure... +<% $cgi->redirect($fsurl.'browse/part_pkg.cgi') %> +% } +<%init> +my %vars = $cgi->Vars; +my %part_pkg_usage; +my $is_error; +foreach my $pkgpart ($cgi->param('pkgpart')) { + next unless $pkgpart =~ /^\d+$/; + my $part_pkg = FS::part_pkg->by_key($pkgpart) + or die "unknown pkgpart $pkgpart"; + my %old = map { $_->pkgusagepart => $_ } $part_pkg->part_pkg_usage; + $part_pkg_usage{$pkgpart} ||= []; + my @rows; + foreach (grep /^pkgpart$pkgpart/, keys %vars) { + /^pkgpart\d+_(\w+\D)(\d+)$/ or die "misspelled field name '$_'"; + my $value = delete $vars{$_}; + my $field = $1; + my $row = $2; + $rows[$row] ||= {}; + $rows[$row]->{$field} = $value; + } + + foreach my $row (@rows) { + next if !defined($row); + my $error; + my %classes; + foreach my $class (grep /^class/, keys %$row) { + $class =~ /^class(\d+)_$/; + my $classnum = $1; + $classes{$classnum} = delete $row->{$class}; + } + my $usage = FS::part_pkg_usage->new($row); + $usage->set('pkgpart', $pkgpart); + if ( $usage->pkgusagepart and $row->{minutes} > 0 ) { + $error = $usage->replace(\%classes); + # and don't delete the existing one + delete($old{$usage->pkgusagepart}); + } elsif ( $row->{minutes} > 0 ) { + $error = $usage->insert(\%classes); + } else { + next; + } + if ( $error ) { + $usage->set('error', $error); + $is_error = 1; + } + push @{ $part_pkg_usage{$pkgpart} }, $usage; + } + + foreach my $usage (values %old) { + # all of these were not sent back by the client, so delete them + my $error = $usage->delete; + if ( $error ) { + $usage->set('error', $error); + $is_error = 1; + unshift @{ $part_pkg_usage{$pkgpart} }, $usage; + } + } + +} +</%init> diff --git a/httemplate/edit/process/quick-cust_pkg.cgi b/httemplate/edit/process/quick-cust_pkg.cgi index 2dadbccdc..0cc17d36b 100644 --- a/httemplate/edit/process/quick-cust_pkg.cgi +++ b/httemplate/edit/process/quick-cust_pkg.cgi @@ -70,6 +70,9 @@ my $quantity = $1 || 1; $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; @@ -109,6 +112,7 @@ my %hash = ( : '' ), 'refnum' => $refnum, + 'contactnum' => $contactnum, 'locationnum' => $locationnum, 'discountnum' => $discountnum, #for the create a new discount case @@ -142,11 +146,19 @@ if ( $quotationnum ) { my %opt = ( 'cust_pkg' => $cust_pkg ); + if ( $contactnum == -1 ) { + my $contact = FS::contact->new({ + 'custnum' => scalar($cgi->param('custnum')), + map { $_ => scalar($cgi->param("contactnum_$_")) } qw( first last ) + }); + $opt{'contact'} = $contact; + } + if ( $locationnum == -1 ) { - my $cust_location = new FS::cust_location { + my $cust_location = FS::cust_location->new_or_existing({ map { $_ => scalar($cgi->param($_)) } - qw( custnum address1 address2 city county state zip country geocode ) - }; + ('custnum', FS::cust_main->location_fields) + }); $opt{'cust_location'} = $cust_location; } diff --git a/httemplate/edit/process/svc_phone.html b/httemplate/edit/process/svc_phone.html index 7a3b43d32..9983ea2cb 100644 --- a/httemplate/edit/process/svc_phone.html +++ b/httemplate/edit/process/svc_phone.html @@ -2,6 +2,7 @@ 'table' => 'svc_phone', 'args_callback' => $args_callback, 'value_callback' => $value_callback, + 'edit_callback' => $edit_callback, %opt, &> <%init> @@ -28,6 +29,9 @@ my $right = $opt{'bulk'} ? 'Bulk provision customer service' die "access denied" unless $FS::CurrentUser::CurrentUser->access_right($right); +$cgi->param('phonenum', $cgi->param('phonenum_manual') ) + if $cgi->param('phonenum_which') eq 'phonenum_manual'; + my $tollfreephonenum = $cgi->param('tollfreephonenum'); $cgi->param('phonenum',$tollfreephonenum) if $tollfreephonenum =~ /^\d+$/; @@ -36,10 +40,10 @@ my $args_callback = sub { my %opt = (); if ( $cgi->param('locationnum') == -1 ) { - my $cust_location = new FS::cust_location { + my $cust_location = FS::cust_location->new_or_existing({ map { $_ => scalar($cgi->param($_)) } qw( custnum address1 address2 city county state zip country ) - }; + }); $opt{'cust_location'} = $cust_location; } @@ -48,8 +52,13 @@ my $args_callback = sub { }; my $value_callback = sub { - my ($field, $value) = @_; - ($field =~ /_date$/) ? parse_datetime($value) : $value; + my ($field, $value) = @_; + ($field =~ /_date$/) ? parse_datetime($value) : $value; +}; + +my $edit_callback = sub { + my( $new, $old ) = @_; + $new->sip_password( $old->sip_password ) if $new->sip_password eq '*HIDDEN*'; }; </%init> diff --git a/httemplate/edit/quick-charge.html b/httemplate/edit/quick-charge.html index 1d9647f2f..466091dfa 100644 --- a/httemplate/edit/quick-charge.html +++ b/httemplate/edit/quick-charge.html @@ -145,7 +145,6 @@ function bill_now_changed (what) { <% mt('with terms') |h %> <& /elements/select-terms.html, 'curr_value' => scalar($cgi->param('invoice_terms')), - 'empty_value' => $default_terms, 'disabled' => ( $cgi->param('bill_now') ? 0 : 1 ), &> </TD> diff --git a/httemplate/edit/rate_region.cgi b/httemplate/edit/rate_region.cgi index 367bbafb6..a1c1bcb7d 100644 --- a/httemplate/edit/rate_region.cgi +++ b/httemplate/edit/rate_region.cgi @@ -33,6 +33,14 @@ </TD> </TR> + <& /elements/tr-checkbox.html, + label => 'Exact match', + field => 'exact_match', + cell_style => 'font-weight: bold', + value => 'Y', + curr_value => $rate_region->exact_match + &> + </TABLE> <BR> diff --git a/httemplate/edit/svc_acct.cgi b/httemplate/edit/svc_acct.cgi index c1f74551d..627791ba7 100755 --- a/httemplate/edit/svc_acct.cgi +++ b/httemplate/edit/svc_acct.cgi @@ -9,19 +9,6 @@ <BR> % } -<SCRIPT TYPE="text/javascript"> -function randomPass() { - var i=0; - var pw_set='<% join('', 'a'..'z', 'A'..'Z', '0'..'9' ) %>'; - var pass=''; - while(i < 8) { - i++; - pass += pw_set.charAt(Math.floor(Math.random() * pw_set.length)); - } - document.OneTrueForm.clear_password.value = pass; -} -</SCRIPT> - <FORM NAME="OneTrueForm" ACTION="<% $p1 %>process/svc_acct.cgi" METHOD=POST> <INPUT TYPE="hidden" NAME="svcnum" VALUE="<% $svcnum %>"> <INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>"> @@ -57,10 +44,11 @@ function randomPass() { %if ( $part_svc->part_svc_column('_password')->columnflag ne 'F' ) { <TR> +% #XXX eventually should require "Edit Password" ACL <TD ALIGN="right"><% mt('Password') |h %></TD> <TD> - <INPUT TYPE="text" NAME="clear_password" VALUE="<% $password %>" SIZE=<% $pmax2 %> MAXLENGTH=<% $pmax %>> - <INPUT TYPE="button" VALUE="<% mt('Generate') |h %>" onclick="randomPass();"> + <INPUT TYPE="text" ID="clear_password" NAME="clear_password" VALUE="<% $password %>" SIZE=<% $pmax2 %> MAXLENGTH=<% $pmax %>> + <& /elements/random_pass.html, 'clear_password' &> </TD> </TR> %}else{ diff --git a/httemplate/edit/svc_broadband.cgi b/httemplate/edit/svc_broadband.cgi index 0d4b9897b..1b85460e6 100644 --- a/httemplate/edit/svc_broadband.cgi +++ b/httemplate/edit/svc_broadband.cgi @@ -104,8 +104,12 @@ my @fields = ( { 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') ) { diff --git a/httemplate/edit/svc_phone.cgi b/httemplate/edit/svc_phone.cgi index 9647b6887..13bbe82a1 100644 --- a/httemplate/edit/svc_phone.cgi +++ b/httemplate/edit/svc_phone.cgi @@ -6,6 +6,11 @@ my( $cgi, $svc_x, $part_svc, $cust_pkg, $fields, $opt ) = @_; $svc_x->locationnum($cust_pkg->locationnum) if $cust_pkg; }, + 'svc_edit_callback' => sub { + my( $cgi, $svc_x, $part_svc, $cust_pkg, $fields, $opt) = @_; + my $conf = new FS::Conf; + $svc_x->sip_password('*HIDDEN*') unless $conf->exists('showpasswords'); + }, &> <%init> @@ -28,6 +33,11 @@ my $begin_callback = sub { type => 'select-did', label => 'Phone number', multiple => $bulk, + }, + { field => 'sim_imsi', + type => 'text', + size => 15, + maxlength => 15, }; push @$fields, { field => 'domsvc', diff --git a/httemplate/elements/auto-table.html b/httemplate/elements/auto-table.html index 9aff94e67..5118b91ff 100644 --- a/httemplate/elements/auto-table.html +++ b/httemplate/elements/auto-table.html @@ -50,7 +50,7 @@ var <%$pre%>next_rownum; 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; @@ -70,8 +70,8 @@ function <%$pre%>set_rownum(obj, rownum) { if ( obj.id ) { obj.id = obj.id + rownum; } - if ( obj.name ) { - obj.name = obj.name + rownum; + if ( obj.getAttribute('name') ) { + obj.setAttribute('name', obj.getAttribute('name') + rownum); // also, in this case it's a form field that will be part of the record // so set up an onchange handler obj.onchange = <%$pre%>possiblyAddRow_factory(obj); @@ -96,17 +96,32 @@ function <%$pre%>addRow(data) { <%$pre%>set_rownum(row, this_rownum); if(data instanceof Array) { for (i = 0; i < data.length && i < <%$pre%>fieldorder.length; i++) { - var el = document.getElementsByName(<%$pre%>fieldorder[i] + this_rownum)[0]; + var el = document.getElementsByName(<%$pre |js_string%> + + <%$pre%>fieldorder[i] + + this_rownum)[0]; if (el) { - el.value = data[i]; + if ( el.tagName.toLowerCase() == 'span' ) { + el.innerHTML = data[i]; + } else if ( el.type == 'checkbox' ) { + el.checked = (el.value == data[i]); + } else { + el.value = data[i]; + } } } } else if (data instanceof Object) { for (var field in data) { - var el = document.getElementsByName(field + this_rownum)[0]; + var el = document.getElementsByName(<%$pre |js_string%> + + field + + this_rownum)[0]; if (el) { - el.value = data[field]; -% # doesn't work for checkbox + if ( el.tagName.toLowerCase() == 'span' ) { + el.innerHTML = data[field]; + } else if ( el.type == 'checkbox' ) { + el.checked = (el.value == data[field]); + } else { + el.value = data[field]; + } } } } // else nothing @@ -123,6 +138,20 @@ function <%$pre%>deleteRow(rownum) { <%$pre%>tbody.removeChild(r); } +function <%$pre%>set_prefix(obj) { + if ( obj.id ) { + obj.id = <%$pre |js_string%> + obj.id; + } + if ( obj.getAttribute('name') ) { + obj.setAttribute('name', <%$pre |js_string%> + obj.getAttribute('name')); + } + for (var i = 0; i < obj.children.length; i++) { + if ( obj.children[i] instanceof Node ) { + <%$pre%>set_prefix(obj.children[i]); + } + } +} + function <%$pre%>init() { <%$pre%>template = document.getElementById(<% $template_row |js_string%>); <%$pre%>tbody = document.getElementById('<%$pre%>autotable'); @@ -131,8 +160,10 @@ function <%$pre%>init() { var table = <%$pre%>template.parentNode; table.removeChild(<%$pre%>template); // give it an id - <%$pre%>template.id = <%$pre |js_string%> + 'row'; - // and a magic identifier so we know it's been submitted + <%$pre%>template.id = 'row'; + // prefix the ids and names of the TR object and all its descendants + <%$pre%>set_prefix(<%$pre%>template); + // add a magic identifier so we know it's been submitted var magic = document.createElement('INPUT'); magic.setAttribute('type', 'hidden'); magic.setAttribute('name', '<%$pre%>magic'); @@ -140,18 +171,26 @@ function <%$pre%>init() { // and a delete button %# should this be enclosed in an actual <button> for aesthetics? var delete_button = document.createElement('IMG'); - delete_button.id = 'delete_button'; + delete_button.id = '<%$pre%>delete_button'; delete_button.src = '<%$fsurl%>images/cross.png'; delete_button.alt = 'X'; // use an inline string for this so that it will be cloned properly delete_button.setAttribute('onclick', "<%$pre%>deleteRow(this.rownum);"); + // and an error display + var error_span = document.createElement('SPAN'); + error_span.className = 'error'; + error_span.style.color = '#FF0000'; + error_span.setAttribute('name', '<%$pre%>error'); + error_span.style.padding = '5px'; var delete_cell = document.createElement('TD'); + delete_cell.style.textAlign = 'left'; delete_cell.appendChild(delete_button); delete_cell.appendChild(magic); // it has to go somewhere + delete_cell.appendChild(error_span); <%$pre%>template.appendChild(delete_cell); // preload rows - var rows = <% to_json(\@rows) %>; + var rows = <% encode_json(\@rows) %>; for (var i = 0; i < rows.length; i++) { <%$pre%>addRow(rows[i]); } diff --git a/httemplate/elements/change_history_common.html b/httemplate/elements/change_history_common.html index 232664e39..34ce70b6c 100644 --- a/httemplate/elements/change_history_common.html +++ b/httemplate/elements/change_history_common.html @@ -15,13 +15,7 @@ <TH CLASS="grid" BGCOLOR="#cccccc">Description</TH> </TR> -% foreach my $item ( sort { $a->history_date <=> $b->history_date -% #|| table order -% || $a->historynum <=> $b->historynum -% } -% @history -% ) -% { +% foreach my $item ( @history ) { % my $history_other = ''; % my $act = $item->history_action; % if ( $act =~ /^replace/ ) { @@ -196,4 +190,11 @@ $cust_pkg_date_format .= ' %l:%M:%S%P' if $conf->exists('cust_pkg-display_times') || $curuser->option('cust_pkg-display_times'); +@history = sort { $a->history_date <=> $b->history_date + || $a->historynum <=> $b->historynum } @history; + +if ( $curuser->option('history_order') eq 'newest' ) { + @history = reverse @history; +} + </%init> diff --git a/httemplate/elements/change_password.html b/httemplate/elements/change_password.html new file mode 100644 index 000000000..625ba1fb5 --- /dev/null +++ b/httemplate/elements/change_password.html @@ -0,0 +1,41 @@ +<STYLE> +.passwordbox { + border: 1px solid #7e0079; + padding: 2px; + position: absolute; + font-size: 80%; + background-color: #ffffff; + display: none; +} +</STYLE> +<A ID="<%$pre%>link" HREF="#" onclick="<%$pre%>toggle(true)">(<% mt('change') %>)</A> +<DIV ID="<%$pre%>form" CLASS="passwordbox"> + <FORM METHOD="POST" ACTION="<%$fsurl%>misc/process/change-password.html"> + <INPUT TYPE="hidden" NAME="svcnum" VALUE="<% $svc_acct->svcnum |h%>"> + <INPUT TYPE="text" ID="<%$pre%>password" NAME="password" VALUE="<% $curr_value |h%>"> + <& /elements/random_pass.html, $pre.'password', 'randomize' &> + <INPUT TYPE="submit" VALUE="change"> + <INPUT TYPE="button" VALUE="cancel" onclick="<%$pre%>toggle(false)"> +% if ( $error ) { + <BR><SPAN STYLE="color: #ff0000"><% $error |h %></SPAN> +% } + </FORM> +</DIV> +<SCRIPT TYPE="text/javascript"> +function <%$pre%>toggle(val) { + document.getElementById('<%$pre%>form').style.display = + val ? 'inline-block' : 'none'; + document.getElementById('<%$pre%>link').style.display = + val ? 'none' : 'inline'; +} +% if ( $error ) { +<%$pre%>toggle(true); +% } +</SCRIPT> +<%init> +my %opt = @_; +my $svc_acct = $opt{'svc_acct'}; +my $curr_value = $opt{'curr_value'} || ''; +my $pre = 'changepw'.$svc_acct->svcnum.'_'; +my $error = $cgi->param($pre.'error'); +</%init> diff --git a/httemplate/elements/checkbox-tristate.html b/httemplate/elements/checkbox-tristate.html new file mode 100644 index 000000000..4c26ed74e --- /dev/null +++ b/httemplate/elements/checkbox-tristate.html @@ -0,0 +1,78 @@ +<%doc> +A tristate checkbox (with three values: true, false, and null). +Internally, this creates a checkbox, coupled via javascript to a hidden +field that actually contains the value. For now, the only values these +can have are 1, 0, and empty. Clicking the checkbox cycles between them. +</%doc> +<%shared> +my $init = 0; +</%shared> +% if ( !$init ) { +% $init = 1; +<SCRIPT TYPE="text/javascript"> +function tristate_onclick() { + var checkbox = this; + var input = checkbox.input; + if ( input.value == "" ) { + input.value = "0"; + checkbox.checked = false; + checkbox.indeterminate = false; + } else if ( input.value == "0" ) { + input.value = "1"; + checkbox.checked = true; + checkbox.indeterminate = false; + } else if ( input.value == "1" ) { + input.value = ""; + checkbox.checked = true; + checkbox.indeterminate = true + } +} + +var tristates = []; +var tristate_boxes = []; +window.onload = function() { // don't do this until all of the checkboxes exist +%# tristates = document.getElementsByClassName('tristate'); # curse you, IE8 + var all_inputs = document.getElementsByTagName('input'); + for (var i=0; i < all_inputs.length; i++) { + if ( all_inputs[i].className == 'tristate' ) { + tristates.push(all_inputs[i]); + } + } + for (var i=0; i < tristates.length; i++) { + tristate_boxes[i] = + document.getElementById('checkbox_' + tristates[i].name); + // make sure they can find each other + tristate_boxes[i].input = tristates[i]; + tristates[i].checkbox = tristate_boxes[i]; + // set event handler + tristate_boxes[i].onclick = tristate_onclick; + // set initial value + if ( tristates[i].value == "" ) { + tristate_boxes[i].indeterminate = true + } + if ( tristates[i].value != "0" ) { + tristate_boxes[i].checked = true; + } + } +}; +</SCRIPT> +% } # end of $init +<INPUT TYPE="hidden" NAME="<% $opt{field} %>" + ID="<% $opt{id} %>" + VALUE="<% $curr_value %>" + CLASS="tristate"> +<INPUT TYPE="checkbox" ID="checkbox_<%$opt{field}%>" CLASS="partial"> +<%init> + +my %opt = @_; + +# might be useful but I'm not implementing it yet +#my $onchange = $opt{'onchange'} +# ? 'onChange="'. $opt{'onchange'}. '(this)"' +# : ''; + +$opt{'id'} ||= 'hidden_'.$opt{'field'}; +my $curr_value = $opt{curr_value}; +$curr_value = undef + unless $curr_value eq '0' or $curr_value eq '1'; +</%init> diff --git a/httemplate/elements/contact.html b/httemplate/elements/contact.html index 490ba2303..3d5177612 100644 --- a/httemplate/elements/contact.html +++ b/httemplate/elements/contact.html @@ -2,9 +2,9 @@ <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=""> @@ -106,6 +106,6 @@ foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) { $label{'comment'} = 'Comment'; -my @fields = keys %label; +my @fields = $opt{'name_only'} ? qw( first last ) : keys %label; </%init> diff --git a/httemplate/elements/dashboard-toplist.html b/httemplate/elements/dashboard-toplist.html index f4a372519..b80af7883 100644 --- a/httemplate/elements/dashboard-toplist.html +++ b/httemplate/elements/dashboard-toplist.html @@ -169,7 +169,6 @@ if ( $FS::TicketSystem::system eq 'RT_Internal' 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, @@ -181,10 +180,8 @@ if ( $FS::TicketSystem::system eq 'RT_Internal' 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> diff --git a/httemplate/elements/fckeditor/fckeditor.js b/httemplate/elements/fckeditor/fckeditor.js index 8e0126bae..eb7d339af 100644 --- a/httemplate/elements/fckeditor/fckeditor.js +++ b/httemplate/elements/fckeditor/fckeditor.js @@ -304,7 +304,7 @@ function FCKeditor_IsCompatibleBrowser() // Internet Explorer 5.5+
if ( /*@cc_on!@*/false && sAgent.indexOf("mac") == -1 )
{
- var sBrowserVersion = navigator.appVersion.match(/MSIE (.\..)/)[1] ;
+ var sBrowserVersion = navigator.appVersion.match(/MSIE ([\d.]+)/)[1] ;
return ( sBrowserVersion >= 5.5 ) ;
}
diff --git a/httemplate/elements/location.html b/httemplate/elements/location.html index 873fe1621..685523314 100644 --- a/httemplate/elements/location.html +++ b/httemplate/elements/location.html @@ -200,7 +200,7 @@ Example: </TR> % } else { % foreach (qw(latitude longitude)) { -<INPUT TYPE="hidden" NAME="<% $_ %>" VALUE="<% $object->get($_) |h%>"> +<INPUT TYPE="hidden" NAME="<% $_ %>" ID="<% $_ %>" VALUE="<% $object->get($_) |h%>"> % } % } <INPUT TYPE="hidden" NAME="<%$pre%>coord_auto" VALUE="<% $object->coord_auto %>"> @@ -226,12 +226,13 @@ Example: <TD COLSPAN=8> <INPUT TYPE="text" SIZE=15 NAME="<%$pre%>district" + ID="<%$pre%>district" VALUE="<% $object->district |h %>"> <% '(automatic)' %> </TD> </TR> % } else { - <INPUT TYPE="hidden" NAME="<%$pre%>district" VALUE="<% $object->district %>"> + <INPUT TYPE="hidden" ID="<%$pre%>" NAME="<%$pre%>district" VALUE="<% $object->district %>"> % } % } @@ -239,7 +240,7 @@ Example: %# keep a clean copy of the address so we know if we need %# to re-standardize % foreach (qw(address1 city state country zip latitude -% longitude censustract addr_clean) ) { +% longitude censustract district addr_clean) ) { <INPUT TYPE="hidden" NAME="old_<%$pre.$_%>" ID="old_<%$pre.$_%>" VALUE="<% $object->get($_) |h%>"> % } %# Placeholders diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 4e6109687..5689b12d2 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -194,7 +194,7 @@ foreach my $svcdb ( FS::part_svc->svc_tables() ) { } elsif ( $svcdb eq 'svc_phone' ) { $report_svc{"${name}' total usage by time period"} = - [ $fsurl. 'search/report_svc_phone.html', + [ $fsurl. 'search/report_svc_phone_usage.html', 'Total usage (minutes, and amount billed) for the specified time period, per phone number.', ]; @@ -209,7 +209,7 @@ foreach my $svcdb ( FS::part_svc->svc_tables() ) { $report_svc{"Advanced $lcsname reports"} = [ $fsurl."search/report_$svcdb.html", '' ] - if $svcdb =~ /^svc_(acct|broadband|hardware)$/ + if $svcdb =~ /^svc_(acct|broadband|hardware|phone)$/ && $curuser->access_right("Services: $name: Advanced search"); if ( $svcdb eq 'svc_phone' ) { @@ -236,7 +236,7 @@ tie my %report_packages, 'Tie::IxHash'; $report_packages{'Package definitions (by # active)'} = [ $fsurl.'browse/part_pkg.cgi?active=1', 'Package definitions by number of active packages' ] if $curuser->access_right('Edit package definitions') || $curuser->access_right('Edit global package definitions'); -$report_packages{'Package Costs Report'} = [ $fsurl.'graph/report_cust_pkg_cost.html', 'Package setup and recurring costs graph' ] +$report_packages{'Package costs'} = [ $fsurl.'graph/report_cust_pkg_cost.html', 'Package setup and recurring costs graph' ] if $curuser->access_right('Financial reports'); $report_packages{'separator'} = '' if keys %report_packages; @@ -294,9 +294,11 @@ tie my %report_ticketing, 'Tie::IxHash', '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', @@ -336,7 +338,8 @@ tie my %report_sales, 'Tie::IxHash', 'Daily Sales, Credits and Receipts' => [ $fsurl.'graph/report_money_time_daily.html', 'Sales, credits and receipts (broken down by day) summary graph' ], 'Sales Report' => [ $fsurl.'graph/report_cust_bill_pkg.html', 'Sales report and graph (by agent, package class and/or date range)' ], 'Rated Call Sales Report' => [ $fsurl.'graph/report_cust_bill_pkg_detail.html', 'Sales report and graph (by agent, package class, usage class and/or date range)' ], - 'Sales With Advertising Source' => [ $fsurl.'search/report_cust_bill_pkg_referral.html' ], + 'Sales with Advertising Source' => [ $fsurl.'search/report_cust_bill_pkg_referral.html' ], + 'Sales with Agent Commissions' => [ $fsurl.'search/report_agent_commission.html' ], ; tie my %report_financial, 'Tie::IxHash'; @@ -396,7 +399,7 @@ $report_menu{'Tickets'} = [ \%report_ticketing, 'Ticket reports' ] 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' ] @@ -463,6 +466,8 @@ $tools_menu{'Job Queue'} = [ $fsurl.'search/queue.html', 'View pending job queu if $curuser->access_right('Job queue'); $tools_menu{'Ticketing'} = [ \%tools_ticketing, 'Ticketing tools' ] if $conf->config('ticket_system'); +$tools_menu{'Customer email settings'} = [ $fsurl.'misc/manage_cust_email.html' ] + if $curuser->access_right('Edit customer'); $tools_menu{'Business card scan'} = [ $fsurl.'edit/prospect_main-upload.html' ] if $curuser->access_right('New prospect'); $tools_menu{'Time Queue'} = [ $fsurl.'search/report_timeworked.html', 'View pending support time' ] @@ -649,7 +654,7 @@ $config_misc{'Advertising sources'} = [ $fsurl.'browse/part_referral.html', 'Whe || $curuser->access_right('Edit global advertising sources'); if ( $curuser->access_right('Configuration') ) { $config_misc{'Custom fields'} = [ $fsurl.'browse/part_virtual_field.html', 'Locally defined fields', ]; - $config_misc{'Message catalog'} = [ $fsurl.'browse/msgcat.html', 'Change error messages and other customizable labels for each locale' ]; + $config_misc{'Translation strings'} = [ $fsurl.'browse/msgcat.html', 'Translations and other customizable labels for each locale' ]; } $config_misc{'Inventory classes and inventory'} = [ $fsurl.'browse/inventory_class.html', 'Setup inventory classes and stock inventory' ] if $curuser->access_right('Edit inventory') diff --git a/httemplate/elements/order_pkg.js b/httemplate/elements/order_pkg.js index 8c1efd93a..1069a0ee4 100644 --- a/httemplate/elements/order_pkg.js +++ b/httemplate/elements/order_pkg.js @@ -44,4 +44,5 @@ function standardize_new_location() { function submit_abort() { document.OrderPkgForm.submitButton.disabled = false; + nd(1); } diff --git a/httemplate/elements/progress-init.html b/httemplate/elements/progress-init.html index 7a282a34c..cef54b824 100644 --- a/httemplate/elements/progress-init.html +++ b/httemplate/elements/progress-init.html @@ -108,7 +108,7 @@ function <%$key%>process () { function <%$key%>myCallback( jobnum ) { - overlib( OLiframeContent('<%$p%>elements/progress-popup.html?jobnum=' + jobnum + ';<%$url_or_message_link%>;formname=<%$formname%>' , 444, 168, '<% $popup_name %>'), CAPTION, 'Please wait...', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', CLOSECLICK, MIDX, 0, MIDY, 0 ); + overlib( OLiframeContent('<%$fsurl%>elements/progress-popup.html?jobnum=' + jobnum + ';<%$url_or_message_link%>;formname=<%$formname%>' , 444, 168, '<% $popup_name %>'), CAPTION, 'Please wait...', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', CLOSECLICK, MIDX, 0, MIDY, 0 ); } diff --git a/httemplate/elements/random_pass.html b/httemplate/elements/random_pass.html new file mode 100644 index 000000000..b215b77d9 --- /dev/null +++ b/httemplate/elements/random_pass.html @@ -0,0 +1,17 @@ +<INPUT TYPE="button" VALUE="<% emt($label) %>" onclick="randomPass()"> +<SCRIPT TYPE="text/javascript"> +function randomPass() { + var i=0; + var pw_set='<% join('', 'a'..'z', 'A'..'Z', '0'..'9' ) %>'; + var pass=''; + while(i < 8) { + i++; + pass += pw_set.charAt(Math.floor(Math.random() * pw_set.length)); + } + document.getElementById('<% $id %>').value = pass; +} +</SCRIPT> +<%init> +my $id = shift; +my $label = shift || 'Generate'; +</%init> diff --git a/httemplate/elements/search-svc_broadband.html b/httemplate/elements/search-svc_broadband.html new file mode 100644 index 000000000..d83516172 --- /dev/null +++ b/httemplate/elements/search-svc_broadband.html @@ -0,0 +1,204 @@ +<%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> diff --git a/httemplate/elements/select-areacode.html b/httemplate/elements/select-areacode.html index a302befc2..f0f56d56d 100644 --- a/httemplate/elements/select-areacode.html +++ b/httemplate/elements/select-areacode.html @@ -17,7 +17,7 @@ what.form.<% $opt{'prefix'} %>areacode.disabled = 'disabled'; what.form.<% $opt{'prefix'} %>areacode.style.display = 'none'; var areacodewait = document.getElementById('<% $opt{'prefix'} %>areacodewait'); - areacodewait.style.display = ''; + areacodewait.style.display = 'inline'; var areacodeerror = document.getElementById('<% $opt{'prefix'} %>areacodeerror'); areacodeerror.style.display = 'none'; @@ -61,7 +61,7 @@ what.form.<% $opt{'prefix'} %>areacode.style.display = ''; } else { var areacodeerror = document.getElementById('<% $opt{'prefix'} %>areacodeerror'); - areacodeerror.style.display = ''; + areacodeerror.style.display = 'inline'; } //run the callback diff --git a/httemplate/elements/select-did.html b/httemplate/elements/select-did.html index 6e205d8ff..c39603156 100644 --- a/httemplate/elements/select-did.html +++ b/httemplate/elements/select-did.html @@ -18,6 +18,28 @@ Example: <TABLE> <TR> +% my( $phonenum_checked, $manual_checked ) = ( '', '' ); +% if ( $export->get_dids_can_manual ) { +% #not 100% perfect UI on error handling, but it'll do +% if ( $opt{'curr_value'} ) { +% $phonenum_checked = ''; +% $manual_checked = 'CHECKED'; +% } else { +% $phonenum_checked = 'CHECKED'; +% $manual_checked = ''; +% } + + <TD VALIGN="top"> + <INPUT TYPE = "radio" + NAME = "phonenum_which" + VALUE = "phonenum" + onChange = "phonenum_which_changed(this)" + onClick = "phonenum_which_changed(this)" + <% $phonenum_checked %> + > Inventory + </TD> +% } + % if ( $export->get_dids_npa_select ) { <TD VALIGN="top"> @@ -27,9 +49,10 @@ Example: 'svcpart' => $svcpart, 'disable_empty' => 0, 'empty_label' => 'Select state', + 'disabled' => ( $manual_checked ? 1 : 0 ), ) %> - <BR><FONT SIZE="-1">State</FONT> + <BR><FONT SIZE="-1" ID="phonenum_state_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>State</FONT> </TD> <TD VALIGN="top"> @@ -39,19 +62,24 @@ Example: 'empty' => 'Select area code', ) %> - <BR><FONT SIZE="-1">Area code</FONT> + <BR><FONT SIZE="-1" ID="areacode_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Area code</FONT> </TD> <TD VALIGN="top"> <% include('/elements/select-exchange.html', - 'svcpart' => $svcpart, - 'empty' => 'Select exchange', + 'svcpart' => $svcpart, + 'empty' => 'Select exchange', ) %> - <BR><FONT SIZE="-1">City / Exchange</FONT> + <BR><FONT SIZE="-1" ID="exchange_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>City / Exchange</FONT> </TD> % } else { +% +% #this code path currently only being used by fibernetics +% # should change "Province" label to "State" or make it configurable +% # if/when other folks need an areacode-less DID selector that goes +% # directly from state to region <TD VALIGN="top"> <% include('/elements/select.html', @@ -60,9 +88,10 @@ Example: 'options' => [ '', @{ $export->get_dids } ], 'labels' => { '' => 'Select province' }, 'onchange' => 'phonenum_state_changed(this);', + 'disabled' => ( $manual_checked ? 1 : 0 ), ) %> - <BR><FONT SIZE="-1">Province</FONT> + <BR><FONT SIZE="-1" ID="phonenum_state_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Province</FONT> </TD> <TD VALIGN="top"> @@ -72,7 +101,7 @@ Example: 'empty' => 'Select region', ) %> - <BR><FONT SIZE="-1">Region</FONT> + <BR><FONT SIZE="-1" ID="region_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Region</FONT> </TD> % } @@ -86,10 +115,132 @@ Example: 'region' => ! $export->get_dids_npa_select, ) %> - <BR><FONT SIZE="-1">Phone number</FONT> + <BR><FONT SIZE="-1" ID="phonenum_phonenum_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Phone number</FONT> </TD> </TR> + +% if ( $export->get_dids_can_manual ) { + <TR> + + <TD VALIGN="top"> + <INPUT TYPE = "radio" + NAME = "phonenum_which" + VALUE = "phonenum_manual" + onChange = "phonenum_which_changed(this)" + onClick = "phonenum_which_changed(this)" + <% $manual_checked %> + > Manual entry + </TD> + + <TD VALIGN="top" COLSPAN=4> + <& /elements/input-text.html, + %opt, + field => 'phonenum_manual', + id => 'phonenum_manual', + type => 'text', + disabled => ( $phonenum_checked ? 1 : 0 ), + &> + </TD> + </TR> + + <SCRIPT TYPE="text/javascript"> + function phonenum_which_changed(what) { + + if ( what.value == 'phonenum' && what.checked ) { + + what.form.phonenum_manual.disabled = true; + what.form.phonenum_manual.style.backgroundColor = '#dddddd'; + + what.form.phonenum_state.disabled = false; + + document.getElementById('phonenum_state_label').style.color = '#000000'; + if ( document.getElementById('areacode_label') ) { + document.getElementById('areacode_label').style.color = '#000000'; + } + if ( document.getElementById('exchange_label') ) { + document.getElementById('exchange_label').style.color = '#000000'; + } + if ( document.getElementById('region_label') ) { + document.getElementById('region_label').style.color = '#000000'; + } + document.getElementById('phonenum_phonenum_label').style.color = '#000000'; + + var value = what.form.phonenum_state.options[ what.form.phonenum_state.selectedIndex].value; + + if ( value != '' ) { + + if ( what.form.areacode ) { + what.form.areacode.disabled = false; + + var areacode_value = what.form.areacode.options[ what.form.areacode.selectedIndex].value; + + if ( areacode_value != '' ) { + what.form.exchange.disabled = false; + + var exchange_value = what.form.exchange.options[ what.form.exchange.selectedIndex].value; + + if ( exchange_value != '' ) { + what.form.phonenum.disabled = false; + } + + } + + } + if ( what.form.region ) { + what.form.region.disabled = false; + + var region_value = what.form.region.options[ what.form.region.selectedIndex].value; + + if ( region_value != '' ) { + what.form.phonenum.disabled = false; + } + + } + + } + + } + + if ( what.value == 'phonenum_manual' && what.checked ) { + + what.form.phonenum_manual.disabled = false; + what.form.phonenum_manual.style.backgroundColor = '#ffffff'; + + what.form.phonenum_state.disabled = true; + + document.getElementById('phonenum_state_label').style.color = '#999999'; + if ( document.getElementById('areacode_label') ) { + document.getElementById('areacode_label').style.color = '#999999'; + } + if ( document.getElementById('exchange_label') ) { + document.getElementById('exchange_label').style.color = '#999999'; + } + if ( document.getElementById('region_label') ) { + document.getElementById('region_label').style.color = '#999999'; + } + document.getElementById('phonenum_phonenum_label').style.color = '#999999'; + + if ( what.form.areacode ) { + what.form.areacode.disabled = true; + } + + if ( what.form.exchange ) { + what.form.exchange.disabled = true; + } + + if ( what.form.region ) { + what.form.region.disabled = true; + } + + what.form.phonenum.disabled = true; + } + + } + </SCRIPT> + +% } + </TABLE> % } diff --git a/httemplate/elements/select-exchange.html b/httemplate/elements/select-exchange.html index 9e4b5ce97..b9677094a 100644 --- a/httemplate/elements/select-exchange.html +++ b/httemplate/elements/select-exchange.html @@ -17,7 +17,7 @@ what.form.<% $opt{'prefix'} %>exchange.disabled = 'disabled'; what.form.<% $opt{'prefix'} %>exchange.style.display = 'none'; var exchangewait = document.getElementById('<% $opt{'prefix'} %>exchangewait'); - exchangewait.style.display = ''; + exchangewait.style.display = 'inline'; var exchangeerror = document.getElementById('<% $opt{'prefix'} %>exchangeerror'); exchangeerror.style.display = 'none'; @@ -56,7 +56,7 @@ what.form.<% $opt{'prefix'} %>exchange.style.display = ''; } else { var exchangeerror = document.getElementById('<% $opt{'prefix'} %>exchangeerror'); - exchangeerror.style.display = ''; + exchangeerror.style.display = 'inline'; } //run the callback diff --git a/httemplate/elements/select-mac.html b/httemplate/elements/select-mac.html index 8b1c71fea..4b406fce0 100644 --- a/httemplate/elements/select-mac.html +++ b/httemplate/elements/select-mac.html @@ -7,7 +7,7 @@ <% include( '/elements/input-text.html', %opt, 'type'=>'text' ) %> <SELECT ID="<% $opt{'prefix'} %>sel_mac_addr" NAME="<% $opt{'prefix'} %>sel_mac_addr" - notonChange="<% $opt{'prefix'} %>mac_addr_changed(this); <% $opt{'onchange'} %>" +%# notonChange="<% $opt{'prefix'} %>mac_addr_changed(this); <% $opt{'onchange'} %>" <% $opt{'disabled'} %> STYLE="display: none"> <OPTION VALUE="">Select MAC address</OPTION> </SELECT> diff --git a/httemplate/elements/select-part_svc.html b/httemplate/elements/select-part_svc.html index 72ab7f6b0..743b2852e 100644 --- a/httemplate/elements/select-part_svc.html +++ b/httemplate/elements/select-part_svc.html @@ -13,6 +13,9 @@ my( %opt ) = @_; $opt{'records'} = delete $opt{'part_svc'} if $opt{'part_svc'}; -$opt{'records'} ||= [ qsearch( 'part_svc', {} ) ]; # { disabled=>'' } ) +my %hash = (); +$hash{'svcdb'} = $opt{'svcdb'} if $opt{'svcdb'}; + +$opt{'records'} ||= [ qsearch( 'part_svc', \%hash ) ]; # { disabled=>'' } ) </%init> diff --git a/httemplate/elements/select-phonenum.html b/httemplate/elements/select-phonenum.html index 18abe3dea..a8d9a7c3e 100644 --- a/httemplate/elements/select-phonenum.html +++ b/httemplate/elements/select-phonenum.html @@ -17,7 +17,7 @@ what.form.<% $opt{'prefix'} %>phonenum.disabled = 'disabled'; what.form.<% $opt{'prefix'} %>phonenum.style.display = 'none'; var phonenumwait = document.getElementById('<% $opt{'prefix'} %>phonenumwait'); - phonenumwait.style.display = ''; + phonenumwait.style.display = 'inline'; var phonenumerror = document.getElementById('<% $opt{'prefix'} %>phonenumerror'); phonenumerror.style.display = 'none'; @@ -54,7 +54,7 @@ what.form.<% $opt{'prefix'} %>phonenum.style.display = ''; } else { var phonenumerror = document.getElementById('<% $opt{'prefix'} %>phonenumerror'); - phonenumerror.style.display = ''; + phonenumerror.style.display = 'inline'; } //run the callback diff --git a/httemplate/elements/select-region.html b/httemplate/elements/select-region.html index 9823290db..7ed959269 100644 --- a/httemplate/elements/select-region.html +++ b/httemplate/elements/select-region.html @@ -17,7 +17,7 @@ what.form.<% $opt{'prefix'} %>region.disabled = 'disabled'; what.form.<% $opt{'prefix'} %>region.style.display = 'none'; var regionwait = document.getElementById('<% $opt{'prefix'} %>regionwait'); - regionwait.style.display = ''; + regionwait.style.display = 'inline'; var regionerror = document.getElementById('<% $opt{'prefix'} %>regionerror'); regionerror.style.display = 'none'; @@ -56,7 +56,7 @@ what.form.<% $opt{'prefix'} %>region.style.display = ''; } else { var regionerror = document.getElementById('<% $opt{'prefix'} %>regionerror'); - regionerror.style.display = ''; + regionerror.style.display = 'inline'; } //run the callback diff --git a/httemplate/elements/select-table.html b/httemplate/elements/select-table.html index c0cd7a50b..b6c1573d1 100644 --- a/httemplate/elements/select-table.html +++ b/httemplate/elements/select-table.html @@ -8,7 +8,7 @@ Example: # required ## 'table' => 'table_name', - 'name_col' => 'name_column', + 'name_col' => 'name_column', #or method if you pass an order_by #strongly recommended (you want your forms to be "sticky" on errors, right?) 'curr_value' => 'current_value', @@ -111,6 +111,7 @@ Example: <% $opt{'label_callback'} ? &{ $opt{'label_callback'} }( $record ) : $record->$name_col() + |h %> % } diff --git a/httemplate/elements/select-tiered.html b/httemplate/elements/select-tiered.html index e332eeff8..3ff5471ae 100644 --- a/httemplate/elements/select-tiered.html +++ b/httemplate/elements/select-tiered.html @@ -124,13 +124,6 @@ my %opt = @_; 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; diff --git a/httemplate/elements/selectlayers.html b/httemplate/elements/selectlayers.html index 01fd590ca..cb1d2d619 100644 --- a/httemplate/elements/selectlayers.html +++ b/httemplate/elements/selectlayers.html @@ -236,7 +236,7 @@ sub layer_callback { $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, diff --git a/httemplate/elements/standardize_locations.js b/httemplate/elements/standardize_locations.js index 15c5761a0..e98039d9d 100644 --- a/httemplate/elements/standardize_locations.js +++ b/httemplate/elements/standardize_locations.js @@ -7,8 +7,8 @@ function status_message(text, caption) { function form_address_info() { var cf = document.<% $formname %>; - var returnobj = { onlyship: <% $onlyship ? 1 : 0 %> }; -% if ( !$onlyship ) { + var returnobj = { billship: <% $billship %> }; +% if ( $billship ) { returnobj['same'] = cf.elements['same'].checked; % } % if ( $withfirm ) { @@ -59,16 +59,12 @@ function standardize_locations() { cf.elements['<% $pre %>coord_auto'].value = 'Y'; changed = true; } - -% } #foreach $pre - // standardize if the old address wasn't clean - if ( cf.elements['old_ship_addr_clean'].value == '' || - cf.elements['old_bill_addr_clean'].value == '' ) { - + if ( cf.elements['<% $pre %>addr_clean'].value == '' ) { changed = true; - } +% } #foreach $pre + // or if it was clean but has been changed for (var key in address_info) { var old_el = cf.elements['old_'+key]; @@ -81,7 +77,7 @@ function standardize_locations() { % # If address hasn't been changed, auto-confirm the existing value of % # censustract so that we don't ask the user to confirm it again. - if ( !changed ) { + if ( !changed && <% $withcensus %> ) { if ( address_info['same'] ) { cf.elements['bill_censustract'].value = address_info['bill_censustract']; @@ -195,12 +191,14 @@ function post_standardization() { % if ( $conf->exists('enable_taxproducts') ) { + var cf = document.<% $formname %>; + if ( new String(cf.elements['<% $taxpre %>zip'].value).length < 10 ) { var country_el = cf.elements['<% $taxpre %>country']; var country = country_el.options[ country_el.selectedIndex ].value; - var geocode = cf.elements['geocode'].value; + var geocode = cf.elements['bill_geocode'].value; if ( country == 'CA' || country == 'US' ) { @@ -222,14 +220,14 @@ function post_standardization() { } else { - cf.elements['geocode'].value = 'DEFAULT'; + cf.elements['bill_geocode'].value = 'DEFAULT'; <% $post_geocode %>; } } else { - cf.elements['geocode'].value = ''; + cf.elements['bill_geocode'].value = ''; <% $post_geocode %>; } @@ -254,14 +252,14 @@ function update_geocode() { cf.elements['<% $taxpre %>city'].value = argsHash['city']; setselect(cf.elements['<% $taxpre %>state'], argsHash['state']); cf.elements['<% $taxpre %>zip'].value = argsHash['zip']; - cf.elements['geocode'].value = argsHash['geocode']; + cf.elements['bill_geocode'].value = argsHash['geocode']; <% $post_geocode %>; } // popup a chooser - overlib( OLresponseAJAX, CAPTION, 'Select tax location', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH, 576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399', TEXTSIZE, 3 ); + overlib( OLresponseAJAX, CAPTION, 'Select tax location', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, WIDTH, 576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399', TEXTSIZE, 3 ); } @@ -279,21 +277,18 @@ function setselect(el, value) { my %opt = @_; my $conf = new FS::Conf; -my $withfirm = 1; -my $withcensus = 1; +my $withfirm = $opt{'with_firm'} ? 1 : 0; +my $withcensus = $opt{'with_census'} ? 1 : 0; + +my @prefixes = ''; +my $billship = $opt{'billship'} ? 1 : 0; # whether to have bill_ and ship_ prefixes +my $taxpre = ''; +if ($billship) { + @prefixes = qw(bill_ ship_); + $taxpre = $conf->exists('tax-ship_address') ? 'ship_' : 'bill_'; +} my $formname = $opt{form} || 'CustomerForm'; -my $onlyship = $opt{onlyship} || ''; -#my $main_prefix = $opt{main_prefix} || ''; -#my $ship_prefix = $opt{ship_prefix} || ($onlyship ? '' : 'ship_'); -# The prefixes are now 'ship_' and 'bill_'. -my $taxpre = 'bill_'; -$taxpre = 'ship_' if ( $conf->exists('tax-ship_address') || $onlyship ); my $post_geocode = $opt{callback} || 'post_geocode();'; -$withfirm = 0 if $opt{no_company}; -$withcensus = 0 if $opt{no_census}; - -my @prefixes = ('ship_'); -unshift @prefixes, 'bill_' unless $onlyship; </%init> diff --git a/httemplate/elements/tr-cust_svc.html b/httemplate/elements/tr-cust_svc.html index 1ca22f6d4..b66654f38 100644 --- a/httemplate/elements/tr-cust_svc.html +++ b/httemplate/elements/tr-cust_svc.html @@ -96,7 +96,8 @@ my $svc_unprovision_link = my $manage_link = $opt{'manage_link'}; my $manage_target = ''; if ( $part_svc->svcdb eq 'svc_broadband' and $manage_link ) { - my $ip_addr = $svc_x->ip_addr; #substitution for $manage_link + my $ip_addr = $svc_x->ip_addr; #substitution for $manage_link + my $mac_addr = $svc_x->mac_addr; # ditto $manage_link = eval(qq("$manage_link")); $opt{'manage_link_text'} ||= mt('Manage Device'); $opt{'manage_link_loc'} ||= 'bottom'; diff --git a/httemplate/elements/tr-input-beginning_ending.html b/httemplate/elements/tr-input-beginning_ending.html index 7481c9bb6..ffc903875 100644 --- a/httemplate/elements/tr-input-beginning_ending.html +++ b/httemplate/elements/tr-input-beginning_ending.html @@ -74,7 +74,7 @@ my( $input_time, $time_format, $time_hint ) = ( '', '', '' ); my( $size, $maxlength ) = ( 11, 10 ); if ( $opt{'input_time'} ) { $input_time = ', showsTime: true, timeFormat: "12"'; # http://www.dynarch.com/demos/jscalendar/doc/html/reference.html#node_sec_2.3 - $time_format = ' %k:%M:%S'; # http://www.dynarch.com/demos/jscalendar/doc/html/reference.html#node_sec_5.3.5 + $time_format = ' %H:%M:%S'; # http://www.dynarch.com/demos/jscalendar/doc/html/reference.html#node_sec_5.3.5 $time_hint = ' h:m:s'; $size = 21; $maxlength = 27; diff --git a/httemplate/elements/tr-search-svc_broadband.html b/httemplate/elements/tr-search-svc_broadband.html new file mode 100644 index 000000000..cd7c11500 --- /dev/null +++ b/httemplate/elements/tr-search-svc_broadband.html @@ -0,0 +1,15 @@ +<& 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> diff --git a/httemplate/elements/tr-select-contact.html b/httemplate/elements/tr-select-contact.html new file mode 100644 index 000000000..d6bc67f36 --- /dev/null +++ b/httemplate/elements/tr-select-contact.html @@ -0,0 +1,204 @@ +<%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> diff --git a/httemplate/elements/tr-select-cust_location.html b/httemplate/elements/tr-select-cust_location.html index 7ffbd6c14..780bf96ad 100644 --- a/httemplate/elements/tr-select-cust_location.html +++ b/httemplate/elements/tr-select-cust_location.html @@ -153,25 +153,16 @@ Example: } } + 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'] ) @@ -185,7 +176,7 @@ Example: <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 %> @@ -258,9 +249,7 @@ if ( $cgi->param('error') ) { 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 ); } diff --git a/httemplate/elements/tr-select-did.html b/httemplate/elements/tr-select-did.html index 987ade689..2aa712f79 100644 --- a/httemplate/elements/tr-select-did.html +++ b/httemplate/elements/tr-select-did.html @@ -1,6 +1,6 @@ <% include('tr-td-label.html', @_ ) %> -% if ( $opt{'curr_value'} ne '' && $use_selector ) { +% if ( $use_selector && $opt{'curr_value'} ne '' && ! $can_edit ) { <TD BGCOLOR="#dddddd" <% $cell_style %>><% $opt{'formatted_value'} || $opt{'curr_value'} || $opt{'value'} |h %></TD> @@ -38,4 +38,6 @@ if ( scalar(@exports) > 1 ) { my $use_selector = scalar(@exports) ? 1 : 0; +my $can_edit = scalar(@exports) && $exports[0]->get_dids_can_edit; + </%init> diff --git a/httemplate/elements/tr-select-discount_term.html b/httemplate/elements/tr-select-discount_term.html index e9faeb228..d4218f848 100644 --- a/httemplate/elements/tr-select-discount_term.html +++ b/httemplate/elements/tr-select-discount_term.html @@ -24,7 +24,9 @@ function change_discount_term(what) { id => 'discount_term', options => [ '', @discount_term ], labels => { '' => mt('1 month'), - map { $_ => mt('[_1] months', $_) } @discount_term }, + map { $_ => mt('[_1] months', sprintf('%.0f', $_)) } + @discount_term + }, curr_value => '', onchange => $amount_id ? 'change_discount_term(this)' : '', &> diff --git a/httemplate/elements/tr-select-from_to.html b/httemplate/elements/tr-select-from_to.html index a27412f99..ad9b40a6b 100644 --- a/httemplate/elements/tr-select-from_to.html +++ b/httemplate/elements/tr-select-from_to.html @@ -39,7 +39,7 @@ my %hash = ( 'show_month_abbr' => 1, 'start_year' => '1999', - 'end_year' => '2013', #haha, well... + 'end_year' => '2014', @_, ); </%init> diff --git a/httemplate/elements/tr-select-inventory_item.html b/httemplate/elements/tr-select-inventory_item.html new file mode 100644 index 000000000..669e85f27 --- /dev/null +++ b/httemplate/elements/tr-select-inventory_item.html @@ -0,0 +1,48 @@ +% if ( scalar(@classnums) == 0 ) { +<& tr-fixed.html, %opt &> +% } elsif ( scalar(@classnums) == 1 ) { +% $opt{'extra_sql'} .= ' AND '.$classnum_sql; +<& tr-select-table.html, + 'table' => 'inventory_item', + 'name_col' => 'item', + 'value_col' => 'item', + %opt +&> +% } else { +<& tr-td-label.html, %opt &> +<TD> +<& select-tiered.html, + 'prefix' => $opt{'field'}.'_', + 'tiers' => [ + { + field => $opt{'field'}.'_classnum', + table => 'inventory_class', + extra_sql => "WHERE $classnum_sql", + name_col => 'classname', + empty_label => '(all)', + }, + { + field => $opt{'field'}, + table => 'inventory_item', + name_col => 'item', + value_col => 'item', + link_col => 'classnum', + extra_sql => delete($opt{'extra_sql'}), + disable_empty => 1, + }, + ], + %opt, +&> +</TD> +</TR> +% } +<%init> +my %opt = @_; +my @classnums; +if (ref($opt{'classnum'})) { + @classnums = @{ $opt{'classnum'} }; +} else { + @classnums = split(',', $opt{'classnum'}); +} +my $classnum_sql = 'classnum IN('.join(',', @classnums).')'; +</%init> diff --git a/httemplate/elements/tr-select-part_svc.html b/httemplate/elements/tr-select-part_svc.html index af5148749..959ac8dd9 100644 --- a/httemplate/elements/tr-select-part_svc.html +++ b/httemplate/elements/tr-select-part_svc.html @@ -5,7 +5,7 @@ % } else { <TR> - <TD ALIGN="right"><% $opt{'label'} || 'Package definition' %></TD> + <TD ALIGN="right"><% $opt{'label'} || 'Service definition' %></TD> <TD> <% include( '/elements/select-part_svc.html', 'multiple' => 1, @@ -21,6 +21,9 @@ my( %opt ) = @_; -$opt{'part_svc'} ||= [ qsearch( 'part_svc', {} ) ]; # { disabled=>'' } ) +my %hash = (); +$hash{'svcdb'} = $opt{'svcdb'} if $opt{'svcdb'}; + +$opt{'part_svc'} ||= [ qsearch( 'part_svc', \%hash ) ]; # { disabled=>'' } ) </%init> diff --git a/httemplate/elements/tr-select-reason.html b/httemplate/elements/tr-select-reason.html index c1df10b94..9a670a26b 100755 --- a/httemplate/elements/tr-select-reason.html +++ b/httemplate/elements/tr-select-reason.html @@ -154,15 +154,12 @@ my $controlledbutton = $opt{'control_button'}; my $id = $opt{'id'} || $func_suffix; -my( $add_access_right, $access_right ); +my $add_access_right; if ($class eq 'C') { - $access_right = 'Cancel customer'; $add_access_right = 'Add on-the-fly cancel reason'; } elsif ($class eq 'S') { - $access_right = 'Suspend customer package'; $add_access_right = 'Add on-the-fly suspend reason'; } elsif ($class eq 'R') { - $access_right = 'Post credit'; $add_access_right = 'Add on-the-fly credit reason'; } else { die "illegal class: $class"; diff --git a/httemplate/elements/tr-select-voip_class.html b/httemplate/elements/tr-select-voip_class.html index dcc1487cc..afd3e1f8a 100644 --- a/httemplate/elements/tr-select-voip_class.html +++ b/httemplate/elements/tr-select-voip_class.html @@ -18,7 +18,8 @@ my @options = ( '' => '', 1 => 'VoIP without Broadband', 2 => 'VoIP with Broadband', - 3 => 'Wholesale VoIP' + 3 => 'Wholesale VoIP', + 4 => 'Local Exchange (non-VoIP)', ); </%init> diff --git a/httemplate/misc/areacodes.cgi b/httemplate/misc/areacodes.cgi index 9d32a3baf..4b31deb00 100644 --- a/httemplate/misc/areacodes.cgi +++ b/httemplate/misc/areacodes.cgi @@ -1,4 +1,4 @@ -<% objToJson(\@areacodes) %> +<% encode_json(\@areacodes) %>\ <%init> my( $state, $svcpart ) = $cgi->param('arg'); diff --git a/httemplate/misc/batch-cust_pay.html b/httemplate/misc/batch-cust_pay.html index 887b92489..0b2f1f18c 100644 --- a/httemplate/misc/batch-cust_pay.html +++ b/httemplate/misc/batch-cust_pay.html @@ -23,15 +23,21 @@ function add_row_callback(rownum, prefix) { function custnum_update_callback(rownum, prefix) { var custnum = document.getElementById('custnum'+rownum).value; - document.getElementById('enable_app'+rownum).disabled = ( - custnum == 0 || - num_open_invoices[rownum] < 2 - ); + // if there is a custnum and more than one open invoice, enable + // (and check) the box + var show_applications = (custnum > 0 && num_open_invoices[rownum] > 1); + var enable_app_checkbox = document.getElementById('enable_app'+rownum); + enable_app_checkbox.disabled = show_applications; + % if ( $use_discounts ) { select_discount_term(rownum, prefix); % } } +function invnum_update_callback(rownum, prefix) { + custnum_update_callback(rownum, prefix); +} + function select_discount_term(row, prefix) { var custnum_obj = document.getElementById('custnum'+prefix+row); var select_obj = document.getElementById('discount_term'+prefix+row); @@ -89,6 +95,17 @@ function toggle_application_row(ev, next) { 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); } } @@ -198,7 +215,6 @@ function change_app_amount() { && amount_unapplied(rownum) > 0 ) { create_application_row(rownum, parseInt(appnum) + 1); - } } @@ -352,6 +368,7 @@ function preload() { footer_align => \@footer_align, onchange => \@onchange, custnum_update_callback => 'custnum_update_callback', + invnum_update_callback => 'invnum_update_callback', add_row_callback => 'add_row_callback', &> diff --git a/httemplate/misc/change_pkg.cgi b/httemplate/misc/change_pkg.cgi index 7b08f7b10..03e336cba 100755 --- a/httemplate/misc/change_pkg.cgi +++ b/httemplate/misc/change_pkg.cgi @@ -32,9 +32,6 @@ <& /elements/standardize_locations.html, 'form' => "OrderPkgForm", - 'onlyship' => 1, - 'no_company' => 1, - 'no_census' => 1, 'callback' => 'document.OrderPkgForm.submit();', &> diff --git a/httemplate/misc/change_pkg_contact.html b/httemplate/misc/change_pkg_contact.html new file mode 100755 index 000000000..d9da5beec --- /dev/null +++ b/httemplate/misc/change_pkg_contact.html @@ -0,0 +1,70 @@ +<& /elements/header-popup.html, mt("Change Package Contact") &> + +<& /elements/error.html &> + +<FORM ACTION="<% $p %>misc/process/change_pkg_contact.html" METHOD=POST> +<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>"> + +<% ntable('#cccccc') %> + + <TR> + <TH ALIGN="right"><% mt('Package') |h %></TH> + <TD COLSPAN=7> + <% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B> - <% $part_pkg->comment |h %> + </TD> + </TR> + +% if ( $cust_pkg->contactnum ) { + <TR> + <TH ALIGN="right"><% mt('Current Contact') %></TH> + <TD COLSPAN=7> + <% $cust_pkg->contact_obj->line |h %> + </TD> + </TR> +% } + +<& /elements/tr-select-contact.html, + 'label' => mt('New Contact'), #XXX test + 'cgi' => $cgi, + 'cust_main' => $cust_pkg->cust_main, +&> + +</TABLE> + +<BR> +<INPUT TYPE = "submit" + VALUE = "<% $cust_pkg->contactnum ? mt("Change contact") : mt("Add contact") |h %>" +> + +</FORM> +</BODY> +</HTML> + +<%init> + +my $conf = new FS::Conf; + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('Change customer package'); + +my $pkgnum = scalar($cgi->param('pkgnum')); +$pkgnum =~ /^(\d+)$/ or die "illegal pkgnum $pkgnum"; +$pkgnum = $1; + +my $cust_pkg = + qsearchs({ + 'table' => 'cust_pkg', + 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )', + 'hashref' => { 'pkgnum' => $pkgnum }, + 'extra_sql' => ' AND '. $curuser->agentnums_sql, + }) or die "unknown pkgnum $pkgnum"; + +my $cust_main = $cust_pkg->cust_main + or die "can't get cust_main record for custnum ". $cust_pkg->custnum. + " ( pkgnum ". cust_pkg->pkgnum. ")"; + +my $part_pkg = $cust_pkg->part_pkg; + +</%init> diff --git a/httemplate/misc/choose_tax_location.html b/httemplate/misc/choose_tax_location.html index dce04c77d..23099c421 100644 --- a/httemplate/misc/choose_tax_location.html +++ b/httemplate/misc/choose_tax_location.html @@ -1,6 +1,5 @@ <FORM NAME="choosegeocodeform"> <CENTER><BR><B>Choose tax location</B><BR><BR> -<P>the geocode is:<% $header %></P> <P STYLE="<% $style %>"><% $header %></P> <SELECT NAME='geocodes' ID='geocodes' STYLE="<% $style %>"> @@ -12,7 +11,7 @@ % 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->$_) ) diff --git a/httemplate/misc/confirm-address_standardize.html b/httemplate/misc/confirm-address_standardize.html index 57201ea5a..420e8ea1d 100644 --- a/httemplate/misc/confirm-address_standardize.html +++ b/httemplate/misc/confirm-address_standardize.html @@ -11,16 +11,14 @@ Confirm address standardization </B><BR><BR> <TABLE WIDTH="100%"> -% my @prefixes; -% if ( $old{onlyship} ) { -% @prefixes = ('ship_'); -% } elsif ( $old{same} ) { +% my @prefixes = (''); +% if ( $old{same} ) { % @prefixes = ('bill_'); -% } else { +% } elsif ( $old{billship} ) { % @prefixes = ('bill_', 'ship_'); % } % for my $pre (@prefixes) { -% my $name = $pre eq 'ship_' ? 'service' : 'billing'; +% my $name = $pre eq 'bill_' ? 'billing' : 'service'; % if ( $new{$pre.'addr_clean'} ) { <TR> <TH>Entered <%$name%> address</TH> @@ -128,6 +126,6 @@ my $q = decode_json($cgi->param('q')); my %old = %{ $q->{old} }; my %new = %{ $q->{new} }; -my $addresses = $old{onlyship} ? 'address' : 'addresses'; +my $addresses = $old{billship} ? 'addresses' : 'address'; </%init> diff --git a/httemplate/misc/confirm-cust_pkg-edit_dates.html b/httemplate/misc/confirm-cust_pkg-edit_dates.html new file mode 100755 index 000000000..8e548527a --- /dev/null +++ b/httemplate/misc/confirm-cust_pkg-edit_dates.html @@ -0,0 +1,289 @@ +<%init> +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('Edit customer package dates'); + +my %arg = $cgi->Vars; + +my $pkgnum = $arg{'pkgnum'}; +$pkgnum =~ /^\d+$/ or die "bad pkgnum '$pkgnum'"; +my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum}); +my %hash = $cust_pkg->hash; +foreach (qw( start_date setup bill last_bill contract_end )) { + # adjourn, expire, resume not editable this way + if( $arg{$_} =~ /^\d+$/ ) { + $hash{$_} = $arg{$_}; + } elsif ( $arg{$_} ) { + $hash{$_} = parse_datetime($arg{$_}); + } else { + $hash{$_} = ''; + } +} + +my (@changes, @confirm, @errors); + +my $part_pkg = $cust_pkg->part_pkg; +my @supp_pkgs = $cust_pkg->supplemental_pkgs; +my $main_pkg = $cust_pkg->main_pkg; + +my $conf = FS::Conf->new; +my $date_format = $conf->config('date_format') || '%b %o, %Y'; +# Start date +if ( $hash{'start_date'} != $cust_pkg->get('start_date') and !$hash{'setup'} ) { + my $start = ''; + $start = time2str($date_format, $hash{'start_date'}) if $hash{'start_date'}; + my $text = 'Set this package'; + if ( @supp_pkgs ) { + $text .= ' and all its supplemental packages'; + } + $text .= ' to start billing'; + if ( $start ) { + $text .= ' on [_1].'; + push @changes, mt($text, $start); + } else { + $text .= ' immediately.'; + push @changes, mt($text); + } + push @confirm, ''; +} + +# Setup date changes +if ( $hash{'setup'} != $cust_pkg->get('setup') ) { + my $setup = time2str($date_format, $hash{'setup'}); + my $has_setup_fee = grep { $_->part_pkg->option('setup_fee',1) > 0 } + $cust_pkg, @supp_pkgs; + if ( !$hash{'setup'} ) { + my $text = 'Remove the setup date'; + $text .= ' from this and all its supplemental packages' if @supp_pkgs; + $text .= '.'; + push @changes, mt($text); + if ( $has_setup_fee ) { + push @confirm, mt('This will re-charge the customer for the setup fee.'); + } else { + push @confirm, ''; + } + } elsif ( $hash{'setup'} and !$cust_pkg->get('setup') ) { + my $text = 'Add a setup date of [_1]'; + $text .= ' to this and all its supplemental packages' if @supp_pkgs; + $text .= '.'; + push @changes, mt($text, $setup); + if ( $has_setup_fee ) { + push @confirm, mt('This will prevent charging the setup fee.'); + } else { + push @confirm, ''; + } + } else { + my $text = 'Set the setup date to [_1]'; + $text .= ' on this and all its supplemental packages' if @supp_pkgs; + $text .= '.'; + push @changes, mt($text, $setup); + push @confirm, ''; + } +} + +# Check for start date + setup date +if ( $hash{'start_date'} and $hash{'setup'} ) { + if ( $cust_pkg->get('setup') ) { + push @errors, mt('Since the package has already started billing, it '. + 'cannot have a start date.'); + } else { + push @errors, mt('You cannot set both a start date and a setup date on '. + 'the same package.'); + } +} + +# Last bill date change +if ( $hash{'last_bill'} != $cust_pkg->get('last_bill') ) { + my $last_bill = time2str($date_format, $hash{'last_bill'}); + my $name = 'last bill date'; + $name = 'last renewal date' if $part_pkg->is_prepaid; + if ( $hash{'last_bill'} ) { + push @changes, mt('Set the [_1] to [_2].', $name, $last_bill); + } else { + push @changes, mt('Remove the [_1].', $name); + } + push @confirm, ''; + # I don't think we want to adjust this on supplemental packages. +} + +# Bill date change +if ( $hash{'bill'} != $cust_pkg->get('bill') ) { + my $bill = time2str($date_format, $hash{'bill'}); + $bill = 'today' if !$hash{'bill'}; # or 'the end of today'?... + my $name = 'next bill date'; + $name = 'end of the prepaid period' if $part_pkg->is_prepaid; + push @changes, mt('Set the [_1] to [_2].', $name, $bill); + + if ( $hash{'bill'} < time and $hash{'bill'} ) { + push @confirm, + mt('The customer will be charged for the interval from [_1] until now.', + $bill); + } elsif ( !$hash{'bill'} and ($hash{'last_bill'} or $hash{'setup'}) ) { + my $last_bill = + time2str($date_format, $hash{'last_bill'} || $hash{'setup'}); + push @confirm, + mt('The customer will be charged for the interval from [_1] until now.', + $last_bill); + } else { + push @confirm, ''; + } + + if ( @supp_pkgs ) { + push @changes, ''; + if ( $cust_pkg->get('bill') and $hash{'bill'} ) { + # the package already has a bill date, so adjust the dates + # of supplementals by the same interval + my $diff = $hash{'bill'} - $cust_pkg->get('bill'); + my $sign = $diff < 0 ? -1 : 1; + $diff = $diff * $sign / 86400; + if ( $diff < 1 ) { + $diff = mt('[quant,_1,hour]', int($diff * 24)); + } else { + $diff = mt('[quant,_1,day]', int($diff)); + } + push @confirm, + mt('[_1] supplemental package will also be billed [_2] [_3].', + (@supp_pkgs > 1 ? 'Each' : 'The'), + $diff, + ($sign > 0 ? 'later' : 'earlier') + ); + } else { + # the package hasn't been billed yet, or you've set bill = null + push @confirm, + mt('[_1] supplemental package will also be billed on [_2].', + (@supp_pkgs > 1 ? 'Each' : 'The'), + $bill + ); + } + } #if @supp_pkgs + + if ( $main_pkg ) { + push @changes, ''; + push @confirm, + mt('This package is a supplemental package. The bill date of its '. + 'main package will not be adjusted.'); + } +} + +# Contract end change +if ( $hash{'contract_end'} != $cust_pkg->get('contract_end') ) { + if ( $hash{'contract_end'} ) { + my $contract_end = time2str($date_format, $hash{'contract_end'}); + push @changes, + mt('Set this package\'s contract end date to [_1]', $contract_end); + } else { + push @changes, mt('Remove this package\'s contract end date.'); + } + if ( @supp_pkgs ) { + my $text = 'This change will also apply to ' . + (@supp_pkgs > 1 ? + 'all supplemental packages.': + 'the supplemental package.'); + push @confirm, mt($text); + } else { + push @confirm, ''; + } +} + +my $title = ''; +if ( @errors ) { + $title = 'Error changing package dates'; +} else { + $title = 'Confirm date changes'; +} +</%init> +<& /elements/header-popup.html, { title => $title, etc => 'BGCOLOR=""' } &> +<STYLE TYPE="text/css"> +.error { + color: #ff0000; + font-weight: bold; + text-align: center; +} +.confirm { color: #ff0000 } +.button-container { + position: fixed; + bottom: 5px; + text-align: center; + width: 100% +} +</STYLE> +<DIV STYLE="text-align: center; padding:1em"> +<% emt('Package #') %><B><% $pkgnum %></B>: <B><% $cust_pkg->part_pkg->pkg %></B><BR> +% if ( @changes ) { + <% emt('The following changes will be made:') %> +% } else { + <% emt('No changes will be made.') %> +% } +</DIV> +<TABLE WIDTH="100%"> +% if ( @errors ) { +% foreach my $error ( @errors ) { +<TR> + <TD><IMG SRC="<%$p%>images/cross.png"></TD> + <TD CLASS="error"><% $error %></TD> +</TR> +% } +% } else { +% while (@changes, @confirm) { +% my $text = shift @changes; +% if (length $text) { +<TR> + <TD><IMG SRC="<%$p%>images/tick.png"></TD> + <TD><% $text %></TD> +</TR> +% } +% $text = shift @confirm; +% if (length $text) { +<TR> + <TD> + <INPUT TYPE="checkbox" NAME="areyousure" VALUE=1 onclick="submit_ready()"> + </TD> + <TD CLASS="confirm"><% $text %></TD> +</TR> +% } +% } +% } +</TABLE> +%# action buttons +<DIV CLASS="button-container"> + <BUTTON TYPE="button" STYLE="width:145px" ID="submit_cancel"\ + onclick="submit_cancel()"> + <IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel + </BUTTON> +% if (!@errors ) { + <BUTTON TYPE="button" STYLE="width:145px" ID="submit_continue"\ + onclick="submit_continue()"> + <IMG SRC="<%$p%>images/tick.png" ALT=""> Continue + </BUTTON> +</DIV> +% } +<FORM NAME="DateEditForm" STYLE="display:none" TARGET="_parent" ACTION="<%$p%>edit/process/REAL_cust_pkg.cgi" METHOD="POST"> +% foreach (keys %hash) { +<INPUT TYPE="hidden" NAME="<%$_%>" VALUE="<% $hash{$_} |h%>"> +% } +</FORM> +<SCRIPT> +function submit_ready() { + var ready = true; + var checkboxes = document.getElementsByName('areyousure'); + var i; + for (i=0; i < checkboxes.length; i++) { + if (! checkboxes[i].checked ) { + ready = false; + } + } + document.getElementById('submit_continue').disabled = !ready; + return ready; +} +function submit_cancel() { + parent.nd(1); +} +function submit_continue() { + if ( submit_ready() ) { + document.forms.DateEditForm.submit(); + } +} +submit_ready(); +</SCRIPT> +<& /elements/footer.html &> diff --git a/httemplate/misc/cust-part_pkg.cgi b/httemplate/misc/cust-part_pkg.cgi index a277ba407..43b92297e 100644 --- a/httemplate/misc/cust-part_pkg.cgi +++ b/httemplate/misc/cust-part_pkg.cgi @@ -1,4 +1,4 @@ -<% objToJson( \@return ) %> +<% encode_json( \@return ) %>\ <%init> my( $custnum, $prospectnum, $classnum ) = $cgi->param('arg'); diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html index d26e40298..fcd79d7f8 100644 --- a/httemplate/misc/email-customers.html +++ b/httemplate/misc/email-customers.html @@ -120,9 +120,11 @@ Template: <TR> <TD ALIGN="right" VALIGN="top" STYLE="padding-top:3px">Message: </TD> - <TD><& '/elements/htmlarea.html', - 'field' => 'html_body', - 'width' => 600 &></TD> + <TD><& /elements/htmlarea.html, + 'field' => 'html_body', + 'width' => 763, + &> + </TD> </TR> </TABLE> diff --git a/httemplate/misc/exchanges.cgi b/httemplate/misc/exchanges.cgi index 8a67f7bab..0de4ace25 100644 --- a/httemplate/misc/exchanges.cgi +++ b/httemplate/misc/exchanges.cgi @@ -1,4 +1,4 @@ -<% objToJson(\@exchanges) %> +<% encode_json(\@exchanges) %>\ <%init> my( $areacode, $svcpart ) = $cgi->param('arg'); diff --git a/httemplate/misc/location.cgi b/httemplate/misc/location.cgi index 188c5c3df..fab61dd01 100644 --- a/httemplate/misc/location.cgi +++ b/httemplate/misc/location.cgi @@ -1,4 +1,4 @@ -<% objToJson(\%hash) %> +<% encode_json(\%hash) %>\ <%init> my $locationnum = $cgi->param('arg'); @@ -24,8 +24,9 @@ my $cust_location = qsearchs({ 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> diff --git a/httemplate/misc/macinventory.cgi b/httemplate/misc/macinventory.cgi index b07da9726..cec0e3121 100644 --- a/httemplate/misc/macinventory.cgi +++ b/httemplate/misc/macinventory.cgi @@ -1,4 +1,4 @@ -<% objToJson(\@macs) %> +<% encode_json(\@macs) %>\ <%init> # XXX: this should be agent-virtualized / limited @@ -13,13 +13,8 @@ die "unknown devicepart $devicepart" unless $part_device; my $inventory_class = $part_device->inventory_class; die "devicepart $devicepart has no inventory" unless $inventory_class; -my @inventory_item = +my @macs = + map $_->item, qsearch('inventory_item', { 'classnum' => $inventory_class->classnum } ); -my @macs; - -foreach my $inventory_item ( @inventory_item ) { - push @macs, $inventory_item->item; -} - </%init> diff --git a/httemplate/misc/maestro-customer_status.html b/httemplate/misc/maestro-customer_status.html index 8acae2b2a..a872d4997 100644 --- a/httemplate/misc/maestro-customer_status.html +++ b/httemplate/misc/maestro-customer_status.html @@ -1,4 +1,4 @@ -<% objToJson( $return ) %> +<% encode_json( $return ) %>\ <%init> my $return; diff --git a/httemplate/misc/manage_cust_email.html b/httemplate/misc/manage_cust_email.html new file mode 100644 index 000000000..3ece459bb --- /dev/null +++ b/httemplate/misc/manage_cust_email.html @@ -0,0 +1,106 @@ +<& /elements/header.html, 'Manage customer email settings' &> +<STYLE TYPE="text/css"> +.hidden { display: none } +</STYLE> +<& /elements/xmlhttp.html, + url => $p.'misc/xmlhttp-cust_main-email_search.html', + subs => ['email_search'] +&> +<SCRIPT TYPE="text/javascript"> + +function receive_search(result) { + var recs = JSON.parse(result); + var tbody = document.getElementById('tbody_results'); + var j = tbody.rows.length; + for(var i = 0; i < j; i++) { + tbody.deleteRow(tbody.rows[i]); + } + if (recs.length > 0) { + for(var i = 0; i < recs.length; i++) { + var rec = recs[i]; + var row = tbody.insertRow(i); + row.style.backgroundColor = (i % 2 ? '#eeeeee' : '#ffffff'); + + var cell = row.insertCell(0); // custnum + cell.appendChild( document.createTextNode(rec[0]) ); + cell = row.insertCell(1); // customer name + cell.appendChild( document.createTextNode(rec[1]) ); + cell = row.insertCell(2); // email + cell.appendChild( document.createTextNode(rec[2]) ); + + cell = row.insertCell(3); // invoice_email + var input = document.createElement('INPUT'); + input.type = 'hidden'; + input.name = 'custnum'; + input.value = rec[0]; + cell.appendChild(input); + + input = document.createElement('INPUT'); + input.type = 'checkbox'; + input.name = 'custnum' + rec[0] + '_invoice_email'; + input.value = 'Y'; + input.checked = (rec[3] != 'Y'); + cell.appendChild(input); + cell.style.textAlign = 'center'; + + cell = row.insertCell(4); // message_email + input = document.createElement('INPUT'); + input.type = 'checkbox'; + input.name = 'custnum' + rec[0] + '_message_email'; + input.value = 'Y'; + input.checked = (rec[4] != 'Y'); + cell.appendChild(input); + cell.style.textAlign = 'center'; + } + document.getElementById('div_found').style.display = ''; + } else { + document.getElementById('div_notfound').style.display = ''; + } +} + +function start_search() { + document.getElementById('div_found').style.display = 'none'; + document.getElementById('div_notfound').style.display = 'none'; + var email = document.getElementById('input_email').value; + email_search(email, receive_search); +} +% if ( $cgi->param('search') ) { +window.onload = start_search; +% } +</SCRIPT> +<FORM ACTION="<%$p%>misc/process/manage_cust_email.html" METHOD="POST"> +<DIV> +% if ( $cgi->param('done') ) { +<P STYLE="font-weight: bold; color: #00ff00">Changes saved.</P> +% } elsif ( $cgi->param('error') ) { +<P STYLE="font-weight: bold; color: #ff0000"><% $cgi->param('error') |h %></P> +% } + Email address: + <INPUT TYPE="text" ID="input_email" NAME="search"\ + VALUE="<% $cgi->param('search') |h %>"> + <INPUT TYPE="button" onclick="start_search()" VALUE="find"> +</DIV> +<DIV ID="div_notfound" STYLE="display: none; padding: 1em"> +No matching email addresses found. +</DIV> +<DIV ID="div_found" STYLE="display: none"> +<TABLE CLASS="grid" STYLE="border-spacing: 0px"> + <THEAD> + <TR STYLE="background-color: #dddddd"> + <TH>#</TH> + <TH>Customer</TH> + <TH>Email</TH> + <TH>Send invoices</TH> + <TH>Send other notices</TH> + </TR> + </THEAD> + <TBODY ID="tbody_results"></TBODY> +</TABLE> +<INPUT TYPE="submit" VALUE="Save changes"> +</FORM> +<& /elements/footer.html &> +<%init> +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Edit customer'); + +</%init> diff --git a/httemplate/misc/order_pkg.html b/httemplate/misc/order_pkg.html index bfc7b6903..e09ba986d 100644 --- a/httemplate/misc/order_pkg.html +++ b/httemplate/misc/order_pkg.html @@ -93,6 +93,12 @@ &> % } +<& /elements/tr-select-contact.html, + 'cgi' => $cgi, + 'cust_main' => $cust_main, + 'prospect_main' => $prospect_main, +&> + % if ( $cgi->param('lock_locationnum') ) { <INPUT TYPE = "hidden" @@ -129,9 +135,6 @@ <& /elements/standardize_locations.html, 'form' => "OrderPkgForm", - 'onlyship' => 1, - 'no_company' => 1, - 'no_census' => 1, 'callback' => 'document.OrderPkgForm.submit();', &> diff --git a/httemplate/misc/part_export/huawei_hlr-import_sim.html b/httemplate/misc/part_export/huawei_hlr-import_sim.html new file mode 100644 index 000000000..9b87b3d2a --- /dev/null +++ b/httemplate/misc/part_export/huawei_hlr-import_sim.html @@ -0,0 +1,52 @@ +<& /elements/header-popup.html, 'Import SIMs' &> +Import a file containing SIM card properties.<BR> +Each row should contain the following fields, separated by spaces:<BR> +IMSI, ICCID, PIN1, PUK1, PIN2, PUK2, ACC, Ki<BR> +<BR> +<& /elements/form-file_upload.html, + 'name' => 'ImportForm', + 'action' => 'process/huawei_hlr-import_sim.html', + 'num_files' => 1, + 'fields' => [ 'exportnum', 'classnum', 'agentnum', ], + 'message' => 'Inventory import successful', + 'onsubmit' => "document.ImportForm.submitButton.disabled=true;", +&> +<TABLE CLASS="inv" WIDTH="100%"> + <INPUT TYPE="hidden" NAME="exportnum" VALUE="<%$exportnum%>"> + <& /elements/file-upload.html, + 'field' => 'file', + 'label' => 'Filename', + &> + <& /elements/tr-select-agent.html, + 'disable_empty' => 1, + &> + <& /elements/tr-select-table.html, + 'table' => 'inventory_class', + 'name_col' => 'classname', + 'label' => 'Inventory class', + 'disable_empty' => 1, + &> + + <TR> + <TD COLSPAN=2 ALIGN="center" STYLE="padding-top:6px"> + <INPUT TYPE = "submit" + NAME = "submitButton" + ID = "submitButton" + VALUE = "Import file" + > + </TD> + </TR> + +</TABLE> + +</FORM> + +<%init> +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my ($exportnum) = $cgi->keywords; +$exportnum =~ /^\d+$/ or die "bad exportnum '$exportnum'"; +my $part_export = FS::part_export->by_key($exportnum) + or die "export $exportnum not found"; +</%init> diff --git a/httemplate/misc/part_export/process/huawei_hlr-import_sim.html b/httemplate/misc/part_export/process/huawei_hlr-import_sim.html new file mode 100644 index 000000000..d46700d5f --- /dev/null +++ b/httemplate/misc/part_export/process/huawei_hlr-import_sim.html @@ -0,0 +1,10 @@ +<% $server->process %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $server = new FS::UI::Web::JSRPC + 'FS::part_export::huawei_hlr::process_import_sim', $cgi; + +</%init> diff --git a/httemplate/misc/part_svc-columns.cgi b/httemplate/misc/part_svc-columns.cgi index 060256154..a86164d06 100644 --- a/httemplate/misc/part_svc-columns.cgi +++ b/httemplate/misc/part_svc-columns.cgi @@ -1,4 +1,4 @@ -<% objToJson(\@output) %> +<% encode_json(\@output) %>\ <%init> my $conf = new FS::Conf; diff --git a/httemplate/misc/phonenums.cgi b/httemplate/misc/phonenums.cgi index 5084628eb..a048280bb 100644 --- a/httemplate/misc/phonenums.cgi +++ b/httemplate/misc/phonenums.cgi @@ -1,4 +1,4 @@ -<% objToJson(\@phonenums) %> +<% encode_json(\@phonenums) %>\ <%init> my( $exchangestring, $svcpart ) = $cgi->param('arg'); diff --git a/httemplate/misc/process/change-password.html b/httemplate/misc/process/change-password.html new file mode 100644 index 000000000..7cab9c4e3 --- /dev/null +++ b/httemplate/misc/process/change-password.html @@ -0,0 +1,26 @@ +<%init> +my $curuser = $FS::CurrentUser::CurrentUser; + +$cgi->param('svcnum') =~ /^(\d+)$/ or die "illegal svcnum"; +my $svcnum = $1; +my $svc_acct = FS::svc_acct->by_key($svcnum) + or die "svc_acct $svcnum not found"; +my $part_svc = $svc_acct->part_svc; +die "access denied" unless ( + $curuser->access_right('Provision customer service') or + ( $curuser->access_right('Edit password') and + ! $part_svc->restrict_edit_password ) + ); +my $error = $svc_acct->set_password($cgi->param('password')) + || $svc_acct->replace; + +# annoyingly specific to view/svc_acct.cgi, for now... +$cgi->delete('password'); +</%init> +% if ( $error ) { +% $cgi->param('svcnum', $svcnum); +% $cgi->param("changepw${svcnum}_error", $error); +% } else { +% $cgi->query_string($svcnum); +% } +<% $cgi->redirect($fsurl.'view/svc_acct.cgi?'.$cgi->query_string) %> diff --git a/httemplate/misc/process/change_pkg_contact.html b/httemplate/misc/process/change_pkg_contact.html new file mode 100644 index 000000000..2795c1197 --- /dev/null +++ b/httemplate/misc/process/change_pkg_contact.html @@ -0,0 +1,49 @@ +<% 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> diff --git a/httemplate/misc/process/manage_cust_email.html b/httemplate/misc/process/manage_cust_email.html new file mode 100644 index 000000000..5bf1470d1 --- /dev/null +++ b/httemplate/misc/process/manage_cust_email.html @@ -0,0 +1,32 @@ +<% $cgi->redirect($fsurl.'misc/manage_cust_email.html?' . + $cgi->query_string) %> +<%init> +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Edit customer'); + +my $error; +foreach my $custnum ($cgi->param('custnum')) { + my $cust = FS::cust_main->by_key($custnum) + or die "customer not found: $custnum\n"; + my $new_invoice_noemail = + $cgi->param('custnum'.$custnum.'_invoice_email') ? '' : 'Y'; + my $new_message_noemail = + $cgi->param('custnum'.$custnum.'_message_email') ? '' : 'Y'; + if ( $new_invoice_noemail ne $cust->invoice_noemail + or $new_message_noemail ne $cust->message_noemail ) { + + $cust->set('invoice_noemail', $new_invoice_noemail); + $cust->set('message_noemail', $new_message_noemail); + $error ||= $cust->replace; + + } + $cgi->delete('custnum'.$custnum.'_invoice_email'); + $cgi->delete('custnum'.$custnum.'_message_email'); +} +$cgi->delete('custnum'); +if ( $error ) { + $cgi->param('error' => $error); # probably unnecessary... +} else { + $cgi->param('done' => 1) unless $error; +} +</%init> diff --git a/httemplate/misc/process/payment.cgi b/httemplate/misc/process/payment.cgi index 506e26684..981614e76 100644 --- a/httemplate/misc/process/payment.cgi +++ b/httemplate/misc/process/payment.cgi @@ -210,7 +210,15 @@ if ( $cgi->param('save') ) { $new->set( 'paycvv' => ''); } - $new->set( $_ => $cgi->param($_) ) foreach @{$payby2fields{$payby}}; + if ( $payby eq 'CARD' ) { + my $bill_location = FS::cust_location->new; + $bill_location->set( $_ => $cgi->param($_) ) + foreach @{$payby2fields{$payby}}; + $new->set('bill_location' => $bill_location); + # will do nothing if the fields are all unchanged + } else { + $new->set( $_ => $cgi->param($_) ) foreach @{$payby2fields{$payby}}; + } my $error = $new->replace($cust_main); errorpage("payment processed successfully, but error saving info: $error") diff --git a/httemplate/misc/regions.cgi b/httemplate/misc/regions.cgi index 2450ea31a..31538b08e 100644 --- a/httemplate/misc/regions.cgi +++ b/httemplate/misc/regions.cgi @@ -1,4 +1,4 @@ -<% objToJson(\@regions) %> +<% encode_json(\@regions) %>\ <%init> my( $state, $svcpart ) = $cgi->param('arg'); diff --git a/httemplate/misc/xmlhttp-address_standardize.html b/httemplate/misc/xmlhttp-address_standardize.html index 988057163..618265364 100644 --- a/httemplate/misc/xmlhttp-address_standardize.html +++ b/httemplate/misc/xmlhttp-address_standardize.html @@ -1,4 +1,4 @@ -<% encode_json($return) %> +<% encode_json($return) %>\ <%init> local $SIG{__DIE__}; #disable Mason error trap @@ -16,12 +16,10 @@ my %old = %{ decode_json($cgi->param('arg')) } my %new; -my @prefixes; -if ($old{onlyship}) { - @prefixes = ('ship_'); -} elsif ( $old{same} ) { +my @prefixes = (''); +if ( $old{same} ) { @prefixes = ('bill_'); -} else { +} elsif ( $old{billship} ) { @prefixes = ('bill_', 'ship_'); } my $all_same = 1; @@ -44,6 +42,8 @@ foreach my $pre ( @prefixes ) { $all_same = 0 if ( $new{$pre.$_} ne $old{$pre.$_} ); last if !$all_same; } + + $all_same = 0 if $new{$pre.'error'}; } my $return = { old => \%old, new => \%new, all_same => $all_same }; diff --git a/httemplate/misc/xmlhttp-calculate_taxes.html b/httemplate/misc/xmlhttp-calculate_taxes.html index d3dc36acf..ed7bd0173 100644 --- a/httemplate/misc/xmlhttp-calculate_taxes.html +++ b/httemplate/misc/xmlhttp-calculate_taxes.html @@ -1,4 +1,4 @@ -<% objToJson($return) %> +<% encode_json($return) %>\ <%init> my $DEBUG = 0; diff --git a/httemplate/misc/xmlhttp-cust_bill-search.html b/httemplate/misc/xmlhttp-cust_bill-search.html index 46f15d1ab..c60a0b05c 100644 --- a/httemplate/misc/xmlhttp-cust_bill-search.html +++ b/httemplate/misc/xmlhttp-cust_bill-search.html @@ -1,4 +1,4 @@ -<% encode_json(\@return) %> +<% encode_json(\@return) %>\ <%init> my $curuser = $FS::CurrentUser::CurrentUser; @@ -6,13 +6,15 @@ die 'access denied' unless $curuser->access_right('View invoices'); 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> diff --git a/httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html b/httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html index 993504619..c0db3e2c4 100644 --- a/httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html +++ b/httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html @@ -1,8 +1,8 @@ -<% to_json($return) %> +<% encode_json($return) %>\ <%init> my $curuser = $FS::CurrentUser::CurrentUser; -die "access denied" unless $curuser->access_right('Post credit'); +die "access denied" unless $curuser->access_right('Credit line items'); my $DEBUG = 0; diff --git a/httemplate/misc/xmlhttp-cust_main-censustract.html b/httemplate/misc/xmlhttp-cust_main-censustract.html index 4b00898da..4c708a4c4 100644 --- a/httemplate/misc/xmlhttp-cust_main-censustract.html +++ b/httemplate/misc/xmlhttp-cust_main-censustract.html @@ -1,4 +1,4 @@ -<% objToJson($return) %> +<% encode_json($return) %>\ <%init> my %arg = $cgi->param('arg'); diff --git a/httemplate/misc/xmlhttp-cust_main-discount_terms.cgi b/httemplate/misc/xmlhttp-cust_main-discount_terms.cgi index b524e69fc..36b18b455 100644 --- a/httemplate/misc/xmlhttp-cust_main-discount_terms.cgi +++ b/httemplate/misc/xmlhttp-cust_main-discount_terms.cgi @@ -16,7 +16,7 @@ % } % } % -<% objToJson($return) %> +<% encode_json($return) %>\ % } <%init> diff --git a/httemplate/misc/xmlhttp-cust_main-email_search.html b/httemplate/misc/xmlhttp-cust_main-email_search.html new file mode 100644 index 000000000..0d830826c --- /dev/null +++ b/httemplate/misc/xmlhttp-cust_main-email_search.html @@ -0,0 +1,29 @@ +<% encode_json(\@result) %>\ +<%init> +die 'access denied' + unless $FS::CurrentUser::CurrentUser->access_right('Edit customer'); + +my $sub = $cgi->param('sub'); +my $email = $cgi->param('arg'); +my @where = ( + "cust_main_invoice.dest != 'POST'", + "cust_main_invoice.dest LIKE ".dbh->quote('%'.$email.'%'), + $FS::CurrentUser::CurrentUser->agentnums_sql(table => 'cust_main'), +); +my @cust_main = qsearch({ + 'table' => 'cust_main', + 'select' => 'cust_main.*, cust_main_invoice.dest', + 'addl_from' => 'JOIN cust_main_invoice USING (custnum)', + 'extra_sql' => 'WHERE '.join(' AND ', @where), +}); + +my @result = map { + [ $_->custnum, + $_->name, + $_->dest, + $_->invoice_noemail, + $_->message_noemail, + ] +} @cust_main; + +</%init> diff --git a/httemplate/misc/xmlhttp-cust_main-search.cgi b/httemplate/misc/xmlhttp-cust_main-search.cgi index acf7e70e2..73c9ff8ec 100644 --- a/httemplate/misc/xmlhttp-cust_main-search.cgi +++ b/httemplate/misc/xmlhttp-cust_main-search.cgi @@ -5,7 +5,7 @@ % # 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'); @@ -22,14 +22,14 @@ % @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 [] % } @@ -47,7 +47,7 @@ % city => $_->city, % }; % } -<% objToJson($return) %> +<% encode_json($return) %>\ % } <%init> diff --git a/httemplate/misc/xmlhttp-ping.html b/httemplate/misc/xmlhttp-ping.html index e99303207..01baa3f57 100644 --- a/httemplate/misc/xmlhttp-ping.html +++ b/httemplate/misc/xmlhttp-ping.html @@ -1,4 +1,4 @@ -<% objToJson($return) %> +<% encode_json($return) %>\ <%init> my $conf = new FS::Conf; diff --git a/httemplate/misc/xmlhttp-svc_broadband-search.cgi b/httemplate/misc/xmlhttp-svc_broadband-search.cgi new file mode 100644 index 000000000..578e6140e --- /dev/null +++ b/httemplate/misc/xmlhttp-svc_broadband-search.cgi @@ -0,0 +1,22 @@ +% 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> diff --git a/httemplate/pref/pref-process.html b/httemplate/pref/pref-process.html index c4fef0311..6b94f7175 100644 --- a/httemplate/pref/pref-process.html +++ b/httemplate/pref/pref-process.html @@ -49,6 +49,7 @@ unless ( $error ) { # if ($access_user) { #XXX autogen my @paramlist = qw( locale menu_position default_customer_view + history_order spreadsheet_format mobile_menu enable_fuzzy_on_exact disable_html_editor disable_enter_submit_onetimecharge @@ -57,7 +58,7 @@ unless ( $error ) { # if ($access_user) { vonage-fromnumber vonage-username vonage-password cust_pkg-display_times show_pkgnum show_confitem_counts export_getsettings - show_db_profile save_db_profile + show_db_profile save_db_profile save_tmp_typesetting height width availHeight availWidth colorDepth ); diff --git a/httemplate/pref/pref.html b/httemplate/pref/pref.html index 1e9671dcc..5babb0181 100644 --- a/httemplate/pref/pref.html +++ b/httemplate/pref/pref.html @@ -75,6 +75,21 @@ Interface </SELECT> </TD> </TR> + +% my $history_order = $curuser->option('history_order') || 'oldest'; + <TR> + <TH ALIGN="right">Customer history sort order: </TH> + <TD COLSPAN=2> + <& /elements/select.html, + field => 'history_order', + curr_value => $history_order, + options => [ 'oldest', 'newest' ], + labels => { 'oldest' => 'Oldest first', + 'newest' => 'Newest first', + }, + &> + </TD> + </TR> <TR> <TH ALIGN="right">Spreadsheet download format: </TH> @@ -92,7 +107,7 @@ Interface </TR> <TR> - <TH ALIGN="right" COLSPAN=1>Enable approximate customer searching even when an exact match is found: </TH> + <TH ALIGN="right" COLSPAN=1>Enable approximate customer searching <BR>even when an exact match is found: </TH> <TD ALIGN="left" COLSPAN=2> <INPUT TYPE="checkbox" NAME="enable_fuzzy_on_exact" VALUE="1" <% $curuser->option('enable_fuzzy_on_exact') ? 'CHECKED' : '' %>> </TD> @@ -157,6 +172,10 @@ Development <TH>Save database profiling logs (when available): </TH> <TD><INPUT TYPE="checkbox" NAME="save_db_profile" VALUE="1" <% $curuser->option('save_db_profile') ? 'CHECKED' : '' %>></TD> </TR> + <TR> + <TH>Save temporary invoice typesetting files: </TH> + <TD><INPUT TYPE="checkbox" NAME="save_tmp_typesetting" VALUE="1" <% $curuser->option('save_tmp_typesetting') ? 'CHECKED' : '' %>></TD> + </TR> </TABLE> <BR> diff --git a/httemplate/search/477.html b/httemplate/search/477.html index 6f5fcdf3b..eed3df946 100755 --- a/httemplate/search/477.html +++ b/httemplate/search/477.html @@ -3,6 +3,14 @@ <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> @@ -38,8 +46,11 @@ % 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) %>> % } @@ -97,6 +108,11 @@ for(my $i=0; $i < scalar(@part2b_row_option); $i++) { &FS::Report::FCC_477::save_fcc477map("part2b_row_option_$i",$part2b_row_option[$i]); } +my $part5_report_option = $cgi->param('part5_report_option'); +if ( $part5_report_option ) { + FS::Report::FCC_477::save_fcc477map('part5_report_option', $part5_report_option); +} + my $url_mangler = sub { my $part = shift; my $url = $cgi->url('-path_info' => 1, '-full' => 1); diff --git a/httemplate/search/477partIA.html b/httemplate/search/477partIA.html new file mode 100755 index 000000000..1cd0b70e0 --- /dev/null +++ b/httemplate/search/477partIA.html @@ -0,0 +1,165 @@ +% 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> diff --git a/httemplate/search/477partIA_detail.html b/httemplate/search/477partIA_detail.html deleted file mode 100755 index 66f3a8651..000000000 --- a/httemplate/search/477partIA_detail.html +++ /dev/null @@ -1,129 +0,0 @@ -<% include( 'elements/search.html', - 'html_init' => $html_init, - 'name' => 'lines', - 'query' => $query, - 'count_query' => $count_query, - 'really_disable_download' => 1, - 'disable_download' => 1, - 'nohtmlheader' => 1, - 'disable_total' => 1, - 'header' => [ '', @column_option_name ], - 'xml_elements' => [ @xml_elements ], - 'xml_omit_empty' => 1, - 'fields' => [ @fields ], - ) -%> -<%init> - -my $curuser = $FS::CurrentUser::CurrentUser; - -die "access denied" - unless $curuser->access_right('List packages'); - -my %opt = @_; -my %search_hash = (); - -for ( qw(agentnum magic state) ) { - $search_hash{$_} = $cgi->param($_) if $cgi->param($_); -} -$search_hash{'country'} = 'US'; - -$search_hash{'classnum'} = [ $cgi->param('classnum') ]; - -my @column_option = grep { /^\d+/ } $cgi->param('part1_column_option') - if $cgi->param('part1_column_option'); - -my @row_option = grep { /^\d+/ } $cgi->param('part1_row_option') - if $cgi->param('part1_row_option'); - -my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi); - -my @column_option_name = scalar(@column_option) - ? ( map { my $part_pkg_report_option = - qsearchs({ 'table' => 'part_pkg_report_option', - 'hashref' => { num => $_ }, - }); - $part_pkg_report_option ? $part_pkg_report_option->name - : 'no such report option'; - } @column_option - ) - : ( 'all packages' ); - -my $where = join(' OR ', map { "num = $_" } @row_option ); -my %row_option_name = $where ? - ( map { $_->num => $_->name } - qsearch({ 'table' => 'part_pkg_report_option', - 'hashref' => {}, - 'extra_sql' => "WHERE $where", - }) - ) : - (); - -my $tech_code = $opt{tech_code}; -my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown'; -my $html_init = "<H2>Part IA $technology breakdown by speeds</H2>"; -my $xml_prefix = 'PartIA_'. chr(65 + $tech_code); - -if ($cgi->param('_type') eq 'xml') { - #rotate data pi/2 - my @temp = @column_option; - @column_option = @row_option; - @row_option = @temp; -} - -my $query = 'SELECT '. join(' UNION ALL SELECT ',@row_option); -my $count_query = 'SELECT '. scalar(@row_option); - -my $xml_element = 'OOPS, I was never set'; -my $rowchar = 101; # 'e' -- rows are columns! (pi/2) - -my $value = sub { - my ($rowref, $column) = (shift, shift); - my $row = $rowref->[0]; - - if ($column eq 'name') { - return $row_option_name{$row} || 'no such report option'; - } elsif ( $column =~ /^(\d+)$/ ) { - my @report_option = ( $row || '', - $column_option[$column] || '', - $technology_option[$tech_code] || '', - ); - - my ( $count, $residential ) = FS::cust_pkg->fcc_477_count( - { %search_hash, 'report_option' => join(',', @report_option) } - ); - - my $percentage = sprintf('%.2f', $count ? 100 * $residential / $count : 0); - my $return = $count; - - if ($cgi->param('_type') eq 'xml') { - $rowchar++ if $column == 0; - $xml_element = $xml_prefix. chr($rowchar). ($column+1); - $return = '' if $count == 0 and $cgi->param('_type') eq 'xml'; - } else { - $return .= "<BR>$percentage% residential"; - } - - return $return; - } else { - return '<FONT SIZE="+1" COLOR="#ff0000">Bad call to column_value</FONT>'; - } -}; - -my @fields = map { my $ci = $_; sub { &{$value}(shift, $ci); } } - ( 'name', (0 .. $#column_option) ); -shift @fields if $cgi->param('_type') eq 'xml'; - -my @xml_elements = ( # -- columns are rows! (pi/2) - sub { return $xml_element; }, - sub { return $xml_element; }, - sub { return $xml_element; }, - sub { return $xml_element; }, - sub { return $xml_element; }, - sub { return $xml_element; }, - sub { return $xml_element; }, - sub { return $xml_element; }, - sub { return $xml_element; }, -); - -</%init> diff --git a/httemplate/search/477partIA_summary.html b/httemplate/search/477partIA_summary.html deleted file mode 100755 index f5c2bc251..000000000 --- a/httemplate/search/477partIA_summary.html +++ /dev/null @@ -1,89 +0,0 @@ -<% include( 'elements/search.html', - 'html_init' => $html_init, - 'name' => 'lines', - 'query' => 'SELECT 1', - 'count_query' => 'SELECT 1', - 'really_disable_download' => 1, - 'disable_download' => 1, - 'nohtmlheader' => 1, - 'disable_total' => 1, - 'header' => [ - 'Total Connections', - '% owned loop', - '% billed to end users', - '% residential', - '% residential > 200kbps', - ], - 'xml_elements' => [ - $xml_prefix. 'a1', - $xml_prefix. 'b1', - $xml_prefix. 'c1', - $xml_prefix. 'd1', - $xml_prefix. 'e1', - ], - 'fields' => [ - sub { $total_count }, - sub { '100.00' }, - sub { '100.00' }, - sub { $total_percentage }, - sub { $above_200_percentage }, - ], - ) -%> -<%init> - -my $curuser = $FS::CurrentUser::CurrentUser; - -die "access denied" - unless $curuser->access_right('List packages'); - -my %opt = @_; -my %search_hash = (); - -for ( qw(agentnum magic state) ) { - $search_hash{$_} = $cgi->param($_) if $cgi->param($_); -} -$search_hash{'country'} = 'US'; -$search_hash{'classnum'} = [ $cgi->param('classnum') ]; - -my @column_option = grep { /^\d+$/ } $cgi->param('part1_column_option') - if $cgi->param('part1_column_option'); - -my @row_option = grep { /^\d+$/ } $cgi->param('part1_row_option') - if $cgi->param('part1_row_option'); - -my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi); - -my $total_count = 0; -my $total_residential = 0; -my $above_200 = 0; -my $tech_code = $opt{tech_code}; -my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown'; -my $html_init = "<H2>Part IA $technology totals</H2>"; -my $xml_prefix = 'PartIA_'. chr(65 + $tech_code); - -my $not_first_row = 0; # ugh; -foreach my $row ( @row_option ) { - foreach my $column ( @column_option ) { - - my @report_option = ( $row || '-1', $column || '-1', $technology_option[$tech_code] ); - - my ( $count, $residential ) = FS::cust_pkg->fcc_477_count( - { %search_hash, 'report_option' => join(',', @report_option) } - ); - - $total_count += $count; - $total_residential += $residential; - $above_200 += $residential if $not_first_row; - } - $not_first_row++; -} - -my $total_percentage = - sprintf("%.2f", $total_count ? 100*$total_residential/$total_count : 0); - -my $above_200_percentage = - sprintf("%.2f", $total_count ? 100*$above_200/$total_count : 0); - - -</%init> diff --git a/httemplate/search/477partIIA.html b/httemplate/search/477partIIA.html index d2cc8c3e9..95c00a3e0 100755 --- a/httemplate/search/477partIIA.html +++ b/httemplate/search/477partIIA.html @@ -1,17 +1,44 @@ -<% include( 'elements/search.html', - 'html_init' => $html_init, - 'name' => 'lines', - 'query' => $query, - 'count_query' => 'SELECT 11', - 'really_disable_download' => 1, - 'disable_download' => 1, - 'nohtmlheader' => 1, - 'disable_total' => 1, - 'header' => [ @headers ], - 'xml_elements' => [ @xml_elements ], - 'fields' => [ @fields ], - ) -%> +% if ( $cgi->param('_type') eq 'xml' ) { +% my @cols = qw(a b c d); +% for ( my $row = 0; $row < scalar(@rows); $row++ ) { +% for my $col (0..3) { +% if ( exists($data[$col][$row]) and $data[$col][$row] > 0 ) { +<PartII_<% $row + 1 %><% $cols[$col] %>>\ +<% $data[$col][$row] %>\ +</PartII_<% $row + 1 %><% $cols[$col] %>> +% } +% } #for $col +% } #for $row +% } else { # HTML mode +% # fake up the search-html.html header +<H2>Part IIA</H2> +<TABLE> + <TR><TD VALIGN="bottom"><BR></TD></TR> + <TR><TD COLSPAN=2> + <TABLE CLASS="grid" CELLSPACING=0> + <TR> +% foreach (@row1_headers) { + <TH><% $_ %></TH> +% } + </TR> +% my $row = 0; +% foreach my $rowhead (@rows) { + <TR CLASS="row<%$row % 2%>"> + <TD STYLE="text-align: left; font-weight: bold"><% $rowhead %></TD> +% for my $col (0..3) { + <TD> +% if ( exists($data[$col][$row]) ) { + <% $data[$col][$row] %> +% } + </TD> +% } # for $col + </TR> +% $row++; +% } #for $rowhead + </TABLE> + </TD></TR> +</TABLE> +% } #XML/HTML <%init> my $curuser = $FS::CurrentUser::CurrentUser; @@ -19,83 +46,76 @@ 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', @@ -103,13 +123,12 @@ my @rows = ( '% 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> diff --git a/httemplate/search/477partIIB.html b/httemplate/search/477partIIB.html index c58310d36..5b9b30769 100755 --- a/httemplate/search/477partIIB.html +++ b/httemplate/search/477partIIB.html @@ -3,9 +3,10 @@ % 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 @@ -14,19 +15,18 @@ <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] %> % } diff --git a/httemplate/search/477partV.html b/httemplate/search/477partV.html index 2fd5119d1..b2dd9ca95 100755 --- a/httemplate/search/477partV.html +++ b/httemplate/search/477partV.html @@ -1,4 +1,7 @@ -<% include( 'elements/search.html', +% if ( $cgi->param('_type') =~ /^xml$/ ) { +<zip_code> +% } +<& elements/search.html, 'html_init' => $html_init, 'name' => 'zip code', 'query' => $sql_query, @@ -12,8 +15,11 @@ 'url' => $opt{url} || '', 'really_disable_download' => 1, - ) -%> + +&> +% if ( $cgi->param('_type') =~ /^xml$/ ) { +</zip_code> +% } <%init> my $curuser = $FS::CurrentUser::CurrentUser; @@ -32,8 +38,8 @@ for ( qw(agentnum magic state) ) { } $search_hash{'country'} = 'US'; $search_hash{'classnum'} = [ $cgi->param('classnum') ]; -$search_hash{report_option} = $cgi->param('partv_report_option') - if $cgi->param('partv_report_option'); +$search_hash{report_option} = $cgi->param('part5_report_option') + if $cgi->param('part5_report_option'); my $sql_query = FS::cust_pkg->search( { %search_hash, 'fcc_line' => 1, diff --git a/httemplate/search/477partVI_census.html b/httemplate/search/477partVI_census.html index 8425c4b48..59a6fb50d 100755 --- a/httemplate/search/477partVI_census.html +++ b/httemplate/search/477partVI_census.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'html_init' => '<H2>Part VI</H2>', 'html_foot' => $html_foot, 'name' => 'regions', @@ -24,8 +24,8 @@ 'url' => $opt{url} || '', 'xml_row_element' => 'Datarow', 'really_disable_download' => 1, - ) -%> + +&> <%init> my $curuser = $FS::CurrentUser::CurrentUser; diff --git a/httemplate/search/agent_commission.html b/httemplate/search/agent_commission.html new file mode 100644 index 000000000..b94ae9f6e --- /dev/null +++ b/httemplate/search/agent_commission.html @@ -0,0 +1,197 @@ +%# still not a good way to do rows grouped by some field in a search.html +%# report +% if ( $type eq 'xls' ) { +<% $data %>\ +% } else { +<& /elements/header.html, $title &> +<P ALIGN="right" CLASS="noprint"> +Download full results<BR> +as <A HREF="<% $cgi->self_url %>;_type=xls">Excel spreadsheet</A></P> +<BR> +<STYLE TYPE="text/css"> +td.cust_head { + border-left: none; + border-right: none; + padding-top: 0.5em; + font-weight: bold; + background-color: #ffffff; +} +td.money { text-align: right; } +td.money:before { content: '<% $money_char %>'; } +.row0 { background-color: #eeeeee; } +.row1 { background-color: #ffffff; } +</STYLE> +<& /elements/table-grid.html &> + <TR STYLE="background-color: #cccccc"> + <TH CLASS="grid">Package</TH> + <TH CLASS="grid">Sales</TH> + <TH CLASS="grid">Percentage</TH> + <TH CLASS="grid">Commission</TH> + </TR> +% my ($custnum, $sales, $commission, $row, $bgcolor) = (0, 0, 0, 0); +% foreach my $cust_pkg ( @cust_pkg ) { +% if ( $custnum ne $cust_pkg->custnum ) { +% # start of a new customer section +% my $cust_main = $cust_pkg->cust_main; +% my $label = $cust_main->custnum . ': '. $cust_main->name; +% $bgcolor = 0; + <TR> + <TD COLSPAN=4 CLASS="cust_head"> + <A HREF="<%$p%>view/cust_main.cgi?<%$cust_main->custnum%>"><% $label %></A> + </TD> + </TR> +% } + <TR CLASS="row<% $bgcolor %>"> + <TD CLASS="grid"><% $cust_pkg->pkg_label %></TD> + <TD CLASS="money"><% sprintf('%.2f', $cust_pkg->sum_charged) %></TD> + <TD ALIGN="right"><% $cust_pkg->percent %>%</TD> + <TD CLASS="money"><% sprintf('%.2f', + $cust_pkg->sum_charged * $cust_pkg->percent / 100) %></TD> + </TR> +% $sales += $cust_pkg->sum_charged; +% $commission += $cust_pkg->sum_charged * $cust_pkg->percent / 100; +% $row++; +% $bgcolor = 1-$bgcolor; +% $custnum = $cust_pkg->custnum; +% } + <TR STYLE="background-color: #f5f6be"> + <TD CLASS="grid"> + <% emt('[quant,_1,package] with commission', $row) %> + </TD> + <TD CLASS="money"><% sprintf('%.2f', $sales) %></TD> + <TD></TD> + <TD CLASS="money"><% sprintf('%.2f', $commission) %></TD> + </TR> +</TABLE> +<& /elements/footer.html &> +% } +<%init> +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my ($begin, $end) = FS::UI::Web::parse_beginning_ending($cgi); +$cgi->param('agentnum') =~ /^(\d+)$/ or die "bad agentnum"; +my $agentnum = $1; +my $agent = FS::agent->by_key($agentnum); + +my $title = $agent->agent . ' commissions'; + +my $sum_charged = + '(SELECT SUM(setup + recur) FROM cust_bill_pkg JOIN cust_bill USING (invnum)'. + 'WHERE cust_bill_pkg.pkgnum = cust_pkg.pkgnum AND '. + "cust_bill._date >= $begin AND cust_bill._date < $end)"; + +my @select = ( + 'cust_pkg.*', + 'agent_pkg_class.commission_percent AS percent', + "$sum_charged AS sum_charged", +); + +my $query = { + 'table' => 'cust_pkg', + 'select' => join(',', @select), + 'addl_from' => 'JOIN cust_main USING (custnum) '. + 'JOIN part_pkg USING (pkgpart) '. + 'JOIN agent_pkg_class ON ( '. + 'cust_main.agentnum = agent_pkg_class.agentnum AND '. + '( agent_pkg_class.classnum = part_pkg.classnum OR '. + '(agent_pkg_class IS NULL AND part_pkg.classnum IS NULL)'. + ' ) ) ', + 'extra_sql' => "WHERE cust_main.agentnum = $agentnum AND ". + 'agent_pkg_class.commission_percent > 0 AND '. + "$sum_charged > 0", + 'order_by' => 'ORDER BY cust_pkg.custnum ASC', +}; + +my @cust_pkg = qsearch($query); + +my $money_char = FS::Conf->new->config('money_char') || '$'; + +my $data = ''; +my $type = $cgi->param('_type'); +if ( $type eq 'xls') { + # some false laziness with the above... + my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format; + my $filename = 'agent_commission' . $format->{extension}; + http_header('Content-Type' => $format->{mime_type}); + http_header('Content-Disposition' => qq!attachment;filename="$filename"!); + my $XLS = IO::Scalar->new(\$data); + my $workbook = $format->{class}->new($XLS); + my $worksheet = $workbook->add_worksheet(substr($title, 0, 31)); + + my $cust_head_format = $workbook->add_format( + bold => 1, + underline => 1, + text_wrap => 0, + bg_color => 'white', + ); + + my $col_head_format = $workbook->add_format( + bold => 1, + align => 'center', + bg_color => 'silver' + ); + + my @format; + foreach (0, 1) { + my %bg = (bg_color => $_ ? 'white' : 'silver'); + $format[$_] = { + 'text' => $workbook->add_format(%bg), + 'money' => $workbook->add_format(%bg, num_format => $money_char.'#0.00'), + 'percent' => $workbook->add_format(%bg, num_format => '0.00%'), + }; + } + my $total_format = $workbook->add_format( + bg_color => 'yellow', + num_format => $money_char.'#0.00', + top => 1 + ); + + my ($r, $c) = (0, 0); + foreach (qw(Package Sales Percentage Commission)) { + $worksheet->write($r, $c++, $_, $col_head_format); + } + $r++; + + my ($custnum, $sales, $commission, $row, $bgcolor) = (0, 0, 0, 0); + my $label_length = 0; + foreach my $cust_pkg ( @cust_pkg ) { + if ( $custnum ne $cust_pkg->custnum ) { + # start of a new customer section + my $cust_main = $cust_pkg->cust_main; + my $label = $cust_main->custnum . ': '. $cust_main->name; + $bgcolor = 0; + $worksheet->set_row($r, 20); + $worksheet->merge_range($r, 0, $r, 3, $label, $cust_head_format); + $r++; + } + $c = 0; + my $percent = $cust_pkg->percent / 100; + $worksheet->write($r, $c++, $cust_pkg->pkg_label, $format[$bgcolor]{text}); + $worksheet->write($r, $c++, $cust_pkg->sum_charged, $format[$bgcolor]{money}); + $worksheet->write($r, $c++, $percent, $format[$bgcolor]{percent}); + $worksheet->write($r, $c++, ($cust_pkg->sum_charged * $percent), + $format[$bgcolor]{money}); + + $label_length = max($label_length, length($cust_pkg->pkg_label)); + $sales += $cust_pkg->sum_charged; + $commission += $cust_pkg->sum_charged * $cust_pkg->percent / 100; + $row++; + $bgcolor = 1-$bgcolor; + $custnum = $cust_pkg->custnum; + $r++; + } + + $c = 0; + $label_length = max($label_length, 20); + $worksheet->set_column($c, $c, $label_length); + $worksheet->write($r, $c++, mt('[quant,_1,package] with commission', $row), + $total_format); + $worksheet->set_column($c, $c + 2, 11); + $worksheet->write($r, $c++, $sales, $total_format); + $worksheet->write($r, $c++, '', $total_format); + $worksheet->write($r, $c++, $commission, $total_format); + + $workbook->close; +} +</%init> diff --git a/httemplate/search/agent_inventory.html b/httemplate/search/agent_inventory.html index ac65371ca..015aca46b 100644 --- a/httemplate/search/agent_inventory.html +++ b/httemplate/search/agent_inventory.html @@ -1,4 +1,4 @@ -<% include('elements/search.html', +<& elements/search.html, 'title' => 'Inventory summary per agent', 'name_singular' => 'agent', 'query' => { 'table' => 'agent', @@ -10,8 +10,7 @@ " AND $agentnums_sql", 'header' => \@header, 'fields' => \@fields, - ) -%> +&> <%init> die "access denied" diff --git a/httemplate/search/bill_batch.cgi b/httemplate/search/bill_batch.cgi index b6676f261..b740bdc68 100755 --- a/httemplate/search/bill_batch.cgi +++ b/httemplate/search/bill_batch.cgi @@ -26,7 +26,7 @@ function start() { % -expires => '-1d', % ); % $r->headers_out->add( 'Set-Cookie' => $cookie->as_string ); -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Invoice Batches', 'name_singular' => 'batch', 'query' => { 'table' => 'bill_batch', @@ -67,9 +67,7 @@ function start() { 'agent_pos' => 1, 'html_foot' => include('.foot'), - ) - -%> +&> %} <%def .foot> <SCRIPT type="text/javascript"> diff --git a/httemplate/search/cdr.html b/httemplate/search/cdr.html index d0d7292d1..ca303d386 100644 --- a/httemplate/search/cdr.html +++ b/httemplate/search/cdr.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => $title, 'name' => 'call detail records', 'query' => $query, @@ -9,27 +9,8 @@ 'fields' => \@fields, 'links' => \@links, 'html_form' => qq!<FORM NAME="cdrForm" ACTION="$p/misc/cdr.cgi" METHOD="POST">!, - #false laziness w/queue.html - 'html_foot' => sub { - if ( $areboxes ) { - '<BR><INPUT TYPE="button" VALUE="select all" onClick="setAll(true)">'. - '<INPUT TYPE="button" VALUE="unselect all" onClick="setAll(false)">'. - qq!<BR><INPUT TYPE="submit" NAME="action" VALUE="reprocess selected" onClick="return confirm('Are you sure you want to reprocess the selected CDRs?')">!. - qq!<INPUT TYPE="submit" NAME="action" VALUE="delete selected" onClick="return confirm('Are you sure you want to delete the selected CDRs?')"><BR>!. - '<SCRIPT TYPE="text/javascript">'. - ' function setAll(setTo) { '. - ' theForm = document.cdrForm;'. - ' for (i=0,n=theForm.elements.length;i<n;i++)'. - ' if (theForm.elements[i].name.indexOf("acctid") != -1)'. - ' theForm.elements[i].checked = setTo;'. - ' }'. - '</SCRIPT>'; - } else { - ''; - } - }, - ) -%> + 'html_foot' => $html_foot, +&> <%init> die "access denied" @@ -44,8 +25,6 @@ my $totalminutes_sub = sub { my $conf = new FS::Conf; -my $areboxes = 0; - my $title = 'Call Detail Records'; my $hashref = {}; @@ -355,7 +334,6 @@ my %links = ( @fields = map { exists($fields{$_}) ? $fields{$_} : $_ } @fields; unshift @fields, sub { return '' unless $edit_data; - $areboxes = 1; my $cdr = shift; my $acctid = $cdr->acctid; qq!<INPUT NAME="acctid$acctid" TYPE="checkbox" VALUE="1">!; @@ -409,4 +387,14 @@ if ( $topmode ) { $nototalminutes = 1; } +my $html_foot = include('/search/elements/checkbox-foot.html', + actions => [ + { submit => "reprocess selected", + name => "action", + confirm => "Are you sure you want to reprocess the selected CDRs?" }, + { submit => "delete selected", + name => "action", + confirm => "Are you sure you want to delete the selected CDRs?" }, + ] +); </%init> diff --git a/httemplate/search/cust_bill.html b/httemplate/search/cust_bill.html index 3c0530e4f..473aed311 100755 --- a/httemplate/search/cust_bill.html +++ b/httemplate/search/cust_bill.html @@ -62,7 +62,7 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('List invoices'); -my $join_cust_main = 'LEFT JOIN cust_main USING ( custnum )'; +my $join_cust_main = FS::UI::Web::join_cust_main('cust_bill'); #here is the agent virtualization my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql; @@ -97,7 +97,7 @@ if ( $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/ ) { $search{'refnum'} = $1; } - if ( $cgi->param('cust_classnum') ) { +if ( grep { $_ eq 'cust_classnum' } $cgi->param ) { $search{'cust_classnum'} = [ $cgi->param('cust_classnum') ]; } @@ -198,7 +198,6 @@ if ( $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/ ) { }; } - my $link = [ "${p}view/cust_bill.cgi?", 'invnum', ]; my $clink = sub { my $cust_bill = shift; diff --git a/httemplate/search/cust_bill_event.cgi b/httemplate/search/cust_bill_event.cgi index 90c89139c..9fb533a5f 100644 --- a/httemplate/search/cust_bill_event.cgi +++ b/httemplate/search/cust_bill_event.cgi @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => $title, 'html_init' => $html_init, 'menubar' => $menubar, @@ -60,8 +60,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> my $curuser = $FS::CurrentUser::CurrentUser; @@ -100,7 +100,7 @@ my $where = 'WHERE '. FS::cust_bill_event->search_sql_where( \%search ); my $join = 'LEFT JOIN part_bill_event USING ( eventpart ) '. 'LEFT JOIN cust_bill USING ( invnum ) '. - 'LEFT JOIN cust_main USING ( custnum ) '; + FS::UI::Web::join_cust_main('cust_bill'); my $sql_query = { 'table' => 'cust_bill_event', diff --git a/httemplate/search/cust_bill_pay.html b/httemplate/search/cust_bill_pay.html index 79de74985..ff20458d8 100644 --- a/httemplate/search/cust_bill_pay.html +++ b/httemplate/search/cust_bill_pay.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => $title, 'name' => 'net payments', 'query' => $sql_query, @@ -71,8 +71,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> die "access denied" @@ -99,9 +99,12 @@ if ( $cgi->param('refnum') && $cgi->param('refnum') =~ /^(\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, 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; } @@ -117,8 +120,8 @@ my $where = 'WHERE '. join(' AND ', @search); # my $count_query = 'SELECT COUNT(*), SUM(amount) FROM cust_bill_pay - LEFT JOIN cust_bill USING ( invnum ) - LEFT JOIN cust_main USING ( custnum ) '. + LEFT JOIN cust_bill USING ( invnum ) '. + FS::UI::Web::join_cust_main('cust_bill') . $where; my $sql_query = { @@ -137,8 +140,8 @@ my $sql_query = { 'hashref' => {}, 'extra_sql' => $where, 'addl_from' => 'LEFT JOIN cust_bill USING ( invnum ) - LEFT JOIN cust_pay USING ( paynum ) - LEFT JOIN cust_main ON ( cust_bill.custnum = cust_main.custnum )', + LEFT JOIN cust_pay USING ( paynum ) '. + FS::UI::Web::join_cust_main('cust_bill') }; my $cust_bill_link = sub { diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index 1e67e9320..3a3b0feb9 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -222,9 +222,9 @@ if ( $conf->exists('enable_taxclasses') ) { # valid in both the tax and non-tax cases my $join_cust = - " LEFT JOIN cust_bill USING (invnum) - LEFT JOIN cust_main USING (custnum) - "; + " LEFT JOIN cust_bill USING (invnum)". + # use cust_pkg.locationnum if it exists + FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg'); #agent virtualization my $agentnums_sql = @@ -260,13 +260,16 @@ if ( $cgi->param('refnum') =~ /^(\d+)$/ ) { 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"; @@ -278,11 +281,11 @@ my $join_pkg = LEFT JOIN part_pkg USING (pkgpart)'; my $part_pkg = 'part_pkg'; -if ( $cgi->param('use_override') ) { +if ( $cgi->param('use_override') ) { #"Separate sub-packages from parents" # still need the real part_pkg for tax applicability, # so alias this one $join_pkg .= " LEFT JOIN part_pkg AS override ON ( - COALESCE(cust_bill_pkg.pkgpart_override, cust_pkg.pkgpart, 0) = part_pkg.pkgpart + COALESCE(cust_bill_pkg.pkgpart_override, cust_pkg.pkgpart, 0) = override.pkgpart )"; $part_pkg = 'override'; } @@ -559,12 +562,11 @@ if ( $cgi->param('nottax') ) { #total payments -my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount) AS pay_amount, - billpkgnum - FROM cust_bill_pay_pkg - GROUP BY billpkgnum"; -$join_pkg .= " LEFT JOIN ($pay_sub) AS item_pay USING (billpkgnum)"; -push @select, 'item_pay.pay_amount'; +my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount) + FROM cust_bill_pay_pkg + WHERE cust_bill_pkg.billpkgnum = cust_bill_pay_pkg.billpkgnum + "; +push @select, "($pay_sub) AS pay_amount"; # credit @@ -630,13 +632,12 @@ if ( $cgi->param('credit') ) { #still want a credit total column - my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount, - billpkgnum - FROM cust_credit_bill_pkg - GROUP BY billpkgnum"; - $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit USING (billpkgnum)"; - - push @select, 'item_credit.credit_amount'; + my $credit_sub = " + SELECT SUM(cust_credit_bill_pkg.amount) + FROM cust_credit_bill_pkg + WHERE cust_bill_pkg.billpkgnum = cust_credit_bill_pkg.billpkgnum + "; + push @select, "($credit_sub) AS credit_amount"; } @@ -647,7 +648,7 @@ $where &&= "WHERE $where"; my $query = { 'table' => 'cust_bill_pkg', - 'addl_from' => "$join_cust $join_pkg", + 'addl_from' => "$join_pkg $join_cust", 'hashref' => {}, 'select' => join(",\n", @select ), 'extra_sql' => $where, @@ -656,7 +657,7 @@ my $query = { my $count_query = 'SELECT ' . join(',', @total) . - " FROM cust_bill_pkg $join_cust $join_pkg + " FROM cust_bill_pkg $join_pkg $join_cust $where"; @peritem_desc = map {emt($_)} @peritem_desc; diff --git a/httemplate/search/cust_bill_pkg_discount.html b/httemplate/search/cust_bill_pkg_discount.html index bb8038a44..f598341a0 100644 --- a/httemplate/search/cust_bill_pkg_discount.html +++ b/httemplate/search/cust_bill_pkg_discount.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Discounts', 'name' => 'discounts', 'query' => $query, @@ -68,8 +68,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> #a little false laziness below w/cust_bill_pkg.cgi @@ -127,12 +127,12 @@ my $join_cust_pkg_discount = 'LEFT JOIN cust_pkg_discount USING (pkgdiscountnum)'; my $join_cust = - ' JOIN cust_bill_pkg USING ( billpkgnum ) - JOIN cust_bill USING ( invnum ) - LEFT JOIN cust_main USING ( custnum ) '; + ' JOIN cust_bill USING ( invnum ) '. + FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg'); my $join_pkg = - ' LEFT JOIN cust_pkg ON ( cust_bill_pkg.pkgnum = cust_pkg.pkgnum ) + ' JOIN cust_bill_pkg USING ( billpkgnum ) + LEFT JOIN cust_pkg ON ( cust_bill_pkg.pkgnum = cust_pkg.pkgnum ) LEFT JOIN part_pkg USING ( pkgpart ) '; #LEFT JOIN part_pkg AS override # ON pkgpart_override = override.pkgpart '; @@ -140,7 +140,7 @@ my $join_pkg = my $where = ' WHERE '. join(' AND ', @where); $count_query .= - " FROM cust_bill_pkg_discount $join_cust_pkg_discount $join_cust $join_pkg ". + " FROM cust_bill_pkg_discount $join_cust_pkg_discount $join_pkg $join_cust ". $where; my @select = ( @@ -155,7 +155,7 @@ push @select, 'cust_main.custnum', my $query = { 'table' => 'cust_bill_pkg_discount', - 'addl_from' => "$join_cust_pkg_discount $join_cust $join_pkg", + 'addl_from' => "$join_cust_pkg_discount $join_pkg $join_cust", 'hashref' => {}, 'select' => join(', ', @select ), 'extra_sql' => $where, diff --git a/httemplate/search/cust_bill_pkg_referral.html b/httemplate/search/cust_bill_pkg_referral.html index 1289ff7ee..c4dde32a0 100644 --- a/httemplate/search/cust_bill_pkg_referral.html +++ b/httemplate/search/cust_bill_pkg_referral.html @@ -156,9 +156,13 @@ if ( @refnum ) { 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+)$/ ) { diff --git a/httemplate/search/cust_credit.html b/httemplate/search/cust_credit.html index f5d8fa19f..cabf8c002 100755 --- a/httemplate/search/cust_credit.html +++ b/httemplate/search/cust_credit.html @@ -103,9 +103,13 @@ if ( $cgi->param('refnum') && $cgi->param('refnum') =~ /^(\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; } @@ -137,7 +141,7 @@ my $where = 'WHERE '. join(' AND ', @search); my $count_query = 'SELECT COUNT(*), SUM(amount) '; $count_query .= ', SUM(' . FS::cust_credit->unapplied_sql . ') ' if $unapplied; -$count_query .= 'FROM cust_credit LEFT JOIN cust_main USING ( custnum ) '. +$count_query .= 'FROM cust_credit'. FS::UI::Web::join_cust_main('cust_credit'). $where; my @count_addl = ( $money_char.'%.2f total credited (gross)' ); @@ -148,7 +152,7 @@ my $sql_query = { 'select' => join(', ',@select), 'hashref' => {}, 'extra_sql' => $where, - 'addl_from' => 'LEFT JOIN cust_main USING ( custnum )', + 'addl_from' => FS::UI::Web::join_cust_main('cust_credit') }; </%init> diff --git a/httemplate/search/cust_credit_bill.html b/httemplate/search/cust_credit_bill.html index 9fd6a987a..88f897d70 100644 --- a/httemplate/search/cust_credit_bill.html +++ b/httemplate/search/cust_credit_bill.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => $title, 'name' => 'net credits', 'query' => $sql_query, @@ -64,8 +64,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> die "access denied" @@ -103,8 +103,8 @@ my $where = 'WHERE '. join(' AND ', @search); # my $count_query = 'SELECT COUNT(*), SUM(amount) FROM cust_credit_bill - LEFT JOIN cust_bill USING ( invnum ) - LEFT JOIN cust_main USING ( custnum ) '. + LEFT JOIN cust_bill USING ( invnum ) '. + FS::UI::Web::join_cust_main('cust_bill') . $where; my $sql_query = { @@ -121,8 +121,8 @@ my $sql_query = { 'hashref' => {}, 'extra_sql' => $where, 'addl_from' => 'LEFT JOIN cust_bill USING ( invnum ) - LEFT JOIN cust_credit USING ( crednum ) - LEFT JOIN cust_main ON ( cust_bill.custnum = cust_main.custnum )', + LEFT JOIN cust_credit USING ( crednum )'. + FS::UI::Web::join_cust_main('cust_bill') }; my $cust_bill_link = sub { diff --git a/httemplate/search/cust_credit_bill_pkg.html b/httemplate/search/cust_credit_bill_pkg.html index 06fd881a8..63d70c27e 100644 --- a/httemplate/search/cust_credit_bill_pkg.html +++ b/httemplate/search/cust_credit_bill_pkg.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Credit application detail', #to line item 'name_singular' => 'credit application', 'query' => $query, @@ -16,6 +16,7 @@ # line item 'Description', + 'Location', @post_desc_header, #invoice @@ -35,6 +36,7 @@ ? $_[0]->get('pkg') # possibly use override.pkg : $_[0]->get('itemdesc') # but i think this correct }, + $location_sub, @post_desc, 'invnum', sub { time2str('%b %d %Y', shift->_date ) }, @@ -46,6 +48,7 @@ '', #'otaker', '', #reason '', #line item description + '', #location @post_desc_null, 'invnum', '_date', @@ -57,6 +60,7 @@ '', '', '', + '', @post_desc_null, $ilink, $ilink, @@ -64,7 +68,7 @@ FS::UI::Web::cust_header() ), ], - 'align' => 'rrlll'. + 'align' => 'rrllll'. $post_desc_align. 'rr'. FS::UI::Web::cust_aligns(), @@ -74,6 +78,7 @@ '', '', '', + '', @post_desc_null, '', '', @@ -85,13 +90,14 @@ '', '', '', + '', @post_desc_null, '', '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> #LOTS of false laziness below w/cust_bill_pkg.cgi @@ -377,8 +383,8 @@ my $count_query = "SELECT COUNT(DISTINCT creditbillpkgnum), SUM(cust_credit_bill_pkg.amount)"; my $join_cust = - ' JOIN cust_bill ON ( cust_bill_pkg.invnum = cust_bill.invnum ) - LEFT JOIN cust_main ON ( cust_bill.custnum = cust_main.custnum ) '; + ' JOIN cust_bill ON ( cust_bill_pkg.invnum = cust_bill.invnum )'. + FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg'); my $join_pkg; @@ -423,10 +429,9 @@ if ( $cgi->param('nottax') ) { s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g for @where; } -} else { +} else { - #die? - warn "neiether nottax nor istax parameters specified"; + #warn "neither nottax nor istax parameters specified"; #same as before? $join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum ) LEFT JOIN part_pkg USING ( pkgpart ) '; @@ -459,7 +464,7 @@ my @post_desc_header = (); my @post_desc = (); my @post_desc_null = (); my $post_desc_align = ''; -if ( $conf->exists('enable_taxclasses') ) { +if ( $conf->exists('enable_taxclasses') && ! $cgi->param('istax') ) { push @post_desc_header, 'Tax class'; push @post_desc, 'taxclass'; push @post_desc_null, ''; @@ -485,4 +490,57 @@ my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ]; my $conf = new FS::Conf; my $money_char = $conf->config('money_char') || '$'; +my $tax_pkg_address = $conf->exists('tax-pkg_address'); +my $tax_ship_address = $conf->exists('tax-ship_address'); + +my $location_sub = sub { + #my $cust_credit_bill_pkg = shift; + my $self = shift; + my $tax_Xlocation = $self->cust_bill_pkg_tax_Xlocation; + if ( defined($tax_Xlocation) && $tax_Xlocation ) { + + if ( ref($tax_Xlocation) eq 'FS::cust_bill_pkg_tax_location' ) { + + if ( $tax_Xlocation->taxtype eq 'FS::cust_main_county' ) { + my $cust_main_county = $tax_Xlocation->cust_main_county; + if ( $cust_main_county ) { + $cust_main_county->label; + } else { + ''; #cust_main_county record is gone... history? yuck. + } + } else { + '(CCH tax_rate)'; #XXX FS::tax_rate.. vendor taxes not yet handled here + } + + } elsif ( ref($tax_Xlocation) eq 'FS::cust_bill_pkg_tax_rate_location' ) { + '(CCH)'; #XXX vendor taxes not yet handled here + } else { + 'unknown tax_Xlocation '. ref($tax_Xlocation); + } + + } else { + + my $cust_bill_pkg = $self->cust_bill_pkg; + if ( $cust_bill_pkg->pkgnum > 0 ) { + my $cust_pkg = $cust_bill_pkg->cust_pkg; + if ( $tax_pkg_address && (my $cust_location = $cust_pkg->cust_location) ){ + $cust_location->county_state_country; + } else { + my $cust_main = $cust_pkg->cust_main; + if ( $tax_ship_address && $cust_main->has_ship_address ) { + $cust_main->county_state_country('ship_'); + } else { + $cust_main->county_state_country; + } + } + + } else { + #tax? we shouldn't have wound up here then... + ''; #return customer ship or bill address? (depending on tax-ship_address) + } + + } + +}; + </%init> diff --git a/httemplate/search/cust_credit_refund.html b/httemplate/search/cust_credit_refund.html index 75138e99d..817420054 100644 --- a/httemplate/search/cust_credit_refund.html +++ b/httemplate/search/cust_credit_refund.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => $title, 'name' => 'net refunds', 'query' => $sql_query, @@ -57,8 +57,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> die "access denied" @@ -85,9 +85,12 @@ if ( $cgi->param('refnum') && $cgi->param('refnum') =~ /^(\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.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; } @@ -103,8 +106,8 @@ my $where = 'WHERE '. join(' AND ', @search); # my $count_query = 'SELECT COUNT(*), SUM(cust_credit_refund.amount) FROM cust_credit_refund - LEFT JOIN cust_credit USING ( crednum ) - LEFT JOIN cust_main USING ( custnum ) '. + LEFT JOIN cust_credit USING ( crednum ) '. + FS::UI::Web::join_cust_main('cust_credit') . $where; my $sql_query = { @@ -121,8 +124,8 @@ my $sql_query = { 'hashref' => {}, 'extra_sql' => $where, 'addl_from' => 'LEFT JOIN cust_credit USING ( crednum ) - LEFT JOIN cust_refund USING ( refundnum ) - LEFT JOIN cust_main ON ( cust_credit.custnum = cust_main.custnum )', + LEFT JOIN cust_refund USING ( refundnum )'. + FS::UI::Web::join_cust_main('cust_credit') }; #my $cust_credit_link = sub { diff --git a/httemplate/search/cust_event.html b/httemplate/search/cust_event.html index deb34b9e5..bfc5f43e8 100644 --- a/httemplate/search/cust_event.html +++ b/httemplate/search/cust_event.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => $title, 'html_init' => $html_init, 'menubar' => $menubar, @@ -62,8 +62,7 @@ #'', FS::UI::Web::cust_styles(), ], - ) -%> +&> <%once> my $status_sub = sub { @@ -175,7 +174,13 @@ $search{'ending'} = $ending; my $where = ' WHERE '. FS::cust_event->search_sql_where( \%search ); -my $join = FS::cust_event->join_sql(); +my $join = FS::cust_event->join_sql() . + 'LEFT JOIN cust_location bill_location '. + 'ON (cust_main.bill_locationnum = bill_location.locationnum) '. + 'LEFT JOIN cust_location ship_location '. + 'ON (cust_main.ship_locationnum = ship_location.locationnum)'; + # warning: does not show the true service address for package events. + # the query to do that would be painfully slow. my $sql_query = { 'table' => 'cust_event', diff --git a/httemplate/search/cust_main-zip.html b/httemplate/search/cust_main-zip.html index 08800d431..f5f8c8f3c 100644 --- a/httemplate/search/cust_main-zip.html +++ b/httemplate/search/cust_main-zip.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Zip code Search Results', 'name' => 'zip codes', 'query' => $sql_query, @@ -6,8 +6,7 @@ 'header' => [ 'Zip code', 'Customers', ], 'fields' => [ 0, 1 ], 'links' => [ '', $link ], - ) -%> +&> <%init> die "access denied" diff --git a/httemplate/search/cust_main.cgi b/httemplate/search/cust_main.cgi index 8e3c8133e..2c09c692c 100755 --- a/httemplate/search/cust_main.cgi +++ b/httemplate/search/cust_main.cgi @@ -244,7 +244,7 @@ % 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 = ''; diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html index 8b39ea962..24348ff8a 100755 --- a/httemplate/search/cust_main.html +++ b/httemplate/search/cust_main.html @@ -42,10 +42,11 @@ my %search_hash = (); #scalars my @scalars = qw ( agentnum status address zip paydate_year paydate_month invoice_terms - no_censustract with_geocode with_email no_POST + no_censustract with_geocode with_email POST no_POST custbatch usernum cancelled_pkgs cust_fields flattened_pkgs + all_tags ); for my $param ( @scalars ) { diff --git a/httemplate/search/cust_pay_batch.cgi b/httemplate/search/cust_pay_batch.cgi index 800df8702..9f9eb30ce 100755 --- a/httemplate/search/cust_pay_batch.cgi +++ b/httemplate/search/cust_pay_batch.cgi @@ -1,4 +1,4 @@ -<% include('elements/search.html', +<& elements/search.html, 'title' => 'Batch payment details', 'name' => 'batch details', 'query' => $sql_query, @@ -7,55 +7,41 @@ 'disable_download' => 1, 'header' => [ '#', 'Inv #', - 'Customer', + 'Cust #', 'Customer', 'Card Name', 'Card', 'Exp', 'Amount', 'Status', + '', # error_message ], - 'fields' => [ sub { - shift->[0]; - }, - sub { - shift->[1]; - }, - sub { - shift->[2]; - }, - sub { - my $cpb = shift; - $cpb->[3] . ', ' . $cpb->[4]; - }, - sub { - shift->[5]; - }, - sub { - my $cardnum = shift->[6]; - 'x'x(length($cardnum)-4). substr($cardnum,(length($cardnum)-4)); - }, - sub { - shift->[7] =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; - my( $mon, $year ) = ( $2, $1 ); - $mon = "0$mon" if length($mon) == 1; - "$mon/$year"; - }, - sub { - shift->[8]; - }, - sub { - shift->[9]; - }, - ], - 'align' => 'lllllllrl', - 'links' => [ ['', sub{'#';}], - ["${p}view/cust_bill.cgi?", sub{shift->[1];},], - ["${p}view/cust_main.cgi?", sub{shift->[2];},], - ["${p}view/cust_main.cgi?", sub{shift->[2];},], + 'fields' => [ 'paybatchnum', + 'invnum', + 'custnum', + sub { $_[0]->cust_main->name_short }, + 'payname', + 'mask_payinfo', + sub { + return('') if $_[0]->payby ne 'CARD'; + $_[0]->get('exp') =~ /^\d\d(\d\d)-(\d\d)/; + sprintf('%02d/%02d',$1,$2); + }, + sub { + sprintf('%.02f', $_[0]->amount) + }, + 'status', + 'error_message', + ], + 'align' => 'rrrlllcrll', + 'links' => [ '', + ["${p}view/cust_bill.cgi?", 'invnum'], + (["${p}view/cust_main.cgi?", 'custnum']) x 2, ], - ) -%> + 'link_onclicks' => [ ('') x 8, + $sub_receipt + ], +&> <%init> my $conf = new FS::Conf; @@ -101,7 +87,7 @@ if ( $cgi->param('payby') ) { } if ( not $cgi->param('dcln') ) { - push @search, "cpb.status IS DISTINCT FROM 'Approved'"; + push @search, "cust_pay_batch.status IS DISTINCT FROM 'Approved'"; } my ($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); @@ -119,18 +105,30 @@ push @search, $curuser->agentnums_sql({ table => 'pay_batch', my $search = ' WHERE ' . join(' AND ', @search); -$count_query = 'SELECT COUNT(*) FROM cust_pay_batch AS cpb ' . +$count_query = 'SELECT COUNT(*) FROM cust_pay_batch ' . 'LEFT JOIN cust_main USING ( custnum ) ' . 'LEFT JOIN pay_batch USING ( batchnum )' . $search; -#grr -$sql_query = "SELECT paybatchnum,invnum,custnum,cpb.last,cpb.first," . - "cpb.payname,cpb.payinfo,cpb.exp,amount,cpb.status " . - "FROM cust_pay_batch AS cpb " . - 'LEFT JOIN cust_main USING ( custnum ) ' . - 'LEFT JOIN pay_batch USING ( batchnum ) ' . - "$search ORDER BY $orderby"; +$sql_query = { + 'table' => 'cust_pay_batch', + 'select' => 'cust_pay_batch.*, cust_main.*, cust_pay.paynum', + 'hashref' => {}, + 'addl_from' => 'LEFT JOIN pay_batch USING ( batchnum ) '. + 'LEFT JOIN cust_main USING ( custnum ) '. + + 'LEFT JOIN cust_pay USING ( batchnum, custnum ) ', + 'extra_sql' => $search, + 'order_by' => "ORDER BY $orderby", +}; + +my $sub_receipt = sub { + my $paynum = shift->paynum or return ''; + include('/elements/popup_link_onclick.html', + 'action' => $p.'view/cust_pay.html?link=popup;paynum='.$paynum, + 'actionlabel' => emt('Payment Receipt'), + ); +}; my $html_init = ''; if ( $pay_batch ) { diff --git a/httemplate/search/cust_pkg.cgi b/httemplate/search/cust_pkg.cgi index 887ec6039..110da91ae 100755 --- a/httemplate/search/cust_pkg.cgi +++ b/httemplate/search/cust_pkg.cgi @@ -9,6 +9,7 @@ emt('Package'), emt('Class'), emt('Status'), + emt('Ordered by'), emt('Setup'), emt('Base Recur'), emt('Freq.'), @@ -34,6 +35,7 @@ sub { $_[0]->pkg; }, 'classname', sub { ucfirst(shift->status); }, + 'otaker', sub { sprintf( $money_char.'%.2f', shift->part_pkg->option('setup_fee'), ); @@ -96,13 +98,14 @@ '', '', '', + '', FS::UI::Web::cust_colors(), '', ], - 'style' => [ '', '', '', '', 'b', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + 'style' => [ '', '', '', '', 'b', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', FS::UI::Web::cust_styles() ], 'size' => [ '', '', '', '', '-1' ], - 'align' => 'rrlccrrlrrrrrrrrrrl'. FS::UI::Web::cust_aligns(). 'r', + 'align' => 'rrlcccrrlrrrrrrrrrrl'. FS::UI::Web::cust_aligns(). 'r', 'links' => [ $link, $link, @@ -119,6 +122,7 @@ '', '', '', + '', '', # link to changed-from package? '', '', diff --git a/httemplate/search/cust_pkg_discount.html b/httemplate/search/cust_pkg_discount.html index d70c3116f..23af1802e 100644 --- a/httemplate/search/cust_pkg_discount.html +++ b/httemplate/search/cust_pkg_discount.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Package discounts', 'name' => 'discounts', 'query' => $query, @@ -50,8 +50,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> die "access denied" @@ -92,8 +92,8 @@ my $count_query = "SELECT COUNT(*), SUM(amount)"; my $join = ' LEFT JOIN discount USING ( discountnum ) LEFT JOIN cust_pkg USING ( pkgnum ) - LEFT JOIN part_pkg USING ( pkgpart ) - LEFT JOIN cust_main USING ( custnum ) '; + LEFT JOIN part_pkg USING ( pkgpart ) '. + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); my $where = ' WHERE '. join(' AND ', @where); diff --git a/httemplate/search/cust_pkg_svc.html b/httemplate/search/cust_pkg_svc.html index 9c5b32fc7..cdc70351a 100644 --- a/httemplate/search/cust_pkg_svc.html +++ b/httemplate/search/cust_pkg_svc.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => $part_svc->svc.' services in package #'.$pkgnum, 'name' => 'services', 'html_form' => $html_form, @@ -30,8 +30,8 @@ ('')x4, ], 'html_foot' => sub { $areboxes ? $html_foot : '' } - ) -%> + +&> <%init> die "access denied" diff --git a/httemplate/search/cust_svc.html b/httemplate/search/cust_svc.html index 2adcbd76f..e2a83b7de 100644 --- a/httemplate/search/cust_svc.html +++ b/httemplate/search/cust_svc.html @@ -62,7 +62,7 @@ if ( length( $cgi->param('search_svc') ) ) { my $addl_from = ' LEFT JOIN part_svc USING ( svcpart ) '. ' LEFT JOIN cust_pkg USING ( pkgnum ) '. - ' LEFT JOIN cust_main USING ( custnum ) '; + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); my @extra_sql = (); diff --git a/httemplate/search/cust_tax_adjustment.html b/httemplate/search/cust_tax_adjustment.html index 925476516..6125a1c04 100644 --- a/httemplate/search/cust_tax_adjustment.html +++ b/httemplate/search/cust_tax_adjustment.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => $title, 'name_singular' => 'tax adjustment', 'query' => $query, @@ -12,9 +12,8 @@ }, ], 'links' => [ '', '', '', $ilink ], - ) -%> - + +&> <%init> die "access denied" diff --git a/httemplate/search/cust_tax_exempt.cgi b/httemplate/search/cust_tax_exempt.cgi index 3704b208a..005d77c33 100644 --- a/httemplate/search/cust_tax_exempt.cgi +++ b/httemplate/search/cust_tax_exempt.cgi @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Legacy tax exemptions', 'name' => 'legacy tax exemptions', 'query' => $query, @@ -46,13 +46,11 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> -my $join_cust = " - LEFT JOIN cust_main USING ( custnum ) -"; +my $join_cust = FS::UI::Web::join_cust_main('cust_tax_exempt'); die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('View customer tax exemptions'); diff --git a/httemplate/search/cust_tax_exempt_pkg.cgi b/httemplate/search/cust_tax_exempt_pkg.cgi index 1b767f846..40b9ed78f 100644 --- a/httemplate/search/cust_tax_exempt_pkg.cgi +++ b/httemplate/search/cust_tax_exempt_pkg.cgi @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Tax exemptions', 'name' => 'tax exemptions', 'query' => $query, @@ -77,14 +77,12 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> +&> <%once> my $join_cust = " - JOIN cust_bill USING ( invnum ) - LEFT JOIN cust_main USING ( custnum ) -"; + JOIN cust_bill USING ( invnum )" . + FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg'); my $join_pkg = " LEFT JOIN cust_pkg USING ( pkgnum ) @@ -93,8 +91,8 @@ my $join_pkg = " my $join = " JOIN cust_bill_pkg USING ( billpkgnum ) - $join_cust $join_pkg + $join_cust "; </%once> diff --git a/httemplate/search/customer_accounting_summary.html b/httemplate/search/customer_accounting_summary.html index 12c896276..b48ff21e3 100644 --- a/httemplate/search/customer_accounting_summary.html +++ b/httemplate/search/customer_accounting_summary.html @@ -142,8 +142,6 @@ $title .= $sel_part_referral->referral.' ' $title .= 'Customer Accounting Summary Report'; -my @cust_classnums = grep /^\d+$/, $cgi->param('cust_classnum'); - my @items = ('netsales', 'cashflow'); my @params = ( [], [] ); my $setuprecur = ''; @@ -173,7 +171,7 @@ foreach (qw(agentnum refnum status)) { } } $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); diff --git a/httemplate/search/elements/checkbox-foot.html b/httemplate/search/elements/checkbox-foot.html new file mode 100644 index 000000000..be1caab91 --- /dev/null +++ b/httemplate/search/elements/checkbox-foot.html @@ -0,0 +1,86 @@ +<%doc> +<& /elements/search.html, + # options... + html_foot => include('elements/checkbox-foot.html', + actions => [ + { label => 'Edit selected packages', + action => 'popup_package_edit()', + }, + { submit => 'Delete selected packages', + confirm => 'Really delete these packages?' + }, + ], + filter => '.name = "pkgpart"', # see below + ), +&> + +This creates a footer for a search page containing a column of checkboxes. +Typically this is used to select several items from the search result and +apply some change to all of them at once. The footer always provides +"select all" and "unselect all" buttons. + +"actions" is an arrayref of action buttons to show. Each element of the +array is a hashref of either: + +- "submit" and, optionally, "confirm". Creates a submit button. The value +of "submit" becomes the "value" property of the button (and thus its label). +If "confirm" is specified, the button will have an onclick handler that +displays the value of "confirm" in a popup message box and asks the user to +confirm the choice. + +- "onclick" and "label". Creates a non-submit button that executes the +Javascript code in "onclick". "label" is used as the text of the button. + +If you want only a single action, you can forget the arrayref-of-hashrefs +business and just put "submit" and "confirm" (or "onclick" and "label") +elements in the argument list. + +"filter" is a javascript expression to limit which checkboxes are included in +the "select/unselect all" actions. By default, any input with type="checkbox" +will be included. If this option is given, it will be evaluated with the +HTML node in a variable named "obj". The expression should return true or +false. + +</%doc> +<DIV ID="checkbox_footer" STYLE="display:block"> +<INPUT TYPE="button" VALUE="<% emt('select all') %>" onclick="setAll(true)"> +<INPUT TYPE="button" VALUE="<% emt('unselect all') %>" onclick="setAll(false)"> +<BR> +% foreach my $action (@$actions) { +% if ( $action->{onclick} ) { +<INPUT TYPE="button" <% $action->{name} %> onclick="<% $opt{onclick} %>"\ + VALUE="<% $action->{label} |h%>"> +% } elsif ( $action->{submit} ) { +<INPUT TYPE="submit" <% $action->{name} %> <% $action->{confirm} %>\ + VALUE="<% $action->{submit} |h%>"> +% } # else do nothing +% } #foreach +</DIV> +<SCRIPT> +var checkboxes = []; +var inputs = document.getElementsByTagName('input'); +for (var i = 0; i < inputs.length; i++) { + var obj = inputs[i]; + if ( obj.type == "checkbox" && <% $filter %> ) { + checkboxes.push(obj); + } +} +%# avoid the need for "$areboxes" late-evaluation hackery +if ( checkboxes.length == 0 ) { + document.getElementById('checkbox_footer').style.display = 'none'; +} +function setAll(setTo) { + for (var i = 0; i < checkboxes.length; i++) { + checkboxes[i].checked = setTo; + } +} +</SCRIPT> +<%init> +my %opt = @_; +my $actions = $opt{'actions'} || [ \%opt ]; +foreach (@$actions) { + $_->{confirm} &&= qq!onclick="return confirm('! . $_->{confirm} . qq!')"!; + $_->{name} &&= qq!NAME="! . $_->{name} . qq!"!; +} +my $filter = $opt{filter} || 'true'; +</%init> diff --git a/httemplate/search/elements/cust_main_dayranges.html b/httemplate/search/elements/cust_main_dayranges.html index eb7566494..cf2d495b1 100644 --- a/httemplate/search/elements/cust_main_dayranges.html +++ b/httemplate/search/elements/cust_main_dayranges.html @@ -162,6 +162,15 @@ if ( grep { $cgi->param('status') eq $_ } FS::cust_main->statuses() ) { 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; @@ -172,10 +181,11 @@ my $count_sql = "select count(*) from cust_main $where"; my $sql_query = { 'table' => 'cust_main', + 'addl_from' => FS::UI::Web::join_cust_main('cust_main'), 'hashref' => {}, 'select' => join(',', #'cust_main.*', - 'custnum', + 'cust_main.custnum', $range_cols, $packages_cols, FS::UI::Web::cust_sql_fields(), diff --git a/httemplate/search/elements/cust_pay_batch_top.html b/httemplate/search/elements/cust_pay_batch_top.html index 1dcc37ac1..bf3047769 100644 --- a/httemplate/search/elements/cust_pay_batch_top.html +++ b/httemplate/search/elements/cust_pay_batch_top.html @@ -120,6 +120,7 @@ my $fixed = $conf->config("batch-fixed_format-$payby"); tie my %download_formats, 'Tie::IxHash', ( '' => 'Default batch mode', +'NACHA' => '94 byte NACHA', 'csv-td_canada_trust-merchant_pc_batch' => 'CSV file for TD Canada Trust Merchant PC Batch', 'csv-chase_canada-E-xactBatch' => diff --git a/httemplate/search/elements/cust_pay_or_refund.html b/httemplate/search/elements/cust_pay_or_refund.html index eeef0c0e1..7b2a17058 100755 --- a/httemplate/search/elements/cust_pay_or_refund.html +++ b/httemplate/search/elements/cust_pay_or_refund.html @@ -51,6 +51,7 @@ Examples: 'sort_fields' => \@sort_fields, 'align' => $align, 'links' => \@links, + 'link_onclicks' => \@link_onclicks, 'color' => \@color, 'style' => \@style, &> @@ -134,11 +135,12 @@ if ( $cgi->param('tax_names') ) { } } -my @header = (); -my @fields = (); -my @sort_fields = (); +my @header; +my @fields; +my @sort_fields; my $align = ''; -my @links = (); +my @links; +my @link_onclicks; if ( $opt{'pre_header'} ) { push @header, @{ $opt{'pre_header'} }; $align .= 'c' x scalar(@{ $opt{'pre_header'} }); @@ -147,6 +149,16 @@ if ( $opt{'pre_header'} ) { push @sort_fields, @{ $opt{'pre_fields'} }; } +my $sub_receipt = sub { + my $obj = shift; + my $objnum = $obj->primary_key . '=' . $obj->get($obj->primary_key); + + include('/elements/popup_link_onclick.html', + 'action' => $p.'view/cust_pay.html?link=popup;'.$objnum, + 'actionlabel' => emt('Payment Receipt'), + ); +}; + push @header, "\u$name_singular", 'Amount', ; @@ -155,6 +167,7 @@ push @links, '', ''; push @fields, 'payby_payinfo_pretty', sub { sprintf('$%.2f', shift->$amount_field() ) }, ; +push @link_onclicks, $sub_receipt, '', push @sort_fields, '', $amount_field; if ( $unapplied ) { @@ -239,9 +252,12 @@ if ( $cgi->param('magic') ) { $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; } @@ -250,78 +266,121 @@ if ( $cgi->param('magic') ) { } if ( $cgi->param('payby') ) { - $cgi->param('payby') =~ - /^(CARD|CHEK|BILL|PREP|CASH|WEST|MCRD)(-(VisaMC|Amex|Discover|Maestro))?$/ - or die "illegal payby ". $cgi->param('payby'); - push @search, "$table.payby = '$1'"; - if ( $3 ) { - - my $cardtype = $3; - - my $search; - if ( $cardtype eq 'VisaMC' ) { - #avoid posix regexes for portability - $search = - " ( ( substring($table.payinfo from 1 for 1) = '4' ". - " AND substring($table.payinfo from 1 for 4) != '4936' ". - " AND substring($table.payinfo from 1 for 6) ". - " NOT SIMILAR TO '49030[2-9]' ". - " AND substring($table.payinfo from 1 for 6) ". - " NOT SIMILAR TO '49033[5-9]' ". - " AND substring($table.payinfo from 1 for 6) ". - " NOT SIMILAR TO '49110[1-2]' ". - " AND substring($table.payinfo from 1 for 6) ". - " NOT SIMILAR TO '49117[4-9]' ". - " AND substring($table.payinfo from 1 for 6) ". - " NOT SIMILAR TO '49118[1-2]' ". - " )". - " OR substring($table.payinfo from 1 for 2) = '51' ". - " OR substring($table.payinfo from 1 for 2) = '52' ". - " OR substring($table.payinfo from 1 for 2) = '53' ". - " OR substring($table.payinfo from 1 for 2) = '54' ". - " OR substring($table.payinfo from 1 for 2) = '54' ". - " OR substring($table.payinfo from 1 for 2) = '55' ". - " OR substring($table.payinfo from 1 for 2) = '36' ". #Diner's int'l processed as Visa/MC inside US - " ) "; - } elsif ( $cardtype eq 'Amex' ) { - $search = - " ( substring($table.payinfo from 1 for 2 ) = '34' ". - " OR substring($table.payinfo from 1 for 2 ) = '37' ". - " ) "; - } elsif ( $cardtype eq 'Discover' ) { - $search = - " ( substring($table.payinfo from 1 for 4 ) = '6011' ". - " OR substring($table.payinfo from 1 for 2 ) = '65' ". - " OR substring($table.payinfo from 1 for 3 ) = '622' ". #China Union Pay processed as Discover outside CN - " ) "; - } elsif ( $cardtype eq 'Maestro' ) { - $search = - " ( substring($table.payinfo from 1 for 2 ) = '63' ". - " OR substring($table.payinfo from 1 for 2 ) = '67' ". - " OR substring($table.payinfo from 1 for 6 ) = '564182' ". - " OR substring($table.payinfo from 1 for 4 ) = '4936' ". - " OR substring($table.payinfo from 1 for 6 ) ". - " SIMILAR TO '49030[2-9]' ". - " OR substring($table.payinfo from 1 for 6 ) ". - " SIMILAR TO '49033[5-9]' ". - " OR substring($table.payinfo from 1 for 6 ) ". - " SIMILAR TO '49110[1-2]' ". - " OR substring($table.payinfo from 1 for 6 ) ". - " SIMILAR TO '49117[4-9]' ". - " OR substring($table.payinfo from 1 for 6 ) ". - " SIMILAR TO '49118[1-2]' ". - " ) "; - } else { - die "unknown card type $cardtype"; - } - my $masksearch = $search; - $masksearch =~ s/$table\.payinfo/$table.paymask/gi; + my @all_payby_search = (); + foreach my $payby ( $cgi->param('payby') ) { + + $payby =~ + /^(CARD|CHEK|BILL|PREP|CASH|WEST|MCRD)(-(VisaMC|Amex|Discover|Maestro))?$/ + or die "illegal payby $payby"; + + my $payby_search = "$table.payby = '$1'"; + + if ( $3 ) { + + my $cardtype = $3; + + my $search; + if ( $cardtype eq 'VisaMC' ) { + #avoid posix regexes for portability + $search = + " ( ( substring($table.payinfo from 1 for 1) = '4' ". + " AND substring($table.payinfo from 1 for 4) != '4936' ". + " AND substring($table.payinfo from 1 for 6) ". + " NOT SIMILAR TO '49030[2-9]' ". + " AND substring($table.payinfo from 1 for 6) ". + " NOT SIMILAR TO '49033[5-9]' ". + " AND substring($table.payinfo from 1 for 6) ". + " NOT SIMILAR TO '49110[1-2]' ". + " AND substring($table.payinfo from 1 for 6) ". + " NOT SIMILAR TO '49117[4-9]' ". + " AND substring($table.payinfo from 1 for 6) ". + " NOT SIMILAR TO '49118[1-2]' ". + " )". + " OR substring($table.payinfo from 1 for 2) = '51' ". + " OR substring($table.payinfo from 1 for 2) = '52' ". + " OR substring($table.payinfo from 1 for 2) = '53' ". + " OR substring($table.payinfo from 1 for 2) = '54' ". + " OR substring($table.payinfo from 1 for 2) = '54' ". + " OR substring($table.payinfo from 1 for 2) = '55' ". +# " OR substring($table.payinfo from 1 for 2) = '36' ". #Diner's int'l was processed as Visa/MC inside US, now Discover + " ) "; + } elsif ( $cardtype eq 'Amex' ) { + $search = + " ( substring($table.payinfo from 1 for 2 ) = '34' ". + " OR substring($table.payinfo from 1 for 2 ) = '37' ". + " ) "; + } elsif ( $cardtype eq 'Discover' ) { + + my $conf = new FS::Conf; + my $country = $conf->config('countrydefault') || 'US'; + + $search = + " ( substring($table.payinfo from 1 for 4 ) = '6011' ". + " OR substring($table.payinfo from 1 for 2 ) = '65' ". + " OR substring($table.payinfo from 1 for 3 ) = '300' ". + " OR substring($table.payinfo from 1 for 3 ) = '301' ". + " OR substring($table.payinfo from 1 for 3 ) = '302' ". + " OR substring($table.payinfo from 1 for 3 ) = '303' ". + " OR substring($table.payinfo from 1 for 3 ) = '304' ". + " OR substring($table.payinfo from 1 for 3 ) = '305' ". + " OR substring($table.payinfo from 1 for 4 ) = '3095' ". + " OR substring($table.payinfo from 1 for 2 ) = '36' ". + " OR substring($table.payinfo from 1 for 2 ) = '38' ". + " OR substring($table.payinfo from 1 for 2 ) = '39' ". + " OR substring($table.payinfo from 1 for 3 ) = '644' ". + " OR substring($table.payinfo from 1 for 3 ) = '645' ". + " OR substring($table.payinfo from 1 for 3 ) = '646' ". + " OR substring($table.payinfo from 1 for 3 ) = '647' ". + " OR substring($table.payinfo from 1 for 3 ) = '648' ". + " OR substring($table.payinfo from 1 for 3 ) = '649' ". + ( $country =~ /^(US|CA)$/ + ?" OR substring($table.payinfo from 1 for 4 ) = '3528' ". # JCB cards in the 3528-3589 range identified as Discover inside US/CA + " OR substring($table.payinfo from 1 for 4 ) = '3529' ". + " OR substring($table.payinfo from 1 for 3 ) = '353' ". + " OR substring($table.payinfo from 1 for 3 ) = '354' ". + " OR substring($table.payinfo from 1 for 3 ) = '355' ". + " OR substring($table.payinfo from 1 for 3 ) = '356' ". + " OR substring($table.payinfo from 1 for 3 ) = '357' ". + " OR substring($table.payinfo from 1 for 3 ) = '358' " + :"" + ). + " OR substring($table.payinfo from 1 for 3 ) = '622' ". #China Union Pay processed as Discover outside CN + " ) "; + } elsif ( $cardtype eq 'Maestro' ) { + $search = + " ( substring($table.payinfo from 1 for 2 ) = '63' ". + " OR substring($table.payinfo from 1 for 2 ) = '67' ". + " OR substring($table.payinfo from 1 for 6 ) = '564182' ". + " OR substring($table.payinfo from 1 for 4 ) = '4936' ". + " OR substring($table.payinfo from 1 for 6 ) ". + " SIMILAR TO '49030[2-9]' ". + " OR substring($table.payinfo from 1 for 6 ) ". + " SIMILAR TO '49033[5-9]' ". + " OR substring($table.payinfo from 1 for 6 ) ". + " SIMILAR TO '49110[1-2]' ". + " OR substring($table.payinfo from 1 for 6 ) ". + " SIMILAR TO '49117[4-9]' ". + " OR substring($table.payinfo from 1 for 6 ) ". + " SIMILAR TO '49118[1-2]' ". + " ) "; + } else { + die "unknown card type $cardtype"; + } + + my $masksearch = $search; + $masksearch =~ s/$table\.payinfo/$table.paymask/gi; + + $payby_search = "( $payby_search AND ( $search OR ( $table.paymask IS NOT NULL AND $masksearch ) ) )"; - push @search, - "( $search OR ( $table.paymask IS NOT NULL AND $masksearch ) )"; + } + + push @all_payby_search, $payby_search; } + + push @search, ' ( '. join(' OR ', @all_payby_search). ' ) ' if @all_payby_search; + } if ( $cgi->param('payinfo') ) { @@ -350,6 +409,7 @@ if ( $cgi->param('magic') ) { } my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); + push @search, "_date >= $beginning ", "_date <= $ending"; @@ -411,7 +471,7 @@ if ( $cgi->param('magic') ) { #here is the agent virtualization push @search, $curuser->agentnums_sql; - my $addl_from = ' LEFT JOIN cust_main USING ( custnum ) '; + my $addl_from = FS::UI::Web::join_cust_main($table); my $group_by = ''; if ( $cgi->param('tax_names') ) { diff --git a/httemplate/search/elements/report_cust_pay_or_refund.html b/httemplate/search/elements/report_cust_pay_or_refund.html index 0e04ab0dd..0462f1cd9 100644 --- a/httemplate/search/elements/report_cust_pay_or_refund.html +++ b/httemplate/search/elements/report_cust_pay_or_refund.html @@ -30,68 +30,33 @@ Examples: <TR> <TD ALIGN="right"><% ucfirst(PL($name_singular)) %> of type: </TD> <TD> - <SELECT NAME="payby" onChange="payby_changed(this)"> - <OPTION VALUE=""><% mt('all') |h %></OPTION> - <OPTION VALUE="CARD"><% mt('credit card (all)') |h %></OPTION> - <OPTION VALUE="CARD-VisaMC"><% mt('credit card (Visa/MasterCard)') |h %></OPTION> - <OPTION VALUE="CARD-Amex"><% mt('credit card (American Express)') |h %></OPTION> - <OPTION VALUE="CARD-Discover"><% mt('credit card (Discover)') |h %></OPTION> - <OPTION VALUE="CARD-Maestro"><% mt('credit card (Maestro/Switch/Solo)') |h %></OPTION> - <OPTION VALUE="CHEK"><% mt('electronic check / ACH') |h %></OPTION> - <OPTION VALUE="BILL"><% mt('check') |h %></OPTION> - <OPTION VALUE="PREP"><% mt('prepaid card') |h %></OPTION> - <OPTION VALUE="CASH"><% mt('cash') |h %></OPTION> - <OPTION VALUE="WEST"><% mt('Western Union') |h %></OPTION> - <OPTION VALUE="MCRD"><% mt('manual credit card') |h %></OPTION> + <SELECT NAME="payby" SIZE=10 MULTIPLE> +%# <OPTION VALUE=""><% mt('all') |h %></OPTION> +%# <OPTION VALUE="CARD"><% mt('credit card (all)') |h %></OPTION> + <OPTION VALUE="CARD-VisaMC" SELECTED><% mt('credit card (Visa/MasterCard)') |h %></OPTION> + <OPTION VALUE="CARD-Amex" SELECTED><% mt('credit card (American Express)') |h %></OPTION> + <OPTION VALUE="CARD-Discover" SELECTED><% mt('credit card (Discover)') |h %></OPTION> + <OPTION VALUE="CARD-Maestro" SELECTED><% mt('credit card (Maestro/Switch/Solo)') |h %></OPTION> + <OPTION VALUE="CHEK" SELECTED><% mt('electronic check / ACH') |h %></OPTION> + <OPTION VALUE="BILL" SELECTED><% mt('check') |h %></OPTION> + <OPTION VALUE="PREP" SELECTED><% mt('prepaid card') |h %></OPTION> + <OPTION VALUE="CASH" SELECTED><% mt('cash') |h %></OPTION> + <OPTION VALUE="WEST" SELECTED><% mt('Western Union') |h %></OPTION> + <OPTION VALUE="MCRD" SELECTED><% mt('manual credit card') |h %></OPTION> </SELECT> </TD> </TR> - <SCRIPT TYPE="text/javascript"> - - function payby_changed(what) { - if ( what.value == 'BILL' ) { - show('payinfo'); - hide('ccpay'); - } else if ( what.value.match(/^CARD|CHEK/) ) { - hide('payinfo'); - show('ccpay'); - } else { - hide('payinfo'); - hide('ccpay'); - } - } - - function show(what) { - document.getElementById(what+'_caption').style.color = '#000000'; - document.getElementById(what).disabled = false; - document.getElementById(what).style.backgroundColor = '#ffffff'; - } - - function hide(what) { - document.getElementById(what+'_caption').style.color = '#bbbbbb'; - document.getElementById(what).disabled = true; - document.getElementById(what).style.backgroundColor = '#dddddd'; - } - - - - </SCRIPT> - <TR> - <TD ALIGN="right"><FONT ID="payinfo_caption" COLOR="#bbbbbb"><% mt('Check #:') |h %> </FONT></TD> + <TD ALIGN="right"><% mt('Check #:') |h %> </TD> <TD> - <INPUT TYPE="text" ID="payinfo" NAME="payinfo" DISABLED STYLE="background-color: #dddddd"> + <INPUT TYPE="text" ID="payinfo" NAME="payinfo"> </TD> </TR> <TR> - <TD ALIGN="right"> - <FONT ID="ccpay_caption" COLOR="#bbbbbb"> - <% mt('Transaction #') |h %> - </FONT> - </TD> + <TD ALIGN="right"><% mt('Transaction #:') |h %> </TD> <TD> - <INPUT TYPE="text" ID="ccpay" NAME="ccpay" DISABLED STYLE="background-color: #dddddd"> + <INPUT TYPE="text" ID="ccpay" NAME="ccpay"> </TD> </TR> @@ -108,7 +73,8 @@ Examples: <TD> <TABLE> <& /elements/tr-input-beginning_ending.html, - layout => 'horiz', + layout => 'horiz', + input_time => $conf->exists('report-cust_pay-select_time'), &> </TABLE> </TD> @@ -158,6 +124,8 @@ my $name_singular = $opt{'name_singular'}; die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); +my $conf = new FS::Conf; + my $void = $cgi->param('void') ? 1 : 0; my $unapplied = $cgi->param('unapplied') ? 1 : 0; diff --git a/httemplate/search/elements/report_svc_Common.html b/httemplate/search/elements/report_svc_Common.html new file mode 100644 index 000000000..434197078 --- /dev/null +++ b/httemplate/search/elements/report_svc_Common.html @@ -0,0 +1,122 @@ +<%doc> + +Example: + + <& elements/report_svc_Common.html, + + #required + 'table' => 'svc_something', + 'title' => 'Page title', + + #optional + 'action' => 'svc_tablename.html', #defaults to svc_tablename.html + + &> + +</%doc> +<& /elements/header.html, $title &> + +<FORM ACTION="<% $opt{'action'} || $opt{'table'}. '.html' %>" METHOD="GET"> +<INPUT TYPE="hidden" NAME="magic" VALUE="advanced"> +<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>"> + + <TABLE BGCOLOR="#cccccc" CELLSPACING=0> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1"><% mt('Search options') |h %></FONT></TH> + </TR> + +% unless ( $custnum ) { + + <& /elements/tr-select-agent.html, + curr_value => scalar( $cgi->param('agentnum') ), + disable_empty => 0, + &> + + <& /elements/tr-select-cust_main-status.html, + label => 'Customer Status', + field => 'cust_status', + &> + + <& /elements/tr-select-payby.html, + label => emt('Payment method:'), + payby_type => 'cust', + multiple => 1, + all_selected => 1, + &> + + <& /elements/tr-input-money.html, + label => 'Balance over', + field => 'balance', + &> + + <& /elements/tr-input-text.html, + label => 'Balance age (days)', + field => 'balance_days', + size => 4, + &> + +% } + +% # just this customer's domains? +%# <& /elements/tr-select-domain.html, +%# 'element_name' => 'domsvc', +%# 'curr_value' => scalar( $cgi->param('domsvc') ), +%# 'disable_empty' => 0, +%# &> + + <& /elements/tr-selectmultiple-part_pkg.html &> + + <& /elements/tr-select-part_svc.html, + 'svcdb' => $svcdb, + 'label' => 'Services', + &> + + <TR> + <TH CLASS="background" COLSPAN=2> </TH> + </TR> + + <TR> + <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1"><% mt('Display options') |h %></FONT></TH> + </TR> + +% #"package fields" ala advanced svc_acct search? +% #move to /elements/tr-select-cust_pkg-fields and use it from there if so... + + <& /elements/tr-select-cust-fields.html &> + + </TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="<% mt('Get Report') |h %>"> + +</FORM> + +<& /elements/footer.html &> +<%init> + +my(%opt) = @_; + +my $svcdb = $opt{'table'}; + +my $name = "FS::$svcdb"->table_info->{'name_plural'} + || PL( "FS::$svcdb"->table_info->{'name'} ); + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right("Services: $name: Advanced search"); + +my $title = $opt{'title'}; + +#false laziness w/report_cust_pkg.html +my( $custnum, $cust_main) = ('', ''); +if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { + $custnum = $1; + my $cust_main = qsearchs({ + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $custnum }, + 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, + }) or die "unknown custnum $custnum"; + $title = mt("$title: [_1]", $cust_main->name); +} + +</%init> diff --git a/httemplate/search/elements/search-html.html b/httemplate/search/elements/search-html.html index 7ccf356ea..e760bc546 100644 --- a/httemplate/search/elements/search-html.html +++ b/httemplate/search/elements/search-html.html @@ -253,7 +253,17 @@ % $bgcolor = $bgcolor1; % } - <TR> +% my $trid = ''; +% if ( $opt{'link_field' } ) { +% my $link_field = $opt{'link_field'}; +% if ( ref($link_field) eq 'CODE' ) { +% $trid = &{$link_field}($row); +% } else { +% $trid = $row->$link_field(); +% } +% } + <TR ID="<%$trid |h%>"> + % if ( $opt{'fields'} ) { % diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html index 5a16a22fe..d44b45465 100644 --- a/httemplate/search/elements/search.html +++ b/httemplate/search/elements/search.html @@ -167,6 +167,11 @@ Example: # miscellany 'download_label' => 'Download this report', # defaults to 'Download full results' + 'link_field' => 'pkgpart' + # will create internal links for each row, + # with the value of this field as the NAME attribute + # If this is a coderef, will evaluate it, passing the + # row as an argument, and use the result as the NAME. &> </%doc> @@ -348,7 +353,7 @@ if ( $opt{'disableable'} ) { 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} ) =~ diff --git a/httemplate/search/elements/svc_Common.html b/httemplate/search/elements/svc_Common.html new file mode 100644 index 000000000..56c75bba3 --- /dev/null +++ b/httemplate/search/elements/svc_Common.html @@ -0,0 +1,48 @@ +<& search.html, %opt &> +<%doc> +Currently does nothing but insert the classnames for fields chosen from an +inventory class. +</%doc> +<%init> +my %opt = @_; +my $query = $opt{query}; +my $svcdb = $query->{'table'}; + +# to avoid looking up the inventory class of every service in the database, +# keep as much of the base query as possible. +my $item_query = { %$query }; +$item_query->{'table'} = 'inventory_item'; +$item_query->{'addl_from'} = + " JOIN ( $svcdb ". $query->{'addl_from'} . + ") ON inventory_item.svcnum = $svcdb.svcnum ". + " JOIN inventory_class ON (inventory_item.classnum = inventory_class.classnum)"; +# avoid conflict with inventory_item.agentnum +$item_query->{'extra_sql'} =~ s/ agentnum/ cust_main.agentnum/g; +$item_query->{'select'} = 'inventory_item.svcnum, '. + 'inventory_item.svc_field, '. + 'inventory_class.classname'; +my @items = qsearch($item_query); +my %item_fields; +foreach my $i (@items) { + $item_fields{ $i->svc_field } ||= {}; + $item_fields{ $i->svc_field }{ $i->svcnum } = $i->classname; +} + +$opt{'sort_fields'} ||= []; +for ( my $i = 0; $i < @{ $opt{'fields'} }; $i++ ) { + my $f = $opt{'fields'}[$i]; + next if ref($f); # it's not a plain table column + $opt{'sort_fields'}[$i] ||= $f; + my $classnames = $item_fields{$f}; # hashref of svcnum -> classname + next if !$classnames; # there are no inventory items in this column + $opt{'fields'}[$i] = sub { + my $svc = $_[0]; + if ( exists($classnames->{$svc->svcnum}) ) { + return $svc->$f . '<BR><I>('. $classnames->{$svc->svcnum} . ')</I>'; + } else { + return $svc->$f; + } + }; #sub +} + +</%init> diff --git a/httemplate/search/employee_audit.html b/httemplate/search/employee_audit.html index 753c7bff3..2bc6ff46e 100644 --- a/httemplate/search/employee_audit.html +++ b/httemplate/search/employee_audit.html @@ -7,7 +7,7 @@ <%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', diff --git a/httemplate/search/inventory_item.html b/httemplate/search/inventory_item.html index 086c8e92d..0e4251f74 100644 --- a/httemplate/search/inventory_item.html +++ b/httemplate/search/inventory_item.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => $title, 'menubar' => [ 'View inventory classes' => @@ -87,8 +87,8 @@ <INPUT TYPE="hidden" NAME="classnum" VALUE="$classnum"> <INPUT TYPE="hidden" NAME="avail" VALUE="! .$cgi->param('avail') . '">', #' 'html_foot' => $sub_foot, - ) -%> + +&> <%init> my $curuser = $FS::CurrentUser::CurrentUser; @@ -157,7 +157,7 @@ my $link_cust = sub { my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. ' LEFT JOIN part_svc USING ( svcpart ) '. ' LEFT JOIN cust_pkg USING ( pkgnum ) '. - ' LEFT JOIN cust_main USING ( custnum ) '; + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); my $areboxes = 0; my $sub_checkbox = sub { diff --git a/httemplate/search/mailinglistmember.html b/httemplate/search/mailinglistmember.html index ee395f416..a678d45ed 100644 --- a/httemplate/search/mailinglistmember.html +++ b/httemplate/search/mailinglistmember.html @@ -1,4 +1,4 @@ -<% include('elements/search.html', +<& elements/search.html, 'title' => $title, 'name_singular' => 'member', 'query' => $query, @@ -6,8 +6,7 @@ 'header' => [ 'Email address' ], 'fields' => [ $email_sub, ], #just this one for now 'html_init' => $html_init, - ) -%> +&> <%init> #XXX ACL: diff --git a/httemplate/search/part_pkg.html b/httemplate/search/part_pkg.html index 57da9d459..a90f13c95 100644 --- a/httemplate/search/part_pkg.html +++ b/httemplate/search/part_pkg.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => $title, 'name_singular' => $name, 'header' => \@header, @@ -14,8 +14,8 @@ 'links' => \@links, 'align' => $align, 'sort_fields' => [], - ) -%> + +&> <%init> #this is about reports about packages definitions (starting w/commission ones) @@ -23,7 +23,7 @@ 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') || '$'; diff --git a/httemplate/search/pay_batch.cgi b/httemplate/search/pay_batch.cgi index aeaa012f4..620996abd 100755 --- a/httemplate/search/pay_batch.cgi +++ b/httemplate/search/pay_batch.cgi @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Payment Batches', 'name_singular' => 'batch', 'query' => { 'table' => 'pay_batch', @@ -101,8 +101,7 @@ ], 'html_init' => $html_init, 'html_foot' => include('.upload_incoming'), - ) -%> +&> <%def .upload_incoming> % if ( FS::payment_gateway->count("gateway_namespace = 'Business::BatchPayment' AND disabled IS NULL") > 0 ) { <& /elements/form-file_upload.html, @@ -149,16 +148,10 @@ my $count_query = 'SELECT COUNT(*) FROM pay_batch'; my($begin, $end) = ( '', '' ); my @where; -if ( $cgi->param('beginning') - && $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/ ) { - $begin = parse_datetime($1); - push @where, "download >= $begin"; -} -if ( $cgi->param('ending') - && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) { - $end = parse_datetime($1) + 86399; - push @where, "download < $end"; -} + +my($beginning,$ending) = FS::UI::Web::parse_beginning_ending($cgi); +push @where, "( (download >= $beginning AND download <= $ending) ". + ' OR download IS NULL )'; my @status; if ( $cgi->param('open') ) { diff --git a/httemplate/search/phone_avail.html b/httemplate/search/phone_avail.html index 1335379ae..faf354420 100644 --- a/httemplate/search/phone_avail.html +++ b/httemplate/search/phone_avail.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Phone Number (DID) Search Results', 'name_singular' => 'phone number', 'query' => { @@ -81,8 +81,8 @@ FS::UI::Web::cust_styles(), '', ], - ) -%> + +&> <%init> die "access denied" @@ -125,9 +125,11 @@ my $search = scalar(@search) my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. #' LEFT JOIN part_svc USING ( svcpart ) '. ' LEFT JOIN cust_pkg USING ( pkgnum ) '. - ' LEFT JOIN cust_main USING ( custnum ) '; + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); my $count_query = "SELECT COUNT(*) FROM phone_avail $search"; #$addl_from? +# All of these relationships are left joined in the many-to-one direction, +# so including $addl_from won't affect the count. Logic! my $hashref = {}; $hashref->{'ordernum'} = $1 if $cgi->param('ordernum') =~ /^(\d+)$/; diff --git a/httemplate/search/phone_inventory_provisioned.html b/httemplate/search/phone_inventory_provisioned.html index 03d21547d..b3efdbd77 100644 --- a/httemplate/search/phone_inventory_provisioned.html +++ b/httemplate/search/phone_inventory_provisioned.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'LATA Search Results', 'name_singular' => 'LATA', 'query' => { @@ -72,8 +72,8 @@ '', '', ], - ) -%> + +&> <%init> die "access denied" diff --git a/httemplate/search/prepaid_income.html b/httemplate/search/prepaid_income.html index 03d121d70..cb58a666d 100644 --- a/httemplate/search/prepaid_income.html +++ b/httemplate/search/prepaid_income.html @@ -129,10 +129,13 @@ if ( $cgi->param('status') =~ /^([a-z]+)$/ ) { 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; } diff --git a/httemplate/search/prepay_credit.html b/httemplate/search/prepay_credit.html index 36403511b..7566e657e 100644 --- a/httemplate/search/prepay_credit.html +++ b/httemplate/search/prepay_credit.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Unused Prepaid Cards'. ($agent ? ' for '. $agent->agent : ''), 'menubar' => [ @@ -47,8 +47,8 @@ $agent ? [ "${p}edit/agent.cgi?", 'agentnum' ] : ''; }, ], - ) -%> + +&> <%init> die "access denied" diff --git a/httemplate/search/prospect_main.html b/httemplate/search/prospect_main.html index 328d1202f..ab37b9089 100644 --- a/httemplate/search/prospect_main.html +++ b/httemplate/search/prospect_main.html @@ -1,4 +1,4 @@ -<% include('elements/search.html', +<& elements/search.html, 'title' => 'Prospect Search Results', 'name_singular' => 'prospect', 'query' => $query, @@ -23,8 +23,7 @@ '', #link to contact edit??? ], 'agent_virt' => 1, - ) -%> +&> <%init> die "access denied" diff --git a/httemplate/search/qual.cgi b/httemplate/search/qual.cgi index 7133ef056..7b718e498 100755 --- a/httemplate/search/qual.cgi +++ b/httemplate/search/qual.cgi @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Qualifications', 'name_singular' => 'qualification', 'query' => { 'table' => 'qual', @@ -51,8 +51,8 @@ '', '', ], - ) -%> + +&> <%init> die "access denied" diff --git a/httemplate/search/queue.html b/httemplate/search/queue.html index 1c124706c..141c535da 100644 --- a/httemplate/search/queue.html +++ b/httemplate/search/queue.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Job Queue', 'name' => 'jobs', 'html_form' => qq!<FORM NAME="jobForm" ACTION="$p/misc/queue.cgi" METHOD="POST">!, @@ -120,9 +120,8 @@ ''; } }, - ) - -%> + +&> <%init> die "access denied" diff --git a/httemplate/search/quotation.html b/httemplate/search/quotation.html index 259c85c22..fbc35bea1 100755 --- a/httemplate/search/quotation.html +++ b/httemplate/search/quotation.html @@ -72,7 +72,7 @@ die "access denied" unless $curuser->access_right('List quotations'); my $join_prospect_main = 'LEFT JOIN prospect_main USING ( prospectnum )'; -my $join_cust_main = 'LEFT JOIN cust_main ON ( quotation.custnum = cust_main.custnum )'; +my $join_cust_main = FS::UI::Web::join_cust_main('quotation'); #here is the agent virtualization my $agentnums_sql = ' ( '. $curuser->agentnums_sql( table=>'prospect_main' ). diff --git a/httemplate/search/reg_code.html b/httemplate/search/reg_code.html index f7d6d2061..42211e571 100644 --- a/httemplate/search/reg_code.html +++ b/httemplate/search/reg_code.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Unused Registration Codes for '. $agent->agent, 'name' => 'registration codes', @@ -23,8 +23,8 @@ #$plink, '', ], - ) -%> + +&> <%init> die "access denied" diff --git a/httemplate/search/report_477.html b/httemplate/search/report_477.html index f593a94d8..b842b1f3f 100755 --- a/httemplate/search/report_477.html +++ b/httemplate/search/report_477.html @@ -231,7 +231,9 @@ 'table' => 'part_pkg_report_option', 'name_col' => 'name', 'hashref' => { 'disabled' => '' }, - 'element_name' => 'partv_report_option', + 'element_name' => 'part5_report_option', + 'curr_value' => + FS::Report::FCC_477::restore_fcc477map("part5_report_option"), ) %> </TD> diff --git a/httemplate/search/report_agent_commission.html b/httemplate/search/report_agent_commission.html new file mode 100644 index 000000000..79f94c52e --- /dev/null +++ b/httemplate/search/report_agent_commission.html @@ -0,0 +1,22 @@ +<% include('/elements/header.html', 'Agent commission report' ) %> + +<FORM ACTION="agent_commission.html"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + +<% include( '/elements/tr-select-agent.html', disable_empty => 1 ) %> + +<% include( '/elements/tr-input-beginning_ending.html', ) %> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Get Report"> + +<% include('/elements/footer.html') %> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +</%init> diff --git a/httemplate/search/report_cust_bill.html b/httemplate/search/report_cust_bill.html index 51618fb24..b339c80e0 100644 --- a/httemplate/search/report_cust_bill.html +++ b/httemplate/search/report_cust_bill.html @@ -4,7 +4,7 @@ <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, diff --git a/httemplate/search/report_cust_main.html b/httemplate/search/report_cust_main.html index acc49aec6..bac4346cf 100755 --- a/httemplate/search/report_cust_main.html +++ b/httemplate/search/report_cust_main.html @@ -96,11 +96,21 @@ </TR> % } - <& /elements/tr-select-cust_tag.html, - 'cgi' => $cgi, - 'is_report' => 1, - 'multiple' => 1, - &> + <TR> + <TD ALIGN="right">Tags</TD> + <TD> + <& /elements/select-cust_tag.html, + 'cgi' => $cgi, + 'is_report' => 1, + 'multiple' => 1, + &> + <DIV STYLE="display:inline-block; vertical-align:baseline"> + <INPUT TYPE="radio" NAME="all_tags" VALUE="0" CHECKED> Any of these + <BR> + <INPUT TYPE="radio" NAME="all_tags" VALUE="1"> All of these + </DIV> + </TD> + </TR> <& /elements/tr-select-payby.html, 'payby_type' => 'cust', @@ -174,10 +184,28 @@ </TR> <TR> + <TD ALIGN="right" VALIGN="center"><% mt('With postal mail invoices') |h %></TD> + <TD><INPUT TYPE="checkbox" NAME="POST" ID="POST" onClick="POST_changed();"></TD> + </TR> + + <TR> <TD ALIGN="right" VALIGN="center"><% mt('Without postal mail invoices') |h %></TD> - <TD><INPUT TYPE="checkbox" NAME="no_POST"></TD> + <TD><INPUT TYPE="checkbox" NAME="no_POST" ID="no_POST" onClick="no_POST_changed();"></TD> </TR> + <SCRIPT TYPE="text/javascript"> + function POST_changed() { + if ( document.getElementById('POST').checked == true ) { + document.getElementById('no_POST').checked = false; + } + } + function no_POST_changed() { + if ( document.getElementById('no_POST').checked == true ) { + document.getElementById('POST').checked = false; + } + } + </SCRIPT> + <TR> <TH CLASS="background" COLSPAN=2> </TH> </TR> diff --git a/httemplate/search/report_employee_audit.html b/httemplate/search/report_employee_audit.html index 757b8232f..461849b76 100644 --- a/httemplate/search/report_employee_audit.html +++ b/httemplate/search/report_employee_audit.html @@ -23,7 +23,7 @@ <%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', diff --git a/httemplate/search/report_employee_commission.html b/httemplate/search/report_employee_commission.html index 51afad3b5..ebfcae82d 100644 --- a/httemplate/search/report_employee_commission.html +++ b/httemplate/search/report_employee_commission.html @@ -25,6 +25,6 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + unless $FS::CurrentUser::CurrentUser->access_right('Employees: Commission Report'); </%init> diff --git a/httemplate/search/report_receivables.html b/httemplate/search/report_receivables.html index 5cff0f4fc..854b24a00 100755 --- a/httemplate/search/report_receivables.html +++ b/httemplate/search/report_receivables.html @@ -15,7 +15,15 @@ <& /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> diff --git a/httemplate/search/report_sqlradius_usage.html b/httemplate/search/report_sqlradius_usage.html index 01215e834..7e54465d3 100644 --- a/httemplate/search/report_sqlradius_usage.html +++ b/httemplate/search/report_sqlradius_usage.html @@ -8,13 +8,18 @@ 'empty_label' => 'all', &> -% my @exporttypes = map { "'$_'" } qw(sqlradius broadband_sqlradius); +%#more future-proof to actually ask all exports if they ->can('usage_sessions') +% my @exporttypes = qw( sqlradius sqlradius_withdomain broadband_sqlradius +% phone_sqlradius radiator +% ); <& /elements/tr-select-table.html, 'label' => 'Export', 'table' => 'part_export', 'name_col' => 'label', 'hashref' => {}, - 'extra_sql' => ' WHERE exporttype IN('.join(',', @exporttypes).')', + 'extra_sql' => ' WHERE exporttype IN ( '. + join(',', map "'$_'", @exporttypes). + ')', 'disable_empty' => 1, 'order_by' => 'ORDER BY exportnum', &> diff --git a/httemplate/search/report_svc_acct.html b/httemplate/search/report_svc_acct.html index 74bf5538e..e47f72726 100755 --- a/httemplate/search/report_svc_acct.html +++ b/httemplate/search/report_svc_acct.html @@ -116,7 +116,7 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Services: Accounts: Advanced search'); #? -my $title = emt('Account Report'); +my $title = mt('Account Report'); #false laziness w/report_cust_pkg.html my $custnum = ''; @@ -127,7 +127,7 @@ if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { 'hashref' => { 'custnum' => $custnum }, 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, }) or die "unknown custnum $custnum"; - $title = emt("Account Report: [_1]", $cust_main->name); + $title = mt("Account Report: [_1]", $cust_main->name); } </%init> diff --git a/httemplate/search/report_svc_phone.html b/httemplate/search/report_svc_phone.html index 9f1042608..63ca03e16 100644 --- a/httemplate/search/report_svc_phone.html +++ b/httemplate/search/report_svc_phone.html @@ -1,32 +1,6 @@ -<% include('/elements/header.html', 'Phone number total usage' ) %> +<& elements/report_svc_Common.html, + 'table' => 'svc_phone', + 'title' => 'Phone number report', -<FORM ACTION="svc_phone.cgi" METHOD="GET"> - -<INPUT TYPE="hidden" NAME="magic" VALUE="all"> -<INPUT TYPE="hidden" NAME="usage_total" VALUE="1"> - -<TABLE BGCOLOR="#cccccc" CELLSPACING=0> - -%# <TR> -%# <TH CLASS="background" COLSPAN=2 ALIGN="left"> -%# <FONT SIZE="+1">Search options</FONT> -%# </TH> -%# </TR> - - <% include ( '/elements/tr-input-beginning_ending.html', prefix=>'usage' ) %> - -</TABLE> - -<BR> -<INPUT TYPE="submit" VALUE="Search phone numbers"> - -</FORM> - -<% include('/elements/footer.html') %> -<%init> - -#? 'List services' ? something new? -die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('List rating data'); - -</%init> + 'action' => 'svc_phone.cgi', +&> diff --git a/httemplate/search/report_svc_phone_usage.html b/httemplate/search/report_svc_phone_usage.html new file mode 100644 index 000000000..9f1042608 --- /dev/null +++ b/httemplate/search/report_svc_phone_usage.html @@ -0,0 +1,32 @@ +<% include('/elements/header.html', 'Phone number total usage' ) %> + +<FORM ACTION="svc_phone.cgi" METHOD="GET"> + +<INPUT TYPE="hidden" NAME="magic" VALUE="all"> +<INPUT TYPE="hidden" NAME="usage_total" VALUE="1"> + +<TABLE BGCOLOR="#cccccc" CELLSPACING=0> + +%# <TR> +%# <TH CLASS="background" COLSPAN=2 ALIGN="left"> +%# <FONT SIZE="+1">Search options</FONT> +%# </TH> +%# </TR> + + <% include ( '/elements/tr-input-beginning_ending.html', prefix=>'usage' ) %> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="Search phone numbers"> + +</FORM> + +<% include('/elements/footer.html') %> +<%init> + +#? 'List services' ? something new? +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('List rating data'); + +</%init> diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi index 42a52d154..479b99044 100755 --- a/httemplate/search/report_tax.cgi +++ b/httemplate/search/report_tax.cgi @@ -250,8 +250,10 @@ my $conf = new FS::Conf; 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); @@ -487,7 +489,8 @@ my $tot_tax = 0; 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; @@ -522,7 +525,7 @@ foreach my $r ( qsearch({ 'table' => 'cust_main_county', })) { } 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, diff --git a/httemplate/search/report_tax.html b/httemplate/search/report_tax.html index 2ab0e0b2e..8a207aafb 100755 --- a/httemplate/search/report_tax.html +++ b/httemplate/search/report_tax.html @@ -34,9 +34,21 @@ % 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') ) { diff --git a/httemplate/search/rt_ticket.html b/httemplate/search/rt_ticket.html index 1ed5a3883..f5ac023b5 100644 --- a/httemplate/search/rt_ticket.html +++ b/httemplate/search/rt_ticket.html @@ -1,21 +1,21 @@ -<% include('elements/search.html', +<& elements/search.html, 'title' => 'Time worked summary', 'name_singular' => 'ticket', 'query' => $query, 'count_query' => $count_query, 'count_addl' => [ $format_seconds_sub, - $applied_time ? $format_seconds_sub : () ], + $applied ? $format_seconds_sub : () ], 'header' => [ 'Ticket #', 'Ticket', 'Time', - $applied_time ? 'Applied' : (), + $applied ? 'Applied' : (), ], 'fields' => [ 'ticketid', sub { encode_entities(shift->get('subject')) }, sub { my $seconds = shift->get('ticket_time'); &{ $format_seconds_sub }( $seconds ); }, - ($applied_time ? + ($applied ? sub { my $seconds = shift->get('applied_time'); &{ $format_seconds_sub }( $seconds ); } : () ), @@ -23,7 +23,7 @@ 'sort_fields' => [ 'ticketid', 'subject', 'transaction_time', - $applied_time ? 'applied_time' : (), + $applied ? 'applied_time' : (), ], 'links' => [ $link, @@ -31,8 +31,7 @@ '', '', ], - ) -%> +&> <%once> my $format_seconds_sub = sub { @@ -60,7 +59,6 @@ my @select = ( ); my @select_total = ( 'COUNT(*)' ); -my ($transaction_time, $applied_time); my $join = 'JOIN Users ON Transactions.Creator = Users.Id '; #. my $twhere = " @@ -68,6 +66,8 @@ my $twhere = " AND Transactions.ObjectId = Tickets.Id "; +my $transaction_time; +my $applied = ''; my $cfname = ''; if ( $cgi->param('cfname') =~ /^\w(\w|\s)*$/ ) { @@ -104,15 +104,14 @@ if ( $cgi->param('cfname') =~ /^\w(\w|\s)*$/ ) { $twhere .= " AND CustomFields.Name = '$cfname' AND (ocfv_new.Id IS NOT NULL OR ocfv_old.Id IS NOT NULL OR ocfv_main.Id IS NOT NULL)"; -} -else { +} else { + $transaction_time = " CASE transactions.type when 'Set' THEN (to_number(newvalue,'999999')-to_number(oldvalue, '999999')) * 60 ELSE timetaken*60 END"; - my $applied = ''; if ( $cgi->param('svcnum') =~ /^\s*(\d+)\s*$/ ) { $twhere .= " AND EXISTS( SELECT 1 FROM acct_rt_transaction WHERE acct_rt_transaction.transaction_id = Transactions.id AND svcnum = $1 )"; $applied = "AND svcnum = $1"; @@ -122,13 +121,11 @@ else { AND ( ( Transactions.Type = 'Set' AND Transactions.Field = 'TimeWorked' AND Transactions.NewValue != Transactions.OldValue ) - OR ( ( Transactions.Type='Create' OR Transactions.Type='Comment' OR Transactions.Type='Correspond' OR Transactions.Type='Touch' ) + OR ( Transactions.Type IN ( 'Create', 'Comment', 'Correspond', 'Touch' ) AND Transactions.TimeTaken > 0 ) )"; - $applied_time = "( SELECT SUM(support) FROM acct_rt_transaction LEFT JOIN Transactions ON ( transaction_id = Id ) $twhere $applied )"; - } @@ -155,9 +152,13 @@ my $ticket_time = "( SELECT SUM($transaction_time) $transactions )"; push @select, "$ticket_time AS ticket_time"; push @select_total, "SUM($ticket_time)"; -if ( $applied_time) { +if ( $applied ) { + + my $applied_time = "( SELECT SUM(support) FROM acct_rt_transaction LEFT JOIN Transactions ON ( transaction_id = Id ) $twhere $applied )"; + push @select, "$applied_time AS applied_time"; push @select_total, "SUM($applied_time)"; + } my $query = { diff --git a/httemplate/search/rt_transaction.html b/httemplate/search/rt_transaction.html index 1ae607be1..eb250fb27 100644 --- a/httemplate/search/rt_transaction.html +++ b/httemplate/search/rt_transaction.html @@ -1,4 +1,4 @@ -<% include('elements/search.html', +<& elements/search.html, 'title' => 'Time worked', 'name_singular' => 'transaction', 'query' => $query, @@ -29,8 +29,7 @@ '', '', ], - ) -%> +&> <%once> my $format_seconds_sub = sub { diff --git a/httemplate/search/sql.html b/httemplate/search/sql.html index bf5446975..71aa00671 100644 --- a/httemplate/search/sql.html +++ b/httemplate/search/sql.html @@ -1,9 +1,9 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Query Results', 'name' => 'rows', 'query' => "SELECT $sql", - ) -%> + +&> <%init> die "access denied" diff --git a/httemplate/search/sqlradius.cgi b/httemplate/search/sqlradius.cgi index 5363944e4..22984731a 100644 --- a/httemplate/search/sqlradius.cgi +++ b/httemplate/search/sqlradius.cgi @@ -51,7 +51,7 @@ % @{ $part_export->usage_sessions( { % 'stoptime_start' => $beginning, % 'stoptime_end' => $ending, -% 'open_sessions' => $open_sessions, +% 'session_status' => $status, % 'starttime_start' => $starttime_beginning, % 'starttime_end' => $starttime_ending, % 'svc_acct' => $cgi_svc_acct, @@ -117,9 +117,9 @@ if ( $cgi->param('end') && $cgi->param('end') =~ /^(\d+)$/ ) { $ending = $1; } -my $open_sessions = ''; -if ( $cgi->param('open_sessions') =~ /^(\d*)$/ ) { - $open_sessions = $1; +my $status = ''; +if ( $cgi->param('session_status') =~ /^(closed|open)$/ ) { + $status = $1; } my( $starttime_beginning, $starttime_ending ) = ( '', '' ); @@ -242,8 +242,15 @@ my $time_format = sub { $pretty; }; +my $time_format_or_open = sub { + my $time = shift; + return '<CENTER>OPEN</CENTER>' if $time == 0; + &{$time_format}($time); +}; + my $duration_format = sub { my $seconds = shift; + return '' if $seconds eq ''; # open session my $hour = int($seconds/3600); my $min = int( ($seconds%3600) / 60 ); my $sec = $seconds%60; @@ -339,7 +346,7 @@ tie %fields, 'Tie::IxHash', 'acctstoptime' => { name => 'End time', attrib => 'Acct-Stop-Time', - fmt => $time_format, + fmt => $time_format_or_open, align => 'left', }, 'acctsessiontime' => { diff --git a/httemplate/search/sqlradius.html b/httemplate/search/sqlradius.html index 7b9fce310..547a9bb44 100644 --- a/httemplate/search/sqlradius.html +++ b/httemplate/search/sqlradius.html @@ -52,8 +52,9 @@ <TR> <TD>Show:</TD> <TD> - <INPUT TYPE="radio" NAME="open_sessions" VALUE="0" onClick="open_changed(this);" CHECKED>Completed sessions<BR> - <INPUT TYPE="radio" NAME="open_sessions" VALUE="1" onClick="open_changed(this);">Open sessions + <INPUT TYPE="radio" NAME="session_status" VALUE="" onClick="enable_stop(true);" CHECKED>All sessions<BR> + <INPUT TYPE="radio" NAME="session_status" VALUE="closed" onClick="enable_stop(true);">Completed sessions<BR> + <INPUT TYPE="radio" NAME="session_status" VALUE="open" onClick="enable_stop(false);">Open sessions </TD> </TR> @@ -69,41 +70,31 @@ <SCRIPT TYPE="text/javascript"> - function open_changed(what) { - - var value=get_open_value(what); - if ( value == '1' ) { - what.form.stoptime_beginning_text.disabled = true; - what.form.stoptime_ending_text.disabled = true; - what.form.stoptime_beginning_text.style.backgroundColor = '#dddddd'; - what.form.stoptime_ending_text.style.backgroundColor = '#dddddd'; - what.form.stoptime_beginning_button.style.display = 'none'; - what.form.stoptime_ending_button.style.display = 'none'; - what.form.stoptime_beginning_disabled.style.display = ''; - what.form.stoptime_ending_disabled.style.display = ''; - } else if ( value == '0' ) { - what.form.stoptime_beginning_text.disabled = false; - what.form.stoptime_ending_text.disabled = false; - what.form.stoptime_beginning_text.style.backgroundColor = '#ffffff'; - what.form.stoptime_ending_text.style.backgroundColor = '#ffffff'; - what.form.stoptime_beginning_button.style.display = ''; - what.form.stoptime_ending_button.style.display = ''; - what.form.stoptime_beginning_disabled.style.display = 'none'; - what.form.stoptime_ending_disabled.style.display = 'none'; + function enable_stop(value) { + + var f = document.OneTrueForm; + if ( value ) { + f.stoptime_beginning_text.disabled = false; + f.stoptime_ending_text.disabled = false; + f.stoptime_beginning_text.style.backgroundColor = '#ffffff'; + f.stoptime_ending_text.style.backgroundColor = '#ffffff'; + f.stoptime_beginning_button.style.display = ''; + f.stoptime_ending_button.style.display = ''; + f.stoptime_beginning_disabled.style.display = 'none'; + f.stoptime_ending_disabled.style.display = 'none'; + } else { + f.stoptime_beginning_text.disabled = true; + f.stoptime_ending_text.disabled = true; + f.stoptime_beginning_text.style.backgroundColor = '#dddddd'; + f.stoptime_ending_text.style.backgroundColor = '#dddddd'; + f.stoptime_beginning_button.style.display = 'none'; + f.stoptime_ending_button.style.display = 'none'; + f.stoptime_beginning_disabled.style.display = ''; + f.stoptime_ending_disabled.style.display = ''; } } - function get_open_value(what) { - var rad_val = ''; - for (var i=0; i < what.form.open_sessions.length; i++) { - if (what.form.open_sessions[i].checked) { - var rad_val = what.form.open_sessions[i].value; - } - } - return rad_val; - } - </SCRIPT> <TR> diff --git a/httemplate/search/svc_acct.cgi b/httemplate/search/svc_acct.cgi index 92e1c500c..b9e5a7cc9 100755 --- a/httemplate/search/svc_acct.cgi +++ b/httemplate/search/svc_acct.cgi @@ -1,4 +1,4 @@ -<& elements/search.html, +<& elements/svc_Common.html, 'title' => emt('Account Search Results'), 'name' => emt('accounts'), 'query' => $sql_query, diff --git a/httemplate/search/svc_broadband.cgi b/httemplate/search/svc_broadband.cgi index ee62e9084..8366d214b 100755 --- a/httemplate/search/svc_broadband.cgi +++ b/httemplate/search/svc_broadband.cgi @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/svc_Common.html, 'title' => 'Broadband Search Results', 'name' => 'broadband services', 'html_init' => $html_init, @@ -49,8 +49,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> die "access denied" unless @@ -72,7 +72,7 @@ else { } if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { - $search_hash{'order_by'} = $1; + $search_hash{'order_by'} = "ORDER BY $1"; } my $sql_query = FS::svc_broadband->search(\%search_hash); diff --git a/httemplate/search/svc_dish.cgi b/httemplate/search/svc_dish.cgi index 94da03537..1f8cbc395 100755 --- a/httemplate/search/svc_dish.cgi +++ b/httemplate/search/svc_dish.cgi @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/svc_Common.html, 'title' => 'Dish Network Search Results', 'name' => 'services', 'query' => $sql_query, @@ -34,8 +34,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> die "access denied" @@ -61,7 +61,7 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. ' LEFT JOIN part_svc USING ( svcpart ) '. ' LEFT JOIN cust_pkg USING ( pkgnum ) '. - ' LEFT JOIN cust_main USING ( custnum ) '; + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); #here is the agent virtualization push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( diff --git a/httemplate/search/svc_domain.cgi b/httemplate/search/svc_domain.cgi index 9827b8d38..56cfa30c8 100755 --- a/httemplate/search/svc_domain.cgi +++ b/httemplate/search/svc_domain.cgi @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => "Domain Search Results", 'name' => 'domains', 'query' => $sql_query, @@ -34,8 +34,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> die "access denied" @@ -66,7 +66,7 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. ' LEFT JOIN part_svc USING ( svcpart ) '. ' LEFT JOIN cust_pkg USING ( pkgnum ) '. - ' LEFT JOIN cust_main USING ( custnum ) '; + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); #here is the agent virtualization push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( diff --git a/httemplate/search/svc_external.cgi b/httemplate/search/svc_external.cgi index cb51d44fd..b282939a7 100755 --- a/httemplate/search/svc_external.cgi +++ b/httemplate/search/svc_external.cgi @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/svc_Common.html, 'title' => 'External service search results', 'name' => 'external services', 'query' => $sql_query, @@ -40,9 +40,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> - + +&> <%init> die "access denied" @@ -90,7 +89,7 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. ' LEFT JOIN part_svc USING ( svcpart ) '. ' LEFT JOIN cust_pkg USING ( pkgnum ) '. - ' LEFT JOIN cust_main USING ( custnum ) '; + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); #here is the agent virtualization push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( diff --git a/httemplate/search/svc_forward.cgi b/httemplate/search/svc_forward.cgi index f17f131ab..6a23bb3bb 100755 --- a/httemplate/search/svc_forward.cgi +++ b/httemplate/search/svc_forward.cgi @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => "Mail forward Search Results", 'name' => 'mail forwards', 'query' => $sql_query, @@ -39,8 +39,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> die "access denied" @@ -67,7 +67,7 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. ' LEFT JOIN part_svc USING ( svcpart ) '. ' LEFT JOIN cust_pkg USING ( pkgnum ) '. - ' LEFT JOIN cust_main USING ( custnum ) '; + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); #here is the agent virtualization push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( diff --git a/httemplate/search/svc_hardware.cgi b/httemplate/search/svc_hardware.cgi index ec09be82b..93fc2c391 100644 --- a/httemplate/search/svc_hardware.cgi +++ b/httemplate/search/svc_hardware.cgi @@ -1,4 +1,4 @@ -<% include('elements/search.html', +<& elements/svc_Common.html, 'title' => 'Hardware service search results', 'name' => 'installations', 'query' => $sql_query, @@ -34,8 +34,7 @@ FS::UI::Web::cust_colors() ], 'style' => [ $svc_cancel_style, ('') x 7, FS::UI::Web::cust_styles() ], - ) -%> +&> <%init> die "access denied" @@ -44,8 +43,8 @@ die "access denied" my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) LEFT JOIN part_svc USING ( svcpart ) - LEFT JOIN cust_pkg USING ( pkgnum ) - LEFT JOIN cust_main USING ( custnum ) + LEFT JOIN cust_pkg USING ( pkgnum )'. + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').' LEFT JOIN hardware_type USING ( typenum )'; my @extra_sql; diff --git a/httemplate/search/svc_phone.cgi b/httemplate/search/svc_phone.cgi index 29434083f..f3a056475 100644 --- a/httemplate/search/svc_phone.cgi +++ b/httemplate/search/svc_phone.cgi @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/svc_Common.html, 'title' => "Phone number search results", 'name' => 'phone numbers', 'query' => $sql_query, @@ -9,7 +9,7 @@ 'Country code', 'Phone number', @header, - FS::UI::Web::cust_header(), + FS::UI::Web::cust_header($cgi->param('cust_fields')), ], 'fields' => [ 'svcnum', 'svc', @@ -24,7 +24,7 @@ $link, ( map '', @header ), ( map { $_ ne 'Cust. Status' ? $link_cust : '' } - FS::UI::Web::cust_header() + FS::UI::Web::cust_header($cgi->param('cust_fields')) ), ], 'align' => 'rlrr'. @@ -46,8 +46,8 @@ ( map '', @header ), FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> die "access denied" @@ -56,8 +56,6 @@ die "access denied" my $conf = new FS::Conf; my @select = (); -my %svc_phone = (); -my @extra_sql = (); my $orderby = 'ORDER BY svcnum'; my @header = (); @@ -65,9 +63,12 @@ my @fields = (); my $link = [ "${p}view/svc_phone.cgi?", 'svcnum' ]; my $redirect = $link; +my %search_hash = (); +my @extra_sql = (); + if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { - push @extra_sql, 'pkgnum IS NULL' + $search_hash{'unlinked'} = 1 if $cgi->param('magic') eq 'unlinked'; if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { @@ -119,52 +120,31 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { } +} elsif ( $cgi->param('magic') =~ /^advanced$/ ) { + + for (qw( agentnum custnum cust_status balance balance_days cust_fields )) { + $search_hash{$_} = $cgi->param($_) if length($cgi->param($_)); + } + + for (qw( payby pkgpart svcpart )) { + $search_hash{$_} = [ $cgi->param($_) ] if $cgi->param($_); + } + } elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) { - push @extra_sql, "svcpart = $1"; + $search_hash{'svcpart'} = [ $1 ]; } else { $cgi->param('phonenum') =~ /^([\d\- ]+)$/; - ( $svc_phone{'phonenum'} = $1 ) =~ s/\D//g; + my $phonenum = $1; + $phonenum =~ s/\D//g; + push @extra_sql, "phonenum = '$phonenum'"; } -my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. - ' LEFT JOIN part_svc USING ( svcpart ) '. - ' LEFT JOIN cust_pkg USING ( pkgnum ) '. - ' LEFT JOIN cust_main USING ( custnum ) '; - -#here is the agent virtualization -push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( - 'null_right' => 'View/link unlinked services' - ); - -my $extra_sql = ''; -if ( @extra_sql ) { - $extra_sql = ( keys(%svc_phone) ? ' AND ' : ' WHERE ' ). - join(' AND ', @extra_sql ); -} +$search_hash{'addl_select'} = \@select; +$search_hash{'order_by'} = $orderby; +$search_hash{'where'} = \@extra_sql; -my $count_query = "SELECT COUNT(*) FROM svc_phone $addl_from "; -if ( keys %svc_phone ) { - $count_query .= ' WHERE '. - join(' AND ', map "$_ = ". dbh->quote($svc_phone{$_}), - keys %svc_phone - ); -} -$count_query .= $extra_sql; - -my $sql_query = { - 'table' => 'svc_phone', - 'hashref' => \%svc_phone, - 'select' => join(', ', - 'svc_phone.*', - 'part_svc.svc', - @select, - 'cust_main.custnum', - FS::UI::Web::cust_sql_fields(), - ), - 'extra_sql' => $extra_sql, - 'order_by' => $orderby, - 'addl_from' => $addl_from, -}; +my $sql_query = FS::svc_phone->search(\%search_hash); +my $count_query = delete($sql_query->{'count_query'}); #smaller false laziness w/svc_*.cgi here my $link_cust = sub { diff --git a/httemplate/search/svc_www.cgi b/httemplate/search/svc_www.cgi index adc31c88a..7410262e8 100755 --- a/httemplate/search/svc_www.cgi +++ b/httemplate/search/svc_www.cgi @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/svc_Common.html, 'title' => 'Virtual Host Search Results', 'name' => 'virtual hosts', 'query' => $sql_query, @@ -45,8 +45,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> die "access denied" @@ -73,7 +73,7 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. ' LEFT JOIN part_svc USING ( svcpart ) '. ' LEFT JOIN cust_pkg USING ( pkgnum ) '. - ' LEFT JOIN cust_main USING ( custnum ) '; + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); #here is the agent virtualization push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( diff --git a/httemplate/search/timeworked.html b/httemplate/search/timeworked.html index bbfd0334d..fa4b89539 100644 --- a/httemplate/search/timeworked.html +++ b/httemplate/search/timeworked.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Time Worked', 'name' => 'time', 'html_form' => qq!<FORM NAME="timeForm" ACTION="${p}misc/timeworked.html" METHOD="POST">!, @@ -33,9 +33,8 @@ '', ], 'html_foot' => $html_foot, - ) - -%> + +&> <%init> die "access denied" diff --git a/httemplate/search/unearned_detail.html b/httemplate/search/unearned_detail.html index f61de052e..285fb50a7 100644 --- a/httemplate/search/unearned_detail.html +++ b/httemplate/search/unearned_detail.html @@ -114,13 +114,12 @@ if ( $cgi->param('status') =~ /^([a-z]+)$/ ) { 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; } @@ -210,8 +209,8 @@ push @select, '(edate - 82799) AS before_edate'; #usage always excluded # always 'nottax', not 'istax' -$join_cust = ' JOIN cust_bill USING ( invnum ) - LEFT JOIN cust_main USING ( custnum ) '; +$join_cust = ' JOIN cust_bill USING ( invnum ) '. + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); $join_pkg .= ' LEFT JOIN cust_pkg USING ( pkgnum ) LEFT JOIN part_pkg USING ( pkgpart ) @@ -222,7 +221,7 @@ my $where = ' WHERE '. join(' AND ', @where); my $count_query = "SELECT COUNT(DISTINCT billpkgnum), SUM( $unearned_base ), SUM( $unearned_sql ) - FROM cust_bill_pkg $join_cust $join_pkg $where"; + FROM cust_bill_pkg $join_pkg $join_cust $where"; push @select, 'part_pkg.pkg', 'part_pkg.freq', @@ -231,7 +230,7 @@ push @select, 'part_pkg.pkg', my $query = { 'table' => 'cust_bill_pkg', - 'addl_from' => "$join_cust $join_pkg", + 'addl_from' => "$join_pkg $join_cust", 'hashref' => {}, 'select' => join(",\n", @select ), 'extra_sql' => $where, diff --git a/httemplate/search/unprovisioned_services.html b/httemplate/search/unprovisioned_services.html index f85e4fb19..a7791ba86 100644 --- a/httemplate/search/unprovisioned_services.html +++ b/httemplate/search/unprovisioned_services.html @@ -1,4 +1,4 @@ -<% include( 'elements/search.html', +<& elements/search.html, 'title' => 'Unprovisioned Service Search Results', 'name' => 'packages with unprovisioned services', 'query' => { @@ -54,8 +54,8 @@ '', FS::UI::Web::cust_styles(), ], - ) -%> + +&> <%init> die "access denied" @@ -74,7 +74,8 @@ my $search = " where cust_pkg.cancel is null and pkg_svc.quantity > 0 and " . " cust_svc.pkgnum = cust_pkg.pkgnum and " . " cust_svc.svcpart = pkg_svc.svcpart) $svcpart_limit"; -my $addl_from = " join pkg_svc using (pkgpart) join cust_main using (custnum) "; +my $addl_from = " join pkg_svc using (pkgpart) ". + FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'); # this was very painful to derive but it appears correct #select cust_pkg.custnum,cust_pkg.pkgpart,cust_pkg.pkgnum, pkg_svc.svcpart from cust_pkg join diff --git a/httemplate/view/bill_batch.cgi b/httemplate/view/bill_batch.cgi index 7d640395e..55ee4be1c 100644 --- a/httemplate/view/bill_batch.cgi +++ b/httemplate/view/bill_batch.cgi @@ -13,7 +13,7 @@ 'hashref' => { }, 'addl_from' => 'LEFT JOIN cust_bill USING ( invnum ) '. - 'LEFT JOIN cust_main USING ( custnum )', + FS::UI::Web::join_cust_main('cust_bill'), 'extra_sql' => " WHERE batchnum = $batchnum", }, 'count_query' => "SELECT COUNT(*) FROM cust_bill_batch WHERE batchnum = $batchnum", diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html index 5c46803d2..b863a734b 100644 --- a/httemplate/view/cust_main/billing.html +++ b/httemplate/view/cust_main/billing.html @@ -247,6 +247,10 @@ <TD ALIGN="right"><% mt('Email address(es)') |h %></TD> <TD BGCOLOR="#ffffff"> <% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) || $no %> +% if ( $cust_main->message_noemail ) { + <BR> + <SPAN STYLE="font-size: small"><% emt('(do not send notices)') %></SPAN> +% } </TD> </TR> % } diff --git a/httemplate/view/cust_main/change_history.html b/httemplate/view/cust_main/change_history.html index ea84b8f75..bf32a49f9 100644 --- a/httemplate/view/cust_main/change_history.html +++ b/httemplate/view/cust_main/change_history.html @@ -43,10 +43,12 @@ tie my %tables, 'Tie::IxHash', '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, @@ -58,6 +60,7 @@ my %table_join = ( 'svc_external' => $svc_join, 'svc_phone' => $svc_join, 'phone_device' => $svc_join, + 'cust_pkg_discount'=> $pkg_join, ); @@ -104,7 +107,7 @@ my $conf = new FS::Conf; 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 diff --git a/httemplate/view/cust_main/packages.html b/httemplate/view/cust_main/packages.html index 7d7930634..546dd89c3 100755 --- a/httemplate/view/cust_main/packages.html +++ b/httemplate/view/cust_main/packages.html @@ -1,3 +1,29 @@ +<STYLE TYPE="text/css"> +td.package { + vertical-align: top; + border-width: 0; + border-style: solid; + border-color: #bbbbff; +} +table.package { + border: none; + padding: 0; + border-spacing: 0; + width: 100%; +} +table.usage { + border: 1px solid black; + margin: auto; + width: 60%; + border-spacing: 0px; +} +.shared > * { + background-color: #ffffaa; +} +.row0 { background-color: #eeeeee; } +.row1 { background-color: #ffffff; } + +</STYLE> % my $s = 0; % if ( $curuser->access_right('Qualify service') ) { @@ -75,7 +101,7 @@ <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, @@ -87,7 +113,6 @@ <& packages/section.html, 'cust_main' => $cust_main, 'packages' => $packages, - 'show_location' => $show_location, &> </TABLE> % } @@ -114,10 +139,6 @@ my $curuser = $FS::CurrentUser::CurrentUser; my( $packages, $num_old_packages ) = get_packages($cust_main, $conf); - -my $show_location = $conf->exists('cust_pkg-always_show_location') - || (grep $_->locationnum, @$packages); # ? '1' : '0'; - my $countrydefault = scalar($conf->config('countrydefault')) || 'US'; #subroutines @@ -178,6 +199,10 @@ sub get_packages { } $num_old_packages -= scalar(@packages); + + # don't include supplemental packages in this list; they'll be found from + # their main packages + @packages = grep !$_->main_pkgnum, @packages; ( \@packages, $num_old_packages ); } diff --git a/httemplate/view/cust_main/packages/contact.html b/httemplate/view/cust_main/packages/contact.html new file mode 100644 index 000000000..93129915f --- /dev/null +++ b/httemplate/view/cust_main/packages/contact.html @@ -0,0 +1,61 @@ +% if ( $contact ) { + <% $contact->line |h %> +% if ( $show_link ) { + <FONT SIZE=-1> + ( <%pkg_change_contact_link($cust_pkg)%> ) + </FONT> +% } +% } elsif ( $show_link ) { + <FONT SIZE=-1> + ( <%pkg_add_contact_link($cust_pkg)%> ) + </FONT> +% } +<%init> + +my $conf = new FS::Conf; +my %opt = @_; + +my $cust_pkg = $opt{'cust_pkg'}; + +my $show_link = + ! $cust_pkg->get('cancel') + && $FS::CurrentUser::CurrentUser->access_right('Change customer package'); + +my $contact = $cust_pkg->contact_obj; + +sub pkg_change_contact_link { + my $cust_pkg = shift; + #my $pkgpart = $cust_pkg->pkgpart; + include( '/elements/popup_link-cust_pkg.html', + 'action' => $p. "misc/change_pkg_contact.html", + 'label' => emt('Change'), # contact'), + 'actionlabel' => emt('Change'), + 'cust_pkg' => $cust_pkg, + 'width' => 616, + 'height' => 220, + ); +} + +sub pkg_add_contact_link { + my $cust_pkg = shift; + #my $pkgpart = $cust_pkg->pkgpart; + include( '/elements/popup_link-cust_pkg.html', + 'action' => $p. "misc/change_pkg_contact.html", + 'label' => emt('Add contact'), + 'actionlabel' => emt('Change'), + 'cust_pkg' => $cust_pkg, + 'width' => 616, + 'height' => 192, + ); +} + +#sub edit_contact_link { +# my $contactnum = shift; +# include( '/elements/popup_link.html', +# 'action' => $p. "edit/cust_contact.cgi?contactnum=$contactnum", +# 'label' => emt('Edit contact'), +# 'actionlabel' => emt('Edit'), +# ); +#} + +</%init> diff --git a/httemplate/view/cust_main/packages/location.html b/httemplate/view/cust_main/packages/location.html index 34e3a64c3..f2d379841 100644 --- a/httemplate/view/cust_main/packages/location.html +++ b/httemplate/view/cust_main/packages/location.html @@ -1,7 +1,5 @@ -<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>', @@ -24,8 +22,8 @@ </FONT> % } -% unless ( $cust_pkg->locationnum ) { - </I> +% if ( $default ) { + </DIV> % } % if ( ! $cust_pkg->get('cancel') @@ -41,19 +39,19 @@ </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; diff --git a/httemplate/view/cust_main/packages/package.html b/httemplate/view/cust_main/packages/package.html index 5d93ad46f..520305a9a 100644 --- a/httemplate/view/cust_main/packages/package.html +++ b/httemplate/view/cust_main/packages/package.html @@ -1,5 +1,6 @@ -<TD CLASS="inv" BGCOLOR="<% $bgcolor %>" VALIGN="top"> - <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%"> +<TD CLASS="inv package" BGCOLOR="<% $bgcolor %>" VALIGN="top" + STYLE="border-left-width: <% $supplemental * 30 %>px"> + <TABLE CLASS="inv package"> <TR> <TD COLSPAN=2> <A NAME="cust_pkg<% $cust_pkg->pkgnum %>" @@ -17,50 +18,62 @@ <B><% $cust_pkg->quantity %></B> </TD> </TR> -% } +% } <TR> <TD COLSPAN=2> <FONT SIZE=-1> -% unless ( $cust_pkg->get('cancel') ) { +% unless ( $cust_pkg->get('cancel') ) { % -% my $br = 0; -% if ( $curuser->access_right('Change customer package') ) { -% $br=1; - ( <%pkg_change_link($cust_pkg)%> ) -% } +% if ( $supplemental or $part_pkg->freq eq '0' ) { +% # Supplemental packages can't be changed independently. +% # One-time charges don't need to be changed. +% # For both of those, we only show "Edit dates", "Add comments", +% # and "Add invoice details". +% if ( $curuser->access_right('Edit customer package dates') ) { + ( <%pkg_dates_link($cust_pkg)%> ) +% } +% } else { +% # the usual case: links to change package definition, +% # discount, and customization +% my $br = 0; +% if ( $curuser->access_right('Change customer package') ) { +% $br=1; + ( <%pkg_change_link($cust_pkg)%> ) +% } % -% if ( $curuser->access_right('Edit customer package dates') ) { -% $br=1; - ( <%pkg_dates_link($cust_pkg)%> ) -% } +% if ( $curuser->access_right('Edit customer package dates') ) { +% $br=1; + ( <%pkg_dates_link($cust_pkg)%> ) +% } % -% if ( $curuser->access_right('Discount customer package') -% && $part_pkg->can_discount -% && ! scalar($cust_pkg->cust_pkg_discount_active) -% && ! scalar($cust_pkg->part_pkg->part_pkg_discount) -% ) -% { -% $br=1; - ( <%pkg_discount_link($cust_pkg)%> ) -% } +% if ( $curuser->access_right('Discount customer package') +% && $part_pkg->can_discount +% && ! scalar($cust_pkg->cust_pkg_discount_active) +% && ! scalar($cust_pkg->part_pkg->part_pkg_discount) +% ) +% { +% $br=1; + ( <%pkg_discount_link($cust_pkg)%> ) +% } % -% if ( $curuser->access_right('Customize customer package') ) { -% $br=1; - ( <%pkg_customize_link($cust_pkg,$part_pkg)%> ) -% } +% if ( $curuser->access_right('Customize customer package') ) { +% $br=1; + ( <%pkg_customize_link($cust_pkg,$part_pkg)%> ) +% } % - <% $br ? '<BR>' : '' %> -% } + <% $br ? '<BR>' : '' %> +% } -% if ( $cust_pkg->num_cust_event -% && ( $curuser->access_right('Billing event reports') -% || $curuser->access_right('View customer billing events') -% ) -% ) { - ( <%pkg_event_link($cust_pkg)%> ) -% } +% if ( $cust_pkg->num_cust_event +% && ( $curuser->access_right('Billing event reports') +% || $curuser->access_right('View customer billing events') +% ) +% ) { + ( <%pkg_event_link($cust_pkg)%> ) +% } +% } #!$supplemental </FONT> </TD> @@ -170,15 +183,40 @@ </TR> % if ( $curuser->access_right('Change customer package') and % !$cust_pkg->get('cancel') and -% !$opt{'show_location'}) { +% !$supplemental and +% $part_pkg->freq ne '0' ) { <TR> +% if ( FS::Conf->new->exists('invoice-unitprice') ) { <TD><FONT SIZE="-1"> - ( <% pkg_change_location_link($cust_pkg) %> ) + ( <% pkg_change_quantity_link($cust_pkg) %> ) </FONT></TD> +% } </TR> % } % } </TABLE> +% if ( @cust_pkg_usage ) { + <TABLE CLASS="usage inv"> + <TR><TH COLSPAN=4><% mt('Included usage') %></TH></TR> +% foreach my $usage (@cust_pkg_usage) { +% my $part = $usage->part_pkg_usage; +% my $ratio = 255 * ($usage->minutes / $part->minutes); +% $ratio = 255 if $ratio > 255; # because rollover +% my $color = sprintf('STYLE="font-weight: bold; color: #%02x%02x00"', 255 - $ratio, $ratio); +% my $trstyle = ''; +% $trstyle = ' CLASS="shared"' if $part->shared; + <TR<%$trstyle%>> + <TD ALIGN="right"><% $part->description %>: </TD> + <TD <%$color%> ALIGN="right"><% $usage->minutes %></TD> + <TD <%$color%>> / </TD> + <TD <%$color%>><% $part->minutes %></TD> +% if ( $part->shared ) { + <TD><I>(shared)</I></TD> +% } + </TR> +% } + </TABLE> +% } </TD> @@ -196,6 +234,18 @@ my $countrydefault = $opt{'countrydefault'} || 'US'; my $statedefault = $opt{'statedefault'} || ($countrydefault eq 'US' ? 'CA' : ''); +my $supplemental = $opt{'supplemental'} || 0; + +$cust_pkg->pkgnum =~ /^(\d+)$/; +my $pkgnum = $1; +my @cust_pkg_usage = qsearch({ + 'select' => 'cust_pkg_usage.*', + 'table' => 'cust_pkg_usage', + 'addl_from' => ' JOIN part_pkg_usage USING (pkgusagepart)', + 'extra_sql' => " WHERE pkgnum = $1", + 'order_by' => ' ORDER BY priority ASC, description ASC', +}); + #subroutines #false laziness w/status.html @@ -229,6 +279,17 @@ sub pkg_change_location_link { ); } +sub pkg_change_quantity_link { + include( '/elements/popup_link-cust_pkg.html', + 'action' => $p. 'edit/cust_pkg_quantity.html?', + 'label' => emt('Change quantity'), + 'actionlabel' => emt('Change'), + 'cust_pkg' => shift, + 'width' => 390, + 'height' => 220, + ); +} + sub pkg_dates_link { pkg_link('edit/REAL_cust_pkg', emt('Edit dates'), @_ ); } sub pkg_discount_link { diff --git a/httemplate/view/cust_main/packages/section.html b/httemplate/view/cust_main/packages/section.html index 85f0c795a..5f54c0a36 100755 --- a/httemplate/view/cust_main/packages/section.html +++ b/httemplate/view/cust_main/packages/section.html @@ -1,53 +1,48 @@ % if ( @$packages ) { -% my $bgcolor1 = '#eeeeee'; -% my $bgcolor2 = '#ffffff'; -% my $bgcolor = ''; - <TR> % #my $width = $show_location ? 'WIDTH="25%"' : 'WIDTH="33%"'; <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Package') |h %></TH> <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Status') |h %></TH> -% if ( $show_location ) { - <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Location') |h %></TH> -% } + <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Contact/Location') |h %></TH> <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Services') |h %></TH> </TR> % #$FS::cust_pkg::DEBUG = 2; % foreach my $cust_pkg (@$packages) { + <& .packagerow, $cust_pkg, + 'cust_main' => $opt{'cust_main'}, + 'bgcolor' => $opt{'bgcolor'}, + %conf_opt + &> +% } +% } else { # there are no packages +<BR> +% } +<%def .packagerow> % -% if ( $bgcolor eq $bgcolor1 ) { -% $bgcolor = $bgcolor2; -% } else { -% $bgcolor = $bgcolor1; -% } -% -% my %iopt = ( -% 'bgcolor' => $bgcolor, -% 'cust_pkg' => $cust_pkg, -% 'part_pkg' => $cust_pkg->part_pkg, -% 'cust_main' => $opt{'cust_main'}, -% %conf_opt, -% ); -% - +% my ($cust_pkg, %iopt) = @_; +% $iopt{'cust_pkg'} = $cust_pkg; +% $iopt{'part_pkg'} = $cust_pkg->part_pkg; <!--pkgnum: <% $cust_pkg->pkgnum %>--> - <TR> + <TR CLASS="row<%$row % 2%>"> <& package.html, %iopt &> - <& status.html, %iopt &> -% if ( $show_location ) { - <& location.html, %iopt &> -% } + <& status.html, %iopt &> + <TD CLASS="inv" BGCOLOR="<% $iopt{bgcolor} %>" WIDTH="20%" VALIGN="top"> + <& contact.html, %iopt &> + <& location.html, %iopt &> + </TD> <& services.html, %iopt &> </TR> - -% } #foreach $cust_pkg -%# </TABLE> -% } #if @$packages -% else { -<BR> +% $row++; +% # include supplemental packages if any +% $iopt{'supplemental'} = ($iopt{'supplemental'} || 0) + 1; +% foreach my $supp_pkg ($cust_pkg->supplemental_pkgs) { + <& .packagerow, $supp_pkg, %iopt &> % } - +</%def> +<%shared> +my $row = 0; +</%shared> <%init> my %opt = @_; @@ -56,7 +51,6 @@ my $conf = new FS::Conf; 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 { @@ -89,10 +83,8 @@ my %conf_opt = ( '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> diff --git a/httemplate/view/cust_main/packages/status.html b/httemplate/view/cust_main/packages/status.html index e9017745b..9d5a88e0f 100644 --- a/httemplate/view/cust_main/packages/status.html +++ b/httemplate/view/cust_main/packages/status.html @@ -1,9 +1,11 @@ -<TD CLASS="inv" BGCOLOR="<% $bgcolor %>"> +<TD CLASS="inv" BGCOLOR="<% $bgcolor %>" VALIGN="top"> <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%"> %#this should use cust_pkg->status and cust_pkg->statuscolor eventually -% if ( $cust_pkg->order_date ) { +% if ( $supplemental ) { + <% pkg_status_row_colspan($cust_pkg, emt('Supplemental'), '', 'color' => '7777FF', %opt) %> +% } elsif ( $cust_pkg->order_date ) { <% pkg_status_row($cust_pkg, emt('Ordered'), 'order_date', %opt ) %> % } @@ -12,30 +14,25 @@ <% pkg_status_row($cust_pkg, emt('Cancelled'), 'cancel', 'color'=>'FF0000', %opt ) %> - <% pkg_status_row_colspan( $cust_pkg, - ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '', - 'align'=>'right', 'color'=>'ff0000', 'size'=>'-2', 'colspan'=>$colspan, - %opt - ) - %> + <% pkg_reason_row($cust_pkg, $cpr, color => 'ff0000', %opt) %> % unless ( $cust_pkg->get('setup') ) { - <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', 'colspan'=>$colspan, %opt, ) %> + <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', %opt, ) %> % } else { <% pkg_status_row( $cust_pkg, emt('Setup'), 'setup', %opt ) %> - <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_changed( $cust_pkg, %opt ) %> <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %> <% pkg_status_row_if( $cust_pkg, emt('Suspended'), 'susp', %opt, curuser=>$curuser ) %> % } % -% if ( $part_pkg->freq ) { #? +% if ( $part_pkg->freq and !$supplemental ) { #? <TR> - <TD COLSPAN=<%$colspan%>> + <TD COLSPAN=<%$opt{colspan}%>> <FONT SIZE=-1> % if ( $curuser->access_right('Un-cancel customer package') ) { ( <% pkg_uncancel_link($cust_pkg) %> ) @@ -52,26 +49,21 @@ <% pkg_status_row( $cust_pkg, emt('Suspended'), 'susp', 'color'=>'FF9900', %opt ) %> - <% pkg_status_row_colspan( $cust_pkg, - ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '', - 'align'=>'right', 'color'=>'FF9900', 'size'=>'-2', 'colspan'=>$colspan, - %opt, - ) - %> + <% pkg_reason_row( $cust_pkg, $cpr, 'color' => 'FF9900', %opt ) %> - <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_noauto( $cust_pkg, %opt ) %> - <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_discount( $cust_pkg, %opt ) %> % unless ( $cust_pkg->get('setup') ) { - <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', 'colspan'=>$colspan, %opt ) %> + <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', %opt ) %> % } else { <% pkg_status_row($cust_pkg, emt('Setup'), 'setup', %opt ) %> % } <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %> - <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_changed( $cust_pkg, %opt ) %> <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %> % if ( $cust_pkg->option('suspend_bill', 1) % || ( $part_pkg->option('suspend_bill', 1) @@ -85,31 +77,33 @@ <% pkg_status_row_if( $cust_pkg, emt('Expires'), 'expire', %opt, curuser=>$curuser ) %> <% pkg_status_row_if( $cust_pkg, emt('Contract ends'), 'contract_end', %opt ) %> - <TR> - <TD COLSPAN=<%$colspan%>> - <FONT SIZE=-1> -% if ( $curuser->access_right('Unsuspend customer package') ) { - ( <% pkg_unsuspend_link($cust_pkg) %> ) - ( <% pkg_resume_link($cust_pkg) %> ) -% } -% if ( $curuser->access_right('Cancel customer package immediately') ) { - ( <% pkg_cancel_link($cust_pkg) %> ) -% } - </FONT> - </TD> - </TR> - +% if ( !$supplemental ) { + <TR> + <TD COLSPAN=<%$opt{colspan}%>> + <FONT SIZE=-1> +% if ( $curuser->access_right('Unsuspend customer package') ) { + ( <% pkg_unsuspend_link($cust_pkg) %> ) + ( <% pkg_resume_link($cust_pkg) %> ) +% } +% if ( $curuser->access_right('Cancel customer package immediately') ) { + ( <% pkg_cancel_link($cust_pkg) %> ) +% } + </FONT> + </TD> + </TR> +% } +% % } else { #status: active % % unless ( $cust_pkg->get('setup') ) { #not setup % % unless ( $part_pkg->freq ) { - <% pkg_status_row_colspan( $cust_pkg, emt('Not yet billed (one-time charge)'), '', 'colspan'=>$colspan, %opt ) %> + <% pkg_status_row_colspan( $cust_pkg, emt('Not yet billed (one-time charge)'), '', %opt ) %> - <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_noauto( $cust_pkg, %opt ) %> - <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_discount( $cust_pkg, %opt ) %> <% pkg_status_row_if( $cust_pkg, @@ -121,8 +115,9 @@ <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %> +% if (!$supplemental) { <TR> - <TD COLSPAN=<%$colspan%>> + <TD COLSPAN=<%$opt{colspan}%>> <FONT SIZE=-1> % if ( $curuser->access_right('Cancel customer package immediately') ) { ( <% pkg_cancel_link($cust_pkg) %> ) @@ -130,14 +125,15 @@ </FONT> </TD> </TR> +% } % } else { - <% pkg_status_row_colspan($cust_pkg, emt("Not yet billed ($billed_or_prepaid [_1])", myfreq($part_pkg) ), '', 'colspan'=>$colspan, %opt ) %> + <% pkg_status_row_colspan($cust_pkg, emt("Not yet billed ($billed_or_prepaid [_1])", myfreq($part_pkg) ), '', %opt ) %> - <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_noauto( $cust_pkg, %opt ) %> - <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_discount( $cust_pkg, %opt ) %> <% pkg_status_row_if($cust_pkg, emt('Start billing'), 'start_date', %opt) %> <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %> @@ -148,13 +144,13 @@ % % unless ( $part_pkg->freq ) { - <% pkg_status_row_colspan($cust_pkg, emt('One-time charge'), '', 'colspan'=>$colspan, %opt ) %> + <% pkg_status_row_colspan($cust_pkg, emt('One-time charge'), '', %opt ) %> <% pkg_status_row($cust_pkg, emt('Billed'), 'setup', %opt) %> - <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_noauto( $cust_pkg, %opt ) %> - <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_discount( $cust_pkg, %opt ) %> <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %> @@ -170,7 +166,7 @@ <% pkg_status_row_colspan( $cust_pkg, emt('Overlimit'), $billed_or_prepaid. ' '. myfreq($part_pkg), - 'color'=>'FFD000', 'colspan'=>$colspan, + 'color'=>'FFD000', %opt ) %> @@ -179,15 +175,15 @@ <% pkg_status_row_colspan( $cust_pkg, emt('Active'), $billed_or_prepaid. ' '. myfreq($part_pkg), - 'color'=>'00CC00', 'colspan'=>$colspan, + 'color'=>'00CC00', %opt ) %> % } - <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_noauto( $cust_pkg, %opt ) %> - <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_discount( $cust_pkg, %opt ) %> <% pkg_status_row($cust_pkg, emt('Setup'), 'setup', %opt) %> @@ -202,7 +198,7 @@ % $cust_pkg->set('autosuspend', $autosuspend) if $autosuspend; % } - <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_changed( $cust_pkg, %opt ) %> <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %> <% pkg_status_row_if( $cust_pkg, $next_bill_or_prepaid_until, 'bill', %opt, curuser=>$curuser ) %> <% pkg_status_row_if($cust_pkg, emt('Will automatically suspend by'), 'autosuspend', %opt) %> @@ -212,10 +208,10 @@ <% pkg_status_row_if( $cust_pkg, emt('Expires'), 'expire', %opt, curuser=>$curuser ) %> <% pkg_status_row_if( $cust_pkg, emt('Contract ends'), 'contract_end', %opt ) %> -% if ( $part_pkg->freq ) { +% if ( $part_pkg->freq and !$supplemental ) { <TR> - <TD COLSPAN=<%$colspan%>> + <TD COLSPAN=<%$opt{colspan}%>> <FONT SIZE=-1> % if ( $curuser->access_right('Suspend customer package') ) { ( <% pkg_suspend_link($cust_pkg) %> ) @@ -251,8 +247,10 @@ my $bgcolor = $opt{'bgcolor'}; my $cust_pkg = $opt{'cust_pkg'}; my $part_pkg = $opt{'part_pkg'}; my $curuser = $FS::CurrentUser::CurrentUser; -my $colspan = $opt{'cust_pkg-display_times'} ? 8 : 4; my $width = $opt{'cust_pkg-display_times'} ? '38%' : '56%'; +my $supplemental = $opt{'supplemental'}; + +$opt{colspan} = $opt{'cust_pkg-display_times'} ? 8 : 4; #false laziness w/edit/REAL_cust_pkg.cgi my( $billed_or_prepaid, $last_bill_or_renewed, $next_bill_or_prepaid_until ); @@ -285,9 +283,27 @@ sub pkg_link { sub pkg_status_row { my( $cust_pkg, $title, $field, %opt ) = @_; + if ( $field and $cust_pkg->main_pkgnum ) { + # for supplemental packages, we mostly only show these if they're + # different from the main package + my $main_pkg = $cust_pkg-> main_pkg; + if ( $main_pkg->get($field) ne $cust_pkg->get($field) + # with some exceptions + or $field eq 'bill' + or $field eq 'last_bill' + or $field eq 'setup' + or $field eq 'susp' + or $field eq 'cancel' + ) { + # handle it normally + } else { + return ''; + } + } + my $color = $opt{'color'}; - my $html = qq(<TR><TD WIDTH="<%$width%>" ALIGN="right">); + my $html = qq(<TR><TD WIDTH="$width" ALIGN="right">); $html .= qq(<FONT COLOR="#$color"><B>) if length($color); $html .= qq($title ); $html .= qq(</B></FONT>) if length($color); @@ -338,7 +354,6 @@ sub pkg_status_row_changed { '', 'size' => '-1', 'align' => 'right', - 'colspan' => $opt{'colspan'}, ); } @@ -356,9 +371,7 @@ sub pkg_status_row_noauto { return '' unless $cust_main->payby =~ /^(CARD|CHEK)$/; my $what = lc(FS::payby->shortname($cust_main->payby)); - pkg_status_row_colspan( $cust_pkg, emt("No automatic $what charge"), '', - 'colspan' => $opt{'colspan'}, - ); + pkg_status_row_colspan( $cust_pkg, emt("No automatic $what charge"), ''); } sub pkg_status_row_discount { @@ -382,15 +395,24 @@ sub pkg_status_row_discount { $cust_pkg_discount->pkgdiscountnum. '">'.emt('remove discount').'</A>)</FONT>'; - $html .= pkg_status_row_colspan( $cust_pkg, $label, '', - 'colspan' => $opt{'colspan'}, - ); + $html .= pkg_status_row_colspan( $cust_pkg, $label, '', %opt ); } $html; } +sub pkg_reason_row { + my ($cust_pkg, $cpr, %opt) = @_; + return '' if $cust_pkg->main_pkgnum; + + my $reasontext = ''; + $reasontext = $cpr->reasontext . ' by ' . $cpr->otaker if $cpr; + pkg_status_row_colspan( $cust_pkg, $reasontext, '', + 'align'=>'right', 'size'=>'-2', %opt + ); +} + sub pkg_status_row_colspan { my($cust_pkg, $title, $addl, %opt) = @_; diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html index 942b42f54..915be49e5 100644 --- a/httemplate/view/cust_main/payment_history.html +++ b/httemplate/view/cust_main/payment_history.html @@ -34,7 +34,7 @@ <A HREF="<% $p %>edit/cust_pay.cgi?payby=WEST;custnum=<% $custnum %>"><% mt('Enter Western Union payment') |h %></A> % } -<BR> +<% $s ? '<BR>' : '' %> % $s=0; % if ( ( $payby{'CARD'} || $payby{'DCRD'} ) @@ -58,11 +58,13 @@ <A HREF="<% $p %>edit/cust_pay.cgi?payby=MCRD;custnum=<% $custnum %>"><% mt('Post manual (offline/POS) credit card payment') |h %></A> % } -<BR> +<% $s ? '<BR>' : '' %> -%# credit link +%# credit links +% $s=0; % if ( $curuser->access_right('Post credit') ) { + <% $s++ ? ' | ' : '' %> <& /elements/popup_link-cust_main.html, 'label' => emt('Enter credit'), 'action' => "${p}edit/cust_credit.cgi", @@ -70,7 +72,9 @@ 'actionlabel' => emt('Enter credit'), 'width' => 616, #make room for reasons #540 default &> - | +% } +% if ( $curuser->access_right('Credit line items') ) { + <% $s++ ? ' | ' : '' %> <& /elements/popup_link-cust_main.html, 'label' => emt('Credit line items'), #'action' => "${p}search/cust_bill_pkg.cgi?nottax=1;type=select", @@ -80,8 +84,8 @@ 'width' => 968, #763, 'height' => 575, &> - <BR> % } +<% $s ? '<BR>' : '' %> %# refund links @@ -224,57 +228,20 @@ %#display payment history -%my $money_char = $conf->config('money_char') || '$'; -% -%sub balance_forward_row { -% my( $b, $date, $money_char ) = @_; -% ( my $balance_forward = $money_char. $b ) =~ s/^\$\-/- \$/; - - <TR ID="balance_forward_row"> - <TD CLASS="grid" BGCOLOR="#dddddd"> - <% time2str($date_format, $date) %> - </TD> - - <TD CLASS="grid" BGCOLOR="#dddddd"> - <I><% mt("Starting balance on [_1]", time2str($date_format, $date) ) |h %></I> - (<A HREF="javascript:void(0);" onClick="show_history();"><% mt('show prior history') |h %></A>) - </TD> - - <TD CLASS="grid" BGCOLOR="#dddddd"></TD> - <TD CLASS="grid" BGCOLOR="#dddddd"></TD> - <TD CLASS="grid" BGCOLOR="#dddddd"></TD> - <TD CLASS="grid" BGCOLOR="#dddddd"></TD> - <TD CLASS="grid" BGCOLOR="#dddddd" ALIGN="right"><I><% $balance_forward %></I></TD> - - </TR> -%} -% -%my $balance = 0; %my %target = (); % -%my $years = $conf->config('payment_history-years') || 2; -%my $older_than = time - $years * 31556926; #60*60*24*365.2422 %my $hidden = 0; %my $seen = 0; %my $old_history = 0; %my $lastdate = 0; % -%foreach my $item ( sort { $a->{'date'} <=> $b->{'date'} } @history ) { +%foreach my $item ( @history ) { % % $lastdate = $item->{'date'}; % -% my $display; -% if ( $item->{'date'} < $older_than ) { +% my $display = ''; +% if ( $item->{'hide'} ) { % $display = ' STYLE="display:none" '; -% $hidden = 1; -% } else { -% -% $display = ''; -% -% if ( $hidden && ! $seen++ ) { -% balance_forward_row($balance, $item->{'date'}, $money_char); -% } -% % } % % if ( $bgcolor eq $bgcolor1 ) { @@ -310,16 +277,8 @@ % % 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 %>"> @@ -355,11 +314,11 @@ <% $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> @@ -382,14 +341,37 @@ function show_history () { } </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; @@ -497,6 +479,17 @@ foreach my $cust_pay_pending ($cust_main->cust_pay_pending_attempt) { #'target' => $target, #XXX }; } +#declined batch payments +foreach my $cust_pay_batch ( + $cust_main->cust_pay_batch(hashref => {status => 'Declined'}) +) { + my $pay_batch = $cust_pay_batch->pay_batch; + push @history, { + 'date' => $pay_batch->upload, + 'desc' => include('payment_history/attempted_batch_payment.html', $cust_pay_batch, %opt), + 'void_payment' => $cust_pay_batch->amount, + }; +} #credits (some false laziness w/payments) foreach my $cust_credit ($cust_main->cust_credit) { @@ -518,6 +511,41 @@ foreach my $cust_refund ($cust_main->cust_refund) { } +# sort in forward order first, and calculate running balances +my $years = $conf->config('payment_history-years') || 2; +my $older_than = time - $years * 31556926; #60*60*24*365.2422 +my $balance = 0; + +@history = sort { $a->{date} <=> $b->{date} } @history; +my $i = 0; +my $balance_forward; +foreach my $item (@history) { + $balance += $item->{'charge'} if exists $item->{'charge'}; + $balance -= $item->{'payment'} if exists $item->{'payment'}; + $balance -= $item->{'credit'} if exists $item->{'credit'}; + $balance += $item->{'refund'} if exists $item->{'refund'}; + $balance = sprintf("%.2f", $balance); + $balance =~ s/^\-0\.00$/0.00/; + $item->{'balance'} = $balance; + + if ( $item->{'date'} < $older_than ) { + $item->{'hide'} = 1; + } elsif ( $history[$i-1]->{'hide'} ) { + # this is the end of the hidden section + $history[$i-1]->{'balance_forward'} = 1; + } + $i++; +} +if ( @history and $history[-1]->{'hide'} ) { + # then everything is hidden + $history[-1]->{'balance_forward'} = 1; +} + +# then sort in user-pref order +if ( $curuser->option('history_order') eq 'newest' ) { + @history = sort { $b->{date} <=> $a->{date} } @history; +} # else it's already oldest-first, and there are no other options yet + sub translate_payby { my ($payby,$payinfo) = (shift,shift); my %payby = ( diff --git a/httemplate/view/cust_main/payment_history/attempted_batch_payment.html b/httemplate/view/cust_main/payment_history/attempted_batch_payment.html new file mode 100644 index 000000000..95947f512 --- /dev/null +++ b/httemplate/view/cust_main/payment_history/attempted_batch_payment.html @@ -0,0 +1,13 @@ +<I><% mt('Payment attempt') |h %> <% $info |h %></I> +<%init> + +my( $cust_pay_batch, %opt ) = @_; + +my ($payby,$payinfo) = translate_payinfo($cust_pay_batch); +$payby = translate_payby($payby,$payinfo); +my $info = $payby ? "($payby$payinfo)" : ''; + +$info .= ': '. $cust_pay_batch->error_message + if length($cust_pay_batch->error_message); + +</%init> diff --git a/httemplate/view/elements/svc_Common.html b/httemplate/view/elements/svc_Common.html index f7c685c28..d735195fe 100644 --- a/httemplate/view/elements/svc_Common.html +++ b/httemplate/view/elements/svc_Common.html @@ -52,26 +52,39 @@ function areyousure(href) { <% mt('Service #') |h %><B><% $svcnum %></B> % my $url = $opt{'edit_url'} || $p. 'edit/'. $opt{'table'}. '.cgi?'; -| <& /view/elements/svc_edit_link.html, 'svc' => $svc_x, 'edit_url' => $url &> +<& /view/elements/svc_edit_link.html, 'svc' => $svc_x, 'edit_url' => $url &> <BR> <% ntable("#cccccc") %><TR><TD><% ntable("#cccccc",2) %> +% my @inventory_items = $svc_x->inventory_item; % foreach my $f ( @$fields ) { % -% my($field, $type, $value, $hack_strict_refs); +% my($field, $type, $value); % if ( ref($f) ) { % $field = $f->{'field'}; -% $hack_strict_refs = \&{ $f->{'value'} } if $f->{'value'}; -% $value = $f->{'value'} ? &$hack_strict_refs($svc_x) : $svc_x->$field; % $type = $f->{'type'} || 'text'; +% if ( $f->{'value_callback'} ) { +% my $hack_strict_refs = \&{ $f->{'value_callback'} }; +% $value = &$hack_strict_refs($svc_x); +% } else { +% $value = exists($f->{'value'}) ? $f->{'value'} : $svc_x->$field; +% } % } else { % $field = $f; -% $value = $svc_x->$field; % $type = 'text'; +% $value = $svc_x->$field; % } % % my $columndef = $part_svc->part_svc_column($field); +% if ( $columndef->columnflag =~ /^[MA]$/ && $columndef->columnvalue =~ /,/ ) +% { +% # inventory-select field with multiple classes +% # show the class name to disambiguate +% my ($item) = grep { $_->svc_field eq $field } @inventory_items; +% my $class = qsearchs('inventory_class', { classnum => $item->classnum }); +% $value .= ' <i>('. $class->classname . ')</i>' if $class; +% } % unless ($columndef->columnflag eq 'F' && !length($columndef->columnvalue)) { <TR> diff --git a/httemplate/view/elements/svc_devices.html b/httemplate/view/elements/svc_devices.html index d71c82f07..38c6d0919 100644 --- a/httemplate/view/elements/svc_devices.html +++ b/httemplate/view/elements/svc_devices.html @@ -12,91 +12,86 @@ ) </%doc> -<% $devices %> +%if ( @devices || $num_part_device || $table eq 'dsl_device' ) { +% my $svcnum = $svc_x->svcnum; + + Devices + (<A HREF="<%$p%>edit/<%$table%>.html?svcnum=<%$svcnum%>">Add device</A>) + <BR> + +% if ( @devices ) { + + <SCRIPT> + function areyousure(href) { + if (confirm("Are you sure you want to delete this device?") == true) + window.location.href = href; + } + </SCRIPT> + + <& /elements/table-grid.html &> + <TR> +% if ( $table eq 'phone_device' ) { + <TH CLASS="grid" BGCOLOR="#cccccc">Type</TH> +% } + <TH CLASS="grid" BGCOLOR="#cccccc">MAC Addr</TH> + <TH CLASS="grid" BGCOLOR="#cccccc"></TH> + <TH CLASS="grid" BGCOLOR="#cccccc"></TH> + </TR> + +% my $bgcolor1 = '#eeeeee'; +% my $bgcolor2 = '#ffffff'; +% my $bgcolor = ''; +% +% foreach my $device ( @devices ) { +% +% if ( $bgcolor eq $bgcolor1 ) { +% $bgcolor = $bgcolor2; +% } else { +% $bgcolor = $bgcolor1; +% } +% +% my $td = qq(<TD CLASS="grid" BGCOLOR="$bgcolor">); +% +% my $devicenum = $device->devicenum; +% my $export_links = ''; +% $export_links = join( '<BR>', @{ $device->export_links } ) +% if $device->can('export_links'); + + <TR> +% if ( $table eq 'phone_device' ) { #$devices->can('part_device') + <% $td %><% $device->part_device->devicename |h %></TD> +% } + <% $td %><% $device->mac_addr %></TD> + <% $td %><% $export_links %></TD> + <% $td %>( +% unless ( $opt{'no_edit'} ) { + <A HREF="<%$p%>edit/<%$table%>.html?<%$devicenum%>">edit</A> | +% } + <A HREF="javascript:areyousure('<%$p%>misc/delete-<%$table%>.html?<%$devicenum%>')">delete</A> + )</TD> + </TR> +% } + </TABLE> + <BR> + +% } + <BR> +%} <%init> - my %opt = @_; - my $table = $opt{'table'}; #part_device, dsl_device - my $svc_x = $opt{'svc_x'}; - - my $devices = ''; - - my $num_part_device = 0; - if ( $table eq 'phone_device' ) { - my $sth = dbh->prepare("SELECT COUNT(*) FROM part_device") - #WHERE disabled = '' OR disabled IS NULL;"); - or die dbh->errstr; - $sth->execute or die $sth->errstr; - $num_part_device = $sth->fetchrow_arrayref->[0]; +my %opt = @_; +my $table = $opt{'table'}; #part_device, dsl_device +my $svc_x = $opt{'svc_x'}; + +my $num_part_device = 0; +if ( $table eq 'phone_device' ) { + my $sth = dbh->prepare("SELECT COUNT(*) FROM part_device") + #WHERE disabled = '' OR disabled IS NULL;"); + or die dbh->errstr; + $sth->execute or die $sth->errstr; + $num_part_device = $sth->fetchrow_arrayref->[0]; } - my @devices = $svc_x->$table(); - - #should move the below to proper mason code above instead of making $devices - if ( @devices || $num_part_device || $table eq 'dsl_device' ) { - my $svcnum = $svc_x->svcnum; - $devices .= - qq[Devices (<A HREF="${p}edit/$table.html?svcnum=$svcnum">Add device</A>)<BR>]; - if ( @devices ) { - - $devices .= qq! - <SCRIPT> - function areyousure(href) { - if (confirm("Are you sure you want to delete this device?") == true) - window.location.href = href; - } - </SCRIPT> - !; - - - $devices .= - include('/elements/table-grid.html'). - '<TR>'; - - $devices .= - '<TH CLASS="grid" BGCOLOR="#cccccc">Type</TH>' - if $table eq 'phone_device'; - - $devices .= - '<TH CLASS="grid" BGCOLOR="#cccccc">MAC Addr</TH>'. - '<TH CLASS="grid" BGCOLOR="#cccccc"></TH>'. - '<TH CLASS="grid" BGCOLOR="#cccccc"></TH>'. - '</TR>'; - my $bgcolor1 = '#eeeeee'; - my $bgcolor2 = '#ffffff'; - my $bgcolor = ''; - - foreach my $device ( @devices ) { - - if ( $bgcolor eq $bgcolor1 ) { - $bgcolor = $bgcolor2; - } else { - $bgcolor = $bgcolor1; - } - my $td = qq(<TD CLASS="grid" BGCOLOR="$bgcolor">); - - my $devicenum = $device->devicenum; - my $export_links = join( '<BR>', @{ $device->export_links } ) - if $device->can('export_links'); - - $devices .= '<TR>'; - $devices .= $td. $device->part_device->devicename. '</TD>' - if $table eq 'phone_device'; #$devices->can('part_device'); - - $devices .= $td. $device->mac_addr. '</TD>'. - $td. $export_links. '</TD>'. - "$td( "; - - $devices .= qq(<A HREF="${p}edit/$table.html?$devicenum">edit</A> | ) - unless $opt{'no_edit'}; - - $devices .= qq(<A HREF="javascript:areyousure('${p}misc/delete-$table.html?$devicenum')">delete</A>). - ' )</TD>'. - '</TR>'; - } - $devices .= '</TABLE><BR>'; - } - $devices .= '<BR>'; - } +my @devices = $svc_x->$table(); </%init> diff --git a/httemplate/view/elements/svc_edit_link.html b/httemplate/view/elements/svc_edit_link.html index d65db0a8f..5438ed266 100644 --- a/httemplate/view/elements/svc_edit_link.html +++ b/httemplate/view/elements/svc_edit_link.html @@ -7,8 +7,12 @@ function areyousure_delete() { window.location.href = '<% $cancel_url %>'; } </SCRIPT> -<A HREF="<% $edit_url %>"><% mt("Edit this [_1]", $label) |h %></A> | -<A HREF="javascript:areyousure_delete()"><% mt('Unprovision this Service') |h %></A> +% if ( $curuser->access_right('Provision customer service') ) { +| <A HREF="<% $edit_url %>"><% mt("Edit this [_1]", $label) |h %></A> +% } +% if ( $curuser->access_right('Unprovision customer service') ) { +| <A HREF="javascript:areyousure_delete()"><% mt('Unprovision this Service') |h %></A> +% } % } <%init> my %opt = @_; @@ -20,4 +24,5 @@ my $cancel_url = $p . 'misc/unprovision.cgi?' . $svc_x->svcnum; my $cust_svc = $svc_x->cust_svc; # always exists my $cancel_date = $cust_svc->pkg_cancel_date; my ($label) = $cust_svc->label; +my $curuser = $FS::CurrentUser::CurrentUser; </%init> diff --git a/httemplate/view/elements/svc_export_status.html b/httemplate/view/elements/svc_export_status.html index d96bb277d..4ce869e27 100644 --- a/httemplate/view/elements/svc_export_status.html +++ b/httemplate/view/elements/svc_export_status.html @@ -7,7 +7,15 @@ % foreach my $key ( sort {$a cmp $b} keys %$hashref ) { <TR> <TD ALIGN="right"><% $key |h %></TD> - <TD BGCOLOR="#ffffff"><% $hashref->{$key} |h %></TD> + <TD BGCOLOR="#ffffff"> +% if ( ref($hashref->{$key}) eq 'ARRAY' ) { +% foreach (@{ $hashref->{$key} }) { + <% $_ |h %><BR> +% } +% } else { + <% $hashref->{$key} |h %> +% } + </TD> </TR> % } diff --git a/httemplate/view/quotation-pdf.cgi b/httemplate/view/quotation-pdf.cgi new file mode 100755 index 000000000..7f62ce173 --- /dev/null +++ b/httemplate/view/quotation-pdf.cgi @@ -0,0 +1,29 @@ +<% $content %>\ +<%init> + +#false laziness w/elements/cust_bill-typeset + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Generate quotation'); #View quotations ? + +my $quotationnum = $cgi->param('quotationnum'); + +my $conf = new FS::Conf; + +my $quotation = qsearchs({ + 'select' => 'quotation.*', + 'table' => 'quotation', + #'addl_from' => 'LEFT JOIN cust_main USING ( custnum )', + 'hashref' => { 'quotationnum' => $quotationnum }, + #'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, +}); +die "Quotation #$quotationnum not found!" unless $quotation; + +my $content = $quotation->print_pdf(); #\%opt); + +http_header('Content-Type' => 'application/pdf'); +http_header('Content-Disposition' => "filename=$quotationnum.pdf" ); +http_header('Content-Length' => length($content) ); +http_header('Cache-control' => 'max-age=60' ); + +</%init> diff --git a/httemplate/view/svc_acct.cgi b/httemplate/view/svc_acct.cgi index 199591356..858ccbe67 100755 --- a/httemplate/view/svc_acct.cgi +++ b/httemplate/view/svc_acct.cgi @@ -22,6 +22,7 @@ % } + <& svc_acct/radius_usage.html, 'svc_acct' => $svc_acct, 'part_svc' => $part_svc, @@ -29,6 +30,7 @@ %gopt, &> + <& svc_acct/change_svc_form.html, 'part_svc' => \@part_svc, 'svcnum' => $svcnum, @@ -37,13 +39,15 @@ &> <% mt('Service #') |h %><B><% $svcnum %></B> -| <& /view/elements/svc_edit_link.html, 'svc' => $svc_acct &> <& svc_acct/change_svc.html, 'part_svc' => \@part_svc, %gopt, &> +</FORM> + + <& svc_acct/basics.html, 'svc_acct' => $svc_acct, 'part_svc' => $part_svc, @@ -90,8 +94,12 @@ die "access denied" my $addl_from = ' LEFT JOIN cust_svc USING ( svcnum ) '. ' LEFT JOIN cust_pkg USING ( pkgnum ) '. ' LEFT JOIN cust_main USING ( custnum ) '; - -my($query) = $cgi->keywords; +my $query; +if ( $cgi->keywords ) { + ($query) = $cgi->keywords; +} else { + $query = $cgi->param('svcnum'); +} $query =~ /^(\d+)$/; my $svcnum = $1; my $svc_acct = qsearchs({ diff --git a/httemplate/view/svc_acct/basics.html b/httemplate/view/svc_acct/basics.html index 2d9953fcc..04e7bcff8 100644 --- a/httemplate/view/svc_acct/basics.html +++ b/httemplate/view/svc_acct/basics.html @@ -20,7 +20,7 @@ % if ( $password =~ /^\*\w+\* (.*)$/ ) { % $password = $1; % $show_pw .= '<I>('. mt('login disabled') .')</I> '; -% } +% } % if ( ! $password % && $svc_acct->_password_encryption ne 'plain' % && $svc_acct->_password @@ -28,13 +28,27 @@ % { % $show_pw .= '<I>('. uc($svc_acct->_password_encryption). ' '.mt('encrypted').')</I>'; % } elsif ( $conf->exists('showpasswords') ) { -% $show_pw .= '<PRE>'. encode_entities($password). '</PRE>'; +% $show_pw .= '<SPAN >'. encode_entities($password). '</PRE>'; % } else { +% $password = ''; % $show_pw .= '<I>('. mt('hidden') .')</I>'; -% } -% $password = ''; -<& /view/elements/tr.html, label=>mt('Password'), value=>$show_pw &> - +% } +<TR> + <TD ALIGN="right"><% mt('Password') %></TD> + <TD STYLE="background-color: #ffffff; white-space: nowrap"> + <% $show_pw %> +% my $curuser = $FS::CurrentUser::CurrentUser; +% if ( $curuser->access_right('Provision customer service') or +% ($curuser->access_right('Edit password') and +% ! $part_svc->restrict_edit_password) ) +% { + <& /elements/change_password.html, + 'svc_acct' => $svc_acct, + 'curr_value' => $password, + &> +% } + </TD> +</TR> % if ( $conf->exists('security_phrase') ) { <& /view/elements/tr.html, label=>mt('Security phrase'), value=>$svc_acct->sec_phrase &> diff --git a/httemplate/view/svc_broadband.cgi b/httemplate/view/svc_broadband.cgi index 75e673c4f..7d6520e57 100644 --- a/httemplate/view/svc_broadband.cgi +++ b/httemplate/view/svc_broadband.cgi @@ -26,23 +26,31 @@ $labels{'coordinates'} = 'Latitude/Longitude'; my @fields = ( 'description', - { field => 'routernum', value => \&router }, + { field => 'routernum', value_callback => \&router }, 'speed_down', 'speed_up', - { field => 'ip_addr', value => \&ip_addr }, - { field => 'sectornum', value => \§ornum }, - { field => 'mac_addr', value => \&mac_addr }, + { field => 'ip_addr', value_callback => \&ip_addr }, + { field => 'sectornum', value_callback => \§ornum }, + { field => 'mac_addr', value_callback => \&mac_addr }, #'latitude', #'longitude', - { field => 'coordinates', value => \&coordinates }, + { field => 'coordinates', value_callback => \&coordinates }, 'altitude', + + 'radio_serialnum', + 'radio_location', + 'poe_location', + 'rssi', + 'suid', + { field => 'shared_svcnum', value_callback=> \&shared_svcnum, }, #value_callback => + 'vlan_profile', 'authkey', 'plan_id', ); push @fields, - { field => 'usergroup', value => \&usergroup } + { field => 'usergroup', value_callback => \&usergroup } if $conf->exists('svc_broadband-radius'); sub router { @@ -112,9 +120,36 @@ sub coordinates { ); } +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 ) { diff --git a/httemplate/view/svc_cert.cgi b/httemplate/view/svc_cert.cgi index 0cd66b422..964b808ab 100644 --- a/httemplate/view/svc_cert.cgi +++ b/httemplate/view/svc_cert.cgi @@ -17,7 +17,7 @@ my %labels = map { $_ => ( ref($fields->{$_}) my @fields = ( { field=>'privatekey', - value=> sub { + value_callback=> sub { my $svc_cert = shift; if ( $svc_cert->privatekey && $svc_cert->check_privatekey ) { '<FONT COLOR="#33ff33">Verification OK</FONT>'; @@ -31,7 +31,7 @@ my @fields = ( qw( common_name organization organization_unit city state country cert_contact ), { 'field'=>'csr', - 'value'=> sub { + 'value_callback'=> sub { my $svc_cert = shift; if ( $svc_cert->csr ) { @@ -67,7 +67,7 @@ my @fields = ( }, }, { 'field'=>'certificate', - 'value'=> sub { + 'value_callback'=> sub { my $svc_cert = shift; if ( $svc_cert->certificate ) { @@ -137,7 +137,7 @@ my @fields = ( }, }, { 'field'=>'cacert', - 'value'=> sub { + 'value_callback'=> sub { my $svc_cert = shift; if ( $svc_cert->cacert ) { diff --git a/httemplate/view/svc_hardware.cgi b/httemplate/view/svc_hardware.cgi index 7f5e889d8..eef1c1140 100644 --- a/httemplate/view/svc_hardware.cgi +++ b/httemplate/view/svc_hardware.cgi @@ -13,17 +13,20 @@ my %labels = map { $_ => ( ref($fields->{$_}) : $fields->{$_} ); } keys %$fields; + +$labels{'display_hw_addr'} = 'Hardware address'; + my $model = { field => 'typenum', type => 'text', - value => sub { $_[0]->hardware_type->description } + value_callback => sub { $_[0]->hardware_type->description } }; my $status = { field => 'statusnum', type => 'text', - value => sub { $_[0]->status_label } + value_callback => sub { $_[0]->status_label } }; my $note = { field => 'note', type => 'text', - value => sub { encode_entities($_[0]->note) } + value_callback => sub { encode_entities($_[0]->note) } }; my @fields = ( diff --git a/httemplate/view/svc_phone.cgi b/httemplate/view/svc_phone.cgi index 323be63dc..ed95c4cea 100644 --- a/httemplate/view/svc_phone.cgi +++ b/httemplate/view/svc_phone.cgi @@ -16,9 +16,20 @@ my %labels = map { $_ => ( ref($fields->{$_}) ); } keys %$fields; -my @fields = qw( countrycode phonenum ); +my @fields = qw( countrycode phonenum sim_imsi ); push @fields, 'domain' if $conf->exists('svc_phone-domain'); -push @fields, qw( pbx_title sip_password pin phone_name forwarddst email ); +push @fields, qw( pbx_title ); + +if ( $conf->exists('showpasswords') ) { + push @fields, qw( sip_password ); +} else { + push @fields, { 'field' => 'sip_password', #'_HIDDEN_sip_password', + 'type' => 'fixed', + 'value' => '<I>('. mt('hidden') .')</I>', + }; +} + +push @fields, qw( pin phone_name forwarddst email ); if ( $conf->exists('svc_phone-lnp') ) { push @fields, 'lnp_status', diff --git a/rt/FREESIDE_MODIFIED b/rt/FREESIDE_MODIFIED index 315d6b200..ace0d499b 100644 --- a/rt/FREESIDE_MODIFIED +++ b/rt/FREESIDE_MODIFIED @@ -160,3 +160,8 @@ share/html/Ticket/Elements/ShowDates share/html/Elements/CustomerFields share/html/Search/Elements/ConditionRow # bugfix for select options list share/html/Search/Elements/PickBasics + +#avoid cloning TimeWorked and related fields +lib/RT/CustomField.pm +share/html/Admin/CustomFields/Modify.html +share/html/Ticket/Create.html diff --git a/rt/lib/RT/Action/SendEmail.pm b/rt/lib/RT/Action/SendEmail.pm index 2a7a2e3c0..1e6607eb4 100755 --- a/rt/lib/RT/Action/SendEmail.pm +++ b/rt/lib/RT/Action/SendEmail.pm @@ -871,21 +871,25 @@ sub SetFrom { my $self = shift; my %args = @_; + my $from = $args{From}; + if ( RT->Config->Get('UseFriendlyFromLine') ) { my $friendly_name = $self->GetFriendlyName(%args); - $self->SetHeader( - 'From', + $from = sprintf( RT->Config->Get('FriendlyFromLineFormat'), $self->MIMEEncodeString( $friendly_name, RT->Config->Get('EmailOutputEncoding') ), $args{From} - ), - ); - } else { - $self->SetHeader( 'From', $args{From} ); + ); } + + $self->SetHeader( 'From', $from ); + + #also set Sender:, otherwise MTAs add a nonsensical value like rt@machine, + #and then Outlook prepends "rt@machine on behalf of" to the From: header + $self->SetHeader( 'Sender', $from ); } =head2 GetFriendlyName diff --git a/rt/lib/RT/CustomField.pm b/rt/lib/RT/CustomField.pm index 7ba24b8be..8d16c1fe1 100644 --- a/rt/lib/RT/CustomField.pm +++ b/rt/lib/RT/CustomField.pm @@ -410,6 +410,10 @@ sub Create { $self->SetUILocation( $args{'UILocation'} ); } + if ( exists $args{'NoClone'} ) { + $self->SetNoClone( $args{'NoClone'} ); + } + return ($rv, $msg) unless exists $args{'Queue'}; # Compat code -- create a new ObjectCustomField mapping @@ -1822,9 +1826,20 @@ sub SetUILocation { } } +sub NoClone { + my $self = shift; + $self->FirstAttribute('NoClone') ? 1 : ''; +} - - +sub SetNoClone { + my $self = shift; + my $value = shift; + if ( $value ) { + return $self->SetAttribute( Name => 'NoClone', Content => 1 ); + } else { + return $self->DeleteAttribute('NoClone'); + } +} =head2 id diff --git a/rt/share/html/Admin/CustomFields/Modify.html b/rt/share/html/Admin/CustomFields/Modify.html index 4ed86b60b..358dcfd70 100644 --- a/rt/share/html/Admin/CustomFields/Modify.html +++ b/rt/share/html/Admin/CustomFields/Modify.html @@ -144,6 +144,11 @@ </td></tr> <tr><td class="label"> </td><td> +<input type="checkbox" class="checkbox" name="YesClone" value="1" <% $YesCloneChecked |n%> /> +<&|/l&>Copy this field to new tickets</&> +</td></tr> + +<tr><td class="label"> </td><td> <input type="hidden" class="hidden" name="SetEnabled" value="1" /> <input type="checkbox" class="checkbox" name="Enabled" value="1" <% $EnabledChecked |n%> /> <&|/l&>Enabled (Unchecking this box disables this custom field)</&> @@ -187,6 +192,7 @@ else { IncludeContentForValue => $IncludeContentForValue, BasedOn => $BasedOn, Disabled => !$Enabled, + NoClone => !$YesClone, ); if (!$val) { push @results, loc("Could not create CustomField: [_1]", $msg); @@ -207,10 +213,12 @@ else { if ( $ARGS{'Update'} && $id ne 'new' ) { #we're asking about enabled on the web page but really care about disabled. $ARGS{'Disabled'} = $Enabled? 0 : 1; + # likewise + $ARGS{'NoClone'} = $YesClone ? 0 : 1; $ARGS{'Required'} ||= 0; - my @attribs = qw(Disabled Required Pattern Name TypeComposite LookupType Description LinkValueTo IncludeContentForValue); + my @attribs = qw(Disabled Required Pattern Name TypeComposite LookupType Description LinkValueTo IncludeContentForValue NoClone); push @results, UpdateRecordObject( AttributesRef => \@attribs, Object => $CustomFieldObj, @@ -313,6 +321,10 @@ $EnabledChecked = '' if $CustomFieldObj->Disabled; my $RequiredChecked = ''; $RequiredChecked = qq[checked="checked"] if $CustomFieldObj->Required; +my $YesCloneChecked = qq[checked="checked"]; +$YesCloneChecked = '' if $CustomFieldObj->NoClone; + + my @CFvalidations = ( '(?#Mandatory).', '(?#Digits)^[\d.]+$', @@ -339,4 +351,5 @@ $LinkValueTo => undef $IncludeContentForValue => undef $BasedOn => undef $UILocation => undef +$YesClone => undef </%ARGS> diff --git a/rt/share/html/Ticket/Create.html b/rt/share/html/Ticket/Create.html index 0419126c6..8c6a58ad0 100755 --- a/rt/share/html/Ticket/Create.html +++ b/rt/share/html/Ticket/Create.html @@ -293,8 +293,8 @@ if ($CloneTicket) { }; $clone->{$_} = $CloneTicketObj->$_() - for qw/Owner Subject FinalPriority TimeEstimated TimeWorked - Status TimeLeft/; + for qw/Owner Subject FinalPriority Status/; + # not TimeWorked, TimeEstimated, or TimeLeft $clone->{$_} = $CloneTicketObj->$_->AsString for grep { $CloneTicketObj->$_->Unix } @@ -330,6 +330,7 @@ if ($CloneTicket) { my $cfs = $CloneTicketObj->QueueObj->TicketCustomFields(); while ( my $cf = $cfs->Next ) { + next if $cf->FirstAttribute('NoClone'); my $cf_id = $cf->id; my $cf_values = $CloneTicketObj->CustomFieldValues( $cf->id ); my @cf_values; |