diff options
216 files changed, 8168 insertions, 2563 deletions
@@ -3,7 +3,7 @@ package FS; use strict; use vars qw($VERSION); -$VERSION = '%%%VERSION%%%'; +$VERSION = '3.0git'; #find missing entries in this file with: # for a in `ls *pm | cut -d. -f1`; do grep 'L<FS::'$a'>' ../FS.pm >/dev/null || echo "missing $a" ; done @@ -95,6 +95,8 @@ L<FS::access_usergroup> - Employee group membership L<FS::access_groupagent> - Group reseller access +L<FS::access_groupsales> - Group sales access + L<FS::access_right> - Access rights L<FS::svc_acct_pop> - POP (Point of Presence, not Post @@ -212,6 +214,8 @@ L<FS::inventory_item> - Inventory items L<FS::part_svc> - Service definition class +L<FS::part_svc_class> - Service class class + L<FS::part_svc_column> - Column constraint class L<FS::export_svc> - Class linking service definitions (see L<FS::part_svc>) @@ -262,6 +266,8 @@ L<FS::rate_tier_details> - Rater tier details for call billing L<FS::usage_class> - Usage class class +L<FS::sales> - Sales person class + L<FS::agent> - Agent (reseller) class L<FS::agent_type> - Agent type class diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index d2417f069..eb9974adf 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -111,6 +111,8 @@ tie my %rights, 'Tie::IxHash', 'Edit customer tags', 'Edit referring customer', 'View customer history', + 'Suspend customer', + 'Unsuspend customer', 'Cancel customer', 'Complimentary customer', #aka users-allow_comp 'Merge customer', @@ -138,6 +140,7 @@ tie my %rights, 'Tie::IxHash', 'Unsuspend customer package', 'Cancel customer package immediately', 'Cancel customer package later', + 'Un-cancel customer package', 'Delay suspension events', 'Add on-the-fly cancel reason', #NEW 'Add on-the-fly suspend reason', #NEW @@ -188,6 +191,7 @@ tie my %rights, 'Tie::IxHash', 'Customer payment rights' => [ 'View payments', { rightname=>'Post payment', desc=>'Make check or cash payments.' }, + { rightname=>'Backdate payment', desc=>'Enable payments to be posted for days other than today.' }, 'Post check payment', 'Post cash payment', 'Post payment batch', @@ -254,6 +258,7 @@ tie my %rights, 'Tie::IxHash', 'Reporting/listing rights' => [ 'List customers', 'List all customers', + 'Advanced customer search', 'List zip codes', #NEW 'List invoices', 'List packages', @@ -269,6 +274,27 @@ tie my %rights, 'Tie::IxHash', { rightname=>'View email logs', global=>1 }, 'Download report data', + 'Services: Accounts', + 'Services: Accounts: Advanced search', + 'Services: Domains', + 'Services: Certificates', + 'Services: Mail forwards', + 'Services: Virtual hosting services', + 'Services: Wireless broadband services', + 'Services: Wireless broadband services: Advanced search', + 'Services: DSLs', + 'Services: Dish services', + 'Services: Hardware', + 'Services: Hardware: Advanced search', + 'Services: Phone numbers', + 'Services: PBXs', + 'Services: Ports', + 'Services: Mailing lists', + 'Services: External services', + 'Usage: RADIUS sessions', + 'Usage: Call Detail Records (CDRs)', + 'Usage: Unrateable CDRs', + 'Usage: Time worked', #{ rightname => 'List customers of all agents', global=>1 }, ], @@ -310,6 +336,8 @@ tie my %rights, 'Tie::IxHash', 'Edit billing events', { rightname=>'Edit global billing events', global=>1 }, + 'View templates', + { rightname=>'View global templates', global=>1 }, 'Edit templates', { rightname=>'Edit global templates', global=>1 }, diff --git a/FS/FS/ClientAPI/MasonComponent.pm b/FS/FS/ClientAPI/MasonComponent.pm index 37cf7ef20..534b48a76 100644 --- a/FS/FS/ClientAPI/MasonComponent.pm +++ b/FS/FS/ClientAPI/MasonComponent.pm @@ -36,7 +36,7 @@ my %session_callbacks = ( my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) or return "unknown custnum $custnum"; my %args = @$argsref; - $args{object} = $cust_main; + $args{object} = $cust_main->bill_location; @$argsref = ( %args ); return ''; #no error }, diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 7bc3011d2..54799b817 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -46,18 +46,17 @@ use FS::msg_template; $DEBUG = 0; $me = '[FS::ClientAPI::MyAccount]'; -use vars qw( @cust_main_editable_fields ); +use vars qw( @cust_main_editable_fields @location_editable_fields ); @cust_main_editable_fields = qw( - first last company address1 address2 city - county state zip country - daytime night fax mobile - ship_first ship_last ship_company ship_address1 ship_address2 ship_city - ship_state ship_zip ship_country - ship_daytime ship_night ship_fax ship_mobile + first last daytime night fax mobile locale payby payinfo payname paystart_month paystart_year payissue payip ss paytype paystate stateid stateid_state ); +@location_editable_fields = qw( + address1 address2 city county state zip country +); + BEGIN { #preload to reduce time customer_info takes if ( $FS::TicketSystem::system ) { @@ -115,7 +114,7 @@ sub skin_info { ( map { $_ => scalar( $conf->config($_, $agentnum) ) } qw( company_name date_format ) ), ( map { $_ => scalar( $conf->config("selfservice-$_", $agentnum ) ) } - qw( body_bgcolor box_bgcolor + qw( body_bgcolor box_bgcolor stripe1_bgcolor stripe2_bgcolor text_color link_color vlink_color hlink_color alink_color font title_color title_align title_size menu_bgcolor menu_fontsize ) @@ -196,6 +195,8 @@ sub login { } else { +warn Dumper($p); + my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } ) or return { error => 'Domain '. $p->{'domain'}. ' not found' }; @@ -381,10 +382,16 @@ sub customer_info { my $cust_main = qsearchs('cust_main', $search ) or return { 'error' => "unknown custnum $custnum" }; + $return{display_custnum} = $cust_main->display_custnum; + if ( $session->{'pkgnum'} ) { $return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} ); + #next_bill_date from cust_pkg? } else { $return{balance} = $cust_main->balance; + $return{next_bill_date} = $cust_main->next_bill_date; + $return{next_bill_date_pretty} = + time2str('%m/%d/%Y', $return{next_bill_date} ); } my @tickets = $cust_main->tickets; @@ -416,21 +423,45 @@ sub customer_info { }; } $cust_main->open_cust_bill; $return{open_invoices} = \@open; + + my $sql = 'SELECT MAX(_date) FROM cust_bill WHERE custnum = ?'; + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute($custnum) or die $sth->errstr; + $return{'last_invoice_date'} = $sth->fetchrow_arrayref->[0]; + $return{'last_invoice_date_pretty'} = + time2str('%m/%d/%Y', $return{'last_invoice_date'} ); } + $return{countrydefault} = scalar($conf->config('countrydefault')); + $return{small_custview} = small_custview( $cust_main, - scalar($conf->config('countrydefault')), + $return{countrydefault}, ( $session->{'pkgnum'} ? 1 : 0 ), #nobalance ); $return{name} = $cust_main->first. ' '. $cust_main->get('last'); - $return{ship_name} = $cust_main->ship_first. ' '. $cust_main->get('ship_last'); + + $return{has_ship_address} = $cust_main->has_ship_address; + $return{status} = $cust_main->status; + $return{statuscolor} = $cust_main->statuscolor; for (@cust_main_editable_fields) { $return{$_} = $cust_main->get($_); } + for (@location_editable_fields) { + $return{$_} = $cust_main->bill_location->get($_); + $return{'ship_'.$_} = $cust_main->ship_location->get($_); + } + $return{has_ship_address} = $cust_main->has_ship_address; + # compatibility: some places in selfservice use this to determine + # if there's a ship address + if ( $return{has_ship_address} ) { + $return{ship_last} = $cust_main->last; + $return{ship_first} = $cust_main->first; + } + if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) { $return{payinfo} = $cust_main->paymask; @return{'month', 'year'} = $cust_main->paydate_monthyear; @@ -444,7 +475,7 @@ sub customer_info { if (scalar($conf->config('support_packages'))) { my @support_services = (); foreach ($cust_main->support_services) { - my $seconds = $_->svc_x->seconds; + my $seconds = $_->svc_x->seconds || 0; my $time_remaining = (($seconds < 0) ? '-' : '' ). int(abs($seconds)/3600)."h". sprintf("%02d",(abs($seconds)%3600)/60)."m"; @@ -485,8 +516,8 @@ sub customer_info { } - return { 'error' => '', - 'custnum' => $custnum, + return { 'error' => '', + 'custnum' => $custnum, %return, }; @@ -509,14 +540,17 @@ sub customer_info_short { my $cust_main = qsearchs('cust_main', $search ) or return { 'error' => "unknown custnum $custnum" }; + $return{display_custnum} = $cust_main->display_custnum; + + $return{countrydefault} = scalar($conf->config('countrydefault')); + $return{small_custview} = small_custview( $cust_main, - scalar($conf->config('countrydefault')), + $return{countrydefault}, 1, ##nobalance ); $return{name} = $cust_main->first. ' '. $cust_main->get('last'); - $return{ship_name} = $cust_main->ship_first. ' '. $cust_main->get('ship_last'); $return{payby} = $cust_main->payby; @@ -524,7 +558,12 @@ sub customer_info_short { for (@cust_main_editable_fields) { $return{$_} = $cust_main->get($_); } - + #maybe a little more expensive, but it should be cached by now + for (@location_editable_fields) { + $return{$_} = $cust_main->bill_location->get($_); + $return{'ship_'.$_} = $cust_main->ship_location->get($_); + } + if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) { $return{payinfo} = $cust_main->paymask; @return{'month', 'year'} = $cust_main->paydate_monthyear; @@ -558,6 +597,103 @@ sub customer_info_short { }; } +sub billing_history { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + return { 'error' => 'No customer' } unless $custnum; + + my $search = { 'custnum' => $custnum }; + $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + my $cust_main = qsearchs('cust_main', $search ) + or return { 'error' => "unknown custnum $custnum" }; + + my %return = (); + + if ( $session->{'pkgnum'} ) { + #$return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} ); + #next_bill_date from cust_pkg? + return { 'error' => 'No history for package' }; + } + + $return{balance} = $cust_main->balance; + $return{next_bill_date} = $cust_main->next_bill_date; + $return{next_bill_date_pretty} = + time2str('%m/%d/%Y', $return{next_bill_date} ); + + my @history = (); + + my $conf = new FS::Conf; + + if ( $conf->exists('selfservice-billing_history-line_items') ) { + + foreach my $cust_bill ( $cust_main->cust_bill ) { + + push @history, { + 'type' => 'Line item', + 'description' => $_->desc. ( $_->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 ), + } + foreach $cust_bill->cust_bill_pkg; + + } + + } else { + + push @history, { + 'type' => 'Invoice', + 'description' => 'Invoice #'. $_->display_invnum, + 'amount' => sprintf('%.2f', $_->charged ), + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $cust_main->cust_bill; + + } + + push @history, { + 'type' => 'Payment', + 'description' => 'Payment', #XXX type + 'amount' => sprintf('%.2f', 0 - $_->paid ), + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $cust_main->cust_pay; + + push @history, { + 'type' => 'Credit', + 'description' => 'Credit', #more info? + 'amount' => sprintf('%.2f', 0 -$_->amount ), + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $cust_main->cust_credit; + + push @history, { + 'type' => 'Refund', + 'description' => 'Refund', #more info? type, like payment? + 'amount' => $_->refund, + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $cust_main->cust_refund; + + @history = sort { $b->{'date'} <=> $a->{'date'} } @history; + + $return{'history'} = \@history; + + return \%return; + +} + sub edit_info { my $p = shift; my $session = _cache->get($p->{'session_id'}) @@ -570,9 +706,32 @@ sub edit_info { or return { 'error' => "unknown custnum $custnum" }; my $new = new FS::cust_main { $cust_main->hash }; + $new->set( $_ => $p->{$_} ) foreach grep { exists $p->{$_} } @cust_main_editable_fields; + if ( exists($p->{address1}) ) { + my $bill_location = FS::cust_location->new({ + map { $_ => $p->{$_} } @location_editable_fields + }); + # if this is unchanged from before, cust_main::replace will ignore it + $new->set('bill_location' => $bill_location); + } + + if ( exists($p->{ship_address1}) ) { + my $ship_location = FS::cust_location->new({ + map { $_ => $p->{"ship_$_"} } @location_editable_fields + }); + if ( !grep { length($p->{"ship_$_"}) } @location_editable_fields ) { + # Selfservice unfortunately tries to indicate "same as billing + # address" by sending all fields empty. Did this ever work? + $ship_location = $cust_main->bill_location; + } + $new->set('ship_location' => $ship_location); + } + # but if it hasn't been passed in at all, leave ship_location alone-- + # DON'T change it to match bill_location. + my $payby = ''; if (exists($p->{'payby'})) { $p->{'payby'} =~ /^([A-Z]{4})$/ @@ -710,7 +869,8 @@ sub payment_info { $return{payname} = $cust_main->payname || ( $cust_main->first. ' '. $cust_main->get('last') ); - $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip); + $return{$_} = $cust_main->bill_location->get($_) + for qw(address1 address2 city state zip); $return{payby} = $cust_main->payby; $return{stateid_state} = $cust_main->stateid_state; @@ -729,7 +889,7 @@ sub payment_info { $return{payinfo2} = $payinfo2; $return{paytype} = $cust_main->paytype; $return{paystate} = $cust_main->paystate; - + $return{payname} = $cust_main->payname; # override 'first/last name' default from above, if any. Is instution-name here. (#15819) } if ( $conf->config('prepayment_discounts-credit_type') ) { @@ -852,6 +1012,8 @@ sub validate_payment { 'card_type' => $card_type, 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01', 'paydate_pretty' => $p->{'month'}. ' / '. $p->{'year'}, + 'month' => $p->{'month'}, + 'year' => $p->{'year'}, 'payname' => $payname, 'paybatch' => $paybatch, #this doesn't actually do anything 'paycvv' => $paycvv, @@ -876,7 +1038,9 @@ sub store_payment { _cache->set( 'payment_'.$p->{'session_id'}, $validate, $timeout ); +{ map { $_=>$validate->{$_} } - qw( card_type paymask payname paydate_pretty amount ) + qw( card_type paymask payname paydate_pretty month year amount + address1 address2 city state zip country + ) }; } @@ -927,9 +1091,16 @@ sub do_process_payment { my $new = new FS::cust_main { $cust_main->hash }; if ($payby eq 'CARD' || $payby eq 'DCRD') { $new->set( $_ => $validate->{$_} ) - foreach qw( payname paystart_month paystart_year payissue payip - address1 address2 city state zip country ); + foreach qw( payname paystart_month paystart_year payissue payip ); $new->set( 'payby' => $validate->{'auto'} ? 'CARD' : 'DCRD' ); + + my $bill_location = FS::cust_location->new({ + map { $_ => $validate->{$_} } + qw(address1 address2 city state country zip) + }); # county? + $new->set('bill_location' => $bill_location); + # but don't allow the service address to change this way. + } elsif ($payby eq 'CHEK' || $payby eq 'DCHK') { $new->set( $_ => $validate->{$_} ) foreach qw( payname payip paytype paystate @@ -1375,6 +1546,7 @@ sub list_pkgs { my $primary_cust_svc = $_->primary_cust_svc; +{ $_->hash, $_->part_pkg->hash, + pkg_label => $_->pkg_label, status => $_->status, part_svc => [ map $_->hashref, $_->available_part_svc ], @@ -1467,12 +1639,14 @@ sub list_svcs { my $part_pkg = $cust_pkg->part_pkg; my %hash = ( - 'svcnum' => $_->svcnum, - 'svcdb' => $svcdb, - 'label' => $label, - 'value' => $value, - 'pkg_status' => $cust_pkg->status, - 'readonly' => ( $part_svc->selfservice_access eq 'readonly' ), + 'svcnum' => $_->svcnum, + 'display_svcnum' => $_->display_svcnum, + 'svcdb' => $svcdb, + 'label' => $label, + 'value' => $value, + 'pkg_label' => $cust_pkg->pkg_label, + 'pkg_status' => $cust_pkg->status, + 'readonly' => ($part_svc->selfservice_access eq 'readonly'), ); if ( $svcdb eq 'svc_acct' ) { @@ -1770,6 +1944,8 @@ sub list_support_usage { sub _list_cdr_usage { # XXX CDR type support... + # XXX any way to do a paged search on this? + # 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, ); diff --git a/FS/FS/ClientAPI/SGNG.pm b/FS/FS/ClientAPI/SGNG.pm deleted file mode 100644 index 7f784dcd0..000000000 --- a/FS/FS/ClientAPI/SGNG.pm +++ /dev/null @@ -1,277 +0,0 @@ -#this stuff is SG-specific (i.e. multi-customer company username hack) - -package FS::ClientAPI::SGNG; - -use strict; -use vars qw( $cache $DEBUG ); -use Time::Local qw(timelocal timelocal_nocheck); -use Business::CreditCard; -use FS::Record qw( qsearch qsearchs ); -use FS::Conf; -use FS::cust_main; -use FS::cust_pkg; -use FS::ClientAPI::MyAccount; #qw( payment_info process_payment ) - -$DEBUG = 0; - -sub _cache { - $cache ||= new FS::ClientAPI_SessionCache( { - 'namespace' => 'FS::ClientAPI::MyAccount', #yes, share session_ids - } ); -} - -sub ping { - #my $p = shift; - - return { 'pong' => '1' }; - -} - -#this might almost be general-purpose -sub decompify_pkgs { - my $p = shift; - - my $session = _cache->get($p->{'session_id'}) - or return { 'error' => "Can't resume session" }; #better error message - - my $custnum = $session->{'custnum'}; - - my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) - or return { 'error' => "unknown custnum $custnum" }; - - return { 'error' => 'Not a complimentary customer' } - unless $cust_main->payby eq 'COMP'; - - my $paydate = - $cust_main->paydate =~ /^\S+$/ ? $cust_main->paydate : '2037-12-31'; - - my ($payyear,$paymonth,$payday) = split (/-/,$paydate); - - my $date = timelocal(0,0,0,$payday,--$paymonth,$payyear); - - foreach my $cust_pkg ( - qsearch({ 'table' => 'cust_pkg', - 'hashref' => { 'custnum' => $custnum, - 'bill' => '', - }, - 'extra_sql' => ' AND '. FS::cust_pkg->active_sql, - }) - ) { - $cust_pkg->set('bill', $date); - my $error = $cust_pkg->replace; - return { 'error' => $error } if $error; - } - - return { 'error' => '' }; - -} - -#find old payment info -# (should work just like MyAccount::payment_info, except returns previous info -# too) -# definitly sg-specific, no one else stores past customer records like this -sub previous_payment_info { - my $p = shift; - - my $session = _cache->get($p->{'session_id'}) - or return { 'error' => "Can't resume session" }; #better error message - - my $payment_info = FS::ClientAPI::MyAccount::payment_info($p); - - my $custnum = $session->{'custnum'}; - - my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) - or return { 'error' => "unknown custnum $custnum" }; - - #? - return $payment_info if $cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/; - - foreach my $prev_cust_main ( - reverse _previous_cust_main( 'custnum' => $custnum, - 'username' => $cust_main->company, - 'with_payments' => 1, - ) - ) { - - next unless $prev_cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/; - - if ( $prev_cust_main->payby =~ /^(CARD|DCRD)$/ ) { - - #card expired? - my ($payyear,$paymonth,$payday) = split (/-/, $cust_main->paydate); - - my $expdate = timelocal_nocheck(0,0,0,1,$paymonth,$payyear); - - next if $expdate < time; - - } elsif ( $prev_cust_main->payby =~ /^(CHEK|DCHK)$/ ) { - - #any check? or just skip these in favor of cards? - - } - - return { %$payment_info, - #$prev_cust_main->payment_info - _cust_main_payment_info( $prev_cust_main ), - 'previous_custnum' => $prev_cust_main->custnum, - }; - - } - - #still nothing? return an error? - return $payment_info; - -} - -#this is really FS::cust_main::payment_info, but here for now -sub _cust_main_payment_info { - my $self = shift; - - my %return = (); - - $return{balance} = $self->balance; - - $return{payname} = $self->payname - || ( $self->first. ' '. $self->get('last') ); - - $return{$_} = $self->get($_) for qw(address1 address2 city state zip); - - $return{payby} = $self->payby; - $return{stateid_state} = $self->stateid_state; - - if ( $self->payby =~ /^(CARD|DCRD)$/ ) { - $return{card_type} = cardtype($self->payinfo); - $return{payinfo} = $self->paymask; - - @return{'month', 'year'} = $self->paydate_monthyear; - - } - - if ( $self->payby =~ /^(CHEK|DCHK)$/ ) { - my ($payinfo1, $payinfo2) = split '@', $self->paymask; - $return{payinfo1} = $payinfo1; - $return{payinfo2} = $payinfo2; - $return{paytype} = $self->paytype; - $return{paystate} = $self->paystate; - - } - - #doubleclick protection - my $_date = time; - $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32; - - %return; - -} - -#find old cust_main records (with payments) -sub _previous_cust_main { - - #safety check! return nothing unless we're enabled explicitly - return () unless FS::Conf->new->exists('sg-multicustomer_hack'); - - my %opt = @_; - my $custnum = $opt{'custnum'}; - my $username = $opt{'username'}; - - my %search = (); - if ( $opt{'with_payments'} ) { - $search{'extra_sql'} = - ' AND 0 < ( SELECT COUNT(*) FROM cust_pay - WHERE cust_pay.custnum = cust_main.custnum - ) - '; - } - - qsearch( { - 'table' => 'cust_main', - 'hashref' => { 'company' => { op => 'ILIKE', value => $opt{'username'} }, - 'custnum' => { op => '!=', value => $opt{'custnum'} }, - }, - 'order_by' => 'ORDER BY custnum', - %search, - } ); - -} - -#since we could be passing masked old CC data, need to look that up and -#replace it (like regular process_payment does) w/info from old customer record -sub previous_process_payment { - my $p = shift; - - return FS::ClientAPI::MyAccount::process_payment($p) - unless $p->{'previous_custnum'} - && ( ( $p->{'payby'} =~ /^(CARD|DCRD)$/ && $p->{'payinfo'} =~ /x/i ) - || ( $p->{'payby'} =~ /^(CHEK|DCHK)$/ && $p->{'payinfo1'} =~ /x/i ) - ); - - my $session = _cache->get($p->{'session_id'}) - or return { 'error' => "Can't resume session" }; #better error message - - my $custnum = $session->{'custnum'}; - - my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) - or return { 'error' => "unknown custnum $custnum" }; - - #make sure this is really a previous custnum of this customer - my @previous_cust_main = - grep { $_->custnum == $p->{'previous_custnum'} } - _previous_cust_main( 'custnum' => $custnum, - 'username' => $cust_main->company, - 'with_payments' => 1, - ); - - my $previous_cust_main = $previous_cust_main[0]; - - #causes problems with old data w/old masking method - #if $previous_cust_main->paymask eq $payinfo; - - if ( $p->{'payby'} =~ /^(CHEK|DCHK)$/ && $p->{'payinfo1'} =~ /x/i ) { - ( $p->{'payinfo1'}, $p->{'payinfo2'} ) = - split('@', $previous_cust_main->payinfo); - } elsif ( $p->{'payby'} =~ /^(CARD|DCRD)$/ && $p->{'payinfo'} =~ /x/i ) { - $p->{'payinfo'} = $previous_cust_main->payinfo; - } - - FS::ClientAPI::MyAccount::process_payment($p); - -} - -sub previous_payment_info_renew_info { - my $p = shift; - my $renew_info = renew_info($p); - my $payment_info = previous_payment_info($p); - return { %$renew_info, - %$payment_info, - }; -} - -sub previous_process_payment_order_pkg { - my $p = shift; - - my $hr = previous_process_payment($p); - return $hr if $hr->{'error'}; - - order_pkg($p); -} - -sub previous_process_payment_change_pkg { - my $p = shift; - - my $hr = previous_process_payment($p); - return $hr if $hr->{'error'}; - - change_pkg($p); -} - -sub previous_process_payment_order_renew { - my $p = shift; - - my $hr = previous_process_payment($p); - return $hr if $hr->{'error'}; - - order_renew($p); -} - -1; - diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm index f17752a45..b7dcdbb64 100644 --- a/FS/FS/ClientAPI/Signup.pm +++ b/FS/FS/ClientAPI/Signup.pm @@ -405,8 +405,8 @@ sub signup_info { && $agent->agent_cust_main ) { my $cust_main = $agent->agent_cust_main; - my $prefix = length($cust_main->ship_last) ? 'ship_' : ''; - $signup_info_cache_agent->{"ship_$_"} = $cust_main->get("$prefix$_") + my $location = $cust_main->ship_location; + $signup_info_cache_agent->{"ship_$_"} = $location->get($_) foreach qw( address1 city county state zip country ); } @@ -509,6 +509,13 @@ sub new_customer { || $conf->config('signup_server-default_agentnum'); } + my ($bill_hash, $ship_hash); + foreach my $f (FS::cust_main->location_fields) { + # avoid having to change this in front-end code + $bill_hash->{$f} = $packet->{"bill_$f"} || $packet->{$f}; + $ship_hash->{$f} = $packet->{"ship_$f"}; + } + #shares some stuff with htdocs/edit/process/cust_main.cgi... take any # common that are still here and library them. my $template_custnum = $conf->config('signup_server-prepaid-template-custnum'); @@ -517,6 +524,7 @@ 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} @@ -556,41 +564,48 @@ sub new_customer { || $conf->config('signup_server-default_refnum'), map { $_ => $packet->{$_} } qw( - - last first ss company address1 address2 - city county state zip country + last first ss company daytime night fax stateid stateid_state - - ship_last ship_first ship_ss ship_company ship_address1 ship_address2 - ship_city ship_county ship_state ship_zip ship_country - ship_daytime ship_night ship_fax - payby payinfo paycvv paydate payname paystate paytype paystart_month paystart_year payissue payip override_ban_warn - referral_custnum comments - ) + ), } ); } + my $bill_location = FS::cust_location->new($bill_hash); + my $ship_location; my $agent = qsearchs('agent', { 'agentnum' => $agentnum } ); if ( $conf->exists('agent-ship_address', $agentnum) && $agent->agent_custnum ) { my $agent_cust_main = $agent->agent_cust_main; my $prefix = length($agent_cust_main->ship_last) ? 'ship_' : ''; - $cust_main->set("ship_$_", $agent_cust_main->get("$prefix$_") ) - foreach qw( address1 city county state zip country ); - - $cust_main->set("ship_$_", $cust_main->get($_)) - foreach qw( last first ); + $ship_location = FS::cust_location->new({ + $agent_cust_main->ship_location->location_hash + }); } + # we don't have an equivalent of the "same" checkbox in selfservice + # so is there a ship address, and if so, is it different from the billing + # address? + elsif ( length($ship_hash->{address1}) > 0 and + grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash) + ) { + + $ship_location = FS::cust_location->new( $ship_hash ); + + } + else { + $ship_location = $bill_location; + } + $cust_main->set('bill_location' => $bill_location); + $cust_main->set('ship_location' => $ship_location); return { 'error' => "Illegal payment type" } unless grep { $_ eq $packet->{'payby'} } diff --git a/FS/FS/ClientAPI_XMLRPC.pm b/FS/FS/ClientAPI_XMLRPC.pm index 98e1910c3..7dd20c652 100644 --- a/FS/FS/ClientAPI_XMLRPC.pm +++ b/FS/FS/ClientAPI_XMLRPC.pm @@ -104,6 +104,7 @@ sub ss2clientapi { 'switch_acct' => 'MyAccount/switch_acct', 'customer_info' => 'MyAccount/customer_info', 'customer_info_short' => 'MyAccount/customer_info_short', + 'billing_history' => 'MyAccount/billing_history', 'edit_info' => 'MyAccount/edit_info', #add to ss cgi! 'invoice' => 'MyAccount/invoice', 'invoice_pdf' => 'MyAccount/invoice_pdf', @@ -176,22 +177,6 @@ sub ss2clientapi { 'call_time' => 'PrepaidPhone/call_time', 'call_time_nanpa' => 'PrepaidPhone/call_time_nanpa', 'phonenum_balance' => 'PrepaidPhone/phonenum_balance', - #izoom - #'bulk_processrow' => 'Bulk/processrow', - #conflicts w/Agentone# 'check_username' => 'Bulk/check_username', - #sg - 'ping' => 'SGNG/ping', - 'decompify_pkgs' => 'SGNG/decompify_pkgs', - 'previous_payment_info' => 'SGNG/previous_payment_info', - 'previous_payment_info_renew_info' - => 'SGNG/previous_payment_info_renew_info', - 'previous_process_payment' => 'SGNG/previous_process_payment', - 'previous_process_payment_order_pkg' - => 'SGNG/previous_process_payment_order_pkg', - 'previous_process_payment_change_pkg' - => 'SGNG/previous_process_payment_change_pkg', - 'previous_process_payment_order_renew' - => 'SGNG/previous_process_payment_order_renew', }; } diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 81443632c..13625da25 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -13,6 +13,7 @@ use FS::payby; use FS::conf; use FS::Record qw(qsearch qsearchs); use FS::UID qw(dbh datasrc use_confcompat); +use FS::Misc::Invoicing qw( spool_formats ); use FS::Misc::Geo; $base_dir = '%%%FREESIDE_CONF%%%'; @@ -183,7 +184,7 @@ sub exists { my $self = shift; return $self->_usecompat('exists', @_) if use_confcompat; - my($name, $agentnum)=@_; + #my($name, $agentnum)=@_; carp "FS::Conf->exists(". join(', ', @_). ") called" if $DEBUG > 1; @@ -191,6 +192,54 @@ sub exists { defined($self->_config(@_)); } +#maybe this should just be the new exists instead of getting a method of its +#own, but i wanted to avoid possible fallout + +sub config_bool { + my $self = shift; + return $self->_usecompat('exists', @_) if use_confcompat; + + my($name,$agentnum,$agentonly) = @_; + + carp "FS::Conf->config_bool(". join(', ', @_). ") called" + if $DEBUG > 1; + + #defined($self->_config(@_)); + + #false laziness w/_config + my $hashref = { 'name' => $name }; + local $FS::Record::conf = undef; # XXX evil hack prevents recursion + my $cv; + my @a = ( + ($agentnum || ()), + ($agentonly && $agentnum ? () : '') + ); + my @l = ( + ($self->{locale} || ()), + ($self->{localeonly} && $self->{locale} ? () : '') + ); + # try with the agentnum first, then fall back to no agentnum if allowed + foreach my $a (@a) { + $hashref->{agentnum} = $a; + foreach my $l (@l) { + $hashref->{locale} = $l; + $cv = FS::Record::qsearchs('conf', $hashref); + if ( $cv ) { + if ( $cv->value eq '0' + && ($hashref->{agentnum} || $hashref->{locale} ) + ) + { + return 0; #an explicit false override, don't continue looking + } else { + return 1; + } + } + } + } + return 0; + +} + =item config_orbase KEY SUFFIX Returns the configuration value or values (depending on context) for @@ -269,8 +318,13 @@ sub touch { return $self->_usecompat('touch', @_) if use_confcompat; my($name, $agentnum) = @_; - unless ( $self->exists($name, $agentnum) ) { - $self->set($name, '', $agentnum); + #unless ( $self->exists($name, $agentnum) ) { + unless ( $self->config_bool($name, $agentnum) ) { + if ( $agentnum && $self->exists($name) && $self->config($name,$agentnum) eq '0' ) { + $self->delete($name, $agentnum); + } else { + $self->set($name, '', $agentnum); + } } } @@ -357,6 +411,31 @@ sub delete { } } +#maybe this should just be the new delete instead of getting a method of its +#own, but i wanted to avoid possible fallout + +sub delete_bool { + my $self = shift; + return $self->_usecompat('delete', @_) if use_confcompat; + + my($name, $agentnum) = @_; + + warn "[FS::Conf] DELETE $name\n" if $DEBUG; + + my $cv = FS::Record::qsearchs('conf', { name => $name, + agentnum => $agentnum, + locale => $self->{locale}, + }); + + if ( $cv ) { + my $error = $cv->delete; + die $error if $error; + } elsif ( $agentnum ) { + $self->set($name, '0', $agentnum); + } + +} + =item import_config_item CONFITEM DIR Imports the item specified by the CONFITEM (see L<FS::ConfItem>) into @@ -1420,6 +1499,7 @@ and customer address. Include units.', 'description' => 'Send payment receipts.', 'type' => 'checkbox', 'per_agent' => 1, + 'agent_bool' => 1, }, { @@ -1846,6 +1926,13 @@ and customer address. Include units.', }, { + 'key' => 'unmask_ss', + 'section' => 'UI', + 'description' => "Don't mask social security numbers in the web interface.", + 'type' => 'checkbox', + }, + + { 'key' => 'show_stateid', 'section' => 'UI', 'description' => "Turns on display/collection of driver's license/state issued id numbers in the web interface. Sometimes required by electronic check (ACH) processors.", @@ -2955,7 +3042,7 @@ and customer address. Include units.', 'section' => 'invoicing', 'description' => 'Enable FTP of raw invoice data - format.', 'type' => 'select', - 'select_enum' => [ '', 'default', 'oneline', 'billco', ], + 'options' => [ spool_formats() ], }, { @@ -2991,7 +3078,7 @@ and customer address. Include units.', 'section' => 'invoicing', 'description' => 'Enable spooling of raw invoice data - format.', 'type' => 'select', - 'select_enum' => [ '', 'default', 'oneline', 'billco', ], + 'options' => [ spool_formats() ], }, { @@ -3002,6 +3089,32 @@ and customer address. Include units.', }, { + 'key' => 'bridgestone-batch_counter', + 'section' => '', + 'description' => 'Batch counter for spool files. Increments every time a spool file is uploaded.', + 'type' => 'text', + 'per_agent' => 1, + }, + + { + 'key' => 'bridgestone-prefix', + 'section' => '', + 'description' => 'Agent identifier for uploading to BABT printing service.', + 'type' => 'text', + 'per_agent' => 1, + }, + + { + 'key' => 'bridgestone-confirm_template', + 'section' => '', + 'description' => 'Confirmation email template for uploading to BABT service. Text::Template format, with variables "$zipfile" (name of the zipped file), "$seq" (sequence number), "$prefix" (user ID string), and "$rows" (number of records in the file). Should include Subject: and To: headers, separated from the rest of the message by a blank line.', + # this could use a true message template, but it's hard to see how that + # would make the world a better place + 'type' => 'textarea', + 'per_agent' => 1, + }, + + { 'key' => 'svc_acct-usage_suspend', 'section' => 'billing', 'description' => 'Suspends the package an account belongs to when svc_acct.seconds or a bytecount is decremented to 0 or below (accounts with an empty seconds and up|down|totalbytes value are ignored). Typically used in conjunction with prepaid packages and freeside-sqlradius-radacctd.', @@ -3052,6 +3165,16 @@ and customer address. Include units.', }, { + 'key' => 'cust_location-label_prefix', + 'section' => 'UI', + 'description' => 'Optional "site ID" to show in the location label', + 'type' => 'select', + 'select_hash' => [ '' => '', + 'CoStAg' => 'CoStAgXXXXX (country, state, agent name, locationnum)', + ], + }, + + { 'key' => 'cust_pkg-display_times', 'section' => 'UI', 'description' => 'Display full timestamps (not just dates) for customer packages. Useful if you are doing real-time things like hourly prepaid.', @@ -3446,7 +3569,14 @@ and customer address. Include units.', { 'key' => 'cust_main-enable_birthdate', 'section' => 'UI', - 'descritpion' => 'Enable tracking of a birth date with each customer record', + 'description' => 'Enable tracking of a birth date with each customer record', + 'type' => 'checkbox', + }, + + { + 'key' => 'cust_main-enable_spouse_birthdate', + 'section' => 'UI', + 'description' => 'Enable tracking of a spouse birth date with each customer record', 'type' => 'checkbox', }, @@ -3952,6 +4082,13 @@ and customer address. Include units.', }, { + 'key' => 'unsuspend_email_admin', + 'section' => '', + 'description' => 'Destination admin email address to enable unsuspension notices', + 'type' => 'text', + }, + + { 'key' => 'email_report-subject', 'section' => '', 'description' => 'Subject for reports emailed by freeside-fetch. Defaults to "Freeside report".', @@ -4001,6 +4138,22 @@ and customer address. Include units.', }, { + 'key' => 'selfservice-stripe1_bgcolor', + 'section' => 'self-service', + 'description' => 'HTML color for self-service interface lists (primary stripe), for example, #FFFFFF', + 'type' => 'text', + 'per_agent' => 1, + }, + + { + 'key' => 'selfservice-stripe2_bgcolor', + 'section' => 'self-service', + 'description' => 'HTML color for self-service interface lists (alternate stripe), for example, #DDDDDD', + 'type' => 'text', + 'per_agent' => 1, + }, + + { 'key' => 'selfservice-text_color', 'section' => 'self-service', 'description' => 'HTML text color for the self-service interface, for example, #000000', @@ -4361,34 +4514,6 @@ and customer address. Include units.', }, { - 'key' => 'sg-multicustomer_hack', - 'section' => '', - 'description' => "Don't use this.", - 'type' => 'checkbox', - }, - - { - 'key' => 'sg-ping_username', - 'section' => '', - 'description' => "Don't use this.", - 'type' => 'text', - }, - - { - 'key' => 'sg-ping_password', - 'section' => '', - 'description' => "Don't use this.", - 'type' => 'text', - }, - - { - 'key' => 'sg-login_username', - 'section' => '', - 'description' => "Don't use this.", - 'type' => 'text', - }, - - { 'key' => 'mc-outbound_packages', 'section' => '', 'description' => "Don't use this.", @@ -4493,6 +4618,13 @@ and customer address. Include units.', }, { + 'key' => 'tax-cust_exempt-groups-require_individual_nums', + 'section' => '', + 'description' => 'When using tax-cust_exempt-groups, require an individual tax exemption number for each exemption from different taxes.', + 'type' => 'checkbox', + }, + + { 'key' => 'cust_main-default_view', 'section' => 'UI', 'description' => 'Default customer view, for users who have not selected a default view in their preferences.', @@ -4540,14 +4672,14 @@ and customer address. Include units.', { 'key' => 'cust_main-edit_signupdate', 'section' => 'UI', - 'descritpion' => 'Enable manual editing of the signup date.', + 'description' => 'Enable manual editing of the signup date.', 'type' => 'checkbox', }, { 'key' => 'svc_acct-disable_access_number', 'section' => 'UI', - 'descritpion' => 'Disable access number selection.', + 'description' => 'Disable access number selection.', 'type' => 'checkbox', }, @@ -4650,6 +4782,13 @@ and customer address. Include units.', }, { + 'key' => 'cust_main-custom_content', + 'section' => 'UI', + 'description' => 'As an alternative to cust_main-custom_link (leave it blank), the contant to display on this customer page, one item per line. Available iems are: small_custview, birthdate, spouse_birthdate, svc_acct, svc_phone and svc_external.', + 'type' => 'textarea', + }, + + { 'key' => 'cust_main-custom_title', 'section' => 'UI', 'description' => 'Title for the "Custom" tab in the View Customer page.', @@ -4857,6 +4996,13 @@ and customer address. Include units.', }, 'option_sub' => sub { FS::Locales->description(shift) }, }, + + { + 'key' => 'cust_main-require_locale', + 'section' => 'UI', + 'description' => 'Require an explicit locale to be chosen for new customers.', + 'type' => 'checkbox', + }, { 'key' => 'translate-auto-insert', @@ -4916,6 +5062,20 @@ and customer address. Include units.', 'type' => 'checkbox', }, + { + 'key' => 'selfservice-billing_history-line_items', + 'section' => 'self-service', + 'description' => 'Return line item billing detail for the self-service billing_history API call.', + 'type' => 'checkbox', + }, + + { + 'key' => 'logout-timeout', + 'section' => 'UI', + 'description' => 'If set, automatically log users out of the backoffice after this many minutes.', + 'type' => 'text', + }, + { 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/Conf_compat17.pm b/FS/FS/Conf_compat17.pm index 6685935d3..2e4bb055f 100644 --- a/FS/FS/Conf_compat17.pm +++ b/FS/FS/Conf_compat17.pm @@ -2458,6 +2458,13 @@ httemplate/docs/config.html }, { + 'key' => 'unsuspend_email_admin', + 'section' => '', + 'description' => 'Destination admin email address to enable unsuspension notices', + 'type' => 'text', + }, + + { 'key' => 'email_report-subject', 'section' => '', 'description' => 'Subject for reports emailed by freeside-fetch. Defaults to "Freeside report".', diff --git a/FS/FS/Cron/bill.pm b/FS/FS/Cron/bill.pm index 8d1223b80..a9df376dc 100644 --- a/FS/FS/Cron/bill.pm +++ b/FS/FS/Cron/bill.pm @@ -200,15 +200,15 @@ sub bill_where { # select * from cust_main where my $where_pkg = <<"END"; EXISTS( - SELECT 1 FROM cust_pkg + SELECT 1 FROM cust_pkg LEFT JOIN part_pkg USING ( pkgpart ) WHERE cust_main.custnum = cust_pkg.custnum AND ( cancel IS NULL OR cancel = 0 ) - AND ( ( ( setup IS NULL OR setup = 0 ) + AND ( ( ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) AND ( start_date IS NULL OR start_date = 0 OR ( start_date IS NOT NULL AND start_date <= $^T ) ) ) - OR bill IS NULL OR bill <= $billtime + OR ( freq != '0' AND ( bill IS NULL OR bill <= $billtime ) ) OR ( expire IS NOT NULL AND expire <= $^T ) OR ( adjourn IS NOT NULL AND adjourn <= $^T ) OR ( resume IS NOT NULL AND resume <= $^T ) diff --git a/FS/FS/Cron/check.pm b/FS/FS/Cron/check.pm index 9d3ffbdbd..75247fbaf 100644 --- a/FS/FS/Cron/check.pm +++ b/FS/FS/Cron/check.pm @@ -16,7 +16,6 @@ use FS::cust_pay_pending; @ISA = qw( Exporter ); @EXPORT_OK = qw( check_queued check_selfservice check_apache check_bop_failures - check_sg check_sg_login check_sgng alert error_msg ); @@ -48,79 +47,6 @@ sub check_selfservice { return 1; } -sub check_sg { - my $conf = new FS::Conf; - #different trigger if they ever stop using multicustomer_hack ? - return 1 unless $conf->exists('sg-multicustomer_hack'); - - my $ua = new LWP::UserAgent; - $ua->agent("FreesideCronCheck/0.1 " . $ua->agent); - - my $USER = $conf->config('sg-ping_username'); - my $PASS = $conf->config('sg-ping_password'); - my $req = new HTTP::Request GET=>"https://$USER:$PASS\@localhost/sg/ping.cgi"; - my $res = $ua->request($req); - - return 1 if $res->is_success - && $res->content =~ /OK/ - && $res->content !~ /error/i; #doh, the error message includes "OK" - - $error_msg = $res->is_success ? $res->content : $res->status_line; - return 0; -} - -sub check_sg_login { - my $conf = new FS::Conf; - #different trigger if they ever stop using multicustomer_hack ? - return 1 unless $conf->exists('sg-multicustomer_hack'); - - my $ua = new LWP::UserAgent; - $ua->agent("FreesideCronCheck/0.1 " . $ua->agent); - - my $USER = $conf->config('sg-ping_username'); - my $PASS = $conf->config('sg-ping_password'); - my $USERNAME = $conf->config('sg-login_username'); - my $req = new HTTP::Request - GET=>"https://$USER:$PASS\@localhost/sg/start.cgi?". - 'username='. uri_escape($USERNAME); - my $res = $ua->request($req); - - return 1 if $res->is_success - && $res->content =~ /[\da-f]{32}/i #session_id - && $res->content !~ /error/i; - - $error_msg = $res->is_success ? $res->content : $res->status_line; - return 0; -} - -sub check_sgng { - my $conf = new FS::Conf; - #different trigger if they ever stop using multicustomer_hack ? - return 1 unless $conf->exists('sg-multicustomer_hack'); - - eval 'use RPC::XML; use RPC::XML::Client;'; - if ($@) { $error_msg = $@; return 0; }; - - my $cli = RPC::XML::Client->new('https://localhost/selfservice/xmlrpc.cgi'); - my $resp = $cli->send_request('FS.SelfService.XMLRPC.ping'); - - return 1 if ref($resp) - && ! $resp->is_fault - && ref($resp->value) - && $resp->value->{'pong'} == 1; - - #hua - $error_msg = ref($resp) - ? ( $resp->is_fault - ? $resp->string - : ( ref($resp->value) ? $resp->value->{'error'} - : $resp->value - ) - ) - : $resp; - return 0; -} - sub _check_fsproc { my $arg = shift; _check_pidfile( "freeside-$arg.pid" ); diff --git a/FS/FS/Cron/upload.pm b/FS/FS/Cron/upload.pm index dceead6b3..51e0d6868 100644 --- a/FS/FS/Cron/upload.pm +++ b/FS/FS/Cron/upload.pm @@ -9,6 +9,8 @@ use FS::Record qw( qsearch qsearchs ); use FS::Conf; use FS::queue; use FS::agent; +use FS::Misc qw( send_email ); #for bridgestone +use FS::ftp_target; use LWP::UserAgent; use HTTP::Request; use HTTP::Request::Common; @@ -39,42 +41,78 @@ sub upload { warn "$me upload called\n" if $DEBUG; - my $conf = new FS::Conf; - my @agent = grep { $conf->config( 'billco-username', $_->agentnum, 1 ) } - grep { $conf->config( 'billco-password', $_->agentnum, 1 ) } - qsearch( 'agent', {} ); + my @tasks; my $date = time2str('%Y%m%d%H%M%S', $^T); # more? - @agent = grep { $_ == $opt{'a'} } @agent if $opt{'a'}; + my $conf = new FS::Conf; + + my @agents = $opt{'a'} ? FS::agent->by_key($opt{'a'}) : qsearch('agent', {}); + + my %task = ( + 'date' => $date, + 'l' => $opt{'l'}, + 'm' => $opt{'m'}, + 'v' => $opt{'v'}, + ); + + my @agentnums = ('', map {$_->agentnum} @agents); + + foreach my $target (qsearch('ftp_target', {})) { + # We don't know here if it's spooled on a per-agent basis or not. + # (It could even be both, via different events.) So queue up an + # upload for each agent, plus one with null agentnum, and we'll + # upload as many files as we find. + foreach my $a (@agentnums) { + push @tasks, { + %task, + 'agentnum' => $a, + 'targetnum' => $target->targetnum, + 'handling' => $target->handling, + }; + } + } - foreach my $agent ( @agent ) { + # deprecated billco method + foreach (@agents) { + my $agentnum = $_->agentnum; + + if ( $conf->config( 'billco-username', $agentnum, 1 ) ) { + my $username = $conf->config('billco-username', $agentnum, 1); + my $password = $conf->config('billco-password', $agentnum, 1); + my $clicode = $conf->config('billco-clicode', $agentnum, 1); + my $url = $conf->config('billco-url', $agentnum); + push @tasks, { + %task, + 'agentnum' => $agentnum, + 'username' => $username, + 'password' => $password, + 'url' => $url, + 'clicode' => $clicode, + 'handling' => 'billco', + }; + } + } # foreach @agents - my $agentnum = $agent->agentnum; + foreach (@tasks) { + + my $agentnum = $_->{agentnum}; if ( $opt{'m'} ) { if ( $opt{'r'} ) { warn "DRY RUN: would add agent $agentnum for queued upload\n"; } else { - my $queue = new FS::queue { - 'job' => 'FS::Cron::upload::billco_upload', + 'job' => 'FS::Cron::upload::spool_upload', }; - my $error = $queue->insert( - 'agentnum' => $agentnum, - 'date' => $date, - 'l' => $opt{'l'} || '', - 'm' => $opt{'m'} || '', - 'v' => $opt{'v'} || '', - ); - + my $error = $queue->insert( %$_ ); } } else { - eval "&billco_upload( 'agentnum' => $agentnum, 'date' => $date );"; - warn "billco_upload failed: $@\n" + eval { spool_upload(%$_) }; + warn "spool_upload failed: $@\n" if $@; } @@ -83,26 +121,14 @@ sub upload { } -sub billco_upload { +sub spool_upload { my %opt = @_; - warn "$me billco_upload called\n" if $DEBUG; + warn "$me spool_upload called\n" if $DEBUG; my $conf = new FS::Conf; my $dir = '%%%FREESIDE_EXPORT%%%/export.'. $FS::UID::datasrc. '/cust_bill'; - my $agentnum = $opt{agentnum} or die "no agentnum provided\n"; - my $url = $conf->config( 'billco-url', $agentnum ) - or die "no url for agent $agentnum\n"; - $url =~ s/^\s+//; $url =~ s/\s+$//; - my $username = $conf->config( 'billco-username', $agentnum, 1 ) - or die "no username for agent $agentnum\n"; - my $password = $conf->config( 'billco-password', $agentnum, 1 ) - or die "no password for agent $agentnum\n"; - my $clicode = $conf->config( 'billco-clicode', $agentnum, 1 ); - #or die "no clicode for agent $agentnum\n"; - - die "no date provided\n" unless $opt{date}; - my $zipfile = "$dir/agentnum$agentnum-$opt{date}.zip"; + my $date = $opt{date} or die "no date provided\n"; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -115,86 +141,228 @@ sub billco_upload { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $agent = qsearchs( 'agent', { agentnum => $agentnum } ) - or die "no such agent: $agentnum"; - $agent->select_for_update; #mutex - - unless ( -f "$dir/agentnum$agentnum-header.csv" || - -f "$dir/agentnum$agentnum-detail.csv" ) - { - warn "$me neither $dir/agentnum$agentnum-header.csv nor ". - "$dir/agentnum$agentnum-detail.csv found\n" if $DEBUG; - $dbh->commit or die $dbh->errstr if $oldAutoCommit; - return; + my $agentnum = $opt{agentnum}; + my $agent; + if ( $agentnum ) { + $agent = qsearchs( 'agent', { agentnum => $agentnum } ) + or die "no such agent: $agentnum"; + $agent->select_for_update; #mutex } - # a better way? - if ($opt{m}) { - my $sql = "SELECT count(*) FROM queue LEFT JOIN cust_main USING(custnum) ". - "WHERE queue.job='FS::cust_main::queued_bill' AND cust_main.agentnum = ?"; - my $sth = $dbh->prepare($sql) or die $dbh->errstr; - while (1) { - $sth->execute( $agentnum ) - or die "Unexpected error executing statement $sql: ". $sth->errstr; - last if $sth->fetchow_arrayref->[0]; - sleep 300; + if ( $opt{'handling'} eq 'billco' ) { + + my $file = "agentnum$agentnum"; + my $zipfile = "$dir/$file-$date.zip"; + + unless ( -f "$dir/$file-header.csv" || + -f "$dir/$file-detail.csv" ) + { + warn "$me neither $dir/$file-header.csv nor ". + "$dir/$file-detail.csv found\n" if $DEBUG > 1; + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + return; } - } - foreach ( qw ( header detail ) ) { - rename "$dir/agentnum$agentnum-$_.csv", - "$dir/agentnum$agentnum-$opt{date}-$_.csv"; - } + my $url = $opt{url} or die "no url for agent $agentnum\n"; + $url =~ s/^\s+//; $url =~ s/\s+$//; + + my $username = $opt{username} or die "no username for agent $agentnum\n"; + my $password = $opt{password} or die "no password for agent $agentnum\n"; + + # a better way? + if ($opt{m}) { + my $sql = "SELECT count(*) FROM queue LEFT JOIN cust_main USING(custnum) ". + "WHERE queue.job='FS::cust_main::queued_bill' AND cust_main.agentnum = ?"; + my $sth = $dbh->prepare($sql) or die $dbh->errstr; + while (1) { + $sth->execute( $agentnum ) + or die "Unexpected error executing statement $sql: ". $sth->errstr; + last if $sth->fetchrow_arrayref->[0]; + sleep 300; + } + } - my $command = "cd $dir; zip $zipfile ". - "agentnum$agentnum-$opt{date}-header.csv ". - "agentnum$agentnum-$opt{date}-detail.csv"; + foreach ( qw ( header detail ) ) { + rename "$dir/$file-$_.csv", + "$dir/$file-$date-$_.csv"; + } - system($command) and die "$command failed\n"; + my $command = "cd $dir; zip $zipfile ". + "$file-$date-header.csv ". + "$file-$date-detail.csv"; - unlink "agentnum$agentnum-$opt{date}-header.csv", - "agentnum$agentnum-$opt{date}-detail.csv"; + system($command) and die "$command failed\n"; - if ( $url =~ /^http/i ) { + unlink "$file-$date-header.csv", + "$file-$date-detail.csv"; - my $ua = new LWP::UserAgent; - my $res = $ua->request( POST( $url, - 'Content_Type' => 'form-data', - 'Content' => [ 'username' => $username, - 'pass' => $password, - 'custid' => $username, - 'clicode' => $clicode, - 'file1' => [ $zipfile ], - ], - ) - ); + if ( $url =~ /^http/i ) { - die "upload failed: ". $res->status_line. "\n" - unless $res->is_success; + my $ua = new LWP::UserAgent; + my $res = $ua->request( POST( $url, + 'Content_Type' => 'form-data', + 'Content' => [ 'username' => $username, + 'pass' => $password, + 'custid' => $username, + 'clicode' => $opt{clicode}, + 'file1' => [ $zipfile ], + ], + ) + ); - } elsif ( $url =~ /^ftp:\/\/([\w\.]+)(\/.*)$/i ) { + die "upload failed: ". $res->status_line. "\n" + unless $res->is_success; - my($hostname, $path) = ($1, $2); + } elsif ( $url =~ /^ftp:\/\/([\w\.]+)(\/.*)$/i ) { - my $ftp = new Net::FTP($hostname) #, Passive=>1 ) - or die "can't connect to $hostname: $@\n"; - $ftp->login($username, $password) - or die "can't login to $hostname: ". $ftp->message."\n"; - unless ( $ftp->cwd($path) ) { - my $msg = "can't cd $path on $hostname: ". $ftp->message. "\n"; - ( $path eq '/' ) ? warn $msg : die $msg; - } - $ftp->binary - or die "can't set binary mode on $hostname\n"; + my($hostname, $path) = ($1, $2); + + my $ftp = new Net::FTP($hostname, Passive=>1) + or die "can't connect to $hostname: $@\n"; + $ftp->login($username, $password) + or die "can't login to $hostname: ". $ftp->message."\n"; + unless ( $ftp->cwd($path) ) { + my $msg = "can't cd $path on $hostname: ". $ftp->message. "\n"; + ( $path eq '/' ) ? warn $msg : die $msg; + } + $ftp->binary + or die "can't set binary mode on $hostname\n"; - $ftp->put($zipfile) - or die "can't put $zipfile: ". $ftp->message. "\n"; + $ftp->put($zipfile) + or die "can't put $zipfile: ". $ftp->message. "\n"; - $ftp->quit; + $ftp->quit; + + } else { + die "unknown scheme in URL $url\n"; + } - } else { - die "unknown scheme in URL $url\n"; } + else { #not billco + + my $targetnum = $opt{targetnum}; + my $ftp_target = FS::ftp_target->by_key($targetnum) + or die "FTP target $targetnum not found\n"; + + $dir .= "/target$targetnum"; + chdir($dir); + + my $file = $agentnum ? "agentnum$agentnum" : 'spool'; #.csv + + unless ( -f "$dir/$file.csv" ) { + warn "$me $dir/$file.csv not found\n" if $DEBUG > 1; + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + return; + } + + rename "$dir/$file.csv", "$dir/$file-$date.csv"; + + if ( $opt{'handling'} eq 'bridgestone' ) { + + my $prefix = $conf->config('bridgestone-prefix', $agentnum); + unless ( $prefix ) { + warn "$me agent $agentnum has no bridgestone-prefix, skipped\n"; + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + return; + } + + my $seq = $conf->config('bridgestone-batch_counter', $agentnum) || 1; + + # extract zip code + join(' ',$conf->config('company_address', $agentnum)) =~ + /(\d{5}(\-\d{4})?)\s*$/; + my $ourzip = $1 || ''; #could be an explicit option if really needed + $ourzip =~ s/\D//; + my $newfile = sprintf('%s_%s_%0.6d.dat', + $prefix, + time2str('%Y%m%d', time), + $seq); + warn "copying spool to $newfile\n" if $DEBUG; + + my ($in, $out); + open $in, '<', "$dir/$file-$date.csv" + or die "unable to read $file-$date.csv\n"; + open $out, '>', "$dir/$newfile" or die "unable to write $newfile\n"; + #header--not sure how much of this generalizes at all + my $head = sprintf( + "%-6s%-4s%-27s%-6s%0.6d%-5s%-9s%-9s%-7s%0.8d%-7s%0.6d\n", + ' COMP:', 'VISP', '', ',SEQ#:', $seq, ',ZIP:', $ourzip, ',VERS:1.1', + ',RUNDT:', time2str('%m%d%Y', $^T), + ',RUNTM:', time2str('%H%M%S', $^T), + ); + warn "HEADER: $head" if $DEBUG; + print $out $head; + + my $rows = 0; + while( <$in> ) { + print $out $_; + $rows++; + } + + #trailer + my $trail = sprintf( + "%-6s%-4s%-27s%-6s%0.6d%-7s%0.9d%-9s%0.9d\n", + ' COMP:', 'VISP', '', ',SEQ:', $seq, + ',LINES:', $rows+2, ',LETTERS:', $rows, + ); + warn "TRAILER: $trail" if $DEBUG; + print $out $trail; + + close $in; + close $out; + + my $zipfile = sprintf('%s_%0.6d.zip', $prefix, $seq); + my $command = "cd $dir; zip $zipfile $newfile"; + warn "compressing to $zipfile\n$command\n" if $DEBUG; + system($command) and die "$command failed\n"; + + my $connection = $ftp_target->connect; # dies on error + $connection->put($zipfile); + + my $template = join("\n",$conf->config('bridgestone-confirm_template')); + if ( $template ) { + my $tmpl_obj = Text::Template->new( + TYPE => 'STRING', SOURCE => $template + ); + my $content = $tmpl_obj->fill_in( HASH => + { + zipfile => $zipfile, + prefix => $prefix, + seq => $seq, + rows => $rows, + } + ); + my ($head, $body) = split("\n\n", $content, 2); + $head =~ /^subject:\s*(.*)$/im; + my $subject = $1; + + $head =~ /^to:\s*(.*)$/im; + my $to = $1; + + send_email( + to => $to, + from => $conf->config('invoice_from', $agentnum), + subject => $subject, + body => $body, + ); + } else { #!$template + warn "$me agent $agentnum has no bridgestone-confirm_template, no email sent\n"; + } + + $seq++; + warn "setting batch counter to $seq\n" if $DEBUG; + $conf->set('bridgestone-batch_counter', $seq, $agentnum); + + } else { # not bridgestone + + # this is the usual case + + my $connection = $ftp_target->connect; # dies on error + $connection->put("$file-$date.csv"); + + } + + } #opt{handling} $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index f6ad714d3..16c9afd96 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -122,6 +122,7 @@ if ( -e $addl_handler_use_file ) { use FS::UID qw( getotaker dbh datasrc driver_name ); use FS::Record qw( qsearch qsearchs fields dbdef str2time_sql str2time_sql_closing + midnight_sql ); use FS::Conf; use FS::CGI qw(header menubar table itable ntable idiot @@ -303,7 +304,11 @@ if ( -e $addl_handler_use_file ) { use FS::discount_plan; use FS::tower; use FS::tower_sector; + use FS::sales; + use FS::access_groupsales; use FS::contact_class; + use FS::part_svc_class; + use FS::ftp_target; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Mason/Request.pm b/FS/FS/Mason/Request.pm index d8fd77a66..0d21df4ca 100644 --- a/FS/FS/Mason/Request.pm +++ b/FS/FS/Mason/Request.pm @@ -33,9 +33,28 @@ sub new { #override alter_superclass ala RT::Interface::Web::Request ?? # for Mason 1.39 vs. Perl 5.10.0 +my $protect_fds; + sub freeside_setup { my( $class, $filename, $mode ) = @_; + #from rt/bin/webmux.pl(.in) + if ( !$protect_fds && $ENV{'MOD_PERL'} && exists $ENV{'MOD_PERL_API_VERSION'} + && $ENV{'MOD_PERL_API_VERSION'} >= 2 + ) { + # under mod_perl2, STDIN and STDOUT get closed and re-opened, + # however they are not on FD 0 and 1. In this case, the next + # socket that gets opened will occupy one of these FDs, and make + # all system() and open "|-" calls dangerous; for example, the + # DBI handle can get this FD, which later system() calls will + # close by putting garbage into the socket. + $protect_fds = []; + push @{$protect_fds}, IO::Handle->new_from_fd(0, "r") + if fileno(STDIN) != 0; + push @{$protect_fds}, IO::Handle->new_from_fd(1, "w") + if fileno(STDOUT) != 1; + } + if ( $filename =~ qr(/REST/\d+\.\d+/NoAuth/) ) { package HTML::Mason::Commands; #? diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm index 297e39fbc..2be9ec203 100644 --- a/FS/FS/Misc.pm +++ b/FS/FS/Misc.pm @@ -913,6 +913,16 @@ sub ocr_image { @lines; } +=item spool_formats + +Returns a list of the invoice spool formats. + +=cut + +sub spool_formats { + qw(default oneline billco bridgestone) +} + =back =head1 BUGS diff --git a/FS/FS/Misc/Invoicing.pm b/FS/FS/Misc/Invoicing.pm new file mode 100644 index 000000000..2fc52a99b --- /dev/null +++ b/FS/FS/Misc/Invoicing.pm @@ -0,0 +1,26 @@ +package FS::Misc::Invoicing; +use base qw( Exporter ); + +use vars qw( @EXPORT_OK ); +@EXPORT_OK = qw( spool_formats ); + +=head1 NAME + +FS::Misc::Invoicing - Invoice subroutines + +=head1 SYNOPSIS + +use FS::Misc::Invoicing qw( spool_formats ); + +=item spool_formats + +Returns a list of the invoice spool formats. + +=cut + +sub spool_formats { + qw(default oneline billco bridgestone) +} + +1; + diff --git a/FS/FS/PagedSearch.pm b/FS/FS/PagedSearch.pm new file mode 100644 index 000000000..09d05c4e6 --- /dev/null +++ b/FS/FS/PagedSearch.pm @@ -0,0 +1,189 @@ +package FS::PagedSearch; + +use strict; +use vars qw($DEBUG $default_limit @EXPORT_OK); +use base qw( Exporter ); +use FS::Record qw(qsearch dbdef); +use Data::Dumper; + +$DEBUG = 0; +$default_limit = 100; + +@EXPORT_OK = 'psearch'; + +=head1 NAME + +FS::PagedSearch - Iterator for querying large data sets + +=head1 SYNOPSIS + +use FS::PagedSearch qw(psearch); + +my $search = psearch('table', { field => 'value' ... }); +$search->limit(100); #optional +while ( my $row = $search->fetch ) { +... +} + +=head1 SUBROUTINES + +=over 4 + +=item psearch ARGUMENTS + +A wrapper around L<FS::Record::qsearch>. Accepts all the same arguments +as qsearch, except for the arrayref union query mode, and returns an +FS::PagedSearch object to access the rows of the query one at a time. +If the query doesn't contain an ORDER BY clause already, it will be ordered +by the table's primary key. + +=cut + +sub psearch { + # deep-copy qsearch args + my $q; + if ( ref($_[0]) eq 'ARRAY' ) { + die "union query not supported with psearch"; #yet + } + elsif ( ref($_[0]) eq 'HASH' ) { + %$q = %{ $_[0] }; + } + else { + $q = { + 'table' => shift, + 'hashref' => shift, + 'select' => shift, + 'extra_sql' => shift, + 'cache_obj' => shift, + 'addl_from' => shift, + }; + } + warn Dumper($q) if $DEBUG > 1; + + # clean up query + my $dbdef = dbdef->table($q->{table}); + # qsearch just appends order_by to extra_sql, so do that ourselves + $q->{extra_sql} ||= ''; + $q->{extra_sql} .= ' '.$q->{order_by} if $q->{order_by}; + $q->{order_by} = ''; + # and impose an ordering if needed + if ( not $q->{extra_sql} =~ /order by/i ) { + $q->{extra_sql} .= ' ORDER BY '.$dbdef->primary_key; + } + # and then we'll use order_by for LIMIT/OFFSET + + my $self = { + query => $q, + buffer => [], + offset => 0, + limit => $default_limit, + increment => 1, + }; + bless $self, 'FS::PagedSearch'; + + $self; +} + +=back + +=head1 METHODS + +=over 4 + +=item fetch + +Fetch the next row from the search results and remove it from the buffer. +Returns undef if there are no more rows. + +=cut + +sub fetch { + my $self = shift; + my $b = $self->{buffer}; + $self->refill if @$b == 0; + $self->{offset} += $self->{increment} if @$b; + return shift @$b; +} + +=item adjust ROWS + +Add ROWS to the offset counter. This won't cause rows to be skipped in the +current buffer but will affect the starting point of the next refill. + +=cut + +sub adjust { + my $self = shift; + my $r = shift; + $self->{offset} += $r; +} + +=item limit [ VALUE ] + +Set/get the number of rows to retrieve per page. The default is 100. + +=cut + +sub limit { + my $self = shift; + my $new_limit = shift; + if ( defined($new_limit) ) { + $self->{limit} = $new_limit; + } + $self->{limit}; +} + +=item increment [ VALUE ] + +Set/get the number of rows to increment the offset for each row that's +retrieved. Defaults to 1. If the rows are being modified in a way that +removes them from the result set of the query, it's probably wise to set +this to zero. Setting it to anything else is probably nonsense. + +=cut + +sub increment { + my $self = shift; + my $new_inc = shift; + if ( defined($new_inc) ) { + $self->{increment} = $new_inc; + } + $self->{increment}; +} + + +=item refill + +Run the query, skipping a number of rows set by the row offset, and replace +the contents of the buffer with the result. If there are no more rows, +this will just empty the buffer. Called automatically as needed; don't call +this from outside. + +=cut + +sub refill { + my $self = shift; + my $b = $self->{buffer}; + warn "refilling (limit ".$self->{limit}.", offset ".$self->{offset}.")\n" + if $DEBUG; + warn "discarding ".scalar(@$b)." rows\n" if $DEBUG and @$b; + if ( $self->{limit} > 0 ) { + $self->{query}->{order_by} = 'LIMIT ' . $self->{limit} . + ' OFFSET ' . $self->{offset}; + } + @$b = qsearch( $self->{query} ); + my $rows = scalar @$b; + warn "$rows returned\n" if $DEBUG; + + $rows; +} + +=back + +=head1 SEE ALSO + +L<FS::Record> + +=cut + +1; diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index dfc2abfc4..0ac269f4c 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -39,6 +39,7 @@ use Tie::IxHash; @EXPORT_OK = qw( dbh fields hfields qsearch qsearchs dbdef jsearch str2time_sql str2time_sql_closing regexp_sql not_regexp_sql concat_sql + midnight_sql ); $DEBUG = 0; @@ -2562,6 +2563,22 @@ sub ut_enumn { : ''; } +=item ut_flag COLUMN + +Check/untaint a column if it contains either an empty string or 'Y'. This +is the standard form for boolean flags in Freeside. + +=cut + +sub ut_flag { + my( $self, $field ) = @_; + my $value = uc($self->getfield($field)); + if ( $value eq '' or $value eq 'Y' ) { + $self->setfield($field, $value); + return ''; + } + return "Illegal (flag) field $field: $value"; +} =item ut_foreign_key COLUMN FOREIGN_TABLE FOREIGN_COLUMN @@ -3030,7 +3047,7 @@ sub not_regexp_sql { =item concat_sql [ DRIVER_NAME ] ITEMS_ARRAYREF -Returns the items concatendated based on database type, using "CONCAT()" for +Returns the items concatenated based on database type, using "CONCAT()" for mysql and " || " for Pg and other databases. You can pass an optional driver name such as "Pg", "mysql" or @@ -3051,6 +3068,24 @@ sub concat_sql { } +=item midnight_sql DATE + +Returns an SQL expression to convert DATE (a unix timestamp) to midnight +on that day in the system timezone, using the default driver name. + +=cut + +sub midnight_sql { + my $driver = driver_name; + my $expr = shift; + if ( $driver =~ /^mysql/i ) { + "UNIX_TIMESTAMP(DATE(FROM_UNIXTIME($expr)))"; + } + else { + "EXTRACT( EPOCH FROM DATE(TO_TIMESTAMP($expr)) )"; + } +} + =back =head1 BUGS diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 3894f65f8..a90c73a95 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -473,6 +473,18 @@ sub tables_hashref { 'index' => [ ['typenum'], ['disabled'], ['agent_custnum'] ], }, + 'sales' => { + 'columns' => [ + 'salesnum', 'serial', '', '', '', '', + 'salesperson', 'varchar', '', $char_d, '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'disabled', 'char', 'NULL', 1, '', '', + ], + 'primary_key' => 'salesnum', + 'unique' => [], + 'index' => [ ['salesnum'], ['disabled'] ], + }, + 'agent_type' => { 'columns' => [ 'typenum', 'serial', '', '', '', '', @@ -845,16 +857,17 @@ sub tables_hashref { 'stateid', 'varchar', 'NULL', $char_d, '', '', 'stateid_state', 'varchar', 'NULL', $char_d, '', '', 'birthdate' ,@date_type, '', '', + 'spouse_birthdate' ,@date_type, '', '', 'signupdate',@date_type, '', '', 'dundate', @date_type, '', '', 'company', 'varchar', 'NULL', $char_d, '', '', - 'address1', 'varchar', '', $char_d, '', '', + 'address1', 'varchar', 'NULL', $char_d, '', '', 'address2', 'varchar', 'NULL', $char_d, '', '', - 'city', 'varchar', '', $char_d, '', '', + 'city', 'varchar', 'NULL', $char_d, '', '', 'county', 'varchar', 'NULL', $char_d, '', '', 'state', 'varchar', 'NULL', $char_d, '', '', 'zip', 'varchar', 'NULL', 10, '', '', - 'country', 'char', '', 2, '', '', + 'country', 'char', 'NULL', 2, '', '', 'latitude', 'decimal', 'NULL', '10,7', '', '', 'longitude','decimal', 'NULL', '10,7', '', '', 'coord_auto', 'char', 'NULL', 1, '', '', @@ -883,7 +896,7 @@ sub tables_hashref { 'payby', 'char', '', 4, '', '', 'payinfo', 'varchar', 'NULL', 512, '', '', 'paycvv', 'varchar', 'NULL', 512, '', '', - 'paymask', 'varchar', 'NULL', $char_d, '', '', + 'paymask', 'varchar', 'NULL', $char_d, '', '', #'paydate', @date_type, '', '', 'paydate', 'varchar', 'NULL', 10, '', '', 'paystart_month', 'int', 'NULL', '', '', '', @@ -915,6 +928,9 @@ sub tables_hashref { 'edit_subject', 'char', 'NULL', 1, '', '', 'locale', 'varchar', 'NULL', 16, '', '', 'calling_list_exempt', 'char', 'NULL', 1, '', '', + 'invoice_noemail', 'char', 'NULL', 1, '', '', + 'bill_locationnum', 'int', 'NULL', '', '', '', + 'ship_locationnum', 'int', 'NULL', '', '', '', ], 'primary_key' => 'custnum', 'unique' => [ [ 'agentnum', 'agent_custid' ] ], @@ -925,16 +941,6 @@ sub tables_hashref { [ 'referral_custnum' ], [ 'payby' ], [ 'paydate' ], [ 'archived' ], - #billing - [ 'last' ], [ 'company' ], - [ 'county' ], [ 'state' ], [ 'country' ], - [ 'zip' ], - [ 'daytime' ], [ 'night' ], [ 'fax' ], [ 'mobile' ], - #shipping - [ 'ship_last' ], [ 'ship_company' ], - [ 'ship_county' ], [ 'ship_state' ], [ 'ship_country' ], - [ 'ship_zip' ], - [ 'ship_daytime' ], [ 'ship_night' ], [ 'ship_fax' ], [ 'ship_mobile' ] ], }, @@ -1067,6 +1073,8 @@ sub tables_hashref { 'country', 'char', '', 2, '', '', 'geocode', 'varchar', 'NULL', 20, '', '', 'district', 'varchar', 'NULL', 20, '', '', + 'censustract', 'varchar', 'NULL', 20, '', '', + 'censusyear', 'char', 'NULL', 4, '', '', 'location_type', 'varchar', 'NULL', 20, '', '', 'location_number', 'varchar', 'NULL', 20, '', '', 'location_kind', 'char', 'NULL', 1, '', '', @@ -1076,6 +1084,7 @@ sub tables_hashref { 'unique' => [], 'index' => [ [ 'prospectnum' ], [ 'custnum' ], [ 'county' ], [ 'state' ], [ 'country' ], [ 'zip' ], + [ 'city' ], [ 'district' ] ], }, @@ -1167,9 +1176,10 @@ sub tables_hashref { 'cust_main_exemption' => { 'columns' => [ - 'exemptionnum', 'serial', '', '', '', '', - 'custnum', 'int', '', '', '', '', - 'taxname', 'varchar', '', $char_d, '', '', + 'exemptionnum', 'serial', '', '', '', '', + 'custnum', 'int', '', '', '', '', + 'taxname', 'varchar', '', $char_d, '', '', + 'exempt_number', 'varchar', 'NULL', $char_d, '', '', #start/end dates? for reporting? ], 'primary_key' => 'exemptionnum', @@ -1500,6 +1510,8 @@ sub tables_hashref { 'adjourn', @date_type, '', '', 'resume', @date_type, '', '', 'cancel', @date_type, '', '', + 'uncancel', @date_type, '', '', + 'uncancel_pkgnum', 'int', 'NULL', '', '', '', 'expire', @date_type, '', '', 'contract_end', @date_type, '', '', 'dundate', @date_type, '', '', @@ -1649,14 +1661,15 @@ sub tables_hashref { 'cust_svc' => { 'columns' => [ - 'svcnum', 'serial', '', '', '', '', - 'pkgnum', 'int', 'NULL', '', '', '', - 'svcpart', 'int', '', '', '', '', - 'overlimit', @date_type, '', '', + 'svcnum', 'serial', '', '', '', '', + 'pkgnum', 'int', 'NULL', '', '', '', + 'svcpart', 'int', '', '', '', '', + 'agent_svcid', 'int', 'NULL', '', '', '', + 'overlimit', @date_type, '', '', ], 'primary_key' => 'svcnum', 'unique' => [], - 'index' => [ ['svcnum'], ['pkgnum'], ['svcpart'] ], + 'index' => [ ['svcnum'], ['pkgnum'], ['svcpart'], [ 'agent_svcid' ] ], }, 'cust_svc_option' => { @@ -1833,6 +1846,7 @@ sub tables_hashref { 'disabled', 'char', 'NULL', 1, '', '', 'preserve', 'char', 'NULL', 1, '', '', 'selfservice_access', 'varchar', 'NULL', $char_d, '', '', + 'classnum', 'int', 'NULL', '', '', '', ], 'primary_key' => 'svcpart', 'unique' => [], @@ -1841,18 +1855,29 @@ sub tables_hashref { 'part_svc_column' => { 'columns' => [ - 'columnnum', 'serial', '', '', '', '', - 'svcpart', 'int', '', '', '', '', - 'columnname', 'varchar', '', 64, '', '', + 'columnnum', 'serial', '', '', '', '', + 'svcpart', 'int', '', '', '', '', + 'columnname', 'varchar', '', 64, '', '', 'columnlabel', 'varchar', 'NULL', $char_d, '', '', - 'columnvalue', 'varchar', 'NULL', $char_d, '', '', - 'columnflag', 'char', 'NULL', 1, '', '', + 'columnvalue', 'varchar', 'NULL', 512, '', '', + 'columnflag', 'char', 'NULL', 1, '', '', ], 'primary_key' => 'columnnum', 'unique' => [ [ 'svcpart', 'columnname' ] ], 'index' => [ [ 'svcpart' ] ], }, + 'part_svc_class' => { + 'columns' => [ + 'classnum', 'serial', '', '', '', '', + 'classname', 'varchar', '', $char_d, '', '', + 'disabled', 'char', 'NULL', 1, '', '', + ], + 'primary_key' => 'classnum', + 'unique' => [], + 'index' => [ ['disabled'] ], + }, + #(this should be renamed to part_pop) 'svc_acct_pop' => { 'columns' => [ @@ -2556,7 +2581,7 @@ sub tables_hashref { 'plan_id', 'varchar', 'NULL', $char_d, '', '', ], 'primary_key' => 'svcnum', - 'unique' => [ [ 'mac_addr' ] ], + 'unique' => [ [ 'ip_addr' ], [ 'mac_addr' ] ], 'index' => [], }, @@ -2994,7 +3019,6 @@ sub tables_hashref { ### 'upstream_currency', 'char', 'NULL', 3, '', '', - 'upstream_price', 'decimal', 'NULL', '10,4', '', '', 'upstream_rateplanid', 'int', 'NULL', '', '', '', #? # how it was rated internally... @@ -3019,6 +3043,10 @@ sub tables_hashref { 'charged_party', 'varchar', 'NULL', $char_d, '', '', + 'upstream_price', 'decimal', 'NULL', '10,4', '', '', + 'upstream_src_regionname', 'varchar', 'NULL', $char_d, '', '', + 'upstream_dst_regionname', 'varchar', 'NULL', $char_d, '', '', + # how it was rated internally... 'rated_pretty_dst', 'varchar', 'NULL', $char_d, '', '', 'rated_regionname', 'varchar', 'NULL', $char_d, '', '', @@ -3154,11 +3182,12 @@ sub tables_hashref { 'inventory_item' => { 'columns' => [ - 'itemnum', 'serial', '', '', '', '', - 'classnum', 'int', '', '', '', '', - 'agentnum', 'int', 'NULL', '', '', '', - 'item', 'varchar', '', $char_d, '', '', - 'svcnum', 'int', 'NULL', '', '', '', + 'itemnum', 'serial', '', '', '', '', + 'classnum', 'int', '', '', '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'item', 'varchar', '', $char_d, '', '', + 'svcnum', 'int', 'NULL', '', '', '', + 'svc_field', 'varchar', 'NULL', $char_d, '', '', ], 'primary_key' => 'itemnum', 'unique' => [ [ 'classnum', 'item' ] ], @@ -3235,6 +3264,17 @@ sub tables_hashref { 'index' => [ [ 'groupnum' ] ], }, + 'access_groupsales' => { + 'columns' => [ + 'groupsalesnum', 'serial', '', '', '', '', + 'groupnum', 'int', '', '', '', '', + 'salesnum', 'int', '', '', '', '', + ], + 'primary_key' => 'groupsalesnum', + 'unique' => [ [ 'groupnum', 'salesnum' ] ], + 'index' => [ [ 'groupnum' ] ], + }, + 'access_right' => { 'columns' => [ 'rightnum', 'serial', '', '', '', '', @@ -3640,6 +3680,23 @@ sub tables_hashref { 'index' => [ [ 'upgrade' ] ], }, + 'ftp_target' => { + 'columns' => [ + 'targetnum', 'serial', '', '', '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'hostname', 'varchar', '', $char_d, '', '', + 'port', 'int', '', '', '', '', + 'username', 'varchar', '', $char_d, '', '', + 'password', 'varchar', '', $char_d, '', '', + 'path', 'varchar', '', $char_d, '', '', + 'secure', 'char', 'NULL', 1, '', '', + 'handling', 'varchar', 'NULL', $char_d, '', '', + ], + 'primary_key' => 'targetnum', + 'unique' => [ [ 'targetnum' ] ], + 'index' => [], + }, + %{ tables_hashref_torrus() }, # tables of ours for doing torrus virtual port combining diff --git a/FS/FS/Setup.pm b/FS/FS/Setup.pm index e2c5a5a2c..e27b66fc5 100644 --- a/FS/FS/Setup.pm +++ b/FS/FS/Setup.pm @@ -209,6 +209,14 @@ sub populate_initial_data { sub initial_data { my %opt = @_; + my $cust_location = FS::cust_location->new({ + 'address1' => '1234 System Lane', + 'city' => 'Systemtown', + 'state' => 'CA', + 'zip' => '54321', + 'country' => 'US', + }); + #tie my %hash, 'Tie::DxHash', tie my %hash, 'Tie::IxHash', @@ -351,14 +359,11 @@ sub initial_data { 'refnum' => 1, #XXX 'first' => 'System', 'last' => 'Accounts', - 'address1' => '1234 System Lane', - 'city' => 'Systemtown', - 'state' => 'CA', - 'zip' => '54321', - 'country' => 'US', 'payby' => 'COMP', 'payinfo' => 'system', #or something 'paydate' => '1/2037', + 'bill_location' => $cust_location, + 'ship_location' => $cust_location, }, ], diff --git a/FS/FS/UI/Web/small_custview.pm b/FS/FS/UI/Web/small_custview.pm index 36dd30c6d..2c42a6b46 100644 --- a/FS/FS/UI/Web/small_custview.pm +++ b/FS/FS/UI/Web/small_custview.pm @@ -29,7 +29,7 @@ sub small_custview { : qsearchs('cust_main', { 'custnum' => $arg } ) or die "unknown custnum $arg"; - my $html; + my $html = '<DIV ID="fs_small_custview">'; $html = qq!View <A HREF="$url?! . $cust_main->custnum . '">' if $url; @@ -82,45 +82,23 @@ sub small_custview { $html .= '</TD></TR></TABLE></TD>'; - if ( defined $cust_main->dbdef_table->column('ship_last') ) { - - my $pre = $cust_main->ship_last ? 'ship_' : ''; - - $html .= '<TD VALIGN="top">'. ntable("#cccccc",2). - '<TR><TD ALIGN="right" VALIGN="top">Service<BR>Address</TD><TD BGCOLOR="#ffffff">'. - $cust_main->get("${pre}last"). ', '. - $cust_main->get("${pre}first"). '<BR>'; - $html .= $cust_main->get("${pre}company"). '<BR>' - if $cust_main->get("${pre}company"); - $html .= $cust_main->get("${pre}address1"). '<BR>'; - $html .= $cust_main->get("${pre}address2"). '<BR>' - if $cust_main->get("${pre}address2"); - $html .= $cust_main->get("${pre}city"). ', '. - $cust_main->get("${pre}state"). ' '. - $cust_main->get("${pre}zip"). '<BR>'; - $html .= $cust_main->get("${pre}country"). '<BR>' - if $cust_main->get("${pre}country") - && $cust_main->get("${pre}country") ne $countrydefault; - - $html .= '</TD></TR><TR><TD></TD><TD BGCOLOR="#ffffff">'; - - if ( $cust_main->get("${pre}daytime") && $cust_main->get("${pre}night") ) { - use FS::Msgcat; - $html .= ( FS::Msgcat::_gettext('daytime') || 'Day' ). - ' '. $cust_main->get("${pre}daytime"). - '<BR>'. ( FS::Msgcat::_gettext('night') || 'Night' ). - ' '. $cust_main->get("${pre}night"); - } elsif ( $cust_main->get("${pre}daytime") - || $cust_main->get("${pre}night") ) { - $html .= $cust_main->get("${pre}daytime") - || $cust_main->get("${pre}night"); - } - if ( $cust_main->get("${pre}fax") ) { - $html .= '<BR>Fax '. $cust_main->get("${pre}fax"); - } + my $ship = $cust_main->ship_location; - $html .= '</TD></TR></TABLE></TD>'; - } + $html .= '<TD VALIGN="top">'. ntable("#cccccc",2). + '<TR><TD ALIGN="right" VALIGN="top">Service<BR>Address</TD><TD BGCOLOR="#ffffff">'; + $html .= join('<BR>', + grep $_, + $cust_main->contact, + $cust_main->company, + $ship->address1, + $ship->address2, + ($ship->city . ', ' . $ship->state . ' ' . $ship->zip), + ($ship->country eq $countrydefault ? '' : $ship->country ), + ); + + # ship phone numbers no longer exist... + + $html .= '</TD></TR></TABLE></TD>'; $html .= '</TR></TABLE>'; @@ -129,6 +107,8 @@ sub small_custview { # last payment might be good here too? + $html .= '</DIV>'; + $html; } diff --git a/FS/FS/access_groupsales.pm b/FS/FS/access_groupsales.pm new file mode 100644 index 000000000..31b07d9f8 --- /dev/null +++ b/FS/FS/access_groupsales.pm @@ -0,0 +1,153 @@ +package FS::access_groupsales; + +use strict; +use base qw( FS::Record ); +use FS::Record qw( qsearch qsearchs ); + +=head1 NAME + +FS::access_groupsales - Object methods for access_groupsales records + +=head1 SYNOPSIS + + use FS::access_groupsales; + + $record = new FS::access_groupsales \%hash; + $record = new FS::access_groupsales { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::access_groupsales object represents an example. FS::access_groupsales inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item groupsalesnum + +primary key + +=item groupnum + +groupnum + +=item salesnum + +salesnum + + +=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 { 'access_groupsales'; } + +=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 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('groupsalesnum') + || $self->ut_number('groupnum') + || $self->ut_number('salesnum') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=item sales + +Returns the associated FS::agent object. + +=cut + +sub sales { + my $self = shift; + qsearchs('sales', { 'salesnum' => $self->salesnum } ); +} + +=item access_group + +Returns the associated FS::access_group object. + +=cut + +sub access_group { + my $self = shift; + qsearchs('access_group', { 'groupnum' => $self->groupnum } ); +} + +=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/access_right.pm b/FS/FS/access_right.pm index 341055bfc..1e65ca354 100644 --- a/FS/FS/access_right.pm +++ b/FS/FS/access_right.pm @@ -2,7 +2,9 @@ package FS::access_right; use strict; use vars qw( @ISA ); +use Tie::IxHash; use FS::Record qw( qsearch qsearchs ); +use FS::upgrade_journal; @ISA = qw(FS::Record); @@ -182,19 +184,56 @@ sub _upgrade_data { # class method my @all_groups = qsearch('access_group', {}); - my %onetime = ( - 'List customers' => 'List all customers', - 'List packages' => 'Summarize packages', - ); + tie my %onetime, 'Tie::IxHash', + 'List customers' => 'List all customers', + 'List all customers' => 'Advanced customer search', + 'List packages' => 'Summarize packages', + 'Post payment' => 'Backdate payment', + 'Cancel customer package immediately' => 'Un-cancel customer package', + 'Suspend customer package' => 'Suspend customer', + 'Unsuspend customer package' => 'Unsuspend customer', + + 'List services' => [ 'Services: Accounts', + 'Services: Domains', + 'Services: Certificates', + 'Services: Mail forwards', + 'Services: Virtual hosting services', + 'Services: Wireless broadband services', + 'Services: DSLs', + 'Services: Dish services', + 'Services: Hardware', + 'Services: Phone numbers', + 'Services: PBXs', + 'Services: Ports', + 'Services: Mailing lists', + 'Services: External services', + ], + + 'Services: Accounts' => 'Services: Accounts: Advanced search', + 'Services: Wireless broadband services' => 'Services: Wireless broadband services: Advanced search', + 'Services: Hardware' => 'Services: Hardware: Advanced search', + + 'List rating data' => [ 'Usage: RADIUS sessions', + 'Usage: Call Detail Records (CDRs)', + 'Usage: Unrateable CDRs', + ], + ; foreach my $old_acl ( keys %onetime ) { - my $new_acl = $onetime{$old_acl}; #support arrayref too? - ( my $journal = 'ACL_'.lc($new_acl) ) =~ s/ /_/g; - next if FS::upgrade_journal->is_done($journal); - # grant $new_acl to all groups who have $old_acl - for my $group (@all_groups) { - if ( $group->access_right($old_acl) ) { + my @new_acl = ref($onetime{$old_acl}) + ? @{ $onetime{$old_acl} } + : ( $onetime{$old_acl} ); + + foreach my $new_acl ( @new_acl ) { + + ( my $journal = 'ACL_'.lc($new_acl) ) =~ s/\W/_/g; + next if FS::upgrade_journal->is_done($journal); + + # grant $new_acl to all groups who have $old_acl + for my $group (@all_groups) { + next unless $group->access_right($old_acl); + next if $group->access_right($new_acl); my $access_right = FS::access_right->new( { 'righttype' => 'FS::access_group', 'rightobjnum' => $group->groupnum, @@ -203,9 +242,11 @@ sub _upgrade_data { # class method my $error = $access_right->insert; die $error if $error; } - } - FS::upgrade_journal->set_done($journal); + FS::upgrade_journal->set_done($journal); + + } + } ### ACL_download_report_data diff --git a/FS/FS/cdr/cia.pm b/FS/FS/cdr/cia.pm index 070f3fb0d..ca44c0fdf 100644 --- a/FS/FS/cdr/cia.pm +++ b/FS/FS/cdr/cia.pm @@ -20,11 +20,12 @@ use FS::cdr qw(_cdr_date_parser_maker); skip(2), # Conference Start Time, Conference End Time _cdr_date_parser_maker('startdate'), # Connect Time _cdr_date_parser_maker('enddate'), # Disconnect Time + skip(1), # Duration sub { my($cdr, $data, $conf, $param) = @_; $cdr->duration($data); $cdr->billsec( $data); - }, # Duration - skip(2), # Roundup Duration, User Name + }, # Roundup Duration + skip(1), # User Name 'dst', # DNIS 'src', # ANI skip(2), # Call Type, Toll Free, diff --git a/FS/FS/cdr/infinite.pm b/FS/FS/cdr/infinite.pm index 90560c8c7..02ff9df6f 100644 --- a/FS/FS/cdr/infinite.pm +++ b/FS/FS/cdr/infinite.pm @@ -6,6 +6,8 @@ use FS::cdr qw(_cdr_date_parser_maker); @ISA = qw(FS::cdr); +my $date_parser = _cdr_date_parser_maker('startdate'); + %info = ( 'name' => 'Infinite Conferencing', 'weight' => 520, @@ -13,26 +15,38 @@ use FS::cdr qw(_cdr_date_parser_maker); 'type' => 'csv', 'sep_char' => ',', 'import_fields' => [ - 'uniqueid', # billid - skip(3), # confid, invoicenum, acctgrpid - 'accountcode', # accountid ("Room Confirmation Number") - skip(2), # billingcode ("Room Billingcode"), confname - skip(1), # participant_type - 'startdate', # starttime_t - skip(2), # startdate, starttime + 'uniqueid', # A. billid + skip(3), # B-D. confid, invoicenum, acctgrpid + skip(1), # E. accountid ("Room Confirmation Number") + skip(2), # F-G. billingcode ("Room Billingcode"), confname + skip(1), # H. participant_type + skip(1), # I. starttime_t - timezone is unreliable + sub { # J. startdate + my ($cdr, $data, $conf, $param) = @_; + $param->{'date_part'} = $data; # stash this and combine with the time + ''; + }, + sub { # K. starttime + my ($cdr, $data, $conf, $param) = @_; + my $datestring = delete($param->{'date_part'}) . ' ' . $data; + &{ $date_parser }($cdr, $datestring); + }, sub { my($cdr, $data, $conf, $param) = @_; $cdr->duration($data * 60); $cdr->billsec( $data * 60); - }, # minutes - 'dst', # dnis - 'src', # ani - skip(8), # calltype, calltype_text, confstart_t, confstartdate, + }, # L. minutes + skip(1), # M. dnis + 'src', # N. ani + 'dst', # O. calltype + skip(7), # P-V. calltype_text, confstart_t, confstartdate, # confstarttime, confminutes, conflegs, ppm - 'upstream_price', # callcost - skip(13), # confcost, rppm, rcallcost, rconfcost, - # auxdata[1..4], ldval, sysname, username, cec, pec - 'userfield', # unnamed field - ], + 'upstream_price', # W. callcost + skip(11), # X-AH. confcost, rppm, rcallcost, rconfcost, + # auxdata[1..4], ldval, sysname, username + 'accountcode', # AI. Chairperson Entry Code + skip(1), # AJ. Participant Entry Code + 'description', # AK. contact name + ], ); diff --git a/FS/FS/cdr/troop.pm b/FS/FS/cdr/troop.pm index 020af2b20..429c25a53 100644 --- a/FS/FS/cdr/troop.pm +++ b/FS/FS/cdr/troop.pm @@ -7,7 +7,7 @@ use Time::Local; #use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker ); %info = ( - 'name' => 'Troop', + 'name' => 'Troop (old?)', 'weight' => 220, 'header' => 2, 'type' => 'xls', diff --git a/FS/FS/cdr/troop2.pm b/FS/FS/cdr/troop2.pm new file mode 100644 index 000000000..ee6474061 --- /dev/null +++ b/FS/FS/cdr/troop2.pm @@ -0,0 +1,94 @@ +package FS::cdr::troop2; + +use strict; +use base qw( FS::cdr ); +use vars qw( %info $tmp_date $tmp_src_city $tmp_dst_city ); +use Date::Parse; +#use Time::Local; +##use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker ); + +%info = ( + 'name' => 'Troop', + 'weight' => 219, + 'header' => 1, + 'type' => 'xls', + + 'import_fields' => [ + + 'userfield', #account_num (userfield?) + + #call_date + sub { my($cdr, $date) = @_; + #is this an excel date? or just text? + $tmp_date = $date; + }, + + #call_time + sub { my($cdr, $time) = @_; + #is this an excel time? or just text? + $cdr->startdate( str2time("$tmp_date $time") ); + }, + + 'src', #orig_tn + 'dst', #term_tn + + #call_dur + sub { my($cdr, $duration) = @_; + $cdr->duration($duration); + $cdr->billsec($duration); + }, + + 'clid', #auth_code_ani (clid?) + + 'accountcode', #account_code + + #ovs_type + # OVS Type / Maybe / add "011" to international calls + # N = DOM LD / normal + # Z = INTL LD + # O = INTL LD + # others...? + sub { my($cdr, $ovs) = @_; + my $pre = ( $ovs =~ /^\s*[OZ]\s*$/i ) ? '011' : '1'; + $cdr->dst( $pre. $cdr->dst ) unless $cdr->dst =~ /^$pre/; + }, + + #orig_city + sub { (my $cdr, $tmp_src_city) = @_; }, + + #orig_prov_state + sub { my($cdr, $state) = @_; + $cdr->upstream_src_regionname("$tmp_src_city, $state"); + }, + + #term_city + sub { (my $cdr, $tmp_dst_city) = @_; }, + + #term_prov_state + sub { my($cdr, $state) = @_; + $cdr->upstream_dst_regionname("$tmp_dst_city, $state"); + }, + + #term_ovs + '', #CANADA / UNITED STATES / BELL. huh. country or terminating provider? + + '', #cc_ind (what's this?) + + 'upstream_price', #call_charge + + #important? + '', #creation_date + '', #creation_time + + #additional upstream pricing details we don't need? + '', #net_charge + '', #surcharge + '', #gst + '', #pst + '', #hst + + ], + +); + +1; diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index a76170a9b..35ab9f388 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -388,8 +388,10 @@ sub previous { my $self = shift; my $total = 0; my @cust_bill = sort { $a->_date <=> $b->_date } - grep { $_->owed != 0 && $_->_date < $self->_date } - qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) + grep { $_->owed != 0 } + qsearch( 'cust_bill', { 'custnum' => $self->custnum, + '_date' => { op=>'<', value=>$self->_date }, + } ) ; foreach ( @cust_bill ) { $total += $_->owed; } $total, @cust_bill; @@ -1314,14 +1316,16 @@ sub send { $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/; } + my $cust_main = $self->cust_main; + return 'N/A' unless ! $agentnums - or grep { $_ == $self->cust_main->agentnum } @$agentnums; + or grep { $_ == $cust_main->agentnum } @$agentnums; return '' - unless $self->cust_main->total_owed_date($self->_date) > $balance_over; + unless $cust_main->total_owed_date($self->_date) > $balance_over; $invoice_from ||= $self->_agent_invoice_from || #XXX should go away - $conf->config('invoice_from', $self->cust_main->agentnum ); + $conf->config('invoice_from', $cust_main->agentnum ); my %opt = ( 'template' => $template, @@ -1329,11 +1333,12 @@ sub send { 'notice_name' => ( $notice_name || 'Invoice' ), ); - my @invoicing_list = $self->cust_main->invoicing_list; + my @invoicing_list = $cust_main->invoicing_list; #$self->email_invoice(\%opt) $self->email(\%opt) - if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list; + if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list ) + && ! $self->invoice_noemail; #$self->print_invoice(\%opt) $self->print(\%opt) @@ -1748,13 +1753,21 @@ Options are: =over 4 -=item format - 'default' or 'billco' +=item format - any of FS::Misc::::Invoicing::spool_formats -=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>). +=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the +customer has the corresponding invoice destinations set (see +L<FS::cust_main_invoice>). -=item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file +=item agent_spools - if set to a true value, will spool to per-agent files +rather than a single global file -=item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount. +=item ftp_targetnum - if set to an FTP target (see L<FS::ftp_target>), will +append to that spool. L<FS::Cron::upload> will then send the spool file to +that destination. + +=item balanceover - if set, only spools the invoice if the total amount owed on +this invoice and all older invoices is greater than the specified amount. =back @@ -1782,11 +1795,23 @@ sub spool_csv { my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time); - my $file = - "$spooldir/". - ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ). - ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) . - '.csv'; + my $file; + if ( $opt{'agent_spools'} ) { + $file = 'agentnum'.$cust_main->agentnum; + } else { + $file = 'spool'; + } + + if ( $opt{'ftp_targetnum'} ) { + $spooldir .= '/target'.$opt{'ftp_targetnum'}; + mkdir $spooldir, 0700 unless -d $spooldir; + } # otherwise it just goes into export.xxx/cust_bill + + if ( lc($opt{'format'}) eq 'billco' ) { + $file .= '-header'; + } + + $file = "$spooldir/$file.csv"; my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum ); @@ -1801,10 +1826,7 @@ sub spool_csv { flock(CSV, LOCK_UN); close CSV; - $file = - "$spooldir/". - ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ). - '-detail.csv'; + $file =~ s/-header.csv$/-detail.csv/; open(CSV,">>$file") or die "can't open $file: $!"; flock(CSV, LOCK_EX); @@ -1826,7 +1848,7 @@ Returns CSV data for this invoice. Options are: -format - 'default' or 'billco' +format - 'default', 'billco', 'oneline', 'bridgestone' Returns a list consisting of two scalars. The first is a single line of CSV header information for this invoice. The second is one or more lines of CSV @@ -1835,7 +1857,8 @@ detail information for this invoice. If I<format> is not specified or "default", the fields of the CSV file are as follows: -record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate +record_type, invnum, custnum, _date, charged, first, last, company, address1, +address2, city, state, zip, country, pkg, setup, recur, sdate, edate =over 4 @@ -1940,6 +1963,26 @@ If I<format> is "billco", the fields of the detail CSV file are as follows: 9 | Grouping Code | GROUP | CHAR | 2 10 | User Defined | ACCT CODE | CHAR | 15 +If format is 'oneline', there is no detail file. Each invoice has a +header line only, with the fields: + +Agent number, agent name, customer number, first name, last name, address +line 1, address line 2, city, state, zip, invoice date, invoice number, +amount charged, amount due, + +and then, for each line item, three columns containing the package number, +description, and amount. + +If format is 'bridgestone', there is no detail file. Each invoice has a +header line with the following fields in a fixed-width format: + +Customer number (in display format), date, name (first last), company, +address 1, address 2, city, state, zip. + +This is a mailing list format, and has no per-invoice fields. To avoid +sending redundant notices, the spooling event should have a "once" or +"once_percust_every" condition. + =cut sub print_csv { @@ -2036,6 +2079,31 @@ sub print_csv { @items, ); + } elsif ( lc($opt{'format'}) eq 'bridgestone' ) { + + # bypass the CSV stuff and just return this + my $longdate = time2str('%B %d, %Y', time); #current time, right? + my $zip = $cust_main->zip; + $zip =~ s/\D//; + my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum) + || ''; + return ( + sprintf( + "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n", + $prefix, + $cust_main->display_custnum, + $longdate, + uc(substr($cust_main->contact_firstlast,0,30)), + uc(substr($cust_main->company ,0,30)), + uc(substr($cust_main->address1 ,0,30)), + uc(substr($cust_main->address2 ,0,30)), + uc(substr($cust_main->city ,0,20)), + uc($cust_main->state), + $zip + ), + '' #detail + ); + } else { $csv->combine( @@ -2777,11 +2845,13 @@ sub print_generic { $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion my $countrydefault = $conf->config('countrydefault') || 'US'; - my $prefix = $cust_main->has_ship_address ? 'ship_' : ''; - foreach ( qw( contact company address1 address2 city state zip country fax) ){ - my $method = $prefix.$_; + foreach ( qw( address1 address2 city state zip country fax) ){ + my $method = 'ship_'.$_; $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method); } + foreach ( qw( contact company ) ) { #compatibility + $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_); + } $invoice_data{'ship_country'} = '' if ( $invoice_data{'ship_country'} eq $countrydefault ); @@ -2978,6 +3048,12 @@ sub print_generic { my $late_sections = []; my $extra_sections = []; my $extra_lines = (); + + my $default_section = { 'description' => '', + 'subtotal' => '', + 'no_subtotal' => 1, + }; + if ( $multisection ) { ($extra_sections, $extra_lines) = $self->_items_extra_usage_sections($escape_function_nonbsp, $format) @@ -3009,8 +3085,7 @@ sub print_generic { } } else {# not multisection # make a default section - push @sections, { 'description' => '', 'subtotal' => '', - 'no_subtotal' => 1 }; + push @sections, $default_section; # and calculate the finance charge total, since it won't get done otherwise. # XXX possibly other totals? # XXX possibly finance_pkgclass should not be used in this manner? @@ -3043,7 +3118,8 @@ sub print_generic { }; $detail->{'ref'} = $line_item->{'pkgnum'}; $detail->{'quantity'} = 1; - $detail->{'section'} = $previous_section; + $detail->{'section'} = $multisection ? $previous_section + : $default_section; $detail->{'description'} = &$escape_function($line_item->{'description'}); if ( exists $line_item->{'ext_description'} ) { @{$detail->{'ext_description'}} = map { @@ -3882,17 +3958,20 @@ sub _items_sections { if ( $display->post_total && !$summarypage ) { if (! $type || $type eq 'S') { $late_subtotal{$section} += $cust_bill_pkg->setup - if $cust_bill_pkg->setup != 0; + if $cust_bill_pkg->setup != 0 + || $cust_bill_pkg->setup_show_zero; } if (! $type) { $late_subtotal{$section} += $cust_bill_pkg->recur - if $cust_bill_pkg->recur != 0; + if $cust_bill_pkg->recur != 0 + || $cust_bill_pkg->recur_show_zero; } if ($type && $type eq 'R') { $late_subtotal{$section} += $cust_bill_pkg->recur - $usage - if $cust_bill_pkg->recur != 0; + if $cust_bill_pkg->recur != 0 + || $cust_bill_pkg->recur_show_zero; } if ($type && $type eq 'U') { @@ -3906,17 +3985,20 @@ sub _items_sections { if (! $type || $type eq 'S') { $subtotal{$section} += $cust_bill_pkg->setup - if $cust_bill_pkg->setup != 0; + if $cust_bill_pkg->setup != 0 + || $cust_bill_pkg->setup_show_zero; } if (! $type) { $subtotal{$section} += $cust_bill_pkg->recur - if $cust_bill_pkg->recur != 0; + if $cust_bill_pkg->recur != 0 + || $cust_bill_pkg->recur_show_zero; } if ($type && $type eq 'R') { $subtotal{$section} += $cust_bill_pkg->recur - $usage - if $cust_bill_pkg->recur != 0; + if $cust_bill_pkg->recur != 0 + || $cust_bill_pkg->recur_show_zero; } if ($type && $type eq 'U') { @@ -4909,6 +4991,8 @@ sub _items_cust_bill_pkg { } } + my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display; + warn "$me _items_cust_bill_pkg considering cust_bill_pkg ". $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n" if $DEBUG > 1; @@ -4919,7 +5003,7 @@ sub _items_cust_bill_pkg { } #grep { !$_->summary || !$summary_page } # bunk! grep { !$_->summary || $multisection } - $cust_bill_pkg->cust_bill_pkg_display + @cust_bill_pkg_display ) { @@ -5421,6 +5505,7 @@ sub process_re_X { } sub re_X { + # spool_invoice ftp_invoice fax_invoice print_invoice my($method, $job, %param ) = @_; if ( $DEBUG ) { warn "re_X $method for job $job with param:\n". diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 1ee5c0943..4220d3c06 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -955,8 +955,6 @@ sub cust_bill_pkg_display { my $default = new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum }; - return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm - my $type = $opt{type} if exists $opt{type}; my @result; @@ -1043,26 +1041,125 @@ sub cust_bill_pkg_discount { =cut -sub recur_show_zero { - #my $self = shift; - # $self->recur == 0 - #&& $self->pkgnum - #&& $self->cust_pkg->part_pkg->recur_show_zero; +sub recur_show_zero { shift->_X_show_zero('recur'); } +sub setup_show_zero { shift->_X_show_zero('setup'); } + +sub _X_show_zero { + my( $self, $what ) = @_; - shift->_X_show_zero('recur'); + return 0 unless $self->$what() == 0 && $self->pkgnum; + $self->cust_pkg->_X_show_zero($what); } -sub setup_show_zero { - shift->_X_show_zero('setup'); +=back + +=head1 CLASS METHODS + +=over 4 + +=item usage_sql + +Returns an SQL expression for the total usage charges in details on +an item. + +=cut + +my $usage_sql = + '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0) + FROM cust_bill_pkg_detail + WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)'; + +sub usage_sql { $usage_sql } + +# this makes owed_sql, etc. much more concise +sub charged_sql { + my ($class, $start, $end, %opt) = @_; + my $charged = + $opt{setuprecur} =~ /^s/ ? 'cust_bill_pkg.setup' : + $opt{setuprecur} =~ /^r/ ? 'cust_bill_pkg.recur' : + 'cust_bill_pkg.setup + cust_bill_pkg.recur'; + + if ($opt{no_usage} and $charged =~ /recur/) { + $charged = "$charged - $usage_sql" + } + + $charged; } -sub _X_show_zero { - my( $self, $what ) = @_; - return 0 unless $self->$what() == 0 && $self->pkgnum; +=item owed_sql [ BEFORE, AFTER, OPTIONS ] + +Returns an SQL expression for the amount owed. BEFORE and AFTER specify +a date window. OPTIONS may include 'no_usage' (excludes usage charges) +and 'setuprecur' (set to "setup" or "recur" to limit to one or the other). + +=cut + +sub owed_sql { + my $class = shift; + '(' . $class->charged_sql(@_) . + ' - ' . $class->paid_sql(@_) . + ' - ' . $class->credited_sql(@_) . ')' +} + +=item paid_sql [ BEFORE, AFTER, OPTIONS ] + +Returns an SQL expression for the sum of payments applied to this item. + +=cut + +sub paid_sql { + my ($class, $start, $end, %opt) = @_; + my $s = $start ? "AND cust_bill_pay._date <= $start" : ''; + my $e = $end ? "AND cust_bill_pay._date > $end" : ''; + my $setuprecur = + $opt{setuprecur} =~ /^s/ ? 'setup' : + $opt{setuprecur} =~ /^r/ ? 'recur' : + ''; + $setuprecur &&= "AND setuprecur = '$setuprecur'"; + + my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0) + FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum) + WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum + $s $e$setuprecur )"; + + if ( $opt{no_usage} ) { + # cap the amount paid at the sum of non-usage charges, + # minus the amount credited against non-usage charges + "LEAST($paid, ". + $class->charged_sql($start, $end, %opt) . ' - ' . + $class->credited_sql($start, $end, %opt).')'; + } + else { + $paid; + } + +} + +sub credited_sql { + my ($class, $start, $end, %opt) = @_; + my $s = $start ? "AND cust_credit_bill._date <= $start" : ''; + my $e = $end ? "AND cust_credit_bill._date > $end" : ''; + my $setuprecur = + $opt{setuprecur} =~ /^s/ ? 'setup' : + $opt{setuprecur} =~ /^r/ ? 'recur' : + ''; + $setuprecur &&= "AND setuprecur = '$setuprecur'"; + + my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0) + FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum) + WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum + $s $e $setuprecur )"; + + if ( $opt{no_usage} ) { + # cap the amount credited at the sum of non-usage charges + "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')'; + } + else { + $credited; + } - $self->cust_pkg->_X_show_zero($what); } =back diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm index a5250ec05..2810dc957 100644 --- a/FS/FS/cust_location.pm +++ b/FS/FS/cust_location.pm @@ -4,7 +4,7 @@ use base qw( FS::geocode_Mixin FS::Record ); use strict; use vars qw( $import ); use Locale::Country; -use FS::UID qw( dbh ); +use FS::UID qw( dbh driver_name ); use FS::Record qw( qsearch ); #qsearchs ); use FS::Conf; use FS::prospect_main; @@ -113,11 +113,16 @@ otherwise returns false. sub insert { my $self = shift; + my $conf = new FS::Conf; + + if ( $self->censustract ) { + $self->set('censusyear' => $conf->config('census_year') || 2012); + } + my $error = $self->SUPER::insert(@_); #false laziness with cust_main, will go away eventually - my $conf = new FS::Conf; - if ( !$error and $conf->config('tax_district_method') ) { + if ( !$import and !$error and $conf->config('tax_district_method') ) { my $queue = new FS::queue { 'job' => 'FS::geocode_Mixin::process_district_update' @@ -144,21 +149,14 @@ sub replace { my $self = shift; my $old = shift; $old ||= $self->replace_old; - my $error = $self->SUPER::replace($old); - - #false laziness with cust_main, will go away eventually - my $conf = new FS::Conf; - if ( !$error and $conf->config('tax_district_method') - and $self->get('address1') ne $old->get('address1') ) { - - my $queue = new FS::queue { - 'job' => 'FS::geocode_Mixin::process_district_update' - }; - $error = $queue->insert( ref($self), $self->locationnum ); - + # the following fields are immutable + foreach (qw(address1 address2 city state zip country)) { + if ( $self->$_ ne $old->$_ ) { + return "can't change cust_location field $_"; + } } - $error || ''; + $self->SUPER::replace($old); } @@ -174,6 +172,7 @@ and replace methods. #fields anyway... sub check { my $self = shift; + my $conf = new FS::Conf; my $error = $self->ut_numbern('locationnum') @@ -185,7 +184,7 @@ sub check { || $self->ut_textn('county') || $self->ut_textn('state') || $self->ut_country('country') - || $self->ut_zip('zip', $self->country) + || (!$import && $self->ut_zip('zip', $self->country)) || $self->ut_coordn('latitude') || $self->ut_coordn('longitude') || $self->ut_enum('coord_auto', [ '', 'Y' ]) @@ -194,22 +193,36 @@ sub check { || $self->ut_enum('location_kind', [ '', 'R', 'B' ] ) || $self->ut_alphan('geocode') || $self->ut_alphan('district') + || $self->ut_numbern('censusyear') ; return $error if $error; + if ( $self->censustract ne '' ) { + $self->censustract =~ /^\s*(\d{9})\.?(\d{2})\s*$/ + or return "Illegal census tract: ". $self->censustract; + + $self->censustract("$1.$2"); + } + + if ( $conf->exists('cust_main-require_address2') and + !$self->ship_address2 =~ /\S/ ) { + return "Unit # is required"; + } $self->set_coord unless $import || ($self->latitude && $self->longitude); - return "No prospect or customer!" unless $self->prospectnum || $self->custnum; + # tricky...we have to allow for the customer to not be inserted yet + return "No prospect or customer!" unless $self->prospectnum + || $self->custnum + || $self->get('custnum_pending'); return "Prospect and customer!" if $self->prospectnum && $self->custnum; - my $conf = new FS::Conf; return 'Location kind is required' if $self->prospectnum && $conf->exists('prospect_main-alt_address_format') && ! $self->location_kind; - unless ( qsearch('cust_main_county', { + unless ( $import or qsearch('cust_main_county', { 'country' => $self->country, 'state' => '', } ) ) { @@ -266,19 +279,40 @@ location_kind. =cut -=item move_to HASHREF +=item disable_if_unused + +Sets the "disabled" flag on the location if it is no longer in use as a +prospect location, package location, or a customer's billing or default +service address. + +=cut + +sub disable_if_unused { + + my $self = shift; + my $locationnum = $self->locationnum; + return '' if FS::cust_main->count('bill_locationnum = '.$locationnum) + or FS::cust_main->count('ship_locationnum = '.$locationnum) + or FS::contact->count( 'locationnum = '.$locationnum) + or FS::cust_pkg->count('cancel IS NULL AND + locationnum = '.$locationnum) + ; + $self->disabled('Y'); + $self->replace; + +} + +=item move_to -Takes a hashref with one or more cust_location fields. Creates a duplicate -of the existing location with all fields set to the values in the hashref. -Moves all packages that use the existing location to the new one, then sets -the "disabled" flag on the old location. Returns nothing on success, an -error message on error. +Takes a new L<FS::cust_location> object. Moves all packages that use the +existing location to the new one, then sets the "disabled" flag on the old +location. Returns nothing on success, an error message on error. =cut sub move_to { my $old = shift; - my $hashref = shift; + my $new = shift; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -292,16 +326,12 @@ sub move_to { my $dbh = dbh; my $error = ''; - my $new = FS::cust_location->new({ - $old->location_hash, - 'custnum' => $old->custnum, - 'prospectnum' => $old->prospectnum, - %$hashref - }); - $error = $new->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "Error creating location: $error"; + if ( !$new->locationnum ) { + $error = $new->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error creating location: $error"; + } } my @pkgs = qsearch('cust_pkg', { @@ -319,15 +349,14 @@ sub move_to { } } - $old->disabled('Y'); - $error = $old->replace; + $error = $old->disable_if_unused; if ( $error ) { $dbh->rollback if $oldAutoCommit; return "Error disabling old location: $error"; } $dbh->commit if $oldAutoCommit; - return; + ''; } =item alternize @@ -408,11 +437,100 @@ sub dealternize { ''; } +=item location_label + +Returns the label of the location object, with an optional site ID +string (based on the cust_location-label_prefix config option). + +=cut + +sub location_label { + my $self = shift; + my %opt = @_; + my $conf = new FS::Conf; + my $prefix = ''; + my $format = $conf->config('cust_location-label_prefix') || ''; + my $cust_or_prospect; + if ( $self->custnum ) { + $cust_or_prospect = FS::cust_main->by_key($self->custnum); + } + elsif ( $self->prospectnum ) { + $cust_or_prospect = FS::prospect_main->by_key($self->prospectnum); + } + + if ( $format eq 'CoStAg' ) { + my $agent = $conf->config('cust_main-custnum-display_prefix', + $cust_or_prospect->agentnum) + || $cust_or_prospect->agent->agent; + # else this location is invalid + $prefix = uc( join('', + $self->country, + ($self->state =~ /^(..)/), + ($agent =~ /^(..)/), + sprintf('%05d', $self->locationnum) + ) ); + } + elsif ( $self->custnum and + $self->locationnum == $cust_or_prospect->ship_locationnum ) { + $prefix = 'Default service location'; + } + $prefix .= ($opt{join_string} || ': ') if $prefix; + $prefix . $self->SUPER::location_label(%opt); +} + =back -=head1 BUGS +=head1 CLASS METHODS -Not yet used for cust_main billing and shipping addresses. +=item in_county_sql OPTIONS + +Returns an SQL expression to test membership in a cust_main_county +geographic area. By default, this requires district, city, county, +state, and country to match exactly. Pass "ornull => 1" to allow +partial matches where some fields are NULL in the cust_main_county +record but not in the location. + +Pass "param => 1" to receive a parameterized expression (rather than +one that requires a join to cust_main_county) and a list of parameter +names in order. + +=cut + +sub in_county_sql { + # replaces FS::cust_pkg::location_sql + my ($class, %opt) = @_; + my $ornull = $opt{ornull} ? ' OR ? IS NULL' : ''; + my $x = $ornull ? 3 : 2; + my @fields = (('district') x 3, + ('city') x 3, + ('county') x $x, + ('state') x $x, + 'country'); + + my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text'; + + my @where = ( + "cust_location.district = ? OR ? = '' OR CAST(? AS $text) IS NULL", + "cust_location.city = ? OR ? = '' OR CAST(? AS $text) IS NULL", + "cust_location.county = ? OR (? = '' AND cust_location.county IS NULL) $ornull", + "cust_location.state = ? OR (? = '' AND cust_location.state IS NULL ) $ornull", + "cust_location.country = ?" + ); + my $sql = join(' AND ', map "($_)\n", @where); + if ( $opt{param} ) { + return $sql, @fields; + } + else { + # do the substitution here + foreach (@fields) { + $sql =~ s/\?/cust_main_county.$_/; + $sql =~ s/cust_main_county.$_ = ''/cust_main_county.$_ IS NULL/; + } + return $sql; + } +} + +=head1 BUGS =head1 SEE ALSO diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 845d09848..b382232b2 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -6,6 +6,7 @@ use strict; use base qw( FS::cust_main::Packages FS::cust_main::Status FS::cust_main::Billing FS::cust_main::Billing_Realtime FS::cust_main::Billing_Discount + FS::cust_main::Location FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin FS::geocode_Mixin FS::o2m_Common @@ -14,7 +15,7 @@ use base qw( FS::cust_main::Packages FS::cust_main::Status use vars qw( $DEBUG $me $conf @encrypted_fields $import - $ignore_expired_card $ignore_illegal_zip $ignore_banned_card + $ignore_expired_card $ignore_banned_card $ignore_illegal_zip $skip_fuzzyfiles @paytypes ); @@ -71,6 +72,7 @@ use FS::cust_main_note; use FS::cust_attachment; use FS::contact; use FS::Locales; +use FS::upgrade_journal; # 1 is mostly method/subroutine entry and options # 2 traces progress of some operations @@ -80,7 +82,6 @@ $me = '[FS::cust_main]'; $import = 0; $ignore_expired_card = 0; -$ignore_illegal_zip = 0; $ignore_banned_card = 0; $skip_fuzzyfiles = 0; @@ -178,28 +179,6 @@ Cocial security number (optional) (optional) -=item address1 - -=item address2 - -(optional) - -=item city - -=item county - -(optional, see L<FS::cust_main_county>) - -=item state - -(see L<FS::cust_main_county>) - -=item zip - -=item country - -(see L<FS::cust_main_county>) - =item daytime phone (optional) @@ -216,56 +195,6 @@ phone (optional) phone (optional) -=item ship_first - -Shipping first name - -=item ship_last - -Shipping last name - -=item ship_company - -(optional) - -=item ship_address1 - -=item ship_address2 - -(optional) - -=item ship_city - -=item ship_county - -(optional, see L<FS::cust_main_county>) - -=item ship_state - -(see L<FS::cust_main_county>) - -=item ship_zip - -=item ship_country - -(see L<FS::cust_main_county>) - -=item ship_daytime - -phone (optional) - -=item ship_night - -phone (optional) - -=item ship_fax - -phone (optional) - -=item ship_mobile - -phone (optional) - =item payby Payment Type (See L<FS::payinfo_Mixin> for valid payby values) @@ -364,6 +293,12 @@ sub table { 'cust_main'; } Adds this customer to the database. If there is an error, returns the error, otherwise returns false. +Usually the customer's location will not yet exist in the database, and +the C<bill_location> and C<ship_location> pseudo-fields must be set to +uninserted L<FS::cust_location> objects. These will be inserted and linked +(in both directions) to the new customer record. If they're references +to the same object, they will become the same location. + CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records are inserted atomicly, or the transaction is rolled back. Passing an empty @@ -399,8 +334,9 @@ The I<noexport> option is deprecated. If I<noexport> is set true, no provisioning jobs (exports) are scheduled. (You can schedule them later with the B<reexport> method.) -The I<tax_exemption> option can be set to an arrayref of tax names. -FS::cust_main_exemption records will be created and inserted. +The I<tax_exemption> option can be set to an arrayref of tax names or a hashref +of tax names and exemption numbers. FS::cust_main_exemption records will be +created and inserted. If I<prospectnum> is set, moves contacts and locations from that prospect. @@ -461,13 +397,47 @@ sub insert { } + # insert locations + foreach my $l (qw(bill_location ship_location)) { + my $loc = delete $self->hashref->{$l}; + # XXX if we're moving a prospect's locations, do that here + if ( !$loc ) { + return "$l not set"; + } + + if ( !$loc->locationnum ) { + # warn the location that we're going to insert it with no custnum + $loc->set(custnum_pending => 1); + warn " inserting $l\n" + if $DEBUG > 1; + my $error = $loc->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + my $label = $l eq 'ship_location' ? 'service' : 'billing'; + return "$error (in $label location)"; + } + } + elsif ( ($loc->custnum || 0) > 0 or $loc->prospectnum ) { + # then it somehow belongs to another customer--shouldn't happen + $dbh->rollback if $oldAutoCommit; + return "$l belongs to customer ".$loc->custnum; + } + # else it already belongs to this customer + # (happens when ship_location is identical to bill_location) + + $self->set($l.'num', $loc->locationnum); + + if ( $self->get($l.'num') eq '' ) { + $dbh->rollback if $oldAutoCommit; + return "$l not set"; + } + } + warn " inserting $self\n" if $DEBUG > 1; $self->signupdate(time) unless $self->signupdate; - $self->censusyear($conf->config('census_year')||'2012') if $self->censustract; - $self->auto_agent_custid() if $conf->config('cust_main-auto_agent_custid') && ! $self->agent_custid; @@ -478,6 +448,20 @@ sub insert { return $error; } + # now set cust_location.custnum + foreach my $l (qw(bill_location ship_location)) { + warn " setting $l.custnum\n" + if $DEBUG > 1; + my $loc = $self->$l; + $loc->set(custnum => $self->custnum); + $error ||= $loc->replace; + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error setting $l custnum: $error"; + } + } + warn " setting invoicing list\n" if $DEBUG > 1; @@ -545,10 +529,15 @@ sub insert { my $tax_exemption = delete $options{'tax_exemption'}; if ( $tax_exemption ) { - foreach my $taxname ( @$tax_exemption ) { + + $tax_exemption = { map { $_ => '' } @$tax_exemption } + if ref($tax_exemption) eq 'ARRAY'; + + foreach my $taxname ( keys %$tax_exemption ) { my $cust_main_exemption = new FS::cust_main_exemption { - 'custnum' => $self->custnum, - 'taxname' => $taxname, + 'custnum' => $self->custnum, + 'taxname' => $taxname, + 'exempt_number' => $tax_exemption->{$taxname}, }; my $error = $cust_main_exemption->insert; if ( $error ) { @@ -1312,7 +1301,7 @@ sub merge { } - my $name = $self->ship_name; + my $name = $self->ship_name; #? my $locationnum = ''; foreach my $cust_pkg ( $self->all_pkgs ) { @@ -1448,10 +1437,13 @@ sub merge { =item replace [ OLD_RECORD ] [ INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ] - Replaces the OLD_RECORD with this one in the database. If there is an error, returns the error, otherwise returns false. +To change the customer's address, set the pseudo-fields C<bill_location> and +C<ship_location>. The address will still only change if at least one of the +address fields differs from the existing values. + INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will be set as the invoicing list (see L<"invoicing_list">). Errors return as expected and rollback the entire transaction; it is not necessary to call @@ -1461,8 +1453,9 @@ check_invoicing_list first. Here's an example: Currently available options are: I<tax_exemption>. -The I<tax_exemption> option can be set to an arrayref of tax names. -FS::cust_main_exemption records will be deleted and inserted as appropriate. +The I<tax_exemption> option can be set to an arrayref of tax names or a hashref +of tax names and exemption numbers. FS::cust_main_exemption records will be +deleted and inserted as appropriate. =cut @@ -1487,41 +1480,19 @@ sub replace { return "You are not permitted to create complimentary accounts."; } - if ( $old->get('geocode') && $old->get('geocode') eq $self->get('geocode') - && $conf->exists('enable_taxproducts') - ) - { - my $pre = ($conf->exists('tax-ship_address') && $self->ship_zip) - ? 'ship_' : ''; - $self->set('geocode', '') - if $old->get($pre.'zip') ne $self->get($pre.'zip') - && length($self->get($pre.'zip')) >= 10; - } - - for my $pre ( grep $old->get($_.'coord_auto'), ( '', 'ship_' ) ) { - - $self->set($pre.'coord_auto', '') && next - if $self->get($pre.'latitude') && $self->get($pre.'longitude') - && ( $self->get($pre.'latitude') != $old->get($pre.'latitude') - || $self->get($pre.'longitude') != $old->get($pre.'longitude') - ); - - $self->set_coord($pre) - if $old->get($pre.'address1') ne $self->get($pre.'address1') - || $old->get($pre.'city') ne $self->get($pre.'city') - || $old->get($pre.'state') ne $self->get($pre.'state') - || $old->get($pre.'country') ne $self->get($pre.'country'); - - } + # should be unnecessary--geocode will default to null on new locations + #if ( $old->get('geocode') && $old->get('geocode') eq $self->get('geocode') + # && $conf->exists('enable_taxproducts') + # ) + #{ + # my $pre = ($conf->exists('tax-ship_address') && $self->ship_zip) + # ? 'ship_' : ''; + # $self->set('geocode', '') + # if $old->get($pre.'zip') ne $self->get($pre.'zip') + # && length($self->get($pre.'zip')) >= 10; + #} - unless ( $import ) { - $self->set_coord - if ! $self->coord_auto && ! $self->latitude && ! $self->longitude; - - $self->set_coord('ship_') - if $self->has_ship_address && ! $self->ship_coord_auto - && ! $self->ship_latitude && ! $self->ship_longitude; - } + # set_coord/coord_auto stuff is now handled by cust_location local($ignore_expired_card) = 1 if $old->payby =~ /^(CARD|DCRD)$/ @@ -1533,11 +1504,10 @@ sub replace { || $old->payby =~ /^(CHEK|DCHK)$/ && $self->payby =~ /^(CHEK|DCHK)$/ ) && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask ); - if ( $self->censustract ne '' and $self->censustract ne $old->censustract ) { - # update censusyear whenever tract code changes - $self->censusyear($conf->config('census_year')||'2012'); - } - + return "Invoicing locale is required" + if $old->locale + && ! $self->locale + && $conf->exists('cust_main-require_locale'); local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -1550,6 +1520,47 @@ sub replace { local $FS::UID::AutoCommit = 0; my $dbh = dbh; + for my $l (qw(bill_location ship_location)) { + my $old_loc = $old->$l; + my $new_loc = $self->$l; + + if ( !$new_loc->locationnum ) { + # changing location + # If the new location is all empty fields, or if it's identical to + # the old location in all fields, don't replace. + my @nonempty = grep { $new_loc->$_ } $self->location_fields; + next if !@nonempty; + my @unlike = grep { $new_loc->$_ ne $old_loc->$_ } $self->location_fields; + + if ( @unlike or $old_loc->disabled ) { + warn " changed $l fields: ".join(',',@unlike)."\n" + if $DEBUG; + $new_loc->set(custnum => $self->custnum); + + # insert it--the old location will be disabled later + my $error = $new_loc->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + } else { + # no fields have changed and $old_loc isn't disabled, so don't change it + next; + } + + } + elsif ( $new_loc->custnum ne $self->custnum or $new_loc->prospectnum ) { + $dbh->rollback if $oldAutoCommit; + return "$l belongs to customer ".$new_loc->custnum; + } + # else the new location belongs to this customer so we're good + + # set the foo_locationnum now that we have one. + $self->set($l.'num', $new_loc->locationnum); + + } #for $l + my $error = $self->SUPER::replace($old); if ( $error ) { @@ -1557,6 +1568,27 @@ sub replace { return $error; } + # now move packages to the new service location + $self->set('ship_location', ''); #flush cache + if ( $old->ship_locationnum and # should only be null during upgrade... + $old->ship_locationnum != $self->ship_locationnum ) { + $error = $old->ship_location->move_to($self->ship_location); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + # don't move packages based on the billing location, but + # disable it if it's no longer in use + if ( $old->bill_locationnum and + $old->bill_locationnum != $self->bill_locationnum ) { + $error = $old->bill_location->disable_if_unused; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + if ( @param && ref($param[0]) eq 'ARRAY' ) { # INVOICING_LIST_ARYREF my $invoicing_list = shift @param; $error = $self->check_invoicing_list( $invoicing_list ); @@ -1594,17 +1626,27 @@ sub replace { my $tax_exemption = delete $options{'tax_exemption'}; if ( $tax_exemption ) { + $tax_exemption = { map { $_ => '' } @$tax_exemption } + if ref($tax_exemption) eq 'ARRAY'; + my %cust_main_exemption = map { $_->taxname => $_ } qsearch('cust_main_exemption', { 'custnum' => $old->custnum } ); - foreach my $taxname ( @$tax_exemption ) { + foreach my $taxname ( keys %$tax_exemption ) { - next if delete $cust_main_exemption{$taxname}; + if ( $cust_main_exemption{$taxname} && + $cust_main_exemption{$taxname}->exempt_number eq $tax_exemption->{$taxname} + ) + { + delete $cust_main_exemption{$taxname}; + next; + } my $cust_main_exemption = new FS::cust_main_exemption { - 'custnum' => $self->custnum, - 'taxname' => $taxname, + 'custnum' => $self->custnum, + 'taxname' => $taxname, + 'exempt_number' => $tax_exemption->{$taxname}, }; my $error = $cust_main_exemption->insert; if ( $error ) { @@ -1648,24 +1690,7 @@ sub replace { } } - # FS::geocode_Mixin::after_replace ? - # though this will go away anyway once we move customer bill/service - # locations into cust_location - # We can trigger this on any address change--just have to make sure - # not to trigger it on itself. - if ( $conf->config('tax_district_method') and !$import - and ( $self->get('ship_address1') ne $old->get('ship_address1') - or $self->get('address1') ne $old->get('address1') ) ) { - my $queue = new FS::queue { - 'job' => 'FS::geocode_Mixin::process_district_update', - 'custnum' => $self->custnum, - }; - my $error = $queue->insert( ref($self), $self->custnum ); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "queueing tax district update: $error"; - } - } + # tax district update in cust_location # cust_main exports! @@ -1710,16 +1735,14 @@ sub queue_fuzzyfiles_update { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $queue = new FS::queue { 'job' => 'FS::cust_main::Search::append_fuzzyfiles' }; - my $error = $queue->insert( map $self->getfield($_), @FS::cust_main::Search::fuzzyfields ); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "queueing job (transaction rolled back): $error"; - } - - if ( $self->ship_last ) { - $queue = new FS::queue { 'job' => 'FS::cust_main::Search::append_fuzzyfiles' }; - $error = $queue->insert( map $self->getfield("ship_$_"), @FS::cust_main::Search::fuzzyfields ); + my @locations = $self->bill_location; + push @locations, $self->ship_location if $self->has_ship_address; + foreach my $location (@locations) { + my $queue = new FS::queue { + 'job' => 'FS::cust_main::Search::append_fuzzyfiles' + }; + my @args = map $location->get($_), @FS::cust_main::Search::fuzzyfields; + my $error = $queue->insert( @args ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return "queueing job (transaction rolled back): $error"; @@ -1750,6 +1773,8 @@ sub check { || $self->ut_number('agentnum') || $self->ut_textn('agent_custid') || $self->ut_number('refnum') + || $self->ut_foreign_key('bill_locationnum', 'cust_location','locationnum') + || $self->ut_foreign_key('ship_locationnum', 'cust_location','locationnum') || $self->ut_foreign_keyn('classnum', 'cust_class', 'classnum') || $self->ut_textn('custbatch') || $self->ut_name('last') @@ -1757,34 +1782,20 @@ sub check { || $self->ut_snumbern('birthdate') || $self->ut_snumbern('signupdate') || $self->ut_textn('company') - || $self->ut_text('address1') - || $self->ut_textn('address2') - || $self->ut_text('city') - || $self->ut_textn('county') - || $self->ut_textn('state') - || $self->ut_country('country') - || $self->ut_coordn('latitude') - || $self->ut_coordn('longitude') - || $self->ut_enum('coord_auto', [ '', 'Y' ]) - || $self->ut_numbern('censusyear') || $self->ut_anything('comments') || $self->ut_numbern('referral_custnum') || $self->ut_textn('stateid') || $self->ut_textn('stateid_state') || $self->ut_textn('invoice_terms') - || $self->ut_alphan('geocode') - || $self->ut_alphan('district') || $self->ut_floatn('cdr_termination_percentage') || $self->ut_floatn('credit_limit') || $self->ut_numbern('billday') || $self->ut_enum('edit_subject', [ '', 'Y' ] ) || $self->ut_enum('calling_list_exempt', [ '', 'Y' ] ) + || $self->ut_enum('invoice_noemail', [ '', 'Y' ] ) || $self->ut_enum('locale', [ '', FS::Locales->locales ]) ; - $self->set_coord - unless $import || ($self->latitude && $self->longitude); - #barf. need message catalogs. i18n. etc. $error .= "Please select an advertising source." if $error =~ /^Illegal or empty \(numeric\) refnum: /; @@ -1800,13 +1811,6 @@ sub check { unless ! $self->referral_custnum || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } ); - if ( $self->censustract ne '' ) { - $self->censustract =~ /^\s*(\d{9})\.?(\d{2})\s*$/ - or return "Illegal census tract: ". $self->censustract; - - $self->censustract("$1.$2"); - } - if ( $self->ss eq '' ) { $self->ss(''); } else { @@ -1817,23 +1821,7 @@ sub check { $self->ss("$1-$2-$3"); } - -# bad idea to disable, causes billing to fail because of no tax rates later -# except we don't fail any more - unless ( $import ) { - unless ( qsearch('cust_main_county', { - 'country' => $self->country, - 'state' => '', - } ) ) { - return "Unknown state/county/country: ". - $self->state. "/". $self->county. "/". $self->country - unless qsearch('cust_main_county',{ - 'state' => $self->state, - 'county' => $self->county, - 'country' => $self->country, - } ); - } - } + # cust_main_county verification now handled by cust_location check $error = $self->ut_phonen('daytime', $self->country) @@ -1843,12 +1831,8 @@ sub check { ; return $error if $error; - unless ( $ignore_illegal_zip ) { - $error = $self->ut_zip('zip', $self->country); - return $error if $error; - } - if ( $conf->exists('cust_main-require_phone', $self->agentnum) + && ! $import && ! length($self->daytime) && ! length($self->night) && ! length($self->mobile) ) { @@ -1867,71 +1851,7 @@ sub check { } - if ( $self->has_ship_address - && scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") } - $self->addr_fields ) - ) - { - my $error = - $self->ut_name('ship_last') - || $self->ut_name('ship_first') - || $self->ut_textn('ship_company') - || $self->ut_text('ship_address1') - || $self->ut_textn('ship_address2') - || $self->ut_text('ship_city') - || $self->ut_textn('ship_county') - || $self->ut_textn('ship_state') - || $self->ut_country('ship_country') - || $self->ut_coordn('ship_latitude') - || $self->ut_coordn('ship_longitude') - || $self->ut_enum('ship_coord_auto', [ '', 'Y' ] ) - ; - return $error if $error; - - $self->set_coord('ship_') - unless $import || ($self->ship_latitude && $self->ship_longitude); - - #false laziness with above - unless ( qsearchs('cust_main_county', { - 'country' => $self->ship_country, - 'state' => '', - } ) ) { - return "Unknown ship_state/ship_county/ship_country: ". - $self->ship_state. "/". $self->ship_county. "/". $self->ship_country - unless qsearch('cust_main_county',{ - 'state' => $self->ship_state, - 'county' => $self->ship_county, - 'country' => $self->ship_country, - } ); - } - #eofalse - - $error = - $self->ut_phonen('ship_daytime', $self->ship_country) - || $self->ut_phonen('ship_night', $self->ship_country) - || $self->ut_phonen('ship_fax', $self->ship_country) - || $self->ut_phonen('ship_mobile', $self->ship_country) - ; - return $error if $error; - - unless ( $ignore_illegal_zip ) { - $error = $self->ut_zip('ship_zip', $self->ship_country); - return $error if $error; - } - return "Unit # is required." - if $self->ship_address2 =~ /^\s*$/ - && $conf->exists('cust_main-require_address2'); - - } else { # ship_ info eq billing info, so don't store dup info in database - - $self->setfield("ship_$_", '') - foreach $self->addr_fields; - - return "Unit # is required." - if $self->address2 =~ /^\s*$/ - && $conf->exists('cust_main-require_address2'); - - } + #ship_ fields are gone #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/ # or return "Illegal payby: ". $self->payby; @@ -1957,7 +1877,9 @@ sub check { # check the credit card. my $check_payinfo = ! $self->is_encrypted($self->payinfo); - if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) { + # Need some kind of global flag to accept invalid cards, for testing + # on scrubbed data. + if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) { my $payinfo = $self->payinfo; $payinfo =~ s/\D//g; @@ -2139,6 +2061,11 @@ sub check { $self->payname($1); } + return "Please select an invoicing locale" + if ! $self->locale + && ! $self->custnum + && $conf->exists('cust_main-require_locale'); + foreach my $flag (qw( tax spool_cdr squelch_cdr archived email_csv_cdr )) { $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag(); $self->$flag($1); @@ -2174,7 +2101,7 @@ Returns true if this customer record has a separate shipping address. sub has_ship_address { my $self = shift; - scalar( grep { $self->getfield("ship_$_") ne '' } $self->addr_fields ); + $self->bill_locationnum != $self->ship_locationnum; } =item location_hash @@ -2185,6 +2112,11 @@ shipping address is used if present. =cut +sub location_hash { + my $self = shift; + $self->ship_location->location_hash; +} + =item cust_location Returns all locations (see L<FS::cust_location>) for this customer. @@ -2193,7 +2125,8 @@ Returns all locations (see L<FS::cust_location>) for this customer. sub cust_location { my $self = shift; - qsearch('cust_location', { 'custnum' => $self->custnum } ); + qsearch('cust_location', { 'custnum' => $self->custnum, + 'prospectnum' => '' } ); } =item cust_contact @@ -2590,6 +2523,8 @@ sub batch_card { $options{$_} = '' unless exists($options{$_}); } + my $loc = $self->bill_location; + my $cust_pay_batch = new FS::cust_pay_batch ( { 'batchnum' => $pay_batch->batchnum, 'invnum' => $invnum || 0, # is there a better value? @@ -2599,16 +2534,16 @@ sub batch_card { 'custnum' => $self->custnum, 'last' => $self->getfield('last'), 'first' => $self->getfield('first'), - 'address1' => $options{address1} || $self->address1, - 'address2' => $options{address2} || $self->address2, - 'city' => $options{city} || $self->city, - 'state' => $options{state} || $self->state, - 'zip' => $options{zip} || $self->zip, - 'country' => $options{country} || $self->country, - 'payby' => $options{payby} || $self->payby, - 'payinfo' => $options{payinfo} || $self->payinfo, - 'exp' => $options{paydate} || $self->paydate, - 'payname' => $options{payname} || $self->payname, + 'address1' => $options{address1} || $loc->address1, + 'address2' => $options{address2} || $loc->address2, + 'city' => $options{city} || $loc->city, + 'state' => $options{state} || $loc->state, + 'zip' => $options{zip} || $loc->zip, + 'country' => $options{country} || $loc->country, + 'payby' => $options{payby} || $loc->payby, + 'payinfo' => $options{payinfo} || $loc->payinfo, + 'exp' => $options{paydate} || $loc->paydate, + 'payname' => $options{payname} || $loc->payname, 'amount' => $amount, # consolidating } ); @@ -3000,7 +2935,8 @@ sub payment_info { $return{payname} = $self->payname || ( $self->first. ' '. $self->get('last') ); - $return{$_} = $self->get($_) for qw(address1 address2 city state zip); + $return{$_} = $self->bill_location->$_ + for qw(address1 address2 city state zip); $return{payby} = $self->payby; $return{stateid_state} = $self->stateid_state; @@ -4010,6 +3946,27 @@ sub name { $name; } +=item service_contact + +Returns the L<FS::contact> object for this customer that has the 'Service' +contact class, or undef if there is no such contact. Deprecated; don't use +this in new code. + +=cut + +sub service_contact { + my $self = shift; + if ( !exists($self->{service_contact}) ) { + my $classnum = $self->scalar_sql( + 'SELECT classnum FROM contact_class WHERE classname = \'Service\'' + ) || 0; #if it's zero, qsearchs will return nothing + $self->{service_contact} = qsearchs('contact', { + 'classnum' => $classnum, 'custnum' => $self->custnum + }) || undef; + } + $self->{service_contact}; +} + =item ship_name Returns a name string for this (service/shipping) contact, either @@ -4019,13 +3976,10 @@ Returns a name string for this (service/shipping) contact, either sub ship_name { my $self = shift; - if ( $self->get('ship_last') ) { - my $name = $self->ship_contact; - $name = $self->ship_company. " ($name)" if $self->ship_company; - $name; - } else { - $self->name; - } + + my $name = $self->ship_contact; + $name = $self->company. " ($name)" if $self->company; + $name; } =item name_short @@ -4048,13 +4002,9 @@ or "First Last". sub ship_name_short { my $self = shift; - if ( $self->get('ship_last') ) { - $self->ship_company !~ /^\s*$/ - ? $self->ship_company - : $self->ship_contact_firstlast; - } else { - $self->name_company_or_firstlast; - } + $self->service_contact + ? $self->ship_contact_firstlast + : $self->name_short } =item contact @@ -4076,9 +4026,8 @@ Returns this customer's full (shipping) contact name only, "Last, First" sub ship_contact { my $self = shift; - $self->get('ship_last') - ? $self->get('ship_last'). ', '. $self->ship_first - : $self->contact; + my $contact = $self->service_contact || $self; + $contact->get('last') . ', ' . $contact->get('first'); } =item contact_firstlast @@ -4100,9 +4049,8 @@ Returns this customer's full (shipping) contact name only, "First Last". sub ship_contact_firstlast { my $self = shift; - $self->get('ship_last') - ? $self->first. ' '. $self->get('ship_last') - : $self->contact_firstlast; + my $contact = $self->service_contact || $self; + $contact->get('first') . ' '. $contact->get('last'); } =item country_full @@ -5077,39 +5025,71 @@ sub process_censustract_update { return; } +#starting to take quite a while for big dbs +# - 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 +# - otaker upgrade? journal and call it good? (double check to make sure +# we're not still setting otaker here) +# +#only going to get worse with new location stuff... + sub _upgrade_data { #class method my ($class, %opts) = @_; my @statements = ( 'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL', - 'UPDATE cust_main SET signupdate = (SELECT signupdate FROM h_cust_main WHERE signupdate IS NOT NULL AND h_cust_main.custnum = cust_main.custnum ORDER BY historynum DESC LIMIT 1) WHERE signupdate IS NULL', ); - # fix yyyy-m-dd formatted paydates - if ( driver_name =~ /^mysql/i ) { + + #this seems to be the only expensive one.. why does it take so long? + unless ( FS::upgrade_journal->is_done('cust_main__signupdate') ) { push @statements, - "UPDATE cust_main SET paydate = CONCAT( SUBSTRING(paydate FROM 1 FOR 5), '0', SUBSTRING(paydate FROM 6) ) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'"; + 'UPDATE cust_main SET signupdate = (SELECT signupdate FROM h_cust_main WHERE signupdate IS NOT NULL AND h_cust_main.custnum = cust_main.custnum ORDER BY historynum DESC LIMIT 1) WHERE signupdate IS NULL'; + FS::upgrade_journal->set_done('cust_main__signupdate'); } - else { # the SQL standard - push @statements, - "UPDATE cust_main SET paydate = SUBSTRING(paydate FROM 1 FOR 5) || '0' || SUBSTRING(paydate FROM 6) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'"; + + unless ( FS::upgrade_journal->is_done('cust_main__paydate') ) { + + # fix yyyy-m-dd formatted paydates + if ( driver_name =~ /^mysql/i ) { + push @statements, + "UPDATE cust_main SET paydate = CONCAT( SUBSTRING(paydate FROM 1 FOR 5), '0', SUBSTRING(paydate FROM 6) ) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'"; + } else { # the SQL standard + push @statements, + "UPDATE cust_main SET paydate = SUBSTRING(paydate FROM 1 FOR 5) || '0' || SUBSTRING(paydate FROM 6) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'"; + } + FS::upgrade_journal->set_done('cust_main__paydate'); } - push @statements, #fix the weird BILL with a cc# in payinfo problem - #DCRD to be safe - "UPDATE cust_main SET payby = 'DCRD' WHERE payby = 'BILL' and length(payinfo) = 16 and payinfo ". regexp_sql. q( '^[0-9]*$' ); + unless ( FS::upgrade_journal->is_done('cust_main__payinfo') ) { + push @statements, #fix the weird BILL with a cc# in payinfo problem + #DCRD to be safe + "UPDATE cust_main SET payby = 'DCRD' WHERE payby = 'BILL' and length(payinfo) = 16 and payinfo ". regexp_sql. q( '^[0-9]*$' ); + + FS::upgrade_journal->set_done('cust_main__payinfo'); + + } + + my $t = time; foreach my $sql ( @statements ) { my $sth = dbh->prepare($sql) or die dbh->errstr; $sth->execute or die $sth->errstr; + #warn ( (time - $t). " seconds\n" ); + #$t = time; } local($ignore_expired_card) = 1; - local($ignore_illegal_zip) = 1; 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); + } =back diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index ca8d9960c..e7b953060 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -721,6 +721,11 @@ jurisdictions (i.e. Texas) have tax exemptions which are date sensitive. sub calculate_taxes { my ($self, $cust_bill_pkg, $taxlisthash, $invoice_time) = @_; + # $taxlisthash is a hashref + # keys are identifiers, values are arrayrefs + # each arrayref starts with a tax object (cust_main_county or tax_rate) + # then any cust_bill_pkg objects the tax applies to + local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG; warn "$me calculate_taxes\n" @@ -746,9 +751,15 @@ sub calculate_taxes { my %tax_rate_location = (); foreach my $tax ( keys %$taxlisthash ) { + # $tax is a tax identifier my $tax_object = shift @{ $taxlisthash->{$tax} }; + # $tax_object is a cust_main_county or tax_rate + # (with pkgnum and locationnum set) + # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg objects warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2; warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2; + # taxline calculates the tax on all cust_bill_pkgs in the + # first (arrayref) argument my $hashref_or_error = $tax_object->taxline( $taxlisthash->{$tax}, 'custnum' => $self->custnum, @@ -767,8 +778,10 @@ sub calculate_taxes { $tax{ $tax } += $amount; + # link records between cust_main_county/tax_rate and cust_location $tax_location{ $tax } ||= []; - if ( $tax_object->get('pkgnum') || $tax_object->get('locationnum') ) { + $tax_rate_location{ $tax } ||= []; + if ( ref($tax_object) eq 'FS::cust_main_county' ) { push @{ $tax_location{ $tax } }, { 'taxnum' => $tax_object->taxnum, @@ -778,9 +791,7 @@ sub calculate_taxes { 'amount' => sprintf('%.2f', $amount ), }; } - - $tax_rate_location{ $tax } ||= []; - if ( ref($tax_object) eq 'FS::tax_rate' ) { + elsif ( ref($tax_object) eq 'FS::tax_rate' ) { my $taxratelocationnum = $tax_object->tax_rate_location->taxratelocationnum; push @{ $tax_rate_location{ $tax } }, @@ -952,7 +963,6 @@ sub _make_lines { # bill recurring fee ### - #XXX unit stuff here too my $recur = 0; my $unitrecur = 0; my @recur_discounts = (); @@ -1011,6 +1021,9 @@ sub _make_lines { return "$@ running $method for $cust_pkg\n" if ( $@ ); + #base_cancel??? + $unitrecur = $cust_pkg->part_pkg->base_recur || $recur; #XXX uuh + if ( $increment_next_bill ) { my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0); @@ -1206,21 +1219,12 @@ sub _handle_taxes { } else { my @loc_keys = qw( district city county state country ); - my %taxhash; - if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) { - my $cust_location = $cust_pkg->cust_location; - %taxhash = map { $_ => $cust_location->$_() } @loc_keys; - } else { - my $prefix = - ( $conf->exists('tax-ship_address') && length($self->ship_last) ) - ? 'ship_' - : ''; - %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys; - } + my $location = $cust_pkg->tax_location; + my %taxhash = map { $_ => $location->$_ } @loc_keys; $taxhash{'taxclass'} = $part_pkg->taxclass; - my @taxes = (); + my @taxes = (); # entries are cust_main_county objects my %taxhash_elim = %taxhash; my @elim = qw( district city county state ); do { @@ -1243,11 +1247,13 @@ sub _handle_taxes { @taxes if $self->cust_main_exemption; #just to be safe - if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) { - foreach (@taxes) { - $_->set('pkgnum', $cust_pkg->pkgnum ); - $_->set('locationnum', $cust_pkg->locationnum ); - } + # all packages now have a locationnum and should get a + # cust_bill_pkg_tax_location record. The tax_locationnum + # may be the package's locationnum, or the customer's bill + # or service location. + foreach (@taxes) { + $_->set('pkgnum', $cust_pkg->pkgnum); + $_->set('locationnum', $cust_pkg->tax_locationnum); } $taxes{''} = [ @taxes ]; @@ -1274,17 +1280,27 @@ sub _handle_taxes { my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate; foreach my $key (keys %tax_cust_bill_pkg) { + # $key is "setup", "recur", or a usage class name. ('' is a usage class.) + # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of + # the line item. + # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that + # apply to $key-class charges. my @taxes = @{ $taxes{$key} || [] }; my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key}; my %localtaxlisthash = (); foreach my $tax ( @taxes ) { + # this is the tax identifier, not the taxname my $taxname = ref( $tax ). ' '. $tax->taxnum; # $taxname .= ' pkgnum'. $cust_pkg->pkgnum. # ' locationnum'. $cust_pkg->locationnum # if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum; + # $taxlisthash: keys are "setup", "recur", and usage classes + # values are arrayrefs, first the tax object (cust_main_county + # or tax_rate) and then any cust_bill_pkg objects that the + # tax applies to $taxlisthash->{ $taxname } ||= [ $tax ]; push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg; @@ -1525,17 +1541,23 @@ sub retry_realtime { cust_bill_batch ); - my $is_realtime_event = ' ( '. join(' OR ', map "part_event.action = '$_'", - @realtime_events - ). - ' ) '; + my $is_realtime_event = + ' part_event.action IN ( '. + join(',', map "'$_'", @realtime_events ). + ' ) '; + + my $batch_or_statustext = + "( part_event.action = 'cust_bill_batch' + OR ( statustext IS NOT NULL AND statustext != '' ) + )"; + my @cust_event = qsearch({ 'table' => 'cust_event', 'select' => 'cust_event.*', 'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join", 'hashref' => { 'status' => 'done' }, - 'extra_sql' => " AND statustext IS NOT NULL AND statustext != '' ". + 'extra_sql' => " AND $batch_or_statustext ". " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1" }); diff --git a/FS/FS/cust_main/Import.pm b/FS/FS/cust_main/Import.pm index 7f5a3f009..6681f9ec2 100644 --- a/FS/FS/cust_main/Import.pm +++ b/FS/FS/cust_main/Import.pm @@ -13,6 +13,7 @@ use FS::cust_main; use FS::svc_acct; use FS::svc_external; use FS::svc_phone; +use FS::svc_hardware; use FS::part_referral; $DEBUG = 0; @@ -197,6 +198,22 @@ sub batch_import { push @fields, map "svc_phone.$_", qw( countrycode phonenum sip_password pin) if $format eq 'svc_external_svc_phone'; $payby = 'BILL'; + } elsif ( $format eq 'birthdates-acct_phone_hardware') { + @fields = qw( agent_custid refnum + last first company address1 address2 city state zip country + daytime night + ship_last ship_first ship_company ship_address1 ship_address2 + ship_city ship_state ship_zip ship_country + birthdate spouse_birthdate + payinfo paycvv paydate + invoicing_list + cust_pkg.pkgpart cust_pkg.bill + svc_acct.username svc_acct._password + ); + push @fields, map "svc_phone.$_", qw(countrycode phonenum sip_password pin); + push @fields, map "svc_hardware.$_", qw(typenum ip_addr hw_addr serial); + + $payby = 'BILL'; } else { die "unknown format $format"; } @@ -314,7 +331,11 @@ sub batch_import { } elsif ( $field =~ /^svc_phone\.(countrycode|phonenum|sip_password|pin)$/ ) { $svc_x{$1} = shift @columns; - + + } elsif ( $field =~ /^svc_hardware\.(typenum|ip_addr|hw_addr|serial)$/ ) { + + $svc_x{$1} = shift @columns; + } else { #refnum interception @@ -353,6 +374,9 @@ sub batch_import { } } + $cust_main{$_} = parse_datetime($cust_main{$_}) + foreach grep $cust_main{$_}, qw( birthdate spouse_birthdate ); + my $invoicing_list = $cust_main{'invoicing_list'} ? [ delete $cust_main{'invoicing_list'} ] : []; @@ -387,11 +411,19 @@ sub batch_import { if ( $svc_x{'countrycode'} || $svc_x{'phonenum'} ) { $svc_phone = FS::svc_phone->new( { map { $_ => delete($svc_x{$_}) } - qw( countrycode phonenum sip_password pin) + qw( countrycode phonenum sip_password pin ) } ); } - if ( $svcdb || $svc_phone ) { + my $svc_hardware = ''; + if ( $svc_x{'typenum'} ) { + $svc_hardware = FS::svc_hardware->new( { + map { $_ => delete($svc_x{$_}) } + qw( typenum ip_addr hw_addr serial ) + } ); + } + + if ( $svcdb || $svc_phone || $svc_hardware ) { my $part_pkg = $cust_pkg->part_pkg; unless ( $part_pkg ) { $dbh->rollback if $oldAutoCommit; @@ -406,6 +438,11 @@ sub batch_import { $svc_phone->svcpart( $part_pkg->svcpart_unique_svcdb('svc_phone') ); push @svc_x, $svc_phone; } + if ( $svc_hardware ) { + $svc_hardware->svcpart( $part_pkg->svcpart_unique_svcdb('svc_hardware') ); + push @svc_x, $svc_hardware; + } + } $hash{$cust_pkg} = \@svc_x; diff --git a/FS/FS/cust_main/Location.pm b/FS/FS/cust_main/Location.pm new file mode 100644 index 000000000..8e30bb65b --- /dev/null +++ b/FS/FS/cust_main/Location.pm @@ -0,0 +1,252 @@ +package FS::cust_main::Location; + +use strict; +use vars qw( $DEBUG $me @location_fields ); +use FS::Record qw(qsearch qsearchs); +use FS::UID qw(dbh); +use FS::cust_location; + +use Carp qw(carp); + +$DEBUG = 0; +$me = '[FS::cust_main::Location]'; + +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 ); + + foreach my $f (@location_fields) { + *{"FS::cust_main::Location::$f"} = sub { + carp "WARNING: tried to set cust_main.$f with accessor" if (@_ > 1); + shift->bill_location->$f + }; + *{"FS::cust_main::Location::ship_$f"} = sub { + carp "WARNING: tried to set cust_main.ship_$f with accessor" if (@_ > 1); + shift->ship_location->$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 (/^(ship_)?($field)$/, @location_fields) ) { + carp "WARNING: tried to get() location field $field"; + $self->$field; + } + $self->FS::Record::get($field); +} + +=head1 NAME + +FS::cust_main::Location - Location-related methods for cust_main + +=head1 DESCRIPTION + +These methods are available on FS::cust_main objects; + +=head1 METHODS + +=over 4 + +=item bill_location + +Returns an L<FS::cust_location> object for the customer's billing address. + +=cut + +sub bill_location { + my $self = shift; + $self->hashref->{bill_location} + ||= FS::cust_location->by_key($self->bill_locationnum); +} + +=item ship_location + +Returns an L<FS::cust_location> object for the customer's service address. + +=cut + +sub ship_location { + my $self = shift; + $self->hashref->{ship_location} + ||= FS::cust_location->by_key($self->ship_locationnum); +} + +=item location TYPE + +An alternative way of saying "bill_location or ship_location, depending on +if TYPE is 'bill' or 'ship'". + +=cut + +sub location { + my $self = shift; + return $self->bill_location if $_[0] eq 'bill'; + return $self->ship_location if $_[0] eq 'ship'; + die "bad location type '$_[0]'"; +} + +=back + +=head1 CLASS METHODS + +=over 4 + +=item location_fields + +Returns a list of fields found in the location objects. All of these fields +can be read (but not written) by calling them as methods on the +L<FS::cust_main> object (prefixed with 'ship_' for the service address +fields). + +=cut + +sub location_fields { @location_fields } + +sub _upgrade_data { + my $class = shift; + eval "use FS::contact; + use FS::contact_class; + use FS::contact_phone; + use FS::phone_type"; + + local $FS::cust_location::import = 1; + local $DEBUG = 0; + my $error; + + # Step 0: set up contact classes and phone types + my $service_contact_class = + qsearchs('contact_class', { classname => 'Service'}) + || new FS::contact_class { classname => 'Service'}; + + if ( !$service_contact_class->classnum ) { + $error = $service_contact_class->insert; + die "error creating contact class for Service: $error" if $error; + } + my %phone_type = ( # fudge slightly + daytime => 'Work', + night => 'Home', + mobile => 'Mobile', + fax => 'Fax' + ); + my $w = 10; + foreach (keys %phone_type) { + $phone_type{$_} = qsearchs('phone_type', { typename => $phone_type{$_}}) + || new FS::phone_type { typename => $phone_type{$_}, + weight => $w }; + # just in case someone still doesn't have these + if ( !$phone_type{$_}->phonetypenum ) { + $error = $phone_type{$_}->insert; + die "error creating phone type '$_': $error" if $error; + } + } + + foreach my $cust_main (qsearch('cust_main', { bill_locationnum => '' })) { + # Step 1: extract billing and service addresses into cust_location + my $custnum = $cust_main->custnum; + my $bill_location = FS::cust_location->new( + { + custnum => $custnum, + map { $_ => $cust_main->get($_) } location_fields() + } + ); + $error = $bill_location->insert; + die "error migrating billing address for customer $custnum: $error" + if $error; + + $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() + } + ); + $error = $ship_location->insert; + die "error migrating service address for customer $custnum: $error" + if $error; + + $cust_main->set(ship_locationnum => $ship_location->locationnum); + + # Step 2: Extract shipping address contact fields into contact + my %unlike = map { $_ => 1 } + grep { $cust_main->get($_) ne $cust_main->get("ship_$_") } + qw( last first company daytime night fax mobile ); + + if ( %unlike ) { + # then there IS a service contact + my $contact = FS::contact->new({ + 'custnum' => $custnum, + 'classnum' => $service_contact_class->classnum, + 'locationnum' => $ship_location->locationnum, + 'last' => $cust_main->get('ship_last'), + 'first' => $cust_main->get('ship_first'), + }); + if ( $unlike{'company'} ) { + # there's no contact.company field, but keep a record of it + $contact->set(comment => 'Company: '.$cust_main->get('ship_company')); + } + $error = $contact->insert; + die "error migrating service contact for customer $custnum: $error" + if $error; + + foreach ( grep { $unlike{$_} } qw( daytime night fax mobile ) ) { + my $phone = $cust_main->get("ship_$_"); + next if !$phone; + my $contact_phone = FS::contact_phone->new({ + 'contactnum' => $contact->contactnum, + 'phonetypenum' => $phone_type{$_}->phonetypenum, + FS::contact::_parse_phonestring( $phone ) + }); + $error = $contact_phone->insert; + # die "whose responsible this" + die "error migrating service contact phone for customer $custnum: $error" + if $error; + $cust_main->set("ship_$_" => ''); + } + + $cust_main->set("ship_$_" => '') foreach qw(last first company); + } #if %unlike + } #if ship_address1 + else { + $cust_main->set(ship_locationnum => $bill_location->locationnum); + } + + # Step 3: Wipe the migrated fields and update the cust_main + + $cust_main->set("ship_$_" => '') foreach location_fields(); + $cust_main->set($_ => '') foreach location_fields(); + + $error = $cust_main->replace; + die "error migrating addresses for customer $custnum: $error" + if $error; + + # Step 4: set packages at the "default service location" to ship_location + foreach my $cust_pkg ( + qsearch('cust_pkg', { custnum => $custnum, locationnum => '' }) + ) { + # not a location change + $cust_pkg->set('locationnum', $cust_main->ship_locationnum); + $error = $cust_pkg->replace; + die "error migrating package ".$cust_pkg->pkgnum.": $error" + if $error; + } + + } #foreach $cust_main +} + +=back + +=cut + +1; diff --git a/FS/FS/cust_main/Packages.pm b/FS/FS/cust_main/Packages.pm index 06331d3df..957043a84 100644 --- a/FS/FS/cust_main/Packages.pm +++ b/FS/FS/cust_main/Packages.pm @@ -40,7 +40,8 @@ FS::cust_pkg object =item cust_location -Optional FS::cust_location object +Optional FS::cust_location object. If not specified, the customer's +ship_location will be used. =item svcs @@ -105,6 +106,9 @@ sub order_pkg { } $cust_pkg->locationnum($opt->{'cust_location'}->locationnum); } + else { + $cust_pkg->locationnum($self->ship_locationnum); + } $cust_pkg->custnum( $self->custnum ); @@ -351,6 +355,7 @@ Returns all suspended packages (see L<FS::cust_pkg>) for this customer. sub suspended_pkgs { my $self = shift; + return $self->num_suspended_pkgs unless wantarray; grep { $_->susp } $self->ncancelled_pkgs; } @@ -377,6 +382,7 @@ this customer. sub unsuspended_pkgs { my $self = shift; + return $self->num_unsuspended_pkgs unless wantarray; grep { ! $_->susp } $self->ncancelled_pkgs; } @@ -438,6 +444,16 @@ sub num_ncancelled_pkgs { shift->num_pkgs("( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )"); } +sub num_suspended_pkgs { + shift->num_pkgs(" ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 ) + AND cust_pkg.susp IS NOT NULL AND cust_pkg.susp != 0 "); +} + +sub num_unsuspended_pkgs { + shift->num_pkgs(" ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 ) + AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 ) "); +} + sub num_pkgs { my( $self ) = shift; my $sql = scalar(@_) ? shift : ''; diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index 31b89cd31..b528a689c 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -85,8 +85,7 @@ sub smart_search { 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ). ' ( '. join(' OR ', map "$_ = '$phonen'", - qw( daytime night fax - ship_daytime ship_night ship_fax ) + qw( daytime night fax ) ). ' ) '. " AND $agentnums_sql", #agent virtualization @@ -101,8 +100,7 @@ sub smart_search { 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ). ' ( '. join(' OR ', map "$_ LIKE '$phonen\%'", - qw( daytime night - ship_daytime ship_night ) + qw( daytime night ) ). ' ) '. " AND $agentnums_sql", #agent virtualization @@ -142,10 +140,12 @@ sub smart_search { my $num = $1; if ( $num =~ /^(\d+)$/ && $num <= 2147483647 ) { #need a bigint custnum? wow + my $agent_custid_null = $conf->exists('cust_main-default_agent_custid') + ? ' AND agent_custid IS NULL ' : ''; push @cust_main, qsearch( { 'table' => 'cust_main', 'hashref' => { 'custnum' => $num, %options }, - 'extra_sql' => " AND $agentnums_sql", #agent virtualization + 'extra_sql' => " AND $agentnums_sql $agent_custid_null", } ); } @@ -175,16 +175,17 @@ sub smart_search { if ( $conf->exists('address1-search') ) { my $len = length($num); $num = lc($num); - foreach my $prefix ( '', 'ship_' ) { - push @cust_main, qsearch( { - 'table' => 'cust_main', - 'hashref' => { %options, }, - 'extra_sql' => - ( keys(%options) ? ' AND ' : ' WHERE ' ). - " LOWER(SUBSTRING(${prefix}address1 FROM 1 FOR $len)) = '$num' ". - " AND $agentnums_sql", - } ); - } + # probably the Right Thing: return customers that have any associated + # locations matching the string, not just bill/ship location + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'addl_from' => ' JOIN cust_location USING (custnum) ', + 'hashref' => { %options, }, + 'extra_sql' => + ( keys(%options) ? ' AND ' : ' WHERE ' ). + " LOWER(SUBSTRING(cust_location.address1 FROM 1 FOR $len)) = '$num' ". + " AND $agentnums_sql", + } ); } } elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) { @@ -196,20 +197,19 @@ sub smart_search { #so just do an exact search (but case-insensitive, so USPS standardization #doesn't throw a wrench in the works) - foreach my $prefix ( '', 'ship_' ) { - push @cust_main, qsearch( { + push @cust_main, qsearch( { 'table' => 'cust_main', 'hashref' => { %options }, 'extra_sql' => - ( keys(%options) ? ' AND ' : ' WHERE ' ). - join(' AND ', - " LOWER(${prefix}first) = ". dbh->quote(lc($first)), - " LOWER(${prefix}last) = ". dbh->quote(lc($last)), - " LOWER(${prefix}company) = ". dbh->quote(lc($company)), - $agentnums_sql, - ), - } ); - } + ( keys(%options) ? ' AND ' : ' WHERE ' ). + join(' AND ', + " LOWER(first) = ". dbh->quote(lc($first)), + " LOWER(last) = ". dbh->quote(lc($last)), + " LOWER(company) = ". dbh->quote(lc($company)), + $agentnums_sql, + ), + } ), + #contacts? } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search # try (ship_){last,company} @@ -247,16 +247,14 @@ sub smart_search { #exact my $sql = scalar(keys %options) ? ' AND ' : ' WHERE '; - $sql .= " - ( ( LOWER(last) = $q_last AND LOWER(first) = $q_first ) - OR ( LOWER(ship_last) = $q_last AND LOWER(ship_first) = $q_first ) - )"; + $sql .= "( LOWER(cust_main.last) = $q_last AND LOWER(cust_main.first) = $q_first )"; push @cust_main, qsearch( { 'table' => 'cust_main', 'hashref' => \%options, 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization } ); + #contacts? # or it just be something that was typed in... (try that in a sec) @@ -268,11 +266,13 @@ sub smart_search { my $sql = scalar(keys %options) ? ' AND ' : ' WHERE '; $sql .= " ( LOWER(last) = $q_value OR LOWER(company) = $q_value - OR LOWER(ship_last) = $q_value - OR LOWER(ship_company) = $q_value "; - $sql .= " OR LOWER(address1) = $q_value - OR LOWER(ship_address1) = $q_value + #yes, it's a kludge + $sql .= " OR EXISTS( + SELECT 1 FROM cust_location + WHERE LOWER(cust_location.address1) = $q_value + AND cust_location.custnum = cust_main.custnum + ) " if $conf->exists('address1-search'); $sql .= " )"; @@ -294,32 +294,21 @@ sub smart_search { my @hashrefs = ( { 'company' => { op=>'ILIKE', value=>"%$value%" }, }, - { 'ship_company' => { op=>'ILIKE', value=>"%$value%" }, }, ); if ( $first && $last ) { + #contacts? ship_first/ship_last are gone push @hashrefs, { 'first' => { op=>'ILIKE', value=>"%$first%" }, 'last' => { op=>'ILIKE', value=>"%$last%" }, }, - { 'ship_first' => { op=>'ILIKE', value=>"%$first%" }, - 'ship_last' => { op=>'ILIKE', value=>"%$last%" }, - }, ; } else { push @hashrefs, { 'last' => { op=>'ILIKE', value=>"%$value%" }, }, - { 'ship_last' => { op=>'ILIKE', value=>"%$value%" }, }, - ; - } - - if ( $conf->exists('address1-search') ) { - push @hashrefs, - { 'address1' => { op=>'ILIKE', value=>"%$value%" }, }, - { 'ship_address1' => { op=>'ILIKE', value=>"%$value%" }, }, ; } @@ -335,27 +324,38 @@ sub smart_search { } + if ( $conf->exists('address1-search') ) { + + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'addl_from' => 'JOIN cust_location USING (custnum)', + 'extra_sql' => 'WHERE cust_location.address1 ILIKE '. + dbh->quote("%$value%"), + } ); + + } + #fuzzy - my @fuzopts = ( - \%options, #hashref - '', #select - " AND $agentnums_sql", #extra_sql #agent virtualization + my %fuzopts = ( + 'hashref' => \%options, + 'select' => '', + 'extra_sql' => " AND $agentnums_sql", #agent virtualization ); if ( $first && $last ) { push @cust_main, FS::cust_main::Search->fuzzy_search( { 'last' => $last, #fuzzy hashref 'first' => $first }, # - @fuzopts + %fuzopts ); } foreach my $field ( 'last', 'company' ) { push @cust_main, - FS::cust_main::Search->fuzzy_search( { $field => $value }, @fuzopts ); + FS::cust_main::Search->fuzzy_search( { $field => $value }, %fuzopts ); } if ( $conf->exists('address1-search') ) { push @cust_main, - FS::cust_main::Search->fuzzy_search( { 'address1' => $value }, @fuzopts ); + FS::cust_main::Search->fuzzy_search( { 'address1' => $value }, %fuzopts ); } } @@ -467,6 +467,14 @@ bool listref of start date, end date +=item birthdate + +listref of start date, end date + +=item spouse_birthdate + +listref of start date, end date + =item payby listref @@ -558,18 +566,28 @@ sub search { ## if ( $params->{'address'} =~ /\S/ ) { my $address = dbh->quote('%'. lc($params->{'address'}). '%'); - push @where, '('. join(' OR ', - map "LOWER($_) LIKE $address", - qw(address1 address2 ship_address1 ship_address2) - ). - ')'; + push @where, "EXISTS( + SELECT 1 FROM cust_location + WHERE cust_location.custnum = cust_main.custnum + AND (LOWER(cust_location.address1) LIKE $address OR + LOWER(cust_location.address2) LIKE $address) + )"; } ### # refnum ### - if ( $params->{'refnum'} =~ /^(\d+)$/ ) { - push @where, "refnum = $1"; + if ( $params->{'refnum'} ) { + + my @refnum = ref( $params->{'refnum'} ) + ? @{ $params->{'refnum'} } + : ( $params->{'refnum'} ); + + @refnum = grep /^(\d*)$/, @refnum; + + push @where, '( '. join(' OR ', map "cust_main.refnum = $_", @refnum ). ' )' + if @refnum; + } ## @@ -599,7 +617,7 @@ sub search { # dates ## - foreach my $field (qw( signupdate )) { + foreach my $field (qw( signupdate birthdate spouse_birthdate )) { next unless exists($params->{$field}); @@ -610,7 +628,7 @@ sub search { "cust_main.$field >= $beginning", "cust_main.$field <= $ending"; - if(defined $hour) { + if($field eq 'signupdate' && defined $hour) { if ($dbh->{Driver}->{Name} =~ /Pg/i) { push @where, "extract(hour from to_timestamp(cust_main.$field)) = $hour"; } @@ -770,22 +788,33 @@ sub search { if ($params->{'flattened_pkgs'}) { #my $pkg_join = ''; + $addl_from .= ' LEFT JOIN cust_pkg USING ( custnum ) '; if ($dbh->{Driver}->{Name} eq 'Pg') { - push @select, "array_to_string(array(select pkg from cust_pkg left join part_pkg using ( pkgpart ) where cust_main.custnum = cust_pkg.custnum $pkgwhere),'|') as magic"; + push @select, " + ARRAY_TO_STRING( + ARRAY( + SELECT pkg FROM cust_pkg LEFT JOIN part_pkg USING ( pkgpart ) + WHERE cust_main.custnum = cust_pkg.custnum $pkgwhere + ), '|' + ) AS magic + "; } elsif ($dbh->{Driver}->{Name} =~ /^mysql/i) { push @select, "GROUP_CONCAT(part_pkg.pkg SEPARATOR '|') as magic"; - $addl_from .= ' LEFT JOIN cust_pkg USING ( custnum ) '; #Pg too w/flatpkg? $addl_from .= ' LEFT JOIN part_pkg USING ( pkgpart ) '; #$pkg_join .= ' LEFT JOIN part_pkg USING ( pkgpart ) '; } else { warn "warning: unknown database type ". $dbh->{Driver}->{Name}. - "omitting packing information from report."; + "omitting package information from report."; } - my $header_query = "SELECT COUNT(cust_pkg.custnum = cust_main.custnum) AS count FROM cust_main $addl_from $extra_sql $pkgwhere group by cust_main.custnum order by count desc limit 1"; + my $header_query = " + SELECT COUNT(cust_pkg.custnum = cust_main.custnum) AS count + FROM cust_main $addl_from $extra_sql $pkgwhere + GROUP BY cust_main.custnum ORDER BY count DESC LIMIT 1 + "; my $sth = dbh->prepare($header_query) or die dbh->errstr; $sth->execute() or die $sth->errstr; @@ -831,20 +860,27 @@ sub search { } -=item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ] +=item fuzzy_search FUZZY_HASHREF [ OPTS ] Performs a fuzzy (approximate) search and returns the matching FS::cust_main records. Currently, I<first>, I<last>, I<company> and/or I<address1> may be -specified (the appropriate ship_ field is also searched). +specified. Additional options are the same as FS::Record::qsearch =cut sub fuzzy_search { - my( $self, $fuzzy, $hash, @opt) = @_; - #$self - $hash ||= {}; + my( $self, $fuzzy ) = @_; + # sensible defaults, then merge in any passed options + my %fuzopts = ( + 'table' => 'cust_main', + 'addl_from' => '', + 'extra_sql' => '', + 'hashref' => {}, + @_ + ); + my @cust_main = (); check_and_rebuild_fuzzyfiles(); @@ -858,8 +894,25 @@ sub fuzzy_search { my @fcust = (); foreach ( keys %match ) { - push @fcust, qsearch('cust_main', { %$hash, $field=>$_}, @opt); - push @fcust, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt); + 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 + }); + } } my %fsaw = (); push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust; diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm index e937b205c..6316f239a 100644 --- a/FS/FS/cust_main_county.pm +++ b/FS/FS/cust_main_county.pm @@ -176,7 +176,7 @@ with different tax classes. sub sql_taxclass_sameregion { my $self = shift; - my $same_query = 'SELECT taxclass FROM cust_main_county '. + my $same_query = 'SELECT DISTINCT taxclass FROM cust_main_county '. ' WHERE taxnum != ? AND country = ?'; my @same_param = ( 'taxnum', 'country' ); foreach my $opt_field (qw( state county )) { diff --git a/FS/FS/cust_main_exemption.pm b/FS/FS/cust_main_exemption.pm index 06d22b7e0..c6f3d5e6e 100644 --- a/FS/FS/cust_main_exemption.pm +++ b/FS/FS/cust_main_exemption.pm @@ -3,6 +3,7 @@ package FS::cust_main_exemption; use strict; use base qw( FS::Record ); use FS::Record qw( qsearch qsearchs ); +use FS::Conf; use FS::cust_main; =head1 NAME @@ -44,6 +45,9 @@ Customer (see L<FS::cust_main>) taxname +=item exempt_number + +Exemption number =back @@ -108,9 +112,15 @@ sub check { $self->ut_numbern('exemptionnum') || $self->ut_foreign_key('custnum', 'cust_main', 'custnum') || $self->ut_text('taxname') + || $self->ut_textn('exempt_number') ; return $error if $error; + my $conf = new FS::Conf; + if ( ! $self->exempt_number && $conf->exists('tax-cust_exempt-groups-require_individual_nums') ) { + return 'Tax exemption number required for '. $self->taxname. ' exemption'; + } + $self->SUPER::check; } diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index ef30809b0..2a2b9d025 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -22,6 +22,7 @@ use FS::cust_pay_refund; use FS::cust_main; use FS::cust_pkg; use FS::cust_pay_void; +use FS::upgrade_journal; $DEBUG = 0; @@ -87,7 +88,7 @@ order taker (see L<FS::access_user>) =item payby -Payment Type (See L<FS::payinfo_Mixin> for valid payby values) +Payment Type (See L<FS::payinfo_Mixin> for valid values) =item payinfo @@ -582,7 +583,7 @@ sub send_receipt { my $conf = new FS::Conf; - return '' unless $conf->exists('payment_receipt', $cust_main->agentnum); + return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum); my @invoicing_list = $cust_main->invoicing_list_emailonly; return '' unless @invoicing_list; @@ -760,6 +761,12 @@ objects. Returns a list, each element representing the status of inserting the corresponding payment - empty. If there is an error inserting any payment, the entire transaction is rolled back, i.e. all payments are inserted or none are. +FS::cust_pay objects may have the pseudo-field 'apply_to', containing a +reference to an array of (uninserted) FS::cust_bill_pay objects. If so, +those objects will be inserted with the paynum of the payment, and for +each one, an error message or an empty string will be inserted into the +list of errors. + For example: my @errors = FS::cust_pay->batch_insert(@cust_pay); @@ -786,19 +793,35 @@ sub batch_insert { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $errors = 0; + my $num_errors = 0; - my @errors = map { - my $error = $_->insert( 'manual' => 1 ); - if ( $error ) { - $errors++; - } else { - $_->cust_main->apply_payments; + my @errors; + foreach my $cust_pay (@_) { + my $error = $cust_pay->insert( 'manual' => 1 ); + push @errors, $error; + $num_errors++ if $error; + + if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) { + + foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) { + if ( $error ) { # insert placeholders if cust_pay wasn't inserted + push @errors, ''; + } + else { + $cust_bill_pay->set('paynum', $cust_pay->paynum); + my $apply_error = $cust_bill_pay->insert; + push @errors, $apply_error || ''; + $num_errors++ if $apply_error; + } + } + + } elsif ( !$error ) { #normal case: apply payments as usual + $cust_pay->cust_main->apply_payments; } - $error; - } @_; - if ( $errors ) { + } + + if ( $num_errors ) { $dbh->rollback if $oldAutoCommit; } else { $dbh->commit or die $dbh->errstr if $oldAutoCommit; @@ -853,93 +876,103 @@ sub _upgrade_data { #class method # otaker/ivan upgrade ## - #not the most efficient, but hey, it only has to run once + unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) { - my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ". - " AND usernum IS NULL ". - " AND 0 < ( SELECT COUNT(*) FROM cust_main ". - " WHERE cust_main.custnum = cust_pay.custnum ) "; + #not the most efficient, but hey, it only has to run once - my $count_sql = "SELECT COUNT(*) FROM cust_pay $where"; + my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ". + " AND usernum IS NULL ". + " AND 0 < ( SELECT COUNT(*) FROM cust_main ". + " WHERE cust_main.custnum = cust_pay.custnum ) "; - my $sth = dbh->prepare($count_sql) or die dbh->errstr; - $sth->execute or die $sth->errstr; - my $total = $sth->fetchrow_arrayref->[0]; - #warn "$total cust_pay records to update\n" - # if $DEBUG; - local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info + my $count_sql = "SELECT COUNT(*) FROM cust_pay $where"; - my $count = 0; - my $lastprog = 0; + my $sth = dbh->prepare($count_sql) or die dbh->errstr; + $sth->execute or die $sth->errstr; + my $total = $sth->fetchrow_arrayref->[0]; + #warn "$total cust_pay records to update\n" + # if $DEBUG; + local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info - my @cust_pay = qsearch( { - 'table' => 'cust_pay', - 'hashref' => {}, - 'extra_sql' => $where, - 'order_by' => 'ORDER BY paynum', - } ); + my $count = 0; + my $lastprog = 0; - foreach my $cust_pay (@cust_pay) { + my @cust_pay = qsearch( { + 'table' => 'cust_pay', + 'hashref' => {}, + 'extra_sql' => $where, + 'order_by' => 'ORDER BY paynum', + } ); - my $h_cust_pay = $cust_pay->h_search('insert'); - if ( $h_cust_pay ) { - next if $cust_pay->otaker eq $h_cust_pay->history_user; - #$cust_pay->otaker($h_cust_pay->history_user); - $cust_pay->set('otaker', $h_cust_pay->history_user); - } else { - $cust_pay->set('otaker', 'legacy'); - } + foreach my $cust_pay (@cust_pay) { + + my $h_cust_pay = $cust_pay->h_search('insert'); + if ( $h_cust_pay ) { + next if $cust_pay->otaker eq $h_cust_pay->history_user; + #$cust_pay->otaker($h_cust_pay->history_user); + $cust_pay->set('otaker', $h_cust_pay->history_user); + } else { + $cust_pay->set('otaker', 'legacy'); + } - delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge - my $error = $cust_pay->replace; + delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge + my $error = $cust_pay->replace; - if ( $error ) { - warn " *** WARNING: Error updating order taker for payment paynum ". - $cust_pay->paynun. ": $error\n"; - next; - } + if ( $error ) { + warn " *** WARNING: Error updating order taker for payment paynum ". + $cust_pay->paynun. ": $error\n"; + next; + } + + $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it - $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it + $count++; + if ( $DEBUG > 1 && $lastprog + 30 < time ) { + warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n"; + $lastprog = time; + } - $count++; - if ( $DEBUG > 1 && $lastprog + 30 < time ) { - warn "$me $count/$total (". sprintf('%.2f',100*$count/$total). '%)'. "\n"; - $lastprog = time; } + FS::upgrade_journal->set_done('cust_pay__otaker_ivan'); } ### # payinfo N/A upgrade ### - #XXX remove the 'N/A (tokenized)' part (or just this entire thing) + unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) { - my @na_cust_pay = qsearch( { - 'table' => 'cust_pay', - 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' }, - 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )", - } ); + #XXX remove the 'N/A (tokenized)' part (or just this entire thing) - foreach my $na ( @na_cust_pay ) { + my @na_cust_pay = qsearch( { + 'table' => 'cust_pay', + 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' }, + 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )", + } ); - next unless $na->payinfo eq 'N/A'; + foreach my $na ( @na_cust_pay ) { + + next unless $na->payinfo eq 'N/A'; + + my $cust_pay_pending = + qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } ); + unless ( $cust_pay_pending ) { + warn " *** WARNING: not-yet recoverable N/A card for payment ". + $na->paynum. " (no cust_pay_pending)\n"; + next; + } + $na->$_($cust_pay_pending->$_) for qw( payinfo paymask ); + my $error = $na->replace; + if ( $error ) { + warn " *** WARNING: Error updating payinfo for payment paynum ". + $na->paynun. ": $error\n"; + next; + } - my $cust_pay_pending = - qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } ); - unless ( $cust_pay_pending ) { - warn " *** WARNING: not-yet recoverable N/A card for payment ". - $na->paynum. " (no cust_pay_pending)\n"; - next; - } - $na->$_($cust_pay_pending->$_) for qw( payinfo paymask ); - my $error = $na->replace; - if ( $error ) { - warn " *** WARNING: Error updating payinfo for payment paynum ". - $na->paynun. ": $error\n"; - next; } + FS::upgrade_journal->set_done('cust_pay__payinfo_na'); } ### diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index bee1b82fb..22559e95f 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -10,9 +10,9 @@ use List::Util qw(max); use Tie::IxHash; use Time::Local qw( timelocal timelocal_nocheck ); use MIME::Entity; -use FS::UID qw( getotaker dbh ); +use FS::UID qw( getotaker dbh driver_name ); use FS::Misc qw( send_email ); -use FS::Record qw( qsearch qsearchs ); +use FS::Record qw( qsearch qsearchs fields ); use FS::CurrentUser; use FS::cust_svc; use FS::part_pkg; @@ -879,6 +879,154 @@ sub cancel_if_expired { ''; } +=item uncancel + +"Un-cancels" this package: Orders a new package with the same custnum, pkgpart, +locationnum, (other fields?). Attempts to re-provision cancelled services +using history information (errors at this stage are not fatal). + +cust_pkg: pass a scalar reference, will be filled in with the new cust_pkg object + +svc_fatal: service provisioning errors are fatal + +svc_errors: pass an array reference, will be filled in with any provisioning errors + +=cut + +sub uncancel { + my( $self, %options ) = @_; + + #in case you try do do $uncancel-date = $cust_pkg->uncacel + return '' unless $self->get('cancel'); + + ## + # Transaction-alize + ## + + 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; + + ## + # insert the new package + ## + + my $cust_pkg = new FS::cust_pkg { + last_bill => ( $options{'last_bill'} || $self->get('last_bill') ), + bill => ( $options{'bill'} || $self->get('bill') ), + uncancel => time, + uncancel_pkgnum => $self->pkgnum, + map { $_ => $self->get($_) } qw( + custnum pkgpart locationnum + setup + susp adjourn resume expire start_date contract_end dundate + change_date change_pkgpart change_locationnum + manual_flag no_auto quantity agent_pkgid recur_show_zero setup_show_zero + ), + }; + + my $error = $cust_pkg->insert( + 'change' => 1, #supresses any referral credit to a referring customer + ); + if ($error) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + ## + # insert services + ## + + #find historical services within this timeframe before the package cancel + # (incompatible with "time" option to cust_pkg->cancel?) + my $fuzz = 2 * 60; #2 minutes? too much? (might catch separate unprovision) + # too little? (unprovisioing export delay?) + my($end, $start) = ( $self->get('cancel'), $self->get('cancel') - $fuzz ); + my @h_cust_svc = $self->h_cust_svc( $end, $start ); + + my @svc_errors; + foreach my $h_cust_svc (@h_cust_svc) { + my $h_svc_x = $h_cust_svc->h_svc_x( $end, $start ); + #next unless $h_svc_x; #should this happen? + (my $table = $h_svc_x->table) =~ s/^h_//; + require "FS/$table.pm"; + my $class = "FS::$table"; + my $svc_x = $class->new( { + 'pkgnum' => $cust_pkg->pkgnum, + 'svcpart' => $h_cust_svc->svcpart, + map { $_ => $h_svc_x->get($_) } fields($table) + } ); + + # radius_usergroup + if ( $h_svc_x->isa('FS::h_svc_Radius_Mixin') ) { + $svc_x->usergroup( [ $h_svc_x->h_usergroup($end, $start) ] ); + } + + my $svc_error = $svc_x->insert; + if ( $svc_error && $options{svc_fatal} ) { + $dbh->rollback if $oldAutoCommit; + return $svc_error; + } else { + my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $svc_x->svcnum }); + if ( $cust_svc ) { + my $cs_error = $cust_svc->delete; + if ( $cs_error ) { + $dbh->rollback if $oldAutoCommit; + return $cs_error; + } + } + } + push @svc_errors, $svc_error if $svc_error; + } + + #these are pretty rare, but should handle them + # - dsl_device (mac addresses) + # - phone_device (mac addresses) + # - dsl_note (ikano notes) + # - domain_record (i.e. restore DNS information w/domains) + # - inventory_item(?) (inventory w/un-cancelling service?) + # - nas (svc_broaband nas stuff) + #this stuff is unused in the wild afaik + # - mailinglistmember + # - router.svcnum? + # - svc_domain.parent_svcnum? + # - acct_snarf (ancient mail fetching config) + # - cgp_rule (communigate) + # - cust_svc_option (used by our Tron stuff) + # - acct_rt_transaction (used by our time worked stuff) + + ## + # also move over any services that didn't unprovision at cancellation + ## + + foreach my $cust_svc ( qsearch('cust_svc', { pkgnum => $self->pkgnum } ) ) { + $cust_svc->pkgnum( $cust_pkg->pkgnum ); + my $error = $cust_svc->replace; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + ## + # Finish + ## + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ${ $options{cust_pkg} } = $cust_pkg if ref($options{cust_pkg}); + @{ $options{svc_errors} } = @svc_errors if ref($options{svc_errors}); + + ''; +} + =item unexpire Cancels any pending expiration (sets the expire field to null). @@ -1239,6 +1387,8 @@ sub unsuspend { } #if $date + my @labels = (); + foreach my $cust_svc ( qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } ) ) { @@ -1258,6 +1408,8 @@ sub unsuspend { $dbh->rollback if $oldAutoCommit; return $error; } + my( $label, $value ) = $cust_svc->label; + push @labels, "$label: $value"; } } @@ -1288,6 +1440,29 @@ sub unsuspend { return $error; } + if ( $conf->config('unsuspend_email_admin') ) { + + my $error = send_email( + 'from' => $conf->config('invoice_from', $self->cust_main->agentnum), + #invoice_from ??? well as good as any + 'to' => $conf->config('unsuspend_email_admin'), + 'subject' => 'FREESIDE NOTIFICATION: Customer package unsuspended', 'body' => [ + "This is an automatic message from your Freeside installation\n", + "informing you that the following customer package has been unsuspended:\n", + "\n", + 'Customer: #'. $self->custnum. ' '. $self->cust_main->name. "\n", + 'Package : #'. $self->pkgnum. " (". $self->part_pkg->pkg_comment. ")\n", + ( map { "Service : $_\n" } @labels ), + ], + ); + + if ( $error ) { + warn "WARNING: can't send unsuspension admin email (unsuspending anyway): ". + "$error\n"; + } + + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no errors @@ -1895,7 +2070,7 @@ sub cust_svc { } if ( $opt{'svcdb'} ) { $search{addl_from} = ' LEFT JOIN part_svc USING ( svcpart ) '; - $search{hashref}->{svcdb} = $opt{'svcdb'}; + $search{extra_sql} = ' AND svcdb = '. dbh->quote( $opt{'svcdb'} ); } cluck "cust_pkg->cust_svc called" if $DEBUG > 2; @@ -2029,11 +2204,14 @@ field, I<num_avail>, which specifies the number of available services. sub available_part_svc { my $self = shift; + + my $pkg_quantity = $self->quantity || 1; + grep { $_->num_avail > 0 } map { my $part_svc = $_->part_svc; $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking - $_->quantity - $self->num_cust_svc($_->svcpart); + $pkg_quantity * $_->quantity - $self->num_cust_svc($_->svcpart); # more evil encapsulation breakage if($part_svc->{'Hash'}{'num_avail'} > 0) { @@ -2075,6 +2253,8 @@ sub part_svc { my $self = shift; my %opt = @_; + my $pkg_quantity = $self->quantity || 1; + #XXX some sort of sort order besides numeric by svcpart... my @part_svc = sort { $a->svcpart <=> $b->svcpart } map { my $pkg_svc = $_; @@ -2082,7 +2262,7 @@ sub part_svc { my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart); $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #more evil $part_svc->{'Hash'}{'num_avail'} = - max( 0, $pkg_svc->quantity - $num_cust_svc ); + max( 0, $pkg_quantity * $pkg_svc->quantity - $num_cust_svc ); $part_svc->{'Hash'}{'cust_pkg_svc'} = $num_cust_svc ? [ $self->cust_svc($part_svc->svcpart) ] : [] unless exists($opt{summarize_size}) && $opt{summarize_size} > 0 @@ -2441,6 +2621,39 @@ Returns the label of the location object (see L<FS::cust_location>). #end of subs in location_Mixin.pm now... unfortunately the POD doesn't mixin +=item tax_locationnum + +Returns the foreign key to a L<FS::cust_location> object for calculating +tax on this package, as determined by the C<tax-pkg_address> and +C<tax-ship_address> configuration flags. + +=cut + +sub tax_locationnum { + my $self = shift; + my $conf = FS::Conf->new; + if ( $conf->exists('tax-pkg_address') ) { + return $self->locationnum; + } + elsif ( $conf->exists('tax-ship_address') ) { + return $self->cust_main->ship_locationnum; + } + else { + return $self->cust_main->bill_locationnum; + } +} + +=item tax_location + +Returns the L<FS::cust_location> object for tax_locationnum. + +=cut + +sub tax_location { + my $self = shift; + FS::cust_location->by_key( $self->tax_locationnum ) +} + =item seconds_since TIMESTAMP Returns the number of seconds all accounts (see L<FS::svc_acct>) in this @@ -3427,6 +3640,25 @@ sub fcc_477_count { } +=item tax_locationnum_sql + +Returns an SQL expression for the tax location for a package, based +on the settings of 'tax-pkg_address' and 'tax-ship_address'. + +=cut + +sub tax_locationnum_sql { + my $conf = FS::Conf->new; + if ( $conf->exists('tax-pkg_address') ) { + 'cust_pkg.locationnum'; + } + elsif ( $conf->exists('tax-ship_address') ) { + 'cust_main.ship_locationnum'; + } + else { + 'cust_main.bill_locationnum'; + } +} =item location_sql @@ -3445,7 +3677,13 @@ sub location_sql { # '?' placeholders in _location_sql_where my $x = $ornull ? 3 : 2; - my @bill_param = ( ('city')x3, ('county')x$x, ('state')x$x, 'country' ); + my @bill_param = ( + ('district')x3, + ('city')x3, + ('county')x$x, + ('state')x$x, + 'country' + ); my $main_where; my @main_param; @@ -3504,16 +3742,19 @@ sub _location_sql_where { $ornull = $ornull ? ' OR ? IS NULL ' : ''; - my $or_empty_city = " OR ( ? = '' AND $table.${prefix}city IS NULL ) "; - my $or_empty_county = " OR ( ? = '' AND $table.${prefix}county IS NULL ) "; - my $or_empty_state = " OR ( ? = '' AND $table.${prefix}state IS NULL ) "; + my $or_empty_city = " OR ( ? = '' AND $table.${prefix}city IS NULL )"; + my $or_empty_county = " OR ( ? = '' AND $table.${prefix}county IS NULL )"; + my $or_empty_state = " OR ( ? = '' AND $table.${prefix}state IS NULL )"; + + my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text'; # ( $table.${prefix}city = ? $or_empty_city $ornull ) " - ( $table.${prefix}city = ? OR ? = '' OR CAST(? AS text) IS NULL ) - AND ( $table.${prefix}county = ? $or_empty_county $ornull ) - AND ( $table.${prefix}state = ? $or_empty_state $ornull ) - AND $table.${prefix}country = ? + ( $table.district = ? OR ? = '' OR CAST(? AS $text) IS NULL ) + AND ( $table.${prefix}city = ? OR ? = '' OR CAST(? AS $text) IS NULL ) + AND ( $table.${prefix}county = ? $or_empty_county $ornull ) + AND ( $table.${prefix}state = ? $or_empty_state $ornull ) + AND $table.${prefix}country = ? "; } diff --git a/FS/FS/cust_pkg_reason.pm b/FS/FS/cust_pkg_reason.pm index 641605f05..c29a2f928 100644 --- a/FS/FS/cust_pkg_reason.pm +++ b/FS/FS/cust_pkg_reason.pm @@ -4,6 +4,7 @@ use strict; use vars qw( $ignore_empty_action ); use base qw( FS::otaker_Mixin FS::Record ); use FS::Record qw( qsearch qsearchs ); +use FS::upgrade_journal; $ignore_empty_action = 0; @@ -209,6 +210,25 @@ sub _upgrade_data { # class method } #remove nullability if scalar(@migrated) - $count == 0 && ->column('action'); + + unless ( FS::upgrade_journal->is_done('cust_pkg_reason__missing_reason') ) { + $class->_upgrade_missing_reason(%opts); + FS::upgrade_journal->set_done('cust_pkg_reason__missing_reason'); + } + + #still can't fill in an action? don't abort the upgrade + local($ignore_empty_action) = 1; + + $class->_upgrade_otaker(%opts); + +} + +sub _upgrade_missing_reason { + my ($class, %opts) = @_; + + #false laziness w/above + my $action_replace = + " AND ( history_action = 'replace_old' OR history_action = 'replace_new' )"; #seek expirations/adjourns without reason foreach my $field (qw( expire adjourn cancel susp )) { @@ -309,10 +329,6 @@ sub _upgrade_data { # class method } } - #still can't fill in an action? don't abort the upgrade - local($ignore_empty_action) = 1; - - $class->_upgrade_otaker(%opts); } =back diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index 6bd8cb837..2ec8f12c2 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -69,6 +69,8 @@ The following fields are currently supported: =item svcpart - Service definition (see L<FS::part_svc>) +=item agent_svcid - Optional legacy service ID + =item overlimit - date the service exceeded its usage limit =back @@ -319,6 +321,7 @@ sub check { $self->ut_numbern('svcnum') || $self->ut_numbern('pkgnum') || $self->ut_number('svcpart') + || $self->ut_numbern('agent_svcid') || $self->ut_numbern('overlimit') ; return $error if $error; @@ -341,6 +344,18 @@ sub check { $self->SUPER::check; } +=item display_svcnum + +Returns the displayed service number for this service: agent_svcid if it has a +value, svcnum otherwise + +=cut + +sub display_svcnum { + my $self = shift; + $self->agent_svcid || $self->svcnum; +} + =item part_svc Returns the definition for this service, as a FS::part_svc object (see @@ -831,24 +846,37 @@ customers, this always requires an exact match. =cut # though perhaps it should be fuzzy in some cases? + sub smart_search { + my %param = __PACKAGE__->smart_search_param(@_); + qsearch(\%param); +} + +sub smart_search_param { + my $class = shift; my %opt = @_; - # some false laziness w/ search/cust_svc.html + my $string = $opt{'search'}; $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace - my @extra_sql = ' ( '. join(' OR ', - map { my $table = $_; - my $search_sql = "FS::$table"->search_sql($string); - " ( svcdb = '$table' - AND 0 < ( SELECT COUNT(*) FROM $table - WHERE $table.svcnum = cust_svc.svcnum - AND $search_sql - ) - ) "; - } - FS::part_svc->svc_tables - ). ' ) '; + my @or = + map { my $table = $_; + my $search_sql = "FS::$table"->search_sql($string); + " ( svcdb = '$table' + AND 0 < ( SELECT COUNT(*) FROM $table + WHERE $table.svcnum = cust_svc.svcnum + AND $search_sql + ) + ) "; + } + FS::part_svc->svc_tables; + + if ( $string =~ /^(\d+)$/ ) { + unshift @or, " ( agent_svcid IS NOT NULL AND agent_svcid = $1 ) "; + } + + my @extra_sql = ' ( '. join(' OR ', @or). ' ) '; + push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( 'null_right' => 'View/link unlinked services' ); @@ -858,14 +886,16 @@ sub smart_search { ' LEFT JOIN cust_main USING ( custnum )'. ' LEFT JOIN part_svc USING ( svcpart )'; - qsearch({ - 'table' => 'cust_svc', - 'addl_from' => $addl_from, - 'hashref' => {}, - 'extra_sql' => $extra_sql, - }); + ( + 'table' => 'cust_svc', + 'addl_from' => $addl_from, + 'hashref' => {}, + 'extra_sql' => $extra_sql, + ); } +=back + =head1 BUGS Behaviour of changing the svcpart of cust_svc records is undefined and should diff --git a/FS/FS/detail_format/sum_duration_prefix.pm b/FS/FS/detail_format/sum_duration_prefix.pm index d70ad0e5a..04590415c 100644 --- a/FS/FS/detail_format/sum_duration_prefix.pm +++ b/FS/FS/detail_format/sum_duration_prefix.pm @@ -71,7 +71,7 @@ sub finish { $prefix, map({ $_->{count}, - (int($_->{duration}/60) . ' min'), + sprintf('%.01f min', $_->{duration}/60), } @subtotals ), $self->money_char . sprintf('%.02f',$total_amount), ); diff --git a/FS/FS/ftp_target.pm b/FS/FS/ftp_target.pm new file mode 100644 index 000000000..bf9fc891a --- /dev/null +++ b/FS/FS/ftp_target.pm @@ -0,0 +1,194 @@ +package FS::ftp_target; + +use strict; +use base qw( FS::Record ); +use FS::Record qw( qsearch qsearchs ); +use vars qw($me $DEBUG); + +$DEBUG = 0; + +=head1 NAME + +FS::ftp_target - Object methods for ftp_target records + +=head1 SYNOPSIS + + use FS::ftp_target; + + $record = new FS::ftp_target \%hash; + $record = new FS::ftp_target { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::ftp_target object represents an account on a remote FTP or SFTP +server for transferring files. FS::ftp_target inherits from FS::Record. + +=over 4 + +=item targetnum - primary key + +=item agentnum - L<FS::agent> foreign key; can be null + +=item hostname - the DNS name of the FTP site + +=item username - username + +=item password - password + +=item path - the working directory to change to upon connecting + +=item secure - a flag ('Y' or null) for whether to use SFTP + +=back + +=head1 METHODS + +=over 4 + +=cut + +sub table { 'ftp_target'; } + +=item new HASHREF + +Creates a new FTP target. To add it to the database, see L<"insert">. + +=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; + + if ( !$self->get('port') ) { + if ( $self->secure ) { + $self->set('port', 22); + } else { + $self->set('port', 21); + } + } + + my $error = + $self->ut_numbern('targetnum') + || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum') + || $self->ut_text('hostname') + || $self->ut_text('username') + || $self->ut_text('password') + || $self->ut_number('port') + || $self->ut_text('path') + || $self->ut_flag('secure') + || $self->ut_enum('handling', [ $self->handling_types ]) + ; + return $error if $error; + + $self->SUPER::check; +} + +=item connect + +Creates a Net::FTP or Net::SFTP::Foreign object (according to the setting +of the 'secure' flag), connects to 'hostname', attempts to log in with +'username' and 'password', and changes the working directory to 'path'. +On success, returns the object. On failure, dies with an error message. + +=cut + +sub connect { + my $self = shift; + if ( $self->secure ) { + eval "use Net::SFTP::Foreign;"; + die $@ if $@; + my %args = ( + port => $self->port, + user => $self->username, + password => $self->password, + more => ($DEBUG ? '-v' : ''), + timeout => 30, + autodie => 1, #we're doing this anyway + ); + my $sftp = Net::SFTP::Foreign->new($self->hostname, %args); + $sftp->setcwd($self->path); + return $sftp; + } + else { + eval "use Net::FTP;"; + die $@ if $@; + my %args = ( + Debug => $DEBUG, + Port => $self->port, + Passive => 1,# optional? + ); + my $ftp = Net::FTP->new($self->hostname, %args) + or die "connect to ".$self->hostname." failed: $@"; + $ftp->login($self->username, $self->password) + or die "login to ".$self->username.'@'.$self->hostname." failed: $@"; + $ftp->binary; #optional? + $ftp->cwd($self->path) + or ($self->path eq '/') + or die "cwd to ".$self->hostname.'/'.$self->path." failed: $@"; + + return $ftp; + } +} + +=item label + +Returns a descriptive label for this target. + +=cut + +sub label { + my $self = shift; + $self->targetnum . ': ' . $self->username . '@' . $self->hostname; +} + +=item handling_types + +Returns a list of values for the "handling" field, corresponding to the +known ways to preprocess a file before uploading. Currently those are +implemented somewhat crudely in L<FS::Cron::upload>. + +=cut + +sub handling_types { + '', + #'billco', #not implemented this way yet + 'bridgestone', +} + +=back + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/h_radius_usergroup.pm b/FS/FS/h_radius_usergroup.pm new file mode 100644 index 000000000..bbccd6bb7 --- /dev/null +++ b/FS/FS/h_radius_usergroup.pm @@ -0,0 +1,24 @@ +package FS::h_radius_usergroup; + +use strict; +use base qw( FS::h_Common FS::radius_usergroup ); + +sub table { 'h_radius_usergroup' }; + +=head1 NAME + +FS::h_radius_usergroup - Historical RADIUS usergroup records. + +=head1 DESCRIPTION + +An FS::h_radius_usergroup object represents historical changes to an account's +RADIUS group (L<FS::radius_usergroup>). + +=head1 SEE ALSO + +L<FS::radius_usergroup>, L<FS::h_Common>, L<FS::Record> + +=cut + +1; + diff --git a/FS/FS/h_svc_Radius_Mixin.pm b/FS/FS/h_svc_Radius_Mixin.pm new file mode 100644 index 000000000..af2977085 --- /dev/null +++ b/FS/FS/h_svc_Radius_Mixin.pm @@ -0,0 +1,17 @@ +package FS::h_svc_Radius_Mixin; + +use strict; +use FS::Record qw( qsearch ); +use FS::h_radius_usergroup; + +sub h_usergroup { + my $self = shift; + map { $_->groupnum } + qsearch( 'h_radius_usergroup', + { svcnum => $self->svcnum }, + FS::h_radius_usergroup->sql_h_searchs(@_), + ); +} + +1; + diff --git a/FS/FS/h_svc_acct.pm b/FS/FS/h_svc_acct.pm index 247d20c9a..f525f8206 100644 --- a/FS/FS/h_svc_acct.pm +++ b/FS/FS/h_svc_acct.pm @@ -1,16 +1,13 @@ package FS::h_svc_acct; +use base qw( FS::h_svc_Radius_Mixin FS::h_Common FS::svc_acct ); use strict; use vars qw( @ISA $DEBUG ); use Carp qw(carp); use FS::Record qw(qsearchs); -use FS::h_Common; -use FS::svc_acct; use FS::svc_domain; use FS::h_svc_domain; -@ISA = qw( FS::h_Common FS::svc_acct ); - $DEBUG = 0; sub table { 'h_svc_acct' }; diff --git a/FS/FS/h_svc_broadband.pm b/FS/FS/h_svc_broadband.pm index d6038fbe8..01477fe1c 100644 --- a/FS/FS/h_svc_broadband.pm +++ b/FS/FS/h_svc_broadband.pm @@ -1,11 +1,8 @@ package FS::h_svc_broadband; +use base qw( FS::h_svc_Radius_Mixin FS::h_Common FS::svc_broadband ); use strict; use vars qw( @ISA ); -use FS::h_Common; -use FS::svc_broadband; - -@ISA = qw( FS::h_Common FS::svc_broadband ); sub table { 'h_svc_broadband' }; diff --git a/FS/FS/inventory_item.pm b/FS/FS/inventory_item.pm index 39a0dff4b..477c93410 100644 --- a/FS/FS/inventory_item.pm +++ b/FS/FS/inventory_item.pm @@ -111,6 +111,7 @@ sub check { 'Edit global inventory'] ) || $self->ut_text('item') || $self->ut_foreign_keyn('svcnum', 'cust_svc', 'svcnum' ) + || $self->ut_alphan('svc_field') ; return $error if $error; diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index e47776c86..ffb4f52fb 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -465,14 +465,12 @@ sub substitutions { name name_short contact contact_firstlast address1 address2 city county state zip country - daytime night fax + daytime night mobile fax has_ship_address - ship_last ship_first ship_company ship_name ship_name_short ship_contact ship_contact_firstlast ship_address1 ship_address2 ship_city ship_county ship_state ship_zip ship_country - ship_daytime ship_night ship_fax paymask payname paytype payip num_cancelled_pkgs num_ncancelled_pkgs num_pkgs @@ -485,6 +483,15 @@ sub substitutions { signupdate dundate packages recurdates ), + #compatibility: obsolete ship_ fields - use the non-ship versions + map ( + { my $field = $_; + [ "ship_$field" => sub { shift->$field } ] + } + qw( last first company daytime night fax ) + ), + # ship_name, ship_name_short, ship_contact, ship_contact_firstlast + # still work, though [ expdate => sub { shift->paydate_epoch } ], #compatibility [ signupdate_ymd => sub { $ymd->(shift->signupdate) } ], [ dundate_ymd => sub { $ymd->(shift->dundate) } ], diff --git a/FS/FS/option_Common.pm b/FS/FS/option_Common.pm index 968dcdf79..c1dda22af 100644 --- a/FS/FS/option_Common.pm +++ b/FS/FS/option_Common.pm @@ -65,7 +65,10 @@ sub insert { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $error = $self->SUPER::insert; + my $error; + + $error = $self->check_options($options) + || $self->SUPER::insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -197,7 +200,17 @@ sub replace { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $error = $self->SUPER::replace($old); + my $error; + + if ($options_supplied) { + $error = $self->check_options($options); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + $error = $self->SUPER::replace($old); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -274,6 +287,21 @@ sub replace { } +=item check_options HASHREF + +This method is called by 'insert' and 'replace' to check the options that were supplied. + +Return error-message, or false. + +(In this class, this is a do-nothing routine that always returns false. Override as necessary. No need to call superclass.) + +=cut + +sub check_options { + my ($self, $options) = @_; + ''; +} + =item option_objects Returns all options as FS::I<tablename>_option objects. diff --git a/FS/FS/part_event/Action/cust_bill_email.pm b/FS/FS/part_event/Action/cust_bill_email.pm index a5cd86145..1a3bca4b7 100644 --- a/FS/FS/part_event/Action/cust_bill_email.pm +++ b/FS/FS/part_event/Action/cust_bill_email.pm @@ -17,7 +17,7 @@ sub do_action { #my $cust_main = $self->cust_main($cust_bill); my $cust_main = $cust_bill->cust_main; - $cust_bill->email; + $cust_bill->email unless $cust_main->invoice_noemail; } 1; diff --git a/FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm b/FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm index bf472683f..56ba680e1 100644 --- a/FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm +++ b/FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm @@ -2,6 +2,7 @@ package FS::part_event::Action::cust_bill_send_csv_ftp; use strict; use base qw( FS::part_event::Action ); +use FS::Misc::Invoicing qw( spool_formats ); sub description { 'Upload CSV invoice data to an FTP server'; } @@ -15,10 +16,7 @@ sub option_fields { ( 'ftpformat' => { label => 'Format', type =>'select', - options => ['default', 'billco'], - option_labels => { 'default' => 'Default', - 'billco' => 'Billco', - }, + options => [ spool_formats() ], }, 'ftpserver' => 'FTP server', 'ftpusername' => 'FTP username', diff --git a/FS/FS/part_event/Action/cust_bill_spool_csv.pm b/FS/FS/part_event/Action/cust_bill_spool_csv.pm index 11ecbc555..14349a9dd 100644 --- a/FS/FS/part_event/Action/cust_bill_spool_csv.pm +++ b/FS/FS/part_event/Action/cust_bill_spool_csv.pm @@ -2,6 +2,7 @@ package FS::part_event::Action::cust_bill_spool_csv; use strict; use base qw( FS::part_event::Action ); +use FS::Misc::Invoicing qw( spool_formats ); sub description { 'Spool CSV invoice data'; } @@ -15,10 +16,7 @@ sub option_fields { ( 'spoolformat' => { label => 'Format', type => 'select', - options => ['default', 'billco'], - option_labels => { 'default' => 'Default', - 'billco' => 'Billco', - }, + options => [ spool_formats() ], }, 'spoolbalanceover' => { label => 'If balance (this invoice and previous) over', @@ -28,6 +26,13 @@ sub option_fields { type => 'checkbox', value => '1', }, + 'ftp_targetnum' => { label => 'Upload spool to FTP target', + type => 'select-table', + table => 'ftp_target', + name_col => 'label', + empty_label => '(do not upload)', + order_by => 'targetnum', + }, ); } @@ -43,6 +48,7 @@ sub do_action { 'format' => $self->option('spoolformat'), 'balanceover' => $self->option('spoolbalanceover'), 'agent_spools' => $self->option('spoolagent_spools'), + 'ftp_targetnum'=> $self->option('ftp_targetnum'), ); } diff --git a/FS/FS/part_event/Condition/balance_age_under.pm b/FS/FS/part_event/Condition/balance_age_under.pm new file mode 100644 index 000000000..ac6d786fc --- /dev/null +++ b/FS/FS/part_event/Condition/balance_age_under.pm @@ -0,0 +1,52 @@ +package FS::part_event::Condition::balance_age_under; + +use strict; +use base qw( FS::part_event::Condition ); + +sub description { 'Customer balance age (under)'; } + +sub option_fields { + ( + 'balance' => { 'label' => 'Balance under (or equal to)', + 'type' => 'money', + 'value' => '0.00', #default + }, + 'age' => { 'label' => 'Age', + 'type' => 'freq', + }, + ); +} + +sub condition { + my($self, $object, %opt) = @_; + + my $cust_main = $self->cust_main($object); + + my $under = $self->option('balance'); + $under = 0 unless length($under); + + my $age = $self->option_age_from('age', $opt{'time'} ); + + $cust_main->balance_date($age) <= $under; +} + +sub condition_sql { + my( $class, $table, %opt ) = @_; + + my $under = $class->condition_sql_option('balance'); + my $age = $class->condition_sql_option_age_from('age', $opt{'time'}); + + my $balance_sql = FS::cust_main->balance_date_sql( $age ); + + "$balance_sql <= CAST( $under AS DECIMAL(10,2) )"; +} + +sub order_sql { + shift->condition_sql_option_age('age'); +} + +sub order_sql_weight { + 10; +} + +1; diff --git a/FS/FS/part_event/Condition/has_referral_custnum.pm b/FS/FS/part_event/Condition/has_referral_custnum.pm index 70c9c7f8b..dee240fec 100644 --- a/FS/FS/part_event/Condition/has_referral_custnum.pm +++ b/FS/FS/part_event/Condition/has_referral_custnum.pm @@ -13,30 +13,49 @@ sub option_fields { 'type' => 'checkbox', 'value' => 'Y', }, + 'check_bal' => { 'label' => 'Check referring custoemr balance', + 'type' => 'checkbox', + 'value' => 'Y', + }, + 'balance' => { 'label' => 'Referring customer balance under (or equal to)', + 'type' => 'money', + 'value' => '0.00', #default + }, + 'age' => { 'label' => 'Referring customer balance age', + 'type' => 'freq', + }, ); } sub condition { - my($self, $object) = @_; + my($self, $object, %opt) = @_; my $cust_main = $self->cust_main($object); if ( $self->option('active') ) { - return 0 unless $cust_main->referral_custnum; - #check for no cust_main for referral_custnum? (deleted?) + return 0 unless $cust_main->referral_custnum_cust_main->status eq 'active'; + } else { + return 0 unless $cust_main->referral_custnum; # ? 1 : 0; + } - $cust_main->referral_custnum_cust_main->status eq 'active'; + return 1 unless $self->option('check_bal'); - } else { + my $referring_cust_main = $cust_main->referral_custnum_cust_main; - $cust_main->referral_custnum; # ? 1 : 0; + #false laziness w/ balance_age_under + my $under = $self->option('balance'); + $under = 0 unless length($under); - } + my $age = $self->option_age_from('age', $opt{'time'} ); + + $referring_cust_main->balance_date($age) <= $under; } +#this is incomplete wrt checking referring customer balances, but that's okay. +# false positives are acceptable here, its just an optimizaiton sub condition_sql { my( $class, $table ) = @_; diff --git a/FS/FS/part_event/Condition/once_percust_every.pm b/FS/FS/part_event/Condition/once_percust_every.pm new file mode 100644 index 000000000..9e2ec1f00 --- /dev/null +++ b/FS/FS/part_event/Condition/once_percust_every.pm @@ -0,0 +1,58 @@ +package FS::part_event::Condition::once_percust_every; + +use strict; +use FS::Record qw( qsearch ); +use FS::part_event; +use FS::cust_event; + +use base qw( FS::part_event::Condition ); + +sub description { "Don't run this event more than once per customer in the specified interval"; } + +sub eventtable_hashref { + { 'cust_main' => 0, + 'cust_bill' => 1, + 'cust_pkg' => 1, + }; +} + +# Runs the event at most "once every X", per customer. + +sub option_fields { + ( + 'run_delay' => { label=>'Interval', type=>'freq', value=>'1m', }, + ); +} + +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 $max_date = $self->option_age_from('run_delay', $opt{'time'}); + + my @existing = qsearch( { + 'table' => 'cust_event', + 'hashref' => { + 'eventpart' => $self->eventpart, + 'status' => { op=>'!=', value=>'failed' }, + '_date' => { op=>'>', value=>$max_date }, + }, + 'extra_sql' => $extra_sql, + } ); + + ! scalar(@existing); + +} + +1; diff --git a/FS/FS/part_event/Condition/pkg_dundate_age.pm b/FS/FS/part_event/Condition/pkg_dundate_age.pm new file mode 100644 index 000000000..75fce1fd2 --- /dev/null +++ b/FS/FS/part_event/Condition/pkg_dundate_age.pm @@ -0,0 +1,43 @@ +package FS::part_event::Condition::pkg_dundate_age; +use base qw( FS::part_event::Condition ); + +use strict; + +sub description { + "Skip until specified # of days before package suspension delay date"; +} + + +sub option_fields { + ( + 'age' => { 'label' => 'Time before suspension delay date', + 'type' => 'freq', + }, + ); +} + +sub eventtable_hashref { + { 'cust_main' => 0, + 'cust_bill' => 0, + 'cust_pkg' => 1, + }; +} + +sub condition { + my($self, $cust_pkg, %opt) = @_; + + my $age = $self->option_age_from('age', $opt{'time'} ); + + $cust_pkg->dundate <= $age; +} + +sub condition_sql { + my( $class, $table, %opt ) = @_; + return 'true' unless $table eq 'cust_pkg'; + + my $age = $class->condition_sql_option_age_from('age', $opt{'time'}); + + "COALESCE($table.dundate,0) <= ". $age; +} + +1; diff --git a/FS/FS/part_export/acct_xmlrpc.pm b/FS/FS/part_export/acct_xmlrpc.pm new file mode 100644 index 000000000..d746f29bc --- /dev/null +++ b/FS/FS/part_export/acct_xmlrpc.pm @@ -0,0 +1,268 @@ +package FS::part_export::acct_xmlrpc; +use base qw( FS::part_export ); + +use vars qw( %info ); # $DEBUG ); +#use Data::Dumper; +use Tie::IxHash; +use Frontier::Client; #to avoid adding a dependency on RPC::XML just now +#use FS::Record qw( qsearch qsearchs ); +use FS::Schema qw( dbdef ); + +#$DEBUG = 1; + +tie my %options, 'Tie::IxHash', + 'xmlrpc_url' => { label => 'XML-RPC URL', }, + 'param_style' => { label => 'Parameter style', + type => 'select', + options => [ 'Individual values', + 'Struct of name/value pairs', + ], + }, + 'insert_method' => { label => 'Insert method', }, + 'insert_params' => { label => 'Insert parameters', type=>'textarea', }, + 'replace_method' => { label => 'Replace method', }, + 'replace_params' => { label => 'Replace parameters', type=>'textarea', }, + 'delete_method' => { label => 'Delete method', }, + 'delete_params' => { label => 'Delete parameters', type=>'textarea', }, + 'suspend_method' => { label => 'Suspend method', }, + 'suspend_params' => { label => 'Suspend parameters', type=>'textarea', }, + 'unsuspend_method' => { label => 'Unsuspend method', }, + 'unsuspend_params' => { label => 'Unsuspend parameters', type=>'textarea', }, +; + +%info = ( + 'svc' => 'svc_acct', + 'desc' => 'Configurable provisioning of accounts via the XML-RPC protocol', + 'options' => \%options, + 'notes' => <<'END', +Configurable, real-time export of accounts via the XML-RPC protocol.<BR> +<BR> +If using "Individual values" parameter style, specfify one parameter per line.<BR> +<BR> +If using "Struct of name/value pairs" parameter style, specify one name and +value on each line, separated by whitespace.<BR> +<BR> +The following variables are available for interpolation (prefixed with new_ or +old_ for replace operations): +<UL> + <LI><code>$username</code> + <LI><code>$_password</code> + <LI><code>$crypt_password</code> - encrypted password + <LI><code>$ldap_password</code> - Password in LDAP/RFC2307 format (for example, "{PLAIN}himom", "{CRYPT}94pAVyK/4oIBk" or "{MD5}5426824942db4253f87a1009fd5d2d4") + <LI><code>$uid</code> + <LI><code>$gid</code> + <LI><code>$finger</code> - Real name + <LI><code>$dir</code> - home directory + <LI><code>$shell</code> + <LI><code>$quota</code> + <LI><code>@radius_groups</code> +<!-- <LI><code>$reasonnum (when suspending)</code> + <LI><code>$reasontext (when suspending)</code> + <LI><code>$reasontypenum (when suspending)</code> + <LI><code>$reasontypetext (when suspending)</code> +--> +<!-- + <LI><code>$pkgnum</code> + <LI><code>$custnum</code> +--> + <LI>All other fields in <b>svc_acct</b> are also available. +<!-- <LI>The following fields from <b>cust_main</b> are also available (except during replace): company, address1, address2, city, state, zip, county, daytime, night, fax, otaker, agent_custid, locale. --> +</UL> + +END +); + +sub _export_insert { shift->_export_command('insert', @_) } +sub _export_delete { shift->_export_command('delete', @_) } +sub _export_suspend { shift->_export_command('suspend', @_) } +sub _export_unsuspend { shift->_export_command('unsuspend', @_) } + +sub _export_command { + my ( $self, $action, $svc_acct) = (shift, shift, shift); + my $method = $self->option($action.'_method'); + return '' if $method =~ /^\s*$/; + + my @params = split("\n", $self->option($action.'_params') ); + + my( @x_param ) = (); + my( %x_struct ) = (); + foreach my $param (@params) { + + my($name, $value) = ('', ''); + if ($self->option('param_style') eq 'Struct of name/value pairs' ) { + ($name, $value) = split(/\s+/, $param); + } else { #'Individual values' + $value = $param; + } + + if ( $value =~ /^\s*(\$|\@)(\w+)\s*$/ ) { + $value = $self->_export_value($2, $svc_acct); + } + + if ($self->option('param_style') eq 'Struct of name/value pairs' ) { + $x_struct{$name} = $value; + } else { #'Individual values' + push @x_param, $value; + } + + } + + my @x = (); + if ($self->option('param_style') eq 'Struct of name/value pairs' ) { + @x = ( \%x_struct ); + } else { #'Individual values' + @x = @x_param; + } + + #option to queue (or not) ? + + my $conn = Frontier::Client->new( url => $self->option('xmlrpc_url') ); + + my $result = $conn->call($method, @x); + + #XXX error checking? $result? from the call? + ''; +} + +sub _export_replace { + my( $self, $new, $old ) = (shift, shift, shift); + + my $method = $self->option($action.'_method'); + return '' if $method =~ /^\s*$/; + + my @params = split("\n", $self->option($action.'_params') ); + + my( @x_param ) = (); + my( %x_struct ) = (); + foreach my $param (@params) { + + my($name, $value) = ('', ''); + if ($self->option('param_style') eq 'Struct of name/value pairs' ) { + ($name, $value) = split(/\s+/, $param); + } else { #'Individual values' + $value = $param; + } + + if ( $value =~ /^\s*(\$|\@)(old|new)_(\w+)\s*$/ ) { + if ($2 eq 'old' ) { + $value = $self->_export_value($3, $old); + } elsif ( $2 eq 'new' ) { + $value = $self->_export_value($3, $new); + } else { + die 'guru meditation stella blue'; + } + } + + if ($self->option('param_style') eq 'Struct of name/value pairs' ) { + $x_struct{$name} = $value; + } else { #'Individual values' + push @x_param, $value; + } + + } + + my @x = (); + if ($self->option('param_style') eq 'Struct of name/value pairs' ) { + @x = ( \%x_struct ); + } else { #'Individual values' + @x = @x_param; + } + + #option to queue (or not) ? + + my $conn = Frontier::Client->new( url => $self->option('xmlrpc_url') ); + + my $result = $conn->call($method, @x); + + #XXX error checking? $result? from the call? + ''; + +} + +#comceptual false laziness w/shellcommands.pm +sub _export_value { + my( $self, $value, $svc_acct) = (shift, shift, shift); + + my %fields = map { $_=>1 } $svc_acct->fields; + + if ( $fields{$value} ) { + my $type = dbdef->table('svc_acct')->column($value)->type; + if ( $type =~ /^(int|serial)/i ) { + return Frontier::Client->new->int( $svc_acct->$value() ); + } elsif ( $value =~ /^last_log/ ) { + return Frontier::Client->new->date_time( $svc_acct->$value() ); #conversion? + } else { + return Frontier::Client->new->string( $svc_acct->$value() ); + } + } elsif ( $value eq 'domain' ) { + return Frontier::Client->new->string( $svc_acct->domain ); + } elsif ( $value eq 'crypt_password' ) { + return Frontier::Client->new->string( $svc_acct->crypt_password( $self->option('crypt') ) ); + } elsif ( $value eq 'ldap_password' ) { + return Frontier::Client->new->string( $svc_acct->ldap_password($self->option('crypt') ) ); + } elsif ( $value eq 'radius_groups' ) { + my @radius_groups = $svc_acct->radius_groups; + #XXX + } + +# my $cust_pkg = $svc_acct->cust_svc->cust_pkg; +# if ( $cust_pkg ) { +# no strict 'vars'; +# { +# no strict 'refs'; +# foreach my $custf (qw( company address1 address2 city state zip country +# daytime night fax otaker agent_custid locale +# )) +# { +# ${$custf} = $cust_pkg->cust_main->$custf(); +# } +# } +# $email = ( grep { $_ !~ /^(POST|FAX)$/ } $cust_pkg->cust_main->invoicing_list )[0]; +# } else { +# $email = ''; +# } + +# my ($reasonnum, $reasontext, $reasontypenum, $reasontypetext); +# if ( $cust_pkg && $action eq 'suspend' && +# (my $r = $cust_pkg->last_reason('susp')) ) +# { +# $reasonnum = $r->reasonnum; +# $reasontext = $r->reason; +# $reasontypenum = $r->reason_type; +# $reasontypetext = $r->reasontype->type; +# +# my %reasonmap = $self->_groups_susp_reason_map; +# my $userspec = ''; +# $userspec = $reasonmap{$reasonnum} +# if exists($reasonmap{$reasonnum}); +# $userspec = $reasonmap{$reasontext} +# if (!$userspec && exists($reasonmap{$reasontext})); +# +# my $suspend_user; +# if ( $userspec =~ /^\d+$/ ) { +# $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } ); +# } elsif ( $userspec =~ /^\S+\@\S+$/ ) { +# my ($username,$domain) = split(/\@/, $userspec); +# for my $user (qsearch( 'svc_acct', { 'username' => $username } )){ +# $suspend_user = $user if $userspec eq $user->email; +# } +# } elsif ($userspec) { +# $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } ); +# } +# +# @radius_groups = $suspend_user->radius_groups +# if $suspend_user; +# +# } else { +# $reasonnum = $reasontext = $reasontypenum = $reasontypetext = ''; +# } + +# $pkgnum = $cust_pkg ? $cust_pkg->pkgnum : ''; +# $custnum = $cust_pkg ? $cust_pkg->custnum : ''; + + ''; + +} + +1; + diff --git a/FS/FS/part_export/broadband_sqlradius.pm b/FS/FS/part_export/broadband_sqlradius.pm index 29bd28899..5806362b5 100644 --- a/FS/FS/part_export/broadband_sqlradius.pm +++ b/FS/FS/part_export/broadband_sqlradius.pm @@ -1,7 +1,7 @@ package FS::part_export::broadband_sqlradius; use strict; -use vars qw($DEBUG @ISA %options %info $conf); +use vars qw($DEBUG @ISA @pw_set %options %info $conf); use Tie::IxHash; use FS::Conf; use FS::Record qw( dbh str2time_sql ); #qsearch qsearchs ); @@ -13,6 +13,8 @@ FS::UID->install_callback(sub { $conf = new FS::Conf }); $DEBUG = 0; +@pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '.', ',' ); + tie %options, 'Tie::IxHash', 'datasrc' => { label=>'DBI data source ' }, 'username' => { label=>'Database username' }, @@ -106,8 +108,65 @@ sub radius_check { %check; } -sub _export_suspend {} -sub _export_unsuspend {} +sub radius_check_suspended { + my($self, $svc_broadband) = (shift, shift); + + return () unless $self->option('mac_as_password') + || length( $self->option('radius_password',1)); + + my $password_attrib = $conf->config('radius-password') || 'Password'; + ( + $password_attrib => join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) ) + ); +} + +#false laziness w/sqlradius.pm +sub _export_suspend { + my( $self, $svc_broadband ) = (shift, shift); + + 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 @newgroups = $self->suspended_usergroups($svc_broadband); + + unless (@newgroups) { #don't change password if assigning to a suspended group + + my $err_or_queue = $self->sqlradius_queue( + $svc_broadband->svcnum, 'insert', + 'check', $self->export_username($svc_broadband), + $self->radius_check_suspended($svc_broadband) + ); + unless ( ref($err_or_queue) ) { + $dbh->rollback if $oldAutoCommit; + return $err_or_queue; + } + + } + + my $error = + $self->sqlreplace_usergroups( + $svc_broadband->svcnum, + $self->export_username($svc_broadband), + '', + [ $svc_broadband->radius_groups('hashref') ], + \@newgroups, + ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; +} sub update_svc {} #do nothing diff --git a/FS/FS/part_export/netsapiens.pm b/FS/FS/part_export/netsapiens.pm index 867c19acc..6e2ee8ae3 100644 --- a/FS/FS/part_export/netsapiens.pm +++ b/FS/FS/part_export/netsapiens.pm @@ -5,6 +5,7 @@ use MIME::Base64; use Tie::IxHash; use FS::part_export; use Date::Format qw( time2str ); +use Regexp::Common qw/URI/; @ISA = qw(FS::part_export); $me = '[FS::part_export::netsapiens]'; @@ -42,6 +43,11 @@ my %features = ( 'sim' => 'Simultaneous Ring', ); +my %feature_param = ( + 'dnd' => 'n/a', + 'sim' => '$phonenum', +); + tie my %options, 'Tie::IxHash', 'login' => { label=>'NetSapiens tac2 User API username' }, 'password' => { label=>'NetSapiens tac2 User API password' }, @@ -80,6 +86,22 @@ END sub rebless { shift; } + +sub check_options { + my ($self, $options) = @_; + + my $rex = qr/$RE{URI}{HTTP}{-scheme => qr|https?|}/; # match any "http:" or "https:" URL + + for my $key (qw/url device_url/) { + if ($$options{$key} && ($$options{$key} !~ $rex)) { + return "Invalid (URL): " . $$options{$key}; + } + } + return ''; +} + + + sub ns_command { my $self = shift; $self->_ns_command('', @_); @@ -245,11 +267,14 @@ sub ns_create_or_update { ### foreach $feature (split /\s+/, $self->option('features') ) { + my $param= exists($feature_param{$feature}) ? $feature_param{$feature} : ''; + $param = $phonenum if $param eq '$phonenum'; + my $nsf = $self->ns_command( 'PUT', $self->ns_feature($svc_phone, $feature), 'control' => 'd', #User Control, disable 'expires' => 'never', #'ts' => '', #? - #'parameters' => '', + 'parameters' => $param, 'hour_match' => '*', 'time_frame' => '*', 'activation' => 'now', diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm index 910346bea..c360c9ef0 100644 --- a/FS/FS/part_export/sqlradius.pm +++ b/FS/FS/part_export/sqlradius.pm @@ -111,6 +111,7 @@ END 'options' => \%options, 'nodomain' => 'Y', 'nas' => 'Y', # show export_nas selection in UI + 'default_svc_class' => 'Internet', 'notes' => $notes1. 'This export does not export RADIUS realms (see also '. 'sqlradius_withdomain). '. @@ -250,6 +251,7 @@ sub _export_replace { ''; } +#false laziness w/broadband_sqlradius.pm sub _export_suspend { my( $self, $svc_acct ) = (shift, shift); @@ -297,7 +299,7 @@ sub _export_suspend { } sub _export_unsuspend { - my( $self, $svc_acct ) = (shift, shift); + my( $self, $svc_x ) = (shift, shift); local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -310,21 +312,21 @@ sub _export_unsuspend { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $err_or_queue = $self->sqlradius_queue( $svc_acct->svcnum, 'insert', - 'check', $self->export_username($svc_acct), $svc_acct->radius_check ); + my $err_or_queue = $self->sqlradius_queue( $svc_x->svcnum, 'insert', + 'check', $self->export_username($svc_x), $self->radius_check($svc_x) ); unless ( ref($err_or_queue) ) { $dbh->rollback if $oldAutoCommit; return $err_or_queue; } my $error; - my (@oldgroups) = $self->suspended_usergroups($svc_acct); + my (@oldgroups) = $self->suspended_usergroups($svc_x); $error = $self->sqlreplace_usergroups( - $svc_acct->svcnum, - $self->export_username($svc_acct), + $svc_x->svcnum, + $self->export_username($svc_x), '', \@oldgroups, - [ $svc_acct->radius_groups('hashref') ], + [ $svc_x->radius_groups('hashref') ], ); if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -358,14 +360,16 @@ sub sqlradius_queue { } sub suspended_usergroups { - my ($self, $svc_acct) = (shift, shift); + my ($self, $svc_x) = (shift, shift); + + return () unless $svc_x; - return () unless $svc_acct; + my $svc_table = $svc_x->table; #false laziness with FS::part_export::shellcommands #subclass part_export? - my $r = $svc_acct->cust_svc->cust_pkg->last_reason('susp'); + my $r = $svc_x->cust_svc->cust_pkg->last_reason('susp'); my %reasonmap = $self->_groups_susp_reason_map; my $userspec = ''; if ($r) { @@ -374,19 +378,19 @@ sub suspended_usergroups { $userspec = $reasonmap{$r->reason} if (!$userspec && exists($reasonmap{$r->reason})); } - my $suspend_user; - if ($userspec =~ /^\d+$/ ){ - $suspend_user = qsearchs( 'svc_acct', { 'svcnum' => $userspec } ); - }elsif ($userspec =~ /^\S+\@\S+$/){ + my $suspend_svc; + if ( $userspec =~ /^\d+$/ ){ + $suspend_svc = qsearchs( $svc_table, { 'svcnum' => $userspec } ); + } elsif ( $userspec =~ /^\S+\@\S+$/ && $svc_table eq 'svc_acct' ){ my ($username,$domain) = split(/\@/, $userspec); for my $user (qsearch( 'svc_acct', { 'username' => $username } )){ - $suspend_user = $user if $userspec eq $user->email; + $suspend_svc = $user if $userspec eq $user->email; } - }elsif ($userspec){ - $suspend_user = qsearchs( 'svc_acct', { 'username' => $userspec } ); + }elsif ( $userspec && $svc_table eq 'svc_acct' ){ + $suspend_svc = qsearchs( 'svc_acct', { 'username' => $userspec } ); } #esalf - return $suspend_user->radius_groups('hashref') if $suspend_user; + return $suspend_svc->radius_groups('hashref') if $suspend_svc; (); } @@ -756,7 +760,7 @@ sub usage_sessions { } -=item update_svc_acct +=item update_svc =cut @@ -1154,8 +1158,13 @@ sub _upgrade_exporttype { sub import_attrs { my $self = shift; - my $dbh = sqlradius_connect( map $self->option($_), + my $dbh = DBI->connect( map $self->option($_), qw( datasrc username password ) ); + unless ( $dbh ) { + warn "Error connecting to RADIUS server: $DBI::errstr\n"; + return; + } + my $usergroup = $self->option('usergroup') || 'usergroup'; my $error; warn "Importing RADIUS groups and attributes from ".$self->option('datasrc'). diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm index 0e44f5db5..22eb69815 100644 --- a/FS/FS/part_pkg/flat.pm +++ b/FS/FS/part_pkg/flat.pm @@ -151,8 +151,9 @@ sub calc_recur { if $self->recur_temporality eq 'preceding' && !$last_bill; my $charge = $self->base_recur($cust_pkg, $sdate); - if ( my $cutoff_day = $self->cutoff_day($cust_pkg) ) { - $charge = $self->calc_prorate(@_, $cutoff_day); + # always treat cutoff_day as a list + if ( my @cutoff_day = $self->cutoff_day($cust_pkg) ) { + $charge = $self->calc_prorate(@_, @cutoff_day); } elsif ( $param->{freq_override} ) { # XXX not sure if this should be mutually exclusive with sync_bill_date. @@ -161,6 +162,9 @@ sub calc_recur { $charge *= $param->{freq_override} if $param->{freq_override}; } + my $quantity = $cust_pkg->quantity || 1; + $charge *= $quantity; + my $discount = $self->calc_discount($cust_pkg, $sdate, $details, $param); return sprintf('%.2f', $charge - $discount); } @@ -174,7 +178,7 @@ sub cutoff_day { return (localtime($next_bill))[3]; } } - return 0; + return (); } sub base_recur { diff --git a/FS/FS/part_pkg/flat_introrate.pm b/FS/FS/part_pkg/flat_introrate.pm index 33cc3d48a..10c205609 100644 --- a/FS/FS/part_pkg/flat_introrate.pm +++ b/FS/FS/part_pkg/flat_introrate.pm @@ -32,8 +32,8 @@ sub base_recur { warn "flat_introrate base_recur requires date!" if !$time; my $now = $time ? $$time : time; - my ($duration) = ($self->option('intro_duration') =~ /^(\d+)$/); - unless ($duration) { + my ($duration) = ($self->option('intro_duration') =~ /^\s*(\d+)\s*$/); + unless (length($duration)) { die "Invalid intro_duration: " . $self->option('intro_duration'); } my $intro_end = $self->add_freq($cust_pkg->setup, $duration); diff --git a/FS/FS/part_pkg/prorate.pm b/FS/FS/part_pkg/prorate.pm index f930d417d..f8d03dcb5 100644 --- a/FS/FS/part_pkg/prorate.pm +++ b/FS/FS/part_pkg/prorate.pm @@ -49,7 +49,7 @@ sub calc_recur { sub cutoff_day { my $self = shift; - $self->option('cutoff_day', 1) || 1; + split(/\s*,\s*/, $self->option('cutoff_day', 1) || '1'); } 1; diff --git a/FS/FS/part_pkg/prorate_Mixin.pm b/FS/FS/part_pkg/prorate_Mixin.pm index a01b5c409..d148c963d 100644 --- a/FS/FS/part_pkg/prorate_Mixin.pm +++ b/FS/FS/part_pkg/prorate_Mixin.pm @@ -4,6 +4,7 @@ use strict; use vars qw( %info ); use Time::Local qw( timelocal timelocal_nocheck ); use Date::Format qw( time2str ); +use List::Util qw( min ); %info = ( 'disabled' => 1, @@ -76,8 +77,8 @@ day arrives. =cut sub calc_prorate { - my ($self, $cust_pkg, $sdate, $details, $param, $cutoff_day) = @_; - die "no cutoff_day" unless $cutoff_day; + my ($self, $cust_pkg, $sdate, $details, $param, @cutoff_days) = @_; + die "no cutoff_day" unless @cutoff_days; die "can't prorate non-monthly package\n" if $self->freq =~ /\D/; my $money_char = FS::Conf->new->config('money_char') || '$'; @@ -103,8 +104,19 @@ sub calc_prorate { $add_period = 1; } + # if the customer alreqady 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 ) { + $next_bill_day = (localtime($next_bill))[3]; + if ( grep {$_ == $next_bill_day} @cutoff_days ) { + # by removing all other cutoff days from the list + @cutoff_days = ($next_bill_day); + } + } + my ($mend, $mstart); - ($mnow, $mend, $mstart) = $self->_endpoints($mnow, $cutoff_day); + ($mnow, $mend, $mstart) = $self->_endpoints($mnow, @cutoff_days); # next bill date will be figured as $$sdate + one period $$sdate = $mstart; @@ -155,12 +167,12 @@ set, in which case it postpones the next bill to the cutoff day. sub prorate_setup { my $self = shift; my ($cust_pkg, $sdate) = @_; - my $cutoff_day = $self->cutoff_day($cust_pkg); + my @cutoff_days = $self->cutoff_day($cust_pkg); if ( ! $cust_pkg->bill and $self->option('prorate_defer_bill',1) - and $cutoff_day + and @cutoff_days ) { - my ($mnow, $mend, $mstart) = $self->_endpoints($sdate, $cutoff_day); + my ($mnow, $mend, $mstart) = $self->_endpoints($sdate, @cutoff_days); # If today is the cutoff day, set the next bill and setup both to # midnight today, so that the customer will be billed normally for a # month starting today. @@ -186,7 +198,9 @@ before the end of the prorate interval. =cut sub _endpoints { - my ($self, $mnow, $cutoff_day) = @_; + my $self = shift; + my $mnow = shift; + my @cutoff_days = sort {$a <=> $b} @_; # only works for freq >= 1 month; probably can't be fixed my ($sec, $min, $hour, $mday, $mon, $year) = (localtime($mnow))[0..5]; @@ -202,12 +216,20 @@ sub _endpoints { } my $mend; my $mstart; + # select the first cutoff day that's on or after the current day + my $cutoff_day = min( grep { $_ >= $mday } @cutoff_days ); + # if today is after the last cutoff, choose the first one + $cutoff_day ||= $cutoff_days[0]; + + # then, if today is on or after the selected day, set period to + # (cutoff day this month) - (cutoff day next month) if ( $mday >= $cutoff_day ) { $mend = timelocal_nocheck(0,0,0,$cutoff_day,$mon == 11 ? 0 : $mon + 1,$year+($mon==11)); $mstart = timelocal_nocheck(0,0,0,$cutoff_day,$mon,$year); } + # otherwise, set period to (cutoff day last month) - (cutoff day this month) else { $mend = timelocal_nocheck(0,0,0,$cutoff_day,$mon,$year); diff --git a/FS/FS/part_pkg/recur_Common.pm b/FS/FS/part_pkg/recur_Common.pm index 7233cc67f..9d7341b76 100644 --- a/FS/FS/part_pkg/recur_Common.pm +++ b/FS/FS/part_pkg/recur_Common.pm @@ -45,7 +45,7 @@ sub cutoff_day { if ( $recur_method eq 'prorate' or $recur_method eq 'subscription' ) { return $self->option('cutoff_day',1) || 1; } else { - return 0; + return (); } } @@ -58,26 +58,26 @@ sub calc_recur_Common { if ( $param->{'increment_next_bill'} ) { my $recur_method = $self->option('recur_method', 1) || 'anniversary'; - my $cutoff_day = $self->cutoff_day($cust_pkg); + my @cutoff_day = $self->cutoff_day($cust_pkg); $charges = $self->base_recur($cust_pkg); $charges += $param->{'override_charges'} if $param->{'override_charges'}; if ( $recur_method eq 'prorate' ) { - $charges = $self->calc_prorate(@_, $cutoff_day); + $charges = $self->calc_prorate(@_, @cutoff_day); $charges += $param->{'override_charges'} if $param->{'override_charges'}; } elsif ( $recur_method eq 'subscription' ) { my ($day, $mon, $year) = ( localtime($$sdate) )[ 3..5 ]; - if ( $day < $cutoff_day ) { + if ( $day < $cutoff_day[0] ) { if ( $mon == 0 ) { $mon=11; $year--; } else { $mon--; } } - $$sdate = timelocal(0, 0, 0, $cutoff_day, $mon, $year); + $$sdate = timelocal(0, 0, 0, $cutoff_day[0], $mon, $year); }#$recur_method diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm index aaad974cf..8c3d80d49 100644 --- a/FS/FS/part_pkg/voip_cdr.pm +++ b/FS/FS/part_pkg/voip_cdr.pm @@ -401,9 +401,10 @@ sub calc_usage { #my @invoice_details_sort; #first rate any outstanding CDRs not yet rated - foreach my $cdr ( - $svc_x->get_cdrs( %options ) - ) { + my $cdr_search = $svc_x->psearch_cdrs(%options); + $cdr_search->limit(1000); + $cdr_search->increment(0); # because we're changing their status as we go + while ( my $cdr = $cdr_search->fetch ) { my $error = $cdr->rate( 'part_pkg' => $self, @@ -414,14 +415,19 @@ sub calc_usage { ); die $error if $error; #?? + $cdr_search->adjust(1) if $cdr->freesidestatus eq ''; + # it was skipped without changing status, so increment the + # offset so that we don't re-fetch it on refill + } # $cdr #then add details to invoices & get a total $options{'status'} = 'rated'; - foreach my $cdr ( - $svc_x->get_cdrs( %options ) - ) { + $cdr_search = $svc_x->psearch_cdrs(%options); + $cdr_search->limit(1000); + $cdr_search->increment(0); + while ( my $cdr = $cdr_search->fetch ) { my $error; # at this point we officially Do Not Care about the rating method if ( $included_calls > 0 ) { @@ -436,7 +442,9 @@ sub calc_usage { } die $error if $error; $formatter->append($cdr); - } + + $cdr_search->adjust(1) if $cdr->freesidestatus eq 'rated'; + } #$cdr } $formatter->finish; #writes into $details diff --git a/FS/FS/part_pkg/voip_inbound.pm b/FS/FS/part_pkg/voip_inbound.pm index f4e51836f..9054f7b99 100644 --- a/FS/FS/part_pkg/voip_inbound.pm +++ b/FS/FS/part_pkg/voip_inbound.pm @@ -227,19 +227,22 @@ sub calc_usage { ) { my $svc_phone = $cust_svc->svc_x; - foreach my $cdr ( $svc_phone->get_cdrs( + my $cdr_search = $svc_phone->psearch_cdrs( 'inbound' => 1, 'default_prefix' => $self->option('default_prefix'), 'status' => '', # unprocessed only 'for_update' => 1, - ) - ) { + ); + $cdr_search->limit(1000); + $cdr_search->increment(0); + while ( my $cdr = $cdr_search->fetch ) { my $reason = $self->check_chargable( $cdr, 'option_cache' => \%opt_cache, ); if ( $reason ) { warn "not charging for CDR ($reason)\n" if $DEBUG; + $cdr_search->adjust(1); next; } @@ -310,6 +313,8 @@ sub calc_usage { die $error if $error; $formatter->append($cdr); + $cdr_search->adjust(1) if $cdr->freesidestatus eq ''; + } #$cdr } # $cust_svc # unshift @$details, { format => 'C', diff --git a/FS/FS/part_pkg/voip_tiered.pm b/FS/FS/part_pkg/voip_tiered.pm index e5dcf6dd8..d8d74c13f 100644 --- a/FS/FS/part_pkg/voip_tiered.pm +++ b/FS/FS/part_pkg/voip_tiered.pm @@ -132,9 +132,11 @@ sub calc_usage { $options{'inbound'} = ( $pass eq 'inbound' ); - foreach my $cdr ( - $svc_x->get_cdrs( %options ) - ) { + my $cdr_search = $svc_x->psearch_cdrs(%options); + $cdr_search->limit(1000); + $cdr_search->increment(0); + while ( my $cdr = $cdr_search->fetch ) { + if ( $DEBUG > 1 ) { warn "rating CDR $cdr\n". join('', map { " $_ => ". $cdr->{$_}. "\n" } keys %$cdr ); @@ -173,6 +175,8 @@ sub calc_usage { $total += $charge_min; + $cdr_search->adjust(1) if $cdr->freesidestatus eq ''; + } # $cdr } # $pass @@ -213,9 +217,10 @@ sub calc_usage { # tell the formatter what we're sending it $formatter->inbound($options{'inbound'}); - foreach my $cdr ( - $svc_x->get_cdrs( %options ) - ) { + my $cdr_search = $svc_x->psearch_cdrs(%options); + $cdr_search->limit(1000); + $cdr_search->increment(0); + while ( my $cdr = $cdr_search->fetch ) { my $object = $options{'inbound'} ? $cdr->cdr_termination( 1 ) #1: inbound @@ -242,6 +247,8 @@ sub calc_usage { $formatter->append($cdr); + $cdr_search->adjust(1) if $cdr->freesidestatus eq 'processing-tiered'; + } # $cdr } # $pass diff --git a/FS/FS/part_referral.pm b/FS/FS/part_referral.pm index c94c57e19..992e1c52a 100644 --- a/FS/FS/part_referral.pm +++ b/FS/FS/part_referral.pm @@ -163,10 +163,16 @@ simply using rather than editing advertising sources). sub all_part_referral { my $self = shift; + my $global = @_ ? shift : ''; + my $disabled = @_ ? shift : ''; + + my $hashref = $disabled ? {} : { 'disabled' => '' }; + my $and = $disabled ? ' WHERE ' : ' AND '; qsearch({ 'table' => 'part_referral', - 'extra_sql' => ' WHERE '. $self->acl_agentnum_sql(@_). ' ORDER BY refnum ', + 'hashref' => $hashref, + 'extra_sql' => $and. $self->acl_agentnum_sql($global). ' ORDER BY refnum ', }); } diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm index 7e592bf72..dd18e87f9 100644 --- a/FS/FS/part_svc.pm +++ b/FS/FS/part_svc.pm @@ -9,6 +9,7 @@ use FS::part_svc_column; use FS::part_export; use FS::export_svc; use FS::cust_svc; +use FS::part_svc_class; @ISA = qw(FS::Record); @@ -51,6 +52,8 @@ FS::Record. The following fields are currently supported: =item svcdb - table used for this service. See L<FS::svc_acct>, L<FS::svc_domain>, and L<FS::svc_forward>, among others. +=item classnum - Optional service class (see L<FS::svc_class>) + =item disabled - Disabled flag, empty or `Y' =item preserve - Preserve after cancellation, empty or 'Y' @@ -387,6 +390,7 @@ sub check { || $self->ut_enum('disabled', [ '', 'Y' ] ) || $self->ut_enum('preserve', [ '', 'Y' ] ) || $self->ut_enum('selfservice_access', [ '', 'hidden', 'readonly' ] ) + || $self->ut_foreign_keyn('classnum', 'part_svc_class', 'classnum' ) ; return $error if $error; diff --git a/FS/FS/part_svc_class.pm b/FS/FS/part_svc_class.pm new file mode 100644 index 000000000..d1c991582 --- /dev/null +++ b/FS/FS/part_svc_class.pm @@ -0,0 +1,126 @@ +package FS::part_svc_class; +use base qw( FS::class_Common ); + +use strict; +use FS::Record; # qw( qsearch qsearchs ); + +=head1 NAME + +FS::part_svc_class - Object methods for part_svc_class records + +=head1 SYNOPSIS + + use FS::part_svc_class; + + $record = new FS::part_svc_class \%hash; + $record = new FS::part_svc_class { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::part_svc_class object represents a service class. FS::part_svc_class +inherits from FS::Record. The following fields are currently supported: + +=over 4 + +=item classnum + +primary key + +=item classname + +classname + +=item disabled + +disabled + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new service class. To add the service class 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_svc_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 service class. 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('classnum') + || $self->ut_text('classname') + || $self->ut_enum('disabled', [ '', 'Y' ] ) + ; + 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/pay_batch/eft_canada.pm b/FS/FS/pay_batch/eft_canada.pm index 23dcc2dc9..ea9d58402 100644 --- a/FS/FS/pay_batch/eft_canada.pm +++ b/FS/FS/pay_batch/eft_canada.pm @@ -17,7 +17,51 @@ $name = 'eft_canada'; my ($trans_code, $process_date); +#ref http://gocanada.about.com/od/canadatravelplanner/a/canada_holidays.htm +my %holiday_yearly = ( + 1 => { map {$_=>1} 1 }, #new year's + 11 => { map {$_=>1} 11 }, #remembrance day + 12 => { map {$_=>1} 25 }, #christmas + 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 + 5 => { map {$_=>1} 20 }, #victoria day + 7 => { map {$_=>1} 1 }, #canada day + 8 => { map {$_=>1} 5 }, #First Monday of August Civic Holiday + 9 => { map {$_=>1} 2 }, #labour day + 10 => { map {$_=>1} 14 }, #thanksgiving + }, + 2014 => { 2 => { map {$_=>1} 17 }, #family day + 4 => { map {$_=>1} 18 }, #good friday + 4 => { map {$_=>1} 21 }, #easter monday + 5 => { map {$_=>1} 19 }, #victoria day + 7 => { map {$_=>1} 1 }, #canada day + 8 => { map {$_=>1} 4 }, #First Monday of August Civic Holiday + 9 => { map {$_=>1} 1 }, #labour day + 10 => { map {$_=>1} 13 }, #thanksgiving + }, + 2015 => { 2 => { map {$_=>1} 16 }, #family day + 4 => { map {$_=>1} 3 }, #good friday + 4 => { map {$_=>1} 6 }, #easter monday + 5 => { map {$_=>1} 18 }, #victoria day + 7 => { map {$_=>1} 1 }, #canada day + 8 => { map {$_=>1} 3 }, #First Monday of August Civic Holiday + 9 => { map {$_=>1} 7 }, #labour day + 10 => { map {$_=>1} 12 }, #thanksgiving + }, +); + %export_info = ( + init => sub { my $conf = shift; my @config = $conf->config('batchconfig-eft_canada'); @@ -25,9 +69,24 @@ my ($trans_code, $process_date); my $process_delay; ($trans_code, $process_delay) = @config[2,3]; $process_delay ||= 1; # days - $process_date = time2str('%D', time + ($process_delay * 86400)); + + my $pt = time + ($process_delay * 86400); + my @lt = localtime($pt); + while ( $lt[6] == 0 #Sunday + || $lt[6] == 6 #Saturday + || $holiday_yearly{ $lt[4]+1 }{ $lt[3] } + || $holiday{ $lt[5]+1900 }{ $lt[4]+1 }{ $lt[3] } + ) + { + $pt += 86400; + @lt = localtime($pt); + } + + $process_date = time2str('%D', $pt); }, + delimiter => '', # avoid blank lines for header/footer + # EFT Upload Specification for .CSV Files, Rev. 2.0 # not a true CSV format--strings aren't quoted, so be careful row => sub { diff --git a/FS/FS/prospect_main.pm b/FS/FS/prospect_main.pm index 5a4048f51..6adc852f8 100644 --- a/FS/FS/prospect_main.pm +++ b/FS/FS/prospect_main.pm @@ -244,7 +244,8 @@ Returns the locations (see L<FS::cust_location>) associated with this prospect. sub cust_location { my $self = shift; - qsearch( 'cust_location', { 'prospectnum' => $self->prospectnum } ); + qsearch( 'cust_location', { 'prospectnum' => $self->prospectnum, + 'custnum' => '' } ); } =item qual diff --git a/FS/FS/sales.pm b/FS/FS/sales.pm new file mode 100644 index 000000000..3cb61fde3 --- /dev/null +++ b/FS/FS/sales.pm @@ -0,0 +1,142 @@ +package FS::sales; + +use strict; +use vars qw( @ISA ); +use base qw( FS::Record ); +use Business::CreditCard 0.28; +use FS::Record qw( dbh qsearch qsearchs ); +use FS::cust_main; +use FS::cust_pkg; +use FS::agent_type; +use FS::reg_code; +use FS::TicketSystem; +#use FS::Conf; + +@ISA = qw( FS::m2m_Common FS::Record ); + +=head1 NAME + +FS::sales - Object methods for sales records + +=head1 SYNOPSIS + + use FS::sales; + + $record = new FS::sales \%hash; + $record = new FS::sales { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::sales object represents an example. FS::sales inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item salesnum + +primary key + +=item agentnum + +agentnum + +=item disabled + +disabled + + +=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 { 'sales'; } + +=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 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('salesnum') + || $self->ut_numbern('agentnum') + ; + return $error if $error; + + if ( $self->dbdef_table->column('disabled') ) { + $error = $self->ut_enum('disabled', [ '', 'Y' ] ); + 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/svc_Common.pm b/FS/FS/svc_Common.pm index ff00ce028..a6daf44c8 100644 --- a/FS/FS/svc_Common.pm +++ b/FS/FS/svc_Common.pm @@ -5,6 +5,7 @@ use vars qw( @ISA $noexport_hack $DEBUG $me $overlimit_missing_cust_svc_nonfatal_kludge ); use Carp qw( cluck carp croak confess ); #specify cluck have to specify them all use Scalar::Util qw( blessed ); +use Lingua::EN::Inflect qw( PL_N ); use FS::Conf; use FS::Record qw( qsearch qsearchs fields dbh ); use FS::cust_main_Mixin; @@ -243,6 +244,7 @@ sub insert { my $svcnum = $self->svcnum; my $cust_svc = $svcnum ? qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) : ''; + my $inserted_cust_svc = 0; #unless ( $svcnum ) { if ( !$svcnum or !$cust_svc ) { $cust_svc = new FS::cust_svc ( { @@ -256,6 +258,7 @@ sub insert { $dbh->rollback if $oldAutoCommit; return $error; } + $inserted_cust_svc = 1; $svcnum = $self->svcnum($cust_svc->svcnum); } else { #$cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum}); @@ -274,6 +277,10 @@ sub insert { || $self->preinsert_hook || $self->SUPER::insert; if ( $error ) { + if ( $inserted_cust_svc ) { + my $derror = $cust_svc->delete; + die $derror if $derror; + } $dbh->rollback if $oldAutoCommit; return $error; } @@ -844,8 +851,7 @@ sub set_auto_inventory { qsearchs('inventory_class', { 'classnum' => $classnum } ); return "Can't find inventory_class.classnum $classnum" unless $inventory_class; - return "Out of ". $inventory_class->classname. "s\n"; #Lingua:: BS - #for pluralizing + return "Out of ". PL_N($inventory_class->classname); } next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum; @@ -853,31 +859,38 @@ sub set_auto_inventory { $self->setfield( $field, $inventory_item->item ); #if $columnflag eq 'A' && $self->$field() eq ''; - $inventory_item->svcnum( $self->svcnum ); - my $ierror = $inventory_item->replace(); - if ( $ierror ) { - $dbh->rollback if $oldAutoCommit; - return "Error provisioning inventory: $ierror"; - } - if ( $old && $old->$field() && $old->$field() ne $self->$field() ) { my $old_inv = qsearchs({ - 'table' => 'inventory_item', - 'hashref' => { 'classnum' => $classnum, - 'svcnum' => $old->svcnum, - 'item' => $old->$field(), - }, + 'table' => 'inventory_item', + 'hashref' => { 'classnum' => $classnum, + 'svcnum' => $old->svcnum, + }, + 'extra_sql' => ' AND '. + '( ( svc_field IS NOT NULL AND svc_field = '.$dbh->quote($field).' )'. + ' OR ( svc_field IS NULL AND item = '. dbh->quote($old->$field).' )'. + ')', }); if ( $old_inv ) { $old_inv->svcnum(''); + $old_inv->svc_field(''); my $oerror = $old_inv->replace; if ( $oerror ) { $dbh->rollback if $oldAutoCommit; return "Error unprovisioning inventory: $oerror"; } + } else { + warn "old inventory_item not found for $field ". $self->$field; } } + $inventory_item->svcnum( $self->svcnum ); + $inventory_item->svc_field( $field ); + my $ierror = $inventory_item->replace(); + if ( $ierror ) { + $dbh->rollback if $oldAutoCommit; + return "Error provisioning inventory: $ierror"; + } + } $dbh->commit or die $dbh->errstr if $oldAutoCommit; @@ -906,6 +919,7 @@ sub return_inventory { foreach my $inventory_item ( $self->inventory_item ) { $inventory_item->svcnum(''); + $inventory_item->svc_field(''); my $error = $inventory_item->replace(); if ( $error ) { $dbh->rollback if $oldAutoCommit; diff --git a/FS/FS/svc_Radius_Mixin.pm b/FS/FS/svc_Radius_Mixin.pm index 731c83262..ac97eab58 100644 --- a/FS/FS/svc_Radius_Mixin.pm +++ b/FS/FS/svc_Radius_Mixin.pm @@ -1,11 +1,14 @@ package FS::svc_Radius_Mixin; +use base qw( FS::m2m_Common FS::svc_Common ); use strict; -use base qw(FS::m2m_Common FS::svc_Common); -use FS::Record qw(qsearch); +use FS::Record qw( qsearch dbh ); use FS::radius_group; use FS::radius_usergroup; -use Carp qw(confess); +use Carp qw( confess ); + +# not really a mixin since it overrides insert/replace/delete and has svc_Common +# as a base class, should probably be renamed svc_Radius_Common =head1 NAME @@ -17,15 +20,34 @@ FS::svc_Radius_Mixin - partial base class for services with RADIUS groups =cut - sub insert { my $self = shift; - $self->SUPER::insert(@_) - || $self->process_m2m( - 'link_table' => 'radius_usergroup', - 'target_table' => 'radius_group', - 'params' => $self->usergroup, - ); + + 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 $error = $self->SUPER::insert(@_) + || $self->process_m2m( + 'link_table' => 'radius_usergroup', + 'target_table' => 'radius_group', + 'params' => $self->usergroup, + ); + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; } sub replace { @@ -33,22 +55,63 @@ sub replace { my $old = shift; $old = $new->replace_old if !defined($old); + 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; + $old->usergroup; # make sure this is cached for exports - $new->process_m2m( - 'link_table' => 'radius_usergroup', - 'target_table' => 'radius_group', - 'params' => $new->usergroup, - ) || $new->SUPER::replace($old, @_); + + my $error = $new->process_m2m( + 'link_table' => 'radius_usergroup', + 'target_table' => 'radius_group', + 'params' => $new->usergroup, + ) + || $new->SUPER::replace($old, @_); + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; } sub delete { my $self = shift; - $self->SUPER::delete(@_) - || $self->process_m2m( - 'link_table' => 'radius_usergroup', - 'target_table' => 'radius_group', - 'params' => [], - ); + + 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 $error = $self->SUPER::delete(@_) + || $self->process_m2m( + 'link_table' => 'radius_usergroup', + 'target_table' => 'radius_group', + 'params' => [], + ); + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; } sub usergroup { diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm index 64cc3770e..82102697d 100755 --- a/FS/FS/svc_broadband.pm +++ b/FS/FS/svc_broadband.pm @@ -543,9 +543,9 @@ sub _check_ip_addr { sub _check_duplicate { my $self = shift; - - $self->lock_table; - + # Not a reliable check because the table isn't locked, but + # that's why we have a unique index. This is just to give a + # friendlier error message. my @dup; @dup = $self->find_duplicates('global', 'ip_addr'); if ( @dup ) { diff --git a/FS/FS/svc_pbx.pm b/FS/FS/svc_pbx.pm index f8b96050d..4182a1315 100644 --- a/FS/FS/svc_pbx.pm +++ b/FS/FS/svc_pbx.pm @@ -3,6 +3,7 @@ package FS::svc_pbx; use strict; use base qw( FS::svc_External_Common ); use FS::Record qw( qsearch qsearchs dbh ); +use FS::PagedSearch qw( psearch ); use FS::Conf; use FS::cust_svc; use FS::svc_phone; @@ -259,11 +260,13 @@ sub _check_duplicate { return ''; } -=item get_cdrs +=item psearch_cdrs OPTIONS -Returns a set of Call Detail Records (see L<FS::cdr>) associated with this -service. By default, "associated with" means that the "charged_party" field of -the CDR matches the "title" field of the service. +Returns a paged search (L<FS::PagedSearch>) for Call Detail Records +associated with this service. By default, "associated with" means that +the "charged_party" field of the CDR matches the "title" field of the +service. To access the CDRs themselves, call "->fetch" on the resulting +object. =over 2 @@ -295,7 +298,7 @@ to allow title to indicate a range of IP addresses. =cut -sub get_cdrs { +sub psearch_cdrs { my($self, %options) = @_; my %hash = (); my @where = (); @@ -343,15 +346,26 @@ sub get_cdrs { my $extra_sql = ( keys(%hash) ? ' AND ' : ' WHERE ' ). join(' AND ', @where ) if @where; - my @cdrs = - qsearch( { + psearch( { 'table' => 'cdr', 'hashref' => \%hash, 'extra_sql' => $extra_sql, 'order_by' => "ORDER BY startdate $for_update", - } ); + } ); +} + +=item get_cdrs (DEPRECATED) + +Like psearch_cdrs, but returns all the L<FS::cdr> objects at once, in a +single list. Arguments are the same as for psearch_cdrs. This can take +an unreasonably large amount of memory and is best avoided. - @cdrs; +=cut + +sub get_cdrs { + my $self = shift; + my $psearch = $self->psearch_cdrs($_); + qsearch ( $psearch->{query} ) } =back diff --git a/FS/FS/svc_phone.pm b/FS/FS/svc_phone.pm index b395ea605..1296c1e85 100644 --- a/FS/FS/svc_phone.pm +++ b/FS/FS/svc_phone.pm @@ -7,6 +7,7 @@ use Data::Dumper; use Scalar::Util qw( blessed ); use FS::Conf; use FS::Record qw( qsearch qsearchs dbh ); +use FS::PagedSearch qw( psearch ); use FS::Msgcat qw(gettext); use FS::part_svc; use FS::phone_device; @@ -648,11 +649,13 @@ sub cust_location_or_main { $cust_pkg ? $cust_pkg->cust_location_or_main : ''; } -=item get_cdrs +=item psearch_cdrs OPTIONS -Returns a set of Call Detail Records (see L<FS::cdr>) associated with this -service. By default, "associated with" means that either the "src" or the -"charged_party" field of the CDR matches the "phonenum" field of the service. +Returns a paged search (L<FS::PagedSearch>) for Call Detail Records +associated with this service. By default, "associated with" means that +either the "src" or the "charged_party" field of the CDR matches the +"phonenum" field of the service. To access the CDRs themselves, call +"->fetch" on the resulting object. =over 2 @@ -676,11 +679,16 @@ with the chosen prefix. =item by_svcnum: not supported for svc_phone +=item billsec_sum: Instead of returning all of the CDRs, return a single +record (as an L<FS::cdr> object) with the sum of the 'billsec' field over +the entire result set. + =back =cut -sub get_cdrs { +sub psearch_cdrs { + my($self, %options) = @_; my @fields; my %hash; @@ -739,18 +747,30 @@ sub get_cdrs { my $extra_sql = ( keys(%hash) ? ' AND ' : ' WHERE ' ). join(' AND ', @where ); - my @cdrs = - qsearch( { + psearch( { 'table' => 'cdr', 'hashref' => \%hash, 'extra_sql' => $extra_sql, 'order_by' => $options{'billsec_sum'} ? '' : "ORDER BY startdate $for_update", 'select' => $options{'billsec_sum'} ? 'sum(billsec) as billsec_sum' : '*', - } ); + } ); +} + +=item get_cdrs (DEPRECATED) + +Like psearch_cdrs, but returns all the L<FS::cdr> objects at once, in a +single list. Arguments are the same as for psearch_cdrs. This can take +an unreasonably large amount of memory and is best avoided. - @cdrs; +=cut + +sub get_cdrs { + my $self = shift; + my $psearch = $self->psearch_cdrs(@_); + qsearch ( $psearch->{query} ) } + =back =head1 BUGS diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm index 48c01967d..e9496e4f5 100644 --- a/FS/FS/tax_rate.pm +++ b/FS/FS/tax_rate.pm @@ -993,7 +993,7 @@ sub _perform_batch_import { } push @insert_list, - 'DETAIL', "$dir/".$files{detail}, \&FS::tax_rate::batch_import, $format + 'DETAIL', "$dir/".$files{detailfile}, \&FS::tax_rate::batch_import, $format if $format =~ /update/; $error ||= _perform_cch_tax_import( $job, diff --git a/FS/MANIFEST b/FS/MANIFEST index 0c9cc5452..e7aba204d 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -37,7 +37,6 @@ FS/ClientAPI/Bulk.pm FS/ClientAPI/MasonComponent.pm FS/ClientAPI/MyAccount.pm FS/ClientAPI/PrepaidPhone.pm -FS/ClientAPI/SGNG.pm FS/ClientAPI/Signup.pm FS/Conf.pm FS/ConfItem.pm @@ -634,3 +633,11 @@ FS/contact_class.pm t/contact_class.t FS/upgrade_journal.pm t/upgrade_journal.t +FS/sales.pm +t/sales.t +FS/access_groupsales.pm +t/access_groupsales.t +FS/part_svc_class.pm +t/part_svc_class.t +FS/ftp_target.pm +t/ftp_target.t diff --git a/FS/bin/freeside-check b/FS/bin/freeside-check index 9930aae6c..fdfc66af0 100644 --- a/FS/bin/freeside-check +++ b/FS/bin/freeside-check @@ -5,7 +5,6 @@ use strict; use FS::UID qw( adminsuidsetup ); use FS::Cron::check qw( check_queued check_selfservice check_apache check_bop_failures - check_sg check_sg_login check_sgng alert error_msg ); @@ -21,11 +20,5 @@ check_queued or alert('Queue daemon not running', @emails); check_selfservice or alert(error_msg(), @emails); check_apache or alert('Apache not running: '. error_msg(), @emails); -#no-ops unless you are sg -my $sg = 'FS::ClientAPI::SG'; -check_sg or alert("$sg not responding: ". error_msg(), @emails); -check_sg_login or alert("$sg login errort: ". error_msg(), @emails); -check_sgng or alert("${sg}NG not responding: ". error_msg(), @emails); - check_bop_failures or alert(error_msg(), @emails); diff --git a/FS/t/access_groupsales.t b/FS/t/access_groupsales.t new file mode 100644 index 000000000..50993cf26 --- /dev/null +++ b/FS/t/access_groupsales.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::access_groupsales; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/ftp_target.t b/FS/t/ftp_target.t new file mode 100644 index 000000000..1a5928118 --- /dev/null +++ b/FS/t/ftp_target.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::ftp_target; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/part_svc_class.t b/FS/t/part_svc_class.t new file mode 100644 index 000000000..e838c0b30 --- /dev/null +++ b/FS/t/part_svc_class.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_svc_class; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/sales.t b/FS/t/sales.t new file mode 100644 index 000000000..e47eb398b --- /dev/null +++ b/FS/t/sales.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::sales; +$loaded=1; +print "ok 1\n"; @@ -115,13 +115,19 @@ RT_PATH = /opt/rt3 FREESIDE_PATH = `pwd` PERL_INC_DEV_KLUDGE = /usr/local/share/perl/5.14.2/ -VERSION=3.0git -TAG=freeside_3_0 +VERSION := `grep '^$$VERSION' FS/FS.pm | cut -d\' -f2` +TAG := freeside_`grep '^$$VERSION' FS/FS.pm | cut -d\' -f2 | perl -pe 's/\./_/g'` -DEBVERSION = `echo ${VERSION} | perl -pe 's/(\d)([a-z])/\1~\2/'`-1 +#DEBVERSION = `echo ${VERSION} | perl -pe 's/(\d)([a-z])/\1~\2/'`-1 TEXMFHOME := "\$$TEXMFHOME" +ver: + @echo "${VERSION}" + +tag: + @echo "${TAG}" + help: @echo "supported targets:" @echo " create-database create-config" @@ -180,9 +186,6 @@ perl-modules: [ -e Makefile ] || perl Makefile.PL; \ make; \ perl -p -i -e "\ - s/%%%VERSION%%%/${VERSION}/g;\ - " blib/lib/FS.pm;\ - perl -p -i -e "\ s|%%%FREESIDE_CONF%%%|${FREESIDE_CONF}|g;\ s|%%%FREESIDE_CACHE%%%|${FREESIDE_CACHE}|g;\ s'%%%FREESIDE_DOCUMENT_ROOT%%%'${FREESIDE_DOCUMENT_ROOT}'g; \ @@ -337,7 +340,7 @@ configure-rt: --with-db-dba=${DB_USER} \ --with-db-database=${RT_DB_DATABASE} \ --with-db-rt-user=${DB_USER} \ - --with-db-rt-pass=${DB_PASSWORD} \ + --with-db-rt-pass="${DB_PASSWORD}" \ --with-web-user=freeside \ --with-web-group=freeside \ --with-rt-group=freeside \ diff --git a/conf/invoice-unitprice b/conf/invoice-unitprice new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/conf/invoice-unitprice diff --git a/fs_selfservice/FS-SelfService/SelfService.pm b/fs_selfservice/FS-SelfService/SelfService.pm index af04fcc74..c22e4269e 100644 --- a/fs_selfservice/FS-SelfService/SelfService.pm +++ b/fs_selfservice/FS-SelfService/SelfService.pm @@ -32,6 +32,7 @@ $socket .= '.'.$tag if defined $tag && length($tag); 'switch_acct' => 'MyAccount/switch_acct', 'customer_info' => 'MyAccount/customer_info', 'customer_info_short' => 'MyAccount/customer_info_short', + 'billing_history' => 'MyAccount/billing_history', 'edit_info' => 'MyAccount/edit_info', #add to ss cgi! 'invoice' => 'MyAccount/invoice', 'invoice_pdf' => 'MyAccount/invoice_pdf', @@ -104,22 +105,6 @@ $socket .= '.'.$tag if defined $tag && length($tag); 'call_time' => 'PrepaidPhone/call_time', 'call_time_nanpa' => 'PrepaidPhone/call_time_nanpa', 'phonenum_balance' => 'PrepaidPhone/phonenum_balance', - #izoom - #'bulk_processrow' => 'Bulk/processrow', - #conflicts w/Agent one# 'check_username' => 'Bulk/check_username', - #sg - 'ping' => 'SGNG/ping', - 'decompify_pkgs' => 'SGNG/decompify_pkgs', - 'previous_payment_info' => 'SGNG/previous_payment_info', - 'previous_payment_info_renew_info' - => 'SGNG/previous_payment_info_renew_info', - 'previous_process_payment' => 'SGNG/previous_process_payment', - 'previous_process_payment_order_pkg' - => 'SGNG/previous_process_payment_order_pkg', - 'previous_process_payment_change_pkg' - => 'SGNG/previous_process_payment_change_pkg', - 'previous_process_payment_order_renew' - => 'SGNG/previous_process_payment_order_renew', ); @EXPORT_OK = ( keys(%autoload), diff --git a/fs_selfservice/FS-SelfService/cgi/myaccount.html b/fs_selfservice/FS-SelfService/cgi/myaccount.html index d27f688e3..5370f7c22 100644 --- a/fs_selfservice/FS-SelfService/cgi/myaccount.html +++ b/fs_selfservice/FS-SelfService/cgi/myaccount.html @@ -2,7 +2,9 @@ <%= include('header', 'My Account') %> Hello <%= $name %>!<BR><BR> -<%= $small_custview %> + +<%= include('small_custview') %> + <BR> <%= if ( $access_pkgnum ) { $OUT .= qq!Balance: <B>\$$balance</B><BR><BR>!; @@ -34,17 +36,17 @@ Hello <%= $name %>!<BR><BR> $OUT .= '<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=2 BGCOLOR="#eeeeee">'. '<TR><TH BGCOLOR="#ff6666" COLSPAN=5>Open Invoices</TH></TR>'; my $link = qq!<A HREF="<%= $url %>myaccount!; - my $col1 = "ffffff"; - my $col2 = "dddddd"; + my $col1 = $stripe1_bgcolor || '#ffffff'; + my $col2 = $stripe2_bgcolor || '#dddddd'; my $col = $col1; foreach my $invoice ( @open_invoices ) { - my $td = qq!<TD BGCOLOR="#$col">!; + my $td = qq!<TD BGCOLOR="$col">!; my $a=qq!<A HREF="${url}view_invoice;invnum=!. $invoice->{'invnum'}. '">'; $OUT .= "<TR>$td${a}Invoice #". $invoice->{'invnum'}. "</A></TD>$td</TD>". "$td$a". $invoice->{'date'}. "</A></TD>$td</TD>". - qq!<TD BGCOLOR="#$col" ALIGN="right">$a\$!. $invoice->{'owed'}. + qq!<TD BGCOLOR="$col" ALIGN="right">$a\$!. $invoice->{'owed'}. '</A></TD>'. '</TR>'; $col = $col eq $col1 ? $col2 : $col1; @@ -61,12 +63,12 @@ Hello <%= $name %>!<BR><BR> '<TR><TH BGCOLOR="#ff6666" COLSPAN="3">Support Time Remaining</TH>'. '</TR><TR><TH>Package</TH><TH></TH>'. '<TH>Time Remaining</TH></TR>'; - my $col1 = "ffffff"; - my $col2 = "dddddd"; + my $col1 = $stripe1_bgcolor || '#ffffff'; + my $col2 = $stripe2_bgcolor || '#dddddd'; my $col = $col1; foreach my $support ( @support_services ) { - my $td = qq!<TD BGCOLOR="#$col">!; + my $td = qq!<TD BGCOLOR="$col">!; my $a = qq!<A HREF="${url}view_support_details;svcnum=!. $support->{'svcnum'}. '">'; $OUT .= diff --git a/fs_selfservice/FS-SelfService/cgi/small_custview.html b/fs_selfservice/FS-SelfService/cgi/small_custview.html new file mode 100644 index 000000000..8d6e07368 --- /dev/null +++ b/fs_selfservice/FS-SelfService/cgi/small_custview.html @@ -0,0 +1,69 @@ +<DIV ID="fs_small_custview"> + +Customer #<B><%= $custnum %></B> + - <B><FONT COLOR="#<%= $statuscolor %>"><%= ucfirst($status)%></FONT></B> + +<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=2 BGCOLOR="<%= $box_bgcolor ||= '#c0c0c0' %>"> + <TR> + <TD VALIGN="top"> + <%= $has_ship_address + ? '<I><FONT SIZE="-1">Billing Address</FONT></I><BR>' + : '' + %> + <%= $first %> <%= $last %><BR> + <%= $company ? $company.'<BR>' : '' %> + <%= $address1 %><BR> + <%= $address2 ? $address2.'<BR>' : '' %> + <%= $city %>, <%= $state %> <%= $zip %><BR> + <%= $country && $country ne ($countrydefault||'US') + ? $country.'<BR>' + : '' + %> + <%= if ( $daytime && $night ) { + $OUT .= "<BR>Day Phone $daytime<BR>Night Phone $night"; + } elsif ( $daytime || $night ) { + $OUT .= '<BR>'. ($daytime || $night); + } + ''; + %> + <%= $fax ? "<BR>Fax $fax" : '' %> + + </TD> + +<%= if ( $has_ship_address ) { + + $OUT .= '<TD> </TD>'. + '<TD VALIGN="top">'. + '<I><FONT SIZE="-1">Service Address</FONT></I><BR>'. + "$ship_first $ship_last<BR>"; + $OUT .= "$ship_company<BR>" if $ship_company; + $OUT .= "$ship_address1<BR>"; + $OUT .= "$ship_address2<BR>" if $ship_address2; + $OUT .= "$ship_city, $ship_state $ship_zip<BR>"; + $OUT .= "$ship_country<BR>" + if $ship_country && $ship_country ne ($countrydefault||'US'); + + + if ( $ship_daytime && $ship_night ) { + $OUT .= "<BR>Day Phone $ship_daytime<BR>Night Phone $ship_night"; + } elsif ( $ship_daytime || $ship_night ) { + $OUT .= '<BR'. ($ship_daytime || $ship_night); + } + + $OUT .= "<BR>Fax $ship_fax" if $ship_fax; + + $OUT .= '</TD>'; +} +''; +%> + +</TR></TABLE> + +<%= unless ( $access_pkgnum ) { + $OUT .= '<BR>Balance: <B>$'. $balance. '</B><BR>'; + } + ''; +%> + +</DIV> + diff --git a/fs_selfservice/FS-SelfService/cgi/ticket_summary.html b/fs_selfservice/FS-SelfService/cgi/ticket_summary.html index c840ca9d6..4333c4e92 100644 --- a/fs_selfservice/FS-SelfService/cgi/ticket_summary.html +++ b/fs_selfservice/FS-SelfService/cgi/ticket_summary.html @@ -1,10 +1,10 @@ <SCRIPT TYPE="text/javascript" SRC="overlibmws.js"></SCRIPT> <TABLE BORDER=0 CELLSPACING=0 CELLPADDING=2 BGCOLOR="#eeeeee"> -<TR><TH BGCOLOR="#ff6666" COLSPAN=15>Open Tickets</TH></TR> +<TR><TH BGCOLOR="#ff6666" COLSPAN=16>Open Tickets</TH></TR> <TR> <%= -my $col1 = "ffffff"; -my $col2 = "dddddd"; +my $col1 = $stripe1_bgcolor || "#ffffff"; +my $col2 = $stripe2_bgcolor || "#dddddd"; my $col = $col1; my $can_set_priority = @@ -26,12 +26,15 @@ my @titles = ('#', qw(Subject Queue Status Created Due)); push @titles, 'Estimated<BR>Hours'; push @titles, 'Priority'; -$OUT .= join("\n", map { "<TH VALIGN=\"top\">$_</TH><TH> </TH>" } @titles) +$box_bgcolor ||= '#c0c0c0'; +my $th = qq(<TH BGCOLOR="$box_bgcolor"); + +$OUT .= join("\n", map { "$th VALIGN=\"top\">$_</TH>$th> </TH>" } @titles) . '</TR>'; foreach my $ticket ( @tickets ) { my $id = $ticket->{'id'}; - my $td = qq!<TD BGCOLOR="#$col">!; + my $td = qq!<TD BGCOLOR="$col">!; my $space = $td.' </TD>'; my $link = qq!<A HREF="${url}tktview;ticket_id=$id">!; $OUT .= '<TR>' . @@ -41,31 +44,34 @@ $td. $ticket->{'queue'} . '</TD>'. $space . $td. $ticket->{'status'} . '</TD>'. $space . $td. $date_formatter->($ticket->{'created'}) . '</TD>'. $space . $td. $date_formatter->($ticket->{'due'}) . '</TD>'. $space . -qq!<TD BGCOLOR="#$col" ALIGN="right">!. +qq!<TD BGCOLOR="$col" ALIGN="right">!. ($ticket->{'timeestimated'} ? sprintf('%.1f', $ticket->{'timeestimated'} / 60.0) # .1f? : '' ) . '</TD>'. $space . -qq!<TD BGCOLOR="#$col" ALIGN="right">!; +qq!<TD BGCOLOR="$col" ALIGN="right">!; if ( $can_set_priority ) { $OUT .= '<INPUT TYPE="hidden" NAME="ticket'.$id.'" VALUE="1">' . '<INPUT TYPE="text" SIZE=4 NAME="priority'.$id.'"' . 'VALUE="'.$ticket->{'_selfservice_priority'}.'"></TD>'; if ( exists($ticket_error{$id}) ) { # display error message compactly - $OUT .= '<TD><FONT COLOR="#ff0000" onmouseover="'. + $OUT .= $td. '<FONT COLOR="#ff0000" onmouseover="'. "return overlib('".$ticket_error{$id}."', AUTOSTATUS, WRAP);" . '" onmouseout="nd();">*</FONT></TD>'; + } else { + $OUT .= $td.'</TD>'; } } else { - $OUT .= ($ticket->{'content'} || $ticket->{'priority'}) . '</TD>'; + $OUT .= ($ticket->{'content'} || $ticket->{'priority'}) . '</TD>'. + $td.'</TD>'; } $OUT .= '</TR>'; $col = $col eq $col1 ? $col2 : $col1; } #foreach my $ticket if ( $can_set_priority ) { - $OUT .= '<TR><TD COLSPAN=15 ALIGN="right"> + $OUT .= '<TR><TD COLSPAN=16 ALIGN="right"> <INPUT TYPE="submit" VALUE="Save changes"></TD></TR></FORM>'; } %> diff --git a/fs_selfservice/php/freeside.class.new.php b/fs_selfservice/php/freeside.class.new.php new file mode 100644 index 000000000..b9d125eb6 --- /dev/null +++ b/fs_selfservice/php/freeside.class.new.php @@ -0,0 +1,71 @@ +<?php + +#pre-php 5.4 compatible version? +function flatten($hash) { + if ( !is_array($hash) ) return $hash; + $flat = array(); + + array_walk($hash, function($value, $key, &$to) { + array_push($to, $key, $value); + }, $flat); + + if ( PHP_VERSION_ID >= 50400 ) { + + #php 5.4+ (deb 7+) + foreach ($hash as $key => $value) { + $flat[] = $key; + $flat[] = $value; + } + + } + + return($flat); +} + +#php 5.4+? +#function flatten($hash) { +# if ( !is_array($hash) ) return $hash; +# +# $flat = array(); +# +# foreach ($hash as $key => $value) { +# $flat[] = $key; +# $flat[] = $value; +# } +# +# return($flat); +#} + +class FreesideSelfService { + + //Change this to match the location of your selfservice xmlrpc.cgi or daemon + #var $URL = 'https://localhost/selfservice/xmlrpc.cgi'; + var $URL = 'http://localhost/selfservice/xmlrpc.cgi'; + + function FreesideSelfService() { + $this; + } + + public function __call($name, $arguments) { + + error_log("[FreesideSelfService] $name called, sending to ". $this->URL); + + $request = xmlrpc_encode_request("FS.ClientAPI_XMLRPC.$name", flatten($arguments[0])); + $context = stream_context_create( array( 'http' => array( + 'method' => "POST", + 'header' => "Content-Type: text/xml", + 'content' => $request + ))); + $file = file_get_contents($this->URL, false, $context); + $response = xmlrpc_decode($file); + if (xmlrpc_is_fault($response)) { + trigger_error("[FreesideSelfService] XML-RPC communication error: $response[faultString] ($response[faultCode])"); + } else { + //error_log("[FreesideSelfService] $response"); + return $response; + } + } + +} + +?> diff --git a/htetc/handler.pl b/htetc/handler.pl index e065f092b..cea366134 100644 --- a/htetc/handler.pl +++ b/htetc/handler.pl @@ -31,11 +31,30 @@ my $ah = new HTML::Mason::ApacheHandler ( # #chown (Apache->server->uid, Apache->server->gid, $interp->files_written); +my $protect_fds; + sub handler { #($r) = @_; my $r = shift; + #from rt/bin/webmux.pl(.in) + if ( !$protect_fds && $ENV{'MOD_PERL'} && exists $ENV{'MOD_PERL_API_VERSION'} + && $ENV{'MOD_PERL_API_VERSION'} >= 2 + ) { + # under mod_perl2, STDIN and STDOUT get closed and re-opened, + # however they are not on FD 0 and 1. In this case, the next + # socket that gets opened will occupy one of these FDs, and make + # all system() and open "|-" calls dangerous; for example, the + # DBI handle can get this FD, which later system() calls will + # close by putting garbage into the socket. + $protect_fds = []; + push @{$protect_fds}, IO::Handle->new_from_fd(0, "r") + if fileno(STDIN) != 0; + push @{$protect_fds}, IO::Handle->new_from_fd(1, "w") + if fileno(STDOUT) != 1; + } + # If you plan to intermix images in the same directory as # components, activate the following to prevent Mason from # evaluating image files as components. @@ -65,7 +84,7 @@ sub handler local $SIG{__WARN__}; local $SIG{__DIE__}; - RT::Init(); + my_rt_init(); $ah->interp($rt_interp); @@ -74,7 +93,7 @@ sub handler local $SIG{__WARN__}; local $SIG{__DIE__}; - RT::Init() if $RT::VERSION; #for lack of something else + my_rt_init(); #we don't want the RT error handlers under FS { @@ -109,4 +128,19 @@ sub handler $status; } +my $rt_initialized = 0; + +sub my_rt_init { + return unless $RT::VERSION; + + if ( $rt_initialized ) { + RT::ConnectToDatabase(); + RT::InitSignalHandlers(); + } else { + RT::LoadConfig(); + RT::Init(); + $rt_initialized++; + } +} + 1; diff --git a/httemplate/browse/ftp_target.html b/httemplate/browse/ftp_target.html new file mode 100644 index 000000000..4a5782058 --- /dev/null +++ b/httemplate/browse/ftp_target.html @@ -0,0 +1,56 @@ +<& elements/browse.html, + 'title' => 'FTP targets', + 'menubar' => [ 'Add a target' => $p.'edit/ftp_target.html', ], + 'name' => 'FTP targets', + 'query' => { 'table' => 'ftp_target', + 'hashref' => {}, + }, + 'count_query' => $count_query, + 'header' => [ '#', + 'Server', + 'Username', + 'Password', + 'Path', + 'Protocol', + '', #handling + ], + 'fields' => [ 'targetnum', + 'hostname', + 'username', + 'password', + 'path', + sub { + my $ftp_target = shift; + my $label; + if ($ftp_target->secure) { + $label = 'SFTP'; + $label .= ' (port '.$ftp_target->port.')' + if $ftp_target->port != 22; + } + else { + $label = 'FTP'; + $label .= ' (port '.$ftp_target->port.')' + if $ftp_target->port != 21; + } + $label; + }, + 'handling', + ], + 'links' => [ $link, $link ], +&> +</TABLE> + +<% include('/elements/footer.html') %> + +<%once> + +my $count_query = 'SELECT COUNT(*) FROM ftp_target'; + +</%once> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $link = [ $p.'edit/ftp_target.html?', 'targetnum' ]; +</%init> diff --git a/httemplate/browse/msg_template.html b/httemplate/browse/msg_template.html index 8a6ccf741..50afc283e 100644 --- a/httemplate/browse/msg_template.html +++ b/httemplate/browse/msg_template.html @@ -1,29 +1,32 @@ <% include( 'elements/browse.html', 'title' => 'Message templates', 'name_singular' => 'template', - 'menubar' => [ 'Add a new template' => - $p.'edit/msg_template.html', - ], - 'query' => { 'table' => 'msg_template', }, - 'count_query' => 'SELECT COUNT(*) FROM msg_template', - 'disableable' => 1, + 'menubar' => \@menubar, + 'query' => { 'table' => 'msg_template', }, + 'count_query' => 'SELECT COUNT(*) FROM msg_template', + 'disableable' => 1, 'disabled_statuspos' => 2, 'agent_virt' => 1, - 'agent_null_right' => ['Edit global templates','Configuration'], + 'agent_null_right' => ['View global templates','Edit global templates'], 'agent_pos' => 1, - 'header' => [ 'Name', '', ('' x scalar @locales) ], - 'fields' => [ 'msgname', @locales ], - 'links' => [ $link, @locale_links ], - 'cell_style' => - [ '', '', ($locale_style) x (scalar @locales) ], + 'header' => [ 'Name', '', map '', @locales ], + 'fields' => [ 'msgname', @locales ], + 'links' => [ $link, @locale_links ], + 'cell_style' => [ '', '', map $locale_style, @locales ], ) %> <%init> +my $curuser = $FS::CurrentUser::CurrentUser; + die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Edit templates') - || $FS::CurrentUser::CurrentUser->access_right('Edit global templates') - || $FS::CurrentUser::CurrentUser->access_right('Configuration'); + unless $curuser->access_right([ 'View templates', 'View global templates', + 'Edit templates', 'Edit global templates', ]); + +my @menubar = (); +if ( $curuser->access_right(['Edit templates', 'Edit global templates']) ) { + push @menubar, 'Add a new template' => $p.'edit/msg_template.html'; +} my $link = [ "${p}edit/msg_template.html?msgnum=", 'msgnum' ]; diff --git a/httemplate/browse/part_pkg.cgi b/httemplate/browse/part_pkg.cgi index 4ca78d718..e3d9de13a 100755 --- a/httemplate/browse/part_pkg.cgi +++ b/httemplate/browse/part_pkg.cgi @@ -436,7 +436,7 @@ push @fields, [ map { [ - { 'data' => $_, + { 'data' => "$_: ", 'align' => 'right', }, { 'data' => $part_pkg->format($_,$options{$_}), diff --git a/httemplate/browse/part_referral.html b/httemplate/browse/part_referral.html index 9cc32c459..c7374673f 100755 --- a/httemplate/browse/part_referral.html +++ b/httemplate/browse/part_referral.html @@ -6,6 +6,13 @@ Where a customer heard about your service. Tracked for informational purposes. <A HREF="<% $p %>edit/part_referral.html"><I>Add a new advertising source</I></A> <BR><BR> +<% $cgi->param('showdisabled') + ? do { $cgi->param('showdisabled', 0); + '( <a href="'. $cgi->self_url. '">hide disabled advertising sources</a> )'; } + : do { $cgi->param('showdisabled', 1); + '( <a href="'. $cgi->self_url. '">show disabled advertising sources</a> )'; } +%> + <% include('/elements/table-grid.html') %> % my $bgcolor1 = '#eeeeee'; % my $bgcolor2 = '#ffffff'; @@ -13,8 +20,12 @@ Where a customer heard about your service. Tracked for informational purposes. <TR> <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=2 ROWSPAN=2>Advertising source</TH> -% if ( $show_agentnums ) { +% if ( ! $cgi->param('showdisabled') ) { + <TH CLASS="grid" BGCOLOR="#cccccc" ALIGN="center" ROWSPAN=2></TH> +% } + +% if ( $show_agentnums ) { <TH CLASS="grid" BGCOLOR="#cccccc" ROWSPAN=2>Agent</TH> % } @@ -27,7 +38,7 @@ Where a customer heard about your service. Tracked for informational purposes. </TR> -%foreach my $part_referral ( FS::part_referral->all_part_referral(1) ) { +%foreach my $part_referral ( FS::part_referral->all_part_referral(1,!scalar($cgi->param('showdisabled'))) ) { % % if ( $bgcolor eq $bgcolor1 ) { % $bgcolor = $bgcolor2; @@ -55,6 +66,16 @@ Where a customer heard about your service. Tracked for informational purposes. % } <% $part_referral->referral %><% $a ? '</A>' : '' %></TD> + +% if ( ! $cgi->param('showdisabled') ) { + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="center"> + <% $part_referral->disabled + ? '<FONT COLOR="#FF0000"><B>DISABLED</B></FONT>' + : '<FONT COLOR="#00CC00"><B>Active</B></FONT>' + %> + </TD> +% } + % if ( $show_agentnums ) { <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $part_referral->agentnum ? $part_referral->agent->agent : '(global)' %></TD> @@ -73,11 +94,11 @@ Where a customer heard about your service. Tracked for informational purposes. <TABLE CLASS="inv" CELLSPACING=0 CELLPADDING=0> <TR> <TD ALIGN="right"><B><% $num_cust %></B></TD> - <TD ALIGN="left">customers</TD> + <TD ALIGN="left"> customers </TD> </TR> <TR> <TD ALIGN="right"><B><% $num_pkg %></B></TD> - <TD ALIGN="left">packages</TD> + <TD ALIGN="left"> packages </TD> </TR> </TABLE> </TD> @@ -94,7 +115,7 @@ Where a customer heard about your service. Tracked for informational purposes. % or die dbh->errstr; <TR> - <TD BGCOLOR="#dddddd" ALIGN="center" COLSPAN=3><B>Total</B></TD> + <TD BGCOLOR="#dddddd" ALIGN="center" COLSPAN=<% 2 + $show_agentnums + ! $cgi->param('showdisabled') %><B>Total</B></TD> % for my $period ( keys %after ) { % my @param = ( $today-$after{$period}, % $today+$before{$period}, @@ -108,11 +129,11 @@ Where a customer heard about your service. Tracked for informational purposes. <TABLE CLASS="inv" CELLSPACING=0 CELLPADDING=0> <TR> <TD ALIGN="right"><B><% $num_cust %></B></TD> - <TD ALIGN="left">customers</TD> + <TD ALIGN="left"> customers </TD> </TR> <TR> <TD ALIGN="right"><B><% $num_pkg %></B></TD> - <TD ALIGN="left">packages</TD> + <TD ALIGN="left"> packages </TD> </TR> </TABLE> </TD> diff --git a/httemplate/browse/part_svc_class.html b/httemplate/browse/part_svc_class.html new file mode 100644 index 000000000..73bd60305 --- /dev/null +++ b/httemplate/browse/part_svc_class.html @@ -0,0 +1,34 @@ +<% include( 'elements/browse.html', + 'title' => 'Service classes', + 'html_init' => $html_init, + 'name' => 'service classes', + 'disableable' => 1, + 'disabled_statuspos' => 1, + 'query' => { 'table' => 'part_svc_class', + 'hashref' => {}, + 'order_by' => 'ORDER BY classnum', + }, + 'count_query' => $count_query, + 'header' => $header, + 'fields' => $fields, + 'links' => $links, + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $html_init = + 'Service classes are user-defined, informational types for services.<BR><BR>'. + qq!<A HREF="${p}edit/part_svc_class.html"><I>Add a service class</I></A><BR><BR>!; + +my $count_query = 'SELECT COUNT(*) FROM part_svc_class'; + +my $link = [ $p.'edit/part_svc_class.html?', 'classnum' ]; + +my $header = [ '#', 'Class' ]; +my $fields = [ 'classnum', 'classname' ]; +my $links = [ $link, $link ]; + +</%init> diff --git a/httemplate/browse/sales.cgi b/httemplate/browse/sales.cgi new file mode 100755 index 000000000..af098121d --- /dev/null +++ b/httemplate/browse/sales.cgi @@ -0,0 +1,100 @@ +<% include("/elements/header.html",'Sales Listing', menubar( + 'Add new sales person' => '../edit/sales.cgi' +)) %> +Sales people bring in business.<BR><BR> +% if ( dbdef->table('sales')->column('disabled') ) { + + <% $cgi->param('showdisabled') + ? do { $cgi->param('showdisabled', 0); + '( <a href="'. $cgi->self_url. '">hide disabled sales people</a> )'; } + : do { $cgi->param('showdisabled', 1); + '( <a href="'. $cgi->self_url. '">show disabled sales people</a> )'; } + %> +% } + + +<% include('/elements/table-grid.html') %> +% my $bgcolor1 = '#eeeeee'; +% my $bgcolor2 = '#ffffff'; +% my $bgcolor = ''; + +<TR> + <TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=<% ( $cgi->param('showdisabled') || !dbdef->table('sales')->column('disabled') ) ? 2 : 3 %>>Sales person</TH> + <TH CLASS="grid" BGCOLOR="#cccccc">Agent</TH> + <TH CLASS="grid" BGCOLOR="#cccccc">Access Groups</TH> +</TR> + +%foreach my $sales ( sort { +% $a->getfield('salesnum') cmp $b->getfield('salesnum') +%} qsearch('sales', \%search ) ) { +% +% if ( $bgcolor eq $bgcolor1 ) { +% $bgcolor = $bgcolor2; +% } else { +% $bgcolor = $bgcolor1; +% } + + <TR> + + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"> + <A HREF="<%$p%>edit/sales.cgi?<% $sales->salesnum %>"><% $sales->salesnum %></A> + </TD> + + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"> + <A HREF="<%$p%>edit/sales.cgi?<% $sales->salesnum %>"><% $sales->salesperson %></A> + </TD> + +% if ( ! $cgi->param('showdisabled') ) { + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" ALIGN="center"> + <% $sales->disabled ? '<FONT COLOR="#FF0000"><B>DISABLED</B></FONT>' + : '<FONT COLOR="#00CC00"><B>Active</B></FONT>' + %> + </TD> +% } + +% my ($agent) = qsearch('agent', { 'agentnum' => $sales->agentnum }); + + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"> + <A HREF="<%$p%>edit/sales.cgi?<% $sales->agentnum %>"><% $sales->agentnum %></A> + <A HREF="<%$p%>edit/agent.cgi?<% $agent->agentnum %>">(<% $agent->agent %>)<BR> + </TD> + + <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"> +% foreach my $access_group ( +% map $_->access_group, +% qsearch('access_groupsales', { 'salesnum' => $sales->salesnum }) +% ) { + <A HREF="<%$p%>edit/access_group.html?<% $access_group->groupnum %>"><% $access_group->groupname |h %><BR> +% } + </TD> + + </TR> +% } + + </TABLE> + +<SCRIPT TYPE="text/javascript"> + function areyousure(what, href) { + if ( confirm("Are you sure you want to " + what + "?") == true ) + window.location.href = href; + } +</SCRIPT> + + </BODY> +</HTML> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my %search; +if ( $cgi->param('showdisabled') + || !dbdef->table('agent')->column('disabled') ) { + %search = (); +} else { + %search = ( 'disabled' => '' ); +} + +my $conf = new FS::Conf; + +</%init> diff --git a/httemplate/config/config-process.cgi b/httemplate/config/config-process.cgi index 3dcb1d3ef..f1cbb1831 100644 --- a/httemplate/config/config-process.cgi +++ b/httemplate/config/config-process.cgi @@ -47,7 +47,7 @@ '</pre></font>'; % } elsif ( $type eq 'checkbox' ) { -% if ( $conf->exists($i->key, $agentnum) ) { +% if ( $conf->config_bool($i->key, $agentnum) ) { configCell.style.backgroundColor = '#00ff00'; configCell.innerHTML = 'YES'; % } else { @@ -184,7 +184,7 @@ foreach my $type ( ref($i->type) ? @{$i->type} : $i->type ) { } # warn @touch; $conf->touch($_, $agentnum) foreach @touch; -$conf->delete($_, $agentnum) foreach @delete; +$conf->delete_bool($_, $agentnum) foreach @delete; if (scalar(@error)) { $cgi->param('error', join(' ', @error)); diff --git a/httemplate/config/config-view.cgi b/httemplate/config/config-view.cgi index 02acd5853..02a24adbf 100644 --- a/httemplate/config/config-view.cgi +++ b/httemplate/config/config-view.cgi @@ -211,7 +211,7 @@ invoice language options: % } elsif ( $type eq 'checkbox' ) { <tr> - <td id="<% $agentnum.$i->key.$n %>" bgcolor="#<% $conf->exists($i->key, $agentnum) ? '00ff00">YES' : 'ff0000">NO' %></td> + <td id="<% $agentnum.$i->key.$n %>" bgcolor="#<% $conf->config_bool($i->key, $agentnum) ? '00ff00">YES' : 'ff0000">NO' %></td> </tr> % } elsif ( $type eq 'select' && $i->select_hash ) { diff --git a/httemplate/config/config.cgi b/httemplate/config/config.cgi index 6a1eaecf7..a4f9890a5 100644 --- a/httemplate/config/config.cgi +++ b/httemplate/config/config.cgi @@ -40,12 +40,14 @@ Setting <b><% $key %></b> <table><tr><td> % my $n = 0; +% my $submit = 0; % foreach my $type (@types) { % if ( $type eq '' ) { <font color="#ff0000">no type</font> % } elsif ( $type eq 'image' ) { +% $submit++; <% $conf->exists($key, $agentnum) ? 'Current image<br>'. @@ -59,24 +61,37 @@ Setting <b><% $key %></b> New image filename <input type="file" name="<% "$key$n" %>"> % } elsif ( $type eq 'binary' ) { +% $submit++; Filename <input type="file" name="<% "$key$n" %>"> % } elsif ( $type eq 'textarea' ) { +% $submit++; <textarea name="<% "$key$n" %>" rows=12 cols=78 wrap="off"><% join("\n", $conf->config($key, $agentnum)) |h %></textarea> % } elsif ( $type eq 'checkbox' ) { +% +% if ( $agentnum && $conf->exists($key) && ! $agent_bool ) { - <input name="<% "$key$n" %>" type="checkbox" value="1" - <% $conf->exists($key, $agentnum) ? 'CHECKED' : '' %> > + <input name="<% "$key$n" %>" type="checkbox" value="1" CHECKED DISABLED> + <FONT SIZE="-1"><I>(global setting cannot yet be overridden)</I></FONT> -% } elsif ( $type eq 'text' ) { +% } else { +% $submit++; - <input name="<% "$key$n" %>" type="text" value="<% $conf->exists($key, $agentnum) ? $conf->config($key, $agentnum) : '' |h %>"> + <input name="<% "$key$n" %>" type="checkbox" value="1" + <% $conf->config_bool($key, $agentnum) ? 'CHECKED' : '' %> > +% } -% } elsif ( $type eq 'select' || $type eq 'selectmultiple' ) { +% } elsif ( $type eq 'text' ) { +% $submit++; + <input name="<% "$key$n" %>" type="text" value="<% $conf->exists($key, $agentnum) ? $conf->config($key, $agentnum) : '' |h %>"> + +% } elsif ( $type eq 'select' || $type eq 'selectmultiple' ) { +% $submit++; + <select name="<% "$key$n" %>" <% $type eq 'selectmultiple' ? 'MULTIPLE' : '' %>> % @@ -131,7 +146,8 @@ Setting <b><% $key %></b> </select> -% } elsif ( $type eq 'select-sub' ) { +% } elsif ( $type eq 'select-sub' ) { +% $submit++; <select name="<% "$key$n" %>" <% $config_item->multiple ? 'MULTIPLE' : '' %>> @@ -167,8 +183,8 @@ Setting <b><% $key %></b> </select> -% } elsif ( $type eq 'editlist' ) { -% +% } elsif ( $type eq 'editlist' ) { +% $submit++; <script> function doremove<% "$key$n" %>() { fromObject = document.OneTrueForm.<% "$key$n" %>; @@ -284,6 +300,7 @@ Setting <b><% $key %></b> </tr></table> % } elsif ( $element_types{$type} ) { +% $submit++; % % my %opt = ( 'element_name' => "$key$n", % 'empty_label' => ' ', @@ -313,7 +330,10 @@ Setting <b><% $key %></b> % } </tr> </table> -<INPUT TYPE="submit" VALUE="<% $title %>"> + +% if ( $submit ) { + <INPUT TYPE="submit" VALUE="<% $title %>"> +% } </FORM> </BODY> @@ -365,5 +385,6 @@ my $config_item = $confitems{$key}; my $description = $config_item->description; my $config_type = $config_item->type; my @types = ref($config_type) ? @$config_type : ($config_type); +my $agent_bool = $config_item->agent_bool; </%init> diff --git a/httemplate/docs/about.html b/httemplate/docs/about.html index e3bb28298..33b21a3ad 100644 --- a/httemplate/docs/about.html +++ b/httemplate/docs/about.html @@ -10,7 +10,7 @@ % my $url = $conf->config('company_url', $agentnum); % if ( $url ) { <BR><BR> - <A HREF="<% $conf->config('company_url', $agentnum) %>" TARGET="_blank"><%$title%> homepage</A> + <FONT SIZE="+1"><A HREF="<% $conf->config('company_url', $agentnum) %>" TARGET="_top"><%$title%> homepage</A></FONT> % } % } else { @@ -46,7 +46,7 @@ GNU <b>Affero</b> General Public License.<BR> <BR><BR> -<A HREF="http://www.freeside.biz/freeside" TARGET="_blank">Freeside homepage</A> +<A HREF="http://www.freeside.biz/freeside" TARGET="_top">Freeside homepage</A> % if ( $agentnum ) { </FONT> % } @@ -56,7 +56,7 @@ GNU <b>Affero</b> General Public License.<BR> % unless ( $agentnum ) { <CENTER> - <FONT SIZE="-3">"A selfish heart is trouble, but a foolish heart is worse" -R. Hunter</FONT> +<!-- <FONT SIZE="-3">"" -R. Hunter</FONT> --> </CENTER> % } diff --git a/httemplate/docs/part_svc-table.html b/httemplate/docs/part_svc-table.html new file mode 100644 index 000000000..48841f550 --- /dev/null +++ b/httemplate/docs/part_svc-table.html @@ -0,0 +1,63 @@ +<& /elements/header-popup.html &> + +<TABLE> + <TR> + <TH ALIGN="left">Generic</TH> + <TH ALIGN="left">Access</TH> + <TH ALIGN="left">Telephony</TH> +<!-- <TH>Hosting</TH> + <TH>Colocation</TH> +--> + </TR> + <TR> + <TD VALIGN="top"> + <UL STYLE="margin:0"> + <LI><B>svc_acct</B>: Accounts - anything with a username (mailbox, shell, RADIUS, etc.) + <LI><B>svc_hardware</B>: Equipment supplied to customers + <LI><B>svc_external</B>: Externally-tracked service + </UL> + </TD> + <TD VALIGN="top"> + <UL STYLE="margin:0"> + <LI><B>svc_dsl</B>: DSL + <LI><B>svc_broadband</B>: Wireless broadband + <LI><B>svc_dish</B>: DISH Network + </UL> + </TD> + <TD VALIGN="top"> + <UL STYLE="margin:0"> + <LI><B>svc_phone</B>: Customer phone number + <LI><B>svc_pbx</B>: Customer PBX + </UL> + </TD> + </TR> +</TABLE> +<BR> +<TABLE> + <TR> + <TH ALIGN="left">Hosting</TH> + <TH ALIGN="left">Colocation</TH> + </TR> + <TD VALIGN="top"> + <UL STYLE="margin:0"> + <LI><B>svc_domain</B>: Domain + <LI><B>svc_cert</B>: Certificate + <LI><B>svc_forward</B>: Mail forwarding + <LI><B>svc_mailinglist</B>: Mailing list + <LI><B>svc_www</B>: Virtual domain website + </UL> + </TD> + <TD VALIGN="top"> + <UL STYLE="margin:0"> + <LI><B>svc_port</B>: Customer router/switch port + </UL> + </TD> + </TR> +<TABLE> +<!-- <LI>svc_charge - One-time charges (Partially unimplemented) + <LI>svc_wo - Work orders (Partially unimplemented) +--> + +</BODY> +</HTML> + diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi index 399431311..ef81ebab1 100755 --- a/httemplate/edit/cust_main.cgi +++ b/httemplate/edit/cust_main.cgi @@ -23,120 +23,89 @@ % } %# agent, agent_custid, refnum (advertising source), referral_custnum +%# better section title for this? +<FONT CLASS="fsinnerbox-title"><% mt('Basics') |h %></FONT> <& cust_main/top_misc.html, $cust_main, 'custnum' => $custnum &> %# birthdate -% if ( $conf->exists('cust_main-enable_birthdate') ) { +% if ( $conf->exists('cust_main-enable_birthdate') +% || $conf->exists('cust_main-enable_spouse_birthdate') +% ) +% { <BR> <& cust_main/birthdate.html, $cust_main &> % } - -%# contact info - -% my $same_checked = ''; -% my $ship_disabled = ''; -% my @ship_style = (); -% unless ( $cust_main->ship_last && $same ne 'Y' ) { -% $same_checked = 'CHECKED'; -% $ship_disabled = 'DISABLED'; -% push @ship_style, 'background-color:#dddddd'; -% foreach ( -% qw( last first company address1 address2 city county state zip country -% latitude longitude coord_auto -% daytime night fax mobile ) -% ) { -% $cust_main->set("ship_$_", $cust_main->get($_) ); -% } -% } - +% my $has_ship_address = ''; +% if ( $cgi->param('error') ) { +% $has_ship_address = !$same; +% } elsif ( $cust_main->custnum ) { +% $has_ship_address = $cust_main->has_ship_address; +% } <BR> -<FONT CLASS="fsinnerbox-title"><% mt('Billing address') |h %></FONT> - -<& cust_main/contact.html, - 'cust_main' => $cust_main, - 'pre' => '', - 'onchange' => 'bill_changed(this)', - 'disabled' => '', - 'ss' => $ss, - 'stateid' => $stateid, - 'same_checked' => $same_checked, #for address2 "Unit #" labeling -&> +<TABLE> <TR> + <TD STYLE="width:650px"> +%#; padding-right:2px; vertical-align:top"> + <FONT CLASS="fsinnerbox-title"><% mt('Billing address') |h %></FONT> + <TABLE CLASS="fsinnerbox"> + <& cust_main/before_bill_location.html, $cust_main &> + <& /elements/location.html, + object => $cust_main->bill_location, + prefix => 'bill_', + &> + <& cust_main/after_bill_location.html, $cust_main &> + </TABLE> + </TD> +</TR> +<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" + ID="same" + onclick="samechanged(this)" + onkeyup="samechanged(this)" + 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, + &> + </TABLE> + <TABLE CLASS="fsinnerbox" ID="table_ship_location_blank" + STYLE="display:none"> + <TR><TD></TD></TR> + </TABLE> + </TD> +</TR></TABLE> <SCRIPT> -function bill_changed(what) { - if ( what.form.same.checked ) { -% for (qw( last first company address1 address2 city zip latitude longitude coord_auto daytime night fax mobile )) { - what.form.ship_<%$_%>.value = what.form.<%$_%>.value; -% } - - what.form.ship_country.selectedIndex = what.form.country.selectedIndex; - - function fix_ship_city() { - what.form.ship_city_select.selectedIndex = what.form.city_select.selectedIndex; - what.form.ship_city.style.display = what.form.city.style.display; - what.form.ship_city_select.style.display = what.form.city_select.style.display; - } - - function fix_ship_county() { - what.form.ship_county.selectedIndex = what.form.county.selectedIndex; - ship_county_changed(what.form.ship_county, fix_ship_city ); - } - - function fix_ship_state() { - what.form.ship_state.selectedIndex = what.form.state.selectedIndex; - ship_state_changed(what.form.ship_state, fix_ship_county ); - } - - ship_country_changed(what.form.ship_country, fix_ship_state ); - - } -} function samechanged(what) { +%# not display = 'none', because we still want it to take up space +%# 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 ) { - bill_changed(what); - -% my @fields = qw( last first company address1 address2 city city_select county state zip country latitude longitude daytime night fax mobile ); -% for (@fields) { - what.form.ship_<%$_%>.disabled = true; - what.form.ship_<%$_%>.style.backgroundColor = '#dddddd'; -% } - -% if ( $conf->exists('cust_main-require_address2') ) { - document.getElementById('address2_required').style.visibility = ''; - document.getElementById('address2_label').style.visibility = ''; - document.getElementById('ship_address2_required').style.visibility = 'hidden'; - document.getElementById('ship_address2_label').style.visibility = 'hidden'; -% } - - } else { - -% for (@fields) { - what.form.ship_<%$_%>.disabled = false; - what.form.ship_<%$_%>.style.backgroundColor = '#ffffff'; -% } - -% if ( $conf->exists('cust_main-require_address2') ) { - document.getElementById('address2_required').style.visibility = 'hidden'; - document.getElementById('address2_label').style.visibility = 'hidden'; - document.getElementById('ship_address2_required').style.visibility = ''; - document.getElementById('ship_address2_label').style.visibility = ''; -% } - + t2.style.width = t1.clientWidth + 'px'; + t2.style.height = t1.clientHeight + 'px'; + t1.style.display = 'none'; + t2.style.display = ''; + } + else { + t2.style.display = 'none'; + t1.style.display = ''; } } +samechanged(document.getElementById('same')); </SCRIPT> <BR> -<FONT CLASS="fsinnerbox-title"><% mt('Service address') |h %></FONT> - -<INPUT TYPE="checkbox" NAME="same" VALUE="Y" onClick="samechanged(this)" <%$same_checked%>><% mt('same as billing address') |h %> -<& cust_main/contact.html, - 'cust_main' => $cust_main, - 'pre' => 'ship_', - 'onchange' => '', - 'disabled' => $ship_disabled, - 'style' => \@ship_style -&> <& cust_main/contacts_new.html, 'cust_main' => $cust_main, @@ -229,27 +198,49 @@ my $conf = new FS::Conf; #get record my($custnum, $cust_main, $ss, $stateid, $payinfo, @invoicing_list); -my $same = ''; my $pkgpart_svcpart = ''; #first_pkg my($username, $password, $popnum, $saved_domsvc) = ( '', '', 0, 0 ); #svc_acct my %svc_phone = (); my %svc_dsl = (); my $prospectnum = ''; my $locationnum = ''; +my $same = ''; + if ( $cgi->param('error') ) { + $same = ($cgi->param('same') || '') eq 'Y'; + # false laziness w/ edit/process/cust_main.cgi + my %locations; + for my $pre (qw(bill ship)) { + my %hash; + foreach ( FS::cust_main->location_fields ) { + $hash{$_} = scalar($cgi->param($pre.'_'.$_)); + } + $hash{'custnum'} = $cgi->param('custnum'); + $locations{$pre} = qsearchs('cust_location', \%hash) + || FS::cust_location->new( \%hash ); + } + if ( $same ) { + $locations{ship} = $locations{bill}; + } + $cust_main = new FS::cust_main ( { - map { $_, scalar($cgi->param($_)) } fields('cust_main') + map { ( $_, scalar($cgi->param($_)) ) } (fields('cust_main')), + map { ( "ship_$_", '' ) } (FS::cust_main->location_fields) } ); + for my $pre (qw(bill ship)) { + $cust_main->set($pre.'_location', $locations{$pre}); + $cust_main->set($pre.'_locationnum', $locations{$pre}->locationnum); + } + $custnum = $cust_main->custnum; die "access denied" unless $curuser->access_right($custnum ? 'Edit customer' : 'New customer'); @invoicing_list = split( /\s*,\s*/, $cgi->param('invoicing_list') ); - $same = $cgi->param('same'); $cust_main->setfield('paid' => $cgi->param('paid')) if $cgi->param('paid'); $ss = $cust_main->ss; # don't mask an entered value on errors $stateid = $cust_main->stateid; # don't mask an entered value on errors @@ -296,7 +287,7 @@ if ( $cgi->param('error') ) { $cust_main->paycvv($paycvv); } @invoicing_list = $cust_main->invoicing_list; - $ss = $cust_main->masked('ss'); + $ss = $conf->exists('unmask_ss') ? $cust_main->ss : $cust_main->masked('ss'); $stateid = $cust_main->masked('stateid'); $payinfo = $cust_main->paymask; @@ -352,6 +343,20 @@ if ( $cgi->param('error') ) { $svc_dsl{$_} = $qual->$_ foreach qw( phonenum vendor_qual_id ); } + else { + my $countrydefault = $conf->config('countrydefault') || 'US'; + my $statedefault = $conf->config('statedefault') || 'CA'; + $cust_main->set('bill_location', + FS::cust_location->new( + { country => $countrydefault, state => $statedefault } + ) + ); + $cust_main->set('ship_location', + FS::cust_location->new( + { country => $countrydefault, state => $statedefault } + ) + ); + } if ( $cgi->param('lock_pkgpart') =~ /^(\d+)$/ ) { my $pkgpart = $1; @@ -364,7 +369,7 @@ if ( $cgi->param('error') ) { } my %keep = map { $_=>1 } qw( error tagnum lock_agentnum lock_pkgpart ); -$cgi->delete( grep !$keep{$_}, $cgi->param ); +$cgi->delete( grep { !$keep{$_} && $_ !~ /^tax_/ } $cgi->param ); my $title = $custnum ? 'Edit Customer' : 'Add Customer'; $title = mt($title); diff --git a/httemplate/edit/cust_main/after_bill_location.html b/httemplate/edit/cust_main/after_bill_location.html new file mode 100644 index 000000000..2f4c3b51c --- /dev/null +++ b/httemplate/edit/cust_main/after_bill_location.html @@ -0,0 +1,12 @@ +% if ( ! $conf->exists('cust-edit-alt-field-order') ) { + <& phones.html, $cust_main &> + <& fax.html, $cust_main &> +% } else { + <& fax.html, $cust_main &> + <& company.html, $cust_main &> +% } +<& stateid.html, $cust_main &> +<%init> +my $cust_main = shift; +my $conf = FS::Conf->new; +</%init> diff --git a/httemplate/edit/cust_main/before_bill_location.html b/httemplate/edit/cust_main/before_bill_location.html new file mode 100644 index 000000000..973201ecb --- /dev/null +++ b/httemplate/edit/cust_main/before_bill_location.html @@ -0,0 +1,10 @@ +<& name.html, $cust_main &> +% if ( ! $conf->exists('cust-edit-alt-field-order') ) { + <& company.html, $cust_main &> +% } else { + <& phones.html, $cust_main &> +% } +<%init> +my $cust_main = shift; +my $conf = FS::Conf->new; +</%init> diff --git a/httemplate/edit/cust_main/billing.html b/httemplate/edit/cust_main/billing.html index 9f4cb74cd..d7082f23a 100644 --- a/httemplate/edit/cust_main/billing.html +++ b/httemplate/edit/cust_main/billing.html @@ -117,6 +117,15 @@ //why? select.selectedIndex = 0; } } + + function tax_changed(what) { + var num = document.getElementById(what.id + '_num'); + if ( what.checked ) { + num.disabled = false; + } else { + num.disabled = true; + } + } </SCRIPT> @@ -437,7 +446,10 @@ % my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups'); -% if ( $conf->exists('cust_class-tax_exempt') ) { +% if ( $conf->exists('cust_class-tax_exempt') +% || $conf->exists('tax-cust_exempt-groups-require_individual_nums') +% ) +% { <INPUT TYPE="hidden" NAME="tax" VALUE="<% $cust_main->tax eq 'Y' ? 'Y' : '' %>"> @@ -450,9 +462,12 @@ % } % foreach my $exempt_group ( @exempt_groups ) { -% #escape $exempt_group for NAME +% 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 WIDTH="608" COLSPAN="2"> <INPUT TYPE="checkbox" NAME="tax_<% $exempt_group %>" VALUE="Y" <% $cust_main->tax_exemption($exempt_group) ? 'CHECKED' : '' %>> Tax Exempt (<% $exempt_group %> taxes)<TD> + <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> % } @@ -466,7 +481,7 @@ ? 'CHECKED' : '' - %>> <% mt('Postal mail invoice') |h %> + %>> <% mt('Postal mail invoices') |h %> </TD> </TR> @@ -478,15 +493,27 @@ ? 'CHECKED' : '' - %>> <% mt('Fax invoice') |h %> + %>> <% mt('Fax invoices') |h %> </TD> </TR> % } -% unless ( $conf->exists('cust-email-high-visibility')) { <TR> + <TD WIDTH="608" COLSPAN="2"><INPUT TYPE="checkbox" NAME="invoice_email" VALUE="Y" <% + + ( $cust_main->invoice_noemail eq 'Y' ) + ? '' + : 'CHECKED' + + %>> <% mt('Email invoices') |h %> + + </TD> + </TR> + +% unless ( $conf->exists('cust-email-high-visibility')) { + <TR> <TD ALIGN="right" WIDTH="200"> <% $conf->exists('cust_main-require_invoicing_list_email', $agentnum) ? $r : '' %>Email address(es) @@ -570,20 +597,25 @@ function toggle(obj) { %my @available_locales = $conf->config('available-locales'); %if ( scalar(@available_locales) ) { -% push @available_locales, ''; -% my %locale_labels = map { -% my %ll; -% my %info = FS::Locales->locale_info($_); -% $ll{$_} = $info{name} . " (" . $info{country} . ")"; -% %ll; -% } FS::Locales->locales; - <& /elements/tr-select.html, - 'label' => emt('Invoicing locale'), - 'field' => 'locale', - 'options' => \@available_locales, - 'labels' => \%locale_labels, - 'curr_value' => $cust_main->locale, - &> +% push @available_locales, '' +% unless $cust_main->locale && $conf->exists('cust_main-require_locale'); +% my %locale_labels = map { +% my %ll; +% my %info = FS::Locales->locale_info($_); +% $ll{$_} = $info{name} . " (" . $info{country} . ")"; +% %ll; +% } FS::Locales->locales; +% +% my $label = ( $conf->exists('cust_main-require_locale') ? $r : '' ). +% emt('Invoicing locale'); + + <& /elements/tr-select.html, + 'label' => $label, + 'field' => 'locale', + 'options' => \@available_locales, + 'labels' => \%locale_labels, + 'curr_value' => $cust_main->locale, + &> % } </TABLE> diff --git a/httemplate/edit/cust_main/birthdate.html b/httemplate/edit/cust_main/birthdate.html index b4e78e3b9..5d6a123b1 100644 --- a/httemplate/edit/cust_main/birthdate.html +++ b/httemplate/edit/cust_main/birthdate.html @@ -1,16 +1,33 @@ <% ntable("#cccccc", 2) %> - <% include( '/elements/tr-input-date-field.html', - 'birthdate', - $cust_main->birthdate, - 'Date of Birth', - ( $conf->config('date_format') || "%m/%d/%Y" ), - 1 - ) +% # maybe put after the contact names? +% if ( $conf->exists('cust_main-enable_birthdate') ) { + <% include( '/elements/tr-input-date-field.html', { + 'name' => 'birthdate', + 'value' => $cust_main->birthdate, + 'label' => 'Date of Birth', + 'format' => ( $conf->config('date_format') || "%m/%d/%Y" ), + 'usedatetime' => 1, + 'noinit' => $noinit++, + }) %> +% } +% if ( $conf->exists('cust_main-enable_spouse_birthdate') ) { + <% include( '/elements/tr-input-date-field.html', { + 'name' => 'spouse_birthdate', + 'value' => $cust_main->spouse_birthdate, + 'label' => 'Spouse Date of Birth', + 'format' => ( $conf->config('date_format') || "%m/%d/%Y" ), + 'usedatetime' => 1, + 'noinit' => $noinit++, + }) + %> +% } </TABLE> <%init> my( $cust_main, %opt ) = @_; my $conf = new FS::Conf; +my $noinit = 0; + </%init> diff --git a/httemplate/edit/cust_main/bottomfixup.js b/httemplate/edit/cust_main/bottomfixup.js index 800864bc8..77d4294a6 100644 --- a/httemplate/edit/cust_main/bottomfixup.js +++ b/httemplate/edit/cust_main/bottomfixup.js @@ -66,21 +66,25 @@ function copy_payby_fields() { %# call submit_continue() on completion... %# otherwise not touching standardize_locations for now <% include( '/elements/standardize_locations.js', - 'callback' => 'submit_continue();' + 'callback' => 'submit_continue();', + 'main_prefix' => 'bill_', + 'no_company' => 1, ) %> +var prefix; function fetch_censustract() { //alert('fetch census tract data'); + prefix = document.getElementById('same').checked ? 'bill_' : 'ship_'; var cf = document.CustomerForm; - var state_el = cf.elements['ship_state']; + var state_el = cf.elements[prefix + 'state']; var census_data = new Array( 'year', <% $conf->config('census_year') || '2012' %>, - 'address1', cf.elements['ship_address1'].value, - 'city', cf.elements['ship_city'].value, + 'address1', cf.elements[prefix + 'address1'].value, + 'city', cf.elements[prefix + 'city'].value, 'state', state_el.options[ state_el.selectedIndex ].value, - 'zip', cf.elements['ship_zip'].value + 'zip', cf.elements[prefix + 'zip'].value ); censustract( census_data, update_censustract ); @@ -109,19 +113,21 @@ function update_censustract(arg) { set_censustract = function () { - cf.elements['censustract'].value = newcensus; + cf.elements[prefix + 'censustract'].value = newcensus; submit_continue(); } - if (error || cf.elements['censustract'].value != newcensus) { + if (error || cf.elements[prefix + 'censustract'].value != newcensus) { // popup an entry dialog if (error) { newcensus = error; } newcensus.replace(/.*ndefined.*/, 'Not found'); - var latitude = cf.elements['latitude' ].value || '<% $company_latitude %>'; - var longitude= cf.elements['longitude'].value || '<% $company_longitude %>'; + var latitude = cf.elements[prefix + 'latitude'].value + || '<% $company_latitude %>'; + var longitude= cf.elements[prefix + 'longitude'].value + || '<% $company_longitude %>'; var choose_censustract = '<CENTER><BR><B>Confirm censustract</B><BR>' + @@ -132,14 +138,14 @@ function update_censustract(arg) { '" target="_blank">Map service module location</A><BR>' + '<A href="http://maps.ffiec.gov/FFIECMapper/TGMapSrv.aspx?' + 'census_year=<% $conf->config('census_year') || '2012' %>' + - '&zip_code=' + cf.elements['ship_zip'].value + + '&zip_code=' + cf.elements[prefix + 'zip'].value + '" target="_blank">Map zip code center</A><BR><BR>' + '<TABLE>'; choose_censustract = choose_censustract + '<TR><TH style="width:50%">Entered census tract</TH>' + '<TH style="width:50%">Calculated census tract</TH></TR>' + - '<TR><TD>' + cf.elements['censustract'].value + + '<TR><TD>' + cf.elements[prefix + 'censustract'].value + '</TD><TD>' + newcensus + '</TD></TR>' + '<TR><TD> </TD><TD> </TD></TR>'; diff --git a/httemplate/edit/cust_main/company.html b/httemplate/edit/cust_main/company.html new file mode 100644 index 000000000..8a6ed0bbf --- /dev/null +++ b/httemplate/edit/cust_main/company.html @@ -0,0 +1,7 @@ +% my $cust_main = shift; +<TR ID="company_row" <% $cust_main->company ? '' : 'STYLE="display:none"' %>> + <TD ALIGN="right"><% mt('Company') |h %></TD> + <TD COLSPAN=6><INPUT TYPE="text" NAME="company" ID="company" SIZE=60 + VALUE="<% $cust_main->company |h %>"> + </TD> +</TR> diff --git a/httemplate/edit/cust_main/fax.html b/httemplate/edit/cust_main/fax.html new file mode 100644 index 000000000..237d4be44 --- /dev/null +++ b/httemplate/edit/cust_main/fax.html @@ -0,0 +1,5 @@ +% my $cust_main = shift; +<TR> + <TD ALIGN="right"><% mt('Fax') |h %></TD> + <TD><INPUT TYPE="text" NAME="fax" VALUE="<% $cust_main->fax %>" SIZE=18></TD> +</TR> diff --git a/httemplate/edit/cust_main/name.html b/httemplate/edit/cust_main/name.html new file mode 100644 index 000000000..2641ec930 --- /dev/null +++ b/httemplate/edit/cust_main/name.html @@ -0,0 +1,53 @@ +<%def .namepart> +% my ($field, $value, $label, $extra) = @_; +<TD> + <INPUT TYPE="text" NAME="<% $field %>" VALUE="<% $value |h %>" <%$extra%>> + <BR><FONT SIZE=-1><% mt($label) %></FONT> +</TD> +</%def> + +<TR> + <TH VALIGN="top" ALIGN="right"><%$r%><% mt('Contact name') |h %></TH> + <TD COLSPAN=6> + <TABLE CELLSPACING=0 CELLPADDING=0> + <TR> + <& .namepart, 'last', $cust_main->last, 'Last' &> + <TD VALIGN="top"> , </TD> + <& .namepart, 'first', $cust_main->first, 'First' &> +% if ( $conf->exists('show_ss') ) { + <TD> </TD> + <& .namepart, 'ss', $ss, 'SS#', "SIZE=11" &> +% } else { + <INPUT TYPE="hidden" NAME="ss" VALUE="<% $ss %>"> +% } + </TR> + </TABLE> + </TD> +</TR> + +% if ( $conf->exists('cust-email-high-visibility') ) { +<TR> + <TD ALIGN="right"> + <% $conf->exists('cust_main-require_invoicing_list_email', $agentnum) + ? $r + : '' %>Email address(es) + </TD> + <TD BGCOLOR="#FFFF00"> + <INPUT TYPE="text" NAME="invoicing_list" + VALUE=<% $cust_main->invoicing_list_emailonly_scalar %>> + </TD> +</TR> +% } +<%init> +my $cust_main = shift; +my $agentnum = $cust_main->agentnum if $cust_main->custnum; +my $conf = FS::Conf->new; +my $r = '<font color="#ff0000">*</font> '; +my $ss; + +if ( $cgi->param('error') or $conf->exists('unmask_ss') ) { + $ss = $cust_main->ss; +} else { + $ss = $cust_main->masked('ss'); +} +</%init> diff --git a/httemplate/edit/cust_main/phones.html b/httemplate/edit/cust_main/phones.html new file mode 100644 index 000000000..9b23e0716 --- /dev/null +++ b/httemplate/edit/cust_main/phones.html @@ -0,0 +1,29 @@ +<TR> + <TD VALIGN="top" ALIGN="right"><% mt('Phones') |h %></TD> + <TD COLSPAN=6> + <TABLE CELLSPACING=0 CELLPADDING=0> + <TR> +% foreach my $phone (qw(daytime night mobile)) { + <TD> + <INPUT TYPE="text" + NAME="<% $phone %>" + VALUE="<% $cust_main->get($phone) %>" + SIZE=18 + > + <BR><FONT SIZE=-1><% mt($phone_label{$phone}) |h %></FONT> + </TD> + <TD> </TD> +% } + </TR> + </TABLE> + </TD> +</TR> +<%init> +my $cust_main = shift; +my $conf = FS::Conf->new; +my %phone_label = ( + daytime => 'Day Phone', + night => 'Night Phone', + mobile => 'Mobile', +); +</%init> diff --git a/httemplate/edit/cust_main/stateid.html b/httemplate/edit/cust_main/stateid.html new file mode 100644 index 000000000..2655f5142 --- /dev/null +++ b/httemplate/edit/cust_main/stateid.html @@ -0,0 +1,39 @@ +% if ( $conf->exists('show_stateid') ) { +<TR> + <TD ALIGN="right"><% $stateid_label %></TD> + <TD><INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>" SIZE=12></TD> + <TD><& /elements/select-state.html, + state => $cust_main->stateid_state, + country => $cust_main->country, # how does this work on new customer? + prefix => 'stateid_', + disable_countyupdate => 1, + &></TD> +</TR> +% } else { +<INPUT TYPE="hidden" NAME="stateid" VALUE="<% $stateid %>"> +<INPUT TYPE="hidden" NAME="stateid_state" VALUE="<% $cust_main->stateid_state %>"> +% } + +<%init> +my $cust_main = shift; +my $conf = FS::Conf->new; +my $stateid; +if ( $cgi->param('error') ) { + $stateid = $cust_main->stateid; +} elsif ( $cust_main->custnum ) { + $stateid = $cust_main->masked('stateid'); +} else { + $stateid = ''; +} +$cust_main->set('stateid_state' => $cust_main->state) + unless $cust_main->stateid_state; + +my $stateid_label = FS::Msgcat::_gettext('stateid') =~ /^(stateid)?$/ + ? 'Driver’s License' + : FS::Msgcat::_gettext('stateid') || 'Driver’s License'; + +my $stateid_state_label = + FS::Msgcat::_gettext('stateid_state') =~ /^(stateid_state)?$/ + ? 'Driver’s License State' + : FS::Msgcat::_gettext('stateid') || 'Driver’s License State'; +</%init> diff --git a/httemplate/edit/cust_main/top_misc.html b/httemplate/edit/cust_main/top_misc.html index 7ba167b7f..7ce283c6c 100644 --- a/httemplate/edit/cust_main/top_misc.html +++ b/httemplate/edit/cust_main/top_misc.html @@ -20,27 +20,16 @@ <% $cust_main->residential_commercial eq 'Commercial' ? 'CHECKED' : '' %> ></TD> </TR> - <SCRIPT TYPE="text/javascript"> - function rescom_changed() { - var f = document.CustomerForm; - - if ( f.residential_commercial_Residential.checked ) { - document.getElementById('contacts_div').style.display = 'none'; - } else { // if ( f.residential_commercial_Commercial.checked ) { - document.getElementById('contacts_div').style.display = ''; - } - - if ( f.residential_commercial_Residential.checked && ! f.company.value.length ) { - document.getElementById('company_row').style.display = 'none' - } else { // if ( f.residential_commercial_Commercial.checked ) { + function rescom_changed(what) { + if ( what.checked == (what.value == 'Commercial' ) ) { document.getElementById('company_row').style.display = ''; - } - - if ( f.residential_commercial_Residential.checked && ! f.ship_company.value.length ) { - document.getElementById('ship_company_row').style.display = 'none' - } else { // if ( f.residential_commercial_Commercial.checked ) { - document.getElementById('ship_company_row').style.display = ''; + document.getElementById('contacts_div').style.display = ''; + } else { + if ( document.getElementById('company').value.length == 0 ) { + document.getElementById('company_row').style.display = 'none'; + } + document.getElementById('contacts_div').style.display = 'none'; } } </SCRIPT> diff --git a/httemplate/edit/cust_pay.cgi b/httemplate/edit/cust_pay.cgi index 7a1bb00fa..d4414e44e 100755 --- a/httemplate/edit/cust_pay.cgi +++ b/httemplate/edit/cust_pay.cgi @@ -23,6 +23,7 @@ <% mt('Payment') |h %> <% ntable("#cccccc", 2) %> +% if ( $FS::CurrentUser::CurrentUser->access_right('Backdate payment') ) { <TR> <TD ALIGN="right"><% mt('Date') |h %></TD> <TD COLSPAN=2> @@ -39,6 +40,15 @@ align: "BR" }); </SCRIPT> +% } +% else { +<TR> + <TD ALIGN="right"><% mt('Date') |h %></TD> + <TD COLSPAN=2> + <% time2str($date_format.' %r',$_date) %> + </TD> +</TR> +% } <TR> <TD ALIGN="right"><% mt('Amount') |h %></TD> diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html index 73faad4c0..a24f23805 100644 --- a/httemplate/edit/elements/edit.html +++ b/httemplate/edit/elements/edit.html @@ -292,6 +292,9 @@ Example: % #& deprecated weird value hashref used only by reason.html % 'value' => $f->{'value'}, % +% #fixed +% 'noescape' => $f->{'noescape'}, +% % #select(-*) % 'options' => $f->{'options'}, % 'labels' => $f->{'labels'}, @@ -308,6 +311,7 @@ Example: % % #umm. for select-agent_types at least % 'disabled' => $f->{'disabled'}, +% 'fixed' => $f->{'fixed'}, % % #any? % 'colspan' => $f->{'colspan'}, @@ -317,7 +321,7 @@ Example: % $include_common{$_} = $f->{$_} foreach grep exists($f->{$_}), % qw( js_only html_only select_only layers_only cell_style ),#selectlayers,? % qw( empty_label ), # select-* -% qw( value_col ), # select-table +% qw( value_col compare_sub ), # select-table % qw( table name_col ), #(select,checkboxes)-table % qw( target_table link_table ), #checkboxes-table % qw( hashref agent_virt agent_null agent_null_right ),#*-table @@ -751,13 +755,15 @@ Example: <BR> - <INPUT TYPE = "submit" - ID = "submit" - VALUE = "<% ( !$clone && $object->$pkey() ) - ? "Apply changes" - : "Add ". ( $opt{'name'} || $opt{'name_singular'} ) - %>" - > +% unless ($opt{'no_submit'}) { + <INPUT TYPE = "submit" + ID = "submit" + VALUE = "<% ( !$clone && $object->$pkey() ) + ? "Apply changes" + : "Add ". ($opt{'name'} || $opt{'name_singular'}) + %>" + > +% } </FORM> diff --git a/httemplate/edit/elements/svc_Common.html b/httemplate/edit/elements/svc_Common.html index 38716f06e..0d9d36c07 100644 --- a/httemplate/edit/elements/svc_Common.html +++ b/httemplate/edit/elements/svc_Common.html @@ -103,7 +103,15 @@ $f->{'extra_sql'} .= ' OR svcnum = '. $object->svcnum if $object->svcnum; $f->{'extra_sql'} .= ' ) '; - $f->{'disable_empty'} = $object->svcnum ? 1 : 0, + $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'} = { @@ -127,6 +135,10 @@ $object->set('custnum', $cust_pkg->custnum); } + if ( my $cb = $opt{'svc_field_callback'} ) { + &{ $cb }( $cgi, $object, $f); + } + }, 'html_init' => sub { diff --git a/httemplate/edit/ftp_target.html b/httemplate/edit/ftp_target.html new file mode 100755 index 000000000..aebf9aaed --- /dev/null +++ b/httemplate/edit/ftp_target.html @@ -0,0 +1,46 @@ +<& elements/edit.html, + 'post_url' => popurl(1).'process/ftp_target.html', + 'name' => 'FTP target', + 'table' => 'ftp_target', + 'viewall_url' => "${p}browse/ftp_target.html", + 'labels' => { targetnum => 'Target', + hostname => 'Server', + username => 'Username', + password => 'Password', + path => 'Directory', + port => 'Port', + secure => 'Use SFTP', + handling => 'Special handling', + }, + 'fields' => [ + { field => 'hostname', size => 40 }, + { field => 'port', size => 8 }, + { field => 'secure', type => 'checkbox', value => 'Y' }, + 'username', + 'password', + { field => 'path', size => 40 }, + { field => 'handling', + type => 'select', + options => [ FS::ftp_target->handling_types ], + }, + ], + 'menubar' => \@menubar, + 'edit_callback' => $edit_callback, +&> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('Configuration'); + +my @menubar = ('View all FTP targets' => $p.'browse/ftp_target.html'); +my $edit_callback = sub { + my ($cgi, $object) = @_; + if ( $object->targetnum ) { + push @menubar, 'Delete this target', + $p.'misc/delete-ftp_target.html?'.$object->targetnum; + } +}; + +</%init> diff --git a/httemplate/edit/invoice_template.html b/httemplate/edit/invoice_template.html index 9cec62c86..3553c617b 100644 --- a/httemplate/edit/invoice_template.html +++ b/httemplate/edit/invoice_template.html @@ -8,7 +8,8 @@ <FORM ACTION="process/invoice_template.html" METHOD="POST"> <INPUT TYPE="hidden" NAME="confname" VALUE="<% $confname %>"> -% if ( $type eq 'html' ) { +% #if ( $type eq 'html' ) { +% if ( 0 ) { #this seems to broken, using a text editor for everything for now <% include('/elements/htmlarea.html', 'field' => 'value', diff --git a/httemplate/edit/msg_template.html b/httemplate/edit/msg_template.html index 941554532..115032a07 100644 --- a/httemplate/edit/msg_template.html +++ b/httemplate/edit/msg_template.html @@ -1,14 +1,57 @@ -<% include( 'elements/edit.html', - 'html_init' => '<TABLE id="outerTable"><TR><TD>', - 'body_etc' => $body_etc, - 'name_singular' => 'template', - 'table' => 'msg_template', - 'viewall_dir' => 'browse', - 'agent_virt' => 1, - 'agent_null' => 1, - 'agent_null_right' => ['Edit global templates', 'Configuration'], +<& elements/edit.html, + 'html_init' => '<TABLE id="outerTable"><TR><TD>', + 'body_etc' => $body_etc, + 'name_singular' => 'template', + 'table' => 'msg_template', + 'viewall_dir' => 'browse', + 'agent_virt' => 1, + 'agent_null' => 1, + 'agent_null_right' => [ 'View global templates', 'Edit global templates' ], - 'fields' => [ + 'fields' => \@fields, + 'labels' => { + 'msgnum' => 'Template', + 'agentnum' => 'Agent', + 'msgname' => 'Template name', + 'from_addr' => 'From: ', + 'bcc_addr' => 'Bcc: ', + 'locale' => 'Language', + 'subject' => 'Subject: ', + 'body' => 'Message body', + }, + 'edit_callback' => \&edit_callback, + 'error_callback' => \&edit_callback, + 'html_bottom' => '</DIV>', + 'html_foot' => ( $no_submit ? '' : "</TD>$sidebar</TR></TABLE>" ), + 'no_submit' => $no_submit, +&> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right([ 'View templates', 'View global templates', + 'Edit templates', 'Edit global templates', + ]); + +my $body_etc = ''; +$body_etc = q!onload="document.getElementById('locale').onchange()"! + if $cgi->param('locale') eq 'new'; + +my $msgnum = $cgi->param('msgnum'); +my $msg_template = $msgnum ? qsearchs('msg_template', {msgnum=>$msgnum}) : ''; + +my $no_submit = 0; +my @fields = (); +if ( $curuser->access_right('Edit global templates') + || ( $curuser->access_right('Edit templates') + && $msg_template + && $msg_template->agentnum + && $curuser->agentnums_href->{$msg_template->agentnum} + ) + ) +{ + push @fields, { field => 'agentnum', type => 'select-agent', }, @@ -25,33 +68,32 @@ type => 'htmlarea', width => 763 }, - ], - 'labels' => { - 'msgnum' => 'Template', - 'agentnum' => 'Agent', - 'msgname' => 'Template name', - 'from_addr' => 'From: ', - 'bcc_addr' => 'Bcc: ', - 'locale' => 'Language', - 'subject' => 'Subject: ', - 'body' => 'Message body', - }, - 'edit_callback' => \&edit_callback, - 'error_callback' => \&edit_callback, - 'html_bottom' => '</DIV>', - 'html_foot' => "</TD>$sidebar</TR></TABLE>", - ) - %> -<%init> + ; +} else { #readonly -die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Edit templates') - || $FS::CurrentUser::CurrentUser->access_right('Edit global templates') - || $FS::CurrentUser::CurrentUser->access_right('Configuration'); + $no_submit = 1; -my $body_etc = ''; -$body_etc = q!onload="document.getElementById('locale').onchange()"! - if $cgi->param('locale') eq 'new'; + push @fields, + { field => 'agentnum', + type => 'select-agent', + fixed => 1, + }, + { field => 'msgname', type => 'fixed', }, + { field => 'from_addr', type => 'fixed', }, + { field => 'bcc_addr', type => 'fixed', }, + { type => 'tablebreak-tabs', + include_opt_callback => \&menubar_opt_callback, + }, + # template_content fields + { field => 'locale', type => 'hidden' }, + { field => 'subject', type => 'fixed', }, + { field => 'body', + type => 'fixed', + noescape => 1, + }, + ; + +} sub new_callback { my ($cgi, $object, $fields_listref, $opt_hashref) = @_; @@ -182,8 +224,18 @@ my %substitutions = ( '$country' => 'Country', '$daytime' => 'Day phone', '$night' => 'Night phone', + '$mobile' => 'Mobile phone', '$fax' => 'Fax', ], + 'service' => [ + '$ship_address1' => 'Address line 1', + '$ship_address2' => 'Address line 2', + '$ship_city' => 'City', + '$ship_county' => 'County', + '$ship_state' => 'State', + '$ship_zip' => 'Zip', + '$ship_country' => 'Country', + ], 'cust_bill' => [ '$invnum' => 'Invoice#', ], @@ -238,15 +290,10 @@ my %substitutions = ( '$error' => 'Decline reason', ], ); -my @c = @{ $substitutions{'contact'} }; -for (my $i=0; $i<scalar(@c); $i += 2) { - $c[$i] =~ s/\$(.*)/\$ship_$1/; -} -$substitutions{'shipping'} = \@c; tie my %sections, 'Tie::IxHash', ( 'contact' => 'Name and contact info (billing)', -'shipping' => 'Name and contact info (shipping)', +'service' => 'Service address', 'cust_main' => 'Customer status and payment info', 'cust_pkg' => 'Package fields', 'cust_bill' => 'Invoice fields', diff --git a/httemplate/edit/part_referral.html b/httemplate/edit/part_referral.html index daf8773f0..e9fd79452 100755 --- a/httemplate/edit/part_referral.html +++ b/httemplate/edit/part_referral.html @@ -3,9 +3,12 @@ 'table' => 'part_referral', 'fields' => [ 'referral', { field=>'agentnum', type=>'select-agent', }, + { field=>'disabled', type=>'checkbox', value=>'Y' } , ], - 'labels' => { 'referral' => 'Advertising source', + 'labels' => { 'refnum' => 'Ad Source', + 'referral' => 'Advertising source', 'agentnum' => 'Agent', + 'disabled' => 'Disabled', }, 'viewall_dir' => 'browse', ) diff --git a/httemplate/edit/part_svc.cgi b/httemplate/edit/part_svc.cgi index fae896154..4bd083798 100755 --- a/httemplate/edit/part_svc.cgi +++ b/httemplate/edit/part_svc.cgi @@ -1,17 +1,27 @@ -<% include('/elements/header.html', "$action Service Definition", +<& /elements/header.html, "$action Service Definition", menubar('View all service definitions' => "${p}browse/part_svc.cgi"), #" onLoad=\"visualize()\"" - ) -%> +&> + +<& /elements/init_overlib.html &> + +<BR> <FORM NAME="dummy"> - Service Part #<% $part_svc->svcpart ? $part_svc->svcpart : "(NEW)" %> -<BR><BR> -Service <INPUT TYPE="text" NAME="svc" VALUE="<% $hashref->{svc} %>"><BR> +<FONT CLASS="fsinnerbox-title">Service Part #<% $part_svc->svcpart ? $part_svc->svcpart : "(NEW)" %></FONT> +<TABLE CLASS="fsinnerbox"> +<TR> + <TD ALIGN="right">Service</TD> + <TD><INPUT TYPE="text" NAME="svc" VALUE="<% $hashref->{svc} %>"></TD> +<TR> + +<& /elements/tr-select-part_svc_class.html, curr_value=>$hashref->{classnum} &> -Self-service access: -<SELECT NAME="selfservice_access"> +<TR> + <TD ALIGN="right">Self-service access</TD> + <TD> + <SELECT NAME="selfservice_access"> % tie my %selfservice_access, 'Tie::IxHash', #false laziness w/browse/part_svc % '' => 'Yes', % 'hidden' => 'Hidden', @@ -22,12 +32,22 @@ Self-service access: <% $_ eq $hashref->{'selfservice_access'} ? 'SELECTED' : '' %> ><% $selfservice_access{$_} %> % } -</SELECT><BR> + </SELECT> + </TD> +</TR> -<INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<% $hashref->{disabled} eq 'Y' ? ' CHECKED' : '' %>> Disable new orders<BR> -<INPUT TYPE="checkbox" NAME="preserve" VALUE="Y"<% $hashref->{'preserve'} eq 'Y' ? ' CHECKED' : '' %>> Preserve this service on package cancellation<BR> +<TR> + <TD ALIGN="right">Disable new orders</TD> + <TD><INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<% $hashref->{disabled} eq 'Y' ? ' CHECKED' : '' %>></TD> +</TR> +<TR> + <TD ALIGN="right">Preserve this service on package cancellation</TD> + <TD><INPUT TYPE="checkbox" NAME="preserve" VALUE="Y"<% $hashref->{'preserve'} eq 'Y' ? ' CHECKED' : '' %>> </TD> +</TR> + +</TABLE> <INPUT TYPE="hidden" NAME="svcpart" VALUE="<% $hashref->{svcpart} %>"> @@ -76,6 +96,18 @@ Self-service access: % ? ( $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, @@ -84,15 +116,16 @@ Self-service access: % 'form_name' => 'dummy', % #'form_action' => 'process/part_svc.cgi', % 'form_action' => 'part_svc.cgi', #self -% 'form_text' => [ qw( svc svcpart ) ], -% 'form_select' => [ 'selfservice_access' ], -% 'form_checkbox' => [ 'disabled', 'preserve' ], +% '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; +% #$html .= $svcdb_info; % % my $columns = 3; % my $count = 0; @@ -267,6 +300,7 @@ Self-service access: % % $html .= include('/elements/select-table.html', % 'element_name' => "${layer}__${field}_classnum", +% 'id' => "${layer}__${field}_classnum", % 'element_etc' => ( $is_inv % ? $disabled % : $nodisplay @@ -349,6 +383,7 @@ Self-service access: % $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', % ); @@ -382,7 +417,8 @@ Self-service access: % % $html .= include('/elements/progress-init.html', % $layer, #form name -% [ qw(svc svcpart selfservice_access disabled preserve +% [ qw(svc svcpart classnum selfservice_access +% disabled preserve % exportnum), % @fields ], % 'process/part_svc.cgi', @@ -401,9 +437,8 @@ Self-service access: % % }, % ); -% -% +<BR> Table <% $widget->html %> <% include('/elements/footer.html') %> @@ -451,66 +486,6 @@ my %communigate_fields = ( #'svc_cert' => { map { $_=>1 } qw( ) }, ); -my $svcdb_info = ' -<TABLE> - <TR> - <TH ALIGN="left">Generic</TH> - <TH ALIGN="left">Access</TH> - <TH ALIGN="left">Telephony</TH> -<!-- <TH>Hosting</TH> - <TH>Colocation</TH> ---> - </TR> - <TR> - <TD VALIGN="top"> - <UL STYLE="margin:0"> - <LI><B>svc_acct</B>: Accounts - anything with a username (mailbox, shell, RADIUS, etc.) - <LI><B>svc_hardware</B>: Equipment supplied to customers - <LI><B>svc_external</B>: Externally-tracked service - </UL> - </TD> - <TD VALIGN="top"> - <UL STYLE="margin:0"> - <LI><B>svc_dsl</B>: DSL - <LI><B>svc_broadband</B>: Wireless broadband - <LI><B>svc_dish</B>: DISH Network - </UL> - </TD> - <TD VALIGN="top"> - <UL STYLE="margin:0"> - <LI><B>svc_phone</B>: Customer phone number - <LI><B>svc_pbx</B>: Customer PBX - </UL> - </TD> - </TR> -</TABLE> -<BR> -<TABLE> - <TR> - <TH ALIGN="left">Hosting</TH> - <TH ALIGN="left">Colocation</TH> - </TR> - <TD VALIGN="top"> - <UL STYLE="margin:0"> - <LI><B>svc_domain</B>: Domain - <LI><B>svc_cert</B>: Certificate - <LI><B>svc_forward</B>: Mail forwarding - <LI><B>svc_mailinglist</B>: Mailing list - <LI><B>svc_www</B>: Virtual domain website - </UL> - </TD> - <TD VALIGN="top"> - <UL STYLE="margin:0"> - <LI><B>svc_port</B>: Customer router/switch port - </UL> - </TD> - </TR> -<TABLE> -<!-- <LI>svc_charge - One-time charges (Partially unimplemented) - <LI>svc_wo - Work orders (Partially unimplemented) ---> -'; - 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 diff --git a/httemplate/edit/part_svc_class.html b/httemplate/edit/part_svc_class.html new file mode 100644 index 000000000..0d9a00727 --- /dev/null +++ b/httemplate/edit/part_svc_class.html @@ -0,0 +1,6 @@ +<% include( 'elements/class_Common.html', + 'name' => 'Service class', + 'table' => 'part_svc_class', + 'nocat' => 1, + ) +%> diff --git a/httemplate/edit/process/cust_location.cgi b/httemplate/edit/process/cust_location.cgi index 790fc8ea4..b9f93db8b 100644 --- a/httemplate/edit/process/cust_location.cgi +++ b/httemplate/edit/process/cust_location.cgi @@ -28,10 +28,12 @@ my $cust_location = qsearchs({ }); die "unknown locationnum $locationnum" unless $cust_location; -my $new = { +my $new = FS::cust_location->new({ + custnum => $cust_location->custnum, + prospectnum => $cust_location->prospectnum, map { $_ => scalar($cgi->param($_)) } qw( address1 address2 city county state zip country ) -}; +}); my $error = $cust_location->move_to($new); diff --git a/httemplate/edit/process/cust_main.cgi b/httemplate/edit/process/cust_main.cgi index 44fbb4f10..5ee553b32 100755 --- a/httemplate/edit/process/cust_main.cgi +++ b/httemplate/edit/process/cust_main.cgi @@ -57,17 +57,40 @@ push @invoicing_list, 'POST' if $cgi->param('invoicing_list_POST'); push @invoicing_list, 'FAX' if $cgi->param('invoicing_list_FAX'); $cgi->param('invoicing_list', join(',', @invoicing_list) ); +# is this actually used? if so, we need to clone locations... +# but I can't find anything that sets this parameter to a non-empty value +$cgi->param('duplicate_of_custnum') =~ /^(\d+)$/; +my $duplicate_of = $1; + +my %locations; +for my $pre (qw(bill ship)) { + + my %hash; + foreach ( FS::cust_main->location_fields ) { + $hash{$_} = scalar($cgi->param($pre.'_'.$_)); + } + $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 ); + +} + +if ( ($cgi->param('same') || '') eq 'Y' ) { + $locations{ship} = $locations{bill}; +} #create new record object +# but explicitly avoid setting ship_ fields my $new = new FS::cust_main ( { - map { - $_, scalar($cgi->param($_)) - } fields('cust_main') + map { ( $_, scalar($cgi->param($_)) ) } (fields('cust_main')), + map { ( "ship_$_", '' ) } (FS::cust_main->location_fields) } ); -$cgi->param('duplicate_of_custnum') =~ /^(\d+)$/; -my $duplicate_of = $1; +$new->invoice_noemail( ($cgi->param('invoice_email') eq 'Y') ? '' : 'Y' ); + if ( $duplicate_of ) { # then negate all changes to the customer; the only change we should # make is to order a package, if requested @@ -76,11 +99,9 @@ if ( $duplicate_of ) { or die "nonexistent existing customer (custnum $duplicate_of)"; } -if ( defined($cgi->param('same')) && $cgi->param('same') eq "Y" ) { - $new->setfield("ship_$_", '') foreach qw( - last first company address1 address2 city county state zip - country daytime night fax - ); +for my $pre (qw(bill ship)) { + $new->set($pre.'_location', $locations{$pre}); + $new->set($pre.'_locationnum', $locations{$pre}->locationnum); } if ( $cgi->param('no_credit_limit') ) { @@ -89,9 +110,11 @@ if ( $cgi->param('no_credit_limit') ) { $new->tagnum( [ $cgi->param('tagnum') ] ); -my %usedatetime = ( 'birthdate' => 1 ); +my %usedatetime = ( 'birthdate' => 1, + 'spouse_birthdate' => 1, + ); -foreach my $dfield (qw( birthdate signupdate )) { +foreach my $dfield (qw( birthdate spouse_birthdate signupdate )) { if ( $cgi->param($dfield) && $cgi->param($dfield) =~ /^([ 0-9\-\/]{0,10})$/) { @@ -130,6 +153,7 @@ $new->setfield('paid', $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; #perhaps this stuff should go to cust_main.pm if ( $new->custnum eq '' or $duplicate_of ) { @@ -237,7 +261,7 @@ if ( $new->custnum eq '' or $duplicate_of ) { else { # create the customer $error ||= $new->insert( \%hash, \@invoicing_list, - 'tax_exemption'=> \@tax_exempt, + 'tax_exemption'=> \%tax_exempt, 'prospectnum' => scalar($cgi->param('prospectnum')), ); @@ -256,6 +280,7 @@ if ( $new->custnum eq '' or $duplicate_of ) { my $old = qsearchs( 'cust_main', { 'custnum' => $new->custnum } ); $error ||= "Old record not found!" unless $old; + if ( length($old->paycvv) && $new->paycvv =~ /^\s*\*+\s*$/ ) { $new->paycvv($old->paycvv); } @@ -294,8 +319,11 @@ if ( $new->custnum eq '' or $duplicate_of ) { local($FS::cust_main::DEBUG) = $DEBUG if $DEBUG; local($FS::Record::DEBUG) = $DEBUG if $DEBUG; + local($Data::Dumper::Sortkeys) = 1; + warn Dumper({ new => $new, old => $old }) if $DEBUG; + $error ||= $new->replace( $old, \@invoicing_list, - 'tax_exemption' => \@tax_exempt, + 'tax_exemption' => \%tax_exempt, ); warn "$me returned from replace" if $DEBUG; diff --git a/httemplate/edit/process/cust_pay.cgi b/httemplate/edit/process/cust_pay.cgi index 06f5e64d5..ce0ec3212 100755 --- a/httemplate/edit/process/cust_pay.cgi +++ b/httemplate/edit/process/cust_pay.cgi @@ -39,7 +39,13 @@ $cgi->param('link') =~ /^(custnum|invnum|popup)$/ my $field = my $link = $1; $field = 'custnum' if $field eq 'popup'; -my $_date = parse_datetime($cgi->param('_date')); +my $_date; +if ( $FS::CurrentUser::CurrentUser->access_right('Backdate payment') ) { + $_date = parse_datetime($cgi->param('_date')); +} +else { + $_date = time; +} my $new = new FS::cust_pay ( { $field => $linknum, diff --git a/httemplate/edit/process/elements/process.html b/httemplate/edit/process/elements/process.html index 12b3bd94b..2d39e9dce 100644 --- a/httemplate/edit/process/elements/process.html +++ b/httemplate/edit/process/elements/process.html @@ -250,8 +250,6 @@ foreach my $value ( @values ) { } - $error ||= $new->check; - my @args = (); if ( !$error && $opt{'args_callback'} ) { @args = &{ $opt{'args_callback'} }( $cgi, $new ); diff --git a/httemplate/edit/process/ftp_target.html b/httemplate/edit/process/ftp_target.html new file mode 100644 index 000000000..35f56c490 --- /dev/null +++ b/httemplate/edit/process/ftp_target.html @@ -0,0 +1,12 @@ +<& elements/process.html, + 'table' => 'ftp_target', + 'viewall_dir' => 'browse', + 'agent_null' => 1, +&> +<%init> +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right('Configuration'); + +</%init> diff --git a/httemplate/edit/process/msg_template.html b/httemplate/edit/process/msg_template.html index 47fe978a8..b19f5c542 100644 --- a/httemplate/edit/process/msg_template.html +++ b/httemplate/edit/process/msg_template.html @@ -9,9 +9,7 @@ %> <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('Edit templates') - || $FS::CurrentUser::CurrentUser->access_right('Edit global templates') - || $FS::CurrentUser::CurrentUser->access_right('Configuration'); + unless $FS::CurrentUser::CurrentUser->access_right(['Edit templates','Edit global templates']); sub precheck_callback { my $cgi = shift; diff --git a/httemplate/edit/process/part_svc_class.html b/httemplate/edit/process/part_svc_class.html new file mode 100644 index 000000000..16165dd18 --- /dev/null +++ b/httemplate/edit/process/part_svc_class.html @@ -0,0 +1,11 @@ +<% include( 'elements/process.html', + 'table' => 'part_svc_class', + 'viewall_dir' => 'browse', + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +</%init> diff --git a/httemplate/edit/process/quick-cust_pkg.cgi b/httemplate/edit/process/quick-cust_pkg.cgi index fab85252b..ba4c5b1b6 100644 --- a/httemplate/edit/process/quick-cust_pkg.cgi +++ b/httemplate/edit/process/quick-cust_pkg.cgi @@ -48,6 +48,9 @@ die 'unknown custnum' unless $cust_main; $cgi->param('pkgpart') =~ /^(\d+)$/ or die 'illegal pkgpart '. $cgi->param('pkgpart'); my $pkgpart = $1; +$cgi->param('quantity') =~ /^(\d+)$/ + or die 'illegal quantity '. $cgi->param('quantity'); +my $quantity = $1; $cgi->param('refnum') =~ /^(\d*)$/ or die 'illegal refnum '. $cgi->param('refnum'); my $refnum = $1; @@ -78,6 +81,7 @@ if ( $cgi->param('qualnum') ) { my $cust_pkg = new FS::cust_pkg { 'custnum' => $custnum, 'pkgpart' => $pkgpart, + 'quantity' => $quantity, 'start_date' => ( scalar($cgi->param('start_date')) ? parse_datetime($cgi->param('start_date')) : '' diff --git a/httemplate/edit/process/sales.cgi b/httemplate/edit/process/sales.cgi new file mode 100644 index 000000000..edef4d65c --- /dev/null +++ b/httemplate/edit/process/sales.cgi @@ -0,0 +1,23 @@ +<% include( 'elements/process.html', + 'table' => 'sales', + 'viewall_dir' => 'browse', + 'viewall_ext' => 'cgi', + 'debug' => '1', + 'process_m2m' => { 'link_table' => 'access_groupsales', + 'target_table' => 'access_group', + }, + 'edit_ext' => 'cgi', + ) +%> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +if ( FS::Conf->new->exists('disable_acl_changes') ) { + errorpage('ACL changes disabled in public demo.'); + die "shouldn't be reached"; +} + +</%init> + diff --git a/httemplate/edit/prospect_main.html b/httemplate/edit/prospect_main.html index c8c8e98e1..d3985410b 100644 --- a/httemplate/edit/prospect_main.html +++ b/httemplate/edit/prospect_main.html @@ -36,6 +36,9 @@ 'empty_label' => 'No address', 'disable_empty' => $conf->exists('prospect_main-location_required'), 'alt_format' => $conf->exists('prospect_main-alt_address_format'), + 'include_opt_callback' => sub { + 'prospect_main' => shift + }, }, ], 'new_callback' => $new_callback, diff --git a/httemplate/edit/quick-charge.html b/httemplate/edit/quick-charge.html index 27841063f..1d9647f2f 100644 --- a/httemplate/edit/quick-charge.html +++ b/httemplate/edit/quick-charge.html @@ -11,13 +11,30 @@ <SCRIPT TYPE="text/javascript"> -function enable_quick_charge () { +function enable_quick_charge (e) { + if ( document.QuickChargeForm.amount.value && document.QuickChargeForm.pkg.value ) { document.QuickChargeForm.submit.disabled = false; } else { document.QuickChargeForm.submit.disabled = true; } + +% if ( $curuser->option('disable_enter_submit_onetimecharge') ) { + + var key; + if (window.event) + key = window.event.keyCode; //IE + else + + key = e.which; //firefox, others + + return (key != 13); + +% } else { + return true; +% } + } function validate_quick_charge () { @@ -76,7 +93,12 @@ function bill_now_changed (what) { </SCRIPT> -<FORM ACTION="process/quick-charge.cgi" NAME="QuickChargeForm" ID="QuickChargeForm" METHOD="POST" onsubmit="document.QuickChargeForm.submit.disabled=true;return validate_quick_charge();"> +<FORM ACTION = "process/quick-charge.cgi" + NAME = "QuickChargeForm" + ID = "QuickChargeForm" + METHOD = "POST" + onSubmit = "document.QuickChargeForm.submit.disabled=true; return validate_quick_charge();" +> <INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>"> @@ -85,7 +107,13 @@ function bill_now_changed (what) { <TR> <TD ALIGN="right"><% mt('Amount') |h %> </TD> <TD> - <% $money_char %><INPUT TYPE="text" NAME="amount" SIZE=6 VALUE="<% $amount %>" onChange="enable_quick_charge()" onKeyPress="enable_quick_charge()"> + <% $money_char %><INPUT TYPE = "text" + NAME = "amount" + SIZE = 6 + VALUE = "<% $amount %>" + onChange = "return enable_quick_charge(event)" + onKeyPress = "return enable_quick_charge(event)" + > </TD> </TR> @@ -93,7 +121,11 @@ function bill_now_changed (what) { <TR> <TD ALIGN="right"><% mt('Quantity') |h %> </TD> <TD> - <INPUT TYPE="text" NAME="quantity" SIZE=4 VALUE="<% $quantity %>"> + <INPUT TYPE = "text" + NAME = "quantity" + SIZE = 4 + VALUE = "<% $quantity %>" + onKeyPress = "return enable_quick_charge(event)"> </TD> </TR> % } @@ -107,6 +139,7 @@ function bill_now_changed (what) { NAME = "bill_now" VALUE = "1" <% $cgi->param('bill_now') ? 'CHECKED' : '' %> + onClick = "bill_now_changed(this);" onChange = "bill_now_changed(this);" > <% mt('with terms') |h %> @@ -127,7 +160,11 @@ function bill_now_changed (what) { SIZE = 32 ID = "start_date_text" VALUE = "<% $start_date %>" - <% $cgi->param('bill_now') ? 'STYLE = "background-color:#dddddd" DISABLED' : '' %> + onKeyPress="return enable_quick_charge(event)" + <% $cgi->param('bill_now') + ? 'STYLE = "background-color:#dddddd" DISABLED' + : '' + %> > <IMG SRC = "<%$fsurl%>images/calendar.png" ID = "start_date_button" @@ -173,7 +210,14 @@ function bill_now_changed (what) { <TR> <TD ALIGN="right"><% mt('Description') |h %> </TD> <TD> - <INPUT TYPE="text" NAME="pkg" SIZE="50" MAXLENGTH="50" VALUE="<% $pkg %>" onChange="enable_quick_charge()" onKeyPress="enable_quick_charge()"> + <INPUT TYPE = "text" + NAME = "pkg" + SIZE = "50" + MAXLENGTH = "50" + VALUE = "<% $pkg %>" + onChange = "return enable_quick_charge(event)" + onKeyPress = "return enable_quick_charge(event)" + > </TD> </TR> @@ -191,7 +235,15 @@ function bill_now_changed (what) { <TR> <TD></TD> <TD> - <INPUT TYPE="text" NAME="description<% $row %>" SIZE="60" MAXLENGTH="65" VALUE="<% $param->{"description$row"} |h %>" rownum="<% $row %>" onkeyup = "possiblyAddRow;" > + <INPUT TYPE = "text" + NAME = "description<% $row %>" + SIZE = "60" + MAXLENGTH = "65" + VALUE = "<% $param->{"description$row"} |h %>" + rownum = "<% $row %>" + onKeyPress = "return enable_quick_charge(event)" + onKeyUp = "return possiblyAddRow(event)" + > </TD> </TR> % } @@ -210,10 +262,26 @@ function bill_now_changed (what) { var rownum = <% $row %>; - function possiblyAddRow() { + function possiblyAddRow(e) { + if ( ( rownum - this.getAttribute('rownum') ) == 1 ) { addRow(); } + +% if ( $curuser->option('disable_enter_submit_onetimecharge') ) { + + var key; + if (window.event) + key = window.event.keyCode; //IE + else + key = e.which; //firefox, others + + return (key != 13); + +% } else { + return true; +% } + } function addRow() { @@ -228,14 +296,16 @@ function bill_now_changed (what) { var description_cell = document.createElement('TD'); - var description_input = document.createElement('INPUT'); - description_input.setAttribute('name', 'description'+rownum); - description_input.setAttribute('id', 'description'+rownum); - description_input.setAttribute('size', 60); - description_input.setAttribute('maxLength', 65); - description_input.setAttribute('rownum', rownum); - description_input.onkeyup = possiblyAddRow; - description_cell.appendChild(description_input); + //var description_input = document.createElement('INPUT'); + var di = document.createElement('INPUT'); + di.setAttribute('name', 'description'+rownum); + di.setAttribute('id', 'description'+rownum); + di.setAttribute('size', 60); + di.setAttribute('maxLength', 65); + di.setAttribute('rownum', rownum); + di.onkeyup = possiblyAddRow; + di.onkeypress = enable_quick_charge; + description_cell.appendChild(di); row.appendChild(description_cell); @@ -251,8 +321,10 @@ function bill_now_changed (what) { </HTML> <%init> +my $curuser = $FS::CurrentUser::CurrentUser; + die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('One-time charge'); + unless $curuser->access_right('One-time charge'); my $conf = new FS::Conf; my $date_format = $conf->config('date_format') || '%m/%d/%Y'; diff --git a/httemplate/edit/sales.cgi b/httemplate/edit/sales.cgi new file mode 100755 index 000000000..3497de505 --- /dev/null +++ b/httemplate/edit/sales.cgi @@ -0,0 +1,79 @@ +<% include("/elements/header.html","$action Sales Person", menubar( + 'View all sales people' => $p. 'browse/sales.cgi', +)) %> + +<% include('/elements/error.html') %> + +<FORM METHOD = POST + ACTION = "<%popurl(1)%>process/sales.cgi" +> + +<INPUT TYPE="hidden" NAME="salesnum" VALUE="<% $sales->salesnum %>"> +Sales #<% $sales->salesnum ? $sales->salesnum : "(NEW)" %> + +<% &ntable("#cccccc", 2, '') %> + + <TR> + <TH ALIGN="right">Sales</TH> + <TD><INPUT TYPE="text" NAME="salesperson" SIZE=32 VALUE="<% $sales->salesperson %>"></TD> + </TR> + + <TR> + <TD ALIGN="right"><% emt('Agent') %></TD> + <TD> + <& /elements/select-agent.html, + 'curr_value' => $sales->salesnum, + 'disable_empty' => 1, + &> + </TD> + </TR> + + <TR> + <TD ALIGN="right">Disable</TD> + <TD><INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<% $sales->disabled eq 'Y' ? ' CHECKED' : '' %>></TD> + </TR> + + <TR> + <TD ALIGN="right">Access Groups</TD> + <TD><% include('/elements/checkboxes-table.html', + 'source_obj' => $sales, + 'link_table' => 'access_groupsales', + 'target_table' => 'access_group', + 'name_col' => 'groupname', + 'target_link' => $p. 'edit/access_group.html?', + ) + %> + </TD> + </TR> + +</TABLE> + +<BR> +<INPUT TYPE="submit" VALUE="<% $sales->salesnum ? "Apply changes" : "Add sales" %>"> + +</FORM> + +<% include('/elements/footer.html') %> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $sales; +if ( $cgi->param('error') ) { + $sales = new FS::sales ( { + map { $_, scalar($cgi->param($_)) } fields('sales') + } ); +} elsif ( $cgi->keywords ) { + my($query) = $cgi->keywords; + $query =~ /^(\d+)$/; + $sales = qsearchs( 'sales', { 'salesnum' => $1 } ); +} else { #adding + $sales = new FS::sales {}; +} +my $action = $sales->salesnum ? 'Edit' : 'Add'; + +my $conf = new FS::Conf; + +</%init> diff --git a/httemplate/edit/svc_broadband.cgi b/httemplate/edit/svc_broadband.cgi index b266928a1..0d4b9897b 100644 --- a/httemplate/edit/svc_broadband.cgi +++ b/httemplate/edit/svc_broadband.cgi @@ -3,7 +3,7 @@ 'name' => 'broadband service', 'table' => 'svc_broadband', 'fields' => \@fields, - 'field_callback' => $field_callback, + 'svc_field_callback' => $svc_field_callback, 'svc_new_callback' => $svc_edit_callback, 'svc_edit_callback' => $svc_edit_callback, 'svc_error_callback' => $svc_edit_callback, @@ -161,20 +161,14 @@ my $svc_edit_callback = sub { } }; -my $field_callback = sub { +my $svc_field_callback = sub { my ($cgi, $object, $fieldref) = @_; my $columndef = $part_svc->part_svc_column($fieldref->{'field'}); - if ($columndef->columnflag eq 'F') { - $fieldref->{'type'} = length($columndef->columnvalue) - ? 'fixed' - : 'hidden'; - $fieldref->{'value'} = $columndef->columnvalue; + if ($fieldref->{field} eq 'usergroup' && $columndef->columnflag eq 'F') { - if ( $fieldref->{field} eq 'usergroup' ) { - $fieldref->{'formatted_value'} = - [ $object->radius_groups('long_description') ]; - } + $fieldref->{'formatted_value'} = + [ $object->radius_groups('long_description') ]; } }; diff --git a/httemplate/edit/tower.html b/httemplate/edit/tower.html index 82513082c..03b488e86 100644 --- a/httemplate/edit/tower.html +++ b/httemplate/edit/tower.html @@ -20,8 +20,8 @@ 'sectornum' => 'Sector', 'disabled' => 'Disabled', 'default_ip_addr' => 'Tower IP address', - 'latitude', => 'Latitude', - 'longitude', => 'Longitude', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', }, &> <%init> diff --git a/httemplate/elements/customer-table.html b/httemplate/elements/customer-table.html index a517ece2a..79443dc8b 100644 --- a/httemplate/elements/customer-table.html +++ b/httemplate/elements/customer-table.html @@ -41,6 +41,8 @@ Example: <SCRIPT TYPE="text/javascript"> + var num_open_invoices = new Array; + function clearhint_invnum() { if ( this.value == 'Not found' || this.value == 'Multiple' ) { @@ -90,7 +92,7 @@ Example: customer_select.style.display = 'none'; return false; - } else if ( customerArray.length == 5 ) { + } else if ( customerArray.length == 6 ) { custnum_obj.value = customerArray[0]; custnum_obj.style.color = '#000000'; @@ -99,6 +101,7 @@ Example: update_balance_text(searchrow, customerArray[2]); update_status_text( searchrow, customerArray[3]); update_status_color(searchrow, '#'+customerArray[4]); + update_num_open(searchrow, customerArray[5]); customer.style.display = ''; customer_select.style.display = 'none'; @@ -140,6 +143,7 @@ Example: update_balance_text(searchrow, ''); update_status_text(searchrow, ''); update_status_color(searchrow, '#000000'); + update_num_open(searchrow, 0); function search_invnum_update(customers) { @@ -175,15 +179,16 @@ Example: if ( ( <% $opt{prefix} %>rownum - searchrow ) == 1 ) { <% $opt{prefix} %>addRow(); } - var customer = document.getElementById('customer'+searchrow); - customer.value = 'searching...'; - customer.disabled = true; - customer.style.color = '#000000'; - customer.style.backgroundColor = '#dddddd'; + + var customer_obj = document.getElementById('customer'+searchrow); + customer_obj.value = 'searching...'; + customer_obj.disabled = true; + customer_obj.style.color = '#000000'; + customer_obj.style.backgroundColor = '#dddddd'; var customer_select = document.getElementById('cust_select'+searchrow); - customer.style.display = ''; + customer_obj.style.display = ''; customer_select.style.display = 'none'; var invnum = document.getElementById('invnum'+searchrow); @@ -192,15 +197,44 @@ Example: update_balance_text(searchrow, ''); update_status_text( searchrow, ''); update_status_color(searchrow, '#000000'); + update_num_open(searchrow, 0); function search_custnum_update(customers) { - var customerArray = eval('(' + customers + ')') || []; - update_customer(searchrow, customerArray); + var customerArrayArray = eval('(' + customers + ')') || []; + + if ( customerArrayArray.length == 1 ) { + update_customer(searchrow, customerArrayArray[0]); % if ( $opt{custnum_update_callback} ) { - <% $opt{custnum_update_callback} %>(searchrow, '<% $opt{prefix} %>') + <% $opt{custnum_update_callback} %>(searchrow, '<% $opt{prefix} %>') % } + + } else { + + custnum_obj.value = 'Multiple'; // or something + custnum_obj.style.color = '#ff0000'; + + //blank the current list + customer_select.options.length = 0; + + opt(customer_select, '', 'Multiple customers match "' + custnum + '" - select one', '#ff0000'); + //add the multiple customers + for ( var s = 0; s < customerArrayArray.length; s++ ) { + opt(customer_select, + JSON.stringify(customerArrayArray[s]), + customerArrayArray[s][1], + '#000000'); + } + + opt(customer_select, 'cancel', '(Edit search string)', '#000000'); + + customer_obj.style.display = 'none'; + + customer_select.style.display = ''; + + } + } custnum_search(custnum, search_custnum_update ); @@ -245,7 +279,7 @@ Example: if ( customerArrayArray.length == 1 ) { - update_customer(customerArrayArray[1]); + update_customer(searchrow, customerArrayArray[0]); % if ( $opt{custnum_update_callback} ) { <% $opt{custnum_update_callback} %>(searchrow, '<% $opt{prefix} %>') % } @@ -337,6 +371,9 @@ Example: document.getElementById('balance'+rownum+'_text').innerHTML = newval; } + function update_num_open(rownum, newval) { + num_open_invoices[rownum] = newval; + } </SCRIPT> @@ -356,7 +393,7 @@ Example: % my $row = 0; % for ( $row = 0; exists($param->{"custnum$row"}); $row++ ) { - <TR> + <TR id="row<%$row%>" rownum="<%$row%>"> <TD> <INPUT TYPE = "text" NAME = "invnum<% $row %>" @@ -458,19 +495,24 @@ Example: % my $color = $opt{color}->[$col]; % my $font = $color ? qq(<FONT COLOR="$color">) : ''; % my $onchange = ''; -% if ( $opt{footer}->[$col] eq '_TOTAL' ) { +% if ( $opt{onchange}->[$col] ) { +% $onchange = 'onchange="'.$opt{onchange}->[$col].'"'; +% } +% elsif ( $opt{footer}->[$col] eq '_TOTAL' ) { % $total[$col] += $value; % $onchange = $opt{prefix}. "calc_total$col();"; % $onchange = qq(onchange="$onchange" onkeyup="$onchange"); % } <TD ALIGN="<% $align %>"> -% if (! $types->[$col] || $types->[$col] eq 'text') { - <INPUT TYPE = "text" +% my $type = $types->[$col] || 'text'; +% if ($type eq 'text' or $type eq 'checkbox') { + <INPUT TYPE = "<% $type %>" NAME = "<% $name %>" ID = "<% $name %>" SIZE = "<% $size %>" STYLE = "text-align: <% $align %>;" VALUE = "<% $value %>" + rownum = "<% $row %>" <% $onchange %> > % } elsif ($types->[$col] eq 'immutable') { @@ -485,7 +527,7 @@ Example: </TR> % } -<TR> +<TR id="row_total"> <TH COLSPAN=5 ID="<% $opt{'prefix'} %>_TOTAL_TOTAL"> Total <% $row ? $row-1 : 0 %> <% PL($opt{name_singular} || 'customer', ( $row ? $row-1 : 0 ) ) %> @@ -559,7 +601,8 @@ Example: var table = document.getElementById('<% $opt{prefix} %>OneTrueTable'); var tablebody = table.getElementsByTagName('tbody').item(0); - var row = table.insertRow(rownum+1); + var row = table.insertRow(table.rows.length - 1); + row.setAttribute('id', 'row'+rownum); var invnum_cell = document.createElement('TD'); @@ -676,7 +719,7 @@ Example: % } else { % $value = $param->{"$field$row"}; % } - var my_text = document.createTextNode('<% $value %>'); + var my_text = document.createTextNode(<% $value |js_string %>); my_cell.appendChild(my_text); % } @@ -686,10 +729,17 @@ Example: my_input.setAttribute('id', '<% $name %>'+<% $opt{prefix} %>rownum); my_input.style.textAlign = '<% $align{ $opt{align}->[$col] || 'l' } %>'; my_input.setAttribute('size', <% $sizes->[$col] || 10 %>); -% if ($types->[$col] eq 'immutable') { + my_input.setAttribute('rownum', <% $opt{prefix} %>rownum); +% if ( $types->[$col] eq 'immutable' ) { my_input.setAttribute('type', 'hidden'); % } -% if ( $opt{footer}->[$col] eq '_TOTAL' ) { +% elsif ( $types->[$col] eq 'checkbox' ) { + my_input.setAttribute('type', 'checkbox'); +% } +% if ( $opt{onchange}->[$col] ) { + my_input.onchange = <% $opt{onchange}->[$col] %>; +% } +% elsif ( $opt{footer}->[$col] eq '_TOTAL' ) { my_input.onchange = <% $opt{prefix} %>calc_total<%$col%>; my_input.onkeyup = <% $opt{prefix} %>calc_total<%$col%>; % } @@ -713,6 +763,11 @@ Example: + ' <% PL($opt{name_singular} || 'customer') %>'; } +% if ( $opt{add_row_callback} ) { + <% $opt{add_row_callback} %>(<% $opt{prefix} %>rownum, + '<% $opt{prefix} %>'); +% } + <% $opt{prefix} %>rownum++; } diff --git a/httemplate/elements/freeside.css b/httemplate/elements/freeside.css index c291e1e33..82eb9b562 100644 --- a/httemplate/elements/freeside.css +++ b/httemplate/elements/freeside.css @@ -108,8 +108,8 @@ a.fstab { -moz-border-radius-topright:8px; -webkit-border-radius-topleft:8px; -webkit-border-radius-topright:8px; - border-radius-topleft:8px; - border-radius-topright:8px; + border-top-left-radius:8px; + border-top-right-radius:8px; /*font-weight:bold;*/ /*padding-left:12px; padding-right:12px;*/ @@ -141,8 +141,8 @@ a.fstabselected { -moz-border-radius-topright:8px; -webkit-border-radius-topleft:8px; -webkit-border-radius-topright:8px; - border-radius-topleft:8px; - border-radius-topright:8px; + border-top-left-radius:8px; + border-top-right-radius:8px; /*font-weight:bold;*/ /*padding-left:12px; padding-right:12px;*/ @@ -175,8 +175,8 @@ div.fstabcontainer { -moz-border-radius-bottomright:8px; -webkit-border-radius-bottomleft:8px; -webkit-border-radius-bottomright:8px; - border-radius-bottomleft:8px; - border-radius-bottomright:8px; + border-bottom-left-radius:8px; + border-bottom-right-radius:8px; -moz-box-shadow: #666666 1px 1px 2px; -webkit-box-shadow: #666666 1px 1px 2px; box-shadow: #666666 1px 1px 2px; @@ -206,8 +206,8 @@ div.fstabcontainer { -moz-border-radius-bottomright:8px; -webkit-border-radius-bottomleft:8px; -webkit-border-radius-bottomright:8px; - border-radius-bottomleft:8px; - border-radius-bottomright:8px; + border-bottom-left-radius:8px; + border-bottom-right-radius:8px; -moz-box-shadow: #666666 1px 1px 2px; -webkit-box-shadow: #666666 1px 1px 2px; box-shadow: #666666 1px 1px 2px; @@ -238,8 +238,8 @@ div.fstabcontainer { -moz-border-radius-topright:8px; -webkit-border-radius-topleft:8px; -webkit-border-radius-topright:8px; - border-radius-topleft:8px; - border-radius-topright:8px; + border-top-left-radius:8px; + border-top-right-radius:8px; -moz-box-shadow: 1px 0px 1px #999999; -webkit-box-shadow: 1px 0px 1px #999999; box-shadow: 1px 0px 1px #999999; diff --git a/httemplate/elements/header.html b/httemplate/elements/header.html index d0ab3055d..c6ad3c387 100644 --- a/httemplate/elements/header.html +++ b/httemplate/elements/header.html @@ -42,6 +42,12 @@ Example: <% include('init_overlib.html') |n %> <% include('rs_init_object.html') |n %> <% include('logout.html') |n %> +% my $timeout = $conf->config('logout-timeout'); +% if ( $timeout && $timeout =~ /^\s*\d+\s*$/ ) { + <script type="text/javascript"> + setTimeout('logout()', <% 60000 * $timeout %>); + </script> +% } <% $head |n %> diff --git a/httemplate/elements/init_overlib.html b/httemplate/elements/init_overlib.html index d27ca3bda..986adec40 100644 --- a/httemplate/elements/init_overlib.html +++ b/httemplate/elements/init_overlib.html @@ -1,9 +1,16 @@ % for my $file (@files) { <SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/<%$file%>.js"></SCRIPT> % } +<%shared> +my $initialized = 0; #won't work if component is "preloaded"... so don't do that +</%shared> <%init> -my @files = map "overlibmws$_", ( '', qw( _iframe _draggable _crossframe ) ); -push @files, map { "${_}contentmws" } qw( iframe ajax ); +my @files = (); +if ( ! $initialized ) { + push @files, map "overlibmws$_", ( '', qw( _iframe _draggable _crossframe ) ); + push @files, map { "${_}contentmws" } qw( iframe ajax ); + $initialized++; +} </%init> diff --git a/httemplate/elements/location.html b/httemplate/elements/location.html index c606523f0..767231856 100644 --- a/httemplate/elements/location.html +++ b/httemplate/elements/location.html @@ -3,16 +3,16 @@ Example: include( '/elements/location.html', - 'object' => $cust_main, # or $cust_location - 'prefix' => $pre, #only for cust_main objects + 'object' => $cust_location + 'prefix' => $pre, # prefixed to form field names 'onchange' => $javascript, - 'disabled' => $disabled, - 'same_checked' => $same_checked, 'geocode' => $geocode, #passed through 'censustract' => $censustract, #passed through 'no_asterisks' => 0, #set true to disable the red asterisks next #to required fields 'address1_label' => 'Address', #label for address + 'enable_district' => 1, #show tax district field + 'enable_censustract' => 1, #show censustract field ) </%doc> @@ -40,12 +40,12 @@ Example: % } <TR> - <<%$th%> ALIGN="right"><%$r%><% $opt{'address1_label'} || emt('Address') %></<%$th%>> + <<%$th%> STYLE="width:16ex" ALIGN="right"><%$r%><% $opt{'address1_label'} || emt('Address') %></<%$th%>> <TD COLSPAN=7> <INPUT TYPE = "text" NAME = "<%$pre%>address1" ID = "<%$pre%>address1" - VALUE = "<% $object->get($pre.'address1') |h %>" + VALUE = "<% $object->get('address1') |h %>" SIZE = 54 onChange = "<% $onchange %>" <% $disabled %> @@ -62,7 +62,7 @@ Example: <INPUT TYPE = "text" NAME = "<%$pre%>address2" ID = "<%$pre%>address2" - VALUE = "<% $object->get($pre.'address2') |h %>" + VALUE = "<% $object->get('address2') |h %>" SIZE = 54 onChange = "<% $onchange %>" <% $disabled %> @@ -75,7 +75,7 @@ Example: <INPUT TYPE = "hidden" NAME = "<%$pre%>address2" - VALUE = "<% $object->get($pre.'address2') |h %>" + VALUE = "<% $object->get('address2') |h %>" > <TR> @@ -83,7 +83,7 @@ Example: <TD COLSPAN=7> % my $location_type = scalar($cgi->param('location_type')) -% || $object->get($pre.'location_type'); +% || $object->get('location_type'); % #my $location_number = scalar($cgi->param('location_number')) % # || $object->get($pre.'location_number'); % @@ -130,7 +130,7 @@ Example: <INPUT TYPE="text" NAME = "location_number" ID = "location_number" - VALUE = "<% scalar($cgi->param('location_number')) || $object->get($pre.'location_number') |h %>" + VALUE = "<% scalar($cgi->param('location_number')) || $object->get('location_number') |h %>" SIZE = "5" <% $disabled || ($location_type ? '' : 'DISABLED') %> <% $style %> @@ -161,7 +161,7 @@ Example: <INPUT TYPE = "text" NAME = "<%$pre%>zip" ID = "<%$pre%>zip" - VALUE = "<% $object->get($pre.'zip') |h %>" + VALUE = "<% $object->get('zip') |h %>" SIZE = 10 onChange = "<% $onchange %>" <% $disabled %> @@ -181,7 +181,7 @@ Example: <INPUT TYPE = "text" NAME = "<%$pre%>latitude" ID = "<%$pre%>latitude" - VALUE = "<% $object->get($pre.'latitude') |h %>" + VALUE = "<% $object->get('latitude') |h %>" <% $disabled %> <% $style %> > @@ -189,36 +189,44 @@ Example: <INPUT TYPE = "text" NAME = "<%$pre%>longitude" ID = "<%$pre%>longitude" - VALUE = "<% $object->get($pre.'longitude') |h %>" + VALUE = "<% $object->get('longitude') |h %>" <% $disabled %> <% $style %> > </TD> </TR> -<INPUT TYPE="hidden" NAME="<%$pre%>coord_auto" VALUE="<% $object->get($pre.'coord_auto') %>"> +<INPUT TYPE="hidden" NAME="<%$pre%>coord_auto" VALUE="<% $object->coord_auto %>"> -% if ( !$pre ) { - <INPUT TYPE="hidden" NAME="geocode" VALUE="<% $opt{geocode} %>"> +<INPUT TYPE="hidden" NAME="<%$pre%>geocode" VALUE="<% $object->geocode %>"> +<INPUT TYPE="hidden" NAME="<%$pre%>censusyear" VALUE="<% $object->censusyear %>"> +<TR> +% if ( $opt{enable_censustract} ) { + <TD ALIGN="right">Census tract</TD> + <TD COLSPAN=8> + <INPUT TYPE="text" SIZE=15 + NAME="<%$pre%>censustract" + VALUE="<% $object->censustract %>"> + <% '(automatic)' %> + </TD> % } else { -% if ( $pre eq 'ship_' && $conf->exists('cust_main-require_censustract') ) { - <TR><<%$th%> ALIGN="right">Census tract<BR>(automatic)</<%$th%>> - <TD> - <INPUT TYPE="text" NAME="censustract" VALUE="<% $opt{censustract} %>"> - <INPUT TYPE="hidden" NAME="censusyear" VALUE="<% $object->get('censusyear') %>"> - </TD> - </TR> + <INPUT TYPE="hidden" NAME="<%$pre%>censustract" VALUE="<% $object->censustract %>"> +% } +</TR> +% if ( $conf->config('tax_district_method') ) { + <TR> +% if ( $opt{enable_district} ) { + <TD ALIGN="right">Tax district</TD> + <TD COLSPAN=8> + <INPUT TYPE="text" SIZE=15 + NAME="<%$pre%>district" + VALUE="<% $object->district %>"> + <% '(automatic)' %> + </TD> % } else { - <INPUT TYPE="hidden" NAME="censustract" VALUE="<% $opt{censustract} %>"> -% } -% if ( $conf->config('tax_district_method') or $object->get('district') ) { - <TR> - <<%$th%> ALIGN="right">Tax district<BR>(automatic)</<%$th%>> - <TD> - <INPUT TYPE="text" NAME="district" VALUE="<%$object->get('district')%>"> - </TD> - </TR> + <INPUT TYPE="hidden" NAME="<%$pre%>district" VALUE="<% $object->district %>"> % } -% } + </TR> +% } <%init> @@ -233,16 +241,13 @@ my $conf = new FS::Conf; my $r = $opt{'no_asterisks'} ? '' : qq!<font color="#ff0000">*</font> !; -#false laziness with ship state my $countrydefault = $conf->config('countrydefault') || 'US'; -$object->set($pre.'country', $countrydefault ) - unless $object->get($pre.'country'); - -my $statedefault = $conf->config('statedefault') +my $statedefault = $conf->config('statedefault') || ($countrydefault eq 'US' ? 'CA' : ''); -$object->set($pre.'state', $statedefault ) - unless $object->get($pre.'state') - || $object->get($pre.'country') ne $countrydefault; +$object ||= FS::cust_location->new({ + 'country' => $countrydefault, + 'state' => $statedefault, +}); my $alt_err = ($opt{'alt_format'} && !$disabled) ? $object->alternize : ''; @@ -255,8 +260,8 @@ push @address2_label_style, 'visibility:hidden' || ! $conf->exists('cust_main-require_address2') || ( !$pre && !$opt{'same_checked'} ); -my @counties = counties( $object->get($pre.'state'), - $object->get($pre.'country'), +my @counties = counties( $object->get('state'), + $object->get('country'), ); my @county_style = (); push @county_style, 'display:none' # 'visibility:hidden' @@ -276,10 +281,10 @@ my $county_style = : ''; my %select_hash = ( - 'city' => $object->get($pre.'city'), - 'county' => $object->get($pre.'county'), - 'state' => $object->get($pre.'state'), - 'country' => $object->get($pre.'country'), + 'city' => $object->get('city'), + 'county' => $object->get('county'), + 'state' => $object->get('state'), + 'country' => $object->get('country'), 'prefix' => $pre, 'onchange' => $onchange, 'disabled' => $disabled, diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index e5f374863..5a17d6da1 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -110,8 +110,7 @@ $report_customers{'List customers'} = [ \%report_customers_lists, 'List customer $report_customers{'Zip code distribution'} = [ $fsurl. 'search/report_cust_main-zip.html', 'Zip codes by number of customers' ]; $report_customers{'Customer signup report'} = [ $fsurl. 'graph/report_cust_signup.html', 'New customer signups by date' ], $report_customers{'Advanced customer reports'} = [ $fsurl. 'search/report_cust_main.html', 'by status, signup date, agent, etc.' ] - if $curuser->access_right('List customers') - && $curuser->access_right('List packages'); + if $curuser->access_right('Advanced customer search'); tie my %report_invoices_open, 'Tie::IxHash', 'All open invoices' => [ $fsurl.'search/cust_bill.html?OPEN_date', 'All invoices with an unpaid balance' ], @@ -201,10 +200,10 @@ foreach my $svcdb ( FS::part_svc->svc_tables() ) { ]; } - if ( $svcdb =~ /^svc_(acct|broadband|hardware)$/ ) { $report_svc{"Advanced $lcsname reports"} = - [ $fsurl."search/report_$svcdb.html", '' ]; - } + [ $fsurl."search/report_$svcdb.html", '' ] + if $svcdb =~ /^svc_(acct|broadband|hardware)$/ + && $curuser->access_right("Services: $name: Advanced search"); if ( $svcdb eq 'svc_phone' ) { @@ -221,7 +220,8 @@ foreach my $svcdb ( FS::part_svc->svc_tables() ) { } - $report_services{$name} = [ \%report_svc, $longname ]; + $report_services{$name} = [ \%report_svc, $longname ] + if $curuser->access_right("Services: $name"); } @@ -253,14 +253,17 @@ tie my %report_inventory, 'Tie::IxHash', 'Inventory activity' => [ $fsurl.'search/report_h_inventory_item.html', '' ], ; -tie my %report_rating, 'Tie::IxHash', - 'RADIUS sessions' => [ $fsurl.'search/sqlradius.html', '' ], - 'Call Detail Records (CDRs)' => [ $fsurl.'search/report_cdr.html', '' ], - 'Unrateable CDRs' => [ $fsurl.'search/cdr.html?freesidestatus=failed'. - ';cdrbatchnum=_ALL_' ], - 'Time worked' => [ $fsurl.'search/report_rt_transaction.html', '' ], - 'Time worked summary' => [ $fsurl.'search/report_rt_ticket.html', '' ], -; +tie my %report_rating, 'Tie::IxHash'; +$report_rating{'RADIUS sessions'} = [ $fsurl.'search/sqlradius.html', '' ] + if $curuser->access_right("Usage: RADIUS sessions"); +$report_rating{'Call Detail Records (CDRs)'} = [ $fsurl.'search/report_cdr.html', '' ] + if $curuser->access_right("Usage: Call Detail Records (CDRs)"); +$report_rating{'Unrateable CDRs'} = [ $fsurl.'search/cdr.html?freesidestatus=failed;cdrbatchnum=_ALL_' ] + if $curuser->access_right("Usage: Unrateable CDRs"); +if ( $curuser->access_right("Usage: Time worked") ) { + $report_rating{'Time worked'} = [ $fsurl.'search/report_rt_transaction.html', '' ]; + $report_rating{'Time worked summary'} = [ $fsurl.'search/report_rt_ticket.html', '' ]; +} tie my %report_ticketing_statistics, 'Tie::IxHash', 'Tickets per day per Queue' => [ $fsurl.'rt/RTx/Statistics/CallsQueueDay', 'View the number of tickets created, resolved or deleted in a specific Queue, over the requested period of days' ], @@ -462,6 +465,7 @@ tie my %config_radius, 'Tie::IxHash', tie my %config_export_svc, 'Tie::IxHash', (); if ( $curuser->access_right('Configuration') ) { $config_export_svc{'Service definitions'} = [ $fsurl.'browse/part_svc.cgi', 'Services are items you offer to your customers' ]; + $config_export_svc{'Service classes'} = [ $fsurl.'browse/part_svc_class.html', 'Services classes are user-defined, informational types for services' ]; $config_export_svc{'Provisioning exports'} = [ $fsurl.'browse/part_export.cgi', 'Provisioning services to external machines, databases and APIs' ]; } $config_export_svc{'Dialup'} = [ \%config_dialup, '' ] @@ -515,6 +519,10 @@ tie my %config_agent, 'Tie::IxHash', 'Agent payment gateways' => [ $fsurl.'browse/payment_gateway.html', 'Credit card and electronic check processors for agent overrides' ]; ; +tie my %config_sales, 'Tie::IxHash', + 'Sales' => [ $fsurl.'browse/sales.cgi', 'Sales bring in new business.' ], +; + tie my %config_billing_rates, 'Tie::IxHash', 'Rate plans' => [ $fsurl.'browse/rate.cgi', 'Manage rate plans' ], 'Regions and prefixes' => [ $fsurl.'browse/rate_region.html', 'Manage regions and prefixes' ], @@ -597,9 +605,8 @@ tie my %config_nms, 'Tie::IxHash', tie my %config_misc, 'Tie::IxHash'; $config_misc{'Message templates'} = [ $fsurl.'browse/msg_template.html', 'Templates for customer notices' ] - if $curuser->access_right('Edit templates') - || $curuser->access_right('Edit global templates') - || $curuser->access_right('Configuration'); + if $curuser->access_right(['View templates', 'View global templates', + 'Edit templates', 'Edit global templates', ]); $config_misc{'Advertising sources'} = [ $fsurl.'browse/part_referral.html', 'Where a customer heard about your service.' ] if $curuser->access_right('Edit advertising sources') || $curuser->access_right('Edit global advertising sources'); @@ -612,6 +619,9 @@ $config_misc{'Inventory classes and inventory'} = [ $fsurl.'browse/inventory_cla || $curuser->access_right('Edit global inventory') || $curuser->access_right('Configuration'); +$config_misc{'FTP targets'} = [ $fsurl.'browse/ftp_target.html', 'FTP servers for billing and payment processing' ] + if $curuser->access_right('Configuration'); + tie my %config_menu, 'Tie::IxHash'; if ( $curuser->access_right('Configuration' ) ) { %config_menu = ( @@ -619,6 +629,7 @@ if ( $curuser->access_right('Configuration' ) ) { 'separator' => '', #its a separator! 'Employees' => [ \%config_employees, '' ], 'Resellers' => [ \%config_agent, '' ], + 'Sales People' => [ \%config_sales, '' ], 'separator2' => '', #its a separator! 'Customers' => [ \%config_cust, '' ], #or this? 'Customers and Contacts' => [ \%config_cust, '' ], @@ -663,18 +674,18 @@ my $doc_link = $conf->config('support-key') eval "use RT;" if $conf->config('ticket_system') eq 'RT_Internal'; -tie my %help_menu, 'Tie::IxHash', 'Billing documentation' => [ $doc_link, 'Freeside documentation' ]; -$help_menu{'Ticketing documentation'} = [ 'http://wiki.bestpractical.com/', 'Request Tracker Wiki' ] - if $conf->config('ticket_system') eq 'RT_Internal'; -$help_menu{'Networking monitoring documentation'} = [ 'http://torrus.org/userguide.pod.html', 'Torrus User Guide' ] - if $conf->config('network_monitoring_system') eq 'Torrus_Internal'; -$help_menu{'separator'} = ''; - +tie my %help_menu, 'Tie::IxHash'; my $agentnum = $conf->config('brand-agent'); if ( $agentnum ) { my $company_name = $conf->config('company_name', $agentnum); $help_menu{"About $company_name"} = [ "javascript:about_freeside()", '' ]; } else { + $help_menu{'Billing documentation'} = [ $doc_link, 'Freeside documentation' ]; + $help_menu{'Ticketing documentation'} = [ 'http://wiki.bestpractical.com/', 'Request Tracker Wiki' ] + if $conf->config('ticket_system') eq 'RT_Internal'; + $help_menu{'Networking monitoring documentation'} = [ 'http://torrus.org/userguide.pod.html', 'Torrus User Guide' ] + if $conf->config('network_monitoring_system') eq 'Torrus_Internal'; + $help_menu{'separator'} = ''; $help_menu{"About Freeside v$FS::VERSION"} = [ "javascript:about_freeside()", '' ]; $help_menu{"About RT v$RT::VERSION"} = [ 'http://www.bestpractical.com/rt', 'Request Tracker Homepage' ] if $conf->config('ticket_system') eq 'RT_Internal'; diff --git a/httemplate/elements/progress-init.html b/httemplate/elements/progress-init.html index 2ec248e32..7a282a34c 100644 --- a/httemplate/elements/progress-init.html +++ b/httemplate/elements/progress-init.html @@ -54,10 +54,7 @@ sub process_whatever { #class method ) %> -% if (!$noinit) { <& /elements/init_overlib.html &> -% $noinit = 1; -% } <SCRIPT TYPE="text/javascript"> @@ -117,9 +114,6 @@ function <%$key%>myCallback( jobnum ) { </SCRIPT> -<%once> -my $noinit = 0; -</%once> <%init> my( $formname, $fields, $action, $url_or_message, $key ) = @_; diff --git a/httemplate/elements/select-part_svc_class.html b/httemplate/elements/select-part_svc_class.html new file mode 100644 index 000000000..280e3e17d --- /dev/null +++ b/httemplate/elements/select-part_svc_class.html @@ -0,0 +1,22 @@ +<% include( '/elements/select-table.html', + 'table' => 'part_svc_class', + 'name_col' => 'classname', + 'value' => $classnum, + 'empty_label' => '(none)', + 'hashref' => \%hash, + %opt, + ) +%> +<%init> + +my %opt = @_; +my $classnum = $opt{'curr_value'} || $opt{'value'}; + +my %hash = (); +$hash{'disabled'} = '' unless $opt{'showdisabled'}; + + +$opt{'records'} = delete $opt{'part_svc_class'} + if $opt{'part_svc_class'}; + +</%init> diff --git a/httemplate/elements/select-table.html b/httemplate/elements/select-table.html index c0dde7414..127028ee5 100644 --- a/httemplate/elements/select-table.html +++ b/httemplate/elements/select-table.html @@ -93,10 +93,17 @@ Example: % ) % { % my $recvalue = $record->$key(); +% my $selected; +% if ( $opt{'all_selected'} ) { +% $selected = 1; +% } elsif ( $opt{'compare_sub'} && !ref($value) ) { +% $selected = &{ $opt{'compare_sub'} }( $value, $recvalue ); +% } else { +% $selected = ( ref($value) && $value->{$recvalue} ) +% || ( $value && $value eq $recvalue ); #not == because of value_col +% } <OPTION VALUE="<% $recvalue %>" - <% $opt{'all_selected'} || ref($value) && $value->{$recvalue} || $value && $value eq $recvalue # not == because of value_col - ? ' SELECTED' : '' - %> + <% $selected ? ' SELECTED' : '' %> % foreach my $att ( @{ $opt{'extra_option_attributes'} } ) { data-<% $att %>="<% $record->$att() |h %>" % } diff --git a/httemplate/elements/standardize_locations.js b/httemplate/elements/standardize_locations.js index e6a4aa607..86f8d2be8 100644 --- a/httemplate/elements/standardize_locations.js +++ b/httemplate/elements/standardize_locations.js @@ -10,7 +10,7 @@ function standardize_locations() { 'onlyship', 1, % } else { % if ( $withfirm ) { - 'company', cf.elements['<% $main_prefix %>company'].value, + 'company', cf.elements['company'].value, % } 'address1', cf.elements['<% $main_prefix %>address1'].value, 'address2', cf.elements['<% $main_prefix %>address2'].value, @@ -18,9 +18,6 @@ function standardize_locations() { 'state', state_el.options[ state_el.selectedIndex ].value, 'zip', cf.elements['<% $main_prefix %>zip'].value, % } -% if ( $withfirm ) { - 'ship_company', cf.elements['<% $ship_prefix %>company'].value, -% } 'ship_address1', cf.elements['<% $ship_prefix %>address1'].value, 'ship_address2', cf.elements['<% $ship_prefix %>address2'].value, 'ship_city', cf.elements['<% $ship_prefix %>city'].value, diff --git a/httemplate/elements/tr-cust_svc.html b/httemplate/elements/tr-cust_svc.html index ca5de86b4..1ca22f6d4 100644 --- a/httemplate/elements/tr-cust_svc.html +++ b/httemplate/elements/tr-cust_svc.html @@ -24,7 +24,7 @@ Usage: <TD ALIGN="right" VALIGN="top"><% FS::UI::Web::svc_link($m, $part_svc, $cust_svc) %></TD> - <TD STYLE="padding-bottom:0px"><B><% + <TD STYLE="padding-bottom:0px"><B><% $cust_svc->agent_svcid ? $cust_svc->agent_svcid.': ' : '' %><% FS::UI::Web::svc_label_link($m, $part_svc, $cust_svc) %></B></TD> <TD ALIGN="right"><% FS::UI::Web::svc_export_links($m, $part_svc, $cust_svc) %> diff --git a/httemplate/elements/tr-fixed.html b/httemplate/elements/tr-fixed.html index f358343dd..dd07d90b6 100644 --- a/httemplate/elements/tr-fixed.html +++ b/httemplate/elements/tr-fixed.html @@ -13,13 +13,15 @@ my %opt = @_; my $style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : ''; my $value = $opt{'formatted_value'} || $opt{'curr_value'} || $opt{'value'}; -#compatibility with select-table and friends -if ( $opt{'multiple'} ) { - $value = [ split(/\s*,\s*/, $value) ] if !ref $value; - $value = join('<BR>', map {encode_entities($_)} @$value); -} -else { - $value = encode_entities($value) + +unless ( $opt{'noescape'} ) { + #compatibility with select-table and friends + if ( $opt{'multiple'} ) { + $value = [ split(/\s*,\s*/, $value) ] if !ref $value; + $value = join('<BR>', map {encode_entities($_)} @$value); + } else { + $value = encode_entities($value) + } } </%init> diff --git a/httemplate/elements/tr-select-agent.html b/httemplate/elements/tr-select-agent.html index ce03c40f5..321bd6b32 100644 --- a/httemplate/elements/tr-select-agent.html +++ b/httemplate/elements/tr-select-agent.html @@ -22,7 +22,7 @@ Example: ); </%doc> -% if ( scalar(@agents) == 1 ) { +% if ( scalar(@agents) == 1 || $opt{'fixed'} ) { <INPUT TYPE = "hidden" NAME = "<% $opt{'field'} || 'agentnum' %>" @@ -30,9 +30,20 @@ Example: VALUE = "<% $agents[0]->agentnum %>" > -%# YUCK. empty row so we don't throw g_row in edit.html off :/ - <TR> - </TR> +% if ( scalar(@agents) != 1 ) { + <TR> + <TD ALIGN="right"><% $opt{'label'} || emt('Agent') %></TD> + <TD BGCOLOR="#dddddd" <% $colspan %>> +% my $agent = qsearchs('agent', { 'agentnum' => $agentnum }); + <% $agent ? $agent->agent : '(all)' |h %> + </TD> + </TR> + +% } else { # YUCK. empty row so we don't throw g_row in edit.html off :/ + <TR> + </TR> +% } +% % } else { <TR> diff --git a/httemplate/elements/tr-select-cust_location.html b/httemplate/elements/tr-select-cust_location.html index 0ca255b3e..d9e3e9e27 100644 --- a/httemplate/elements/tr-select-cust_location.html +++ b/httemplate/elements/tr-select-cust_location.html @@ -11,7 +11,6 @@ Example: #optional 'empty_label' => '(default service address)', - 'disable_empty' => 0, #1 to disable ) </%doc> @@ -52,11 +51,12 @@ Example: var ftype = what.form.<%$_%>.tagName; if( ftype != 'SELECT') what.form.<%$_%>.style.backgroundColor = '#ffffff'; % } - +% if ( $opt{'alt_format'} ) { if ( what.form.location_type.options[what.form.location_type.selectedIndex].value ) { what.form.location_number.disabled = false; what.form.location_number.style.backgroundColor = '#ffffff'; } +% } } function locationnum_changed(what) { @@ -101,25 +101,8 @@ Example: return; } - if ( locationnum == 0 ) { //(default service address) -% if ( $cust_main ) { - what.form.address1.value = <% $cust_main->get($prefix.'address1') |js_string %>; - what.form.address2.value = <% $cust_main->get($prefix.'address2') |js_string %>; - what.form.city.value = <% $cust_main->get($prefix.'city') |js_string %>; - what.form.zip.value = <% $cust_main->get($prefix.'zip') |js_string %>; - - changeSelect(what.form.country, <% $cust_main->get($prefix.'country') | js_string %> ); - - country_changed( what.form.country, - fix_state_factory( <% $cust_main->get($prefix.'state') | js_string %>, - <% $cust_main->get($prefix.'county') | js_string %> - ) - ); -% } - - } else { - get_location( locationnum, update_location ); - } +%# default service address is now just another location + get_location( locationnum, update_location ); % if ( $editable ) { if ( locationnum == 0 ) { @@ -203,14 +186,16 @@ Example: ID = "locationnum" onChange = "locationnum_changed(this);" > -% if ( !$prospect_main && !$opt{'disable_empty'} ) { - <OPTION VALUE=""><% $opt{'empty_label'} || '(default service address)' |h %> +% if ( $cust_main ) { + <OPTION VALUE="<% $cust_main->ship_locationnum %>"><% $opt{'empty_label'} || '(default service address)' |h %> % } % if ( $opt{'is_optional'} ) { <OPTION VALUE="-2" <% $locationnum == -2 ? 'SELECTED' : ''%>><% $opt{'optional_label'} || '(not required)' |h %> % } % % foreach my $loc ( @cust_location ) { +% # don't show the ship_location redundantly +% next if $cust_main && $cust_main->ship_locationnum == $loc->locationnum; <OPTION VALUE="<% $loc->locationnum %>" <% $locationnum == $loc->locationnum ? 'SELECTED' : '' %> ><% $loc->line |h %> @@ -233,7 +218,9 @@ Example: 'alt_format' => $opt{'alt_format'}, ) %> - +<SCRIPT TYPE="text/javascript"> + locationnum_changed(document.getElementById('locationnum')); +</SCRIPT> <%init> my $conf = new FS::Conf; @@ -246,8 +233,7 @@ my $cgi = $opt{'cgi'}; my $cust_pkg = $opt{'cust_pkg'}; my $cust_main = $opt{'cust_main'}; my $prospect_main = $opt{'prospect_main'}; - -my $prefix = ($cust_main && length($cust_main->ship_last)) ? 'ship_' : ''; +die "cust_main or prospect_main required" unless $cust_main or $prospect_main; my $locationnum = ''; if ( $cgi->param('error') ) { @@ -259,9 +245,9 @@ if ( $cgi->param('error') ) { } elsif ($prospect_main) { my @cust_location = $prospect_main->cust_location; $locationnum = $cust_location[0]->locationnum if scalar(@cust_location)==1; - } else { #? + } else { #$cust_main $cgi->param('locationnum') =~ /^(\-?\d*)$/ or die "illegal locationnum"; - $locationnum = $1; + $locationnum = $1 || $cust_main->ship_locationnum; } } @@ -277,7 +263,7 @@ if ( $opt{'alt_format'} ) { push @location_fields, qw( location_type location_number location_kind ); } -my $cust_location; +my $cust_location; #the one that shows by default in the location edit space if ( $locationnum && $locationnum > 0 ) { $cust_location = qsearchs('cust_location', { 'locationnum' => $locationnum } ) or die "unknown locationnum"; @@ -290,7 +276,7 @@ if ( $locationnum && $locationnum > 0 ) { $cust_location->$_( $pkg_location->$_ ) foreach @location_fields; $opt{'empty_label'} ||= 'package address: '.$pkg_location->line; } elsif ( $cust_main ) { - $cust_location->$_( $cust_main->get($prefix.$_) ) foreach @location_fields; + $cust_location = $cust_main->ship_location; #I think } } @@ -301,7 +287,7 @@ my $location_sort = sub { or lc($a->address2) cmp lc($b->address2) }; -my @cust_location = (); +my @cust_location; push @cust_location, $cust_main->cust_location if $cust_main; push @cust_location, $prospect_main->cust_location if $prospect_main; push @cust_location, $cust_location @@ -311,14 +297,14 @@ push @cust_location, $cust_location @cust_location = sort $location_sort grep !$_->disabled, @cust_location; $cust_location = $cust_location[0] - if ( $prospect_main || $opt{'disable_empty'} ) + if ( $prospect_main ) && !$opt{'is_optional'} && @cust_location; my $disabled = ( $locationnum < 0 || ( $editable && $locationnum ) - || ( ( $prospect_main || $opt{'disable_empty'} ) + || ( $prospect_main && !$opt{'is_optional'} && !@cust_location && $addnew ) ) diff --git a/httemplate/elements/tr-select-part_svc_class.html b/httemplate/elements/tr-select-part_svc_class.html new file mode 100644 index 000000000..2f4b09381 --- /dev/null +++ b/httemplate/elements/tr-select-part_svc_class.html @@ -0,0 +1,27 @@ +% if ( scalar(@{ $opt{'part_svc_class'} }) == 0 ) { + + <INPUT TYPE="hidden" NAME="<% $opt{'element_name'} || $opt{'field'} || 'classnum' %>" VALUE=""> + +% } else { + + <TR> + <TD ALIGN="right"><% $opt{'label'} || 'Service class' %></TD> + <TD> + <% include( '/elements/select-part_svc_class.html', + 'curr_value' => $classnum, + %opt + ) + %> + </TD> + </TR> + +% } + +<%init> + +my %opt = @_; +my $classnum = $opt{'curr_value'} || $opt{'value'}; + +$opt{'part_svc_class'} ||= [ qsearch( 'part_svc_class', { disabled=>'' } ) ]; + +</%init> diff --git a/httemplate/loginout/logout.html b/httemplate/loginout/logout.html index 40371d596..d8e1c634a 100644 --- a/httemplate/loginout/logout.html +++ b/httemplate/loginout/logout.html @@ -8,11 +8,11 @@ <BODY> <BR><BR> <CENTER> - You have logged out + You have logged out. </CENTER> <BR><BR> <CENTER> - Return to <a href="..">freeside</a> + You can <a href="..">log in</a> again. </CENTER> </BODY> </HTML> diff --git a/httemplate/misc/batch-cust_pay.html b/httemplate/misc/batch-cust_pay.html index 2e798652d..887b92489 100644 --- a/httemplate/misc/batch-cust_pay.html +++ b/httemplate/misc/batch-cust_pay.html @@ -1,6 +1,9 @@ -<% include('/elements/header.html', 'Quick payment entry') %> +<& /elements/header.html, { + title => 'Quick payment entry', + etc => 'onload="preload()"' +} &> -<% include('/elements/error.html') %> +<& /elements/error.html &> <SCRIPT TYPE="text/javascript"> function warnUnload() { @@ -14,6 +17,21 @@ function warnUnload() { } window.onbeforeunload = warnUnload; +function add_row_callback(rownum, prefix) { + document.getElementById('enable_app'+rownum).disabled = true; +} + +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 ( $use_discounts ) { + select_discount_term(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); @@ -46,6 +64,265 @@ function select_discount_term(row, prefix) { discount_terms(custnum_obj.value, select_discount_term_update); } + +var invoices_for_row = new Object; + +function update_invoices(rownum, invoices) { + invoices_for_row[rownum] = new Object; + // only called before create_application_row + for ( var i=0; i<invoices.length; i++ ) { + invoices_for_row[rownum][ invoices[i].invnum ] = invoices[i]; + } +} + +function toggle_application_row(ev, next) { + if (!next) next = function(){}; //optional continuation + var rownum = this.getAttribute('rownum'); + if ( this.checked ) { + var custnum = document.getElementById('custnum'+rownum).value; + if (!custnum) return; + lock_payment_row(rownum, true); + custnum_search_open( custnum, + function(returned) { + update_invoices(rownum, JSON.parse(returned)); + create_application_row(rownum, 0); + next.call(this, rownum); + } + ); + } +} + +function lock_payment_row(rownum, flag) { +% foreach (qw(invnum custnum customer)) { + obj = document.getElementById('<% $_ %>'+rownum); + obj.readOnly = flag; +% } + document.getElementById('enable_app'+rownum).disabled = flag; +} + +function delete_application_row() { + var rownum = this.getAttribute('rownum'); + var appnum = this.getAttribute('appnum'); + var tr_app = document.getElementById('row'+rownum+'.'+appnum); + var select_invnum = document.getElementById('invnum'+rownum+'.'+appnum); + if ( select_invnum.value ) { + invoices_for_row[rownum][ select_invnum.value ] = select_invnum.curr_invoice; + } + + tr_app.parentNode.removeChild(tr_app); + if ( appnum > 0 ) { + document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = ''; + } + else { + lock_payment_row(rownum, false); + document.getElementById('enable_app'+rownum).checked = false; + } +} + +function amount_unapplied(rownum) { + var appnum = 0; + var total = 0; + var payment_amount = parseFloat(document.getElementById('paid'+rownum).value) + || 0; + while (true) { + var input_amount = document.getElementById('amount'+rownum+'.'+appnum); + if ( input_amount ) { + total += parseFloat(input_amount.value || 0); + appnum++; + } + else { + return payment_amount - total; + } + } +} + +var change_app_amount; + +function choose_app_invnum() { + var rownum = this.getAttribute('rownum'); + var appnum = this.getAttribute('appnum'); + var last_invoice = this.curr_invoice; + if ( last_invoice ) { + invoices_for_row[rownum][ last_invoice['invnum'] ] = last_invoice; + } + + if ( this.value ) { + var this_invoice = invoices_for_row[rownum][this.value]; + this.curr_invoice = invoices_for_row[rownum][this.value]; + var span_owed = document.getElementById('owed'+rownum+'.'+appnum); + span_owed.innerHTML = this_invoice['owed'] + ' '; + delete invoices_for_row[rownum][this.value]; + + var input_amount = document.getElementById('amount'+rownum+'.'+appnum); + if ( input_amount.value == '' ) { + input_amount.value = + Math.max( + 0, Math.min( amount_unapplied(rownum), this_invoice['owed']) + ).toFixed(2); + // trigger onchange + change_app_amount.call(input_amount); + } + } +} + +function focus_app_invnum() { +% # invoice numbers just display as invoice numbers + var rownum = this.getAttribute('rownum'); + var add_opt = function(obj, value) { + var o = document.createElement('OPTION'); + o.text = value; + o.value = value; + obj.add(o); + } + this.options.length = 0; + var this_invoice = this.curr_invoice; + if ( this_invoice ) { + add_opt(this, this_invoice.invnum); + } else { + add_opt(this, ''); + } + for ( var x in invoices_for_row[rownum] ) { + add_opt(this, invoices_for_row[rownum][x].invnum); + } +} + +function change_app_amount() { + var rownum = this.getAttribute('rownum'); + var appnum = this.getAttribute('appnum'); +%# maybe some kind of warning if amount_unapplied < 0? +%# only spawn a new application row if there are open invoices left, +%# and this is the highest-numbered application row for the customer, +%# and the sum of the applied amounts is < the amount of the payment, + if ( Object.keys(invoices_for_row[rownum]).length > 0 + && !document.getElementById( 'row'+rownum+'.'+(parseInt(appnum) + 1) ) + && amount_unapplied(rownum) > 0 ) { + + create_application_row(rownum, parseInt(appnum) + 1); + + } +} + +function create_application_row(rownum, appnum) { + var payment_row = document.getElementById('row'+rownum); + var tr_app = document.createElement('TR'); + tr_app.setAttribute('rownum', rownum); + tr_app.setAttribute('appnum', appnum); + tr_app.setAttribute('id', 'row'+rownum+'.'+appnum); + + var td_invnum = document.createElement('TD'); + td_invnum.setAttribute('colspan', 4); + td_invnum.style.textAlign = 'right'; + td_invnum.appendChild( + document.createTextNode('<% mt('Apply to Invoice ') %>') + ); + var select_invnum = document.createElement('SELECT'); + select_invnum.setAttribute('rownum', rownum); + select_invnum.setAttribute('appnum', appnum); + select_invnum.setAttribute('id', 'invnum'+rownum+'.'+appnum); + select_invnum.setAttribute('name', 'invnum'+rownum+'.'+appnum); + select_invnum.style.textAlign = 'right'; + select_invnum.style.width = '50px'; + select_invnum.onchange = choose_app_invnum; + select_invnum.onfocus = focus_app_invnum; + + td_invnum.appendChild(select_invnum); + tr_app.appendChild(td_invnum); + + var td_owed = document.createElement('TD'); + td_owed.style.textAlign= 'right'; + var span_owed = document.createElement('SPAN'); + span_owed.setAttribute('rownum', rownum); + span_owed.setAttribute('appnum', appnum); + span_owed.setAttribute('id', 'owed'+rownum+'.'+appnum); + td_owed.appendChild(span_owed); + tr_app.appendChild(td_owed); + + var td_amount = document.createElement('TD'); + td_amount.style.textAlign = 'right'; + var input_amount = document.createElement('INPUT'); + input_amount.size = 6; + input_amount.setAttribute('rownum', rownum); + input_amount.setAttribute('appnum', appnum); + input_amount.setAttribute('name', 'amount'+rownum+'.'+appnum); + input_amount.setAttribute('id', 'amount'+rownum+'.'+appnum); + input_amount.style.textAlign = 'right'; + input_amount.onchange = change_app_amount; + td_amount.appendChild(input_amount); + tr_app.appendChild(td_amount); + + var td_delete = document.createElement('TD'); + td_delete.setAttribute('colspan', <% scalar(@fields)-2 %>); + var button_delete = document.createElement('INPUT'); + button_delete.setAttribute('rownum', rownum); + button_delete.setAttribute('appnum', appnum); + button_delete.setAttribute('id', 'delete'+rownum+'.'+appnum); + button_delete.setAttribute('type', 'button'); + button_delete.setAttribute('value', 'X'); + button_delete.onclick = delete_application_row; + button_delete.style.color = '#ff0000'; + button_delete.style.fontWeight = 'bold'; + button_delete.style.paddingLeft = '2px'; + button_delete.style.paddingRight = '2px'; + td_delete.appendChild(button_delete); + tr_app.appendChild(td_delete); + + var td_error = document.createElement('TD'); + var span_error = document.createElement('SPAN'); + span_error.setAttribute('rownum', rownum); + span_error.setAttribute('appnum', appnum); + span_error.setAttribute('id', 'error'+rownum+'.'+appnum); + span_error.style.color = '#ff0000'; + td_error.appendChild(span_error); + tr_app.appendChild(td_error); + + if ( appnum > 0 ) { + //remove delete button on the previous row + document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = 'none'; + } + rownum++; + var next_row = document.getElementById('row'+rownum); // always exists + payment_row.parentNode.insertBefore(tr_app, next_row); + +} + +%# for error handling--ugly, but the alternative is translating the whole +%# process of creating rows into Mason +var row_array = <% encode_json(\@rows) %>; +function preload() { + var rownum; + var appnum; + for (rownum=0; rownum < row_array.length; rownum++) { + if ( row_array[rownum].length ) { + var enable = document.getElementById('enable_app'+rownum); + enable.checked = true; + var preload_row = function(r) {//continuation from toggle_application_row + for (appnum=0; appnum < row_array[r].length; appnum++) { + this_app = row_array[r][appnum]; + var x = r + '.' + appnum; + //set invnum + var select_invnum = document.getElementById('invnum'+x); + focus_app_invnum.call(select_invnum); + for (i=0; i<select_invnum.options.length; i++) { + if (select_invnum.options[i].value == this_app.invnum) { + select_invnum.selectedIndex = i; + } + } + choose_app_invnum.call(select_invnum); + //set amount + var input_amount = document.getElementById('amount'+x); + input_amount.value = this_app.amount; + + //set error + var span_error = document.getElementById('error'+x); + span_error.innerHTML = this_app.error; + change_app_amount.call(input_amount); //creates next row + } //for appnum + }; //preload_row function + toggle_application_row.call(enable, null, preload_row); + } // if row_array[rownum].length + } //for rownum +} + </SCRIPT> <% include('/elements/xmlhttp.html', @@ -57,21 +334,26 @@ function select_discount_term(row, prefix) { <FORM ACTION="process/batch-cust_pay.cgi" NAME="OneTrueForm" METHOD="POST" onsubmit="document.OneTrueForm.btnsubmit.disabled=true;window.onbeforeunload = null;"> <!-- <B>Batch</B> <INPUT TYPE="text" NAME="paybatch"><BR><BR> --> +<& /elements/xmlhttp.html, + url => $p.'misc/xmlhttp-cust_bill-search.html', + subs => ['custnum_search_open'] +&> -<% include( "/elements/customer-table.html", - name_singular => 'payment', - header => \@header, - fields => \@fields, - type => \@types, - align => \@align, - size => \@sizes, - color => \@colors, - param => \%param, - footer => \@footer, - footer_align => \@footer_align, - custnum_update_callback => $custnum_update_callback, - ) -%> +<& /elements/customer-table.html, + name_singular => 'payment', + header => \@header, + fields => \@fields, + type => \@types, + align => \@align, + size => \@sizes, + color => \@colors, + param => \%param, + footer => \@footer, + footer_align => \@footer_align, + onchange => \@onchange, + custnum_update_callback => 'custnum_update_callback', + add_row_callback => 'add_row_callback', +&> <BR> <INPUT TYPE="button" VALUE="Post payment batch" name="btnsubmit" onclick="window.onbeforeunload = null; document.OneTrueForm.submit(); this.disabled = true;"> @@ -105,7 +387,8 @@ my @colors = ( '', '' ); my %param = (); my @footer = ( '_TOTAL', '' ); my @footer_align = ( 'r', 'r' ); -my $custnum_update_callback = ''; +my @onchange = ( '', '' );; +my $use_discounts = ''; if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) { #push @header, 'Discount'; @@ -117,9 +400,20 @@ if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) { push @colors, ''; push @footer, ''; push @footer_align, ''; - $custnum_update_callback = 'select_discount_term'; + push @onchange, ''; + $use_discounts = 'Y'; } +push @header, 'Allocate'; +push @fields, 'enable_app'; +push @types, 'checkbox'; +push @align, 'c'; +push @sizes, '0'; +push @colors, ''; +push @footer, ''; +push @footer_align, ''; +push @onchange, 'toggle_application_row'; + #push @header, 'Error'; push @header, ''; push @fields, 'error'; @@ -129,7 +423,34 @@ push @sizes, '0'; push @colors, '#ff0000'; push @footer, ''; push @footer_align, ''; +push @onchange, ''; $m->comp('/elements/handle_uri_query'); +# set up for preloading +my @rows; +my @row_errors; +if ( $cgi->param('error') ) { + my $param = $cgi->Vars; + my $enum = 0; #errors numbered separately + for( my $row = 0; exists($param->{"custnum$row"}); $row++ ) { + $rows[$row] = []; + $row_errors[$row] = $param->{"error$enum"}; + $enum++; + for( my $app = 0; exists($param->{"invnum$row.$app"}); $app++ ) { + next if !$param->{"invnum$row.$app"}; + my %this_app = map { $_ => ($param->{$_.$row.'.'.$app} || '') } + qw( invnum amount ); + $this_app{'error'} = $param->{"error$enum"} || ''; + $param->{"error$enum"} = ''; # don't pass this error through + $rows[$row][$app] = \%this_app; + $enum++; + } + } + for( my $row = 0; $row < @row_errors; $row++ ) { + $param->{"error$row"} = $row_errors[$row]; + } +} +#warn Dumper {rows => \@rows, row_errors => \@row_errors }; + </%init> diff --git a/httemplate/misc/cancel_pkg.html b/httemplate/misc/cancel_pkg.html index 4b5df8654..348f0a6cb 100755 --- a/httemplate/misc/cancel_pkg.html +++ b/httemplate/misc/cancel_pkg.html @@ -13,22 +13,53 @@ % my $date_init = 0; % if ($method eq 'expire' || $method eq 'adjourn' || $method eq 'resume') { % $submit =~ /^(\w*)\s/; -<& /elements/tr-input-date-field.html, { - 'name' => 'date', - 'value' => $date, - 'label' => mt("$1 package on"), - 'format' => $date_format, -} &> + <& /elements/tr-input-date-field.html, { + 'name' => 'date', + 'value' => $date, + 'label' => mt("$1 package on"), + 'format' => $date_format, + } &> % $date_init = 1; % } -% unless ( $method eq 'resume' ) { #the only one that doesn't need a reason -<& /elements/tr-select-reason.html, - 'field' => 'reasonnum', - 'reason_class' => $class, - 'curr_value' => $reasonnum, - 'control_button' => "document.getElementById('confirm_cancel_pkg_button')", -&> +% if ($method eq 'uncancel' ) { +% +% #XXX customer also requested setup +% # setup: what usefulness is changing or blanking this? re-charge setup fee? +% # an option that says that would be better if that's what we want to do + +% # last_bill: isn't this informational? what good would editing it do? +% # something about invoice display? + <& /elements/tr-input-date-field.html, { + 'name' => 'last_bill', + 'value' => ( $cgi->param('last_bill') || $cust_pkg->get('last_bill') ), + 'label' => mt("Last bill date"), + 'format' => $date_format, + } &> + + <& /elements/tr-input-date-field.html, { + 'name' => 'bill', + 'value' => ( $cgi->param('bill') || $cust_pkg->get('bill') ), + 'label' => mt("Next bill date"), + 'format' => $date_format, + } &> + + <& /elements/tr-checkbox.html, + 'label' => mt("Uncancel even if a service can't be re-provisioned"), + 'field' => 'svc_not_fatal', + 'value' => 'Y', + &> + +% $date_init = 1; +% } + +% unless ( $method eq 'resume' || $method eq 'uncancel' ) { + <& /elements/tr-select-reason.html, + field => 'reasonnum', + reason_class => $class, + curr_value => $reasonnum, + control_button => "document.getElementById('confirm_cancel_pkg_button')", + &> % } % if ( ( $method eq 'adjourn' or $method eq 'suspend' ) and @@ -37,26 +68,27 @@ % ? str2time($cgi->param('resume_date')) % : $cust_pkg->get('resume'); -<& /elements/tr-input-date-field.html, { - 'name' => 'resume_date', - 'value' => $resume_date, - 'label' => mt('Unsuspend on'), - 'format' => $date_format, - 'noinit' => $date_init, -} &> + <& /elements/tr-input-date-field.html, { + 'name' => 'resume_date', + 'value' => $resume_date, + 'label' => mt('Unsuspend on'), + 'format' => $date_format, + 'noinit' => $date_init, + } &> % } </TABLE> <BR> <INPUT TYPE="submit" NAME="submit" ID="confirm_cancel_pkg_button" VALUE="<% mt($submit) |h %>" - <% $method ne 'resume' ? 'DISABLED' : '' %>> + <% $method !~ /^(resume|uncancel)$/ ? 'DISABLED' : '' %>> </FORM> </BODY> </HTML> <%init> +use Date::Parse qw(str2time); my $conf = new FS::Conf; my $date_format = $conf->config('date_format') || '%m/%d/%Y'; @@ -99,6 +131,10 @@ if ($method eq 'cancel') { $class = ''; $submit = 'Unsuspend Later'; $right = 'Unsuspend customer package'; #later? +} elsif ( $method eq 'uncancel') { + $class = ''; + $submit = 'Un-Cancel'; + $right = 'Un-cancel customer package'; #later? } else { die 'illegal query (unknown method param)'; } @@ -107,6 +143,7 @@ my $curuser = $FS::CurrentUser::CurrentUser; die "access denied" unless $curuser->access_right($right); my $title = ucfirst($method) . ' Package'; +$title =~ s/Uncancel/Un-cancel/; my $cust_pkg = qsearchs('cust_pkg', {'pkgnum' => $pkgnum}) or die "Unknown pkgnum: $pkgnum"; diff --git a/httemplate/misc/cust_main-cancel.cgi b/httemplate/misc/cust_main-cancel.cgi index 002c0011a..2ae9f1021 100755 --- a/httemplate/misc/cust_main-cancel.cgi +++ b/httemplate/misc/cust_main-cancel.cgi @@ -1,4 +1,4 @@ -<& /elements/header.html, mt("Customer cancelled") &> +<& /elements/header-popup.html, mt("Customer cancelled") &> <SCRIPT TYPE="text/javascript"> window.top.location.reload(); </SCRIPT> diff --git a/httemplate/misc/cust_main-import.cgi b/httemplate/misc/cust_main-import.cgi index 664651069..74f9b4c89 100644 --- a/httemplate/misc/cust_main-import.cgi +++ b/httemplate/misc/cust_main-import.cgi @@ -35,6 +35,7 @@ Import a file containing customer records. <OPTION VALUE="extended-plus_company_and_options">Extended plus company and options <OPTION VALUE="svc_external">External service <OPTION VALUE="svc_external_svc_phone">External service and phone service + <OPTION VALUE="birthdates-acct_phone_hardware">Birthdates and account, phone and hardware services </SELECT> </TD> </TR> @@ -106,6 +107,9 @@ Uploaded files can be CSV (comma-separated value) files or Excel spreadsheets. <b>External service and phone service</b> format has the following field order: <i>agent_custid, refnum<%$req%>, last<%$req%>, first<%$req%>, company, address1<%$req%>, address2, city<%$req%>, state<%$req%>, zip<%$req%>, country, daytime, night, ship_last, ship_first, ship_company, ship_address1, ship_address2, ship_city, ship_state, ship_zip, ship_country, payinfo, paycvv, paydate, invoicing_list, pkgpart, next_bill_date, id, title, countrycode, phonenum, sip_password, pin</i> <BR><BR> +<b>Birthdates and account, phone and hardware services</b> format has the following field order: <i>agent_custid, refnum<%$req%>, last<%$req%>, first<%$req%>, company, address1<%$req%>, address2, city<%$req%>, state<%$req%>, zip<%$req%>, country, daytime, night, ship_last, ship_first, ship_company, ship_address1, ship_address2, ship_city, ship_state, ship_zip, ship_country, birthdate, spouse_birthdate, payinfo, paycvv, paydate, invoicing_list, pkgpart, next_bill_date, username, _password, countrycode, phonenum, sip_password, pin, typenum, ip_addr, hw_addr, serial</i> +<BR><BR> + <%$req%> Required fields <BR><BR> diff --git a/httemplate/misc/cust_main-suspend.cgi b/httemplate/misc/cust_main-suspend.cgi new file mode 100755 index 000000000..61851364e --- /dev/null +++ b/httemplate/misc/cust_main-suspend.cgi @@ -0,0 +1,75 @@ +<& /elements/header-popup.html, mt("Customer suspended") &> + <SCRIPT TYPE="text/javascript"> + window.top.location.reload(); + </SCRIPT> + </BODY> +</HTML> +<%init> + +#false laziness w/cust_main-cancel.cgi + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Suspend customer'); + +my $custnum; +my $adjourn = ''; +if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { + $custnum = $1; + $adjourn = $cgi->param('adjourn'); +} else { + my($query) = $cgi->keywords; + $query =~ /^(\d+)$/ || die "Illegal custnum"; + $custnum = $1; +} + +#false laziness w/process/cancel_pkg.html + +#untaint reasonnum +my $reasonnum = $cgi->param('reasonnum'); +$reasonnum =~ /^(-?\d+)$/ || die "Illegal reasonnum"; +$reasonnum = $1; + +if ($reasonnum == -1) { + $reasonnum = { + 'typenum' => scalar( $cgi->param('newreasonnumT') ), + 'reason' => scalar( $cgi->param('newreasonnum' ) ), + }; +} + +#eslaf + +my $cust_main = qsearchs( { + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $custnum }, + 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, +} ); + +my @errors; +if($cgi->param('now_or_later')) { + $adjourn = parse_datetime($adjourn); + if($adjourn) { + #warn "setting adjourn dates on custnum#$custnum\n"; + my @pkgs = $cust_main->unsuspended_pkgs; + @errors = grep {$_} map { $_->suspend( + 'reason' => $reasonnum, + 'date' => $adjourn, + ) } @pkgs; + } + else { + @errors = ("error parsing adjourn date: ".$cgi->param('adjourn')); + } +} +else { + warn "suspending $cust_main"; + @errors = $cust_main->suspend( + 'reason' => $reasonnum, + ); +} +my $error = join(' / ', @errors) if scalar(@errors); + +if ( $error ) { + $cgi->param('error', $error); + print $cgi->redirect(popurl(1). "suspend_cust.html?". $cgi->query_string ); +} + +</%init> diff --git a/httemplate/misc/cust_main-unsuspend.cgi b/httemplate/misc/cust_main-unsuspend.cgi new file mode 100755 index 000000000..eb4a2c8f8 --- /dev/null +++ b/httemplate/misc/cust_main-unsuspend.cgi @@ -0,0 +1,56 @@ +<& /elements/header-popup.html, mt("Customer unsuspended") &> + <SCRIPT TYPE="text/javascript"> + window.top.location.reload(); + </SCRIPT> + </BODY> +</HTML> +<%init> + +#false laziness w/cust_main-cancel.cgi + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Unsuspend customer'); + +my $custnum; +my $resume = ''; +if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { + $custnum = $1; + $resume = $cgi->param('resume'); +} else { + my($query) = $cgi->keywords; + $query =~ /^(\d+)$/ || die "Illegal custnum"; + $custnum = $1; +} + +my $cust_main = qsearchs( { + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $custnum }, + 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, +} ); + +my @errors; +if($cgi->param('now_or_later')) { + $resume = parse_datetime($resume); + if($resume) { + #warn "setting resume dates on custnum#$custnum\n"; + my @pkgs = $cust_main->suspended_pkgs; + @errors = grep {$_} map { $_->unsuspend( + 'date' => $resume, + ) } @pkgs; + } + else { + @errors = ("error parsing adjourn date: ".$cgi->param('adjourn')); + } +} +else { + warn "unsuspending $cust_main"; + @errors = $cust_main->unsuspend; +} +my $error = join(' / ', @errors) if scalar(@errors); + +if ( $error ) { + $cgi->param('error', $error); + print $cgi->redirect(popurl(1). "unsuspend_cust.html?". $cgi->query_string ); +} + +</%init> diff --git a/httemplate/misc/delete-ftp_target.html b/httemplate/misc/delete-ftp_target.html new file mode 100644 index 000000000..c8bd29701 --- /dev/null +++ b/httemplate/misc/delete-ftp_target.html @@ -0,0 +1,18 @@ +% if ( $error ) { +% errorpage($error); +% } else { +<% $cgi->redirect("${p}browse/ftp_target.html") %> +% } +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my($query) = $cgi->keywords; +$query =~ /^(\d+)$/ || die "Illegal targetnum"; +my $targetnum = $1; + +my $target = qsearchs('ftp_target',{'targetnum'=>$targetnum}); +my $error = $target->delete; + +</%init> diff --git a/httemplate/misc/order_pkg.html b/httemplate/misc/order_pkg.html index 2332f2028..7aa024a34 100644 --- a/httemplate/misc/order_pkg.html +++ b/httemplate/misc/order_pkg.html @@ -32,6 +32,15 @@ &> % } +% if ( $conf->exists('invoice-unitprice') ) { + <TR> + <TH ALIGN="right"><% mt('Quantity') |h %> </TD> + <TD> + <INPUT TYPE="text" NAME="quantity" SIZE=4 VALUE="<% $quantity %>"> + </TD> + </TR> +% } + <TR> <TH ALIGN="right"><% mt('Start date') |h %> </TD> <TD COLSPAN=6> @@ -163,6 +172,11 @@ if ( $cgi->param('lock_pkgpart') ) { my $pkgpart = $part_pkg ? $part_pkg->pkgpart : scalar($cgi->param('pkgpart')); +my $quantity = 1; +if ( $cgi->param('quantity') =~ /^\s*(\d+)\s*$/ ) { + $quantity = $1; +} + my $format = $date_format. ' %T %z (%Z)'; #false laziness w/REAL_cust_pkg.cgi? my $start_date = ''; if( ! $conf->exists('order_pkg-no_start_date') ) { diff --git a/httemplate/misc/process/batch-cust_pay.cgi b/httemplate/misc/process/batch-cust_pay.cgi index a6b90ea74..1105af943 100644 --- a/httemplate/misc/process/batch-cust_pay.cgi +++ b/httemplate/misc/process/batch-cust_pay.cgi @@ -1,51 +1,69 @@ -% die "access denied" -% unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch'); -% -% my $param = $cgi->Vars; -% -% #my $paybatch = $param->{'paybatch'}; -% my $paybatch = time2str('webbatch-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time); -% -% my @cust_pay = (); -% #my $row = 0; -% #while ( exists($param->{"custnum$row"}) ) { -% for ( my $row = 0; exists($param->{"custnum$row"}); $row++ ) { -% my $custnum = $param->{"custnum$row"}; -% my $cust_main; -% if ( $custnum =~ /^(\d+)$/ and $1 <= 2147483647 ) { -% $cust_main = qsearchs({ -% 'table' => 'cust_main', -% 'hashref' => { 'custnum' => $1 }, -% 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, -% }); -% } -% if ( length($custnum) and !$cust_main ) { # not found, try agent_custid -% $cust_main = qsearchs({ -% 'table' => 'cust_main', -% 'hashref' => { 'agent_custid' => $custnum }, -% 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, -% }); -% } -% $custnum = $cust_main->custnum if $cust_main; -% # if !$cust_main, then this will throw an error on batch_insert -% -% push @cust_pay, new FS::cust_pay { -% 'custnum' => $custnum, -% 'paid' => $param->{"paid$row"}, -% 'payby' => 'BILL', -% 'payinfo' => $param->{"payinfo$row"}, -% 'discount_term' => $param->{"discount_term$row"}, -% 'paybatch' => $paybatch, -% } -% if $param->{"custnum$row"} -% || $param->{"paid$row"} -% || $param->{"payinfo$row"}; -% #$row++; -% } -% -% my @errors = FS::cust_pay->batch_insert(@cust_pay); -% my $num_errors = scalar(grep $_, @errors); -% +<%init> +my $DEBUG = 0; +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch'); + +my $param = $cgi->Vars; +warn Dumper($param) if $DEBUG; + +#my $paybatch = $param->{'paybatch'}; +my $paybatch = time2str('webbatch-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time); + +my @cust_pay = (); +#my $row = 0; +#while ( exists($param->{"custnum$row"}) ) { +for ( my $row = 0; exists($param->{"custnum$row"}); $row++ ) { + my $custnum = $param->{"custnum$row"}; + my $cust_main; + if ( $custnum =~ /^(\d+)$/ and $1 <= 2147483647 ) { + $cust_main = qsearchs({ + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $1 }, + 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, + }); + } + if ( length($custnum) and !$cust_main ) { # not found, try agent_custid + $cust_main = qsearchs({ + 'table' => 'cust_main', + 'hashref' => { 'agent_custid' => $custnum }, + 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, + }); + } + $custnum = $cust_main->custnum if $cust_main; + # if !$cust_main, then this will throw an error on batch_insert + + my $cust_pay = new FS::cust_pay { + 'custnum' => $custnum, + 'paid' => $param->{"paid$row"}, + 'payby' => 'BILL', + 'payinfo' => $param->{"payinfo$row"}, + 'discount_term' => $param->{"discount_term$row"}, + 'paybatch' => $paybatch, + } + if $param->{"custnum$row"} + || $param->{"paid$row"} + || $param->{"payinfo$row"}; + next if !$cust_pay; + #$row++; + + # payment applications, if any + my @cust_bill_pay = (); + for ( my $app = 0; exists($param->{"invnum$row.$app"}); $app++ ) { + next if !$param->{"invnum$row.$app"}; + push @cust_bill_pay, new FS::cust_bill_pay { + 'invnum' => $param->{"invnum$row.$app"}, + 'amount' => $param->{"amount$row.$app"} + }; + } + $cust_pay->set('apply_to', \@cust_bill_pay) if scalar(@cust_bill_pay) > 0; + + push @cust_pay, $cust_pay; + +} + +my @errors = FS::cust_pay->batch_insert(@cust_pay); +my $num_errors = scalar(grep $_, @errors); +</%init> % if ( $num_errors ) { % % $cgi->param('error', "$num_errors error". ($num_errors>1 ? 's' : ''). @@ -65,4 +83,3 @@ % <% $cgi->redirect(popurl(3). "search/cust_pay.html?magic=paybatch;paybatch=$paybatch") %> % } - diff --git a/httemplate/misc/process/cancel_pkg.html b/httemplate/misc/process/cancel_pkg.html index 662a77670..b2d7bfaa4 100755 --- a/httemplate/misc/process/cancel_pkg.html +++ b/httemplate/misc/process/cancel_pkg.html @@ -6,19 +6,21 @@ </HTML> <%once> -my %past = ( 'cancel' => 'cancelled', - 'expire' => 'expired', - 'suspend' => 'suspended', - 'adjourn' => 'adjourned', - 'resume' => 'scheduled to resume', +my %past = ( 'cancel' => 'cancelled', + 'expire' => 'expired', + 'suspend' => 'suspended', + 'adjourn' => 'adjourned', + 'resume' => 'scheduled to resume', + 'uncancel' => 'un-cancelled', ); #i'm sure this is false laziness with somewhere, at least w/misc/cancel_pkg.html -my %right = ( 'cancel' => 'Cancel customer package immediately', - 'expire' => 'Cancel customer package later', - 'suspend' => 'Suspend customer package', - 'adjourn' => 'Suspend customer package later', - 'resume' => 'Unsuspend customer package', #later? +my %right = ( 'cancel' => 'Cancel customer package immediately', + 'expire' => 'Cancel customer package later', + 'suspend' => 'Suspend customer package', + 'adjourn' => 'Suspend customer package later', + 'resume' => 'Unsuspend customer package', #later? + 'uncancel' => 'Un-cancel customer package', ); </%once> @@ -26,7 +28,8 @@ my %right = ( 'cancel' => 'Cancel customer package immediately', #untaint method my $method = $cgi->param('method'); -$method =~ /^(cancel|expire|suspend|adjourn|resume)$/ or die "Illegal method"; +$method =~ /^(cancel|expire|suspend|adjourn|resume|uncancel)$/ + or die "Illegal method"; $method = $1; my $past_method = $past{$method}; @@ -39,7 +42,7 @@ $pkgnum =~ /^(\d+)$/ or die "Illegal pkgnum"; $pkgnum = $1; my $date = time; -if ($method eq 'expire' || $method eq 'adjourn' || $method eq 'resume'){ +if ($method eq 'expire' || $method eq 'adjourn' || $method eq 'resume') { #untaint date $date = $cgi->param('date'); #huh? parse_datetime($cgi->param('date')) =~ /^(\d+)$/ or die "Illegal date"; @@ -59,7 +62,7 @@ my $cust_pkg = qsearchs( 'cust_pkg', {'pkgnum'=>$pkgnum} ); #untaint reasonnum my $reasonnum = $cgi->param('reasonnum'); -if ( $method ne 'unsuspend' ) { #i.e. 'resume' +if ( $method !~ /^(unsuspend|uncancel)$/ ) { $reasonnum =~ /^(-?\d+)$/ or die "Illegal reasonnum"; $reasonnum = $1; @@ -71,9 +74,21 @@ if ( $method ne 'unsuspend' ) { #i.e. 'resume' } } +#for uncancel +my $last_bill = + $cgi->param('last_bill') ? parse_datetime($cgi->param('last_bill')) : ''; +my $bill = + $cgi->param('bill') ? parse_datetime($cgi->param('bill')) : ''; + +my $svc_fatal = ( $cgi->param('svc_not_fatal') ne 'Y' ); + my $error = $cust_pkg->$method( 'reason' => $reasonnum, 'date' => $date, - 'resume_date' => $resume_date ); + 'resume_date' => $resume_date, + 'last_bill' => $last_bill, + 'bill' => $bill, + 'svc_fatal' => $svc_fatal, + ); if ($error) { $cgi->param('error', $error); diff --git a/httemplate/misc/suspend_cust.html b/httemplate/misc/suspend_cust.html new file mode 100644 index 000000000..b41f36f8c --- /dev/null +++ b/httemplate/misc/suspend_cust.html @@ -0,0 +1,79 @@ +<& /elements/header-popup.html, mt('Suspend customer') &> + +<& /elements/error.html &> + +<FORM NAME="cust_suspend_popup" ACTION="<% popurl(1) %>cust_main-suspend.cgi" METHOD=POST> +<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>"> + + <P ALIGN="center"><B><% mt('Suspend this customer?') |h %></B> + +<TABLE BORDER="0" CELLSPACING="2" +STYLE="margin-left:auto; margin-right:auto"> +<TR> + <TD ALIGN="right"> + <INPUT TYPE="radio" NAME="now_or_later" VALUE="0" onclick="toggle(false)" CHECKED /> + </TD> + <TD ALIGN="left"><% mt('Suspend now') |h %></TD> +</TR> +<TR> + <TD ALIGN="right"> + <INPUT TYPE="radio" NAME="now_or_later" VALUE="1" onclick="toggle(true)" /> + </TD> + <TD ALIGN="left"><% mt('Suspend on date: ') |h %> + <& /elements/input-date-field.html, { + 'name' => 'adjourn', + 'value' => time, + } &> + </TD> +</TR> +</TABLE> +<SCRIPT type="text/javascript"> +function toggle(val) { + document.getElementById("adjourn_text").disabled = !val; + document.getElementById("adjourn_button").style.visibility = + val ? 'visible' : 'hidden'; +} +toggle(false); +</SCRIPT> + +<TABLE BGCOLOR="#cccccc", BORDER="0" CELLSPACING="2" +STYLE="margin-left:auto; margin-right:auto"> +<& /elements/tr-select-reason.html, + 'field' => 'reasonnum', + 'reason_class' => 'C', + 'cgi' => $cgi, + 'control_button' => "document.getElementById('confirm_suspend_cust_button')", +&> + +</TABLE> + +<BR> +<P ALIGN="CENTER"> +<INPUT TYPE="submit" NAME="submit" ID="confirm_suspend_cust_button" VALUE="<% mt('Suspend customer') |h %>" DISABLED> + +<INPUT TYPE="BUTTON" VALUE="<% mt("Don't suspend") |h %>" onClick="parent.cClick();"> + +</FORM> +</BODY> +</HTML> + +<%init> + +#false laziness w/cancel_cust.html + +$cgi->param('custnum') =~ /^(\d+)$/ or die 'illegal custnum'; +my $custnum = $1; + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" unless $curuser->access_right('Suspend customer'); + +my $cust_main = qsearchs( { + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $custnum }, + 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, +} ); +die "No customer # $custnum" unless $cust_main; + +</%init> + diff --git a/httemplate/misc/unsuspend_cust.html b/httemplate/misc/unsuspend_cust.html new file mode 100644 index 000000000..600eb268a --- /dev/null +++ b/httemplate/misc/unsuspend_cust.html @@ -0,0 +1,68 @@ +<& /elements/header-popup.html, mt('Unsuspend customer') &> + +<& /elements/error.html &> + +<FORM NAME="cust_unsuspend_popup" ACTION="<% popurl(1) %>cust_main-unsuspend.cgi" METHOD=POST> +<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>"> + + <P ALIGN="center"><B><% mt('Unsuspend this customer?') |h %></B> + +<TABLE BORDER="0" CELLSPACING="2" +STYLE="margin-left:auto; margin-right:auto"> +<TR> + <TD ALIGN="right"> + <INPUT TYPE="radio" NAME="now_or_later" VALUE="0" onclick="toggle(false)" CHECKED /> + </TD> + <TD ALIGN="left"><% mt('Unsuspend now') |h %></TD> +</TR> +<TR> + <TD ALIGN="right"> + <INPUT TYPE="radio" NAME="now_or_later" VALUE="1" onclick="toggle(true)" /> + </TD> + <TD ALIGN="left"><% mt('Unsuspend on date: ') |h %> + <& /elements/input-date-field.html, { + 'name' => 'resume', + 'value' => time, + } &> + </TD> +</TR> +</TABLE> +<SCRIPT type="text/javascript"> +function toggle(val) { + document.getElementById("resume_text").disabled = !val; + document.getElementById("resume_button").style.visibility = + val ? 'visible' : 'hidden'; +} +toggle(false); +</SCRIPT> + +<BR> +<P ALIGN="CENTER"> +<INPUT TYPE="submit" NAME="submit" ID="confirm_unsuspend_cust_button" VALUE="<% mt('Unsuspend customer') |h %>"> + +<INPUT TYPE="BUTTON" VALUE="<% mt("Don't unsuspend") |h %>" onClick="parent.cClick();"> + +</FORM> +</BODY> +</HTML> + +<%init> + +#false laziness w/cancel_cust.html + +$cgi->param('custnum') =~ /^(\d+)$/ or die 'illegal custnum'; +my $custnum = $1; + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" unless $curuser->access_right('Unsuspend customer'); + +my $cust_main = qsearchs( { + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $custnum }, + 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, +} ); +die "No customer # $custnum" unless $cust_main; + +</%init> + diff --git a/httemplate/misc/xmlhttp-cust_bill-search.html b/httemplate/misc/xmlhttp-cust_bill-search.html new file mode 100644 index 000000000..46f15d1ab --- /dev/null +++ b/httemplate/misc/xmlhttp-cust_bill-search.html @@ -0,0 +1,18 @@ +<% encode_json(\@return) %> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +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 }; +} + +</%init> diff --git a/httemplate/misc/xmlhttp-cust_main-search.cgi b/httemplate/misc/xmlhttp-cust_main-search.cgi index 436501e8b..16f7cd2bc 100644 --- a/httemplate/misc/xmlhttp-cust_main-search.cgi +++ b/httemplate/misc/xmlhttp-cust_main-search.cgi @@ -1,9 +1,9 @@ % if ( $sub eq 'custnum_search' ) { % my $custnum = $cgi->param('arg'); % my $return = []; -% if ( $custnum =~ /^(\d+)$/ ) { -% $return = findbycustnum($1,0); -% $return = findbycustnum($1,1) if(!scalar(@$return)); +% if ( $custnum =~ /^(\d+)$/ ) { #should also handle +% # cust_main-agent_custid-format') eq 'ww?d+' +% $return = findbycustnum_or_agent_custid($1); % } <% objToJson($return) %> % } elsif ( $sub eq 'smart_search' ) { @@ -12,7 +12,15 @@ % my @cust_main = smart_search( 'search' => $string, % 'no_fuzzy_on_exact' => 1, #pref? % ); -% my $return = [ map [ $_->custnum, $_->name, $_->balance, $_->ucfirst_status, $_->statuscolor ], @cust_main ]; +% my $return = [ map [ $_->custnum, +% $_->name, +% $_->balance, +% $_->ucfirst_status, +% $_->statuscolor, +% scalar($_->open_cust_bill) +% ], +% @cust_main +% ]; % <% objToJson($return) %> % } elsif ( $sub eq 'invnum_search' ) { @@ -20,7 +28,7 @@ % my $string = $cgi->param('arg'); % if ( $string =~ /^(\d+)$/ ) { % my $inv = qsearchs('cust_bill', { 'invnum' => $1 }); -% my $return = $inv ? findbycustnum($inv->custnum,0) : []; +% my $return = $inv ? findbycustnum($inv->custnum) : []; <% objToJson($return) %> % } else { #return nothing [] @@ -43,22 +51,58 @@ % } <%init> -my $conf = new FS::Conf; - my $sub = $cgi->param('sub'); -sub findbycustnum{ - my $custnum = shift; - my $agent = shift; - my $hashref = { 'custnum' => $custnum }; - $hashref = { 'agent_custid' => $custnum } if $agent; - my $c = qsearchs({ - 'table' => 'cust_main', - 'hashref' => $hashref, - 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, - }); - return [ $c->custnum, $c->name, $c->balance, $c->ucfirst_status, $c->statuscolor ] - if $c; - []; +sub findbycustnum { + + my $c = qsearchs({ + 'table' => 'cust_main', + 'hashref' => { 'custnum' => shift }, + 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, + }) or return []; + + [ $c->custnum, + $c->name, + $c->balance, + $c->ucfirst_status, + $c->statuscolor, + scalar($c->open_cust_bill) + ]; +} + +sub findbycustnum_or_agent_custid { + my $num = shift; + + my @or = ( 'agent_custid = ?' ); + my @param = ( $num ); + + if ( $num =~ /^\d+$/ && $num <= 2147483647 ) { #need a bigint custnum? wow + my $conf = new FS::Conf; + if ( $conf->exists('cust_main-default_agent_custid') ) { + push @or, "( agent_custid IS NULL AND custnum = $num )"; + } else { + push @or, "custnum = $num"; + } + } + + my $extra_sql = ' WHERE '. $FS::CurrentUser::CurrentUser->agentnums_sql. + ' AND ( '. join(' OR ', @or). ' )'; + + [ map [ $_->custnum, + $_->name, + $_->balance, + $_->ucfirst_status, + $_->statuscolor, + scalar($_->open_cust_bill), + ], + + qsearch({ + 'table' => 'cust_main', + 'hashref' => {}, + 'extra_sql' => $extra_sql, + 'extra_param' => \@param, + }) + ]; } + </%init> diff --git a/httemplate/pref/pref-process.html b/httemplate/pref/pref-process.html index 974b96dc2..bd6bb860a 100644 --- a/httemplate/pref/pref-process.html +++ b/httemplate/pref/pref-process.html @@ -49,7 +49,7 @@ unless ( $error ) { # if ($access_user) { #XXX autogen my @paramlist = qw( locale menu_position default_customer_view mobile_menu - disable_html_editor + disable_html_editor disable_enter_submit_onetimecharge email_address snom-ip snom-username snom-password vonage-fromnumber vonage-username vonage-password diff --git a/httemplate/pref/pref.html b/httemplate/pref/pref.html index 8fd1eaa73..8e56355db 100644 --- a/httemplate/pref/pref.html +++ b/httemplate/pref/pref.html @@ -32,7 +32,7 @@ Interface <TR> <TH ALIGN="right">Locale: </TH> - <TD> + <TD COLSPAN=2> <SELECT NAME="locale"> % foreach my $locale ( FS::Locales->locales ) { % my %info = FS::Locales->locale_info($locale); @@ -83,6 +83,13 @@ Interface </TD> </TR> + <TR> + <TH ALIGN="right" COLSPAN=1>Disable submission on [Enter] key - one-time charges: </TH> + <TD ALIGN="left" COLSPAN=2> + <INPUT TYPE="checkbox" NAME="disable_enter_submit_onetimecharge" VALUE="1" <% $curuser->option('disable_enter_submit_onetimecharge') ? 'CHECKED' : '' %>> + </TD> + </TR> + </TABLE> <BR> diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index 94860d3f2..f9dd4a20e 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -9,7 +9,9 @@ 'header' => [ emt('Description'), ( $unearned - ? ( emt('Unearned'), emt('Owed'), emt('Payment date') ) + ? ( emt('Unearned'), + emt('Owed'), # useful in 'paid' mode? + emt('Payment date') ) : ( emt('Setup charge') ) ), ( $use_usage eq 'usage' @@ -33,15 +35,9 @@ # they're not applicable to pkg_tax search sub { my $cust_bill_pkg = shift; if ( $unearned ) { - my $period = - $cust_bill_pkg->edate - $cust_bill_pkg->sdate; - my $elapsed = $unearned - $cust_bill_pkg->sdate; - $elapsed = 0 if $elapsed < 0; - my $remaining = 1 - $elapsed/$period; - - sprintf($money_char. '%.2f', - $remaining * $cust_bill_pkg->recur ); + sprintf($money_char.'%.2f', + $cust_bill_pkg->unearned_revenue) } else { sprintf($money_char.'%.2f', $cust_bill_pkg->setup ); @@ -53,7 +49,7 @@ ), sub { my $row = shift; my $value = 0; - if ( $use_usage eq 'recurring' ) { + if ( $use_usage eq 'recurring' or $unearned ) { $value = $row->recur - $row->usage; } elsif ( $use_usage eq 'usage' ) { $value = $row->usage; @@ -64,7 +60,10 @@ }, ( $unearned ? ( sub { time2str('%b %d %Y', shift->sdate ) }, - sub { time2str('%b %d %Y', shift->edate ) }, + # shift edate back a day + # 82799 = 3600*23 - 1 + # (to avoid skipping a day during DST) + sub { time2str('%b %d %Y', shift->edate - 82799 ) }, ) : () ), @@ -76,9 +75,11 @@ '', 'setup', #broken in $unearned case i guess ( $unearned ? ('', '') : () ), - ( $use_usage eq 'recurring' ? 'recur - usage' : - $use_usage eq 'usage' ? 'usage' - : 'recur' + ( $use_usage eq 'recurring' or $unearned + ? 'recur - usage' : + $use_usage eq 'usage' + ? 'usage' + : 'recur' ), ( $unearned ? ('sdate', 'edate') : () ), 'invnum', @@ -137,6 +138,12 @@ die "access denied" my $conf = new FS::Conf; my $unearned = ''; +my $unearned_mode = ''; +my $unearned_base = ''; +my $unearned_sql = ''; + +my @select = ( 'cust_bill_pkg.*', 'cust_bill._date' ); +my ($join_cust, $join_pkg ) = ('', ''); #here is the agent virtualization my $agentnums_sql = @@ -146,14 +153,18 @@ my @where = ( $agentnums_sql ); my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); +if ( $cgi->param('status') =~ /^([a-z]+)$/ ) { + push @where, FS::cust_main->cust_status_sql . " = '$1'"; +} + if ( $cgi->param('distribute') == 1 ) { push @where, "sdate <= $ending", "edate > $beginning", ; } else { - push @where, "_date >= $beginning", - "_date <= $ending"; + push @where, "cust_bill._date >= $beginning", + "cust_bill._date <= $ending"; } if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { @@ -218,7 +229,7 @@ if ( $cgi->param('taxclass') } -my @loc_param = qw( city county state country ); +my @loc_param = qw( district city county state country ); if ( $cgi->param('out') ) { @@ -266,7 +277,7 @@ if ( $cgi->param('out') ) { my %ph = ( 'county' => dbh->quote($_), map { $_ => dbh->quote( $cgi->param($_) ) } - qw( city state country ) + qw( district city state country ) ); my ( $loc_sql, @param ) = FS::cust_pkg->location_sql; @@ -330,21 +341,71 @@ if ( $cgi->param('out') ) { push @where, FS::tax_rate_location->location_sql( map { $_ => (scalar($cgi->param($_)) || '') } - qw( city county state locationtaxid ) + qw( district city county state locationtaxid ) ); -} elsif ( $cgi->param('unearned_now') =~ /^(\d+)$/ ) { +} + +# unearned revenue mode +if ( $cgi->param('unearned_now') =~ /^(\d+)$/ ) { $unearned = $1; + $unearned_mode = $cgi->param('mode'); push @where, "cust_bill_pkg.sdate < $unearned", "cust_bill_pkg.edate > $unearned", "cust_bill_pkg.recur != 0", - "part_pkg.freq != '0'", + "part_pkg.freq != '0'"; + + if ( !$cgi->param('include_monthly') ) { + push @where, "part_pkg.freq != '1'", "part_pkg.freq NOT LIKE '%h'", "part_pkg.freq NOT LIKE '%d'", "part_pkg.freq NOT LIKE '%w'"; + } + + my $usage_sql = FS::cust_bill_pkg->usage_sql; + push @select, "($usage_sql) AS usage"; # we need this + my $paid_sql = 'GREATEST(' . + FS::cust_bill_pkg->paid_sql($unearned, '', setuprecur => 'recur') . + " - $usage_sql, 0)"; + + push @select, "$paid_sql AS paid_no_usage"; # need this either way + + if ( $unearned_mode eq 'paid' ) { + # then use the amount paid, minus usage charges + $unearned_base = $paid_sql; + } + else { + # use the amount billed, minus usage charges and credits + $unearned_base = "GREATEST( cust_bill_pkg.recur - ". + FS::cust_bill_pkg->credited_sql($unearned, '', setuprecur => 'recur') . + " - $usage_sql, 0)"; + # include only rows that have some non-usage, non-credited portion + } + # whatever we're using as the base, only show rows where it's positive + push @where, "$unearned_base > 0"; + + my $period = "CAST(cust_bill_pkg.edate - cust_bill_pkg.sdate AS REAL)"; + my $elapsed = "GREATEST( $unearned - cust_bill_pkg.sdate, 0 )"; + my $remaining = "(1 - $elapsed/$period)"; + + $unearned_sql = "CAST( $unearned_base * $remaining AS DECIMAL(10,2) )"; + push @select, "$unearned_sql AS unearned_revenue"; + + # last payment/credit date + my %t = (pay => 'cust_bill_pay', credit => 'cust_credit_bill'); + foreach my $x (qw(pay credit)) { + my $table = $t{$x}; + my $link = $table.'_pkg'; + my $pkey = dbdef->table($table)->primary_key; + my $last_date_sql = "SELECT MAX(_date) + FROM $table JOIN $link USING ($pkey) + WHERE $link.billpkgnum = cust_bill_pkg.billpkgnum + AND $table._date <= $unearned"; + push @select, "($last_date_sql) AS last_$x"; + } } @@ -463,12 +524,12 @@ if ( $cgi->param('pkg_tax') ) { $count_query = "SELECT COUNT(DISTINCT billpkgnum), "; } - if ( $use_usage eq 'recurring' ) { - $count_query .= "SUM(setup + recur - usage)"; + if ( $unearned ) { + $count_query .= "SUM( $unearned_base ), SUM( $unearned_sql )"; + } elsif ( $use_usage eq 'recurring' ) { + $count_query .= "SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - usage)"; } elsif ( $use_usage eq 'usage' ) { $count_query .= "SUM(usage)"; - } elsif ( $unearned ) { - $count_query .= "SUM(cust_bill_pkg.recur)"; } elsif ( scalar( grep( /locationtaxid/, $cgi->param ) ) ) { $count_query .= "SUM( COALESCE(cust_bill_pkg_tax_rate_location.amount, cust_bill_pkg.setup + cust_bill_pkg.recur))"; } elsif ( $cgi->param('iscredit') eq 'rate') { @@ -477,38 +538,17 @@ if ( $cgi->param('pkg_tax') ) { $count_query .= "SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)"; } - if ( $unearned ) { - - #false laziness w/report_prepaid_income.cgi - - my $float = 'REAL'; #'DOUBLE PRECISION'; - - my $period = "CAST(cust_bill_pkg.edate - cust_bill_pkg.sdate AS $float)"; - my $elapsed = "(CASE WHEN cust_bill_pkg.sdate > $unearned - THEN 0 - ELSE ($unearned - cust_bill_pkg.sdate) - END)"; - #my $elapsed = "CAST($unearned - cust_bill_pkg.sdate AS $float)"; - - my $remaining = "(1 - $elapsed/$period)"; - - $count_query .= ", SUM($remaining * cust_bill_pkg.recur)"; - - } - } -my $join_cust = ' JOIN cust_bill USING ( invnum ) - LEFT JOIN cust_main USING ( custnum ) '; - +$join_cust = ' JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_main USING ( custnum ) '; -my $join_pkg; if ( $cgi->param('nottax') ) { - $join_pkg = ' LEFT JOIN cust_pkg USING ( pkgnum ) - LEFT JOIN part_pkg USING ( pkgpart ) - LEFT JOIN part_pkg AS override - ON pkgpart_override = override.pkgpart '; + $join_pkg .= ' LEFT JOIN cust_pkg USING ( pkgnum ) + LEFT JOIN part_pkg USING ( pkgpart ) + LEFT JOIN part_pkg AS override + ON pkgpart_override = override.pkgpart '; $join_pkg .= ' LEFT JOIN cust_location USING ( locationnum ) ' if $conf->exists('tax-pkg_address'); @@ -567,9 +607,6 @@ if ($use_usage) { $count_query .= " FROM cust_bill_pkg $join_cust $join_pkg $where"; } -my @select = ( 'cust_bill_pkg.*', - 'cust_bill._date', ); - push @select, 'part_pkg.pkg', 'part_pkg.freq', unless $cgi->param('istax'); @@ -581,9 +618,9 @@ my $query = { 'table' => 'cust_bill_pkg', 'addl_from' => "$join_cust $join_pkg", 'hashref' => {}, - 'select' => join(', ', @select ), + 'select' => join(",\n", @select ), 'extra_sql' => $where, - 'order_by' => 'ORDER BY _date, billpkgnum', + 'order_by' => 'ORDER BY cust_bill._date, billpkgnum', }; my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ]; @@ -593,9 +630,8 @@ my $conf = new FS::Conf; my $money_char = $conf->config('money_char') || '$'; my $owed_sub = sub { - $money_char. shift->owed_recur; #_recur :/ + $money_char . shift->get('owed') # owed_recur is not correct here }; - my $payment_date_sub = sub { #my $cust_bill_pkg = shift; my @cust_pay = sort { $a->_date <=> $b->_date } diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html index 498024ba0..e164b98f4 100755 --- a/httemplate/search/cust_main.html +++ b/httemplate/search/cust_main.html @@ -33,9 +33,7 @@ <%init> die "access denied" - unless ( $FS::CurrentUser::CurrentUser->access_right('List customers') && - $FS::CurrentUser::CurrentUser->access_right('List packages') - ); + unless $FS::CurrentUser::CurrentUser->access_right('Advanced customer search'); my %search_hash = (); @@ -47,7 +45,6 @@ my @scalars = qw ( no_censustract with_geocode custbatch usernum cancelled_pkgs cust_fields flattened_pkgs - refnum ); for my $param ( @scalars ) { @@ -56,7 +53,7 @@ for my $param ( @scalars ) { } #lists -for my $param (qw( classnum payby tagnum )) { +for my $param (qw( classnum refnum payby tagnum )) { $search_hash{$param} = [ $cgi->param($param) ]; } @@ -64,14 +61,22 @@ for my $param (qw( classnum payby tagnum )) { # parse dates ### -foreach my $field (qw( signupdate )) { +foreach my $field (qw( signupdate birthdate spouse_birthdate )) { my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field); - next if $beginning == 0 && $ending == 4294967295 && !defined($cgi->param('signuphour')); + next if $beginning == 0 && $ending == 4294967295 && ( $field ne 'signupdate' || !defined($cgi->param('signuphour')) ); #or $disable{$cgi->param('status')}->{$field}; - $search_hash{$field} = [ $beginning, $ending, $cgi->param('signuphour') ]; + unless ( $field eq 'signupdate' ) { + $beginning -= 43200; + $ending -= 43200; + } + + my @ary = ( $beginning, $ending ); + push @ary, scalar($cgi->param('signuphour')) if $field eq 'signupdate'; + + $search_hash{$field} = \@ary; } diff --git a/httemplate/search/cust_pkg_summary.html b/httemplate/search/cust_pkg_summary.html index f9adf044e..8c05f7382 100644 --- a/httemplate/search/cust_pkg_summary.html +++ b/httemplate/search/cust_pkg_summary.html @@ -23,6 +23,6 @@ <%init> die "access denied" - unless $curuser->access_right('Summarize packages'); + unless $FS::CurrentUser::CurrentUser->access_right('Summarize packages'); </%init> diff --git a/httemplate/search/cust_svc.html b/httemplate/search/cust_svc.html index 9cf4bbda6..2adcbd76f 100644 --- a/httemplate/search/cust_svc.html +++ b/httemplate/search/cust_svc.html @@ -13,7 +13,10 @@ sub { #$_[0]->svc. ': '. $_[0]->label; my($label, $value, $svcdb) = $_[0]->label; - "$label: $value"; + my $id = $_[0]->agent_svcid + ? $_[0]->agent_svcid.': ' + : ''; + "$label: $id$value"; }, # package? \&FS::UI::Web::cust_fields, @@ -42,79 +45,75 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('List services'); -my $addl_from = ' LEFT JOIN part_svc USING ( svcpart ) '. - ' LEFT JOIN cust_pkg USING ( pkgnum ) '. - ' LEFT JOIN cust_main USING ( custnum ) '; +my $sql_query; -my @extra_sql = (); my $orderby = 'ORDER BY svcnum'; #has to be ordered by something #for pagination to work + if ( length( $cgi->param('search_svc') ) ) { - my $string = $cgi->param('search_svc'); - $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace - - # implement fuzzy searching in subclasses too at some point? - # service searching maybe shouldn't be fuzzy... - - push @extra_sql, - ' ( '. join(' OR ', - map { my $table = $_; - my $search_sql = "FS::$table"->search_sql($string); - " ( svcdb = '$table' - AND 0 < ( SELECT COUNT(*) FROM $table - WHERE $table.svcnum = cust_svc.svcnum - AND $search_sql - ) - ) "; - } - FS::part_svc->svc_tables - ). ' ) '; - -} elsif ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { - - $cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unknown svcdb"; - push @extra_sql, "svcdb = '$1'"; - $addl_from .= " LEFT JOIN $1 USING ( svcnum ) "; - - push @extra_sql, 'pkgnum IS NULL' - if $cgi->param('magic') eq 'unlinked'; - - if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { - my $sortby = $1; - $orderby = "ORDER BY $sortby"; + $sql_query = { + FS::cust_svc->smart_search_param( + 'search' => scalar($cgi->param('search_svc')) + ) + }; + +} else { + + my $addl_from = ' LEFT JOIN part_svc USING ( svcpart ) '. + ' LEFT JOIN cust_pkg USING ( pkgnum ) '. + ' LEFT JOIN cust_main USING ( custnum ) '; + + my @extra_sql = (); + + if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) { + + $cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unknown svcdb"; + push @extra_sql, "svcdb = '$1'"; + $addl_from .= " LEFT JOIN $1 USING ( svcnum ) "; + + push @extra_sql, 'pkgnum IS NULL' + if $cgi->param('magic') eq 'unlinked'; + + if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { + my $sortby = $1; + $orderby = "ORDER BY $sortby"; + } + + } elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) { + + push @extra_sql, "svcpart = $1"; + + } else { + errorpage("No search term specified"); } -} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) { + #here is the agent virtualization + push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( + 'null_right' => 'View/link unlinked services' + ); - push @extra_sql, "svcpart = $1"; + my $extra_sql = ' WHERE '. join(' AND ', @extra_sql ); + + $sql_query = { + 'table' => 'cust_svc', + 'addl_from' => $addl_from, + 'hashref' => {}, + 'extra_sql' => $extra_sql, + }; -} else { - errorpage("No search term specified"); } -#here is the agent virtualization -push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( - 'null_right' => 'View/link unlinked services' - ); - -my $extra_sql = ' WHERE '. join(' AND ', @extra_sql ); - -my $sql_query = { - 'select' => join(', ', - 'cust_svc.*', - 'part_svc.*', - 'cust_main.custnum', - FS::UI::Web::cust_sql_fields(), - ), - 'table' => 'cust_svc', - 'addl_from' => $addl_from, - 'hashref' => {}, - 'extra_sql' => $extra_sql, - 'order_by' => $orderby, -}; +$sql_query->{'select'} = join(', ', + 'cust_svc.*', + 'part_svc.*', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(), + ); +$sql_query->{'order_by'} = $orderby; -my $count_query = "SELECT COUNT(*) FROM cust_svc $addl_from $extra_sql"; +my $count_query = "SELECT COUNT(*) FROM cust_svc ". $sql_query->{addl_from}. + ' '. $sql_query->{extra_sql}; my $link = sub { my $cust_svc = shift; diff --git a/httemplate/search/elements/cust_main_dayranges.html b/httemplate/search/elements/cust_main_dayranges.html index cc1601455..eb7566494 100644 --- a/httemplate/search/elements/cust_main_dayranges.html +++ b/httemplate/search/elements/cust_main_dayranges.html @@ -108,13 +108,14 @@ my $ranges = $opt{'ranges'} ? delete($opt{'ranges'}) : [ my $range_sub = delete($opt{'range_sub'}); #or die -my $offset = 0; +my $as_of; if($cgi->param('as_of')) { - $offset = int((time - parse_datetime($cgi->param('as_of'))) / 86400); - $opt{'title'} .= ' ('.$cgi->param('as_of').')' if $offset > 0; + $as_of = parse_datetime($cgi->param('as_of')) || ''; + $opt{'title'} .= ' ('.$cgi->param('as_of').')' if $as_of; } -my $range_cols = join(',', map call_range_sub($range_sub, @$_, 'offset' => $offset ), @$ranges ); +my $range_cols = join(',', + map call_range_sub($range_sub, @$_, 'as_of' => $as_of ), @$ranges ); my $select_count_pkgs = FS::cust_main->select_count_pkgs_sql; @@ -144,7 +145,7 @@ unless ( $cgi->param('all_customers') ) { my $negative = $cgi->param('negative') || 0; push @where, - call_range_sub($range_sub, $days, 0, 'offset' => $offset, 'no_as'=>1). + call_range_sub($range_sub, $days, 0, 'as_of' => $as_of, 'no_as'=>1). ($negative ? ' != 0' : ' > 0'); } @@ -186,7 +187,9 @@ my $sql_query = { my $total_sql = "SELECT ". - join(',', map call_range_sub( $range_sub, @$_, 'offset' => $offset, 'sum'=>1 ), @$ranges). + join(',', + map call_range_sub( $range_sub, @$_, 'as_of' => $as_of, 'sum'=>1 ), + @$ranges). " FROM cust_main $where"; my $total_sth = dbh->prepare($total_sql) or die dbh->errstr; @@ -251,10 +254,11 @@ sub call_range_sub { my $as = $opt{'no_as'} ? '' : " AS rangecol_${startdays}_$enddays"; - my $offset = $opt{'offset'} || 0; - # Always use $offset - 1day + 1sec = the last second of that day - my $cutoff = DateTime->now->set(hour => 23, minute => 59, second => 59); - $cutoff->subtract(days => $offset); + my $as_of = $opt{'as_of'} || time; + my $cutoff = DateTime->from_epoch(epoch => $as_of, time_zone => 'local'); + $cutoff->truncate(to => 'day'); # local midnight on the report day + $cutoff->add(days => 1); # the day after that + $cutoff->subtract(seconds => 1); # the last second of the report day my $start = $cutoff->clone; $start->subtract(days => $startdays); @@ -262,7 +266,7 @@ sub call_range_sub { my $end = $cutoff->clone; $end->subtract(days => $enddays); - #warn "offset $offset (".$cutoff->epoch."), range $startdays-$enddays (".$start->epoch . '-' . ($enddays ? $end->epoch : '').")\n"; + #warn "cutoff ".$cutoff->epoch.", range $startdays-$enddays (".$start->epoch . '-' . ($enddays ? $end->epoch : '').")\n"; my $sql = &{$range_sub}( $start->epoch, $enddays ? $end->epoch : '', $cutoff->epoch ); #%opt? diff --git a/httemplate/search/report_prepaid_income.cgi b/httemplate/search/prepaid_income.html index 2fe5b6f10..ebac5a2a9 100644 --- a/httemplate/search/report_prepaid_income.cgi +++ b/httemplate/search/prepaid_income.html @@ -56,9 +56,11 @@ <BR> <% $actual_label %><% $actual_label ? 'u' : 'U' %>nearned revenue -is the amount of unearned revenue +is the as-yet-unearned portion of revenue <% $actual_label ? 'Freeside has actually' : '' %> -invoiced for packages with longer-than monthly terms. +invoiced for packages with +<% $cgi->param('include_monthly') ? 'terms extending into the future.' + : 'longer-than monthly terms.' %> % if ( $legacy ) { <BR><BR> @@ -82,13 +84,25 @@ my $actual_label = $legacy ? 'Actual ' : ''; #doesn't yet deal with daily/weekly packages +my $mode = $cgi->param('mode'); + my $time = time; my $now = $cgi->param('date') && parse_datetime($cgi->param('date')) || $time; $now =~ /^(\d+)$/ or die "unparsable date?"; $now = $1; -my $link = "cust_bill_pkg.cgi?nottax=1;unearned_now=$now"; +my $dt = DateTime->from_epoch(epoch => $now, time_zone => 'local'); +$dt->truncate(to => 'day'); # local midnight on the report day +$dt->add(days => 1); # the day after that +$dt->subtract(seconds => 1); # the last second of the report day +$now = $dt->epoch; + +my $link = "unearned_detail.html?date=$now;mode=$mode"; + +if ( $cgi->param('include_monthly') ) { + $link .= ';include_monthly=1'; +} my $curuser = $FS::CurrentUser::CurrentUser; @@ -108,6 +122,13 @@ my @where = (); #here is the agent virtualization push @where, $curuser->agentnums_sql( 'table'=>'cust_main' ); +my $status = ''; +if ( $cgi->param('status') =~ /^([a-z]+)$/ ) { + $status = $1; + $link .= ";status=$status"; + push @where, FS::cust_main->cust_status_sql . " = '$status'"; +} + my %total = (); my %total_legacy = (); foreach my $agentnum (@agentnums) { @@ -117,66 +138,54 @@ foreach my $agentnum (@agentnums) { my( $total, $total_legacy ) = ( 0, 0 ); - # my @cust_bill_pkg = - # grep { $_->cust_pkg && $_->cust_pkg->part_pkg->freq !~ /^([01]|\d+[hdw])$/ } - # qsearch({ - # 'select' => 'cust_bill_pkg.*', - # 'table' => 'cust_bill_pkg', - # 'addl_from' => ' LEFT JOIN cust_bill USING ( invnum ) '. - # ' LEFT JOIN cust_main USING ( custnum ) ', - # 'hashref' => { - # 'recur' => { op=>'!=', value=>0 }, - # 'sdate' => { op=>'<', value=>$now }, - # 'edate' => { op=>'>', value=>$now }, - # }, - # 'extra_sql' => $where, - # }); - # - # foreach my $cust_bill_pkg ( @cust_bill_pkg) { - # my $period = $cust_bill_pkg->edate - $cust_bill_pkg->sdate; - # - # my $elapsed = $now - $cust_bill_pkg->sdate; - # $elapsed = 0 if $elapsed < 0; - # - # my $remaining = 1 - $elapsed/$period; - # - # my $unearned = $remaining * $cust_bill_pkg->recur; - # $total += $unearned; - # - # } - - #re-written in sql: - - #false laziness w/cust_bill_pkg.cgi - - my $float = 'REAL'; #'DOUBLE PRECISION'; - - my $period = "CAST(cust_bill_pkg.edate - cust_bill_pkg.sdate AS $float)"; - my $elapsed = "(CASE WHEN cust_bill_pkg.sdate > $now - THEN 0 - ELSE ($now - cust_bill_pkg.sdate) - END)"; - #my $elapsed = "CAST($unearned - cust_bill_pkg.sdate AS $float)"; - - my $remaining = "(1 - $elapsed/$period)"; - - my $select = "SUM($remaining * cust_bill_pkg.recur)"; - - #[...] - - my $sql = "SELECT $select FROM cust_bill_pkg - LEFT JOIN cust_pkg USING ( pkgnum ) - LEFT JOIN part_pkg USING ( pkgpart ) - LEFT JOIN cust_main USING ( custnum ) - WHERE pkgpart > 0 - AND sdate < $now - AND edate > $now - AND cust_bill_pkg.recur != 0 - AND part_pkg.freq != '0' + my @opt = ($now, '', setuprecur => 'recur', no_usage => 1); + # balance owed, recurring only, not including usage charges + my $unearned_base; + if ( $mode eq 'billed' ) { + $unearned_base = '( ' . + FS::cust_bill_pkg->charged_sql(@opt) . ' - ' . + FS::cust_bill_pkg->credited_sql(@opt) . ' )'; + } elsif ( $mode eq 'paid' ) { + $unearned_base = FS::cust_bill_pkg->paid_sql(@opt); + } + + my $edate_zero = midnight_sql('edate'); + my $sdate_zero = midnight_sql('sdate'); + my $period = "CAST( ($edate_zero - $sdate_zero) / 86400.0 AS DECIMAL(10,0) )"; + my $remaining = "GREATEST( + CAST( ($edate_zero - $now) / 86400.0 AS DECIMAL(10,0) ), + 0)"; + my $fraction = "$remaining / $period"; + + my $unearned_sql = "CAST( + GREATEST( $unearned_base * $fraction, 0 ) + AS DECIMAL(10,2) + )"; + + my $select = "SUM( $unearned_sql )"; + + if ( !$cgi->param('include_monthly') ) { + # all except freq != 0; one-time charges should never be included + $where .= " AND part_pkg.freq != '1' AND part_pkg.freq NOT LIKE '%h' AND part_pkg.freq NOT LIKE '%d' - AND part_pkg.freq NOT LIKE '%w' + AND part_pkg.freq NOT LIKE '%w'"; + } + + # $mode actually doesn't matter here, since unpaid invoices have zero + # unearned revenue + + my $sql = + "SELECT $select FROM cust_bill_pkg + LEFT JOIN cust_pkg ON (cust_bill_pkg.pkgnum = cust_pkg.pkgnum) + LEFT JOIN part_pkg USING ( pkgpart ) + LEFT JOIN cust_main ON (cust_pkg.custnum = cust_main.custnum) + WHERE pkgpart > 0 + AND cust_bill_pkg.sdate < $now + AND cust_bill_pkg.edate > $now + AND cust_bill_pkg.recur != 0 + AND part_pkg.freq != '0' $where "; diff --git a/httemplate/search/report_cust_main.html b/httemplate/search/report_cust_main.html index 0ef5a5196..39cf695d8 100755 --- a/httemplate/search/report_cust_main.html +++ b/httemplate/search/report_cust_main.html @@ -25,6 +25,12 @@ 'all_selected' => 1, &> + <& /elements/tr-select-part_referral.html, + 'label' => emt('Advertising Source'), + 'multiple' => 1, + 'all_selected' => 1, + &> + <TR> <TD ALIGN="right" VALIGN="center"><% mt('Address') |h %></TD> <TD><INPUT TYPE="text" NAME="address" SIZE=54></TD> @@ -42,6 +48,34 @@ </TD> </TR> +% if ( $conf->exists('cust_main-enable_birthdate') ) { + <TR> + <TD ALIGN="right" VALIGN="center"><% mt('Date of Birth') |h %></TD> + <TD> + <TABLE> + <& /elements/tr-input-beginning_ending.html, + prefix => 'birthdate', + layout => 'horiz', + &> + </TABLE> + </TD> + </TR> +% } + +% if ( $conf->exists('cust_main-enable_spouse_birthdate') ) { + <TR> + <TD ALIGN="right" VALIGN="center"><% mt('Spouse Date of Birth') |h %></TD> + <TD> + <TABLE> + <& /elements/tr-input-beginning_ending.html, + prefix => 'spouse_birthdate', + layout => 'horiz', + &> + </TABLE> + </TD> + </TR> +% } + <& /elements/tr-select-cust_tag.html, 'cgi' => $cgi, 'is_report' => 1, @@ -101,11 +135,6 @@ &> <TR> - <TD ALIGN="right" VALIGN="center"><% mt('Include cancelled packages') |h %></TD> - <TD><INPUT TYPE="checkbox" NAME="cancelled_pkgs"></TD> - </TR> - - <TR> <TD ALIGN="right" VALIGN="center"><% mt('Without census tract') |h %></TD> <TD><INPUT TYPE="checkbox" NAME="no_censustract"></TD> </TR> @@ -132,6 +161,12 @@ <TD ALIGN="right" VALIGN="center"><% mt('Add package columns') |h %></TD> <TD><INPUT TYPE="checkbox" NAME="flattened_pkgs"></TD> </TR> + + <TR> + <TD ALIGN="right" VALIGN="center"><% mt('Include cancelled packages') |h %></TD> + <TD><INPUT TYPE="checkbox" NAME="cancelled_pkgs"></TD> + </TR> + </TABLE> <BR> @@ -143,9 +178,7 @@ <%init> die "access denied" - unless ( $FS::CurrentUser::CurrentUser->access_right('List customers') && - $FS::CurrentUser::CurrentUser->access_right('List packages') - ); + unless $FS::CurrentUser::CurrentUser->access_right('Advanced customer search'); my $conf = new FS::Conf; diff --git a/httemplate/search/report_prepaid_income.html b/httemplate/search/report_prepaid_income.html index 061b24c68..4743e2d21 100644 --- a/httemplate/search/report_prepaid_income.html +++ b/httemplate/search/report_prepaid_income.html @@ -2,7 +2,7 @@ <% include('/elements/init_calendar.html') %> -<FORM ACTION="report_prepaid_income.cgi" METHOD="GET"> +<FORM ACTION="prepaid_income.html" METHOD="GET"> <TABLE BGCOLOR="#cccccc" CELLSPACING=0> @@ -13,7 +13,7 @@ </TR> <TR> - <TD>As of </TD> + <TD ALIGN="right">As of </TD> <TD> <INPUT TYPE="text" NAME="date" ID="date_text" VALUE="now"> <IMG SRC="../images/calendar.png" ID="date_button" STYLE="cursor: pointer" TITLE="Select date"> @@ -30,7 +30,20 @@ </TR> <% include( '/elements/tr-select-agent.html', 'disable_empty'=>0 ) %> - + + <& /elements/tr-select-cust_main-status.html, + label => mt('Customer Status') &> + <& /elements/tr-select.html, + label => 'Invoice Status', + field => 'mode', + options => [ qw(billed paid) ] &> + <TR> + <TD ALIGN="right"> + <INPUT TYPE="checkbox" NAME="include_monthly" VALUE=1> + </TD> + <TD ALIGN="left"><% mt('Include packages with period ≤ 1 month') %> + </TD> + </TR> <TR> <TD COLSPAN=2> </TD> </TR> diff --git a/httemplate/search/report_receivables.html b/httemplate/search/report_receivables.html index 19f863525..5cff0f4fc 100755 --- a/httemplate/search/report_receivables.html +++ b/httemplate/search/report_receivables.html @@ -52,7 +52,7 @@ function toggle(obj) { <& /elements/tr-input-date-field.html, { 'name' => 'as_of', 'value' => time, - 'label' => emt('As of date '), + 'label' => emt('At the end of date '), 'format' => FS::Conf->new->config('date_format') || '%m/%d/%Y', } &> diff --git a/httemplate/search/report_svc_acct.html b/httemplate/search/report_svc_acct.html index 14c284fb7..74bf5538e 100755 --- a/httemplate/search/report_svc_acct.html +++ b/httemplate/search/report_svc_acct.html @@ -114,7 +114,7 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('List packages'); #? + unless $FS::CurrentUser::CurrentUser->access_right('Services: Accounts: Advanced search'); #? my $title = emt('Account Report'); diff --git a/httemplate/search/report_svc_broadband.html b/httemplate/search/report_svc_broadband.html index 37f21b79f..d7422eeae 100755 --- a/httemplate/search/report_svc_broadband.html +++ b/httemplate/search/report_svc_broadband.html @@ -76,7 +76,7 @@ <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('List packages'); #? + unless $FS::CurrentUser::CurrentUser->access_right('Services: Wireless broadband services: Advanced search'); my $title = 'Broadband Service Report'; my $routernum = [ $cgi->param('routernum') || '' ]; diff --git a/httemplate/search/report_svc_hardware.html b/httemplate/search/report_svc_hardware.html index 61ba4ab8a..b0bfc08c7 100755 --- a/httemplate/search/report_svc_hardware.html +++ b/httemplate/search/report_svc_hardware.html @@ -61,7 +61,7 @@ OR (SELECT COUNT(*) FROM svc_hardware <%init> die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('List packages'); #? + unless $FS::CurrentUser::CurrentUser->access_right('Services: Hardware: Advanced search'); my $title = 'Hardware Service Report'; diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi index 0cd652d83..2786f571b 100755 --- a/httemplate/search/report_tax.cgi +++ b/httemplate/search/report_tax.cgi @@ -239,6 +239,8 @@ as <A HREF="<% $p.'search/report_tax-xls.cgi?'.$cgi->query_string%>">Excel sprea <%init> +my $DEBUG = $cgi->param('debug') || 0; + die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); @@ -252,15 +254,19 @@ my $join_cust = ' JOIN cust_bill USING ( invnum ) LEFT JOIN cust_main USING ( custnum ) '; my $join_cust_pkg = $join_cust. ' LEFT JOIN cust_pkg USING ( pkgnum ) - LEFT JOIN part_pkg USING ( pkgpart ) '; -$join_cust_pkg .= ' LEFT JOIN cust_location USING ( locationnum )' - if $conf->exists('tax-pkg_address'); + LEFT JOIN part_pkg USING ( pkgpart ) + LEFT JOIN cust_location + ON ( cust_location.locationnum = ' . + FS::cust_pkg->tax_locationnum_sql . ' )'; my $from_join_cust_pkg = " FROM cust_bill_pkg $join_cust_pkg "; my $where = "WHERE _date >= $beginning AND _date <= $ending "; -my( $location_sql, @base_param ) = FS::cust_pkg->location_sql; +# this query will be run once per cust_main_county, +# or maybe once per country/state/city tuple, +# or maybe once per country/state...it's hard to say. +my ($location_sql, @base_param) = FS::cust_location->in_county_sql(param => 1); $where .= " AND $location_sql "; my $agentname = ''; @@ -275,7 +281,10 @@ sub gotcust { my $table = shift; my $prefix = @_ ? shift : ''; " - ( $table.${prefix}city = cust_main_county.city + ( $table.district = cust_main_county.district + OR cust_main_county.district = '' + OR cust_main_county.district IS NULL ) + AND ( $table.${prefix}city = cust_main_county.city OR cust_main_county.city = '' OR cust_main_county.city IS NULL ) AND ( $table.${prefix}county = cust_main_county.county @@ -288,59 +297,29 @@ sub gotcust { "; } -my $gotcust; -if ( $conf->exists('tax-ship_address') ) { - - $gotcust = " - ( cust_main_county.country = cust_main.country - OR cust_main_county.country = cust_main.ship_country - ) - - AND - - ( - ( ( ship_last IS NULL OR ship_last = '' ) - AND ". gotcust('cust_main'). " - ) - OR - ( ship_last IS NOT NULL AND ship_last != '' - AND ". gotcust('cust_main', 'ship_'). " - ) - ) - "; - -} else { - - $gotcust = gotcust('cust_main'); - -} -if ( $conf->exists('tax-pkg_address') ) { - $gotcust = " - ( cust_pkg.locationnum IS NULL AND $gotcust) - OR ( cust_pkg.locationnum IS NOT NULL AND ". gotcust('cust_location'). " )"; - $gotcust = - "WHERE 0 < ( SELECT COUNT(*) FROM cust_pkg - LEFT JOIN cust_main USING ( custnum ) - LEFT JOIN cust_location USING ( locationnum ) - WHERE $gotcust - LIMIT 1 - ) - "; -} else { - $gotcust = - "WHERE 0 < ( SELECT COUNT(*) FROM cust_main WHERE $gotcust LIMIT 1 )"; -} +#non-parameterized form +my $location_in_county = FS::cust_location->in_county_sql; +my $gotcust = "WHERE EXISTS( + SELECT 1 FROM cust_location WHERE $location_in_county AND disabled IS NULL +)"; my $out = 'Out of taxable region(s)'; +# these are actually tax labels, not regions my %regions = (); +# Phase 1: Taxable and exempt sales +# Collect for each cust_main_county, and assign to a bin based on label. +# Note that "label" includes city if show_cities is on, and taxclass if +# show_taxclasses is on. foreach my $r ( qsearch({ 'table' => 'cust_main_county', 'extra_sql' => $gotcust, + 'debug' => $DEBUG, }) ) { - #warn $r->county. ' '. $r->state. ' '. $r->country. "\n"; + warn $r->county. ' '. $r->state. ' '. $r->country. "\n" if $DEBUG > 1; + # set up a %regions entry for this region's tax label my $label = getlabel($r); $regions{$label}->{'label'} = $label; @@ -366,6 +345,7 @@ foreach my $r ( qsearch({ 'table' => 'cust_main_county', } else { + # SQL for "taxclass doesn't match any other tax in the region" my $same_sql = $r->sql_taxclass_sameregion; $mywhere .= " AND $same_sql" if $same_sql; @@ -375,42 +355,24 @@ foreach my $r ( qsearch({ 'table' => 'cust_main_county', } + # FROM cust_bill_pkg JOIN (whatever is needed to determine tax location) + # WHERE (matches tax location and agentnum and taxclass) + # takes parameters in @base_param, plus taxclass if there is one my $fromwhere = "$from_join_cust_pkg $mywhere"; # AND payby != 'COMP' "; -# my $label = getlabel($r); -# $regions{$label}->{'label'} = $label; - my $nottax = 'pkgnum != 0'; - ## calculate total for this region + ## calculate total of sales (non-tax line items) for this region my $t_sql = "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) $fromwhere AND $nottax"; my $t = scalar_sql($r, \@param, $t_sql); $regions{$label}->{'total'} += $t; - #if ( $label eq $out ) # && $t ) { - # warn "adding $t for ". - # join('/', map $r->$_, qw( taxclass county state country ) ). "\n"; - # #warn $t_sql if $r->state eq 'FL'; - #} + #$regions{$label}->{subtotals}->{$r->taxnum} = $t; #useful debug ## calculate customer-exemption for this region -## my $taxable = $t; - -# my($taxable, $x_cust) = (0, 0); -# foreach my $e ( grep { $r->get($_.'tax') !~ /^Y/i } -# qw( cust_bill_pkg.setup cust_bill_pkg.recur ) ) { -# $taxable += scalar_sql($r, \@param, -# "SELECT SUM($e) $fromwhere AND $nottax AND ( tax != 'Y' OR tax IS NULL )" -# ); -# -# $x_cust += scalar_sql($r, \@param, -# "SELECT SUM($e) $fromwhere AND $nottax AND tax = 'Y'" -# ); -# } - #false laziness -ish w/report_tax.cgi my $cust_exempt; if ( $r->taxname ) { @@ -486,10 +448,12 @@ foreach my $r ( qsearch({ 'table' => 'cust_main_county', } else { $regions{$label}->{'rate'} = $r->tax.'%'; } - } +warn Dumper(\%regions) if $DEBUG > 1; +# $regions{$label} now contains 'total', 'exempt_cust', 'exempt_pkg', +# 'exempt_monthly', summed over each set of regions with the same label. -my $distinct = "country, state, county, city, +my $distinct = "country, state, county, city, district, CASE WHEN taxname IS NULL THEN '' ELSE taxname END AS taxname"; my $taxclass_distinct = #a little bit unsure of this part... test? @@ -501,38 +465,44 @@ my $taxclass_distinct = )." AS taxclass"; +# Phase 2: invoiced/credited tax items +# Collect this data for each country/state/city/district/taxname(/taxclass). my %qsearch = ( 'select' => "DISTINCT $distinct, $taxclass_distinct", 'table' => 'cust_main_county', 'hashref' => {}, 'extra_sql' => $gotcust, + 'debug' => $DEBUG, ); -my $taxfromwhere = " FROM cust_bill_pkg $join_cust "; +# Join to cust_main the same as before (we need agentnum) +# but not to cust_pkg (because tax line items don't have a package) +# and then to cust_location via cust_bill_pkg_tax_location +my $taxfromwhere = "FROM cust_bill_pkg $join_cust + LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum ) + LEFT JOIN cust_location USING ( locationnum ) + "; my $taxwhere = $where; -if ( $conf->exists('tax-pkg_address') ) { - - $taxfromwhere .= 'LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum ) - LEFT JOIN cust_location USING ( locationnum ) '; - #quelle kludge - $taxwhere =~ s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g; - -} my $creditfromwhere = $taxfromwhere. - " JOIN cust_credit_bill_pkg USING (billpkgnum"; -$creditfromwhere .= " ,billpkgtaxlocationnum" - if $conf->exists('tax-pkg_address'); -$creditfromwhere .= ")"; + " JOIN cust_credit_bill_pkg USING (billpkgnum, billpkgtaxlocationnum)"; $taxfromwhere .= " $taxwhere "; #AND payby != 'COMP' "; $creditfromwhere .= " $taxwhere AND billpkgtaxratelocationnum IS NULL"; #AND payby != 'COMP' "; -my @taxparam = @base_param; +#should i be a cust_main_county method or something +# yes. yes, you should. + +# $taxfromwhere: Most of a query to find cust_bill_pkg records linked to a +# customer matching a given state/county/city/district (and within the date +# range for the report). +# @base_param: A list of the fields from cust_main_county to use as parameters. +# $_taxamount_sub: Takes a cust_main_county and returns the sum of taxes billed +# within the report period for all customers located in that county. If +# the cust_main_county has a taxname, limits to taxes with that name; otherwise +# includes all line items with pkgnum = 0 and description either 'Tax' or empty. -#should i be a cust_main_county method or something -#need to pass in $taxfromwhere & @taxparam??? my $_taxamount_sub = sub { my $r = shift; @@ -545,9 +515,11 @@ my $_taxamount_sub = sub { my $sql = "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) ". " $taxfromwhere AND cust_bill_pkg.pkgnum = 0 $named_tax"; - scalar_sql($r, \@taxparam, $sql ); + scalar_sql($r, [ @base_param ], $sql ); }; +# $_creditamount_sub: As above, but returns the sum of credits applied + my $_creditamount_sub = sub { my $r = shift; @@ -560,7 +532,7 @@ my $_creditamount_sub = sub { my $sql = "SELECT SUM(cust_credit_bill_pkg.amount) ". " $creditfromwhere AND cust_bill_pkg.pkgnum = 0 $named_tax"; - scalar_sql($r, \@taxparam, $sql ); + scalar_sql($r, [ @base_param ], $sql ); }; #tax-report_groups filtering @@ -611,6 +583,10 @@ foreach my $r ( qsearch(\%qsearch) ) { } +# Phase 3: Non-taxclassed totals for invoiced/credited tax +# (If show_taxclasses is not in use, this was phase 2, but it +# displays somewhere different.) +# Don't filter by report_groups. my %base_regions = (); if ( $cgi->param('show_taxclasses') ) { @@ -734,7 +710,8 @@ sub getlabel { my $label; if ( $r->tax == 0 - && ! scalar( qsearch('cust_main_county', { 'city' => $r->city, + && ! scalar( qsearch('cust_main_county', { 'district'=> $r->district, + 'city' => $r->city, 'county' => $r->county, 'state' => $r->state, 'country' => $r->country, @@ -747,10 +724,6 @@ sub getlabel { #kludge to avoid "will not stay shared" warning my $out = 'Out of taxable region(s)'; $label = $out; -# } elsif ( $r->taxname && count_taxname($r->taxname) == 1 ) { -# $label = $r->taxname; -## $regions{$label}->{'taxname'} = $label; -## push @{$regions{$label}->{$_}}, $r->$_() foreach qw( county state country ); } else { $label = $r->country; $label = $r->state.", $label" if $r->state; diff --git a/httemplate/search/unearned_detail.html b/httemplate/search/unearned_detail.html new file mode 100644 index 000000000..02d514cbe --- /dev/null +++ b/httemplate/search/unearned_detail.html @@ -0,0 +1,257 @@ +<& elements/search.html, + 'title' => emt("Unearned revenue - ".ucfirst($unearned_mode)) . ' (' . + time2str('%b %d %Y', $unearned) . ')', + 'name' => emt('line items'), + 'query' => $query, + 'count_query' => $count_query, + 'count_addl' => [ $money_char. '%.2f total', + $money_char. '%.2f unearned revenue' + ], + 'header' => [ map( {emt $_} + 'Description', + 'Unearned', # depends on mode + 'Recurring charge', #recur - usage + 'Owed', #recur - usage - credits - payments + 'Paid', #payments + 'Payment date', #of last payment + 'Credit date', #of last credit + 'Charge start', + 'Charge end', + 'Invoice', + 'Date' + ), + FS::UI::Web::cust_header(), + ], + 'fields' => [ + #Description + sub { $_[0]->pkgnum > 0 + ? $_[0]->get('pkg') # possibly use override.pkg + : $_[0]->get('itemdesc') # but i think this correct + }, + #Unearned + money_sub('unearned_revenue'), + #Recurring charge + money_sub('recur_no_usage'), + #Owed + money_sub('owed_no_usage'), + #Paid + money_sub('paid_no_usage'), + #Payment date + date_sub('last_pay'), + #Credit date + date_sub('last_credit'), + #Charge start + date_sub('sdate'), + #Charge end, minus most of a day + date_sub('before_edate'), + #Invoice + 'invnum', + #Invoice date + date_sub('_date'), + \&FS::UI::Web::cust_fields, + ], + 'sort_fields' => [ + 'pkg', + # SQL expressions work as sort keys... + 'unearned_revenue', + 'recur_no_usage', + 'owed_no_usage', + 'paid_no_usage', + 'last_pay', + 'last_credit', + 'sdate', + 'edate', + 'invnum', + '_date', + ], + 'links' => [ + ('' x 9), + $ilink, + $ilink, + ( map { $_ ne 'Cust. Status' ? $clink : '' } + FS::UI::Web::cust_header() + ), + ], + 'align' => 'lrrcrccrc'. FS::UI::Web::cust_aligns(), + 'color' => [ + ('' x 11), + FS::UI::Web::cust_colors(), + ], + 'style' => [ + ('' x 11), + FS::UI::Web::cust_styles(), + ], +&> +<%init> + +# Separated from cust_bill_pkg.cgi to simplify things. + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $conf = new FS::Conf; + +my $unearned = ''; +my $unearned_mode = ''; +my $unearned_base = ''; +my $unearned_sql = ''; + +my @select = ( 'cust_bill_pkg.*', 'cust_bill._date' ); +my ($join_cust, $join_pkg ) = ('', ''); + +#here is the agent virtualization +my $agentnums_sql = + $FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' ); + +my @where = ( $agentnums_sql ); + +my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); + +if ( $cgi->param('status') =~ /^([a-z]+)$/ ) { + push @where, FS::cust_main->cust_status_sql . " = '$1'"; +} + +push @where, "cust_bill._date >= $beginning", + "cust_bill._date <= $ending"; + +if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { + push @where, "cust_main.agentnum = $1"; +} + +# no pkgclass, no taxclass, no tax location... + +# unearned revenue mode +$cgi->param('date') =~ /^(\d+)$/ + or die "date required"; + +$unearned = $1; +$unearned_mode = $cgi->param('mode'); + +push @where, "cust_bill_pkg.sdate < $unearned", + "cust_bill_pkg.edate > $unearned", + "cust_bill_pkg.recur != 0", + "part_pkg.freq != '0'"; + +if ( !$cgi->param('include_monthly') ) { + push @where, + "part_pkg.freq != '1'", + "part_pkg.freq NOT LIKE '%h'", + "part_pkg.freq NOT LIKE '%d'", + "part_pkg.freq NOT LIKE '%w'"; +} + +my @opt = ( + $unearned, #before this date + '', #after this date + setuprecur => 'recur', + no_usage => 1 +); + +my $charged = FS::cust_bill_pkg->charged_sql(@opt); +push @select, "($charged) AS recur_no_usage"; + +my $owed_sql = FS::cust_bill_pkg->owed_sql(@opt); +push @select, "($owed_sql) AS owed_no_usage"; + +my $paid_sql = FS::cust_bill_pkg->paid_sql(@opt); +push @select, "$paid_sql AS paid_no_usage"; + +if ( $unearned_mode eq 'paid' ) { + # then use the amount paid, minus usage charges + $unearned_base = $paid_sql; +} +else { + # use the amount billed, minus usage charges and credits + $unearned_base = "( $charged - " . + FS::cust_bill_pkg->credited_sql(@opt) . ' )'; +} +# whatever we're using as the base, only show rows where it's positive +push @where, "$unearned_base > 0"; + +my $edate_zero = midnight_sql('edate'); +my $sdate_zero = midnight_sql('sdate'); +# $unearned is one second before midnight on the date requested for the report. + +# suppress partial days for more accounting-like behavior +my $period = "CAST( ($edate_zero - $sdate_zero) / 86400.0 AS DECIMAL(10,0) )"; + +my $remaining = "GREATEST( + CAST( ($edate_zero - $unearned) / 86400.0 AS DECIMAL(10,0) ), + 0)"; +my $fraction = "$remaining / $period"; + +$unearned_sql = "CAST( $unearned_base * $fraction AS DECIMAL(10,2) )"; +push @select, "$unearned_sql AS unearned_revenue"; + +# last payment/credit date +my %t = (pay => 'cust_bill_pay', credit => 'cust_credit_bill'); +foreach my $x (qw(pay credit)) { + my $table = $t{$x}; + my $link = $table.'_pkg'; + my $pkey = dbdef->table($table)->primary_key; + my $last_date_sql = "SELECT MAX(_date) + FROM $table JOIN $link USING ($pkey) + WHERE $link.billpkgnum = cust_bill_pkg.billpkgnum + AND $table._date <= $unearned"; + push @select, "($last_date_sql) AS last_$x"; +} + +push @select, '(edate - 82799) AS before_edate'; + +#no itemdesc +#no tax report group kludge +#no tax exemption +#usage always excluded + +# always 'nottax', not 'istax' +$join_cust = ' JOIN cust_bill USING ( invnum ) + LEFT JOIN cust_main USING ( custnum ) '; + +$join_pkg .= ' LEFT JOIN cust_pkg USING ( pkgnum ) + LEFT JOIN part_pkg USING ( pkgpart ) + LEFT JOIN part_pkg AS override + ON pkgpart_override = override.pkgpart '; + +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"; + +push @select, 'part_pkg.pkg', + 'part_pkg.freq', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields(); + +my $query = { + 'table' => 'cust_bill_pkg', + 'addl_from' => "$join_cust $join_pkg", + 'hashref' => {}, + 'select' => join(",\n", @select ), + 'extra_sql' => $where, + 'order_by' => 'ORDER BY cust_bill._date, billpkgnum', +}; + +my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ]; +my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ]; + +my $money_char; + +sub money_sub { + $conf ||= new FS::Conf; + $money_char ||= $conf->config('money_char') || '$'; + my $field = shift; + sub { + $money_char . sprintf('%.2f', $_[0]->get($field)); + }; +} + +sub date_sub { + my $field = shift; + sub { + my $value = $_[0]->get($field); + $value ? time2str('%b %d %Y', $value) : ''; + }; +}; + +</%init> diff --git a/httemplate/view/cust_main.cgi b/httemplate/view/cust_main.cgi index 9ce55b0b9..ec3191971 100755 --- a/httemplate/view/cust_main.cgi +++ b/httemplate/view/cust_main.cgi @@ -46,10 +46,39 @@ function areyousure(href, message) { <A HREF="<% $p %>edit/cust_main.cgi?<% $custnum %>"><% mt('Edit this customer') |h %></A> | % } -% if ( $curuser->access_right('Cancel customer') -% && $cust_main->ncancelled_pkgs +% if ( $curuser->access_right('Suspend customer') +% && scalar($cust_main->unsuspended_pkgs) % ) { + <& /elements/popup_link-cust_main.html, + { 'action' => $p. 'misc/suspend_cust.html', + 'label' => emt('Suspend this customer'), + 'actionlabel' => emt('Confirm Suspension'), + 'color' => '#ff9900', + 'cust_main' => $cust_main, + 'width' => 616, #make room for reasons + 'height' => 366, + } + &> | +% } +% if ( $curuser->access_right('Unsuspend customer') +% && scalar($cust_main->suspended_pkgs) +% ) { + <& /elements/popup_link-cust_main.html, + { 'action' => $p. 'misc/unsuspend_cust.html', + 'label' => emt('Unsuspend this customer'), + 'actionlabel' => emt('Confirm Unsuspension'), + #'color' => '#ff9900', + 'cust_main' => $cust_main, + #'width' => 616, #make room for reasons + #'height' => 366, + } + &> | +% } + +% if ( $curuser->access_right('Cancel customer') +% && scalar($cust_main->ncancelled_pkgs) +% ) { <& /elements/popup_link-cust_main.html, { 'action' => $p. 'misc/cancel_cust.html', 'label' => emt('Cancel this customer'), @@ -60,11 +89,9 @@ function areyousure(href, message) { 'height' => 366, } &> | - % } % if ( $curuser->access_right('Merge customer') ) { - <& /elements/popup_link-cust_main.html, { 'action' => $p. 'misc/merge_cust.html', 'label' => emt('Merge this customer'), @@ -74,7 +101,6 @@ function areyousure(href, message) { 'height' => 192, } &> | - % } % if ( $conf->exists('deletecustomers') @@ -277,7 +303,13 @@ function areyousure(href, message) { % } % if ( $view eq 'custom' ) { +% if ( $conf->config('cust_main-custom_link') ) { <& cust_main/custom.html, $cust_main &> +% } elsif ( $conf->config('cust_main-custom_content') ) { + <& cust_main/custom_content.html, $cust_main &> +% #} else { +% # warn "custom view without cust_main-custom_link or -custom_content?"; +% } % } </DIV> @@ -326,7 +358,8 @@ $views{emt('Payment History')} = 'payment_history' $views{emt('Change History')} = 'change_history' if $curuser->access_right('View customer history'); $views{$conf->config('cust_main-custom_title') || emt('Custom')} = 'custom' - if $conf->config('cust_main-custom_link'); + if $conf->config('cust_main-custom_link') + || $conf->config('cust_main-custom_content'); $views{emt('Jumbo')} = 'jumbo'; my %viewname = reverse %views; diff --git a/httemplate/view/cust_main/billing.html b/httemplate/view/cust_main/billing.html index 4d55f7002..b2a0efdef 100644 --- a/httemplate/view/cust_main/billing.html +++ b/httemplate/view/cust_main/billing.html @@ -146,7 +146,7 @@ % if ( $cust_main->payinfo ) { <TR> - <TD ALIGN="right"<% mt('P.O.') |h %></TD> + <TD ALIGN="right"><% mt('P.O.') |h %></TD> <TD BGCOLOR="#ffffff"><% $cust_main->payinfo %></TD> </TR> % } @@ -190,7 +190,10 @@ % my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups'); -% unless ( $conf->exists('cust_class-tax_exempt') ) { +% unless ( $conf->exists('cust_class-tax_exempt') +% || $conf->exists('tax-cust_exempt-groups-require_individual_nums') +% ) +% { <TR> <TD ALIGN="right"><% mt('Tax exempt') |h %><% @exempt_groups ? ' ('.emt('all taxes').')' : '' %></TD> <TD BGCOLOR="#ffffff"><% $cust_main->tax ? $yes : $no %></TD> @@ -198,9 +201,12 @@ % } % foreach my $exempt_group ( @exempt_groups ) { +% my $cust_main_exemption = $cust_main->tax_exemption($exempt_group); <TR> <TD ALIGN="right"><% mt('Tax exempt') |h %> (<% $exempt_group %> taxes)</TD> - <TD BGCOLOR="#ffffff"><% $cust_main->tax_exemption($exempt_group) ? $yes : $no %></TD> + <TD BGCOLOR="#ffffff"><% $cust_main_exemption ? $yes : $no %> + <% $cust_main_exemption ? $cust_main_exemption->exempt_number : '' |h %> + </TD> </TR> % } @@ -211,21 +217,27 @@ </TR> % } <TR> - <TD ALIGN="right"><% mt('Postal invoices') |h %></TD> + <TD ALIGN="right"><% mt('Postal mail invoices') |h %></TD> <TD BGCOLOR="#ffffff"> <% ( grep { $_ eq 'POST' } @invoicing_list ) ? $yes : $no %> </TD> </TR> <TR> - <TD ALIGN="right"><% mt('FAX invoices') |h %></TD> + <TD ALIGN="right"><% mt('Fax invoices') |h %></TD> <TD BGCOLOR="#ffffff"> <% ( grep { $_ eq 'FAX' } @invoicing_list ) ? $yes : $no %> </TD> </TR> -% unless ( $conf->exists('cust-email-high-visibility')) { <TR> <TD ALIGN="right"><% mt('Email invoices') |h %></TD> <TD BGCOLOR="#ffffff"> + <% $cust_main->invoice_noemail ? $no : $yes %> + </TD> +</TR> +% unless ( $conf->exists('cust-email-high-visibility')) { +<TR> + <TD ALIGN="right"><% mt('Email address(es)') |h %></TD> + <TD BGCOLOR="#ffffff"> <% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) || $no %> </TD> </TR> diff --git a/httemplate/view/cust_main/contacts.html b/httemplate/view/cust_main/contacts.html index b3e52b556..6213f27dd 100644 --- a/httemplate/view/cust_main/contacts.html +++ b/httemplate/view/cust_main/contacts.html @@ -1,122 +1,128 @@ -% my %which = ( -% '' => emt('Billing'), -% 'ship_' => emt('Service'), -% ); -% foreach my $which ( '', 'ship_' ) { -% my $pre = $cust_main->get("${which}last") ? $which : ''; - -<FONT CLASS="fsinnerbox-title"><% $which{$which} %> <% mt('address') |h %></FONT> +% my %addr_label = ('bill' => 'Billing address', 'ship' => 'Service address'); + +%# Locations (possibly break this out) +% my @which = ('bill'); +% push @which, 'ship' if $cust_main->has_ship_address; +% while (@which) { +% my $this = shift @which; +% my $method = $this.'_location'; +% my $location = $cust_main->$method; +<FONT CLASS="fsinnerbox-title"><% mt( $addr_label{$this} ) |h %></FONT> <TABLE CLASS="fsinnerbox"> -<TR> - <TD ALIGN="right"><% mt('Contact name') |h %></TD> - <TD COLSPAN=5 BGCOLOR="#ffffff"> - <% $cust_main->get("${pre}last"). ', '. $cust_main->get("${pre}first") |h %> - </TD> -% if ( $which eq '' && $conf->exists('show_ss') ) { - <TD ALIGN="right"><% mt('SS#') |h %></TD> - <TD BGCOLOR="#ffffff"><% $cust_main->masked('ss') || ' ' %></TD> -% } -</TR> -% if ( $conf->exists('cust-email-high-visibility') && $which eq '') { +% if ( $this eq 'bill' ) { +% #billing contact fields + <TR> + <TD ALIGN="right"><% mt('Contact name') |h %></TD> + <TD COLSPAN=5 BGCOLOR="#ffffff"><% $cust_main->contact |h %></TD> +% if ( $conf->exists('show_ss') ) { + <TD ALIGN="right"><% mt('SS#') |h %></TD> + <TD BGCOLOR="#ffffff"><% $conf->exists('unmask_ss') + ? $cust_main->ss + : $cust_main->masked('ss') || ' ' %></TD> +% } + </TR> +% if ( $conf->exists('cust-email-high-visibility') ) { <TR> - <TD ALIGN="right"><% mt('Email invoices') |h %></TD> + <TD ALIGN="right"><% mt('Email address(es)') |h %></TD> <TD BGCOLOR="#ffff00"> - <% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) || $no %> + <% $cust_main->invoicing_list_emailonly_scalar || $no %> </TD> </TR> -% } - -% if ( $cust_main->get("${pre}company") ) { +% } +% if ( $cust_main->company ) { <TR> <TD ALIGN="right"><% mt('Company') |h %></TD> - <TD COLSPAN=7 BGCOLOR="#ffffff"><% $cust_main->get("${pre}company") |h %></TD> + <TD COLSPAN=7 BGCOLOR="#ffffff"><% $cust_main->company %></TD> </TR> -% } - +% } +% } # if $this eq 'bill' +% # now the actual address <TR> <TD ALIGN="right"><% mt('Address') |h %></TD> - <TD COLSPAN=7 BGCOLOR="#ffffff"><% $cust_main->get("${pre}address1") |h %></TD> + <TD COLSPAN=7 BGCOLOR="#ffffff"><% $location->address1 |h %></TD> </TR> -% if ( $cust_main->get("${pre}address2") ) { -% my $address2_label = -% ( $conf->exists('cust_main-require_address2') -% && ! ( $pre xor $cust_main->has_ship_address ) -% ) -% ? emt('Unit #') -% : ' '; +% if ( $location->get('address2') ) { +% my $address2_label = $conf->exists('cust_main-require_address2') +% ? emt('Unit #') +% : ' '; - <TR> - <TD ALIGN="right"><% $address2_label %></TD> - <TD COLSPAN=7 BGCOLOR="#ffffff"><% $cust_main->get("${pre}address2") |h %></TD> - </TR> +<TR> + <TD ALIGN="right"><% $address2_label %></TD> + <TD COLSPAN=7 BGCOLOR="#ffffff"><% $location->address2 |h %></TD> +</TR> % } <TR> <TD ALIGN="right"><% mt('City') |h %></TD> - <TD BGCOLOR="#ffffff"><% $cust_main->get("${pre}city") |h %></TD> -% if ( $cust_main->get("${pre}county") ) { + <TD BGCOLOR="#ffffff"><% $location->city |h %></TD> +% if ( $location->county ) { <TD ALIGN="right"><% mt('County') |h %></TD> - <TD BGCOLOR="#ffffff"><% $cust_main->get("${pre}county") |h %></TD> + <TD BGCOLOR="#ffffff"><% $location->county |h %></TD> % } <TD ALIGN="right"><% mt('State') |h %></TD> - <TD BGCOLOR="#ffffff"><% state_label( $cust_main->get("${pre}state"), $cust_main->get("${pre}country") ) |h %></TD> + <TD BGCOLOR="#ffffff"><% state_label( $location->state, $location->country ) |h %></TD> <TD ALIGN="right"><% mt('Zip') |h %></TD> - <TD BGCOLOR="#ffffff"><% $cust_main->get("${pre}zip") %></TD> + <TD BGCOLOR="#ffffff"><% $location->zip %></TD> </TR> <TR> <TD ALIGN="right"><% mt('Country') |h %></TD> - <TD BGCOLOR="#ffffff"><% code2country( $cust_main->get("${pre}country") ) %></TD> + <TD BGCOLOR="#ffffff"><% code2country( $location->country ) %></TD> </TR> -% if ( $cust_main->get($pre.'latitude') && $cust_main->get($pre.'longitude') ) { - <& /elements/tr-coords.html, $cust_main->get($pre.'latitude'), - $cust_main->get($pre.'longitude'), +% if ( $location->latitude && $location->longitude ) { + <& /elements/tr-coords.html, $location->latitude, + $location->longitude, $cust_main->name_short, $cust_main->agentnum, &> % } + +% if ( $this eq 'bill' ) { +% # billing contact phone numbers +% foreach my $phone (qw(daytime night mobile)) { +% next if !$cust_main->get($phone); +<TR> + <TD ALIGN="right"><% $phone_label{$phone} %></TD> + <TD COLSPAN=3 BGCOLOR="#ffffff"> + <& /elements/phonenumber.html, + $cust_main->get($phone), + callable => 1, + calling_list_exempt => $cust_main->calling_list_exempt, + &> + </TD> +</TR> -% foreach my $phone (grep $cust_main->get($pre.$_), qw( daytime night mobile )){ - - <TR> - <TD ALIGN="right"><% $phone_label{$phone} %></TD> - <TD COLSPAN=3 BGCOLOR="#ffffff"> - <& /elements/phonenumber.html, - $cust_main->get($pre.$phone), - 'callable'=>1, - 'calling_list_exempt'=>$cust_main->calling_list_exempt, - &> - </TD> - </TR> - -% } +% } #foreach $phone +% if ( $cust_main->get('fax') ) { -% if ( $cust_main->get("${pre}fax") ) { <TR> <TD ALIGN="right"><% mt('Fax') |h %></TD> <TD COLSPAN=3 BGCOLOR="#ffffff"> - <% $cust_main->get("${pre}fax") || ' ' %> + <% $cust_main->get('fax') || ' ' %> </TD> </TR> -% } -% if ( $which eq '' && $conf->exists('show_stateid') ) { - <TR> +% } +% +% if ( $conf->exists('show_stateid') ) { + +<TR> <TD ALIGN="right"><% $stateid_label %></TD> <TD BGCOLOR="#ffffff"><% $cust_main->masked('stateid') || ' ' %></TD> <TD ALIGN="right"><% $stateid_state_label %></TD> <TD BGCOLOR="#ffffff"><% $cust_main->stateid_state || ' ' %></TD> </TR> -% } +% } +% } #if $this eq 'bill' </TABLE> -% if ( $which ne 'ship_' ) { +% if ( @which ) { <BR> % } -% } +% } #while @which <%once> my %phone_label = ( @@ -147,7 +153,7 @@ my $stateid_state_label = FS::Msgcat::_gettext('stateid_state') =~ /^(stateid_st </%once> <%init> -my( $cust_main ) = @_; +my $cust_main = shift; my $conf = new FS::Conf; my @invoicing_list = $cust_main->invoicing_list; my $no = emt('no'); diff --git a/httemplate/view/cust_main/custom_content.html b/httemplate/view/cust_main/custom_content.html new file mode 100644 index 000000000..dd3d1f0d4 --- /dev/null +++ b/httemplate/view/cust_main/custom_content.html @@ -0,0 +1,31 @@ +% foreach my $item (@content) { +% +% if ( $item =~ /^\s*$/ ) { + <BR> +% next; +% } +% +% if ( $items{$item} ) { + <& "custom_content/$item.html", $cust_main &> +% } else { + Unknown item <% $item |h %><BR> +% } +% } +<%init> + +my($cust_main) = @_; + +my $conf = new FS::Conf; + +my @content = $conf->config('cust_main-custom_content'); + +my %items = map { $_=>1 } qw( + small_custview + birthdate + spouse_birthdate + svc_acct + svc_phone + svc_hardware +); + +</%init> diff --git a/httemplate/view/cust_main/custom_content/.birthdate.html.swp b/httemplate/view/cust_main/custom_content/.birthdate.html.swp Binary files differnew file mode 100644 index 000000000..9571d22cf --- /dev/null +++ b/httemplate/view/cust_main/custom_content/.birthdate.html.swp diff --git a/httemplate/view/cust_main/custom_content/.small_custview.html.swp b/httemplate/view/cust_main/custom_content/.small_custview.html.swp Binary files differnew file mode 100644 index 000000000..a39f52dde --- /dev/null +++ b/httemplate/view/cust_main/custom_content/.small_custview.html.swp diff --git a/httemplate/view/cust_main/custom_content/.spouse_birthdate.html.swp b/httemplate/view/cust_main/custom_content/.spouse_birthdate.html.swp Binary files differnew file mode 100644 index 000000000..0042012f7 --- /dev/null +++ b/httemplate/view/cust_main/custom_content/.spouse_birthdate.html.swp diff --git a/httemplate/view/cust_main/custom_content/.svc_Common.html.swp b/httemplate/view/cust_main/custom_content/.svc_Common.html.swp Binary files differnew file mode 100644 index 000000000..15591b96d --- /dev/null +++ b/httemplate/view/cust_main/custom_content/.svc_Common.html.swp diff --git a/httemplate/view/cust_main/custom_content/.svc_acct.html.swp b/httemplate/view/cust_main/custom_content/.svc_acct.html.swp Binary files differnew file mode 100644 index 000000000..e2db6d5d1 --- /dev/null +++ b/httemplate/view/cust_main/custom_content/.svc_acct.html.swp diff --git a/httemplate/view/cust_main/custom_content/.svc_hardware.html.swp b/httemplate/view/cust_main/custom_content/.svc_hardware.html.swp Binary files differnew file mode 100644 index 000000000..1106f9ed5 --- /dev/null +++ b/httemplate/view/cust_main/custom_content/.svc_hardware.html.swp diff --git a/httemplate/view/cust_main/custom_content/.svc_phone.html.swp b/httemplate/view/cust_main/custom_content/.svc_phone.html.swp Binary files differnew file mode 100644 index 000000000..79b8185e1 --- /dev/null +++ b/httemplate/view/cust_main/custom_content/.svc_phone.html.swp diff --git a/httemplate/view/cust_main/custom_content/birthdate.html b/httemplate/view/cust_main/custom_content/birthdate.html new file mode 100644 index 000000000..1f16963af --- /dev/null +++ b/httemplate/view/cust_main/custom_content/birthdate.html @@ -0,0 +1,15 @@ +<TABLE CLASS="fsinnerbox"> + <& /elements/tr-td-label.html, 'label' => mt('Date of Birth') &> + <TD BGCOLOR="#ffffff"><% $dt ? $dt->strftime($date_format) : '' %></TD> +</TR> +</TABLE> +<%init> +my($cust_main) = @_; +my $conf = new FS::Conf; +my $date_format = ($conf->config('date_format') || "%m/%d/%Y"); +my $dt = $cust_main->birthdate ne '' + ? DateTime->from_epoch( 'epoch' => $cust_main->birthdate, + 'time_zone' =>'floating', + ) + : ''; +</%init> diff --git a/httemplate/view/cust_main/custom_content/small_custview.html b/httemplate/view/cust_main/custom_content/small_custview.html new file mode 100644 index 000000000..7c724c78f --- /dev/null +++ b/httemplate/view/cust_main/custom_content/small_custview.html @@ -0,0 +1,4 @@ +<& /elements/small_custview.html, $cust_main &> +<%init> +my($cust_main) = @_; +</%init> diff --git a/httemplate/view/cust_main/custom_content/spouse_birthdate.html b/httemplate/view/cust_main/custom_content/spouse_birthdate.html new file mode 100644 index 000000000..c78fd26a5 --- /dev/null +++ b/httemplate/view/cust_main/custom_content/spouse_birthdate.html @@ -0,0 +1,15 @@ +<TABLE CLASS="fsinnerbox"> + <& /elements/tr-td-label.html, 'label' => mt('Spouse Date of Birth') &> + <TD BGCOLOR="#ffffff"><% $dt ? $dt->strftime($date_format) : '' %></TD> +</TR> +</TABLE> +<%init> +my($cust_main) = @_; +my $conf = new FS::Conf; +my $date_format = ($conf->config('date_format') || "%m/%d/%Y"); +my $dt = $cust_main->spouse_birthdate ne '' + ? DateTime->from_epoch( 'epoch' => $cust_main->spouse_birthdate, + 'time_zone' =>'floating', + ) + : ''; +</%init> diff --git a/httemplate/view/cust_main/custom_content/svc_Common.html b/httemplate/view/cust_main/custom_content/svc_Common.html new file mode 100644 index 000000000..bddb8bf16 --- /dev/null +++ b/httemplate/view/cust_main/custom_content/svc_Common.html @@ -0,0 +1,40 @@ +% foreach my $cust_svc (@cust_svc) { +% my $svc_x = $cust_svc->svc_x; +<TABLE CLASS="fsinnerbox"> + <TR> + <TH COLSPAN=2><% $cust_svc->part_svc->svc |h %></TH> + </TR> +% foreach my $field ( grep $svc_x->$_(), @{ $opt{fields} } ) { + <& /elements/tr-td-label.html, 'label' => $labels{$field} &> + <TD BGCOLOR="#ffffff"><% $svc_x->$field() |h %></TD> + </TR> +% } +</TABLE> +% } +<%init> +my($cust_main, %opt) = @_; + +my $table = $opt{table}; +my @cust_svc = (); +foreach my $cust_pkg ( + grep $_->num_cust_svc( 'svcdb'=>$table ), + $cust_main->all_pkgs +) { + my @wtf = $cust_pkg->cust_svc( 'svcdb'=>$table ); + push @cust_svc, $cust_pkg->cust_svc( 'svcdb'=>$table ); +} + +my %labels; +if ( UNIVERSAL::can("FS::$table", 'table_info') ) { +# $opt{'name'} = "FS::$table"->table_info->{'name'}; + + my $fields = "FS::$table"->table_info->{'fields'}; + %labels = map { $_ => ( ref($fields->{$_}) + ? $fields->{$_}{'label'} + : $fields->{$_} + ); + } + keys %$fields; +} + +</%init> diff --git a/httemplate/view/cust_main/custom_content/svc_acct.html b/httemplate/view/cust_main/custom_content/svc_acct.html new file mode 100644 index 000000000..49b97986d --- /dev/null +++ b/httemplate/view/cust_main/custom_content/svc_acct.html @@ -0,0 +1,7 @@ +<& svc_Common.html, $cust_main, + 'table' => 'svc_acct', + 'fields' => [qw( username _password )], +&> +<%init> +my($cust_main) = @_; +</%init> diff --git a/httemplate/view/cust_main/custom_content/svc_hardware.html b/httemplate/view/cust_main/custom_content/svc_hardware.html new file mode 100644 index 000000000..f5d53a251 --- /dev/null +++ b/httemplate/view/cust_main/custom_content/svc_hardware.html @@ -0,0 +1,7 @@ +<& svc_Common.html, $cust_main, + 'table' => 'svc_hardware', + 'fields' => [qw( ip_addr hw_addr serial )], +&> +<%init> +my($cust_main) = @_; +</%init> diff --git a/httemplate/view/cust_main/custom_content/svc_phone.html b/httemplate/view/cust_main/custom_content/svc_phone.html new file mode 100644 index 000000000..46ec476bc --- /dev/null +++ b/httemplate/view/cust_main/custom_content/svc_phone.html @@ -0,0 +1,7 @@ +<& svc_Common.html, $cust_main, + 'table' => 'svc_phone', + 'fields' => [qw( phonenum )], +&> +<%init> +my($cust_main) = @_; +</%init> diff --git a/httemplate/view/cust_main/locations.html b/httemplate/view/cust_main/locations.html index 98c933645..b29d0ce4d 100755 --- a/httemplate/view/cust_main/locations.html +++ b/httemplate/view/cust_main/locations.html @@ -5,12 +5,17 @@ span.loclabel { background-color: #cccccc; border: 1px solid black } +table.location { + width: 100%; + padding: 1px; + border-spacing: 0px; +} </STYLE> % foreach my $locationnum (@sorted) { % my $packages = $packages_in{$locationnum}; % my $loc = $locations{$locationnum}; % next if $loc->disabled and scalar(@$packages) == 0; -<& /elements/table-grid.html &> +<TABLE CLASS="grid location"> <TR><TH COLSPAN=3 ALIGN="left" VALIGN="bottom" STYLE="padding-bottom: 0px; padding-left: 0px; @@ -18,10 +23,7 @@ STYLE="padding-bottom: 0px; border-bottom-color: black; border-bottom-width: 1px;"> <SPAN CLASS="loclabel"> -% if (! $locationnum) { -<% mt('Default service location:') |h %> -% } -% elsif ( $loc->disabled ) { +% if ( $loc->disabled ) { <FONT COLOR="#808080"><I> % } <% $loc->location_label %></SPAN> @@ -49,8 +51,7 @@ my %locations = map { $_->locationnum => $_ } qsearch({ 'order_by' => 'ORDER BY country, state, city, address1, locationnum', }); my @sections = keys %locations; -$locations{''} = $cust_main; -my %packages_in = map { $_ => [] } ('', @sections); +my %packages_in = map { $_ => [] } (@sections); my %active = (); # groups with non-canceled packages foreach my $cust_pkg ( @$all_packages ) { @@ -58,10 +59,13 @@ foreach my $cust_pkg ( @$all_packages ) { push @{ $packages_in{$key} }, $cust_pkg; $active{$key} = 1 if !$cust_pkg->getfield('cancel'); } +# prevent disabling these +$active{$cust_main->ship_locationnum} = 1; +$active{$cust_main->bill_locationnum} = 1; my @sorted = ( - '', - grep ( { $active{$_} } @sections), + $cust_main->ship_locationnum, + grep ( { $active{$_} && $_ != $cust_main->ship_locationnum } @sections), grep ( { !$active{$_} } @sections), ); diff --git a/httemplate/view/cust_main/misc.html b/httemplate/view/cust_main/misc.html index 295328721..a0ab403e8 100644 --- a/httemplate/view/cust_main/misc.html +++ b/httemplate/view/cust_main/misc.html @@ -116,13 +116,27 @@ % } +% if ( $conf->exists('cust_main-enable_spouse_birthdate') ) { +% my $dt = $cust_main->spouse_birthdate ne '' +% ? DateTime->from_epoch( 'epoch' => $cust_main->spouse_birthdate, +% 'time_zone' =>'floating', +% ) +% : ''; + + <TR> + <TD ALIGN="right"><% mt('Spouse Date of Birth') |h %></TD> + <TD BGCOLOR="#ffffff"><% $dt ? $dt->strftime($date_format) : '' %></TD> + </TR> + +% } + % if ( $conf->exists('cust_main-require_censustract') ) { <TR> <TD ALIGN="right"> - <% mt('Census tract ([_1])', $cust_main->censusyear) |h %> + <% mt('Census tract ([_1])', $cust_main->ship_location->censusyear) |h %> </TD> - <TD BGCOLOR="#ffffff"><% $cust_main->censustract %></TD> + <TD BGCOLOR="#ffffff"><% $cust_main->ship_location->censustract %></TD> </TR> % } @@ -131,7 +145,7 @@ <TR> <TD ALIGN="right"><% mt('Tax district') |h %></TD> - <TD BGCOLOR="#ffffff"><% $cust_main->district %></TD> + <TD BGCOLOR="#ffffff"><% $cust_main->ship_location->district %></TD> </TR> % } diff --git a/httemplate/view/cust_main/packages/services.html b/httemplate/view/cust_main/packages/services.html index 5f458e635..c0a56d0f3 100644 --- a/httemplate/view/cust_main/packages/services.html +++ b/httemplate/view/cust_main/packages/services.html @@ -3,7 +3,7 @@ % ### <TD CLASS="inv" BGCOLOR="<% $bgcolor %>"> - <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%"> + <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=2 WIDTH="100%"> <SCRIPT TYPE="text/javascript"> function clearhint_search_cust_svc(obj, str) { if (obj.value == str) obj.value = ''; diff --git a/httemplate/view/cust_main/packages/status.html b/httemplate/view/cust_main/packages/status.html index 28df9da95..4aec90efb 100644 --- a/httemplate/view/cust_main/packages/status.html +++ b/httemplate/view/cust_main/packages/status.html @@ -32,7 +32,20 @@ % } % -% } else { +% if ( $part_pkg->freq ) { #? + + <TR> + <TD COLSPAN=<%$colspan%>> + <FONT SIZE=-1> +% if ( $curuser->access_right('Un-cancel customer package') ) { + ( <% pkg_uncancel_link($cust_pkg) %> ) +% } + <FONT> + </TD> + </TR> +% } +% +% } else { % % if ( $cust_pkg->get('susp') ) { #status: suspended % my $cpr = $cust_pkg->last_cust_pkg_reason('susp'); @@ -56,6 +69,8 @@ <% 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_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %> % if ( $part_pkg->option('suspend_bill', 1) ) { @@ -99,6 +114,8 @@ ) %> + <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %> + <TR> <TD COLSPAN=<%$colspan%>> <FONT SIZE=-1> @@ -118,6 +135,7 @@ <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %> <% pkg_status_row_if($cust_pkg, emt('Start billing'), 'start_date', %opt) %> + <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %> % } % @@ -133,6 +151,8 @@ <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %> + <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %> + % } else { % % my $num_cust_svc = $cust_pkg->num_cust_svc; @@ -166,6 +186,8 @@ <% pkg_status_row($cust_pkg, emt('Setup'), 'setup', %opt) %> + <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %> + % } % % } @@ -467,6 +489,16 @@ sub pkg_cancel_link { ) } +sub pkg_uncancel_link { + include( '/elements/popup_link-cust_pkg.html', + 'action' => $p. 'misc/cancel_pkg.html?method=uncancel', + 'label' => emt('Un-cancel'), + 'actionlabel' => emt('Un-cancel'), + #'color' => #? + 'cust_pkg' => shift, + ) +} + sub pkg_expire_link { include( '/elements/popup_link-cust_pkg.html', 'action' => $p. 'misc/cancel_pkg.html?method=expire', diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html index c7a7c8024..9e08c0c5d 100644 --- a/httemplate/view/cust_main/payment_history.html +++ b/httemplate/view/cust_main/payment_history.html @@ -491,7 +491,7 @@ foreach my $cust_credit ($cust_main->cust_credit) { foreach my $cust_refund ($cust_main->cust_refund) { push @history, { 'date' => $cust_refund->_date, - 'desc' => include('payment_history/refund.html', $cust_refund), + 'desc' => include('payment_history/refund.html', $cust_refund, %opt), 'refund' => $cust_refund->refund, }; diff --git a/httemplate/view/directions.html b/httemplate/view/directions.html index 599d049c2..f14a11a07 100644 --- a/httemplate/view/directions.html +++ b/httemplate/view/directions.html @@ -22,7 +22,6 @@ body { height: 100%; margin: 0px; padding: 0px } #map_canvas { height: 100%; - margin-right: 320px; } #directions_panel { diff --git a/httemplate/view/svc_acct.cgi b/httemplate/view/svc_acct.cgi index de97a0796..3ad21bb4d 100755 --- a/httemplate/view/svc_acct.cgi +++ b/httemplate/view/svc_acct.cgi @@ -47,6 +47,7 @@ <& svc_acct/basics.html, 'svc_acct' => $svc_acct, 'part_svc' => $part_svc, + 'cust_svc' => $cust_svc, %gopt, &> @@ -79,6 +80,7 @@ <& /elements/table-tickets.html, object => $cust_svc &> % } +<BR> <% joblisting({'svcnum'=>$svcnum}, 1) %> <& /elements/footer.html &> diff --git a/httemplate/view/svc_acct/basics.html b/httemplate/view/svc_acct/basics.html index 8f180b648..bcd84696e 100644 --- a/httemplate/view/svc_acct/basics.html +++ b/httemplate/view/svc_acct/basics.html @@ -1,6 +1,9 @@ <% &ntable("#cccccc") %><TR><TD><% &ntable("#cccccc",2) %> <& /view/elements/tr.html, label=>mt('Service'), value=>$part_svc->svc &> +% if ( $opt{cust_svc}->agent_svcid ) { + <& /view/elements/tr.html, label=>mt('Legacy ID'), value=>$opt{cust_svc}->agent_svcid &> +% } <& /view/elements/tr.html, label=>mt('Username'), value=>$svc_acct->username &> <& /view/elements/tr.html, label=>mt('Domain'), value=>$domain &> diff --git a/rt/lib/RT/Action/Accumulate.pm b/rt/lib/RT/Action/Accumulate.pm index c4ca667ea..14675b894 100644 --- a/rt/lib/RT/Action/Accumulate.pm +++ b/rt/lib/RT/Action/Accumulate.pm @@ -23,7 +23,8 @@ the same name, and should be single-valued fields. sub Prepare { my $self = shift; my $cfname = $self->Argument or return 0; - $self->{'inc_by'} = $self->TransactionObj->FirstCustomFieldValue($cfname); + $self->{'inc_by'} = $self->TransactionObj->FirstCustomFieldValue($cfname) + || ''; return ( $self->{'inc_by'} =~ /^(\d+)$/ ); } diff --git a/rt/lib/RT/Action/EscalateQueue.pm b/rt/lib/RT/Action/EscalateQueue.pm index adafbdfb7..2775a8384 100755 --- a/rt/lib/RT/Action/EscalateQueue.pm +++ b/rt/lib/RT/Action/EscalateQueue.pm @@ -116,15 +116,18 @@ sub Commit { if ( my $due_in = $new_queue->DefaultDueIn ) { $Due->SetToNow; $Due->AddDays( $due_in ); - } - ( $val, $msg ) = $ticket->_Set( - Field => 'Due', - Value => $Due->ISO, - RecordTransaction => 0, - ); - if (! $val) { - $RT::Logger->error( "Couldn't set new due date: $msg" ); - return (0, $msg); + + if ( $Due->ISO ne $ticket->Due ) { + ( $val, $msg ) = $ticket->_Set( + Field => 'Due', + Value => $Due->ISO, + RecordTransaction => 0, + ); + if (! $val) { + $RT::Logger->error( "Couldn't set new due date: $msg" ); + return (0, $msg); + } + } } return 1; } |